-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
28 changed files
with
2,656 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
name: Go Code Quality and Security Check | ||
|
||
on: | ||
push: | ||
pull_request: | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- name: Set up Go | ||
uses: actions/setup-go@v3 | ||
with: | ||
go-version: '1.22' | ||
|
||
- name: Check out code | ||
uses: actions/checkout@v3 | ||
|
||
- name: Install Staticcheck | ||
run: go install honnef.co/go/tools/cmd/staticcheck@latest | ||
|
||
- name: Run Staticcheck | ||
run: staticcheck ./... | ||
|
||
- name: Run Go Vet | ||
run: go vet ./... | ||
|
||
- name: Install and Run GoSec | ||
run: | | ||
go install github.com/securego/gosec/v2/cmd/gosec@latest | ||
gosec ./... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
||
```go | ||
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). | ||
|
||
```go | ||
// 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. | ||
|
||
```go | ||
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. | ||
|
||
```go | ||
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. | ||
|
||
```go | ||
closeSource := indicator.NewAttributeSource[decimal.Decimal](barSeries, &indicator.ClosePriceStrategy{}) | ||
highSource := indicator.NewAttributeSource[decimal.Decimal](barSeries, &indicator.HighPriceStrategy{}) | ||
volumeSource := indicator.NewAttributeSource[decimal.Decimal](barSeries, &indicator.VolumeStrategy{}) | ||
``` | ||
|
||
If you want to customize your own data source, refer to the strategy pattern below, and ensure you design your custom Bar. | ||
|
||
```go | ||
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. | ||
|
||
|
||
```go | ||
IndicatorFactory := new(factory.IndicatorFactory[decimal.Decimal]) | ||
ema, err := tech.NewEMAIndicator(20) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
closeSource := indicator.NewAttributeSource[decimal.Decimal](barSeries, &indicator.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 | ||
|
||
```go | ||
v, err := EMAIndicator.Calculate(idx) | ||
``` | ||
|
||
#### interface | ||
|
||
To implement your own Indicator, you need to implement the Indicator interface. | ||
|
||
```go | ||
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. | ||
|
||
```go | ||
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 | ||
} | ||
|
||
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 | ||
} | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
) |
Oops, something went wrong.