diff --git a/config/drift.yaml b/config/drift.yaml index aa4e4785f..698b70b75 100644 --- a/config/drift.yaml +++ b/config/drift.yaml @@ -14,23 +14,24 @@ exchangeStrategies: # kline interval for indicators interval: 15m window: 3 - exits: - - roiStopLoss: - percentage: 0.8% - - roiTakeProfit: - percentage: 35% - - protectiveStopLoss: - activationRatio: 0.6% - stopLossRatio: 0.1% - placeStopOrder: false - - protectiveStopLoss: - activationRatio: 5% - stopLossRatio: 1% - placeStopOrder: false - - cumulatedVolumeTakeProfit: - interval: 5m - window: 2 - minQuoteVolume: 200_000_000 + stoploss: 2% + #exits: + #- roiStopLoss: + # percentage: 0.8% + #- roiTakeProfit: + # percentage: 35% + #- protectiveStopLoss: + # activationRatio: 0.6% + # stopLossRatio: 0.1% + # placeStopOrder: false + #- protectiveStopLoss: + # activationRatio: 5% + # stopLossRatio: 1% + # placeStopOrder: false + #- cumulatedVolumeTakeProfit: + # interval: 5m + # window: 2 + # minQuoteVolume: 200_000_000 #- protectiveStopLoss: # activationRatio: 2% # stopLossRatio: 1% @@ -53,8 +54,8 @@ backtest: sessions: [binance] accounts: binance: - #makerFeeRate: 0 - #takerFeeRate: 0 + #makerFeeRate: 0.00001 + #takerFeeRate: 0.00001 balances: - ETH: 0.0 + ETH: 10.0 USDT: 5000.0 diff --git a/go.mod b/go.mod index 34bd47ce8..777a3464d 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/c9s/bbgo -go 1.17 +go 1.18 require ( github.com/DATA-DOG/go-sqlmock v1.5.0 @@ -43,6 +43,7 @@ require ( github.com/spf13/viper v1.7.1 github.com/stretchr/testify v1.7.0 github.com/valyala/fastjson v1.5.1 + github.com/wcharczuk/go-chart/v2 v2.1.0 github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90 github.com/x-cray/logrus-prefixed-formatter v0.5.2 github.com/zserge/lorca v0.1.9 @@ -75,6 +76,7 @@ require ( github.com/go-test/deep v1.0.6 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -117,6 +119,7 @@ require ( go.opentelemetry.io/otel/trace v0.19.0 // indirect go.uber.org/atomic v1.9.0 // indirect golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect + golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect golang.org/x/mod v0.5.1 // indirect golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect diff --git a/go.sum b/go.sum index 96f942c4d..01b2cdcaa 100644 --- a/go.sum +++ b/go.sum @@ -182,6 +182,7 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -516,6 +517,7 @@ github.com/ugorji/go/codec v1.2.3 h1:/mVYEV+Jo3IZKeA5gBngN0AvNnQltEDkR+eQikkWQu0 github.com/ugorji/go/codec v1.2.3/go.mod h1:5FxzDJIgeiWJZslYHPj+LS1dq1ZBQVelZFnjsFGI/Uc= github.com/valyala/fastjson v1.5.1 h1:SXaQZVSwLjZOVhDEhjiCcDtnX0Feu7Z7A1+C5atpoHM= github.com/valyala/fastjson v1.5.1/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I= github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90 h1:G/O1RFjhc9hgVYjaPQ0Oceqxf3GwRQl/5XEAWYetjmg= github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90/go.mod h1:rpXAuuHgyEJb6kXcXldlkOjU6y4x+YcASKKXJNUhh0Y= @@ -584,6 +586,7 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= diff --git a/pkg/indicator/atr.go b/pkg/indicator/atr.go index c44202596..1fdd8cef8 100644 --- a/pkg/indicator/atr.go +++ b/pkg/indicator/atr.go @@ -22,6 +22,24 @@ type ATR struct { var _ types.SeriesExtend = &ATR{} +func (inc *ATR) Clone() *ATR { + out := &ATR{ + IntervalWindow: inc.IntervalWindow, + PercentageVolatility: inc.PercentageVolatility[:], + PreviousClose: inc.PreviousClose, + RMA: inc.RMA.Clone().(*RMA), + EndTime: inc.EndTime, + } + out.SeriesBase.Series = out + return out +} + +func (inc *ATR) TestUpdate(high, low, cloze float64) *ATR { + c := inc.Clone() + c.Update(high, low, cloze) + return c +} + func (inc *ATR) Update(high, low, cloze float64) { if inc.Window <= 0 { panic("window must be greater than 0") diff --git a/pkg/indicator/cci.go b/pkg/indicator/cci.go index d7165037a..6a75ff563 100644 --- a/pkg/indicator/cci.go +++ b/pkg/indicator/cci.go @@ -78,7 +78,6 @@ func (inc *CCI) Length() int { var _ types.SeriesExtend = &CCI{} - func (inc *CCI) PushK(k types.KLine) { inc.Update(k.High.Add(k.Low).Add(k.Close).Div(three).Float64()) } diff --git a/pkg/indicator/const.go b/pkg/indicator/const.go index 4b48f2b99..7764c75dd 100644 --- a/pkg/indicator/const.go +++ b/pkg/indicator/const.go @@ -9,4 +9,3 @@ import ( var three = fixedpoint.NewFromInt(3) var zeroTime = time.Time{} - diff --git a/pkg/indicator/dema.go b/pkg/indicator/dema.go index 69e75c4f2..bec582329 100644 --- a/pkg/indicator/dema.go +++ b/pkg/indicator/dema.go @@ -18,6 +18,23 @@ type DEMA struct { UpdateCallbacks []func(value float64) } +func (inc *DEMA) Clone() *DEMA { + out := &DEMA{ + IntervalWindow: inc.IntervalWindow, + Values: inc.Values[:], + a1: inc.a1.Clone(), + a2: inc.a2.Clone(), + } + out.SeriesBase.Series = out + return out +} + +func (inc *DEMA) TestUpdate(value float64) *DEMA { + out := inc.Clone() + out.Update(value) + return out +} + func (inc *DEMA) Update(value float64) { if len(inc.Values) == 0 { inc.SeriesBase.Series = inc diff --git a/pkg/indicator/drift.go b/pkg/indicator/drift.go index f0cc1f757..acac77f0b 100644 --- a/pkg/indicator/drift.go +++ b/pkg/indicator/drift.go @@ -45,6 +45,24 @@ func (inc *Drift) Update(value float64) { } } +func (inc *Drift) Clone() (out *Drift) { + out = &Drift{ + IntervalWindow: inc.IntervalWindow, + chng: inc.chng.Clone(), + Values: inc.Values[:], + SMA: inc.SMA.Clone().(*SMA), + LastValue: inc.LastValue, + } + out.SeriesBase.Series = out + return out +} + +func (inc *Drift) TestUpdate(value float64) *Drift { + out := inc.Clone() + out.Update(value) + return out +} + func (inc *Drift) Index(i int) float64 { if inc.Values == nil { return 0 diff --git a/pkg/indicator/ewma.go b/pkg/indicator/ewma.go index 4830682ba..bb7360afc 100644 --- a/pkg/indicator/ewma.go +++ b/pkg/indicator/ewma.go @@ -23,6 +23,22 @@ type EWMA struct { var _ types.SeriesExtend = &EWMA{} +func (inc *EWMA) Clone() *EWMA { + out := &EWMA{ + IntervalWindow: inc.IntervalWindow, + Values: inc.Values[:], + LastOpenTime: inc.LastOpenTime, + } + out.SeriesBase.Series = out + return out +} + +func (inc *EWMA) TestUpdate(value float64) *EWMA { + out := inc.Clone() + out.Update(value) + return out +} + func (inc *EWMA) Update(value float64) { var multiplier = 2.0 / float64(1+inc.Window) diff --git a/pkg/indicator/rma.go b/pkg/indicator/rma.go index 4601272a7..413c51bd9 100644 --- a/pkg/indicator/rma.go +++ b/pkg/indicator/rma.go @@ -25,6 +25,20 @@ type RMA struct { updateCallbacks []func(value float64) } +func (inc *RMA) Clone() types.UpdatableSeriesExtend { + out := &RMA{ + IntervalWindow: inc.IntervalWindow, + Values: inc.Values[:], + counter: inc.counter, + Adjust: inc.Adjust, + tmp: inc.tmp, + sum: inc.sum, + EndTime: inc.EndTime, + } + out.SeriesBase.Series = out + return out +} + func (inc *RMA) Update(x float64) { lambda := 1 / float64(inc.Window) if inc.counter == 0 { diff --git a/pkg/indicator/sma.go b/pkg/indicator/sma.go index fe50dd9c4..d0f9ac99c 100644 --- a/pkg/indicator/sma.go +++ b/pkg/indicator/sma.go @@ -40,6 +40,16 @@ func (inc *SMA) Length() int { return inc.Values.Length() } +func (inc *SMA) Clone() types.UpdatableSeriesExtend { + out := &SMA{ + Values: inc.Values[:], + rawValues: types.Clone(inc.rawValues).(*types.Queue), + EndTime: inc.EndTime, + } + out.SeriesBase.Series = out + return out +} + var _ types.SeriesExtend = &SMA{} func (inc *SMA) Update(value float64) { diff --git a/pkg/indicator/util.go b/pkg/indicator/util.go index d676406cf..722d45a36 100644 --- a/pkg/indicator/util.go +++ b/pkg/indicator/util.go @@ -1,2 +1 @@ package indicator - diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index aab805b42..250436652 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -7,6 +7,7 @@ import ( "sync" "github.com/sirupsen/logrus" + "github.com/wcharczuk/go-chart/v2" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -35,11 +36,13 @@ type Strategy struct { *types.ProfitStats *types.TradeStats - drift types.UpdatableSeriesExtend + drift *indicator.Drift atr *indicator.ATR midPrice fixedpoint.Value lock sync.RWMutex + stoploss float64 `json:"stoploss"` + ExitMethods bbgo.ExitMethodSet `json:"exits"` Session *bbgo.ExchangeSession *bbgo.GeneralOrderExecutor @@ -69,6 +72,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } var Three fixedpoint.Value = fixedpoint.NewFromInt(3) +var Two fixedpoint.Value = fixedpoint.NewFromInt(2) func (s *Strategy) GetLastPrice() (lastPrice fixedpoint.Value) { var ok bool @@ -98,6 +102,9 @@ var Delta fixedpoint.Value = fixedpoint.NewFromFloat(0.01) func (s *Strategy) ClosePosition(ctx context.Context) (*types.Order, bool) { order := s.Position.NewMarketCloseOrder(fixedpoint.One) + if order == nil { + return nil, false + } order.TimeInForce = "" balances := s.Session.GetAccount().Balances() baseBalance := balances[s.Market.BaseCurrency].Available @@ -133,7 +140,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } if s.TradeStats == nil { - s.TradeStats = &types.TradeStats{} + s.TradeStats = types.NewTradeStats(s.Symbol) } // StrategyController @@ -165,9 +172,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se store, _ := session.MarketDataStore(s.Symbol) + getSource := func(kline *types.KLine) fixedpoint.Value { + //return kline.High.Add(kline.Low).Div(Two) + //return kline.Close + return kline.High.Add(kline.Low).Add(kline.Close).Div(Three) + } + s.drift = &indicator.Drift{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.Window}} - s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: 34}} - s.atr.Bind(store) + s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: 14}} klines, ok := store.KLinesOfInterval(s.Interval) if !ok { @@ -175,7 +187,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } for _, kline := range *klines { - s.drift.Update(kline.High.Add(kline.Low).Add(kline.Close).Div(Three).Float64()) + source := getSource(&kline).Float64() + s.drift.Update(source) s.atr.Update(kline.High.Float64(), kline.Low.Float64(), kline.Close.Float64()) } @@ -198,17 +211,46 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }) + dynamicKLine := &types.KLine{} + priceLine := types.NewQueue(100) + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { if s.Status != types.StrategyStatusRunning { return } - if kline.Symbol != s.Symbol || kline.Interval != s.Interval { + if kline.Symbol != s.Symbol { return } - hlc3 := kline.High.Add(kline.Low).Add(kline.Close).Div(Three) - s.drift.Update(hlc3.Float64()) + var driftPred, atr float64 + var drift []float64 + + if !kline.Closed { + return + } + if kline.Interval == types.Interval1m { + return + } + dynamicKLine.Copy(&kline) + + source := getSource(dynamicKLine) + sourcef := source.Float64() + priceLine.Update(sourcef) + dynamicKLine.Closed = false + s.drift.Update(sourcef) + drift = s.drift.Array(2) + driftPred = s.drift.Predict(3) + atr = s.atr.Last() price := s.GetLastPrice() - if s.drift.Last() < 0 && s.drift.Index(1) > 0 { + avg := s.Position.AverageCost.Float64() + + shortCondition := (driftPred <= 0 && drift[0] <= 0) && (s.Position.IsClosed() || s.Position.IsDust(fixedpoint.Max(price, source))) + longCondition := (driftPred >= 0 && drift[0] >= 0) && (s.Position.IsClosed() || s.Position.IsDust(fixedpoint.Min(price, source))) + exitShortCondition := ((drift[1] < 0 && drift[0] >= 0) || avg+atr/2 <= price.Float64() || avg*(1.+s.stoploss) <= price.Float64()) && + (!s.Position.IsClosed() && !s.Position.IsDust(fixedpoint.Max(price, source))) + exitLongCondition := ((drift[1] > 0 && drift[0] < 0) || avg-atr/2 >= price.Float64() || avg*(1.-s.stoploss) >= price.Float64()) && + (!s.Position.IsClosed() && !s.Position.IsDust(fixedpoint.Min(price, source))) + + if shortCondition { if s.ActiveOrderBook.NumOfOrders() > 0 { if err := s.GeneralOrderExecutor.GracefulCancelActiveOrderBook(ctx, s.ActiveOrderBook); err != nil { log.WithError(err).Errorf("cannot cancel orders") @@ -220,22 +262,19 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.Errorf("unable to get baseBalance") return } - if hlc3.Compare(price) < 0 { - hlc3 = price + if source.Compare(price) < 0 { + source = price } - if s.Market.IsDustQuantity(baseBalance.Available, hlc3) { - return - } - if !s.Position.IsClosed() && !s.Position.IsDust(hlc3) { + if s.Market.IsDustQuantity(baseBalance.Available, source) { return } _, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeSell, Type: types.OrderTypeLimitMaker, - Price: hlc3, - StopPrice: hlc3.Add(fixedpoint.NewFromFloat(s.atr.Last() / 3)), + Price: source, + StopPrice: fixedpoint.NewFromFloat(sourcef + atr/2), Quantity: baseBalance.Available, }) if err != nil { @@ -243,15 +282,24 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } } - if s.drift.Last() > 0 && s.drift.Index(1) < 0 { + if exitShortCondition { if s.ActiveOrderBook.NumOfOrders() > 0 { if err := s.GeneralOrderExecutor.GracefulCancelActiveOrderBook(ctx, s.ActiveOrderBook); err != nil { log.WithError(err).Errorf("cannot cancel orders") return } } - if hlc3.Compare(price) > 0 { - hlc3 = price + _, _ = s.ClosePosition(ctx) + } + if longCondition { + if s.ActiveOrderBook.NumOfOrders() > 0 { + if err := s.GeneralOrderExecutor.GracefulCancelActiveOrderBook(ctx, s.ActiveOrderBook); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + return + } + } + if source.Compare(price) > 0 { + source = price } quoteBalance, ok := s.Session.GetAccount().Balance(s.Market.QuoteCurrency) if !ok { @@ -259,29 +307,50 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } if s.Market.IsDustQuantity( - quoteBalance.Available.Div(hlc3), hlc3) { + quoteBalance.Available.Div(source), source) { return } - if !s.Position.IsClosed() && !s.Position.IsDust(hlc3) { + if !s.Position.IsClosed() && !s.Position.IsDust(source) { return } _, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeLimitMaker, - Price: hlc3, - StopPrice: hlc3.Sub(fixedpoint.NewFromFloat(s.atr.Last() / 3)), - Quantity: quoteBalance.Available.Div(hlc3), + Price: source, + StopPrice: fixedpoint.NewFromFloat(sourcef - atr/2), + Quantity: quoteBalance.Available.Div(source), }) if err != nil { log.WithError(err).Errorf("cannot place buy order") return } } + if exitLongCondition { + if s.ActiveOrderBook.NumOfOrders() > 0 { + if err := s.GeneralOrderExecutor.GracefulCancelActiveOrderBook(ctx, s.ActiveOrderBook); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + return + } + } + _, _ = s.ClosePosition(ctx) + } }) bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) + canvas := types.NewCanvas(s.InstanceID(), s.Interval) + fmt.Println(dynamicKLine.StartTime, dynamicKLine.EndTime) + mean := priceLine.Mean(100) + highestPrice := priceLine.Highest(100) + highestDrift := s.drift.Highest(100) + ratio := highestDrift / highestPrice + canvas.Plot("drift", s.drift, dynamicKLine.StartTime, 100) + canvas.Plot("zero", types.NumberSeries(0), dynamicKLine.StartTime, 100) + canvas.Plot("price", priceLine.Minus(mean).Mul(ratio), dynamicKLine.StartTime, 100) + f, _ := os.Create("output.png") + defer f.Close() + canvas.Render(chart.PNG, f) wg.Done() }) return nil diff --git a/pkg/types/float_slice.go b/pkg/types/float_slice.go index 3d53e3bc7..3632d77b8 100644 --- a/pkg/types/float_slice.go +++ b/pkg/types/float_slice.go @@ -12,6 +12,10 @@ func (s *Float64Slice) Push(v float64) { *s = append(*s, v) } +func (s *Float64Slice) Update(v float64) { + *s = append(*s, v) +} + func (s *Float64Slice) Pop(i int64) (v float64) { v = (*s)[i] *s = append((*s)[:i], (*s)[i+1:]...) diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index f7767b514..c39b0ea74 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -3,9 +3,11 @@ package types import ( "fmt" "math" + "time" "reflect" "gonum.org/v1/gonum/stat" + "github.com/wcharczuk/go-chart/v2" ) // Super basic Series type that simply holds the float64 data @@ -43,6 +45,15 @@ func (inc *Queue) Length() int { return len(inc.arr) } +func (inc *Queue) Clone() *Queue { + out := &Queue { + arr: inc.arr[:], + size: inc.size, + } + out.SeriesBase.Series = out + return out +} + func (inc *Queue) Update(v float64) { inc.arr = append(inc.arr, v) if len(inc.arr) > inc.size { @@ -50,7 +61,7 @@ func (inc *Queue) Update(v float64) { } } -var _ SeriesExtend = &Queue{} +var _ UpdatableSeriesExtend = &Queue{} // Float64Indicator is the indicators (SMA and EWMA) that we want to use are returning float64 data. type Float64Indicator interface { @@ -93,6 +104,7 @@ type SeriesExtend interface { Variance(length int) float64 Covariance(b Series, length int) float64 Correlation(b Series, length int, method ...CorrFunc) float64 + AutoCorrelation(length int, lag ...int) float64 Rank(length int) SeriesExtend Sigmoid() SeriesExtend Softmax(window int) SeriesExtend @@ -120,6 +132,24 @@ type UpdatableSeriesExtend interface { Update(float64) } +func Clone(u UpdatableSeriesExtend) UpdatableSeriesExtend { + method, ok := reflect.TypeOf(u).MethodByName("Clone") + if ok { + out := method.Func.Call([]reflect.Value{reflect.ValueOf(u)}) + return out[0].Interface().(UpdatableSeriesExtend) + } + panic("method Clone not exist") +} + +func TestUpdate(u UpdatableSeriesExtend, input float64) UpdatableSeriesExtend { + method, ok := reflect.TypeOf(u).MethodByName("TestUpdate") + if ok { + out := method.Func.Call([]reflect.Value{reflect.ValueOf(u), reflect.ValueOf(input)}) + return out[0].Interface().(UpdatableSeriesExtend) + } + panic("method TestUpdate not exist") +} + // The interface maps to pinescript basic type `series` for bool type // Access the internal historical data from the latest to the oldest // Index(0) always maps to Last() @@ -335,6 +365,10 @@ func (a NumberSeries) Length() int { return math.MaxInt32 } +func (a NumberSeries) Clone() NumberSeries { + return a +} + var _ Series = NumberSeries(0) type AddSeriesResult struct { @@ -601,7 +635,7 @@ func Array(a Series, limit ...int) (result []float64) { if len(limit) > 0 { l = limit[0] } - if l < a.Length() { + if l > a.Length() { l = a.Length() } result = make([]float64, l) @@ -621,7 +655,7 @@ func Reverse(a Series, limit ...int) (result Float64Slice) { if len(limit) > 0 { l = limit[0] } - if l < a.Length() { + if l > a.Length() { l = a.Length() } result = make([]float64, l) @@ -817,6 +851,17 @@ func Correlation(a Series, b Series, length int, method ...CorrFunc) float64 { return runner(a, b, length) } +// similar to pandas.Series.autocorr() function. +// +// The method computes the Pearson correlation between Series and shifted itself +func AutoCorrelation(a Series, length int, lags ...int) float64 { + lag := 1 + if len(lags) > 0 { + lag = lags[0] + } + return Pearson(a, Shift(a, lag), length) +} + // similar to pandas.Series.cov() function with ddof=0 // // Compute covariance with Series @@ -1118,4 +1163,47 @@ func (l *LogisticRegressionModel) Predict(x []float64) float64 { return sigmoid(z + l.Gradient) } +type Canvas struct { + chart.Chart + Interval Interval +} + +func NewCanvas(title string, interval Interval) *Canvas { + valueFormatter := chart.TimeValueFormatter + if interval.Minutes() > 24 * 60 { + valueFormatter = chart.TimeDateValueFormatter + } else if interval.Minutes() > 60 { + valueFormatter = chart.TimeHourValueFormatter + } else { + valueFormatter = chart.TimeMinuteValueFormatter + } + out := &Canvas { + Chart: chart.Chart { + Title: title, + XAxis: chart.XAxis{ + ValueFormatter: valueFormatter, + }, + }, + Interval: interval, + } + out.Chart.Elements = []chart.Renderable{ + chart.LegendLeft(&out.Chart), + } + return out +} + +func (canvas *Canvas) Plot(tag string, a Series, endTime Time, length int) { + var timeline []time.Time + e := endTime.Time() + for i := length - 1; i >= 0; i-- { + shiftedT := e.Add(-time.Duration(i * canvas.Interval.Minutes()) * time.Minute) + timeline = append(timeline, shiftedT) + } + canvas.Series = append(canvas.Series, chart.TimeSeries{ + Name: tag, + YValues: Reverse(a, length), + XValues: timeline, + }) +} + // TODO: ta.linreg diff --git a/pkg/types/indicator_test.go b/pkg/types/indicator_test.go index 830b7b583..f1acbb91c 100644 --- a/pkg/types/indicator_test.go +++ b/pkg/types/indicator_test.go @@ -1,9 +1,13 @@ package types import ( + //"os" + "testing" + "time" + "github.com/stretchr/testify/assert" "gonum.org/v1/gonum/stat" - "testing" + "github.com/wcharczuk/go-chart/v2" ) func TestFloat(t *testing.T) { @@ -144,3 +148,23 @@ func TestDot(t *testing.T) { out3 := Dot(3., &a, 2) assert.InDelta(t, out2, out3, 0.001) } + +func TestClone(t *testing.T) { + a := NewQueue(3) + a.Update(3.) + b := Clone(a) + b.Update(4.) + assert.Equal(t, a.Last(), 3.) + assert.Equal(t, b.Last(), 4.) +} + +func TestPlot(t *testing.T) { + ct := NewCanvas("test", Interval5m) + a := Float64Slice{200., 205., 230., 236} + ct.Plot("test", &a, Time(time.Now()), 4) + assert.Equal(t, ct.Interval, Interval5m) + assert.Equal(t, ct.Series[0].(chart.TimeSeries).Len(), 4) + //f, _ := os.Create("output.png") + //defer f.Close() + //ct.Render(chart.PNG, f) +} diff --git a/pkg/types/kline.go b/pkg/types/kline.go index d510b8b48..4da1a8254 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -71,6 +71,26 @@ type KLine struct { Closed bool `json:"closed" db:"closed"` } +func (k *KLine) Copy(o *KLine) { + k.GID = o.GID + k.Exchange = o.Exchange + k.Symbol = o.Symbol + k.StartTime = o.StartTime + k.EndTime = o.EndTime + k.Interval = o.Interval + k.Open = o.Open + k.Close = o.Close + k.High = o.High + k.Low = o.Low + k.Volume = o.Volume + k.QuoteVolume = o.QuoteVolume + k.TakerBuyBaseAssetVolume = o.TakerBuyBaseAssetVolume + k.TakerBuyQuoteAssetVolume = o.TakerBuyQuoteAssetVolume + k.LastTradeID = o.LastTradeID + k.NumberOfTrades = o.NumberOfTrades + k.Closed = o.Closed +} + func (k KLine) GetStartTime() Time { return k.StartTime } diff --git a/pkg/types/seriesbase_imp.go b/pkg/types/seriesbase_imp.go index 0e29aa4e3..41675f262 100644 --- a/pkg/types/seriesbase_imp.go +++ b/pkg/types/seriesbase_imp.go @@ -121,6 +121,10 @@ func (s *SeriesBase) Correlation(b Series, length int, method ...CorrFunc) float return Correlation(s, b, length, method...) } +func (s *SeriesBase) AutoCorrelation(length int, lag ...int) float64 { + return AutoCorrelation(s, length, lag...) +} + func (s *SeriesBase) Rank(length int) SeriesExtend { return Rank(s, length) }