From eff75239ccb4e186a15a5f21e2d4be115e2d8634 Mon Sep 17 00:00:00 2001 From: lychiyu Date: Thu, 12 Sep 2024 22:47:33 +0800 Subject: [PATCH] =?UTF-8?q?[add]=20=E6=96=B0=E5=A2=9Eroll=E5=92=8Cbolladxe?= =?UTF-8?q?ma=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/bolladxema.yaml | 89 ++++ config/roll.yaml | 71 +++ pkg/cmd/strategy/builtin.go | 2 + pkg/strategy/bolladxema/strategy.go | 702 ++++++++++++++++++++++++++ pkg/strategy/roll/strategy.go | 751 ++++++++++++++++++++++++++++ 5 files changed, 1615 insertions(+) create mode 100644 config/bolladxema.yaml create mode 100644 config/roll.yaml create mode 100644 pkg/strategy/bolladxema/strategy.go create mode 100644 pkg/strategy/roll/strategy.go diff --git a/config/bolladxema.yaml b/config/bolladxema.yaml new file mode 100644 index 0000000..3ccf632 --- /dev/null +++ b/config/bolladxema.yaml @@ -0,0 +1,89 @@ +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 + bolladxema: + dryRun: false + symbol: BTCUSDT + interval: 5m + leverage: 100.0 + enableADX: true + adxHSingle: 40.0 + adxMSingle: 30.0 + adxLSingle: 25.0 + longCCI: -180.0 + shortCCI: 180.0 + amount: 12.0 + enablePause: false + tradeStartHour: 0 + tradeEndHour: 0 + pauseTradeLoss: -10.0 + # 1:ATR 0:range + profitType: 0 + profitHRange: 0.8% + lossHRange: 0.5% + profitMRange: 0.5% + lossMRange: 0.5% + profitLRange: 0.3% + lossLRange: 0.3% + atrProfitMultiple: 1.5 + atrLossMultiple: 1.5 + placeOrderType: 0 + stageHalfAmount: + - 40 + - 60 + - 120.0 + - 360.0 + - 1080.0 + - 3240.0 + - 9720.0 + - 29160.0 + - 87480.0 + - 262440.0 + - 787320.0 + - 2361960.0 + - 7085880.0 + - 21257640.0 + bollinger: + interval: "5m" + window: 21 + bandWidth: 2.0 + emaSetting: + interval: "5m" + window: 20 + adxSetting: + interval: "5m" + window: 14 + atrSetting: + interval: "5m" + window: 14 + cciSetting: + interval: "5m" + window: 20 + +backtest: + startTime: "2022-01-01" + endTime: "2025-03-01" + symbols: + - BTCUSDT + sessions: [ binance ] + # syncSecKLines: true + accounts: + binance: + makerFeeRate: 0.02% + takerFeeRate: 0.05% + balances: + BTC: 0.0 + USDT: 20.0 \ No newline at end of file diff --git a/config/roll.yaml b/config/roll.yaml new file mode 100644 index 0000000..53817e9 --- /dev/null +++ b/config/roll.yaml @@ -0,0 +1,71 @@ +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 + roll: + symbol: BTCUSDT + interval: 5m + leverage: 100.0 + nrInterval: 5m + cciInterval: 5m + atrInterval: 5m + nrCount: 4 + StrictMode: false + longCCI: -180.0 + shortCCI: 180.0 + cciWindow: 20 + ATRWindow: 14 + tradeStartHour: 0 + tradeEndHour: 0 + pauseTradeLoss: -10.0 + enablePause: false + dryRun: false + placePriceType: 1 + profitOrderType: 0 + lossType: 0 + profitRange: 0.3% + lossRange: 0.5% + atrProfitRange: 1.0 + atrLossRange: 2.0 + midPriceRange: 0.003 + amount: 78.0 + stageHalfAmount: + - 360.0 + - 1080.0 + - 3240.0 + - 9720.0 + - 29160.0 + - 87480.0 + - 262440.0 + - 787320.0 + - 2361960.0 + - 7085880.0 + - 21257640.0 + + +backtest: + startTime: "2022-01-01" + endTime: "2025-03-01" + symbols: + - BTCUSDT + sessions: [binance] + # syncSecKLines: true + accounts: + binance: + makerFeeRate: 0.02% + takerFeeRate: 0.05% + balances: + BTC: 0.0 + USDT: 20.0 \ No newline at end of file diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index e74dfe6..cbbc1bb 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -6,6 +6,7 @@ import ( _ "git.qtrade.icu/lychiyu/bbgo/pkg/strategy/audacitymaker" _ "git.qtrade.icu/lychiyu/bbgo/pkg/strategy/autoborrow" _ "git.qtrade.icu/lychiyu/bbgo/pkg/strategy/autobuy" + _ "git.qtrade.icu/lychiyu/bbgo/pkg/strategy/bolladxema" _ "git.qtrade.icu/lychiyu/bbgo/pkg/strategy/bollgrid" _ "git.qtrade.icu/lychiyu/bbgo/pkg/strategy/bollmaker" _ "git.qtrade.icu/lychiyu/bbgo/pkg/strategy/convert" @@ -34,6 +35,7 @@ import ( _ "git.qtrade.icu/lychiyu/bbgo/pkg/strategy/pricedrop" _ "git.qtrade.icu/lychiyu/bbgo/pkg/strategy/random" _ "git.qtrade.icu/lychiyu/bbgo/pkg/strategy/rebalance" + _ "git.qtrade.icu/lychiyu/bbgo/pkg/strategy/roll" _ "git.qtrade.icu/lychiyu/bbgo/pkg/strategy/rsicross" _ "git.qtrade.icu/lychiyu/bbgo/pkg/strategy/rsmaker" _ "git.qtrade.icu/lychiyu/bbgo/pkg/strategy/schedule" diff --git a/pkg/strategy/bolladxema/strategy.go b/pkg/strategy/bolladxema/strategy.go new file mode 100644 index 0000000..4d0914b --- /dev/null +++ b/pkg/strategy/bolladxema/strategy.go @@ -0,0 +1,702 @@ +package bolladxema + +import ( + "context" + "errors" + "fmt" + "git.qtrade.icu/lychiyu/bbgo/pkg/bbgo" + "git.qtrade.icu/lychiyu/bbgo/pkg/exchange/binance" + "git.qtrade.icu/lychiyu/bbgo/pkg/fixedpoint" + indicatorv2 "git.qtrade.icu/lychiyu/bbgo/pkg/indicator/v2" + "git.qtrade.icu/lychiyu/bbgo/pkg/types" + "github.com/sirupsen/logrus" + "strconv" + "strings" + "sync" + "time" +) + +/* +布林带+ADX+EMA策略 +1. 布林带,判断是否在布林带内,在布林带上,做多,在布林带下,做空。 +2. ADX,判断是否在ADX区间内,在区间内,做多,在区间外,做空。 +3. EMA,判断是否在EMA区间内,在区间内,做多,在区间外,做空。 +4. 默认开仓量,默认止盈止损。 +*/ + +const ID = "bolladxema" + +var log = logrus.WithField("strategy", ID) + +var ten = fixedpoint.NewFromInt(10) +var Two = fixedpoint.NewFromInt(2) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type State struct { + Counter int `json:"counter,omitempty"` +} + +type BollingerSetting struct { + types.IntervalWindow + BandWidth float64 `json:"bandWidth"` +} + +type Strategy struct { + Environment *bbgo.Environment + Market types.Market + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + + exchange *binance.Exchange + + // persistence fields + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` + + DryRun bool `json:"dryRun"` + Symbol string `json:"symbol"` + Interval types.Interval `json:"interval"` + Leverage fixedpoint.Value `json:"leverage,omitempty"` + ProfitType int `json:"profitType"` + PlaceOrderType int `json:"placeOrderType"` + EnablePause bool `json:"enablePause"` + TradeStartHour int `json:"tradeStartHour"` + TradeEndHour int `json:"tradeEndHour"` + PauseTradeLoss fixedpoint.Value `json:"pauseTradeLoss"` + ProfitHRange fixedpoint.Value `json:"profitHRange"` + LossHRange fixedpoint.Value `json:"lossHRange"` + ProfitMRange fixedpoint.Value `json:"profitMRange"` + LossMRange fixedpoint.Value `json:"lossMRange"` + ProfitLRange fixedpoint.Value `json:"profitLRange"` + LossLRange fixedpoint.Value `json:"lossLRange"` + AtrProfitMultiple float64 `json:"atrProfitMultiple"` + AtrLossMultiple float64 `json:"atrLossMultiple"` + EnableADX bool `json:"enableADX"` + ADXHSingle float64 `json:"adxHSingle"` + ADXMSingle float64 `json:"adxMSingle"` + ADXLSingle float64 `json:"adxLSingle"` + LongCCI fixedpoint.Value `json:"longCCI"` + ShortCCI fixedpoint.Value `json:"shortCCI"` + State *State `persistence:"state"` + Bollinger *BollingerSetting `json:"bollinger"` + EMASetting types.IntervalWindow `json:"emaSetting"` + ADXSetting types.IntervalWindow `json:"adxSetting"` + ATRSetting types.IntervalWindow `json:"atrSetting"` + CCISetting types.IntervalWindow `json:"cciSetting"` + StageHalfAmount []fixedpoint.Value `json:"stageHalfAmount"` + + bbgo.QuantityOrAmount + + // 当前的盈利阶段 + CurrentStage int + + Traded bool + TradeSignal string + TradeRetry int + + PauseTradeCount fixedpoint.Value + // 最近一次暂停交易的时间 + PauseTradeTime time.Time + // 总盈利 + TotalProfit fixedpoint.Value + // 总手续费 + TotalFree fixedpoint.Value + // 总交易次数 + TotalOrderCount int + TotalProfitCount int + TotalLossCount int + + LongOrder types.SubmitOrder + LongProfitOrder types.SubmitOrder + LongLossOrder types.SubmitOrder + ShortOrder types.SubmitOrder + ShortProfitOrder types.SubmitOrder + ShortLossOrder types.SubmitOrder + + // 开仓 + OpenTrade []types.Trade + // 清仓 + EndTrade []types.Trade + OpenQuantity fixedpoint.Value + EndQuantity fixedpoint.Value + + ADX *indicatorv2.ADXStream + EMA *indicatorv2.EWMAStream + BOLL *indicatorv2.BOLLStream + ATR *indicatorv2.ATRStream + CCI *indicatorv2.CCIStream + + bbgo.StrategyController +} + +func (s *Strategy) Defaults() error { + s.PauseTradeCount = fixedpoint.Zero + s.TotalProfit = fixedpoint.Zero + s.TotalFree = fixedpoint.Zero + s.OpenQuantity = fixedpoint.Zero + s.EndQuantity = fixedpoint.Zero + s.PauseTradeTime = time.Now().Add(-24 * time.Hour) + s.TradeRetry = 0 + s.Traded = false + s.TradeSignal = "" + return nil +} + +// ID should return the identity of this strategy +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return ID + ":" + s.Symbol +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) + + if s.Bollinger != nil && s.Bollinger.Interval != "" && s.Bollinger.Interval != s.Interval { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Bollinger.Interval}) + } + if s.EMASetting.Interval != "" && s.EMASetting.Interval != s.Interval { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.EMASetting.Interval}) + } + + if s.ADXSetting.Interval != "" && s.ADXSetting.Interval != s.Interval { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.ADXSetting.Interval}) + } + if s.ATRSetting.Interval != "" && s.ATRSetting.Interval != s.Interval { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.ATRSetting.Interval}) + } + + if !bbgo.IsBackTesting { + session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{}) + } +} + +func (s *Strategy) cancelOrders(ctx context.Context, symbol string) { + if len(s.orderExecutor.ActiveMakerOrders().Orders()) <= 0 { + return + } + log.Infof(fmt.Sprintf("[%s] the order is not filled, will cancel all orders", symbol)) + if err := s.orderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("failed to cancel orders") + } + + s.Traded = false + s.TradeSignal = "" + s.TradeRetry = 0 +} + +func (s *Strategy) isTradeTime(ctx context.Context) bool { + // 如果时间一致则表示不限制交易时间 + if s.TradeEndHour == s.TradeStartHour { + return true + } + location, err := time.LoadLocation("Asia/Shanghai") + if err != nil { + return false + } + now := time.Now().In(location) + + hour := now.Hour() + return !(hour >= s.TradeStartHour && hour < s.TradeEndHour) +} + +func (s *Strategy) isPauseTrade(ctx context.Context) bool { + if !s.EnablePause { + return false + } + // 被暂停次数不为0,且最近一次的暂停时间和今天一致,则表示暂停 + if s.PauseTradeCount != fixedpoint.Zero && s.PauseTradeTime.Day() == time.Now().Day() { + return true + } + // 总收益大于(暂停次数+1)*暂停亏损,则表示暂停 + if s.TotalProfit < s.PauseTradeLoss.Mul(s.PauseTradeCount.Add(fixedpoint.One)) { + s.PauseTradeCount.Add(fixedpoint.One) + s.PauseTradeTime = time.Now() + return true + } + + return false +} + +func (s *Strategy) setInitialLeverage(ctx context.Context) error { + log.Infof("setting futures leverage to %d", s.Leverage.Int()+1) + var ok bool + s.exchange, ok = s.session.Exchange.(*binance.Exchange) + if !ok { + return errors.New("not binance exchange, currently only support binance exchange") + } + futuresClient := s.exchange.GetFuturesClient() + req := futuresClient.NewFuturesChangeInitialLeverageRequest() + req.Symbol(s.Symbol) + req.Leverage(s.Leverage.Int() + 1) + resp, err := req.Do(ctx) + if err != nil { + return err + } + + log.Infof("adjusted initial leverage: %+v", resp) + return nil +} + +func (s *Strategy) GetTradeSignal(k types.KLine, adx, bollUp, bollDown, ema, cciV float64) string { + if k.High.Float64() >= bollUp && k.Low.Float64() <= bollDown { + // k线跨越布林带,不入场 + return "" + } + + // 小于最小ADX信号 + if s.EnableADX && adx < s.ADXLSingle { + return "" + } + + if k.Open >= k.Close && k.Low.Float64() <= bollDown && k.High.Float64() >= bollDown && k.Close.Float64() <= ema && cciV <= s.LongCCI.Float64() { + // k线收跌,且触及下轨,但是最高价会在下轨上,并小于ema,开多 + return "long" + } + if k.Open <= k.Close && k.High.Float64() >= bollUp && k.Low.Float64() <= bollUp && k.Close.Float64() >= ema && cciV >= s.ShortCCI.Float64() { + // k线收涨,且触及上轨,但是最高价会在上轨下,并大于ema,开空 + return "short" + } + return "" +} + +func (s *Strategy) generateOrders(k types.KLine, bollDiff, adx float64) ([]types.SubmitOrder, error) { + var orders []types.SubmitOrder + symbol := k.Symbol + + // 止盈订单类型 + profitOrderType := types.OrderTypeTakeProfitMarket + // 止损订单类型 + lossOrderType := types.OrderTypeStopMarket + if s.PlaceOrderType == 1 { + profitOrderType = types.OrderTypeStopMarket + } + + if bbgo.IsBackTesting { + profitOrderType = types.OrderTypeStopLimit + lossOrderType = types.OrderTypeStopLimit + } + + // 下单价格 + placePrice := k.Close + // 计算止损止盈价格,以ATR为基准或者固定百分比 + lossPrice := fixedpoint.Zero + profitPrice := fixedpoint.Zero + lastATR, err := strconv.ParseFloat(strconv.FormatFloat(s.ATR.Last(0), 'f', 6, 64), 64) + + if err != nil { + log.WithError(err).Error("failed parse atr last value float") + lastATR = 0.0 + } + + // 依据不同的adx来设置止盈止损 + profitRange := s.ProfitLRange + lossRange := s.LossLRange + if adx >= s.ADXHSingle { + profitRange = s.ProfitHRange + lossRange = s.LossHRange + } else if adx >= s.ADXMSingle { + profitRange = s.ProfitMRange + lossRange = s.LossMRange + } + + //if bollDiff >= 0.03 { + // profitRange = profitRange.Mul(fixedpoint.NewFromFloat(1.5)) + //} + if s.TradeSignal == "long" { + // 做多 + if s.ProfitType == 0 || s.ATR.Last(0) == 0.0 { + lossPrice = placePrice.Mul(fixedpoint.One.Sub(lossRange)) + profitPrice = placePrice.Mul(fixedpoint.One.Add(profitRange)) + } else { + lossPrice = placePrice.Sub(fixedpoint.Value(1e8 * lastATR * s.AtrLossMultiple)) + profitPrice = placePrice.Add(fixedpoint.Value(1e8 * lastATR * s.AtrProfitMultiple)) + } + } else { + //做空 + if s.ProfitType == 0 || s.ATR.Last(0) == 0.0 { + lossPrice = placePrice.Mul(fixedpoint.One.Add(lossRange)) + profitPrice = placePrice.Mul(fixedpoint.One.Sub(profitRange)) + } else { + lossPrice = placePrice.Add(fixedpoint.Value(1e8 * lastATR * s.AtrLossMultiple)) + profitPrice = placePrice.Sub(fixedpoint.Value(1e8 * lastATR * s.AtrProfitMultiple)) + } + } + + // 下单数量 + placeQuantity := s.QuantityOrAmount.CalculateQuantity(placePrice).Mul(s.Leverage) + msg := fmt.Sprintf("%v, will place order, amount %v, price %v, quantity %v, lossprice %v, profitprice: %v, atr: %v", s.Symbol, + s.QuantityOrAmount.Amount.Float64(), placePrice.Float64(), placeQuantity.Float64(), lossPrice.Float64(), profitPrice.Float64(), + lastATR) + bbgo.Notify(msg) + + s.ShortOrder = types.SubmitOrder{ + Symbol: symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimit, + Price: placePrice, + PositionSide: types.PositionSideTypeShort, + Quantity: placeQuantity, + TimeInForce: types.TimeInForceGTC, + Market: s.Market, + } + + s.ShortProfitOrder = types.SubmitOrder{ + Symbol: symbol, + Side: types.SideTypeBuy, + Type: profitOrderType, + PositionSide: types.PositionSideTypeShort, + StopPrice: profitPrice, + TimeInForce: types.TimeInForceGTC, + Market: s.Market, + ClosePosition: true, + } + + s.ShortLossOrder = types.SubmitOrder{ + Symbol: symbol, + Side: types.SideTypeBuy, + Type: lossOrderType, + PositionSide: types.PositionSideTypeShort, + StopPrice: lossPrice, + TimeInForce: types.TimeInForceGTC, + Market: s.Market, + ClosePosition: true, + } + + s.LongOrder = types.SubmitOrder{ + Symbol: symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Price: placePrice, + PositionSide: types.PositionSideTypeLong, + Quantity: placeQuantity, + TimeInForce: types.TimeInForceGTC, + Market: s.Market, + } + + s.LongProfitOrder = types.SubmitOrder{ + Symbol: symbol, + Side: types.SideTypeSell, + Type: profitOrderType, + PositionSide: types.PositionSideTypeLong, + StopPrice: profitPrice, + TimeInForce: types.TimeInForceGTC, + Market: s.Market, + ClosePosition: true, + } + + s.LongLossOrder = types.SubmitOrder{ + Symbol: symbol, + Side: types.SideTypeSell, + Type: lossOrderType, + PositionSide: types.PositionSideTypeLong, + StopPrice: lossPrice, + TimeInForce: types.TimeInForceGTC, + Market: s.Market, + ClosePosition: true, + } + + if s.TradeSignal == "short" { + // 挂空单 + orders = append(orders, s.ShortOrder) + // 空单止盈 + orders = append(orders, s.ShortProfitOrder) + // 空单止损 + orders = append(orders, s.ShortLossOrder) + } + + if s.TradeSignal == "long" { + // 挂多单 + orders = append(orders, s.LongOrder) + // 多单止盈 + orders = append(orders, s.LongProfitOrder) + // 多单止损 + orders = append(orders, s.LongLossOrder) + } + + return orders, nil + +} + +func (s *Strategy) placeOrders(ctx context.Context, k types.KLine, bollDiff, adx float64) { + if s.TradeSignal == "" { + return + } + symbol := k.Symbol + orders, err := s.generateOrders(k, bollDiff, adx) + if err != nil { + log.WithError(err).Error(fmt.Sprintf("failed to generate orders (%s)", symbol)) + return + } + log.Infof("orders: %+v", orders) + + if s.DryRun { + log.Infof("dry run, not submitting orders (%s)", symbol) + return + } + + createdOrders, err := s.orderExecutor.SubmitOrders(ctx, orders...) + if err != nil { + log.WithError(err).Error(fmt.Sprintf("failed to submit orders (%s)", symbol)) + return + } + log.Infof("created orders (%s): %+v", symbol, createdOrders) + return +} + +func (s *Strategy) notifyProfit(ctx context.Context, symbol string) { + if s.EndQuantity != s.OpenQuantity { + return + } + profit := fixedpoint.Zero + openProfit := fixedpoint.Zero + endProfit := fixedpoint.Zero + free := fixedpoint.Zero + + var openMsgs []string + var endMsgs []string + // 开仓成本 + for _, trade := range s.OpenTrade { + openProfit = openProfit.Add(trade.Price.Mul(trade.Quantity)) + free = free.Add(trade.Fee) + openMsgs = append(openMsgs, fmt.Sprintf("price:%v, quantity:%v, fee:%v;", + trade.Price.Float64(), trade.Quantity.Float64(), trade.Fee.Float64())) + } + + // 清仓资产 + for _, trade := range s.EndTrade { + endProfit = endProfit.Add(trade.Price.Mul(trade.Quantity)) + free = free.Add(trade.Fee) + endMsgs = append(endMsgs, fmt.Sprintf("price:%v, quantity:%v, fee:%v;", + trade.Price.Float64(), trade.Quantity.Float64(), trade.Fee.Float64())) + } + + side := s.OpenTrade[0].Side + // 做多 + if side == types.SideTypeBuy { + profit = endProfit.Sub(openProfit).Sub(free) + } + + // 做空 + if side == types.SideTypeSell { + profit = openProfit.Sub(endProfit).Sub(free) + } + + msg := fmt.Sprintf("Trade finish:\n symbol: %s, signal:%v, profit:%v, fee:%v \n Trade details:\n OpenTrade:\n %s\n CloseTrade:\n %s", + symbol, s.TradeSignal, profit.Float64(), free.Float64(), strings.Join(openMsgs, "\n"), strings.Join(endMsgs, "\n")) + + s.updateAmount(ctx, profit) + s.TotalProfit = s.TotalProfit.Add(profit) + s.TotalFree = s.TotalFree.Add(free) + s.TotalOrderCount += 1 + if profit > fixedpoint.Zero { + s.TotalProfitCount += 1 + } else { + s.TotalLossCount += 1 + } + + log.Infof(msg) + bbgo.Notify(msg) + + // 重置 + s.OpenTrade = []types.Trade{} + s.EndTrade = []types.Trade{} + s.OpenQuantity = fixedpoint.Zero + s.EndQuantity = fixedpoint.Zero + + // 记得取消订单 + s.cancelOrders(ctx, symbol) + + bbgo.Notify(fmt.Sprintf("%v, Total Count:%v, Profit:%v, Fee:%v, Profit Count:%v, Loss Count:%v", s.Symbol, + s.TotalOrderCount, s.TotalProfit.Float64(), s.TotalFree.Float64(), s.TotalProfitCount, s.TotalLossCount)) +} + +func (s *Strategy) updateAmount(ctx context.Context, profit fixedpoint.Value) { + // 更新amount + newAmount := s.QuantityOrAmount.Amount.Add(profit) + // 如果当前的总金额大于阶梯上的某一个值,则更新为减半 + if newAmount >= s.StageHalfAmount[s.CurrentStage] { + s.QuantityOrAmount.Amount = newAmount.Div(Two) + bbgo.Notify(fmt.Sprintf("%v 结余资金:%v", s.Symbol, s.QuantityOrAmount.Amount.Float64())) + s.CurrentStage += 1 + bbgo.Sync(ctx, s) + return + } + s.QuantityOrAmount.Amount = newAmount +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + instanceID := s.InstanceID() + // Initialize the default value for state + if s.State == nil { + s.State = &State{Counter: 1} + } + + 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) + } + + 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(ctx, s) + }) + s.orderExecutor.Bind() + + bbgo.Notify("BTC滚仓布林带策略开始运行") + + s.BOLL = session.Indicators(s.Symbol).BOLL(s.Bollinger.IntervalWindow, s.Bollinger.BandWidth) + s.EMA = session.Indicators(s.Symbol).EMA(s.EMASetting) + s.ADX = session.Indicators(s.Symbol).ADX(s.ADXSetting.Interval, s.ADXSetting.Window) + s.ATR = session.Indicators(s.Symbol).ATR(s.ATRSetting.Interval, s.ATRSetting.Window) + s.CCI = session.Indicators(s.Symbol).CCI(s.CCISetting.Interval, s.CCISetting.Window) + session.MarketDataStream.OnKLineClosed(func(k types.KLine) { + if k.Symbol != s.Symbol { + return + } + adx := s.ADX.Last(0) + + // 小于最小ADX信号 + if s.EnableADX && adx < s.ADXLSingle { + return + } + + if !s.isTradeTime(ctx) || s.isPauseTrade(ctx) { + //pauseMsg := fmt.Sprintf("暂停交易:总收益:%v, 暂停次数:%v, 暂停时间:%v; 暂停时间段:[%v, %v)", + // s.TotalProfit.Float64(), s.PauseTradeCount.Float64(), s.PauseTradeTime, s.TradeStartHour, + // s.TradeEndHour) + //bbgo.Notify(pauseMsg) + return + } + + if !s.Traded { + // 如若在下一根k线未成交 则取消订单 + if s.TradeSignal != "" && s.TradeRetry > 1 { + bbgo.Notify(fmt.Sprintf("Trade signal not traded, cancel orders: %s", s.Symbol)) + s.cancelOrders(ctx, s.Symbol) + } + + if s.TradeSignal != "" && s.TradeRetry <= 1 { + s.TradeRetry = s.TradeRetry + 1 + } + } + + if s.TradeSignal != "" { + return + } + + bollUp := s.BOLL.UpBand.Last(0) + bolldown := s.BOLL.DownBand.Last(0) + ema := s.EMA.Last(0) + cciV := s.CCI.Last(0) + signal := s.GetTradeSignal(k, adx, bollUp, bolldown, ema, cciV) + if signal == "" { + return + } + s.TradeSignal = signal + msg := fmt.Sprintf("trade singal info, symbol:%s, single %s, time: %s,open:%f,close:%f, high:%f,low:%f, ema: %v, adx: %v, bollUp %f, bollDown %f, cci %f", + s.Symbol, signal, k.EndTime, k.Open.Float64(), k.Close.Float64(), k.High.Float64(), k.Low.Float64(), ema, adx, bollUp, bolldown, cciV) + bollDiff := (bollUp - bolldown) / bolldown + s.placeOrders(ctx, k, bollDiff, adx) + bbgo.Notify(msg) + }) + + session.UserDataStream.OnOrderUpdate(func(order types.Order) { + orderSymbol := order.Symbol + if orderSymbol != s.Symbol { + return + } + if order.Status == types.OrderStatusFilled { + if order.Type == types.OrderTypeLimit && order.Side == types.SideTypeBuy { + log.Infof("the long order is filled: %+v,id is %d, symbol is %s, type is %s, status is %s", + order, order.OrderID, orderSymbol, order.Type, order.Status) + s.Traded = true + s.TradeRetry = 0 + bbgo.Notify("Order traded notify:\n symbol:%s, signal:%s, price:%s, quantity:%s", order.Symbol, s.TradeSignal, + order.Price, order.Quantity) + } + if order.Type == types.OrderTypeLimit && order.Side == types.SideTypeSell { + log.Infof("the short order is filled: %+v,id is %d, symbol is %s, type is %s, status is %s", + order, order.OrderID, orderSymbol, order.Type, order.Status) + s.Traded = true + s.TradeRetry = 0 + bbgo.Notify("Order traded notify:\n symbol:%s, signal:%s, price:%s, quantity:%s", order.Symbol, s.TradeSignal, + order.Price, order.Quantity) + } + + if order.Type == types.OrderTypeMarket { + log.Infof("the loss or profit order is filled: %+v,id is %d, symbol is %s, type is %s, "+ + "status is %s", order, order.OrderID, orderSymbol, order.Type, order.Status) + bbgo.Notify("Order stop profit or loss notify:\n %s", order.Symbol) + s.Traded = false + s.TradeRetry = 0 + s.TradeSignal = "" + } else { + log.Infof("the order is: %+v,id is %d, symbol is %s, type is %s, status is %s", + order, order.OrderID, orderSymbol, order.Type, order.Status) + } + } else if order.Status == types.OrderStatusCanceled { + log.Infof("canceled order %+v", order) + } + }) + + session.UserDataStream.OnTradeUpdate(func(trade types.Trade) { + symbol := trade.Symbol + if symbol != s.Symbol { + return + } + if (trade.Side == types.SideTypeBuy && s.TradeSignal == "long") || (trade.Side == types.SideTypeSell && s.TradeSignal == "short") { + s.OpenTrade = append(s.OpenTrade, trade) + s.OpenQuantity = s.OpenQuantity.Add(trade.Quantity) + } + if (trade.Side == types.SideTypeSell && s.TradeSignal == "long") || (trade.Side == types.SideTypeBuy && s.TradeSignal == "short") { + s.EndTrade = append(s.EndTrade, trade) + s.EndQuantity = s.EndQuantity.Add(trade.Quantity) + s.notifyProfit(ctx, symbol) + } + log.Infof("trade: symbol %s, side %s, price %f, fee %f, quantity %f, buyer %v, maker %v", + symbol, trade.Side, trade.Price.Float64(), trade.Fee.Float64(), trade.Quantity.Float64(), + trade.IsBuyer, trade.IsMaker) + }) + + 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) + }) + + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + if err := s.orderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Error("unable to cancel open orders...") + } + + bbgo.Sync(ctx, s) + }) + + return nil +} diff --git a/pkg/strategy/roll/strategy.go b/pkg/strategy/roll/strategy.go new file mode 100644 index 0000000..8e53f66 --- /dev/null +++ b/pkg/strategy/roll/strategy.go @@ -0,0 +1,751 @@ +package roll + +import ( + "context" + "errors" + "fmt" + "git.qtrade.icu/lychiyu/bbgo/pkg/bbgo" + "git.qtrade.icu/lychiyu/bbgo/pkg/exchange/binance" + "git.qtrade.icu/lychiyu/bbgo/pkg/fixedpoint" + indicatorv2 "git.qtrade.icu/lychiyu/bbgo/pkg/indicator/v2" + "git.qtrade.icu/lychiyu/bbgo/pkg/types" + "github.com/sirupsen/logrus" + "strconv" + "strings" + "sync" + "time" +) + +const ID = "roll" + +var log = logrus.WithField("strategy", ID) +var ten = fixedpoint.NewFromInt(10) +var Two = fixedpoint.NewFromInt(2) +var Delta = fixedpoint.NewFromFloat(0.00001) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type State struct { + Counter int `json:"counter,omitempty"` +} + +type Strategy struct { + Environment *bbgo.Environment + Market types.Market + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + + exchange *binance.Exchange + + // persistence fields + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` + + Symbol string `json:"symbol"` + Interval types.Interval `json:"interval"` + Leverage fixedpoint.Value `json:"leverage,omitempty"` + NRInterval types.Interval `json:"nrInterval" modifiable:"true"` + CCIInterval types.Interval `json:"cciInterval" modifiable:"true"` + ATRInterval types.Interval `json:"atrInterval" modifiable:"true"` + NrCount int `json:"nrCount" modifiable:"true"` + CCIWindow int `json:"cciWindow"` + ATRWindow int `json:"atrWindow"` + StrictMode bool `json:"strictMode" modifiable:"true"` + TradeStartHour int `json:"tradeStartHour"` + TradeEndHour int `json:"tradeEndHour"` + PauseTradeLoss fixedpoint.Value `json:"pauseTradeLoss"` + LongCCI fixedpoint.Value `json:"longCCI"` + ShortCCI fixedpoint.Value `json:"shortCCI"` + DryRun bool `json:"dryRun"` + EnablePause bool `json:"enablePause"` + PlacePriceType int `json:"placePriceType"` + ProfitOrderType int `json:"profitOrderType"` + LossType int `json:"lossType"` + ProfitRange fixedpoint.Value `json:"profitRange"` + LossRange fixedpoint.Value `json:"lossRange"` + MidPriceRange float64 `json:"midPriceRange"` + AtrProfitRange float64 `json:"atrProfitRange"` + AtrLossRange float64 `json:"atrLossRange"` + StageHalfAmount []fixedpoint.Value `json:"stageHalfAmount"` + bbgo.QuantityOrAmount + + nr *indicatorv2.NRStrean + cci *indicatorv2.CCIStream + atr *indicatorv2.ATRStream + + // 当前的盈利阶段 + CurrentStage int + + Traded bool + TradeType string + TradeRetry int + PauseTradeCount fixedpoint.Value + // 最近一次暂停交易的时间 + PauseTradeTime time.Time + // 总盈利 + TotalProfit fixedpoint.Value + // 总手续费 + TotalFree fixedpoint.Value + // 总交易次数 + TotalOrderCount int + TotalProfitCount int + TotalLossCount int + + LongOrder types.SubmitOrder + LongProfitOrder types.SubmitOrder + LongLossOrder types.SubmitOrder + ShortOrder types.SubmitOrder + ShortProfitOrder types.SubmitOrder + ShortLossOrder types.SubmitOrder + + // 开仓 + OpenTrade []types.Trade + // 清仓 + EndTrade []types.Trade + OpenQuantity fixedpoint.Value + EndQuantity fixedpoint.Value + + // State is a state of your strategy + // When BBGO shuts down, everything in the memory will be dropped + // If you need to store something and restore this information back, + // Simply define the "persistence" tag + State *State `persistence:"state"` + + bbgo.StrategyController + + getLastPrice func() fixedpoint.Value +} + +func (s *Strategy) Defaults() error { + s.PauseTradeCount = fixedpoint.Zero + s.TotalProfit = fixedpoint.Zero + s.TotalFree = fixedpoint.Zero + s.OpenQuantity = fixedpoint.Zero + s.EndQuantity = fixedpoint.Zero + s.PauseTradeTime = time.Now().Add(-24 * time.Hour) + s.TradeRetry = 0 + return nil +} + +// ID should return the identity of this strategy +func (s *Strategy) ID() string { + return ID +} + +// InstanceID returns the identity of the current instance of this strategy. +// You may have multiple instance of a strategy, with different symbols and settings. +// This value will be used for persistence layer to separate the storage. +// +// Run: +// +// redis-cli KEYS "*" +// +// And you will see how this instance ID is used in redis. +func (s *Strategy) InstanceID() string { + return ID + ":" + s.Symbol +} + +func (s *Strategy) Initialize() error { + return nil +} + +// Subscribe method subscribes specific market data from the given session. +// Before BBGO is connected to the exchange, we need to collect what we want to subscribe. +// Here the strategy needs kline data, so it adds the kline subscription. +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + // We want 1m kline data of the symbol + // It will be BTCUSDT 1m if our s.Symbol is BTCUSDT + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) + + if !bbgo.IsBackTesting { + session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{}) + } +} + +// Position control + +func (s *Strategy) CurrentPosition() *types.Position { + return s.Position +} + +func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { + order := s.Position.NewMarketCloseOrder(percentage) + if order == nil { + return nil + } + order.Tag = "close" + order.TimeInForce = "" + balances := s.orderExecutor.Session().GetAccount().Balances() + baseBalance := balances[s.Market.BaseCurrency].Available + price := s.getLastPrice() + if order.Side == types.SideTypeBuy { + quoteAmount := balances[s.Market.QuoteCurrency].Available.Div(price) + if order.Quantity.Compare(quoteAmount) > 0 { + order.Quantity = quoteAmount + } + } else if order.Side == types.SideTypeSell && order.Quantity.Compare(baseBalance) > 0 { + order.Quantity = baseBalance + } + order.MarginSideEffect = types.SideEffectTypeAutoRepay + for { + if s.Market.IsDustQuantity(order.Quantity, price) { + return nil + } + _, err := s.orderExecutor.SubmitOrders(ctx, *order) + if err != nil { + order.Quantity = order.Quantity.Mul(fixedpoint.One.Sub(Delta)) + continue + } + return nil + } + +} + +func (s *Strategy) cancelOrders(ctx context.Context, symbol string) { + if len(s.orderExecutor.ActiveMakerOrders().Orders()) <= 0 { + return + } + log.Infof(fmt.Sprintf("[%s] the order is not filled, will cancel all orders", symbol)) + if err := s.orderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("failed to cancel orders") + } + + s.Traded = false + s.TradeType = "" + s.TradeRetry = 0 +} + +// isTradeTime 是否交易时间 +func (s *Strategy) isTradeTime(ctx context.Context) bool { + // 如果时间一致则表示不限制交易时间 + if s.TradeEndHour == s.TradeStartHour { + return true + } + location, err := time.LoadLocation("Asia/Shanghai") + if err != nil { + return false + } + now := time.Now().In(location) + + hour := now.Hour() + return !(hour >= s.TradeStartHour && hour < s.TradeEndHour) +} + +func (s *Strategy) isPauseTrade(ctx context.Context) bool { + if !s.EnablePause { + return false + } + // 被暂停次数不为0,且最近一次的暂停时间和今天一致,则表示暂停 + if s.PauseTradeCount != fixedpoint.Zero && s.PauseTradeTime.Day() == time.Now().Day() { + return true + } + // 总收益大于(暂停次数+1)*暂停亏损,则表示暂停 + if s.TotalProfit < s.PauseTradeLoss.Mul(s.PauseTradeCount.Add(fixedpoint.One)) { + s.PauseTradeCount.Add(fixedpoint.One) + s.PauseTradeTime = time.Now() + return true + } + + return false +} + +func (s *Strategy) setInitialLeverage(ctx context.Context) error { + log.Infof("setting futures leverage to %d", s.Leverage.Int()+1) + var ok bool + s.exchange, ok = s.session.Exchange.(*binance.Exchange) + if !ok { + return errors.New("not binance exchange, currently only support binance exchange") + } + futuresClient := s.exchange.GetFuturesClient() + req := futuresClient.NewFuturesChangeInitialLeverageRequest() + req.Symbol(s.Symbol) + req.Leverage(s.Leverage.Int() + 1) + resp, err := req.Do(ctx) + if err != nil { + return err + } + + log.Infof("adjusted initial leverage: %+v", resp) + return nil +} + +func (s *Strategy) getPlacePrice(ctx context.Context, kline types.KLine) fixedpoint.Value { + + placePrice := fixedpoint.Zero + midPrice := (kline.High.Add(kline.Low)).Div(fixedpoint.One * 2) + shouldMid := (((kline.High.Sub(kline.Low)).Div(kline.Low)).Abs()).Float64() <= s.MidPriceRange + switch s.PlacePriceType { + case 0: + if s.TradeType == "long" { + placePrice = kline.High + } else if s.TradeType == "short" { + placePrice = kline.Low + } + case 1: + if s.TradeType == "long" { + if !shouldMid { + placePrice = kline.Low + } else { + placePrice = midPrice + } + } else if s.TradeType == "short" { + if !shouldMid { + placePrice = kline.High + } else { + placePrice = midPrice + } + } + case 2: + if s.TradeType == "long" { + placePrice = midPrice + } else if s.TradeType == "short" { + placePrice = midPrice + } + } + return placePrice +} + +func (s *Strategy) generateOrders(ctx context.Context, kline types.KLine) ([]types.SubmitOrder, error) { + var orders []types.SubmitOrder + symbol := kline.Symbol + + log.Infof(fmt.Sprintf("place order keline info: symbol %s, high %v, low %v, open %v, close %v", symbol, + kline.High.Float64(), kline.Low.Float64(), kline.Open.Float64(), kline.Close.Float64())) + // 获取下单价格 + + if s.TradeType == "" { + return orders, nil + } + + // 获取下单价格 + placePrice := s.getPlacePrice(ctx, kline) + + // 止盈订单类型 + profitOrderType := types.OrderTypeTakeProfitMarket + // 止损订单类型 + lossOrderType := types.OrderTypeStopMarket + if s.ProfitOrderType == 1 { + profitOrderType = types.OrderTypeStopMarket + } + + if bbgo.IsBackTesting { + profitOrderType = types.OrderTypeStopLimit + lossOrderType = types.OrderTypeStopLimit + } + + // 计算止损止盈价格,以ATR为基准或者固定百分比 + lossPrice := fixedpoint.Zero + profitPrice := fixedpoint.Zero + lastATR, err := strconv.ParseFloat(strconv.FormatFloat(s.atr.Last(0), 'f', 6, 64), 64) + if err != nil { + log.WithError(err).Error("failed parse atr last value float") + lastATR = 0.0 + } + if s.TradeType == "long" { + if s.LossType == 0 || s.atr.Last(0) == 0.0 { + lossPrice = placePrice.Sub(placePrice.Mul(s.LossRange)) + profitPrice = placePrice.Add(placePrice.Mul(s.ProfitRange)) + } else if s.LossType == 1 { + lossPrice = placePrice.Sub(fixedpoint.Value(1e8 * lastATR * s.AtrLossRange)) + profitPrice = placePrice.Add(fixedpoint.Value(1e8 * lastATR * s.AtrProfitRange)) + } + } else if s.TradeType == "short" { + if s.LossType == 0 || s.atr.Last(0) == 0.0 { + lossPrice = placePrice.Add(placePrice.Mul(s.LossRange)) + profitPrice = placePrice.Sub(placePrice.Mul(s.ProfitRange)) + } else if s.LossType == 1 { + lossPrice = placePrice.Add(fixedpoint.Value(1e8 * lastATR * s.AtrLossRange)) + profitPrice = placePrice.Sub(fixedpoint.Value(1e8 * lastATR * s.AtrProfitRange)) + } + } + + // 下单数量 + placeQuantity := s.QuantityOrAmount.CalculateQuantity(placePrice).Mul(s.Leverage) + msg := fmt.Sprintf("%v, will place order, amount %v, price %v, quantity %v, lossprice %v, profitprice: %v, atr: %v", s.Symbol, + s.QuantityOrAmount.Amount.Float64(), placePrice.Float64(), placeQuantity.Float64(), lossPrice.Float64(), profitPrice.Float64(), + lastATR) + log.Infof(msg) + bbgo.Notify(msg) + + s.ShortOrder = types.SubmitOrder{ + Symbol: symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimit, + Price: placePrice, + PositionSide: types.PositionSideTypeShort, + Quantity: placeQuantity, + TimeInForce: types.TimeInForceGTC, + Market: s.Market, + } + + s.ShortProfitOrder = types.SubmitOrder{ + Symbol: symbol, + Side: types.SideTypeBuy, + Type: profitOrderType, + PositionSide: types.PositionSideTypeShort, + StopPrice: profitPrice, + TimeInForce: types.TimeInForceGTC, + Market: s.Market, + ClosePosition: true, + } + + s.ShortLossOrder = types.SubmitOrder{ + Symbol: symbol, + Side: types.SideTypeBuy, + Type: lossOrderType, + PositionSide: types.PositionSideTypeShort, + StopPrice: lossPrice, + TimeInForce: types.TimeInForceGTC, + Market: s.Market, + ClosePosition: true, + } + + s.LongOrder = types.SubmitOrder{ + Symbol: symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Price: placePrice, + PositionSide: types.PositionSideTypeLong, + Quantity: placeQuantity, + TimeInForce: types.TimeInForceGTC, + Market: s.Market, + } + + s.LongProfitOrder = types.SubmitOrder{ + Symbol: symbol, + Side: types.SideTypeSell, + Type: profitOrderType, + PositionSide: types.PositionSideTypeLong, + StopPrice: profitPrice, + TimeInForce: types.TimeInForceGTC, + Market: s.Market, + ClosePosition: true, + } + + s.LongLossOrder = types.SubmitOrder{ + Symbol: symbol, + Side: types.SideTypeSell, + Type: lossOrderType, + PositionSide: types.PositionSideTypeLong, + StopPrice: lossPrice, + TimeInForce: types.TimeInForceGTC, + Market: s.Market, + ClosePosition: true, + } + + if s.TradeType == "short" { + // 挂空单 + orders = append(orders, s.ShortOrder) + // 空单止盈 + orders = append(orders, s.ShortProfitOrder) + // 空单止损 + orders = append(orders, s.ShortLossOrder) + } + + if s.TradeType == "long" { + // 挂多单 + orders = append(orders, s.LongOrder) + // 多单止盈 + orders = append(orders, s.LongProfitOrder) + // 多单止损 + orders = append(orders, s.LongLossOrder) + } + + return orders, nil +} + +func (s *Strategy) placeOrders(ctx context.Context, kline types.KLine) { + symbol := kline.Symbol + orders, err := s.generateOrders(ctx, kline) + if err != nil { + log.WithError(err).Error(fmt.Sprintf("failed to generate orders (%s)", symbol)) + return + } + log.Infof("orders: %+v", orders) + + if s.DryRun { + log.Infof("dry run, not submitting orders (%s)", symbol) + return + } + + createdOrders, err := s.orderExecutor.SubmitOrders(ctx, orders...) + if err != nil { + log.WithError(err).Error(fmt.Sprintf("failed to submit orders (%s)", symbol)) + return + } + log.Infof("created orders (%s): %+v", symbol, createdOrders) + return +} + +func (s *Strategy) notifyProfit(ctx context.Context, symbol string) { + if s.EndQuantity != s.OpenQuantity { + return + } + profit := fixedpoint.Zero + openProfit := fixedpoint.Zero + endProfit := fixedpoint.Zero + free := fixedpoint.Zero + + var openMsgs []string + var endMsgs []string + // 开仓成本 + for _, trade := range s.OpenTrade { + openProfit = openProfit.Add(trade.Price.Mul(trade.Quantity)) + free = free.Add(trade.Fee) + openMsgs = append(openMsgs, fmt.Sprintf("价格:%v, 数量:%v, 手续费:%v;", + trade.Price.Float64(), trade.Quantity.Float64(), trade.Fee.Float64())) + } + + // 清仓资产 + for _, trade := range s.EndTrade { + endProfit = endProfit.Add(trade.Price.Mul(trade.Quantity)) + free = free.Add(trade.Fee) + endMsgs = append(endMsgs, fmt.Sprintf("价格:%v, 数量:%v, 手续费:%v;", + trade.Price.Float64(), trade.Quantity.Float64(), trade.Fee.Float64())) + } + + side := s.OpenTrade[0].Side + // 做多 + if side == types.SideTypeBuy { + profit = endProfit.Sub(openProfit).Sub(free) + } + + // 做空 + if side == types.SideTypeSell { + profit = openProfit.Sub(endProfit).Sub(free) + } + + msg := fmt.Sprintf("交易完成:\n 币种: %s, 方向:%v, 收益:%v, 手续费:%v \n Trade详情:\n 开仓Trade:\n %s\n 清仓Trade:\n %s", + symbol, s.TradeType, profit.Float64(), free.Float64(), strings.Join(openMsgs, "\n"), strings.Join(endMsgs, "\n")) + + s.updateAmount(ctx, profit) + s.TotalProfit = s.TotalProfit.Add(profit) + s.TotalFree = s.TotalFree.Add(free) + s.TotalOrderCount += 1 + if profit > fixedpoint.Zero { + s.TotalProfitCount += 1 + } else { + s.TotalLossCount += 1 + } + + log.Infof(msg) + bbgo.Notify(msg) + + // 重置 + s.OpenTrade = []types.Trade{} + s.EndTrade = []types.Trade{} + s.OpenQuantity = fixedpoint.Zero + s.EndQuantity = fixedpoint.Zero + + // 记得取消订单 + s.cancelOrders(ctx, symbol) + + bbgo.Notify(fmt.Sprintf("%v, 总交易次数:%v, 总收益:%v, 总手续费:%v, 盈利次数:%v, 亏损次数:%v", s.Symbol, + s.TotalOrderCount, s.TotalProfit.Float64(), s.TotalFree.Float64(), s.TotalProfitCount, s.TotalLossCount)) +} + +func (s *Strategy) updateAmount(ctx context.Context, profit fixedpoint.Value) { + // 更新amount + newAmount := s.QuantityOrAmount.Amount.Add(profit) + // 如果当前的总金额大于阶梯上的某一个值,则更新为减半 + if newAmount >= s.StageHalfAmount[s.CurrentStage] { + s.QuantityOrAmount.Amount = newAmount.Div(Two) + bbgo.Notify(fmt.Sprintf("%v 结余资金:%v", s.Symbol, s.QuantityOrAmount.Amount.Float64())) + s.CurrentStage += 1 + bbgo.Sync(ctx, s) + return + } + s.QuantityOrAmount.Amount = newAmount + + //for i, stage := range s.StageHalfAmount { + // if newAmount <= stage { + // s.CurrentStage = i + // s.QuantityOrAmount.Amount = newAmount + // bbgo.Sync(ctx, s) + // return + // } + //} +} + +// This strategy simply spent all available quote currency to buy the symbol whenever kline gets closed +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + instanceID := s.InstanceID() + + if s.State == nil { + s.State = &State{Counter: 1} + } + + 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) + } + + s.Status = types.StrategyStatusRunning + //s.OnSuspend(func() { + // _ = s.orderExecutor.GracefulCancel(ctx) + //}) + //s.OnEmergencyStop(func() { + // _ = s.orderExecutor.GracefulCancel(ctx) + // _ = s.ClosePosition(ctx, fixedpoint.One) + //}) + + 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(ctx, s) + }) + s.orderExecutor.Bind() + + bbgo.Notify("BTC滚仓CCINR策略开始运行") + s.nr = session.Indicators(s.Symbol).NR(s.NRInterval, s.NrCount, s.StrictMode) + s.cci = session.Indicators(s.Symbol).CCI(s.CCIInterval, s.CCIWindow) + s.atr = session.Indicators(s.Symbol).ATR(s.ATRInterval, s.ATRWindow) + + session.MarketDataStream.OnKLineClosed(func(k types.KLine) { + if k.Symbol != s.Symbol { + return + } + + if !s.Traded && k.Interval == s.NRInterval { + // 如若在下一根k线未成交 则取消订单 + if s.TradeType != "" && s.TradeRetry > 1 { + bbgo.Notify(fmt.Sprintf("交易信号未成交,取消订单: %s", s.Symbol)) + s.cancelOrders(ctx, s.Symbol) + } + + if s.TradeType != "" && s.TradeRetry <= 1 { + s.TradeRetry = s.TradeRetry + 1 + } + } + }) + + s.nr.OnUpdate(func(v float64) { + if s.Traded || s.nr.NrKLine.Symbol != s.Symbol { + return + } + + if !s.isTradeTime(ctx) || s.isPauseTrade(ctx) { + //pauseMsg := fmt.Sprintf("暂停交易:总收益:%v, 暂停次数:%v, 暂停时间:%v; 暂停时间段:[%v, %v)", + // s.TotalProfit.Float64(), s.PauseTradeCount.Float64(), s.PauseTradeTime, s.TradeStartHour, + // s.TradeEndHour) + //bbgo.Notify(pauseMsg) + return + } + + cciV := s.cci.Last(0) + //if cciV > 150 || cciV < -150 { + // testMsg := fmt.Sprintf("Test交易信号:币种:%s, 方向 %s, 时间: %s, 最高价:%f,最低价:%f, CCI: %v, ATR: %v", + // s.Symbol, s.TradeType, s.nr.NrKLine.GetStartTime(), s.nr.NrKLine.High.Float64(), + // s.nr.NrKLine.Low.Float64(), cciV, s.atr.Last(0)) + // bbgo.Notify(testMsg) + //} + if cciV <= s.LongCCI.Float64() { + s.TradeType = "long" + } else if cciV >= s.ShortCCI.Float64() { + s.TradeType = "short" + } else { + return + } + msg := fmt.Sprintf("交易信号:币种:%s, 方向 %s, 时间: %s, 最高价:%f,最低价:%f, CCI: %v, ATR: %v", + s.Symbol, s.TradeType, s.nr.NrKLine.GetStartTime(), s.nr.NrKLine.High.Float64(), + s.nr.NrKLine.Low.Float64(), cciV, s.atr.Last(0)) + bbgo.Notify(msg) + tk := s.nr.NrKLine + s.placeOrders(ctx, tk) + }) + + session.UserDataStream.OnOrderUpdate(func(order types.Order) { + orderSymbol := order.Symbol + if orderSymbol != s.Symbol { + return + } + if order.Status == types.OrderStatusFilled { + if order.Type == types.OrderTypeLimit && order.Side == types.SideTypeBuy { + log.Infof("the long order is filled: %+v,id is %d, symbol is %s, type is %s, status is %s", + order, order.OrderID, orderSymbol, order.Type, order.Status) + s.Traded = true + s.TradeRetry = 0 + bbgo.Notify("订单成交通知:\n 币种:%s, 方向:%s, 价格:%s, 数量:%s", order.Symbol, s.TradeType, + order.Price, order.Quantity) + } + if order.Type == types.OrderTypeLimit && order.Side == types.SideTypeSell { + log.Infof("the short order is filled: %+v,id is %d, symbol is %s, type is %s, status is %s", + order, order.OrderID, orderSymbol, order.Type, order.Status) + s.Traded = true + s.TradeRetry = 0 + bbgo.Notify("订单成交通知:\n 币种:%s, 方向:%s, 价格:%s, 数量:%s", order.Symbol, s.TradeType, + order.Price, order.Quantity) + } + + if order.Type == types.OrderTypeMarket { + log.Infof("the loss or profit order is filled: %+v,id is %d, symbol is %s, type is %s, "+ + "status is %s", order, order.OrderID, orderSymbol, order.Type, order.Status) + bbgo.Notify("订单止盈或止损通知:\n %s", order.Symbol) + s.Traded = false + s.TradeRetry = 0 + s.TradeType = "" + } else { + log.Infof("the order is: %+v,id is %d, symbol is %s, type is %s, status is %s", + order, order.OrderID, orderSymbol, order.Type, order.Status) + } + } else if order.Status == types.OrderStatusCanceled { + log.Infof("canceled order %+v", order) + } + }) + + session.UserDataStream.OnTradeUpdate(func(trade types.Trade) { + symbol := trade.Symbol + if symbol != s.Symbol { + return + } + if (trade.Side == types.SideTypeBuy && s.TradeType == "long") || (trade.Side == types.SideTypeSell && s.TradeType == "short") { + s.OpenTrade = append(s.OpenTrade, trade) + s.OpenQuantity = s.OpenQuantity.Add(trade.Quantity) + } + if (trade.Side == types.SideTypeSell && s.TradeType == "long") || (trade.Side == types.SideTypeBuy && s.TradeType == "short") { + s.EndTrade = append(s.EndTrade, trade) + s.EndQuantity = s.EndQuantity.Add(trade.Quantity) + s.notifyProfit(ctx, symbol) + } + log.Infof("trade: symbol %s, side %s, price %f, fee %f, quantity %f, buyer %v, maker %v", + symbol, trade.Side, trade.Price.Float64(), trade.Fee.Float64(), trade.Quantity.Float64(), + trade.IsBuyer, trade.IsMaker) + }) + + 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) + }) + + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + if err := s.orderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Error("unable to cancel open orders...") + } + + bbgo.Sync(ctx, s) + }) + + return nil +}