From a4c22d912720be8612091e98a2dc7b20254beef6 Mon Sep 17 00:00:00 2001 From: hanchen1103 <3457580866@qq.com> Date: Fri, 30 Aug 2024 10:03:54 +0800 Subject: [PATCH] Add bar_series and base_indicator --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 8 + .idea/.gitignore | 8 + .idea/GOTA.iml | 9 + .idea/copyright/MIT.xml | 6 + .idea/copyright/profiles_settings.xml | 7 + .idea/misc.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + .idea/watcherTasks.xml | 53 ++++ README.md | 310 +++++++++++++++++++++- go.mod | 23 ++ go.sum | 58 ++++ pkg/.DS_Store | Bin 0 -> 6148 bytes pkg/base/.DS_Store | Bin 0 -> 6148 bytes pkg/base/bar/bar.go | 60 +++++ pkg/base/bar/base_bar.go | 163 ++++++++++++ pkg/base/bar_series/bar_series.go | 156 +++++++++++ pkg/base/bar_series/circular_buffer.go | 213 +++++++++++++++ pkg/base/bar_series/slice.go | 263 ++++++++++++++++++ pkg/base/common/bar_series_meta_info.go | 35 +++ pkg/base/common/error.go | 38 +++ pkg/base/common/tech_indicator_name.go | 36 +++ pkg/base/contact/broadcast.go | 147 ++++++++++ pkg/base/factory/indicator_factory.go | 50 ++++ pkg/base/indicator/.DS_Store | Bin 0 -> 6148 bytes pkg/base/indicator/base_indicator.go | 58 ++++ pkg/base/indicator/cache.go | 172 ++++++++++++ pkg/base/indicator/decorator.go | 106 ++++++++ pkg/base/indicator/scheduler/executor.go | 79 ++++++ pkg/base/indicator/scheduler/processor.go | 83 ++++++ pkg/base/indicator/source.go | 103 +++++++ pkg/base/indicator/tech/ema.go | 97 +++++++ pkg/base/indicator/tech/sma.go | 63 +++++ pkg/base/logger/logger.go | 47 ++++ pkg/constant/kline/kline_interval.go | 75 ++++++ pkg/helper/math.go | 40 +++ pkg/helper/time.go | 32 +++ pkg/platform/binance/kline.go | 107 ++++++++ 39 files changed, 2724 insertions(+), 1 deletion(-) create mode 100644 .DS_Store create mode 100644 .idea/.gitignore create mode 100644 .idea/GOTA.iml create mode 100644 .idea/copyright/MIT.xml create mode 100644 .idea/copyright/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/watcherTasks.xml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/.DS_Store create mode 100644 pkg/base/.DS_Store create mode 100644 pkg/base/bar/bar.go create mode 100644 pkg/base/bar/base_bar.go create mode 100644 pkg/base/bar_series/bar_series.go create mode 100644 pkg/base/bar_series/circular_buffer.go create mode 100644 pkg/base/bar_series/slice.go create mode 100644 pkg/base/common/bar_series_meta_info.go create mode 100644 pkg/base/common/error.go create mode 100644 pkg/base/common/tech_indicator_name.go create mode 100644 pkg/base/contact/broadcast.go create mode 100644 pkg/base/factory/indicator_factory.go create mode 100644 pkg/base/indicator/.DS_Store create mode 100644 pkg/base/indicator/base_indicator.go create mode 100644 pkg/base/indicator/cache.go create mode 100644 pkg/base/indicator/decorator.go create mode 100644 pkg/base/indicator/scheduler/executor.go create mode 100644 pkg/base/indicator/scheduler/processor.go create mode 100644 pkg/base/indicator/source.go create mode 100644 pkg/base/indicator/tech/ema.go create mode 100644 pkg/base/indicator/tech/sma.go create mode 100644 pkg/base/logger/logger.go create mode 100644 pkg/constant/kline/kline_interval.go create mode 100644 pkg/helper/math.go create mode 100644 pkg/helper/time.go create mode 100644 pkg/platform/binance/kline.go diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..dc2c851d5cea021454d58863ad54bc0833288137 GIT binary patch literal 6148 zcmeHKyG{c!5S%4fM50Ma>0jUvtSEc|KS1Cl3L*sw=&$0t@@dRIibSWA($K)Hv>toC zW6M+A-U6`g*LV+X0Icbb`1WCLzVAM>n~FG^pD}*C9G;KI0~gnta|evr;}iRH{tEAS z!|QqI+B20W1*Cu!kOERb3M?y7WqN(PoT42mAO-%Z0{(qybjMyeCB~ + + + + + + + + \ No newline at end of file diff --git a/.idea/copyright/MIT.xml b/.idea/copyright/MIT.xml new file mode 100644 index 0000000..f01adcd --- /dev/null +++ b/.idea/copyright/MIT.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..c1e2ebc --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..7bc123c --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..3d6e22d --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml new file mode 100644 index 0000000..ce7ffb5 --- /dev/null +++ b/.idea/watcherTasks.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 4e2c1a1..b9d5f79 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,310 @@ # GOTA -A Comprehensive Go Library for Technical and Financial Analysis + +A Comprehensive Go Library for Technical and Financial Analysis. + +In the context of financial technical analysis, Golang currently lacks the rich ecosystem found in programming languages like Java, C++, and Python. Therefore, we aim to develop a robust, well-documented, feature-rich, high-performance, and highly customizable and extensible technical analysis library. This will serve as a solid foundation for our team to develop more advanced technical analysis libraries, visualization tools, and a multi-language strategy platform in the future. +## Getting Started + +``` +go get -u github.com/dearnostalgia/gota +``` + +## Usage + +### Bar + +One possible implementation + +``` +b := bar.NewBaseBar( + time.UnixMilli(k.StartTime), + openPrice, + closePrice, + highPrice, + lowPrice, + bar.WithEndTime(time.UnixMilli(k.EndTime)), + bar.WithAmount(a), + bar.WithVolume(v), + true, + ) +b.GetBeginTime() +b.GetHighPrice() +b.GetVolume() +... +``` + +#### interface + +Implement the basic interface: `Bar`, which allows you to freely customize your own candlestick (K-line). + +``` +// Bar represents a financial market data bar, typically used in time-series analysis. +type Bar interface { + // GetBeginTime returns the starting time of the bar. + GetBeginTime() time.Time + + // GetEndTime returns the ending time of the bar. + GetEndTime() time.Time + + // GetOpenPrice returns the opening price of the bar. + GetOpenPrice() decimal.Decimal + + // GetHighPrice returns the highest price during the bar's time period. + GetHighPrice() decimal.Decimal + + // GetLowPrice returns the lowest price during the bar's time period. + GetLowPrice() decimal.Decimal + + // GetClosePrice returns the closing price of the bar. + GetClosePrice() decimal.Decimal + + // GetVolume returns the trading volume during the bar's time period. + GetVolume() decimal.Decimal + + // IsEnd indicates whether the bar represents the end of a time period, + // such as the last bar in a trading day or the final bar in a series. + IsEnd() bool +} +``` + +### BarSeries + +`CircularBarSeries` is our primary implementation. + +If WithMaxSize is used, the oldest data will be automatically deleted when the capacity limit is reached. + +``` +var barSeries *bar_series.CircularBarSeries +// var barSeries bar_series.BarSeries +barSeries = bar_series.NewCircularBarSeries( + bar_series.WithSymbol("BTCUSDT.P"), + bar_series.WithInterval(kline.Interval3m.Duration), + bar_series.WithMaxSize(512), +) +// All of the above parameters are optional. If WithMaxSize is not configured, please ensure sufficient memory is available. + +barSeries.GetBar(9) +barSeries.GetFirstBar() +barSeries.GetLastBar() +barSeries.GetBarsCopy() +barSeries.Size() +barSeries.IsEmpty() +barSeries.GetSymbol() +barSeries.GetMaxSize() +l := barSeries.Subscribe() +for event := range l.Ch() { + // handle event +} + +``` + +#### interface + +Implement the basic interface: `BarSeries`, which allows you to freely customize your own `K-line` series. + +``` +type BarSeries interface { + // Size returns the current number of bars in the series. + // It indicates how many data points (bars) are currently stored in the series. + Size() int + + // IsEmpty checks if the bar series is empty. + // It returns true if the series contains no bars, and false otherwise. + IsEmpty() bool + + // GetBar returns the Bar at the specified index. + // The index is zero-based, meaning that the first bar in the series is at index 0. + // If the index is out of range, it returns nil. + GetBar(idx int) *bar.Bar + + // GetFirstBar returns the first Bar in the series. + // If the series is empty, it returns nil. + // The first bar is the one that was added to the series first, based on time. + GetFirstBar() *bar.Bar + + // GetLastBar returns the last Bar in the series. + // If the series is empty, it returns nil. + // The last bar is the most recently added bar in the series. + GetLastBar() *bar.Bar + + // AddBar adds a bar to the series. + // If realTimeUpdateBar is true, the new data will be compared with the latest data in the bar series. + // If the beginTime is the same, the existing data will be updated; if it is different, the new data will be added. + // If realTimeUpdateBar is false, new data will be continuously added. + AddBar(bar bar.Bar, realTimeUpdateBar bool) error + + // GetBarsCopy returns a deep copy of the bar.Bar slice. + // This method ensures that the returned slice contains independent copies of + // the elements from the original barSlice, so any modifications made to the + // returned slice will not affect the original BarSeries structure or its data. + GetBarsCopy() []bar.Bar + + // Subscribe registers a subscription to the barSeries data and returns a contact.Listener[BarSeriesEvent]. + // This method allows the subscriber to listen for broadcasted data events within the CircularBarSeries. + // Usage: + // l := barSeries.Subscribe() + // for event := range l.Ch() { + // // handle event + // } + // The CircularBarSeries broadcasts data to all subscribed goroutines. + Subscribe() *contact.Listener[BarSeriesEvent] +} +``` + +### DataSource + +DataSource serves as the calculation parameter for the Indicator. We have implemented the basic data source information, such as `HighPrice`, `ClosePrice`, etc. + +``` +closeSource := core.NewAttributeSource[decimal.Decimal](barSeries, &core.ClosePriceStrategy{}) +highSource := core.NewAttributeSource[decimal.Decimal](barSeries, &core.HighPriceStrategy{}) +volumeSource := core.NewAttributeSource[decimal.Decimal](barSeries, &core.VolumeStrategy{}) +``` + +If you want to customize your own data source, refer to the strategy pattern below, and ensure you design your custom Bar. + +``` +type ClosePriceStrategy struct{} + +func (s *ClosePriceStrategy) GetAttribute(bar *bar.Bar) decimal.Decimal { + return (*bar).GetClosePrice() +} + +type CustomeStrategy struct{} + +func (s *CustomeStrategy) GetAttribute(bar *bar.Bar) decimal.Decimal { + return (*bar).GetCustomeValue() +} + +``` + +### Indicator + +Using the factory pattern, `indicatorExecutor.Shoot` not only performs the initial calculation but also subscribes to `BarSeries` updates to maintain synchronized calculations for the EMAIndicator. + +It is strongly recommended to use `IndicatorFactory.CreateIndicator` to generate any indicator. We have utilized the decorator pattern to encapsulate operations such as caching and concurrency protection. + +If the barSeries is pre-populated (i.e., fixed before the initialization of the Executor), calculations can be performed directly by iteration, without the need to reserve additional time. + +Otherwise, after invoking `indicatorExecutor.Shoot`, it is advisable to wait for approximately 50 milliseconds (depending on individual machine performance) to allow sufficient time for initialization and subscription. +During the `addBar` process, it is also recommended to reserve some time for broadcasting events and performing calculations. + + +``` +IndicatorFactory := new(factory.IndicatorFactory[decimal.Decimal]) +ema, err := tech.NewEMAIndicator(20) +if err != nil { + return err +} + +closeSource := core.NewAttributeSource[decimal.Decimal](barSeries, &core.ClosePriceStrategy{}) +EMAIndicator := IndicatorFactory.CreateIndicator(ema, closeSource) +indicatorExecutor := &schedule.Executor[decimal.Decimal]{} + +err := indicatorExecutor.Shoot(EMAIndicator, 0) + if err != nil { + return err +} +time.Sleep(50 * time.Microsecond) + +for _, bar := range bars { + err := barSeries.AddBar(bar, true) + if err != nil { + return err + } + time.Sleep(30 * time.Microsecond) +} +res := EMAIndicator.GetResults() +``` + +If you want to retrieve the value of a specific calculation + +``` +v, err := EMAIndicator.Calculate(idx) +``` + +#### interface + +To implement your own Indicator, you need to implement the Indicator interface. + +``` +type Indicator[T any] interface { + Calculate(idx int) (T, error) + SetSource(s *AttributeSource[T]) + GetSource() *AttributeSource[T] + GetResults() []*Result[T] +} +``` + +If the indicator relies on previous values like `EMA`, it is recommended to reuse the `CacheIndicator` functionality. You can refer to the EMA implementation. + +``` +var _ core.CacheIndicator[decimal.Decimal] = (*EMAIndicator)(nil) + +type EMAIndicator struct { + *core.EnhancedCacheIndicator[decimal.Decimal] + period int + multiplier decimal.Decimal +} + +func NewEMAIndicator(period int) (core.CacheIndicator[decimal.Decimal], error) { + multiplier, err := decimal.NewFromFloat64(2.0 / float64(period+1)) + if err != nil { + common.IndicatorCalculateErrLog(0, common.EMA, errors.New("period:"+err.Error())) + return nil, err + } + emaIndicator := &EMAIndicator{ + EnhancedCacheIndicator: core.NewEnhancedCacheIndicator[decimal.Decimal](), + period: period, + multiplier: multiplier, + } + return emaIndicator, nil +} + +func (e *EMAIndicator) Calculate(idx int) (decimal.Decimal, error) { + var ( + sv = e.Source.GetValue(idx) + err error + ) + + if idx == 0 { + return sv, nil + } + + defer func() { + if err != nil { + common.IndicatorCalculateErrLog(idx, common.EMA, err) + } + }() + + prevEMA, err := e.Cache.GetValue(idx-1, e.Calculate) + + if err != nil { + return decimal.Zero, err + } + + res, err := e.calculateEMA(prevEMA, sv) + if err != nil { + return decimal.Zero, err + } + return res, nil +} + +func (e *EMAIndicator) calculateEMA(prevEMA decimal.Decimal, sourceVal decimal.Decimal) (decimal.Decimal, error) { + s, err := sourceVal.Sub(prevEMA) + if err != nil { + return decimal.Zero, err + } + m, err := s.Mul(e.multiplier) + if err != nil { + return decimal.Zero, err + } + + res, err := m.Add(prevEMA) + if err != nil { + return decimal.Zero, err + } + return res, nil +} +``` + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cb6e17c --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module github.com/dearnostalgia/gota + +go 1.22.3 + +require ( + github.com/bytedance/sonic v1.12.0 + github.com/google/uuid v1.6.0 + github.com/govalues/decimal v0.1.29 + github.com/huandu/go-clone/generic v1.7.2 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/bytedance/sonic/loader v0.2.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/huandu/go-clone v1.7.2 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/sys v0.22.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..47d06ad --- /dev/null +++ b/go.sum @@ -0,0 +1,58 @@ +github.com/bytedance/sonic v1.12.0 h1:YGPgxF9xzaCNvd/ZKdQ28yRovhfMFZQjuk6fKBzZ3ls= +github.com/bytedance/sonic v1.12.0/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= +github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/govalues/decimal v0.1.29 h1:GKC5g9y9oWxKIy51czdHTShOABwHm/shVuOVPwG415M= +github.com/govalues/decimal v0.1.29/go.mod h1:LUlHHucpCmA4rJfNrDvMgrWibDpYnDNWqJuNU1/gxW8= +github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= +github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= +github.com/huandu/go-clone v1.6.0 h1:HMo5uvg4wgfiy5FoGOqlFLQED/VGRm2D9Pi8g1FXPGc= +github.com/huandu/go-clone v1.6.0/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE= +github.com/huandu/go-clone v1.7.2 h1:3+Aq0Ed8XK+zKkLjE2dfHg0XrpIfcohBE1K+c8Usxoo= +github.com/huandu/go-clone v1.7.2/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE= +github.com/huandu/go-clone/generic v1.7.2 h1:47pQphxs1Xc9cVADjOHN+Bm5D0hNagwH9UXErbxgVKA= +github.com/huandu/go-clone/generic v1.7.2/go.mod h1:xgd9ZebcMsBWWcBx5mVMCoqMX24gLWr5lQicr+nVXNs= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/pkg/.DS_Store b/pkg/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..fef7f52ee48d1a7b8da1bb944e3af4f5a3ea0fe6 GIT binary patch literal 6148 zcmeHKJ5EC}5S)c46p1D!rLVvZtSFp-3qVLDiYEmT(Z7mwaWrN>MWPp^G&C?Pt;b&P z*zy!_-vY4J$Mzmr0$9);@#(|deBXU$R~2zIKVy4%88`d=$i=1R+yNUr~;gpzkSlrBW>Sn7B#o~6xTcpE!qDCno z1x^*X&gIhU{{#Jp{{NJul@yQye@X!x){pBIpH#JV_BgM#js8UUoG-c?=Rx5R<(L@d hmsFfkLYnY-=05NSt%d|q<|EV0#aa30c)+a0s~?fVg1Vjn6(y5UVAKEpZHFhE{?~3~I>{qXeD#nsqI43=BGo-^_FJ=8_Xi z@!OfNUXIcN6{Ua_xK!XemMiQ3kMv*W|4WiqQa}p)D+S78^R!v>m9n=^Ue0=LrN7XB qjJ00Q;H{YGt(XgI#rJ1*)t= 0 && idx < len(c.circularBuffer.bars) { + return &c.circularBuffer.bars[idx] + } + return nil + } + + if idx >= (c.circularBuffer.cnt) || idx < 0 { + return nil + } + return &c.circularBuffer.bars[(c.circularBuffer.recoverIdx+idx)%(c.circularBuffer.cnt)] +} + +func (c *CircularBarSeries) GetFirstBar() *bar.Bar { + return c.GetBar(0) +} + +func (c *CircularBarSeries) GetLastBar() *bar.Bar { + return c.GetBar(c.circularBuffer.cnt - 1) +} + +func (c *CircularBarSeries) AddBar(bar bar.Bar, realTimeUpdateBar bool) error { + if bar == nil { + return errors.New("bar cannot be nil") + } + + c.mu.Lock() + defer c.mu.Unlock() + + if c.maxSize != nil && *c.maxSize <= 0 { + return nil + } + + size := c.circularBuffer.cnt + if size == 0 { + c.addBar(bar) + return nil + } + + var realLastIdx int + if c.maxSize != nil { + realLastIdx = (c.circularBuffer.recoverIdx + size) % size + } else { + realLastIdx = size - 1 + } + + lastBar := c.circularBuffer.bars[realLastIdx] + + if realTimeUpdateBar && (lastBar.GetBeginTime().Equal(bar.GetBeginTime()) || !bar.IsEnd()) { + c.updateBar(bar, realLastIdx) + return nil + } + c.addBar(bar) + return nil +} + +func (c *CircularBarSeries) updateBar(bar bar.Bar, idx int) { + c.circularBuffer.bars[idx] = bar + c.publish(&BarSeriesEvent{ + Idx: c.circularBuffer.cnt - 1, + Bar: &bar, + BarSeriesEventType: UpdateLatestBar, + }) +} + +func (c *CircularBarSeries) addBar(bar bar.Bar) { + defer func() { + c.publish(&BarSeriesEvent{ + Idx: c.circularBuffer.cnt - 1, + Bar: &bar, + BarSeriesEventType: AddNewBar, + }) + }() + + if c.maxSize == nil { + c.circularBuffer.bars = append(c.circularBuffer.bars, bar) + c.circularBuffer.cnt++ + return + } + c.circularBuffer.recoverIdx = c.circularBuffer.recoverIdx % (*c.maxSize) + c.circularBuffer.bars[c.circularBuffer.recoverIdx] = bar + c.circularBuffer.recoverIdx++ + if !c.circularBuffer.inCircular { + c.circularBuffer.cnt++ + } + if c.circularBuffer.recoverIdx == *c.maxSize { + c.circularBuffer.inCircular = true + } +} + +func (c *CircularBarSeries) Subscribe() *contact.Listener[BarSeriesEvent] { + c.mu.Lock() + defer c.mu.Unlock() + + l := c.relay.Listener(common.DefaultDescribeCapacity) + return l +} + +func (c *CircularBarSeries) publish(e *BarSeriesEvent) { + if (*e.Bar).IsEnd() { + c.relay.Notify(*e) + return + } + c.relay.Broadcast(*e) +} + +func (c *CircularBarSeries) GetBarsCopy() []bar.Bar { + c.mu.RLock() + defer c.mu.RUnlock() + + return clone.Wrap(c.circularBuffer.bars) +} diff --git a/pkg/base/bar_series/slice.go b/pkg/base/bar_series/slice.go new file mode 100644 index 0000000..6178b09 --- /dev/null +++ b/pkg/base/bar_series/slice.go @@ -0,0 +1,263 @@ +// /* +// * MIT License +// * +// * Copyright (c) 2024 DearNostalgia +// * +// * Permission is hereby granted, free of charge, to any person obtaining a copy +// * of this software and associated documentation files (the "Software"), to deal +// * in the Software without restriction, including without limitation the rights +// * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// * copies of the Software, and to permit persons to whom the Software is +// * furnished to do so, subject to the following conditions: +// * +// * The above copyright notice and this permission notice shall be included in all +// * copies or substantial portions of the Software. +// * +// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// * SOFTWARE. +// * +// */ +package bar_series + +// +//import ( +// "math" +// "sync" +// "time" +// +// clone "github.com/huandu/go-clone/generic" +// +// "github.com/google/uuid" +// +// "github.com/dearnostalgia/gota/pkg/base/bar" +//) +// +//const DefaultInitialSliceCapacity = 1024 +// +//var DefaultMaxSize = math.MaxInt32 +// +//var _ BarSeries = (*SliceBarSeries)(nil) +//var _ BarSeriesCreator = (*SliceBarSeriesCreator)(nil) +// +//type SliceBarSeries struct { +// *BarSeriesMetaInfo +// mu sync.RWMutex +// eventChan chan BarSeriesEvent +// barSlice *barSlice +//} +// +//type barSlice struct { +// bars []bar.Bar +//} +// +//type SliceBarSeriesCreator struct{} +// +//func (*SliceBarSeriesCreator) Create(opts ...BaseSeriesOption) BarSeries { +// sliceBarSeries := &SliceBarSeries{ +// BarSeriesMetaInfo: &BarSeriesMetaInfo{}, +// barSlice: &barSlice{}, +// mu: sync.RWMutex{}, +// eventChan: make(chan BarSeriesEvent, 1), +// } +// +// for _, opt := range opts { +// opt(sliceBarSeries.BarSeriesMetaInfo) +// } +// +// if sliceBarSeries.BarSeriesMetaInfo.symbol == nil { +// symbol := uuid.New().String() +// sliceBarSeries.BarSeriesMetaInfo.symbol = &symbol +// } +// +// maxSize := sliceBarSeries.BarSeriesMetaInfo.maxSize +// +// if maxSize == nil { +// sliceBarSeries.barSlice.bars = make([]bar.Bar, 0, DefaultInitialSliceCapacity) +// sliceBarSeries.maxSize = &DefaultMaxSize +// return sliceBarSeries +// } +// +// if *maxSize <= 0 { +// sliceBarSeries.barSlice.bars = make([]bar.Bar, 0) +// return sliceBarSeries +// } +// +// sliceBarSeries.barSlice.bars = make([]bar.Bar, 0, *maxSize) +// return sliceBarSeries +//} +// +//// NewSliceBarSeries creates and initializes a new SliceBarSeries instance. +//// It accepts a variable number of options (BaseSeriesOption) to configure the instance. +//// If no options are provided, the symbol, maxSize, and other fields may remain uninitialized +//// until they are set by the logic within the function. +//// +//// This function handles the following scenarios: +//// - If the symbol is not provided, it defaults to a string representation of a new UUID. +//// - If maxSize is not provided, the function sets a default initial slice capacity for the barSlice. +//// - If maxSize is provided and is less than or equal to 0, an empty slice is created. +//// - Otherwise, the slice is initialized with a capacity equal to maxSize. +//// +//// Example usage: +//// +//// sliceBarSeries := NewSliceBarSeries( +//// WithSymbol("BTCUSDT.P"), // Sets the symbol to "BTCUSDT.P" +//// WithInterval(kline.Interval3m.Duration), // Sets the interval to 3 minutes +//// WithMaxSize(3060), // Sets the maximum size to 3060 bars +//// ) +////func NewSliceBarSeries(opts ...BaseSeriesOption) *SliceBarSeries { +//// sliceBarSeries := &SliceBarSeries{ +//// BarSeriesMetaInfo: &BarSeriesMetaInfo{}, +//// barSlice: &barSlice{}, +//// mu: sync.RWMutex{}, +//// eventChan: make(chan BarSeriesEvent, 1), +//// } +//// +//// for _, opt := range opts { +//// opt(sliceBarSeries.BarSeriesMetaInfo) +//// } +//// +//// if sliceBarSeries.BarSeriesMetaInfo.symbol == nil { +//// symbol := uuid.New().String() +//// sliceBarSeries.BarSeriesMetaInfo.symbol = &symbol +//// } +//// +//// maxSize := sliceBarSeries.BarSeriesMetaInfo.maxSize +//// +//// if maxSize == nil { +//// sliceBarSeries.barSlice.bars = make([]bar.Bar, 0, DefaultInitialSliceCapacity) +//// sliceBarSeries.maxSize = &DefaultMaxSize +//// return sliceBarSeries +//// } +//// +//// if *maxSize <= 0 { +//// sliceBarSeries.barSlice.bars = make([]bar.Bar, 0) +//// return sliceBarSeries +//// } +//// +//// sliceBarSeries.barSlice.bars = make([]bar.Bar, 0, *maxSize) +//// return sliceBarSeries +////} +// +//func (s *SliceBarSeries) Publish(e *BarSeriesEvent) { +// s.eventChan <- *e +//} +// +//func (s *SliceBarSeries) ReceiveBarEvent() BarSeriesEvent { +// return <-s.eventChan +//} +// +//func (s *SliceBarSeries) GetBarSeriesCopy() []bar.Bar { +// barsCopy := clone.Clone(s.barSlice.bars) +// return barsCopy +//} +// +//func (s *SliceBarSeries) Size() int { +// s.mu.RLock() +// defer s.mu.RUnlock() +// +// return len(s.barSlice.bars) +//} +// +//func (s *SliceBarSeries) IsEmpty() bool { +// s.mu.RLock() +// defer s.mu.RUnlock() +// +// return len(s.barSlice.bars) == 0 +//} +// +//func (s *SliceBarSeries) GetBar(idx int) *bar.Bar { +// s.mu.RLock() +// defer s.mu.RUnlock() +// +// if idx >= len(s.barSlice.bars) || idx < 0 { +// return nil +// } +// +// return &s.barSlice.bars[idx] +//} +// +//func (s *SliceBarSeries) GetFirstBar() *bar.Bar { +// s.mu.RLock() +// defer s.mu.RUnlock() +// +// return &s.barSlice.bars[0] +//} +// +//func (s *SliceBarSeries) GetLastBar() *bar.Bar { +// s.mu.RLock() +// defer s.mu.RUnlock() +// +// return &s.barSlice.bars[len(s.barSlice.bars)-1] +//} +// +//func (s *SliceBarSeries) AddBar(bar bar.Bar, realTimeUpdateBar bool) error { +// s.mu.Lock() +// defer s.mu.Unlock() +// +// maxSize := *s.maxSize +// if maxSize <= 0 { +// return nil +// } +// +// size := len(s.barSlice.bars) +// if size == 0 { +// s.barSlice.bars = append(s.barSlice.bars, bar) +// //s.Publish(&BarSeriesEvent{ +// // BarSeriesEventType: AddNewBar, +// // idx: size, +// // bar: &bar, +// //}) +// return nil +// } +// latestBar := s.barSlice.bars[size-1] +// if realTimeUpdateBar && (latestBar.GetBeginTime().Equal(bar.GetBeginTime()) || !bar.IsEnd()) { +// s.barSlice.bars[size-1] = bar +// //s.Publish(&BarSeriesEvent{ +// // BarSeriesEventType: UpdateLatestBar, +// // idx: size - 1, +// // bar: &bar, +// //}) +// return nil +// } +// if size >= maxSize { +// remove := size - maxSize + 1 +// s.barSlice.bars = s.barSlice.bars[remove:] +// //s.Publish(&BarSeriesEvent{ +// // BarSeriesEventType: RemoveInvalidBar, +// // idx: remove, +// //}) +// } +// s.barSlice.bars = append(s.barSlice.bars, bar) +// //s.Publish(&BarSeriesEvent{ +// // BarSeriesEventType: AddNewBar, +// // idx: size, +// // bar: &bar, +// //}) +// return nil +//} +// +//func (b barSlice) binarySearch(targetTime time.Time, size int) int { +// l, r := 0, size-1 +// for l <= r { +// mid := l + (r-l)/2 +// +// if (b.bars[mid]).GetBeginTime().Equal(targetTime) { +// return mid +// } else if (b.bars[mid]).GetBeginTime().Before(targetTime) { +// l = mid + 1 +// } else { +// r = mid - 1 +// } +// } +// return l +//} +// +//func (s *SliceBarSeries) Subscribe() *BarSeriesEvent { +// //TODO implement me +// panic("implement me") +//} diff --git a/pkg/base/common/bar_series_meta_info.go b/pkg/base/common/bar_series_meta_info.go new file mode 100644 index 0000000..ded17d0 --- /dev/null +++ b/pkg/base/common/bar_series_meta_info.go @@ -0,0 +1,35 @@ +/* + * MIT License + * + * Copyright (c) 2024 DearNostalgia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package common + +import "math" + +const ( + DefaultInitialBarSeriesCapacity = 1024 + DefaultDescribeCapacity = 1024 +) + +var DefaultBarSeriesMaxSize = math.MaxInt32 diff --git a/pkg/base/common/error.go b/pkg/base/common/error.go new file mode 100644 index 0000000..f0528bc --- /dev/null +++ b/pkg/base/common/error.go @@ -0,0 +1,38 @@ +/* + * MIT License + * + * Copyright (c) 2024 DearNostalgia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package common + +import ( + "github.com/dearnostalgia/gota/pkg/base/logger" + "go.uber.org/zap" +) + +func IndicatorCalculateErrLog(idx int, techName TechIndicatorName, err error) { + logger.Logger.Error("indicator calculator failed", + zap.Int("bar_series_idx", idx), + zap.Any("indicator", techName), + zap.Error(err)) +} diff --git a/pkg/base/common/tech_indicator_name.go b/pkg/base/common/tech_indicator_name.go new file mode 100644 index 0000000..2dbffa3 --- /dev/null +++ b/pkg/base/common/tech_indicator_name.go @@ -0,0 +1,36 @@ +/* + * MIT License + * + * Copyright (c) 2024 DearNostalgia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package common + +type TechIndicatorName string + +const ( + EMA TechIndicatorName = "EMA" + SMA TechIndicatorName = "SMA" + MACD TechIndicatorName = "MACD" + RSI TechIndicatorName = "RSI" + KDJ TechIndicatorName = "KDJ" +) diff --git a/pkg/base/contact/broadcast.go b/pkg/base/contact/broadcast.go new file mode 100644 index 0000000..0e7d64c --- /dev/null +++ b/pkg/base/contact/broadcast.go @@ -0,0 +1,147 @@ +/* + * MIT License + * + * Copyright (c) 2024 DearNostalgia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package contact + +import ( + "context" + "sync" +) + +// Relay is the struct in charge of handling the listeners and dispatching the notifications. +type Relay[T any] struct { + mu sync.RWMutex + n uint32 + clients map[uint32]*Listener[T] +} + +// Listener is a Relay listener. +type Listener[T any] struct { + ch chan T + id uint32 + relay *Relay[T] + once sync.Once +} + +// NewRelay is the factory to create a Relay. +func NewRelay[T any]() *Relay[T] { + return &Relay[T]{ + clients: make(map[uint32]*Listener[T]), + } +} + +// Notify sends a notification to all the listeners. +// It guarantees that all the listeners will receive the notification. +func (r *Relay[T]) Notify(v T) { + r.mu.RLock() + defer r.mu.RUnlock() + + for _, client := range r.clients { + client.ch <- v + } +} + +// NotifyCtx tries sending a notification to all the listeners until the context times out or is canceled. +func (r *Relay[T]) NotifyCtx(ctx context.Context, v T) { + r.mu.RLock() + defer r.mu.RUnlock() + + for _, client := range r.clients { + select { + case client.ch <- v: + case <-ctx.Done(): + return + } + } +} + +// Broadcast broadcasts a notification to all the listeners. +// The notification is sent in a non-blocking manner, so there's no guarantee that a listener receives it. +func (r *Relay[T]) Broadcast(v T) { + r.mu.RLock() + defer r.mu.RUnlock() + + for _, client := range r.clients { + select { + case client.ch <- v: + default: + } + } +} + +// Listener creates a new listener given a channel capacity. +func (r *Relay[T]) Listener(capacity int) *Listener[T] { + r.mu.Lock() + defer r.mu.Unlock() + + listener := &Listener[T]{ + ch: make(chan T, capacity), + id: r.n, + relay: r, + } + r.clients[r.n] = listener + r.n++ + return listener +} + +// Close closes a relay. +// This operation can be safely called in the meantime as Listener.Close() +func (r *Relay[T]) Close() { + r.mu.Lock() + defer r.mu.Unlock() + + for _, client := range r.clients { + r.closeRelay(client) + } + r.clients = nil +} + +func (r *Relay[T]) closeRelay(l *Listener[T]) { + l.once.Do(func() { + close(l.ch) + delete(r.clients, l.id) + }) +} + +func (r *Relay[T]) closeListener(l *Listener[T]) { + r.mu.Lock() + defer r.mu.Unlock() + + close(l.ch) + delete(r.clients, l.id) +} + +// Ch returns the Listener channel. +func (l *Listener[T]) Ch() <-chan T { + return l.ch +} + +// Close closes a listener. +// This operation can be safely called in the meantime as Relay.Close() +func (l *Listener[T]) Close() { + l.once.Do(func() { + l.relay.closeListener(l) + }) +} diff --git a/pkg/base/factory/indicator_factory.go b/pkg/base/factory/indicator_factory.go new file mode 100644 index 0000000..6faa160 --- /dev/null +++ b/pkg/base/factory/indicator_factory.go @@ -0,0 +1,50 @@ +/* + * MIT License + * + * Copyright (c) 2024 DearNostalgia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package factory + +import ( + "github.com/dearnostalgia/gota/pkg/base/indicator" +) + +type IndicatorFactory[T any] struct { +} + +func (f *IndicatorFactory[T]) CreateIndicator(i indicator.Indicator[T], source *indicator.AttributeSource[T]) indicator.Indicator[T] { + if ci, ok := any(i).(indicator.CacheIndicator[T]); ok { + decorator := &indicator.CacheIndicatorDecorator[T]{ + CacheIndicator: ci, + Cache: ci.GetCache(), + } + decorator.SetSource(source) + return decorator + } + decorator := &indicator.BaseIndicatorDecorator[T]{ + Indicator: i, + Res: i.GetResults(), + } + decorator.SetSource(source) + return decorator +} diff --git a/pkg/base/indicator/.DS_Store b/pkg/base/indicator/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..809a455b5dbf984c9b020f71e7a06074497d3735 GIT binary patch literal 6148 zcmeH~J&wXa427SU6iC~oq@0EWaDx$o6YK>LKhhRR6ub0ube=tKHf)U|^eoxm*b^(s zD>5+v+uqL`U<9zFJF)gKF=Ia97Z;rHef~LJkGI>^i?ojpcuF5J+0Si33P=GdAO)m= z6qu0$d5kYVXY@>Z6e%DD=AnRp9}3-BldUs89Sku7kORwMT*oXy7B7%B**aOFSxyg@ ztrla5*Q1>*d0kDm&fX5o;luLI=2HyKdONHzp;-+mNC7D@QDD*Y(a--q{lEEt(xOxf zNP#y~z=qw|Zp)X-v-Roqyne{4uN$3=%NhRs1TgWVctsE6e(?oaldY2#ntlXA1_deb GQUx9>ixQ~- literal 0 HcmV?d00001 diff --git a/pkg/base/indicator/base_indicator.go b/pkg/base/indicator/base_indicator.go new file mode 100644 index 0000000..cef30c8 --- /dev/null +++ b/pkg/base/indicator/base_indicator.go @@ -0,0 +1,58 @@ +/* + * MIT License + * + * Copyright (c) 2024 DearNostalgia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package indicator + +import ( + "time" +) + +type Result[T any] struct { + Value T + BeginTime time.Time +} + +type Indicator[T any] interface { + Calculate(idx int) (T, error) + SetSource(s *AttributeSource[T]) + GetSource() *AttributeSource[T] + GetResults() []*Result[T] +} + +type BaseIndicator[T any] struct { + Source *AttributeSource[T] +} + +func NewBasicIndicator[T any]() *BaseIndicator[T] { + return &BaseIndicator[T]{} +} + +func (b *BaseIndicator[T]) SetSource(s *AttributeSource[T]) { + b.Source = s +} + +func (b *BaseIndicator[T]) GetSource() *AttributeSource[T] { + return b.Source +} diff --git a/pkg/base/indicator/cache.go b/pkg/base/indicator/cache.go new file mode 100644 index 0000000..58d7317 --- /dev/null +++ b/pkg/base/indicator/cache.go @@ -0,0 +1,172 @@ +/* + * MIT License + * + * Copyright (c) 2024 DearNostalgia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package indicator + +import ( + "errors" + "fmt" + "github.com/dearnostalgia/gota/pkg/base/bar_series" + "sync" +) + +var ( + errorCacheResultsOutRange = errors.New("index out of range") + errorBarSeriesNotMatchIndicator = errors.New("bar series not match") +) + +type CacheIndicator[T any] interface { + Indicator[T] + GetCache() *Cache[T] +} + +type EnhancedCacheIndicator[T any] struct { + *BaseIndicator[T] + Cache *Cache[T] +} + +func NewEnhancedCacheIndicator[T any]() *EnhancedCacheIndicator[T] { + return &EnhancedCacheIndicator[T]{ + BaseIndicator: NewBasicIndicator[T](), + } +} + +func (b *EnhancedCacheIndicator[T]) GetCache() *Cache[T] { + return b.Cache +} + +func (b *EnhancedCacheIndicator[T]) GetResults() []*Result[T] { + return b.Cache.GetResults() +} + +func (b *EnhancedCacheIndicator[T]) SetSource(s *AttributeSource[T]) { + b.Source = s + if s != nil && s.GetBarSeries() != nil && b.Cache == nil { + b.Cache = NewCache[T](s.GetBarSeries()) + } +} + +type Cache[T any] struct { + barSeries *bar_series.BarSeries + results []*Result[T] + unstableValue T + mu sync.RWMutex +} + +func NewCache[T any](barSeries *bar_series.BarSeries) *Cache[T] { + cacheIndicator := &Cache[T]{ + barSeries: barSeries, + results: make([]*Result[T], 0), + mu: sync.RWMutex{}, + } + return cacheIndicator +} + +func (cs *Cache[T]) GetResults() []*Result[T] { + return cs.results +} + +func (cs *Cache[T]) checkPosition() bool { + + barStartTime := (*(*cs.barSeries).GetFirstBar()).GetBeginTime() + + if len(cs.results) == 0 || cs.results[0].BeginTime.Equal(barStartTime) { + return true + } + + if cs.results[0].BeginTime.Before(barStartTime) { + removeIdx := 0 + for removeIdx < len(cs.results) && cs.results[removeIdx].BeginTime.Before(barStartTime) { + removeIdx++ + } + + cs.results = cs.results[removeIdx:] + if len(cs.results) > 0 && cs.results[0].BeginTime.Equal(barStartTime) { + return true + } + } + return false +} + +func (cs *Cache[T]) GetValue(idx int, calcFunc func(int) (T, error)) (T, error) { + cs.mu.Lock() + defer cs.mu.Unlock() + + var ( + zero T + err error + b = *(*cs.barSeries).GetBar(idx) + barSeriesSize = (*cs.barSeries).Size() + ) + + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("cache get value panic occurred: %v", r) + } + }() + + if !cs.checkPosition() { + return zero, errorBarSeriesNotMatchIndicator + } + + // Ensure index is within bounds + if idx < 0 || idx >= barSeriesSize { + return zero, errorCacheResultsOutRange + } + + if idx < len(cs.results) { + tRes := cs.results[idx] + if tRes.BeginTime != b.GetBeginTime() { + return zero, errorBarSeriesNotMatchIndicator + } + return tRes.Value, nil + } + + // Calculate the result using the provided function + result, err := calcFunc(idx) + if err != nil { + return zero, err + } + return result, nil +} + +func (cs *Cache[T]) store(idx int, result T) error { + cs.mu.Lock() + defer cs.mu.Unlock() + + if !cs.checkPosition() { + return errorBarSeriesNotMatchIndicator + } + var b = *(*cs.barSeries).GetBar(idx) + if b.IsEnd() && idx >= len(cs.results) { + cs.results = append(cs.results, &Result[T]{ + BeginTime: b.GetBeginTime(), + Value: result, + }) + return nil + } + cs.unstableValue = result + return nil +} diff --git a/pkg/base/indicator/decorator.go b/pkg/base/indicator/decorator.go new file mode 100644 index 0000000..61299dc --- /dev/null +++ b/pkg/base/indicator/decorator.go @@ -0,0 +1,106 @@ +/* + * MIT License + * + * Copyright (c) 2024 DearNostalgia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package indicator + +import ( + clone "github.com/huandu/go-clone/generic" + "sync" +) + +type BaseIndicatorDecorator[T any] struct { + Indicator Indicator[T] + Res []*Result[T] + mu sync.RWMutex +} + +func (b *BaseIndicatorDecorator[T]) SetSource(s *AttributeSource[T]) { + b.mu.Lock() + defer b.mu.Unlock() + b.Indicator.SetSource(s) +} + +func (b *BaseIndicatorDecorator[T]) GetSource() *AttributeSource[T] { + b.mu.RLock() + defer b.mu.RUnlock() + return b.Indicator.GetSource() +} + +func (b *BaseIndicatorDecorator[T]) Calculate(idx int) (T, error) { + b.mu.Lock() + defer b.mu.Unlock() + var zero T + res, err := b.Indicator.Calculate(idx) + if err != nil { + return zero, err + } + return res, nil +} + +func (b *BaseIndicatorDecorator[T]) GetResults() []*Result[T] { + b.mu.RLock() + defer b.mu.RUnlock() + return clone.Wrap(b.Res) +} + +type CacheIndicatorDecorator[T any] struct { + CacheIndicator CacheIndicator[T] + Cache *Cache[T] + mu sync.RWMutex +} + +func (c *CacheIndicatorDecorator[T]) SetSource(s *AttributeSource[T]) { + c.mu.Lock() + defer c.mu.Unlock() + c.CacheIndicator.SetSource(s) + c.Cache = c.CacheIndicator.GetCache() +} + +func (c *CacheIndicatorDecorator[T]) GetSource() *AttributeSource[T] { + c.mu.RLock() + defer c.mu.RUnlock() + return c.CacheIndicator.GetSource() +} + +func (c *CacheIndicatorDecorator[T]) Calculate(idx int) (T, error) { + c.mu.Lock() + defer c.mu.Unlock() + var zero T + res, err := c.CacheIndicator.Calculate(idx) + if err != nil { + return zero, err + } + err = c.Cache.store(idx, res) + if err != nil { + return zero, err + } + return res, nil +} + +func (c *CacheIndicatorDecorator[T]) GetResults() []*Result[T] { + c.mu.RLock() + defer c.mu.RUnlock() + return clone.Wrap(c.Cache.GetResults()) +} diff --git a/pkg/base/indicator/scheduler/executor.go b/pkg/base/indicator/scheduler/executor.go new file mode 100644 index 0000000..558cbc3 --- /dev/null +++ b/pkg/base/indicator/scheduler/executor.go @@ -0,0 +1,79 @@ +/* + * MIT License + * + * Copyright (c) 2024 DearNostalgia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package schedule + +import ( + "github.com/dearnostalgia/gota/pkg/base/indicator" + "github.com/dearnostalgia/gota/pkg/base/logger" + clone "github.com/huandu/go-clone/generic" + "go.uber.org/zap" +) + +type Executor[T any] struct{} + +func (e *Executor[T]) Shoot(indicator indicator.Indicator[T], startCalIdx int) error { + source := indicator.GetSource() + barSeries := source.GetBarSeries() + eventManager := NewEventProcessor[T](indicator) + + done := make(chan struct{}) + + go func() { + <-done + l := (*barSeries).Subscribe() + for event := range l.Ch() { + err := (*eventManager).ProcessEvent(event) + if err != nil { + l.Close() + logger.Logger.Error("indicator calculator failed to process event, close describe, ", + zap.Int("event_idx", event.Idx), + zap.Any("event_type", event.BarSeriesEventType), + zap.Any("bar_data", *event.Bar), + zap.Any("indicator", indicator), + zap.Error(err), + ) + return + } + } + }() + + if startCalIdx >= 0 { + copSource := clone.Wrap(source) + indicator.SetSource(copSource) + copSize := (*copSource.GetBarSeries()).Size() + + for i := startCalIdx; i < copSize; i++ { + _, err := indicator.Calculate(i) + if err != nil { + return err + } + } + indicator.SetSource(source) + } + + close(done) + return nil +} diff --git a/pkg/base/indicator/scheduler/processor.go b/pkg/base/indicator/scheduler/processor.go new file mode 100644 index 0000000..20fd842 --- /dev/null +++ b/pkg/base/indicator/scheduler/processor.go @@ -0,0 +1,83 @@ +/* + * MIT License + * + * Copyright (c) 2024 DearNostalgia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package schedule + +import ( + "github.com/dearnostalgia/gota/pkg/base/bar_series" + "github.com/dearnostalgia/gota/pkg/base/indicator" + "sync" + "sync/atomic" +) + +const ( + oldLock = iota + newLock +) + +type EventProcessor[T any] struct { + indicator indicator.Indicator[T] + locked int32 + mu sync.Mutex +} + +func NewEventProcessor[T any](indicator indicator.Indicator[T]) *EventProcessor[T] { + return &EventProcessor[T]{ + indicator: indicator, + locked: oldLock, + mu: sync.Mutex{}, + } +} + +func (e *EventProcessor[T]) ProcessEvent(event bar_series.BarSeriesEvent) error { + var bar = *event.Bar + if bar.IsEnd() { + return e.ProcessFinalBar(event.Idx) + } + return e.ProcessUnstableBar(event.Idx) +} + +func (e *EventProcessor[T]) ProcessFinalBar(idx int) error { + e.mu.Lock() + defer e.mu.Unlock() + + _, err := e.indicator.Calculate(idx) + if err != nil { + return err + } + return nil +} + +func (e *EventProcessor[T]) ProcessUnstableBar(idx int) error { + if atomic.CompareAndSwapInt32(&e.locked, oldLock, newLock) { + defer atomic.StoreInt32(&e.locked, oldLock) + _, err := e.indicator.Calculate(idx) + if err != nil { + return err + } + return nil + } + return nil +} diff --git a/pkg/base/indicator/source.go b/pkg/base/indicator/source.go new file mode 100644 index 0000000..f947264 --- /dev/null +++ b/pkg/base/indicator/source.go @@ -0,0 +1,103 @@ +/* + * MIT License + * + * Copyright (c) 2024 DearNostalgia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package indicator + +import ( + "github.com/dearnostalgia/gota/pkg/base/bar" + "github.com/dearnostalgia/gota/pkg/base/bar_series" + "github.com/govalues/decimal" +) + +type AttributeStrategy[T any] interface { + GetAttribute(bar *bar.Bar) T +} + +type AttributeSource[T any] struct { + series *bar_series.BarSeries + strategy AttributeStrategy[T] +} + +// NewAttributeSource creates a new instance of AttributeSource, which is used to retrieve a specific attribute +// from a BarSeries. The generic parameter T represents the type of the attribute, such as decimal.Decimal or +// any other type you require. +// +// Parameters: +// - series: bar_series.BarSeries is a dataset containing a series of bars, typically representing time-series data. +// - strategy: AttributeStrategy[T] is a strategy interface that defines how to extract a specific type of +// attribute from a given bar. +// +// Returns: +// - A pointer to an AttributeSource[T] instance, which allows you to use the provided strategy to retrieve +// the specified type of attribute from the BarSeries. +// +// Example usage: +// var sliceBarSeries bar_series.BarSeries +// closeSource := NewAttributeSource[decimal.Decimal](sliceBarSeries, &ClosePriceStrategy{}) +// closeValue := closeSource.GetValue(0) +func NewAttributeSource[T any](series bar_series.BarSeries, strategy AttributeStrategy[T]) *AttributeSource[T] { + return &AttributeSource[T]{ + series: &series, + strategy: strategy, + } +} + +func (as *AttributeSource[T]) GetValue(idx int) T { + return as.strategy.GetAttribute((*as.series).GetBar(idx)) +} + +func (as *AttributeSource[T]) GetBarSeries() *bar_series.BarSeries { + return as.series +} + +type ClosePriceStrategy struct{} + +func (s *ClosePriceStrategy) GetAttribute(bar *bar.Bar) decimal.Decimal { + return (*bar).GetClosePrice() +} + +type OpenPriceStrategy struct{} + +func (s *OpenPriceStrategy) GetAttribute(bar *bar.Bar) decimal.Decimal { + return (*bar).GetOpenPrice() +} + +type HighPriceStrategy struct{} + +func (s *HighPriceStrategy) GetAttribute(bar *bar.Bar) decimal.Decimal { + return (*bar).GetHighPrice() +} + +type LowPriceStrategy struct{} + +func (s *LowPriceStrategy) GetAttribute(bar *bar.Bar) decimal.Decimal { + return (*bar).GetLowPrice() +} + +type VolumeStrategy struct{} + +func (s *VolumeStrategy) GetAttribute(bar *bar.Bar) decimal.Decimal { + return (*bar).GetVolume() +} diff --git a/pkg/base/indicator/tech/ema.go b/pkg/base/indicator/tech/ema.go new file mode 100644 index 0000000..1fd0381 --- /dev/null +++ b/pkg/base/indicator/tech/ema.go @@ -0,0 +1,97 @@ +/* + * MIT License + * + * Copyright (c) 2024 DearNostalgia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package tech + +import ( + "errors" + "github.com/dearnostalgia/gota/pkg/base/common" + "github.com/dearnostalgia/gota/pkg/base/indicator" + "github.com/govalues/decimal" +) + +var _ indicator.CacheIndicator[decimal.Decimal] = (*EMAIndicator)(nil) + +type EMAIndicator struct { + *indicator.EnhancedCacheIndicator[decimal.Decimal] + period int + multiplier decimal.Decimal +} + +func NewEMAIndicator(period int) (indicator.CacheIndicator[decimal.Decimal], error) { + multiplier, err := decimal.NewFromFloat64(2.0 / float64(period+1)) + if err != nil { + common.IndicatorCalculateErrLog(0, common.EMA, errors.New("period:"+err.Error())) + return nil, err + } + emaIndicator := &EMAIndicator{ + EnhancedCacheIndicator: indicator.NewEnhancedCacheIndicator[decimal.Decimal](), + period: period, + multiplier: multiplier, + } + return emaIndicator, nil +} + +func (e *EMAIndicator) Calculate(idx int) (decimal.Decimal, error) { + var ( + sv = e.Source.GetValue(idx) + err error + ) + + if idx == 0 { + return sv, nil + } + + prevEMA, err := e.Cache.GetValue(idx-1, e.Calculate) + + if err != nil { + common.IndicatorCalculateErrLog(idx, common.EMA, err) + return decimal.Zero, err + } + + res, err := e.calculateEMA(prevEMA, sv) + if err != nil { + common.IndicatorCalculateErrLog(idx, common.EMA, err) + return decimal.Zero, err + } + return res, nil +} + +func (e *EMAIndicator) calculateEMA(prevEMA decimal.Decimal, sourceVal decimal.Decimal) (decimal.Decimal, error) { + s, err := sourceVal.Sub(prevEMA) + if err != nil { + return decimal.Zero, err + } + m, err := s.Mul(e.multiplier) + if err != nil { + return decimal.Zero, err + } + + res, err := m.Add(prevEMA) + if err != nil { + return decimal.Zero, err + } + return res, nil +} diff --git a/pkg/base/indicator/tech/sma.go b/pkg/base/indicator/tech/sma.go new file mode 100644 index 0000000..4286a0c --- /dev/null +++ b/pkg/base/indicator/tech/sma.go @@ -0,0 +1,63 @@ +/* + * MIT License + * + * Copyright (c) 2024 DearNostalgia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package tech + +import ( + "github.com/dearnostalgia/gota/pkg/base/common" + "github.com/dearnostalgia/gota/pkg/base/indicator" + "github.com/dearnostalgia/gota/pkg/helper" + "github.com/govalues/decimal" +) + +var _ indicator.CacheIndicator[decimal.Decimal] = (*SMAIndicator)(nil) + +type SMAIndicator struct { + *indicator.EnhancedCacheIndicator[decimal.Decimal] + period int +} + +func NewSMAIndicator(period int) (indicator.CacheIndicator[decimal.Decimal], error) { + smaIndicator := &SMAIndicator{ + EnhancedCacheIndicator: indicator.NewEnhancedCacheIndicator[decimal.Decimal](), + period: period, + } + return smaIndicator, nil +} + +func (e *SMAIndicator) Calculate(idx int) (decimal.Decimal, error) { + var err error + sum := decimal.Zero + for i := helper.Max(0, idx-e.period+1); i <= idx; i++ { + sum, err = sum.Add(e.Source.GetValue(i)) + if err != nil { + common.IndicatorCalculateErrLog(idx, common.SMA, err) + return decimal.Zero, err + } + } + + realBarCnt, _ := decimal.NewFromFloat64(float64(helper.Min(e.period, idx+1))) + return sum.Quo(realBarCnt) +} diff --git a/pkg/base/logger/logger.go b/pkg/base/logger/logger.go new file mode 100644 index 0000000..0828ade --- /dev/null +++ b/pkg/base/logger/logger.go @@ -0,0 +1,47 @@ +/* + * MIT License + * + * Copyright (c) 2024 DearNostalgia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package logger + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var Logger *zap.Logger + +func init() { + + config := zap.NewDevelopmentConfig() + + var err error + Logger, err = config.Build(zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)) + if err != nil { + panic("failed to initialize zap logger: " + err.Error()) + } + + zap.ReplaceGlobals(Logger) + defer Logger.Sync() +} diff --git a/pkg/constant/kline/kline_interval.go b/pkg/constant/kline/kline_interval.go new file mode 100644 index 0000000..bd6b238 --- /dev/null +++ b/pkg/constant/kline/kline_interval.go @@ -0,0 +1,75 @@ +/* + * MIT License + * + * Copyright (c) 2024 DearNostalgia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package kline + +import "time" + +type Interval struct { + Interval string + Seconds int + DataPointsPerDay int + Duration time.Duration +} + +var ( + Interval1m = Interval{"1m", 60, 1440, time.Minute} + Interval3m = Interval{"3m", 3 * 60, 480, 3 * time.Minute} + Interval5m = Interval{"5m", 5 * 60, 288, 5 * time.Minute} + Interval15m = Interval{"15m", 15 * 60, 96, 15 * time.Minute} + Interval30m = Interval{"30m", 30 * 60, 48, 30 * time.Minute} + Interval1h = Interval{"1h", 60 * 60, 24, time.Hour} + Interval2h = Interval{"2h", 2 * 60 * 60, 12, 2 * time.Hour} + Interval3h = Interval{"3h", 3 * 60 * 60, 8, 3 * time.Hour} + Interval4h = Interval{"4h", 4 * 60 * 60, 6, 4 * time.Hour} + Interval6h = Interval{"6h", 6 * 60 * 60, 4, 6 * time.Hour} + Interval12h = Interval{"12h", 12 * 60 * 60, 2, 12 * time.Hour} + Interval1d = Interval{"1d", 24 * 60 * 60, 1, 24 * time.Hour} + Interval1w = Interval{"1w", 7 * 24 * 60 * 60, 1 / 7, 7 * 24 * time.Hour} +) + +func (k Interval) GetInterval() string { + return k.Interval +} + +func (k Interval) GetDuration() time.Duration { + return k.Duration +} + +func (k Interval) ToSeconds() int { + return k.Seconds +} + +func (k Interval) ToMillis() int { + return k.ToSeconds() * 1000 +} + +func (k Interval) GetDataPointsPerDay() int { + return k.DataPointsPerDay +} + +func (k Interval) GetDataPointsForDays(days int) int { + return days * k.GetDataPointsPerDay() +} diff --git a/pkg/helper/math.go b/pkg/helper/math.go new file mode 100644 index 0000000..d4ac755 --- /dev/null +++ b/pkg/helper/math.go @@ -0,0 +1,40 @@ +/* + * MIT License + * + * Copyright (c) 2024 DearNostalgia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package helper + +func Max(a, b int) int { + if a >= b { + return a + } + return b +} + +func Min(a, b int) int { + if a <= b { + return a + } + return b +} diff --git a/pkg/helper/time.go b/pkg/helper/time.go new file mode 100644 index 0000000..be02fa8 --- /dev/null +++ b/pkg/helper/time.go @@ -0,0 +1,32 @@ +/* + * MIT License + * + * Copyright (c) 2024 DearNostalgia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package helper + +import "time" + +func IsBetween(t, start, end time.Time) bool { + return (t.After(start) || t.Equal(start)) && (t.Before(end) || t.Equal(end)) +} diff --git a/pkg/platform/binance/kline.go b/pkg/platform/binance/kline.go new file mode 100644 index 0000000..a33bead --- /dev/null +++ b/pkg/platform/binance/kline.go @@ -0,0 +1,107 @@ +/* + * MIT License + * + * Copyright (c) 2024 DearNostalgia + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package binance + +import ( + "time" + + "github.com/govalues/decimal" + + "github.com/dearnostalgia/gota/pkg/base/bar" +) + +type KlineTicker struct { + //todo fix websocket + //for example + // + // + //"k":{ + // "t":1607443020000, // Kline start time + // "T":1607443079999, // Kline close time + // "i":"1m", // Interval + // "f":116467658886, // First updateId + // "L":116468012423, // Last updateId + // "o":"18787.00", // Open price + // "c":"18804.04", // Close price + // "h":"18804.04", // High price + // "l":"18786.54", // Low price + // "v":"197.664", // volume + // "n": 543, // Number of trades + // "x":false, // Is this kline closed? + // "q":"3715253.19494", // Quote asset volume + // "V":"184.769", // Taker buy volume + // "Q":"3472925.84746", //Taker buy quote asset volume + // "B":"0" // Ignore + // } + StartTime int64 `json:"startTime"` + EndTime int64 `json:"endTime"` + OpenPrice float64 `json:"openPrice"` + HighPrice float64 `json:"highPrice"` + LowPrice float64 `json:"lowPrice"` + ClosePrice float64 `json:"closePrice"` + Volume float64 `json:"volume,omitempty"` + TurnOver float64 `json:"turnOver,omitempty"` + IsEnd bool `json:"isEnd,omitempty"` +} + +func (k *KlineTicker) Convert2BaseBar() (*bar.BaseBar, error) { + var err error + o, err := decimal.NewFromFloat64(k.OpenPrice) + if err != nil { + return nil, err + } + h, err := decimal.NewFromFloat64(k.HighPrice) + if err != nil { + return nil, err + } + l, err := decimal.NewFromFloat64(k.LowPrice) + if err != nil { + return nil, err + } + c, err := decimal.NewFromFloat64(k.ClosePrice) + if err != nil { + return nil, err + } + v, err := decimal.NewFromFloat64(k.Volume) + if err != nil { + return nil, err + } + a, err := decimal.NewFromFloat64(k.TurnOver) + if err != nil { + return nil, err + } + + return bar.NewBaseBar(time.UnixMilli(k.StartTime), + o, + c, + h, + l, + bar.WithEndTime(time.UnixMilli(k.EndTime)), + bar.WithAmount(a), + bar.WithVolume(v), + bar.WithIsEnd(k.IsEnd), + ), nil +}