diff --git a/config/trendtrader.yaml b/config/trendtrader.yaml new file mode 100644 index 000000000..30a166c0c --- /dev/null +++ b/config/trendtrader.yaml @@ -0,0 +1,50 @@ +persistence: + json: + directory: var/data + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sessions: + binance: + exchange: binance + envVarPrefix: binance +# futures: true + +exchangeStrategies: +- on: binance + trendtrader: + symbol: BTCBUSD + trendLine: + interval: 30m + pivotRightWindow: 40 + quantity: 1 + exits: + - trailingStop: + callbackRate: 1% + activationRatio: 1% + closePosition: 100% + minProfit: 15% + interval: 1m + side: buy + - trailingStop: + callbackRate: 1% + activationRatio: 1% + closePosition: 100% + minProfit: 15% + interval: 1m + side: sell + +backtest: + sessions: + - binance + startTime: "2021-01-01" + endTime: "2022-08-31" + symbols: + - BTCBUSD + accounts: + binance: + balances: + BTC: 1 + BUSD: 50_000.0 \ No newline at end of file diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index d10cc94f0..e46d2bbd2 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -29,6 +29,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/support" _ "github.com/c9s/bbgo/pkg/strategy/swing" _ "github.com/c9s/bbgo/pkg/strategy/techsignal" + _ "github.com/c9s/bbgo/pkg/strategy/trendtrader" _ "github.com/c9s/bbgo/pkg/strategy/wall" _ "github.com/c9s/bbgo/pkg/strategy/xbalance" _ "github.com/c9s/bbgo/pkg/strategy/xgap" diff --git a/pkg/strategy/trendtrader/strategy.go b/pkg/strategy/trendtrader/strategy.go new file mode 100644 index 000000000..c7680f1e1 --- /dev/null +++ b/pkg/strategy/trendtrader/strategy.go @@ -0,0 +1,134 @@ +package trendtrader + +import ( + "context" + "fmt" + "github.com/c9s/bbgo/pkg/dynamic" + "os" + "sync" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/sirupsen/logrus" +) + +const ID = "trendtrader" + +var one = fixedpoint.One +var zero = fixedpoint.Zero + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type IntervalWindowSetting struct { + types.IntervalWindow +} + +type Strategy struct { + Environment *bbgo.Environment + Symbol string `json:"symbol"` + Market types.Market + + types.IntervalWindow + + // persistence fields + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` + + activeOrders *bbgo.ActiveOrderBook + + TrendLine *TrendLine `json:"trendLine"` + + ExitMethods bbgo.ExitMethodSet `json:"exits"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + + // StrategyController + bbgo.StrategyController +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + //session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Trend.Interval}) + //session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) + + if s.TrendLine != nil { + dynamic.InheritStructValues(s.TrendLine, s) + s.TrendLine.Subscribe(session) + } + s.ExitMethods.SetAndSubscribe(session, s) +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + var 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.NewTradeStats(s.Symbol) + } + + // StrategyController + s.Status = types.StrategyStatusRunning + + s.OnSuspend(func() { + // Cancel active orders + _ = s.orderExecutor.GracefulCancel(ctx) + }) + + s.OnEmergencyStop(func() { + // Cancel active orders + _ = s.orderExecutor.GracefulCancel(ctx) + // Close 100% position + //_ = s.ClosePosition(ctx, fixedpoint.One) + }) + + // initial required information + s.session = session + + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.BindTradeStats(s.TradeStats) + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(s) + }) + s.orderExecutor.Bind() + s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) + + for _, method := range s.ExitMethods { + method.Bind(session, s.orderExecutor) + } + + if s.TrendLine != nil { + s.TrendLine.Bind(session, s.orderExecutor) + } + + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) + _ = s.orderExecutor.GracefulCancel(ctx) + }) + + return nil +} diff --git a/pkg/strategy/trendtrader/trend.go b/pkg/strategy/trendtrader/trend.go new file mode 100644 index 000000000..fb27c8568 --- /dev/null +++ b/pkg/strategy/trendtrader/trend.go @@ -0,0 +1,156 @@ +package trendtrader + +import ( + "context" + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type TrendLine struct { + Symbol string + Market types.Market `json:"-"` + types.IntervalWindow + + PivotRightWindow fixedpoint.Value `json:"pivotRightWindow"` + + // MarketOrder is the option to enable market order short. + MarketOrder bool `json:"marketOrder"` + + Quantity fixedpoint.Value `json:"quantity"` + + orderExecutor *bbgo.GeneralOrderExecutor + session *bbgo.ExchangeSession + activeOrders *bbgo.ActiveOrderBook + + pivotHigh *indicator.PivotHigh + pivotLow *indicator.PivotLow + + bbgo.QuantityOrAmount +} + +func (s *TrendLine) 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 s.pivot != nil { + // session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) + //} +} + +func (s *TrendLine) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + + position := orderExecutor.Position() + symbol := position.Symbol + standardIndicator := session.StandardIndicatorSet(s.Symbol) + s.pivotHigh = standardIndicator.PivotHigh(types.IntervalWindow{s.Interval, int(3. * s.PivotRightWindow.Float64()), int(s.PivotRightWindow.Float64())}) + s.pivotLow = standardIndicator.PivotLow(types.IntervalWindow{s.Interval, int(3. * s.PivotRightWindow.Float64()), int(s.PivotRightWindow.Float64())}) + + resistancePrices := types.NewQueue(3) + pivotHighDurationCounter := 0. + resistanceDuration := types.NewQueue(2) + supportPrices := types.NewQueue(3) + pivotLowDurationCounter := 0. + supportDuration := types.NewQueue(2) + + resistanceSlope := 0. + resistanceSlope1 := 0. + resistanceSlope2 := 0. + supportSlope := 0. + supportSlope1 := 0. + supportSlope2 := 0. + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + if s.pivotHigh.Last() != resistancePrices.Last() { + resistancePrices.Update(s.pivotHigh.Last()) + resistanceDuration.Update(pivotHighDurationCounter) + pivotHighDurationCounter = 0 + } else { + pivotHighDurationCounter++ + } + if s.pivotLow.Last() != supportPrices.Last() { + supportPrices.Update(s.pivotLow.Last()) + supportDuration.Update(pivotLowDurationCounter) + pivotLowDurationCounter = 0 + } else { + pivotLowDurationCounter++ + } + + if line(resistancePrices.Index(2), resistancePrices.Index(1), resistancePrices.Index(0)) < 0 { + resistanceSlope1 = (resistancePrices.Index(1) - resistancePrices.Index(2)) / resistanceDuration.Index(1) + resistanceSlope2 = (resistancePrices.Index(0) - resistancePrices.Index(1)) / resistanceDuration.Index(0) + + resistanceSlope = (resistanceSlope1 + resistanceSlope2) / 2. + } + if line(supportPrices.Index(2), supportPrices.Index(1), supportPrices.Index(0)) > 0 { + supportSlope1 = (supportPrices.Index(1) - supportPrices.Index(2)) / supportDuration.Index(1) + supportSlope2 = (supportPrices.Index(0) - supportPrices.Index(1)) / supportDuration.Index(0) + + supportSlope = (supportSlope1 + supportSlope2) / 2. + } + + if converge(resistanceSlope, supportSlope) { + // y = mx+b + currentResistance := resistanceSlope*pivotHighDurationCounter + resistancePrices.Last() + currentSupport := supportSlope*pivotLowDurationCounter + supportPrices.Last() + log.Info(currentResistance, currentSupport, kline.Close) + + if kline.High.Float64() > currentResistance { + if position.IsShort() { + s.orderExecutor.ClosePosition(context.Background(), one) + } + if position.IsDust(kline.Close) || position.IsClosed() { + s.placeOrder(context.Background(), types.SideTypeBuy, s.Quantity, symbol) // OrAmount.CalculateQuantity(kline.Close) + } + + } else if kline.Low.Float64() < currentSupport { + if position.IsLong() { + s.orderExecutor.ClosePosition(context.Background(), one) + } + if position.IsDust(kline.Close) || position.IsClosed() { + s.placeOrder(context.Background(), types.SideTypeSell, s.Quantity, symbol) // OrAmount.CalculateQuantity(kline.Close) + } + } + } + })) + + if !bbgo.IsBackTesting { + session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { + }) + } +} + +func (s *TrendLine) placeOrder(ctx context.Context, side types.SideType, quantity fixedpoint.Value, symbol string) error { + market, _ := s.session.Market(symbol) + _, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: symbol, + Market: market, + Side: side, + Type: types.OrderTypeMarket, + Quantity: quantity, + Tag: "trend-break", + }) + if err != nil { + log.WithError(err).Errorf("can not place market order") + } + return err +} + +func line(p1, p2, p3 float64) int64 { + if p1 >= p2 && p2 >= p3 { + return -1 + } else if p1 <= p2 && p2 <= p3 { + return +1 + } + return 0 +} + +func converge(mr, ms float64) bool { + if ms > mr { + return true + } + return false +}