diff --git a/config/drift.yaml b/config/drift.yaml new file mode 100644 index 000000000..2b5c8b8c4 --- /dev/null +++ b/config/drift.yaml @@ -0,0 +1,38 @@ +--- +sessions: + binance: + exchange: binance + futures: true + envVarPrefix: binance + heikinAshi: false + +exchangeStrategies: + +- on: binance + drift: + symbol: ETHUSDT + # kline interval for indicators + interval: 15m + +sync: + userDataStream: + trades: true + filledOrders: true + sessions: + - binance + symbols: + - ETHUSDT + +backtest: + startTime: "2022-04-01" + endTime: "2022-06-18" + symbols: + - ETHUSDT + sessions: [binance] + accounts: + binance: + #makerFeeRate: 0 + #takerFeeRate: 15 + balances: + ETH: 10.0 + USDT: 5000.0 diff --git a/pkg/cmd/builtin.go b/pkg/cmd/builtin.go index 84bfc5647..2bb81b931 100644 --- a/pkg/cmd/builtin.go +++ b/pkg/cmd/builtin.go @@ -33,4 +33,5 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/xmaker" _ "github.com/c9s/bbgo/pkg/strategy/xnav" _ "github.com/c9s/bbgo/pkg/strategy/xpuremaker" + _ "github.com/c9s/bbgo/pkg/strategy/drift" ) diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go new file mode 100644 index 000000000..1053c2a02 --- /dev/null +++ b/pkg/strategy/drift/strategy.go @@ -0,0 +1,231 @@ +package drift + +import ( + "context" + "fmt" + "os" + "sync" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" +) + +const ID = "drift" + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type Strategy struct { + + Symbol string `json:"symbol"` + + bbgo.StrategyController + types.Market + types.IntervalWindow + + *bbgo.Graceful + *bbgo.Environment + *types.Position + *types.ProfitStats + *types.TradeStats + + drift types.UpdatableSeriesExtend + atr *indicator.ATR + midPrice fixedpoint.Value + lock sync.RWMutex + + Session *bbgo.ExchangeSession + *bbgo.GeneralOrderExecutor +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: s.Interval, + }) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: types.Interval1m, + }) + + if !bbgo.IsBackTesting { + session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) + } +} + +var Three fixedpoint.Value = fixedpoint.NewFromInt(3) + +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 +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + instanceID := s.InstanceID() + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + if s.TradeStats == nil { + s.TradeStats = &types.TradeStats{} + } + + // StrategyController + s.Status = types.StrategyStatusRunning + + s.OnSuspend(func() { + _ = s.GeneralOrderExecutor.GracefulCancel(ctx) + }) + + s.OnEmergencyStop(func() { + _ = s.GeneralOrderExecutor.GracefulCancel(ctx) + _ = s.GeneralOrderExecutor.ClosePosition(ctx, fixedpoint.One) + }) + + s.Session = session + s.GeneralOrderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.GeneralOrderExecutor.BindEnvironment(s.Environment) + s.GeneralOrderExecutor.BindProfitStats(s.ProfitStats) + s.GeneralOrderExecutor.BindTradeStats(s.TradeStats) + s.GeneralOrderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(s) + }) + s.GeneralOrderExecutor.Bind() + + store, _ := session.MarketDataStore(s.Symbol) + + s.drift = &indicator.Drift{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: 3}} + s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: 34}} + s.atr.Bind(store) + + klines, ok := store.KLinesOfInterval(s.Interval) + if !ok { + log.Errorf("klines not exists") + return nil + } + for _, kline := range *klines { + s.drift.Update(kline.High.Add(kline.Low).Add(kline.Close).Div(Three).Float64()) + 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 + } + s.lock.Unlock() + } + }) + + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + if s.Status != types.StrategyStatusRunning { + return + } + if kline.Symbol != s.Symbol || kline.Interval != s.Interval { + return + } + hlc3 := kline.High.Add(kline.Low).Add(kline.Close).Div(Three) + s.drift.Update(hlc3.Float64()) + baseBalance, ok := s.Session.GetAccount().Balance(s.Market.BaseCurrency) + if !ok { + log.Errorf("unable to get baseBalance") + return + } + quoteBalance, ok := s.Session.GetAccount().Balance(s.Market.QuoteCurrency) + if !ok { + log.Errorf("unable to get quoteCurrency") + return + } + price := s.GetLastPrice() + if s.Position.IsClosed() || s.Position.IsDust(price) { + /*if s.drift.PercentageChange(2).Abs().Last() <= 0.5 { + return + }*/ + if s.drift.Last() <= 0 && s.drift.Index(1) > 0 { + _, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Price: price, + StopPrice: hlc3.Add(fixedpoint.NewFromFloat(s.atr.Last()/2)), + Quantity: baseBalance.Available, + }) + if err != nil { + log.WithError(err).Errorf("cannot place order") + return + } + } + if s.drift.Last() >= 0 && s.drift.Index(1) < 0 { + _, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Price: price, + StopPrice: hlc3.Sub(fixedpoint.NewFromFloat(s.atr.Last()/2)), + Quantity: quoteBalance.Available.Div(price), + }) + if err != nil { + log.WithError(err).Errorf("cannot place order") + return + } + } + } else { + if (s.drift.Last() <= 0 && s.drift.Index(1) > 0) || + (s.drift.Last() >= 0 && s.drift.Index(1) < 0 ) { + s.GeneralOrderExecutor.ClosePosition(ctx, fixedpoint.One) + } + } + }) + + s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) + wg.Done() + }) + return nil +} diff --git a/pkg/strategy/ewoDgtrd/strategy.go b/pkg/strategy/ewoDgtrd/strategy.go index 1d2e2aef3..668cd69e2 100644 --- a/pkg/strategy/ewoDgtrd/strategy.go +++ b/pkg/strategy/ewoDgtrd/strategy.go @@ -15,6 +15,7 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" ) const ID = "ewo_dgtrd" @@ -114,11 +115,6 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } } -type UpdatableSeries interface { - types.Series - Update(value float64) -} - // Refer: https://tw.tradingview.com/script/XZyG5SOx-CCI-Stochastic-and-a-quick-lesson-on-Scalping-Trading-Systems/ type CCISTOCH struct { cci *indicator.CCI @@ -180,8 +176,8 @@ func (inc *CCISTOCH) SellSignal() bool { } type VWEMA struct { - PV UpdatableSeries - V UpdatableSeries + PV types.UpdatableSeries + V types.UpdatableSeries } func (inc *VWEMA) Last() float64 { @@ -1010,7 +1006,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bestAsk := ticker.Sell var midPrice fixedpoint.Value - if tryLock(&s.lock) { + if util.TryLock(&s.lock) { if !bestAsk.IsZero() && !bestBid.IsZero() { s.midPrice = bestAsk.Add(bestBid).Div(types.Two) } else if !bestAsk.IsZero() { diff --git a/pkg/strategy/ewoDgtrd/trylock.go b/pkg/util/trylock.go similarity index 53% rename from pkg/strategy/ewoDgtrd/trylock.go rename to pkg/util/trylock.go index f3e6e551a..5913b76bd 100644 --- a/pkg/strategy/ewoDgtrd/trylock.go +++ b/pkg/util/trylock.go @@ -1,16 +1,16 @@ //go:build !go1.18 // +build !go1.18 -package ewoDgtrd +package util import "sync" -func tryLock(lock *sync.RWMutex) bool { +func TryLock(lock *sync.RWMutex) bool { lock.Lock() return true } -func tryRLock(lock *sync.RWMutex) bool { +func TryRLock(lock *sync.RWMutex) bool { lock.RLock() return true } diff --git a/pkg/strategy/ewoDgtrd/trylock_18.go b/pkg/util/trylock_18.go similarity index 51% rename from pkg/strategy/ewoDgtrd/trylock_18.go rename to pkg/util/trylock_18.go index 1511766ae..9e9323789 100644 --- a/pkg/strategy/ewoDgtrd/trylock_18.go +++ b/pkg/util/trylock_18.go @@ -1,14 +1,14 @@ //go:build go1.18 // +build go1.18 -package ewoDgtrd +package util import "sync" -func tryLock(lock *sync.RWMutex) bool { +func TryLock(lock *sync.RWMutex) bool { return lock.TryLock() } -func tryRLock(lock *sync.RWMutex) bool { +func TryRLock(lock *sync.RWMutex) bool { return lock.TryRLock() }