use trailingstop

This commit is contained in:
zenix 2022-04-13 10:43:31 +09:00
parent 574c5b77b1
commit a0e218a5c6

View File

@ -2,6 +2,7 @@ package ewo_dgtrd
import ( import (
"context" "context"
"sync"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -21,21 +22,27 @@ func init() {
} }
type Strategy struct { type Strategy struct {
Symbol string `json:"symbol"` *bbgo.Graceful
Interval types.Interval `json:"interval"` bbgo.SmartStops
Threshold float64 `json:"threshold"` // strength threshold tradeCollector *bbgo.TradeCollector
UseEma bool `json:"useEma"` // use exponential ma or simple ma Symbol string `json:"symbol"`
SignalWindow int `json:"sigWin"` // signal window Interval types.Interval `json:"interval"`
StopLoss fixedpoint.Value `json:"stoploss"` // stop price = latest price * (1 - stoploss) UseEma bool `json:"useEma"` // use exponential ma or simple ma
SignalWindow int `json:"sigWin"` // signal window
} }
func (s *Strategy) ID() string { func (s *Strategy) ID() string {
return ID return ID
} }
func (s *Strategy) Initialize() error {
return s.SmartStops.InitializeStopControllers(s.Symbol)
}
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
log.Infof("subscribe %s", s.Symbol) log.Infof("subscribe %s", s.Symbol)
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval.String()}) session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval.String()})
s.SmartStops.Subscribe(session)
} }
type EwoSignal interface { type EwoSignal interface {
@ -44,38 +51,56 @@ type EwoSignal interface {
} }
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 {
log.Infof("stoploss: %v", s.StopLoss)
indicatorSet, ok := session.StandardIndicatorSet(s.Symbol)
if !ok {
log.Errorf("cannot get indicatorSet of %s", s.Symbol)
return nil
}
orders, ok := session.OrderStore(s.Symbol)
if !ok {
log.Errorf("cannot get orderbook of %s", s.Symbol)
return nil
}
/*store, ok := session.MarketDataStore(s.Symbol)
if !ok {
log.Errorf("cannot get marketdatastore of %s", s.Symbol)
return nil
}*/
market, ok := session.Market(s.Symbol) market, ok := session.Market(s.Symbol)
if !ok { if !ok {
log.Errorf("fetch market fail %s", s.Symbol) log.Errorf("fetch market fail %s", s.Symbol)
return nil return nil
} }
indicatorSet, ok := session.StandardIndicatorSet(s.Symbol)
if !ok {
log.Errorf("cannot get indicatorSet of %s", s.Symbol)
return nil
}
orderbook, ok := session.OrderStore(s.Symbol)
if !ok {
log.Errorf("cannot get orderbook of %s", s.Symbol)
return nil
}
position := types.NewPositionFromMarket(market)
s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, position, orderbook)
s.tradeCollector.OnTrade(func(trade types.Trade, profit, netprofit fixedpoint.Value) {
if !profit.IsZero() {
log.Warnf("generate profit: %v, netprofit: %v", profit, netprofit)
}
})
s.tradeCollector.OnPositionUpdate(func(position *types.Position) {
log.Infof("position changed: %s", position)
})
s.tradeCollector.BindStream(session.UserDataStream)
s.SmartStops.RunStopControllers(ctx, session, s.tradeCollector)
/*store, ok := session.MarketDataStore(s.Symbol)
if !ok {
log.Errorf("cannot get marketdatastore of %s", s.Symbol)
return nil
}*/
/*window, ok := store.KLinesOfInterval(s.Interval) /*window, ok := store.KLinesOfInterval(s.Interval)
if !ok { if !ok {
log.Errorf("cannot get klinewindow of %s", s.Interval) log.Errorf("cannot get klinewindow of %s", s.Interval)
}*/ }*/
var ma5, ma34, ewo types.Series var ma5, ma34, ma50, ewo types.Series
if s.UseEma { if s.UseEma {
ma5 = indicatorSet.EWMA(types.IntervalWindow{s.Interval, 5}) ma5 = indicatorSet.EWMA(types.IntervalWindow{s.Interval, 5})
ma34 = indicatorSet.EWMA(types.IntervalWindow{s.Interval, 34}) ma34 = indicatorSet.EWMA(types.IntervalWindow{s.Interval, 34})
ma50 = indicatorSet.EWMA(types.IntervalWindow{s.Interval, 50})
} else { } else {
ma5 = indicatorSet.SMA(types.IntervalWindow{s.Interval, 5}) ma5 = indicatorSet.SMA(types.IntervalWindow{s.Interval, 5})
ma34 = indicatorSet.SMA(types.IntervalWindow{s.Interval, 34}) ma34 = indicatorSet.SMA(types.IntervalWindow{s.Interval, 34})
ma50 = indicatorSet.SMA(types.IntervalWindow{s.Interval, 50})
} }
ewo = types.Mul(types.Minus(types.Div(ma5, ma34), 1.0), 100.) ewo = types.Mul(types.Minus(types.Div(ma5, ma34), 1.0), 100.)
var ewoSignal EwoSignal var ewoSignal EwoSignal
@ -84,9 +109,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
} else { } else {
ewoSignal = &indicator.SMA{IntervalWindow: types.IntervalWindow{s.Interval, s.SignalWindow}} ewoSignal = &indicator.SMA{IntervalWindow: types.IntervalWindow{s.Interval, s.SignalWindow}}
} }
entryPrice := fixedpoint.Zero
stopPrice := fixedpoint.Zero
tradeDirectionLong := true
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
if kline.Symbol != s.Symbol { if kline.Symbol != s.Symbol {
return return
@ -104,131 +126,99 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
} }
} }
lastPrice, ok := session.LastPrice(s.Symbol) /*lastPrice, ok := session.LastPrice(s.Symbol)
if !ok { if !ok {
log.Errorf("cannot get last price")
return return
}*/
// cancel non-traded orders
var toCancel []types.Order
var toRepost []types.SubmitOrder
for _, order := range orderbook.Orders() {
if order.Status == types.OrderStatusNew || order.Status == types.OrderStatusPartiallyFilled {
toCancel = append(toCancel, order)
}
}
if len(toCancel) > 0 {
if err := orderExecutor.CancelOrders(ctx, toCancel...); err != nil {
log.WithError(err).Errorf("cancel order error")
}
s.tradeCollector.Process()
} }
// stoploss // well, only track prices on 1m
if tradeDirectionLong && kline.Low.Compare(stopPrice) <= 0 && !stopPrice.IsZero() { if kline.Interval != s.Interval {
balances := session.Account.Balances() for _, order := range toCancel {
baseBalance := balances[market.BaseCurrency].Available.Mul(modifier) if order.Side == types.SideTypeBuy && order.Price.Compare(kline.Low) < 0 {
baseAmount := baseBalance.Mul(lastPrice) order.Quantity = order.Quantity.Mul(order.Price).Div(kline.Low)
if baseBalance.Sign() <= 0 || order.Price = kline.Low
baseBalance.Compare(market.MinQuantity) < 0 || toRepost = append(toRepost, order.SubmitOrder)
baseAmount.Compare(market.MinNotional) < 0 { } else if order.Side == types.SideTypeSell && order.Price.Compare(kline.High) > 0 {
} else { order.Price = kline.High
_, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ toRepost = append(toRepost, order.SubmitOrder)
Symbol: kline.Symbol, }
Side: types.SideTypeSell, }
Type: types.OrderTypeMarket,
Quantity: baseBalance, if len(toRepost) > 0 {
Price: lastPrice, createdOrders, err := orderExecutor.SubmitOrders(ctx, toRepost...)
Market: market,
TimeInForce: types.TimeInForceGTC,
})
if err != nil { if err != nil {
log.WithError(err).Errorf("cannot place order for stoploss") log.WithError(err).Errorf("cannot place order")
return return
} }
log.Warnf("StopLoss Long at %v", lastPrice) log.Infof("repost order %v", createdOrders)
entryPrice = fixedpoint.Zero s.tradeCollector.Process()
stopPrice = fixedpoint.Zero
}
} else if !tradeDirectionLong && kline.High.Compare(stopPrice) >= 0 && !stopPrice.IsZero() {
quoteBalance, ok := session.Account.Balance(market.QuoteCurrency)
if !ok {
return
}
quantityAmount := quoteBalance.Available
totalQuantity := quantityAmount.Div(lastPrice).Mul(modifier)
if quantityAmount.Sign() <= 0 ||
quantityAmount.Compare(market.MinNotional) < 0 ||
totalQuantity.Compare(market.MinQuantity) < 0 {
} else {
_, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: kline.Symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeMarket,
Quantity: totalQuantity,
Price: lastPrice,
Market: market,
TimeInForce: types.TimeInForceGTC,
})
if err != nil {
log.WithError(err).Errorf("cannot place order for stoploss")
return
}
log.Warnf("StopLoss Short at %v", lastPrice)
entryPrice = fixedpoint.Zero
stopPrice = fixedpoint.Zero
} }
} }
if kline.Interval != s.Interval { if kline.Interval != s.Interval {
return return
} }
var toCancel []types.Order
for _, order := range orders.Orders() { // To get the threshold for ewo
if order.Status == types.OrderStatusNew || order.Status == types.OrderStatusPartiallyFilled { mean := types.Mean(types.Abs(ewo), 7)
toCancel = append(toCancel, order)
}
}
if err := orderExecutor.CancelOrders(ctx, toCancel...); err != nil {
log.WithError(err).Errorf("cancel order error")
}
longSignal := types.CrossOver(ewo, ewoSignal) longSignal := types.CrossOver(ewo, ewoSignal)
shortSignal := types.CrossUnder(ewo, ewoSignal) shortSignal := types.CrossUnder(ewo, ewoSignal)
IsBull := kline.Close.Compare(kline.Open) > 0 bull := types.Predict(ma50, 50, 2) > ma50.Last()
// kline breakthrough ma5, ma50 trend up, and ewo > threshold
IsBull := bull && kline.High.Float64() > ma5.Last() && ewo.Last() > mean
// kline downthrough ma5, ma50 trend down, and ewo < threshold
IsBear := !bull && kline.Low.Float64() < ma5.Last() && ewo.Last() < -mean
var orders []types.SubmitOrder var orders []types.SubmitOrder
price := lastPrice
if longSignal.Index(1) && !shortSignal.Last() && IsBull { if longSignal.Index(1) && !shortSignal.Last() && IsBull {
price := kline.Low
quoteBalance, ok := session.Account.Balance(market.QuoteCurrency) quoteBalance, ok := session.Account.Balance(market.QuoteCurrency)
if !ok { if !ok {
return return
} }
quantityAmount := quoteBalance.Available quantityAmount := quoteBalance.Available.Mul(modifier)
totalQuantity := quantityAmount.Div(price).Mul(modifier).Div(types.Two) totalQuantity := quantityAmount.Div(price)
if quantityAmount.Sign() <= 0 || if quantityAmount.Sign() <= 0 ||
quantityAmount.Compare(market.MinNotional) < 0 || quantityAmount.Compare(market.MinNotional) < 0 ||
totalQuantity.Compare(market.MinQuantity) < 0 { totalQuantity.Compare(market.MinQuantity) < 0 {
log.Infof("quote balance %v is not enough. stop generating buy orders", quoteBalance) log.Infof("quote balance %v is not enough. stop generating buy orders", quoteBalance)
return return
} }
if ewo.Last() < -s.Threshold { // strong long
// strong long log.Infof("long at %v, timestamp: %s", price, kline.StartTime)
log.Infof("strong long at %v, timestamp: %s", price, kline.StartTime)
orders = append(orders, types.SubmitOrder{ orders = append(orders, types.SubmitOrder{
Symbol: kline.Symbol, Symbol: kline.Symbol,
Side: types.SideTypeBuy, Side: types.SideTypeBuy,
Type: types.OrderTypeMarket, Type: types.OrderTypeLimit,
Price: price, Price: price,
Quantity: totalQuantity.Mul(types.Two), Quantity: totalQuantity,
Market: market, Market: market,
TimeInForce: types.TimeInForceGTC, TimeInForce: types.TimeInForceGTC,
}) })
} else if ewo.Last() < 0 { } else if shortSignal.Index(1) && !longSignal.Last() && IsBear {
log.Infof("long at %v, timestamp: %s", price, kline.StartTime) price := kline.High
// Long
// TODO: smaller quantity?
orders = append(orders, types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeMarket,
Price: price,
Quantity: totalQuantity,
Market: market,
TimeInForce: types.TimeInForceGTC,
})
}
} else if shortSignal.Index(1) && !longSignal.Last() && !IsBull {
balances := session.Account.Balances() balances := session.Account.Balances()
baseBalance := balances[market.BaseCurrency].Available.Mul(modifier).Div(types.Two) baseBalance := balances[market.BaseCurrency].Available.Mul(modifier)
baseAmount := baseBalance.Mul(price) baseAmount := baseBalance.Mul(price)
if baseBalance.Sign() <= 0 || if baseBalance.Sign() <= 0 ||
baseBalance.Compare(market.MinQuantity) < 0 || baseBalance.Compare(market.MinQuantity) < 0 ||
@ -236,32 +226,16 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
log.Infof("base balance %v is not enough. stop generating sell orders", baseBalance) log.Infof("base balance %v is not enough. stop generating sell orders", baseBalance)
return return
} }
if ewo.Last() > s.Threshold { log.Infof("short at %v, timestamp: %s", price, kline.StartTime)
// Strong short orders = append(orders, types.SubmitOrder{
log.Infof("strong short at %v, timestamp: %s", price, kline.StartTime) Symbol: s.Symbol,
orders = append(orders, types.SubmitOrder{ Side: types.SideTypeSell,
Symbol: s.Symbol, Type: types.OrderTypeLimit,
Side: types.SideTypeSell, Market: market,
Type: types.OrderTypeMarket, Quantity: baseBalance,
Market: market, Price: price,
Quantity: baseBalance.Mul(types.Two), TimeInForce: types.TimeInForceGTC,
Price: price, })
TimeInForce: types.TimeInForceGTC,
})
} else if ewo.Last() > 0 {
// short
log.Infof("short at %v, timestamp: %s", price, kline.StartTime)
// TODO: smaller quantity?
orders = append(orders, types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeMarket,
Market: market,
Quantity: baseBalance,
Price: price,
TimeInForce: types.TimeInForceGTC,
})
}
} }
if len(orders) > 0 { if len(orders) > 0 {
createdOrders, err := orderExecutor.SubmitOrders(ctx, orders...) createdOrders, err := orderExecutor.SubmitOrders(ctx, orders...)
@ -269,15 +243,25 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
log.WithError(err).Errorf("cannot place order") log.WithError(err).Errorf("cannot place order")
return return
} }
entryPrice = lastPrice log.Infof("post order %v", createdOrders)
tradeDirectionLong = IsBull s.tradeCollector.Process()
if tradeDirectionLong {
stopPrice = entryPrice.Mul(fixedpoint.One.Sub(s.StopLoss))
} else {
stopPrice = entryPrice.Mul(fixedpoint.One.Add(s.StopLoss))
}
log.Infof("Place orders %v stop @ %v", createdOrders, stopPrice)
} }
}) })
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
log.Infof("canceling active orders...")
var toCancel []types.Order
for _, order := range orderbook.Orders() {
if order.Status == types.OrderStatusNew || order.Status == types.OrderStatusPartiallyFilled {
toCancel = append(toCancel, order)
}
}
if err := orderExecutor.CancelOrders(ctx, toCancel...); err != nil {
log.WithError(err).Errorf("cancel order error")
}
s.tradeCollector.Process()
})
return nil return nil
} }