optimize: drift strategy to use market trade signals

This commit is contained in:
zenix 2022-10-20 20:03:10 +09:00
parent a15d125679
commit ce86544c43
4 changed files with 145 additions and 287 deletions

View File

@ -1,102 +0,0 @@
---
persistence:
redis:
host: 127.0.0.1
port: 6379
db: 0
sessions:
binance:
exchange: binance
futures: false
envVarPrefix: binance
heikinAshi: false
# Drift strategy intends to place buy/sell orders as much value mas it could be. To exchanges that requires to
# calculate fees before placing limit orders (e.g. FTX Pro), make sure the fee rate is configured correctly and
# enable modifyOrderAmountForFee to prevent order rejection.
makerFeeRate: 0.0002
takerFeeRate: 0.0007
modifyOrderAmountForFee: false
exchangeStrategies:
- on: binance
drift:
canvasPath: "./output.png"
symbol: ETHBUSD
limitOrder: false
quantity: 0.01
# kline interval for indicators
interval: 1m
window: 1
useAtr: true
useStopLoss: true
stoploss: 0.23%
source: ohlc4
predictOffset: 2
noTrailingStopLoss: false
trailingStopLossType: kline
# stddev on high/low-source
hlVarianceMultiplier: 0.13
hlRangeWindow: 4
smootherWindow: 19
fisherTransformWindow: 73
atrWindow: 14
# orders not been traded will be canceled after `pendingMinutes` minutes
pendingMinutes: 5
noRebalance: true
trendWindow: 12
rebalanceFilter: 2
trailingActivationRatio: [0.0015, 0.002, 0.004, 0.01]
trailingCallbackRate: [0.0001, 0.00012, 0.001, 0.002]
generateGraph: true
graphPNLDeductFee: true
graphPNLPath: "./pnl.png"
graphCumPNLPath: "./cumpnl.png"
#exits:
#- roiStopLoss:
# percentage: 0.8%
#- roiTakeProfit:
# percentage: 35%
#- protectiveStopLoss:
# activationRatio: 0.6%
# stopLossRatio: 0.1%
# placeStopOrder: false
#- protectiveStopLoss:
# activationRatio: 5%
# stopLossRatio: 1%
# placeStopOrder: false
#- cumulatedVolumeTakeProfit:
# interval: 5m
# window: 2
# minQuoteVolume: 200_000_000
#- protectiveStopLoss:
# activationRatio: 2%
# stopLossRatio: 1%
# placeStopOrder: false
sync:
userDataStream:
trades: true
filledOrders: true
sessions:
- binance
symbols:
- ETHBUSD
backtest:
startTime: "2022-09-25"
endTime: "2022-09-30"
symbols:
- ETHBUSD
sessions: [binance]
accounts:
binance:
makerFeeRate: 0.0000
takerFeeRate: 0.0000
balances:
ETH: 0.03
BUSD: 0

View File

@ -26,6 +26,7 @@ exchangeStrategies:
- on: binance
drift:
debug: false
minInterval: 1s
limitOrder: true
#quantity: 0.0012
@ -33,26 +34,24 @@ exchangeStrategies:
symbol: BTCUSDT
# kline interval for indicators
interval: 1s
window: 6
window: 2
useAtr: true
useStopLoss: true
stoploss: 0.05%
stoploss: 0.01%
source: hl2
predictOffset: 2
noTrailingStopLoss: false
trailingStopLossType: kline
noTrailingStopLoss: true
# stddev on high/low-source
hlVarianceMultiplier: 0.14
hlRangeWindow: 4
smootherWindow: 3
fisherTransformWindow: 125
#fisherTransformWindow: 117
hlVarianceMultiplier: 0.7
hlRangeWindow: 6
smootherWindow: 10
fisherTransformWindow: 45
atrWindow: 24
# orders not been traded will be canceled after `pendingMinutes` minutes
pendingMinutes: 10
pendingMinInterval: 6
noRebalance: true
trendWindow: 15
rebalanceFilter: -0.1
trendWindow: 4
rebalanceFilter: 2
# ActivationRatio should be increasing order
# when farest price from entry goes over that ratio, start using the callback ratio accordingly to do trailingstop
@ -126,8 +125,8 @@ sync:
- BTCUSDT
backtest:
startTime: "2022-10-18"
endTime: "2022-10-19"
startTime: "2022-10-19"
endTime: "2022-10-20"
symbols:
- BTCUSDT
sessions: [binance]

View File

@ -66,57 +66,58 @@ type Strategy struct {
*types.TradeStats `persistence:"trade_stats"`
p *types.Position
MinInterval types.Interval `json:"MinInterval"`
MinInterval types.Interval `json:"MinInterval"` // minimum interval referred for doing stoploss/trailing exists and updating highest/lowest
priceLines *types.Queue
trendLine types.UpdatableSeriesExtend
ma types.UpdatableSeriesExtend
stdevHigh *indicator.StdDev
stdevLow *indicator.StdDev
drift *DriftMA
atr *indicator.ATR
midPrice fixedpoint.Value
lock sync.RWMutex `ignore:"true"`
positionLock sync.RWMutex `ignore:"true"`
startTime time.Time
counter int
orderPendingCounter map[uint64]int
frameKLine *types.KLine
klineMin *types.KLine
priceLines *types.Queue
trendLine types.UpdatableSeriesExtend
ma types.UpdatableSeriesExtend
stdevHigh *indicator.StdDev
stdevLow *indicator.StdDev
drift *DriftMA
atr *indicator.ATR
midPrice fixedpoint.Value // the midPrice is the average of bestBid and bestAsk in public orderbook
lock sync.RWMutex `ignore:"true"` // lock for midPrice
positionLock sync.RWMutex `ignore:"true"` // lock for highest/lowest and p
startTime time.Time // trading start time
counter int // number of MinInterval since startTime
maxCounterBuyCanceled int // the largest counter of the order on the buy side been cancelled. meaning the latest cancelled buy order.
maxCounterSellCanceled int // the largest counter of the order on the sell side been cancelled. meaning the latest cancelled sell order.
orderPendingCounter map[uint64]int // records the timepoint when the orders are created, using the counter at the time.
frameKLine *types.KLine // last kline in Interval
klineMin *types.KLine // last kline in MinInterval
beta float64
beta float64 // last beta value from trendline's linear regression (previous slope of the trendline)
UseStopLoss bool `json:"useStopLoss" modifiable:"true"`
UseAtr bool `json:"useAtr" modifiable:"true"`
StopLoss fixedpoint.Value `json:"stoploss" modifiable:"true"`
CanvasPath string `json:"canvasPath"`
PredictOffset int `json:"predictOffset"`
HighLowVarianceMultiplier float64 `json:"hlVarianceMultiplier" modifiable:"true"`
NoTrailingStopLoss bool `json:"noTrailingStopLoss" modifiable:"true"`
TrailingStopLossType string `json:"trailingStopLossType" modifiable:"true"` // trailing stop sources. Possible options are `kline` for 1m kline and `realtime` from order updates
HLRangeWindow int `json:"hlRangeWindow"`
SmootherWindow int `json:"smootherWindow"`
FisherTransformWindow int `json:"fisherTransformWindow"`
ATRWindow int `json:"atrWindow"`
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
Debug bool `json:"debug" modifiable:"true"` // to print debug message or not
UseStopLoss bool `json:"useStopLoss" modifiable:"true"` // whether to use stoploss rate to do stoploss
UseAtr bool `json:"useAtr" modifiable:"true"` // use atr as stoploss
StopLoss fixedpoint.Value `json:"stoploss" modifiable:"true"` // stoploss rate
PredictOffset int `json:"predictOffset"` // the lookback length for the prediction using linear regression
HighLowVarianceMultiplier float64 `json:"hlVarianceMultiplier" modifiable:"true"` // modifier to set the limit order price
NoTrailingStopLoss bool `json:"noTrailingStopLoss" modifiable:"true"` // turn off the trailing exit and stoploss
HLRangeWindow int `json:"hlRangeWindow"` // ma window for kline high/low changes
SmootherWindow int `json:"smootherWindow"` // window that controls the smoothness of drift
FisherTransformWindow int `json:"fisherTransformWindow"` // fisher transform window to filter drift's negative signals
ATRWindow int `json:"atrWindow"` // window for atr indicator
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"`
buyPrice float64 `persistence:"buy_price"`
sellPrice float64 `persistence:"sell_price"`
highestPrice float64 `persistence:"highest_price"`
lowestPrice float64 `persistence:"lowest_price"`
buyPrice float64 `persistence:"buy_price"` // price when a long position is opened
sellPrice float64 `persistence:"sell_price"` // price when a short position is opened
highestPrice float64 `persistence:"highest_price"` // highestPrice when the position is opened
lowestPrice float64 `persistence:"lowest_price"` // lowestPrice when the position is opened
// This is not related to trade but for statistics graph generation
// Will deduct fee in percentage from every trade
GraphPNLDeductFee bool `json:"graphPNLDeductFee"`
GraphPNLPath string `json:"graphPNLPath"`
GraphCumPNLPath string `json:"graphCumPNLPath"`
// Whether to generate graph when shutdown
GenerateGraph bool `json:"generateGraph"`
CanvasPath string `json:"canvasPath"` // backtest related. the path to store the indicator graph
GraphPNLPath string `json:"graphPNLPath"` // backtest related. the path to store the pnl % graph per trade graph.
GraphCumPNLPath string `json:"graphCumPNLPath"` // backtest related. the path to store the asset changes in graph
GenerateGraph bool `json:"generateGraph"` // whether to generate graph when shutdown
ExitMethods bbgo.ExitMethodSet `json:"exits"`
Session *bbgo.ExchangeSession
@ -139,7 +140,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
if !bbgo.IsBackTesting {
session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{})
session.Subscribe(types.AggTradeChannel, s.Symbol, types.SubscribeOptions{})
session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{})
// able to preload
if s.MinInterval.Milliseconds() >= types.Interval1s.Milliseconds() && s.MinInterval.Milliseconds()%types.Interval1s.Milliseconds() == 0 {
maxWindow := (s.Window + s.SmootherWindow + s.FisherTransformWindow) * (s.Interval.Milliseconds() / s.MinInterval.Milliseconds())
@ -296,6 +297,15 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64) (int, e
err := s.GeneralOrderExecutor.CancelNoWait(ctx)
// TODO: clean orderPendingCounter on cancel/trade
for _, order := range nonTraded {
if order.Side == types.SideTypeSell {
if s.maxCounterSellCanceled < s.orderPendingCounter[order.OrderID] {
s.maxCounterSellCanceled = s.orderPendingCounter[order.OrderID]
}
} else {
if s.maxCounterBuyCanceled < s.orderPendingCounter[order.OrderID] {
s.maxCounterBuyCanceled = s.orderPendingCounter[order.OrderID]
}
}
delete(s.orderPendingCounter, order.OrderID)
}
log.Warnf("cancel all %v", err)
@ -346,7 +356,6 @@ func (s *Strategy) initTickerFunctions(ctx context.Context) {
bestBid := ticker.Buy
bestAsk := ticker.Sell
var pricef float64
if !util.TryLock(&s.lock) {
return
}
@ -357,37 +366,10 @@ func (s *Strategy) initTickerFunctions(ctx context.Context) {
} else {
s.midPrice = bestBid
}
pricef = s.midPrice.Float64()
s.lock.Unlock()
if !util.TryLock(&s.positionLock) {
return
}
// we removed realtime stoploss and trailingStop.
if s.highestPrice > 0 && s.highestPrice < pricef {
s.highestPrice = pricef
}
if s.lowestPrice > 0 && s.lowestPrice > pricef {
s.lowestPrice = pricef
}
if s.CheckStopLoss() {
s.positionLock.Unlock()
s.ClosePosition(ctx, fixedpoint.One)
return
}
// for trailing stoploss during the realtime
if s.NoTrailingStopLoss || s.TrailingStopLossType == "kline" {
s.positionLock.Unlock()
return
}
exitCondition := s.trailingCheck(pricef, "short") || s.trailingCheck(pricef, "long")
s.positionLock.Unlock()
if exitCondition {
s.ClosePosition(ctx, fixedpoint.One)
}
})
s.getLastPrice = func() (lastPrice fixedpoint.Value) {
var ok bool
@ -585,35 +567,29 @@ func (s *Strategy) klineHandlerMin(ctx context.Context, kline types.KLine) {
if s.highestPrice > 0 && highf > s.highestPrice {
s.highestPrice = highf
}
s.positionLock.Unlock()
numPending := 0
var err error
if numPending, err = s.smartCancel(ctx, pricef, atr); err != nil {
log.WithError(err).Errorf("cannot cancel orders")
s.positionLock.Unlock()
return
}
if numPending > 0 {
s.positionLock.Unlock()
return
}
if s.NoTrailingStopLoss || s.TrailingStopLossType == "realtime" {
s.positionLock.Unlock()
if s.NoTrailingStopLoss {
return
}
exitCondition := s.CheckStopLoss() || s.trailingCheck(highf, "short") || s.trailingCheck(lowf, "long")
s.positionLock.Unlock()
if exitCondition {
_ = s.ClosePosition(ctx, fixedpoint.One)
}
}
func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
var driftPred, atr float64
var drift []float64
s.frameKLine.Set(&kline)
source := s.GetSource(&kline)
@ -625,9 +601,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
s.atr.PushK(kline)
driftPred = s.drift.Predict(s.PredictOffset)
ddriftPred := s.drift.drift.Predict(s.PredictOffset)
atr = s.atr.Last()
atr := s.atr.Last()
price := s.getLastPrice()
pricef := price.Float64()
lowf := math.Min(kline.Low.Float64(), pricef)
@ -636,7 +610,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
s.stdevLow.Update(lowdiff)
highdiff := highf - s.ma.Last()
s.stdevHigh.Update(highdiff)
drift = s.drift.Array(2)
drift := s.drift.Array(2)
if len(drift) < 2 || len(drift) < s.PredictOffset {
return
}
@ -649,77 +623,78 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
return
}
s.positionLock.Lock()
log.Infof("highdiff: %3.2f ma: %.2f, open: %8v, close: %8v, high: %8v, low: %8v, time: %v %v", s.stdevHigh.Last(), s.ma.Last(), kline.Open, kline.Close, kline.High, kline.Low, kline.StartTime, kline.EndTime)
s.positionLock.Lock()
if s.lowestPrice > 0 && lowf < s.lowestPrice {
s.lowestPrice = lowf
}
if s.highestPrice > 0 && highf > s.highestPrice {
s.highestPrice = highf
}
s.positionLock.Unlock()
if !s.NoRebalance {
s.Rebalance(ctx)
}
balances := s.GeneralOrderExecutor.Session().GetAccount().Balances()
bbgo.Notify("source: %.4f, price: %.4f, driftPred: %.4f, ddriftPred: %.4f, drift[1]: %.4f, ddrift[1]: %.4f, atr: %.4f, lowf %.4f, highf: %.4f lowest: %.4f highest: %.4f sp %.4f bp %.4f",
sourcef, pricef, driftPred, ddriftPred, drift[1], ddrift[1], atr, lowf, highf, s.lowestPrice, s.highestPrice, s.sellPrice, s.buyPrice)
// Notify will parse args to strings and process separately
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(),
)
if s.Debug {
balances := s.GeneralOrderExecutor.Session().GetAccount().Balances()
bbgo.Notify("source: %.4f, price: %.4f, drift[0]: %.4f, ddrift[0]: %.4f, lowf %.4f, highf: %.4f lowest: %.4f highest: %.4f sp %.4f bp %.4f",
sourcef, pricef, drift[0], ddrift[0], atr, lowf, highf, s.lowestPrice, s.highestPrice, s.sellPrice, s.buyPrice)
// Notify will parse args to strings and process separately
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 := drift[1] >= 0 && drift[0] <= 0 || (drift[1] >= drift[0] && drift[1] <= 0) || ddrift[1] >= 0 && ddrift[0] <= 0 || (ddrift[1] >= ddrift[0] && ddrift[1] <= 0)
longCondition := drift[1] <= 0 && drift[0] >= 0 || (drift[1] <= drift[0] && drift[1] >= 0) || ddrift[1] <= 0 && ddrift[0] >= 0 || (ddrift[1] <= ddrift[0] && ddrift[1] >= 0)
if shortCondition && longCondition {
if drift[1] > drift[0] {
if s.priceLines.Index(1) > s.priceLines.Last() {
longCondition = false
} else {
shortCondition = false
}
}
exitCondition := s.CheckStopLoss() || s.trailingCheck(pricef, "short") || s.trailingCheck(pricef, "long")
exitCondition := !s.NoTrailingStopLoss && (s.CheckStopLoss() || s.trailingCheck(pricef, "short") || s.trailingCheck(pricef, "long"))
if exitCondition {
s.positionLock.Unlock()
if err := s.GeneralOrderExecutor.CancelNoWait(ctx); err != nil {
if exitCondition || longCondition || shortCondition {
var err error
var hold int
if hold, err = s.smartCancel(ctx, sourcef, atr); err != nil {
log.WithError(err).Errorf("cannot cancel orders")
}
if hold > 0 {
return
}
_ = s.ClosePosition(ctx, fixedpoint.One)
if shortCondition || longCondition {
s.positionLock.Lock()
} else {
return
} else {
if _, err := s.smartCancel(ctx, sourcef, atr); err != nil {
log.WithError(err).Errorf("cannot cancel orders")
}
return
}
if longCondition {
if err := s.GeneralOrderExecutor.CancelNoWait(ctx); err != nil {
log.WithError(err).Errorf("cannot cancel orders")
s.positionLock.Unlock()
return
}
source = source.Sub(fixedpoint.NewFromFloat(s.stdevLow.Last() * s.HighLowVarianceMultiplier))
if source.Compare(price) > 0 {
source = price
}
/*source = fixedpoint.NewFromFloat(s.ma.Last() - s.stdevLow.Last()*s.HighLowVarianceMultiplier)
if source.Compare(price) > 0 {
source = price
}
sourcef = source.Float64()*/
log.Infof("source in long %v %v %f", source, price, s.stdevLow.Last())
s.positionLock.Unlock()
opt := s.OpenPositionOptions
opt.Long = true
opt.LimitOrder = true
// force to use market taker
if s.counter-s.maxCounterBuyCanceled <= 1 {
opt.LimitOrder = false
}
opt.Price = source
opt.Tags = []string{"long"}
createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt)
@ -738,28 +713,20 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
return
}
if shortCondition {
if err := s.GeneralOrderExecutor.CancelNoWait(ctx); err != nil {
log.WithError(err).Errorf("cannot cancel orders")
s.positionLock.Unlock()
return
}
source = source.Add(fixedpoint.NewFromFloat(s.stdevHigh.Last() * s.HighLowVarianceMultiplier))
if source.Compare(price) < 0 {
source = price
}
/*source = fixedpoint.NewFromFloat(s.ma.Last() + s.stdevHigh.Last()*s.HighLowVarianceMultiplier)
if source.Compare(price) < 0 {
source = price
}
sourcef = source.Float64()*/
log.Infof("source in short: %v", source)
s.positionLock.Unlock()
opt := s.OpenPositionOptions
opt.Short = true
opt.Price = source
opt.LimitOrder = true
if s.counter-s.maxCounterSellCanceled <= 1 {
opt.LimitOrder = false
}
opt.Tags = []string{"short"}
createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt)
if err != nil {
@ -775,7 +742,6 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
}
return
}
s.positionLock.Unlock()
}
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
@ -842,13 +808,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
s.GeneralOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _profit, _netProfit fixedpoint.Value) {
s.p.AddTrade(trade)
order, ok := s.GeneralOrderExecutor.TradeCollector().OrderStore().Get(trade.OrderID)
if !ok {
panic(fmt.Sprintf("cannot find order: %v", trade))
}
tag := order.Tag
price := trade.Price.Float64()
delete(s.orderPendingCounter, trade.OrderID)
if s.buyPrice > 0 {
profit.Update(modify(price / s.buyPrice))
@ -858,19 +819,18 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
cumProfit.Update(s.CalcAssetValue(trade.Price).Float64())
}
s.positionLock.Lock()
defer s.positionLock.Unlock()
if s.p.IsDust(trade.Price) {
s.buyPrice = 0
s.sellPrice = 0
s.highestPrice = 0
s.lowestPrice = 0
} else if s.p.IsLong() {
s.buyPrice = s.p.ApproximateAverageCost.Float64() // trade.Price.Float64()
s.buyPrice = s.p.ApproximateAverageCost.Float64()
s.sellPrice = 0
s.highestPrice = math.Max(s.buyPrice, s.highestPrice)
s.lowestPrice = s.buyPrice
} else if s.p.IsShort() {
s.sellPrice = s.p.ApproximateAverageCost.Float64() // trade.Price.Float64()
s.sellPrice = s.p.ApproximateAverageCost.Float64()
s.buyPrice = 0
s.highestPrice = s.sellPrice
if s.lowestPrice == 0 {
@ -879,7 +839,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.lowestPrice = math.Min(s.lowestPrice, s.sellPrice)
}
}
bbgo.Notify("tag: %s, sp: %.4f bp: %.4f hp: %.4f lp: %.4f, trade: %s, pos: %s", tag, s.sellPrice, s.buyPrice, s.highestPrice, s.lowestPrice, trade.String(), s.p.String())
s.positionLock.Unlock()
})
s.frameKLine = &types.KLine{}
@ -891,42 +851,43 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, s.startTime))
s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, s.startTime))
// default value: use 1m kline
if !s.NoTrailingStopLoss && s.IsBackTesting() || s.TrailingStopLossType == "" {
s.TrailingStopLossType = "kline"
}
bbgo.RegisterCommand("/draw", "Draw Indicators", func(reply interact.Reply) {
canvas := s.DrawIndicators(s.frameKLine.StartTime)
var buffer bytes.Buffer
if err := canvas.Render(chart.PNG, &buffer); err != nil {
log.WithError(err).Errorf("cannot render indicators in drift")
reply.Message(fmt.Sprintf("[error] cannot render indicators in drift: %v", err))
return
}
bbgo.SendPhoto(&buffer)
go func() {
canvas := s.DrawIndicators(s.frameKLine.StartTime)
var buffer bytes.Buffer
if err := canvas.Render(chart.PNG, &buffer); err != nil {
log.WithError(err).Errorf("cannot render indicators in drift")
reply.Message(fmt.Sprintf("[error] cannot render indicators in drift: %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 drift: %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 drift")
reply.Message(fmt.Sprintf("[error] cannot render pnl in drift: %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 drift")
reply.Message(fmt.Sprintf("[error] canot render cumpnl in drift: %v", err))
return
}
bbgo.SendPhoto(&buffer)
}()
})
bbgo.RegisterCommand("/config", "Show latest config", func(reply interact.Reply) {
@ -965,7 +926,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
store.OnKLineClosed(func(kline types.KLine) {
s.counter = int(kline.StartTime.Time().Add(kline.Interval.Duration()).Sub(s.startTime).Milliseconds())
s.counter = int(kline.StartTime.Time().Add(kline.Interval.Duration()).Sub(s.startTime).Milliseconds()) / s.MinInterval.Milliseconds()
if kline.Interval == s.Interval {
s.klineHandler(ctx, kline)
} else if kline.Interval == s.MinInterval {

View File

@ -104,7 +104,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
// this is not enough if we're subscribing 30m intervals using SerialMarketDataStore
if !bbgo.IsBackTesting {
session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{})
session.Subscribe(types.AggTradeChannel, s.Symbol, types.SubscribeOptions{})
session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{})
if s.MinInterval.Milliseconds() >= types.Interval1s.Milliseconds() && s.MinInterval.Milliseconds()%types.Interval1s.Milliseconds() == 0 {
bbgo.KLinePreloadLimit = int64(((s.Interval.Milliseconds()/s.MinInterval.Milliseconds())*s.WindowSlow/1000 + 1) + 1000)
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{