mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
use trailingstop
This commit is contained in:
parent
574c5b77b1
commit
a0e218a5c6
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user