diff --git a/config/drift.yaml b/config/drift.yaml index e30c9d3c5..03d62343c 100644 --- a/config/drift.yaml +++ b/config/drift.yaml @@ -16,6 +16,8 @@ exchangeStrategies: interval: 15m window: 3 stoploss: 2% + source: hl2 + predictOffset: 5 #exits: #- roiStopLoss: # percentage: 0.8% @@ -48,7 +50,7 @@ sync: - ETHUSDT backtest: - startTime: "2022-04-01" + startTime: "2022-01-01" endTime: "2022-06-18" symbols: - ETHUSDT diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 2ea93f59b..6567171c5 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -114,6 +114,9 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders .. // GracefulCancelActiveOrderBook cancels the orders from the active orderbook. func (e *GeneralOrderExecutor) GracefulCancelActiveOrderBook(ctx context.Context, activeOrders *ActiveOrderBook) error { + if activeOrders.NumOfOrders() == 0 { + return nil + } if err := activeOrders.GracefulCancel(ctx, e.session.Exchange); err != nil { return fmt.Errorf("graceful cancel order error: %w", err) } diff --git a/pkg/indicator/drift.go b/pkg/indicator/drift.go index acac77f0b..32490d3ec 100644 --- a/pkg/indicator/drift.go +++ b/pkg/indicator/drift.go @@ -15,7 +15,7 @@ type Drift struct { types.IntervalWindow chng *types.Queue Values types.Float64Slice - SMA *SMA + MA types.UpdatableSeriesExtend LastValue float64 UpdateCallbacks []func(value float64) @@ -24,7 +24,9 @@ type Drift struct { func (inc *Drift) Update(value float64) { if inc.chng == nil { inc.SeriesBase.Series = inc - inc.SMA = &SMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: inc.Window}} + if inc.MA == nil { + inc.MA = &SMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: inc.Window}} + } inc.chng = types.NewQueue(inc.Window) inc.LastValue = value return @@ -36,11 +38,11 @@ func (inc *Drift) Update(value float64) { chng = math.Log(value / inc.LastValue) inc.LastValue = value } - inc.SMA.Update(chng) + inc.MA.Update(chng) inc.chng.Update(chng) if inc.chng.Length() >= inc.Window { stdev := types.Stdev(inc.chng, inc.Window) - drift := inc.SMA.Last() - stdev*stdev*0.5 + drift := inc.MA.Last() - stdev*stdev*0.5 inc.Values.Push(drift) } } @@ -50,7 +52,7 @@ func (inc *Drift) Clone() (out *Drift) { IntervalWindow: inc.IntervalWindow, chng: inc.chng.Clone(), Values: inc.Values[:], - SMA: inc.SMA.Clone().(*SMA), + MA: types.Clone(inc.MA), LastValue: inc.LastValue, } out.SeriesBase.Series = out diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index 259c98b6f..0467283df 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -2,10 +2,14 @@ package drift import ( "context" + "encoding/json" "fmt" + "math" "os" + "strings" "sync" + "github.com/fatih/color" "github.com/sirupsen/logrus" "github.com/wcharczuk/go-chart/v2" @@ -19,11 +23,17 @@ import ( const ID = "drift" var log = logrus.WithField("strategy", ID) +var Four fixedpoint.Value = fixedpoint.NewFromInt(4) +var Three fixedpoint.Value = fixedpoint.NewFromInt(3) +var Two fixedpoint.Value = fixedpoint.NewFromInt(2) +var Delta fixedpoint.Value = fixedpoint.NewFromFloat(0.01) func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } +type SourceFunc func(*types.KLine) fixedpoint.Value + type Strategy struct { Symbol string `json:"symbol"` @@ -41,13 +51,30 @@ type Strategy struct { midPrice fixedpoint.Value lock sync.RWMutex - Stoploss fixedpoint.Value `json:"stoploss"` - CanvasPath string `json:"canvasPath"` + Source string `json:"source"` + Stoploss fixedpoint.Value `json:"stoploss"` + CanvasPath string `json:"canvasPath"` + PredictOffset int `json:"predictOffset"` ExitMethods bbgo.ExitMethodSet `json:"exits"` Session *bbgo.ExchangeSession *bbgo.GeneralOrderExecutor - *bbgo.ActiveOrderBook + + getLastPrice func() fixedpoint.Value +} + +func (s *Strategy) Print() { + b, _ := json.MarshalIndent(s.ExitMethods, " ", " ") + hiyellow := color.New(color.FgHiYellow).FprintfFunc() + hiyellow(os.Stderr, "------ %s Settings ------\n", s.InstanceID()) + hiyellow(os.Stderr, "canvasPath: %s\n", s.CanvasPath) + hiyellow(os.Stderr, "source: %s\n", s.Source) + hiyellow(os.Stderr, "stoploss: %v\n", s.Stoploss) + hiyellow(os.Stderr, "predictOffset: %d\n", s.PredictOffset) + hiyellow(os.Stderr, "exits:\n %s\n", string(b)) + hiyellow(os.Stderr, "symbol: %s\n", s.Symbol) + hiyellow(os.Stderr, "interval: %s\n", s.Interval) + hiyellow(os.Stderr, "window: %d\n", s.Window) } func (s *Strategy) ID() string { @@ -72,35 +99,6 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { s.ExitMethods.SetAndSubscribe(session, s) } -var Three fixedpoint.Value = fixedpoint.NewFromInt(3) -var Two fixedpoint.Value = fixedpoint.NewFromInt(2) - -func (s *Strategy) GetLastPrice() (lastPrice fixedpoint.Value) { - var ok bool - if s.Environment.IsBackTesting() { - lastPrice, ok = s.Session.LastPrice(s.Symbol) - if !ok { - log.Error("cannot get lastprice") - return lastPrice - } - } else { - s.lock.RLock() - if s.midPrice.IsZero() { - lastPrice, ok = s.Session.LastPrice(s.Symbol) - if !ok { - log.Error("cannot get lastprice") - return lastPrice - } - } else { - lastPrice = s.midPrice - } - s.lock.RUnlock() - } - return lastPrice -} - -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 { @@ -109,7 +107,7 @@ func (s *Strategy) ClosePosition(ctx context.Context) (*types.Order, bool) { order.TimeInForce = "" balances := s.Session.GetAccount().Balances() baseBalance := balances[s.Market.BaseCurrency].Available - price := s.GetLastPrice() + price := s.getLastPrice() if order.Side == types.SideTypeBuy { quoteAmount := balances[s.Market.QuoteCurrency].Available.Div(price) if order.Quantity.Compare(quoteAmount) > 0 { @@ -131,6 +129,38 @@ func (s *Strategy) ClosePosition(ctx context.Context) (*types.Order, bool) { } } +func (s *Strategy) SourceFuncGenerator() SourceFunc { + switch strings.ToLower(s.Source) { + case "close": + return func(kline *types.KLine) fixedpoint.Value { return kline.Close } + case "high": + return func(kline *types.KLine) fixedpoint.Value { return kline.High } + case "low": + return func(kline *types.KLine) fixedpoint.Value { return kline.Low } + case "hl2": + return func(kline *types.KLine) fixedpoint.Value { + return kline.High.Add(kline.Low).Div(Two) + } + case "hlc3": + return func(kline *types.KLine) fixedpoint.Value { + return kline.High.Add(kline.Low).Add(kline.Close).Div(Three) + } + case "ohlc4": + return func(kline *types.KLine) fixedpoint.Value { + return kline.Open.Add(kline.High).Add(kline.Low).Add(kline.Close).Div(Four) + } + case "open": + return func(kline *types.KLine) fixedpoint.Value { return kline.Open } + case "": + return func(kline *types.KLine) fixedpoint.Value { + log.Infof("source not set, use hl2 by default") + return kline.High.Add(kline.Low).Div(Two) + } + default: + panic(fmt.Sprintf("Unable to parse: %s", s.Source)) + } +} + func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { instanceID := s.InstanceID() if s.Position == nil { @@ -168,18 +198,15 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se for _, method := range s.ExitMethods { method.Bind(session, s.GeneralOrderExecutor) } - s.ActiveOrderBook = bbgo.NewActiveOrderBook(s.Symbol) - s.ActiveOrderBook.BindStream(session.UserDataStream) 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) - } + getSource := s.SourceFuncGenerator() - s.drift = &indicator.Drift{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.Window}} + s.drift = &indicator.Drift{ + MA: &indicator.SMA{IntervalWindow: s.IntervalWindow}, + IntervalWindow: s.IntervalWindow, + } s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: 14}} klines, ok := store.KLinesOfInterval(s.Interval) @@ -187,33 +214,57 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.Errorf("klines not exists") return nil } + + dynamicKLine := &types.KLine{} for _, kline := range *klines { source := getSource(&kline).Float64() s.drift.Update(source) s.atr.Update(kline.High.Float64(), kline.Low.Float64(), kline.Close.Float64()) } - session.MarketDataStream.OnBookTickerUpdate(func(ticker types.BookTicker) { - if s.Environment.IsBackTesting() { - return - } - bestBid := ticker.Buy - bestAsk := ticker.Sell - - if util.TryLock(&s.lock) { - if !bestAsk.IsZero() && !bestBid.IsZero() { - s.midPrice = bestAsk.Add(bestBid).Div(types.Two) - } else if !bestAsk.IsZero() { - s.midPrice = bestAsk - } else { - s.midPrice = bestBid + if s.Environment.IsBackTesting() { + s.getLastPrice = func() fixedpoint.Value { + lastPrice, ok := s.Session.LastPrice(s.Symbol) + if !ok { + log.Error("cannot get lastprice") } - s.lock.Unlock() + return lastPrice } - }) + } else { + session.MarketDataStream.OnBookTickerUpdate(func(ticker types.BookTicker) { + bestBid := ticker.Buy + bestAsk := ticker.Sell + + if util.TryLock(&s.lock) { + if !bestAsk.IsZero() && !bestBid.IsZero() { + s.midPrice = bestAsk.Add(bestBid).Div(Two) + } else if !bestAsk.IsZero() { + s.midPrice = bestAsk + } else { + s.midPrice = bestBid + } + s.lock.Unlock() + } + }) + s.getLastPrice = func() (lastPrice fixedpoint.Value) { + var ok bool + s.lock.RLock() + if s.midPrice.IsZero() { + lastPrice, ok = s.Session.LastPrice(s.Symbol) + if !ok { + log.Error("cannot get lastprice") + return lastPrice + } + } else { + lastPrice = s.midPrice + } + s.lock.RUnlock() + return lastPrice + } + } - dynamicKLine := &types.KLine{} priceLine := types.NewQueue(100) + stoploss := s.Stoploss.Float64() session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { if s.Status != types.StrategyStatusRunning { @@ -236,15 +287,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se 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) + driftPred = s.drift.Predict(s.PredictOffset) atr = s.atr.Last() - price := s.GetLastPrice() + price := s.getLastPrice() pricef := price.Float64() avg := s.Position.AverageCost.Float64() - stoploss := s.Stoploss.Float64() shortCondition := (driftPred <= 0 && drift[0] <= 0) longCondition := (driftPred >= 0 && drift[0] >= 0) @@ -253,30 +302,17 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se exitLongCondition := ((drift[1] > 0 && drift[0] < 0) || avg-atr/2 >= pricef || avg*(1.-stoploss) >= pricef) && (!s.Position.IsClosed() && !s.Position.IsDust(fixedpoint.Min(price, source))) && !shortCondition - 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 - } - } - _, _ = s.ClosePosition(ctx) - } - 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 - } + if exitShortCondition || exitLongCondition { + if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + return } _, _ = s.ClosePosition(ctx) } if shortCondition { - if s.ActiveOrderBook.NumOfOrders() > 0 { - if err := s.GeneralOrderExecutor.GracefulCancelActiveOrderBook(ctx, s.ActiveOrderBook); err != nil { - log.WithError(err).Errorf("cannot cancel orders") - return - } + if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + return } baseBalance, ok := s.Session.GetAccount().Balance(s.Market.BaseCurrency) if !ok { @@ -295,7 +331,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se Side: types.SideTypeSell, Type: types.OrderTypeLimitMaker, Price: source, - StopPrice: fixedpoint.NewFromFloat(sourcef + atr/2), + StopPrice: fixedpoint.NewFromFloat(math.Min(sourcef+atr/2, sourcef*(1.+stoploss))), Quantity: baseBalance.Available, }) if err != nil { @@ -304,11 +340,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } 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 err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + return } if source.Compare(price) > 0 { source = price @@ -322,15 +356,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se quoteBalance.Available.Div(source), source) { return } - 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: source, - StopPrice: fixedpoint.NewFromFloat(sourcef - atr/2), + StopPrice: fixedpoint.NewFromFloat(math.Max(sourcef-atr/2, sourcef*(1.-stoploss))), Quantity: quoteBalance.Available.Div(source), }) if err != nil { @@ -342,8 +373,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) + s.Print() canvas := types.NewCanvas(s.InstanceID(), s.Interval) - fmt.Println(dynamicKLine.StartTime, dynamicKLine.EndTime) mean := priceLine.Mean(100) highestPrice := priceLine.Minus(mean).Highest(100) highestDrift := s.drift.Highest(100) diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index 64d2f649b..66ff9f14e 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -3,11 +3,11 @@ package types import ( "fmt" "math" - "time" "reflect" + "time" - "gonum.org/v1/gonum/stat" "github.com/wcharczuk/go-chart/v2" + "gonum.org/v1/gonum/stat" ) // Super basic Series type that simply holds the float64 data @@ -46,8 +46,8 @@ func (inc *Queue) Length() int { } func (inc *Queue) Clone() *Queue { - out := &Queue { - arr: inc.arr[:], + out := &Queue{ + arr: inc.arr[:], size: inc.size, } out.SeriesBase.Series = out @@ -213,7 +213,7 @@ func Abs(a Series) SeriesExtend { var _ Series = &AbsResult{} -func Predict(a Series, lookback int, offset ...int) float64 { +func LinearRegression(a Series, lookback int) (alpha float64, beta float64) { if a.Length() < lookback { lookback = a.Length() } @@ -224,7 +224,12 @@ func Predict(a Series, lookback int, offset ...int) float64 { x[i] = float64(i) y[i] = a.Index(i) } - alpha, beta := stat.LinearRegression(x, y, weights, false) + alpha, beta = stat.LinearRegression(x, y, weights, false) + return +} + +func Predict(a Series, lookback int, offset ...int) float64 { + alpha, beta := LinearRegression(a, lookback) o := -1.0 if len(offset) > 0 { o = -float64(offset[0]) @@ -1167,15 +1172,15 @@ type Canvas struct { func NewCanvas(title string, interval Interval) *Canvas { valueFormatter := chart.TimeValueFormatter - if interval.Minutes() > 24 * 60 { + 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 { + out := &Canvas{ + Chart: chart.Chart{ Title: title, XAxis: chart.XAxis{ ValueFormatter: valueFormatter, @@ -1193,11 +1198,11 @@ 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) + shiftedT := e.Add(-time.Duration(i*canvas.Interval.Minutes()) * time.Minute) timeline = append(timeline, shiftedT) } canvas.Series = append(canvas.Series, chart.TimeSeries{ - Name: tag, + Name: tag, YValues: Reverse(a, length), XValues: timeline, })