Merge pull request #970 from zenixls2/fix/drift_position

refactor: extract stoploss, fix highest/lowest in trailingExit
This commit is contained in:
Yo-An Lin 2022-09-30 18:20:29 +08:00 committed by GitHub
commit 294a58cbfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 83 additions and 136 deletions

View File

@ -26,35 +26,31 @@ exchangeStrategies:
canvasPath: "./output.png"
symbol: ETHBUSD
limitOrder: false
quantity: 0.01
# kline interval for indicators
interval: 2m
window: 6
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.03
hlVarianceMultiplier: 0.13
hlRangeWindow: 4
smootherWindow: 3
fisherTransformWindow: 117
window1m: 42
smootherWindow1m: 118
fisherTransformWindow1m: 319
smootherWindow: 19
fisherTransformWindow: 73
atrWindow: 14
# orders not been traded will be canceled after `pendingMinutes` minutes
pendingMinutes: 3
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]
#driftFilterPos: 0.4
#driftFilterNeg: -0.42
#ddriftFilterPos: 0
#ddriftFilterNeg: 0
generateGraph: true
graphPNLDeductFee: true
@ -92,7 +88,7 @@ sync:
- ETHBUSD
backtest:
startTime: "2022-09-01"
startTime: "2022-09-25"
endTime: "2022-09-30"
symbols:
- ETHBUSD
@ -102,5 +98,5 @@ backtest:
makerFeeRate: 0.0000
takerFeeRate: 0.0000
balances:
ETH: 0
BUSD: 1000.0
ETH: 0.03
BUSD: 0

View File

@ -27,46 +27,42 @@ exchangeStrategies:
- on: binance
drift:
limitOrder: false
quantity: 0.001
#quantity: 0.0012
canvasPath: "./output.png"
symbol: BTCUSDT
# kline interval for indicators
interval: 1m
window: 4
stoploss: 0.02%
window: 6
useAtr: true
useStopLoss: false
stoploss: 0.04%
source: hl2
predictOffset: 2
noTrailingStopLoss: false
trailingStopLossType: realtime
# stddev on high/low-source
hlVarianceMultiplier: 0.2
hlVarianceMultiplier: 0.15
hlRangeWindow: 4
smootherWindow: 31
fisherTransformWindow: 80
window1m: 8
smootherWindow1m: 4
fisherTransformWindow1m: 320
atrWindow: 14
smootherWindow: 3
fisherTransformWindow: 181
#fisherTransformWindow: 117
atrWindow: 24
# orders not been traded will be canceled after `pendingMinutes` minutes
pendingMinutes: 3
pendingMinutes: 5
noRebalance: true
trendWindow: 185
rebalanceFilter: 1.2
#driftFilterPos: 0.5
#driftFilterNeg: -0.5
#ddriftFilterPos: 0.0008
#ddriftFilterNeg: -0.0008
trendWindow: 15
rebalanceFilter: -0.1
# ActivationRatio should be increasing order
# when farest price from entry goes over that ratio, start using the callback ratio accordingly to do trailingstop
#trailingActivationRatio: [0.01, 0.016, 0.05]
#trailingActivationRatio: [0.001, 0.0081, 0.022]
trailingActivationRatio: [0.0007, 0.0016, 0.008, 0.01]
trailingActivationRatio: [0.0004, 0.008, 0.002, 0.01]
#trailingActivationRatio: []
#trailingCallbackRate: []
#trailingCallbackRate: [0.002, 0.01, 0.1]
#trailingCallbackRate: [0.0004, 0.0009, 0.018]
trailingCallbackRate: [0.0003, 0.0005, 0.0010, 0.0016]
trailingCallbackRate: [0.00005, 0.00012, 0.0008, 0.0016]
generateGraph: true
graphPNLDeductFee: false
@ -129,7 +125,7 @@ sync:
- BTCUSDT
backtest:
startTime: "2022-09-26"
startTime: "2022-09-25"
endTime: "2022-09-30"
symbols:
- BTCUSDT
@ -140,4 +136,4 @@ backtest:
takerFeeRate: 0.000
balances:
BTC: 0
USDT: 50
USDT: 49

View File

@ -207,7 +207,7 @@ type OpenPositionOptions struct {
Leverage fixedpoint.Value `json:"leverage,omitempty" modifiable:"true"`
// Quantity will be used first, it will override the leverage if it's given
Quantity fixedpoint.Value `json:"quantity,omitempty"`
Quantity fixedpoint.Value `json:"quantity,omitempty" modifiable:"true"`
// LimitOrder set to true to open a position with a limit order
// default is false, and will send MarketOrder

View File

@ -0,0 +1,19 @@
package drift
func (s *Strategy) CheckStopLoss() bool {
stoploss := s.StopLoss.Float64()
atr := s.atr.Last()
if s.UseStopLoss {
if s.sellPrice > 0 && s.sellPrice*(1.+stoploss) <= s.highestPrice ||
s.buyPrice > 0 && s.buyPrice*(1.-stoploss) >= s.lowestPrice {
return true
}
}
if s.UseAtr {
if s.sellPrice > 0 && s.sellPrice+atr <= s.highestPrice ||
s.buyPrice > 0 && s.buyPrice-atr >= s.lowestPrice {
return true
}
}
return false
}

View File

@ -73,7 +73,6 @@ type Strategy struct {
stdevHigh *indicator.StdDev
stdevLow *indicator.StdDev
drift *DriftMA
drift1m *DriftMA
atr *indicator.ATR
midPrice fixedpoint.Value
lock sync.RWMutex `ignore:"true"`
@ -86,6 +85,8 @@ type Strategy struct {
beta float64
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"`
@ -93,9 +94,6 @@ type Strategy struct {
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"`
Window1m int `json:"window1m"`
FisherTransformWindow1m int `json:"fisherTransformWindow1m"`
SmootherWindow1m int `json:"smootherWindow1m"`
SmootherWindow int `json:"smootherWindow"`
FisherTransformWindow int `json:"fisherTransformWindow"`
ATRWindow int `json:"atrWindow"`
@ -106,11 +104,6 @@ type Strategy struct {
TrailingCallbackRate []float64 `json:"trailingCallbackRate" modifiable:"true"`
TrailingActivationRatio []float64 `json:"trailingActivationRatio" modifiable:"true"`
DriftFilterNeg float64 //`json:"driftFilterNeg" modifiable:"true"`
DriftFilterPos float64 //`json:"driftFilterPos" modifiable:"true"`
DDriftFilterNeg float64 //`json:"ddriftFilterNeg" modifiable:"true"`
DDriftFilterPos float64 //`json:"ddriftFilterPos" modifiable:"true"`
buyPrice float64 `persistence:"buy_price"`
sellPrice float64 `persistence:"sell_price"`
highestPrice float64 `persistence:"highest_price"`
@ -143,10 +136,6 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
// by default, bbgo only pre-subscribe 1000 klines.
// this is not enough if we're subscribing 30m intervals using SerialMarketDataStore
maxWindow := (s.Window + s.SmootherWindow + s.FisherTransformWindow) * s.Interval.Minutes()
maxWindow1m := s.Window1m + s.SmootherWindow1m + s.FisherTransformWindow1m
if maxWindow < maxWindow1m {
maxWindow = maxWindow1m
}
bbgo.KLinePreloadLimit = int64((maxWindow/1000 + 1) * 1000)
log.Errorf("set kLinePreloadLimit to %d, %d %d", bbgo.KLinePreloadLimit, s.Interval.Minutes(), maxWindow)
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{
@ -163,6 +152,8 @@ func (s *Strategy) CurrentPosition() *types.Position {
return s.Position
}
const closeOrderRetryLimit = 5
func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error {
order := s.p.NewMarketCloseOrder(percentage)
if order == nil {
@ -170,19 +161,20 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
}
order.Tag = "close"
order.TimeInForce = ""
balances := s.GeneralOrderExecutor.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 {
for i := 0; i < closeOrderRetryLimit; i++ {
price := s.getLastPrice()
balances := s.GeneralOrderExecutor.Session().GetAccount().Balances()
baseBalance := balances[s.Market.BaseCurrency].Available
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
}
if s.Market.IsDustQuantity(order.Quantity, price) {
return nil
}
@ -193,6 +185,7 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
}
return nil
}
return errors.New("exceed retry limit")
}
func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error {
@ -212,20 +205,6 @@ func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error {
},
}
s.drift.SeriesBase.Series = s.drift
s.drift1m = &DriftMA{
drift: &indicator.WeightedDrift{
MA: &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: types.Interval1m, Window: s.Window1m}},
IntervalWindow: types.IntervalWindow{Interval: types.Interval1m, Window: s.Window1m},
},
ma1: &indicator.EWMA{
IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.SmootherWindow1m},
},
ma2: &indicator.FisherTransform{
IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.FisherTransformWindow1m},
},
}
s.drift1m.SeriesBase.Series = s.drift1m
s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.ATRWindow}}
s.trendLine = &indicator.EWMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.TrendWindow}}
@ -256,13 +235,6 @@ func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error {
return errors.New("klines not exists")
}
log.Infof("loaded %d klines1m", klinesLength)
for _, kline := range *klines {
source := s.GetSource(&kline).Float64()
s.drift1m.Update(source, kline.Volume.Abs().Float64())
if s.drift1m.Last() != s.drift1m.Last() {
panic(fmt.Sprintf("%f %v %f %f", source, s.drift1m.drift.Values.Index(1), s.drift1m.ma2.Last(), s.drift1m.drift.LastValue))
}
}
if s.kline1m != nil && klines != nil {
s.kline1m.Set(&(*klines)[len(*klines)-1])
}
@ -278,18 +250,12 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64) (int, e
}
toCancel := false
drift := s.drift1m.Array(2)
for _, order := range nonTraded {
if order.Status != types.OrderStatusNew && order.Status != types.OrderStatusPartiallyFilled {
continue
}
log.Warnf("%v | counter: %d, system: %d", order, s.orderPendingCounter[order.OrderID], s.minutesCounter)
if s.minutesCounter-s.orderPendingCounter[order.OrderID] > s.PendingMinutes {
if order.Side == types.SideTypeBuy && drift[1] < drift[0] {
continue
} else if order.Side == types.SideTypeSell && drift[1] > drift[0] {
continue
}
toCancel = true
} else if order.Side == types.SideTypeBuy {
// 75% of the probability
@ -328,6 +294,9 @@ func (s *Strategy) trailingCheck(price float64, direction string) bool {
s.lowestPrice = price
}
isShort := direction == "short"
if isShort && s.sellPrice == 0 || !isShort && s.buyPrice == 0 {
return false
}
for i := len(s.TrailingCallbackRate) - 1; i >= 0; i-- {
trailingCallbackRate := s.TrailingCallbackRate[i]
trailingActivationRatio := s.TrailingActivationRatio[i]
@ -389,17 +358,12 @@ func (s *Strategy) initTickerFunctions(ctx context.Context) {
return
}
stoploss := s.StopLoss.Float64()
exitShortCondition := s.sellPrice > 0 && (s.sellPrice*(1.+stoploss) <= pricef ||
s.trailingCheck(pricef, "short"))
exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= pricef ||
s.trailingCheck(pricef, "long"))
exitCondition := s.CheckStopLoss() ||
s.trailingCheck(pricef, "short") || s.trailingCheck(pricef, "long")
s.positionLock.Unlock()
if exitShortCondition || exitLongCondition {
if exitCondition {
s.ClosePosition(ctx, fixedpoint.One)
log.Infof("close position by orderbook changes")
}
})
s.getLastPrice = func() (lastPrice fixedpoint.Value) {
@ -432,21 +396,16 @@ func (s *Strategy) DrawIndicators(time types.Time) *types.Canvas {
highestPrice := s.priceLines.Minus(mean).Abs().Highest(Length)
highestDrift := s.drift.Abs().Highest(Length)
hi := s.drift.drift.Abs().Highest(Length)
h1m := s.drift1m.Abs().Highest(Length * s.Interval.Minutes())
ratio := highestPrice / highestDrift
//canvas.Plot("upband", s.ma.Add(s.stdevHigh), time, Length)
canvas.Plot("ma", s.ma, time, Length)
//canvas.Plot("downband", s.ma.Minus(s.stdevLow), time, Length)
canvas.Plot("pos", types.NumberSeries(s.DriftFilterPos*ratio+mean), time, Length)
canvas.Plot("neg", types.NumberSeries(s.DriftFilterNeg*ratio+mean), time, Length)
fmt.Printf("%f %f\n", highestPrice, hi)
canvas.Plot("ppos", types.NumberSeries(s.DDriftFilterPos*(highestPrice/hi)+mean), time, Length)
canvas.Plot("nneg", types.NumberSeries(s.DDriftFilterNeg*(highestPrice/hi)+mean), time, Length)
canvas.Plot("trend", s.trendLine, time, Length)
canvas.Plot("drift", s.drift.Mul(ratio).Add(mean), time, Length)
canvas.Plot("driftOrig", s.drift.drift.Mul(highestPrice/hi).Add(mean), time, Length)
canvas.Plot("drift1m", s.drift1m.Mul(highestPrice/h1m).Add(mean), time, Length*s.Interval.Minutes(), types.Interval1m)
canvas.Plot("zero", types.NumberSeries(mean), time, Length)
canvas.Plot("price", s.priceLines, time, Length)
return canvas
@ -586,7 +545,6 @@ func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value {
func (s *Strategy) klineHandler1m(ctx context.Context, kline types.KLine) {
s.kline1m.Set(&kline)
s.drift1m.Update(s.GetSource(&kline).Float64(), kline.Volume.Abs().Float64())
if s.Status != types.StrategyStatusRunning {
return
}
@ -594,7 +552,6 @@ func (s *Strategy) klineHandler1m(ctx context.Context, kline types.KLine) {
atr := s.atr.Last()
price := s.getLastPrice()
pricef := price.Float64()
stoploss := s.StopLoss.Float64()
lowf := math.Min(kline.Low.Float64(), pricef)
highf := math.Max(kline.High.Float64(), pricef)
@ -605,11 +562,6 @@ func (s *Strategy) klineHandler1m(ctx context.Context, kline types.KLine) {
if s.highestPrice > 0 && highf > s.highestPrice {
s.highestPrice = highf
}
drift := s.drift1m.Array(2)
if len(drift) < 2 {
s.positionLock.Unlock()
return
}
numPending := 0
var err error
@ -628,13 +580,9 @@ func (s *Strategy) klineHandler1m(ctx context.Context, kline types.KLine) {
return
}
//log.Infof("d1m: %f, hf: %f, lf: %f", s.drift1m.Last(), highf, lowf)
exitShortCondition := s.sellPrice > 0 && (s.sellPrice*(1.+stoploss) <= highf ||
s.trailingCheck(highf, "short") /* || s.drift1m.Last() > 0*/)
exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= lowf ||
s.trailingCheck(lowf, "long") /* || s.drift1m.Last() < 0*/)
exitCondition := s.CheckStopLoss() || s.trailingCheck(highf, "short") || s.trailingCheck(lowf, "long")
s.positionLock.Unlock()
if exitShortCondition || exitLongCondition {
if exitCondition {
_ = s.ClosePosition(ctx, fixedpoint.One)
}
}
@ -677,7 +625,6 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
if s.Status != types.StrategyStatusRunning {
return
}
stoploss := s.StopLoss.Float64()
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)
@ -704,18 +651,6 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
s.Market.QuoteCurrency,
balances[s.Market.QuoteCurrency].String(),
)
s.DriftFilterPos = s.drift.Filter(func(i int, v float64) bool {
return v >= 0
}, 50).Mean(50)
s.DriftFilterNeg = s.drift.Filter(func(i int, v float64) bool {
return v <= 0
}, 50).Mean(50)
s.DDriftFilterPos = s.drift.drift.Filter(func(i int, v float64) bool {
return v >= 0
}, 50).Mean(50)
s.DDriftFilterNeg = s.drift.drift.Filter(func(i int, v float64) bool {
return v <= 0
}, 50).Mean(50)
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)
@ -726,12 +661,9 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
shortCondition = false
}
}
exitShortCondition := s.sellPrice > 0 && (s.sellPrice*(1.+stoploss) <= highf ||
s.trailingCheck(pricef, "short"))
exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= lowf ||
s.trailingCheck(pricef, "long"))
exitCondition := s.CheckStopLoss() || s.trailingCheck(pricef, "short") || s.trailingCheck(pricef, "long")
if exitShortCondition || exitLongCondition {
if exitCondition {
s.positionLock.Unlock()
if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil {
log.WithError(err).Errorf("cannot cancel orders")
@ -918,7 +850,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.sellPrice = s.p.ApproximateAverageCost.Float64() //trade.Price.Float64()
s.buyPrice = 0
s.highestPrice = 0
s.lowestPrice = math.Min(s.lowestPrice, s.sellPrice)
if s.lowestPrice == 0 {
s.lowestPrice = s.sellPrice
} else {
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())
})