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

View File

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