diff --git a/config/elliottwave.yaml b/config/elliottwave.yaml index 1d57a20dd..a2f37d1ce 100644 --- a/config/elliottwave.yaml +++ b/config/elliottwave.yaml @@ -24,17 +24,17 @@ exchangeStrategies: - on: binance elliottwave: minInterval: 1s - symbol: BNBBUSD + symbol: BTCUSDT limitOrder: true - quantity: 0.16 + #quantity: 0.16 # kline interval for indicators - interval: 1m + interval: 1s stoploss: 0.01% windowATR: 14 - windowQuick: 5 - windowSlow: 9 + windowQuick: 4 + windowSlow: 155 source: hl2 - pendingMinutes: 10 + pendingMinInterval: 5 useHeikinAshi: true drawGraph: true @@ -47,12 +47,12 @@ exchangeStrategies: # when farest price from entry goes over that ratio, start using the callback ratio accordingly to do trailingstop #trailingActivationRatio: [0.01, 0.016, 0.05] #trailingActivationRatio: [0.001, 0.0081, 0.022] - trailingActivationRatio: [0.0017, 0.01, 0.015] - #trailingActivationRatio: [] - #trailingCallbackRate: [] + #trailingActivationRatio: [0.0017, 0.01, 0.015] + trailingActivationRatio: [] + trailingCallbackRate: [] #trailingCallbackRate: [0.002, 0.01, 0.1] #trailingCallbackRate: [0.0004, 0.0009, 0.018] - trailingCallbackRate: [0.0006, 0.0019, 0.006] + #trailingCallbackRate: [0.0006, 0.0019, 0.006] #exits: # - roiStopLoss: @@ -108,13 +108,13 @@ sync: sessions: - binance symbols: - - BNBBUSD + - BTCUSDT backtest: - startTime: "2022-09-01" - endTime: "2022-09-30" + startTime: "2022-10-15" + endTime: "2022-10-19" symbols: - - BNBBUSD + - BTCUSDT sessions: [binance] syncSecKLines: true accounts: @@ -122,5 +122,5 @@ backtest: makerFeeRate: 0.000 takerFeeRate: 0.000 balances: - BNB: 0 - BUSD: 100 + BTC: 0 + USDT: 100 diff --git a/pkg/bbgo/serialmarketdatastore.go b/pkg/bbgo/serialmarketdatastore.go index a9ee20e29..588c2ad8f 100644 --- a/pkg/bbgo/serialmarketdatastore.go +++ b/pkg/bbgo/serialmarketdatastore.go @@ -46,7 +46,7 @@ func (store *SerialMarketDataStore) Subscribe(interval types.Interval) { func (store *SerialMarketDataStore) BindStream(ctx context.Context, stream types.Stream) { if store.UseAggTrade { if IsBackTesting { - log.Errorf("Right now in backtesting, aggTrade event is not yet supported. Use OnKLineClosed instead.") + log.Errorf("right now in backtesting, aggTrade event is not yet supported. Use OnKLineClosed instead.") stream.OnKLineClosed(store.handleKLineClosed) return } @@ -85,6 +85,10 @@ func (store *SerialMarketDataStore) handleMarketTrade(trade types.Trade) { func (store *SerialMarketDataStore) tickerProcessor(ctx context.Context) { duration := store.MinInterval.Duration() + relativeTime := time.Now().UnixNano() % int64(duration) + waitTime := int64(duration) - relativeTime + ch := time.After(time.Duration(waitTime)) + <-ch intervalCloseTicker := time.NewTicker(duration) defer intervalCloseTicker.Stop() diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index b80e15dc9..b8cecdea5 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -166,52 +166,6 @@ func (trader *Trader) SetRiskControls(riskControls *RiskControls) { trader.riskControls = riskControls } -func (trader *Trader) Subscribe() { - // pre-subscribe the data - for sessionName, strategies := range trader.exchangeStrategies { - session := trader.environment.sessions[sessionName] - for _, strategy := range strategies { - if defaulter, ok := strategy.(StrategyDefaulter); ok { - if err := defaulter.Defaults(); err != nil { - panic(err) - } - } - - if initializer, ok := strategy.(StrategyInitializer); ok { - if err := initializer.Initialize(); err != nil { - panic(err) - } - } - - if subscriber, ok := strategy.(ExchangeSessionSubscriber); ok { - subscriber.Subscribe(session) - } else { - log.Errorf("strategy %s does not implement ExchangeSessionSubscriber", strategy.ID()) - } - } - } - - for _, strategy := range trader.crossExchangeStrategies { - if defaulter, ok := strategy.(StrategyDefaulter); ok { - if err := defaulter.Defaults(); err != nil { - panic(err) - } - } - - if initializer, ok := strategy.(StrategyInitializer); ok { - if err := initializer.Initialize(); err != nil { - panic(err) - } - } - - if subscriber, ok := strategy.(CrossExchangeSessionSubscriber); ok { - subscriber.CrossSubscribe(trader.environment.sessions) - } else { - log.Errorf("strategy %s does not implement CrossExchangeSessionSubscriber", strategy.ID()) - } - } -} - func (trader *Trader) RunSingleExchangeStrategy(ctx context.Context, strategy SingleExchangeStrategy, session *ExchangeSession, orderExecutor OrderExecutor) error { if v, ok := strategy.(StrategyValidator); ok { if err := v.Validate(); err != nil { @@ -262,7 +216,7 @@ func (trader *Trader) RunAllSingleExchangeStrategy(ctx context.Context) error { return nil } -func (trader *Trader) injectFields(ctx context.Context) error { +func (trader *Trader) injectFieldsAndSubscribe(ctx context.Context) error { // load and run Session strategies for sessionName, strategies := range trader.exchangeStrategies { var session = trader.environment.sessions[sessionName] @@ -350,6 +304,24 @@ func (trader *Trader) injectFields(ctx context.Context) error { if err := trader.injectCommonServices(strategy); err != nil { return err } + + if defaulter, ok := strategy.(StrategyDefaulter); ok { + if err := defaulter.Defaults(); err != nil { + return err + } + } + + if initializer, ok := strategy.(StrategyInitializer); ok { + if err := initializer.Initialize(); err != nil { + return err + } + } + + if subscriber, ok := strategy.(CrossExchangeSessionSubscriber); ok { + subscriber.CrossSubscribe(trader.environment.sessions) + } else { + log.Errorf("strategy %s does not implement CrossExchangeSessionSubscriber", strategy.ID()) + } } return nil @@ -361,7 +333,7 @@ func (trader *Trader) Run(ctx context.Context) error { // trader.environment.Connect will call interact.Start interact.AddCustomInteraction(NewCoreInteraction(trader.environment, trader)) - if err := trader.injectFields(ctx); err != nil { + if err := trader.injectFieldsAndSubscribe(ctx); err != nil { return err } diff --git a/pkg/notifier/telegramnotifier/telegram.go b/pkg/notifier/telegramnotifier/telegram.go index e2018b4ca..82bab5d24 100644 --- a/pkg/notifier/telegramnotifier/telegram.go +++ b/pkg/notifier/telegramnotifier/telegram.go @@ -15,7 +15,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -var apiLimiter = rate.NewLimiter(rate.Every(1*time.Second), 1) +var apiLimiter = rate.NewLimiter(rate.Every(50*time.Millisecond), 20) var log = logrus.WithField("service", "telegram") diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index 1113e9433..40d62c24b 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -98,10 +98,10 @@ type Strategy struct { SmootherWindow int `json:"smootherWindow"` FisherTransformWindow int `json:"fisherTransformWindow"` ATRWindow int `json:"atrWindow"` - PendingMinutes int `json:"pendingMinutes" modifiable:"true"` // if order not be traded for pendingMinutes of time, cancel it. - NoRebalance bool `json:"noRebalance" modifiable:"true"` // disable rebalance - TrendWindow int `json:"trendWindow"` // trendLine is used for rebalancing the position. When trendLine goes up, hold base, otherwise hold quote - RebalanceFilter float64 `json:"rebalanceFilter" modifiable:"true"` // beta filter on the Linear Regression of trendLine + PendingMinInterval int `json:"pendingMinInterval" modifiable:"true"` // if order not be traded for pendingMinInterval of time, cancel it. + NoRebalance bool `json:"noRebalance" modifiable:"true"` // disable rebalance + TrendWindow int `json:"trendWindow"` // trendLine is used for rebalancing the position. When trendLine goes up, hold base, otherwise hold quote + RebalanceFilter float64 `json:"rebalanceFilter" modifiable:"true"` // beta filter on the Linear Regression of trendLine TrailingCallbackRate []float64 `json:"trailingCallbackRate" modifiable:"true"` TrailingActivationRatio []float64 `json:"trailingActivationRatio" modifiable:"true"` @@ -276,7 +276,7 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64) (int, e continue } log.Warnf("%v | counter: %d, system: %d", order, s.orderPendingCounter[order.OrderID], s.counter) - if s.counter-s.orderPendingCounter[order.OrderID] > s.PendingMinutes { + if s.counter-s.orderPendingCounter[order.OrderID] > s.PendingMinInterval { toCancel = true } else if order.Side == types.SideTypeBuy { // 75% of the probability diff --git a/pkg/strategy/elliottwave/draw.go b/pkg/strategy/elliottwave/draw.go index 2bf767ce9..6378839dc 100644 --- a/pkg/strategy/elliottwave/draw.go +++ b/pkg/strategy/elliottwave/draw.go @@ -13,47 +13,49 @@ import ( func (s *Strategy) InitDrawCommands(store *bbgo.SerialMarketDataStore, profit, cumProfit types.Series) { bbgo.RegisterCommand("/draw", "Draw Indicators", func(reply interact.Reply) { - canvas := s.DrawIndicators(store) - if canvas == nil { - reply.Message("cannot render indicators") - return - } - var buffer bytes.Buffer - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render indicators in ewo") - reply.Message(fmt.Sprintf("[error] cannot render indicators in ewo: %v", err)) - return - } - bbgo.SendPhoto(&buffer) + go func() { + canvas := s.DrawIndicators(store) + if canvas == nil { + reply.Message("cannot render indicators") + return + } + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render indicators in ewo") + reply.Message(fmt.Sprintf("[error] cannot render indicators in ewo: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }() }) bbgo.RegisterCommand("/pnl", "Draw PNL(%) per trade", func(reply interact.Reply) { - canvas := s.DrawPNL(profit) - var buffer bytes.Buffer - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render pnl in drift") - reply.Message(fmt.Sprintf("[error] cannot render pnl in ewo: %v", err)) - return - } - bbgo.SendPhoto(&buffer) + go func() { + canvas := s.DrawPNL(profit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render pnl in ewo") + reply.Message(fmt.Sprintf("[error] cannot render pnl in ewo: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }() }) bbgo.RegisterCommand("/cumpnl", "Draw Cummulative PNL(Quote)", func(reply interact.Reply) { - canvas := s.DrawCumPNL(cumProfit) - var buffer bytes.Buffer - if err := canvas.Render(chart.PNG, &buffer); err != nil { - log.WithError(err).Errorf("cannot render cumpnl in drift") - reply.Message(fmt.Sprintf("[error] canot render cumpnl in drift: %v", err)) - return - } - bbgo.SendPhoto(&buffer) + go func() { + canvas := s.DrawCumPNL(cumProfit) + var buffer bytes.Buffer + if err := canvas.Render(chart.PNG, &buffer); err != nil { + log.WithError(err).Errorf("cannot render cumpnl in ewo") + reply.Message(fmt.Sprintf("[error] canot render cumpnl in ewo: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }() }) } func (s *Strategy) DrawIndicators(store *bbgo.SerialMarketDataStore) *types.Canvas { - klines, ok := store.KLinesOfInterval(types.Interval1m) - if !ok { - return nil - } - time := (*klines)[len(*klines)-1].StartTime + time := types.Time(s.startTime) canvas := types.NewCanvas(s.InstanceID(), s.Interval) Length := s.priceLines.Length() if Length > 300 { @@ -109,10 +111,10 @@ func (s *Strategy) Draw(store *bbgo.SerialMarketDataStore, profit, cumProfit typ log.WithError(err).Errorf("cannot create on path " + s.GraphIndicatorPath) return } - defer f.Close() if err = canvas.Render(chart.PNG, f); err != nil { log.WithError(err).Errorf("cannot render elliottwave") } + f.Close() canvas = s.DrawPNL(profit) f, err = os.Create(s.GraphPNLPath) @@ -120,19 +122,19 @@ func (s *Strategy) Draw(store *bbgo.SerialMarketDataStore, profit, cumProfit typ log.WithError(err).Errorf("cannot create on path " + s.GraphPNLPath) return } - defer f.Close() if err = canvas.Render(chart.PNG, f); err != nil { log.WithError(err).Errorf("cannot render pnl") return } + f.Close() canvas = s.DrawCumPNL(cumProfit) f, err = os.Create(s.GraphCumPNLPath) if err != nil { log.WithError(err).Errorf("cannot create on path " + s.GraphCumPNLPath) return } - defer f.Close() if err = canvas.Render(chart.PNG, f); err != nil { log.WithError(err).Errorf("cannot render cumpnl") } + f.Close() } diff --git a/pkg/strategy/elliottwave/strategy.go b/pkg/strategy/elliottwave/strategy.go index 7128affcc..f135d4739 100644 --- a/pkg/strategy/elliottwave/strategy.go +++ b/pkg/strategy/elliottwave/strategy.go @@ -43,14 +43,14 @@ type Strategy struct { types.Market Session *bbgo.ExchangeSession - Interval types.Interval `json:"interval"` - MinInterval types.Interval `json:"minInterval"` - Stoploss fixedpoint.Value `json:"stoploss" modifiable:"true"` - WindowATR int `json:"windowATR"` - WindowQuick int `json:"windowQuick"` - WindowSlow int `json:"windowSlow"` - PendingMinutes int `json:"pendingMinutes" modifiable:"true"` - UseHeikinAshi bool `json:"useHeikinAshi"` + Interval types.Interval `json:"interval"` + MinInterval types.Interval `json:"minInterval"` + Stoploss fixedpoint.Value `json:"stoploss" modifiable:"true"` + WindowATR int `json:"windowATR"` + WindowQuick int `json:"windowQuick"` + WindowSlow int `json:"windowSlow"` + PendingMinInterval int `json:"pendingMinInterval" modifiable:"true"` + UseHeikinAshi bool `json:"useHeikinAshi"` // whether to draw graph or not by the end of backtest DrawGraph bool `json:"drawGraph"` @@ -196,7 +196,7 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef float64) int { } log.Warnf("%v | counter: %d, system: %d", order, s.orderPendingCounter[order.OrderID], s.counter) toCancel := false - if s.counter-s.orderPendingCounter[order.OrderID] >= s.PendingMinutes { + if s.counter-s.orderPendingCounter[order.OrderID] >= s.PendingMinInterval { toCancel = true } else if order.Side == types.SideTypeBuy { if order.Price.Float64()+s.atr.Last()*2 <= pricef { @@ -211,7 +211,7 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef float64) int { panic("not supported side for the order") } if toCancel { - err := s.GeneralOrderExecutor.GracefulCancel(ctx, order) + err := s.GeneralOrderExecutor.CancelNoWait(ctx, order) if err == nil { delete(s.orderPendingCounter, order.OrderID) } else { @@ -235,6 +235,9 @@ func (s *Strategy) trailingCheck(price float64, direction string) bool { s.lowestPrice = price } isShort := direction == "short" + if isShort && s.sellPrice == 0 || !isShort && s.buyPrice == 0 { + return false + } for i := len(s.TrailingCallbackRate) - 1; i >= 0; i-- { trailingCallbackRate := s.TrailingCallbackRate[i] trailingActivationRatio := s.TrailingActivationRatio[i] @@ -244,7 +247,7 @@ func (s *Strategy) trailingCheck(price float64, direction string) bool { } } else { if (s.highestPrice-s.buyPrice)/s.buyPrice > trailingActivationRatio { - return (s.highestPrice-price)/price > trailingCallbackRate + return (s.highestPrice-price)/s.buyPrice > trailingCallbackRate } } } @@ -351,15 +354,19 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.highestPrice = 0 s.lowestPrice = 0 } else if s.Position.IsLong() { - s.buyPrice = price + s.buyPrice = s.Position.ApproximateAverageCost.Float64() s.sellPrice = 0 - s.highestPrice = s.buyPrice + s.highestPrice = math.Max(s.buyPrice, s.highestPrice) s.lowestPrice = 0 } else { - s.sellPrice = price + s.sellPrice = s.Position.ApproximateAverageCost.Float64() s.buyPrice = 0 s.highestPrice = 0 - s.lowestPrice = s.sellPrice + if s.lowestPrice == 0 { + s.lowestPrice = s.sellPrice + } else { + s.lowestPrice = math.Min(s.lowestPrice, s.sellPrice) + } } }) s.initTickerFunctions() @@ -477,15 +484,18 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { bull := kline.Close.Compare(kline.Open) > 0 balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() - bbgo.Notify("source: %.4f, price: %.4f lowest: %.4f highest: %.4f sp %.4f bp %.4f", sourcef, pricef, s.lowestPrice, s.highestPrice, s.sellPrice, s.buyPrice) - bbgo.Notify("balances: [Total] %v %s [Base] %s(%v %s) [Quote] %s", - s.CalcAssetValue(price), - s.Market.QuoteCurrency, - balances[s.Market.BaseCurrency].String(), - balances[s.Market.BaseCurrency].Total().Mul(price), - s.Market.QuoteCurrency, - balances[s.Market.QuoteCurrency].String(), - ) + startTime := kline.StartTime.Time() + if startTime.Round(time.Second) == startTime.Round(time.Minute) { + bbgo.Notify("source: %.4f, price: %.4f lowest: %.4f highest: %.4f sp %.4f bp %.4f", sourcef, pricef, s.lowestPrice, s.highestPrice, s.sellPrice, s.buyPrice) + bbgo.Notify("balances: [Total] %v %s [Base] %s(%v %s) [Quote] %s", + s.CalcAssetValue(price), + s.Market.QuoteCurrency, + balances[s.Market.BaseCurrency].String(), + balances[s.Market.BaseCurrency].Total().Mul(price), + s.Market.QuoteCurrency, + balances[s.Market.QuoteCurrency].String(), + ) + } shortCondition := ewo[0] < ewo[1] && ewo[1] >= ewo[2] && (ewo[1] <= ewo[2] || ewo[2] >= ewo[3]) || s.sellPrice == 0 && ewo[0] < ewo[1] && ewo[1] < ewo[2] longCondition := ewo[0] > ewo[1] && ewo[1] <= ewo[2] && (ewo[1] >= ewo[2] || ewo[2] <= ewo[3]) || s.buyPrice == 0 && ewo[0] > ewo[1] && ewo[1] > ewo[2] @@ -493,18 +503,19 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { exitShortCondition := s.sellPrice > 0 && !shortCondition && s.sellPrice*(1.+stoploss) <= highf || s.sellPrice+atr <= highf || s.trailingCheck(highf, "short") exitLongCondition := s.buyPrice > 0 && !longCondition && s.buyPrice*(1.-stoploss) >= lowf || s.buyPrice-atr >= lowf || s.trailingCheck(lowf, "long") - if exitShortCondition || exitLongCondition { - if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { - log.WithError(err).Errorf("cannot cancel orders") + if exitShortCondition || exitLongCondition || (longCondition && bull) || (shortCondition && !bull) { + if hold := s.smartCancel(ctx, pricef); hold > 0 { return } + } else { + s.smartCancel(ctx, pricef) + return + } + if exitShortCondition || exitLongCondition { s.ClosePosition(ctx, fixedpoint.One) } + if longCondition && bull { - if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { - log.WithError(err).Errorf("cannot cancel orders") - return - } if source.Compare(price) > 0 { source = price } @@ -527,10 +538,6 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { return } if shortCondition && !bull { - if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { - log.WithError(err).Errorf("cannot cancel orders") - return - } if source.Compare(price) < 0 { source = price }