package ewoDgtrd import ( "context" "errors" "fmt" "math" "os" "sync" "github.com/fatih/color" "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/types" ) const ID = "ewo_dgtrd" var log = logrus.WithField("strategy", ID) func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } type Strategy struct { Position *types.Position `json:"position,omitempty" persistence:"position"` ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` Market types.Market Session *bbgo.ExchangeSession UseHeikinAshi bool `json:"useHeikinAshi"` // use heikinashi kline Stoploss fixedpoint.Value `json:"stoploss"` Symbol string `json:"symbol"` Interval types.Interval `json:"interval"` UseEma bool `json:"useEma"` // use exponential ma or not UseSma bool `json:"useSma"` // if UseEma == false, use simple ma or not SignalWindow int `json:"sigWin"` // signal window DisableShortStop bool `json:"disableShortStop"` // disable SL on short DisableLongStop bool `json:"disableLongStop"` // disable SL on long FilterHigh float64 `json:"cciStochFilterHigh"` // high filter for CCI Stochastic indicator FilterLow float64 `json:"cciStochFilterLow"` // low filter for CCI Stochastic indicator EwoChangeFilterHigh float64 `json:"ewoChangeFilterHigh"` // high filter for ewo histogram EwoChangeFilterLow float64 `json:"ewoChangeFilterLow"` // low filter for ewo histogram Record bool `json:"record"` // print record messages on position exit point KLineStartTime types.Time KLineEndTime types.Time *bbgo.Environment *bbgo.Notifiability *bbgo.Persistence *bbgo.Graceful bbgo.StrategyController activeMakerOrders *bbgo.ActiveOrderBook orderStore *bbgo.OrderStore tradeCollector *bbgo.TradeCollector entryPrice fixedpoint.Value waitForTrade bool atr *indicator.ATR emv *indicator.EMV ccis *CCISTOCH ma5 types.Series ma34 types.Series ewo types.Series ewoSignal types.Series ewoHistogram types.Series ewoChangeRate float64 heikinAshi *HeikinAshi peakPrice fixedpoint.Value bottomPrice fixedpoint.Value midPrice fixedpoint.Value lock sync.RWMutex buyPrice fixedpoint.Value sellPrice fixedpoint.Value } func (s *Strategy) ID() string { return ID } func (s *Strategy) InstanceID() string { return fmt.Sprintf("%s:%s", ID, s.Symbol) } func (s *Strategy) Initialize() error { return nil } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { log.Infof("subscribe %s", s.Symbol) session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{}) } type UpdatableSeries interface { types.Series Update(value float64) } // Refer: https://tw.tradingview.com/script/XZyG5SOx-CCI-Stochastic-and-a-quick-lesson-on-Scalping-Trading-Systems/ type CCISTOCH struct { cci *indicator.CCI stoch *indicator.STOCH ma *indicator.SMA filterHigh float64 filterLow float64 } func NewCCISTOCH(i types.Interval, filterHigh, filterLow float64) *CCISTOCH { cci := &indicator.CCI{IntervalWindow: types.IntervalWindow{Interval: i, Window: 28}} stoch := &indicator.STOCH{IntervalWindow: types.IntervalWindow{Interval: i, Window: 28}} ma := &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: i, Window: 3}} return &CCISTOCH{ cci: cci, stoch: stoch, ma: ma, filterHigh: filterHigh, filterLow: filterLow, } } func (inc *CCISTOCH) Update(cloze float64) { inc.cci.Update(cloze) inc.stoch.Update(inc.cci.Last(), inc.cci.Last(), inc.cci.Last()) inc.ma.Update(inc.stoch.LastD()) } func (inc *CCISTOCH) BuySignal() bool { hasGrey := false for i := 0; i < len(inc.ma.Values); i++ { v := inc.ma.Index(i) if v > inc.filterHigh { return false } else if v >= inc.filterLow && v <= inc.filterHigh { hasGrey = true continue } else if v < inc.filterLow { return hasGrey } } return false } func (inc *CCISTOCH) SellSignal() bool { hasGrey := false for i := 0; i < len(inc.ma.Values); i++ { v := inc.ma.Index(i) if v < inc.filterLow { return false } else if v >= inc.filterLow && v <= inc.filterHigh { hasGrey = true continue } else if v > inc.filterHigh { return hasGrey } } return false } type VWEMA struct { PV UpdatableSeries V UpdatableSeries } func (inc *VWEMA) Last() float64 { return inc.PV.Last() / inc.V.Last() } func (inc *VWEMA) Index(i int) float64 { if i >= inc.PV.Length() { return 0 } vi := inc.V.Index(i) if vi == 0 { return 0 } return inc.PV.Index(i) / vi } func (inc *VWEMA) Length() int { pvl := inc.PV.Length() vl := inc.V.Length() if pvl < vl { return pvl } return vl } func (inc *VWEMA) Update(kline types.KLine) { inc.PV.Update(kline.Close.Mul(kline.Volume).Float64()) inc.V.Update(kline.Volume.Float64()) } func (inc *VWEMA) UpdateVal(price float64, vol float64) { inc.PV.Update(price * vol) inc.V.Update(vol) } // Setup the Indicators going to be used func (s *Strategy) SetupIndicators(store *bbgo.MarketDataStore) { window5 := types.IntervalWindow{Interval: s.Interval, Window: 5} window34 := types.IntervalWindow{Interval: s.Interval, Window: 34} s.atr = &indicator.ATR{IntervalWindow: window34} s.emv = &indicator.EMV{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: 14}} s.ccis = NewCCISTOCH(s.Interval, s.FilterHigh, s.FilterLow) getSource := func(window types.KLineWindow) types.Series { if s.UseHeikinAshi { return s.heikinAshi.Close } return window.Close() } getVol := func(window types.KLineWindow) types.Series { if s.UseHeikinAshi { return s.heikinAshi.Volume } return window.Volume() } s.heikinAshi = NewHeikinAshi(500) store.OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow) { if interval == s.atr.Interval { if s.atr.RMA == nil { for _, kline := range window { high := kline.High.Float64() low := kline.Low.Float64() cloze := kline.Close.Float64() vol := kline.Volume.Float64() s.atr.Update(high, low, cloze) s.emv.Update(high, low, vol) } } else { kline := window[len(window)-1] high := kline.High.Float64() low := kline.Low.Float64() cloze := kline.Close.Float64() vol := kline.Volume.Float64() s.atr.Update(high, low, cloze) s.emv.Update(high, low, vol) } } if s.Interval != interval { return } if s.heikinAshi.Close.Length() == 0 { for _, kline := range window { s.heikinAshi.Update(kline) s.ccis.Update(getSource(window).Last()) } } else { s.heikinAshi.Update(window[len(window)-1]) s.ccis.Update(getSource(window).Last()) } }) if s.UseEma { ema5 := &indicator.EWMA{IntervalWindow: window5} ema34 := &indicator.EWMA{IntervalWindow: window34} store.OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow) { if s.Interval != interval { return } if ema5.Length() == 0 { closes := types.ToReverseArray(getSource(window)) for _, cloze := range closes { ema5.Update(cloze) ema34.Update(cloze) } } else { cloze := getSource(window).Last() ema5.Update(cloze) ema34.Update(cloze) } }) s.ma5 = ema5 s.ma34 = ema34 } else if s.UseSma { sma5 := &indicator.SMA{IntervalWindow: window5} sma34 := &indicator.SMA{IntervalWindow: window34} store.OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow) { if s.Interval != interval { return } if sma5.Length() == 0 { closes := types.ToReverseArray(getSource(window)) for _, cloze := range closes { sma5.Update(cloze) sma34.Update(cloze) } } else { cloze := getSource(window).Last() sma5.Update(cloze) sma34.Update(cloze) } }) s.ma5 = sma5 s.ma34 = sma34 } else { evwma5 := &VWEMA{ PV: &indicator.EWMA{IntervalWindow: window5}, V: &indicator.EWMA{IntervalWindow: window5}, } evwma34 := &VWEMA{ PV: &indicator.EWMA{IntervalWindow: window34}, V: &indicator.EWMA{IntervalWindow: window34}, } store.OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow) { if s.Interval != interval { return } clozes := getSource(window) vols := getVol(window) if evwma5.PV.Length() == 0 { for i := clozes.Length() - 1; i >= 0; i-- { price := clozes.Index(i) vol := vols.Index(i) evwma5.UpdateVal(price, vol) evwma34.UpdateVal(price, vol) } } else { price := clozes.Last() vol := vols.Last() evwma5.UpdateVal(price, vol) evwma34.UpdateVal(price, vol) } }) s.ma5 = evwma5 s.ma34 = evwma34 } s.ewo = types.Mul(types.Minus(types.Div(s.ma5, s.ma34), 1.0), 100.) s.ewoHistogram = types.Minus(s.ma5, s.ma34) windowSignal := types.IntervalWindow{Interval: s.Interval, Window: s.SignalWindow} if s.UseEma { sig := &indicator.EWMA{IntervalWindow: windowSignal} store.OnKLineWindowUpdate(func(interval types.Interval, _ types.KLineWindow) { if interval != s.Interval { return } if sig.Length() == 0 { // lazy init ewoVals := types.ToReverseArray(s.ewo) for _, ewoValue := range ewoVals { sig.Update(ewoValue) } } else { sig.Update(s.ewo.Last()) } }) s.ewoSignal = sig } else if s.UseSma { sig := &indicator.SMA{IntervalWindow: windowSignal} store.OnKLineWindowUpdate(func(interval types.Interval, _ types.KLineWindow) { if interval != s.Interval { return } if sig.Length() == 0 { // lazy init ewoVals := types.ToReverseArray(s.ewo) for _, ewoValue := range ewoVals { sig.Update(ewoValue) } } else { sig.Update(s.ewo.Last()) } }) s.ewoSignal = sig } else { sig := &VWEMA{ PV: &indicator.EWMA{IntervalWindow: windowSignal}, V: &indicator.EWMA{IntervalWindow: windowSignal}, } store.OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow) { if interval != s.Interval { return } if sig.Length() == 0 { // lazy init ewoVals := types.ToReverseArray(s.ewo) for i, ewoValue := range ewoVals { vol := window.Volume().Index(i) sig.PV.Update(ewoValue * vol) sig.V.Update(vol) } } else { vol := window.Volume().Last() sig.PV.Update(s.ewo.Last() * vol) sig.V.Update(vol) } }) s.ewoSignal = sig } } // Utility to evaluate if the order is valid or not to send to the exchange func (s *Strategy) validateOrder(order *types.SubmitOrder) error { if order.Type == types.OrderTypeMarket && order.TimeInForce != "" { return errors.New("wrong field: market vs TimeInForce") } if order.Side == types.SideTypeSell { baseBalance, ok := s.Session.GetAccount().Balance(s.Market.BaseCurrency) if !ok { log.Error("cannot get account") return errors.New("cannot get account") } if order.Quantity.Compare(baseBalance.Available) > 0 { log.Errorf("qty %v > avail %v", order.Quantity, baseBalance.Available) return errors.New("qty > avail") } price := order.Price if price.IsZero() { price, ok = s.Session.LastPrice(s.Symbol) if !ok { log.Error("no price") return errors.New("no price") } } orderAmount := order.Quantity.Mul(price) if order.Quantity.Sign() <= 0 || order.Quantity.Compare(s.Market.MinQuantity) < 0 || orderAmount.Compare(s.Market.MinNotional) < 0 { log.Debug("amount fail") return errors.New(fmt.Sprintf("amount fail: quantity: %v, amount: %v", order.Quantity, orderAmount)) } return nil } else if order.Side == types.SideTypeBuy { quoteBalance, ok := s.Session.GetAccount().Balance(s.Market.QuoteCurrency) if !ok { log.Error("cannot get account") return errors.New("cannot get account") } price := order.Price if price.IsZero() { price, ok = s.Session.LastPrice(s.Symbol) if !ok { log.Error("no price") return errors.New("no price") } } totalQuantity := quoteBalance.Available.Div(price) if order.Quantity.Compare(totalQuantity) > 0 { log.Errorf("qty %v > avail %v", order.Quantity, totalQuantity) return errors.New("qty > avail") } orderAmount := order.Quantity.Mul(price) if order.Quantity.Sign() <= 0 || orderAmount.Compare(s.Market.MinNotional) < 0 || order.Quantity.Compare(s.Market.MinQuantity) < 0 { log.Debug("amount fail") return errors.New(fmt.Sprintf("amount fail: quantity: %v, amount: %v", order.Quantity, orderAmount)) } return nil } log.Error("side error") return errors.New("side error") } func (s *Strategy) PlaceBuyOrder(ctx context.Context, price fixedpoint.Value) (*types.Order, *types.Order) { var closeOrder *types.Order var ok bool waitForTrade := false base := s.Position.GetBase() if base.Abs().Compare(s.Market.MinQuantity) >= 0 && base.Mul(s.GetLastPrice()).Abs().Compare(s.Market.MinNotional) >= 0 && base.Sign() < 0 { if closeOrder, ok = s.ClosePosition(ctx); !ok { log.Errorf("sell position %v remained not closed, skip placing order", base) return closeOrder, nil } } if s.Position.GetBase().Sign() < 0 { // we are not able to make close trade at this moment, // will close the rest of the position by normal limit order // s.entryPrice is set in the last trade waitForTrade = true } quoteBalance, ok := s.Session.GetAccount().Balance(s.Market.QuoteCurrency) if !ok { log.Infof("buy order at price %v failed", price) return closeOrder, nil } quantityAmount := quoteBalance.Available totalQuantity := quantityAmount.Div(price) order := types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeLimit, Price: price, Quantity: totalQuantity, Market: s.Market, TimeInForce: types.TimeInForceGTC, } if err := s.validateOrder(&order); err != nil { log.Infof("validation failed %v: %v", order, err) return closeOrder, nil } log.Warnf("long at %v, position %v, closeOrder %v, timestamp: %s", price, s.Position.GetBase(), closeOrder, s.KLineStartTime) createdOrders, err := s.Session.Exchange.SubmitOrders(ctx, order) if err != nil { log.WithError(err).Errorf("cannot place order") return closeOrder, nil } log.Infof("post order c: %v, entryPrice: %v o: %v", waitForTrade, s.entryPrice, createdOrders) s.waitForTrade = waitForTrade s.orderStore.Add(createdOrders...) s.activeMakerOrders.Add(createdOrders...) s.tradeCollector.Process() return closeOrder, &createdOrders[0] } func (s *Strategy) PlaceSellOrder(ctx context.Context, price fixedpoint.Value) (*types.Order, *types.Order) { var closeOrder *types.Order var ok bool waitForTrade := false base := s.Position.GetBase() if base.Abs().Compare(s.Market.MinQuantity) >= 0 && base.Abs().Mul(s.GetLastPrice()).Compare(s.Market.MinNotional) >= 0 && base.Sign() > 0 { if closeOrder, ok = s.ClosePosition(ctx); !ok { log.Errorf("buy position %v remained not closed, skip placing order", base) return closeOrder, nil } } if s.Position.GetBase().Sign() > 0 { // we are not able to make close trade at this moment, // will close the rest of the position by normal limit order // s.entryPrice is set in the last trade waitForTrade = true } baseBalance, ok := s.Session.GetAccount().Balance(s.Market.BaseCurrency) if !ok { return closeOrder, nil } order := types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeSell, Type: types.OrderTypeLimit, Market: s.Market, Quantity: baseBalance.Available, Price: price, TimeInForce: types.TimeInForceGTC, } if err := s.validateOrder(&order); err != nil { log.Infof("validation failed %v: %v", order, err) return closeOrder, nil } log.Warnf("short at %v, position %v closeOrder %v, timestamp: %s", price, s.Position.GetBase(), closeOrder, s.KLineStartTime) createdOrders, err := s.Session.Exchange.SubmitOrders(ctx, order) if err != nil { log.WithError(err).Errorf("cannot place order") return closeOrder, nil } log.Infof("post order, c: %v, entryPrice: %v o: %v", waitForTrade, s.entryPrice, createdOrders) s.waitForTrade = waitForTrade s.orderStore.Add(createdOrders...) s.activeMakerOrders.Add(createdOrders...) s.tradeCollector.Process() return closeOrder, &createdOrders[0] } // ClosePosition(context.Context) -> (closeOrder *types.Order, ok bool) // this will decorate the generated order from NewMarketCloseOrder // add do necessary checks // if available quantity is zero, will return (nil, true) // if any of the checks failed, will return (nil, false) // otherwise, return the created close order and true func (s *Strategy) ClosePosition(ctx context.Context) (*types.Order, bool) { order := s.Position.NewMarketCloseOrder(fixedpoint.One) // no position exists if order == nil { // no base s.sellPrice = fixedpoint.Zero s.buyPrice = fixedpoint.Zero return nil, true } order.TimeInForce = "" // If there's any order not yet been traded in the orderbook, // we need this additional check to make sure we have enough balance to post a close order balances := s.Session.GetAccount().Balances() baseBalance := balances[s.Market.BaseCurrency].Available if order.Side == types.SideTypeBuy { price := s.GetLastPrice() 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 no available balance... if order.Quantity.IsZero() { return nil, true } if err := s.validateOrder(order); err != nil { log.Errorf("cannot place close order %v: %v", order, err) return nil, false } createdOrders, err := s.Session.Exchange.SubmitOrders(ctx, *order) if err != nil { log.WithError(err).Errorf("cannot place close order") return nil, false } log.Infof("close order %v", createdOrders) s.orderStore.Add(createdOrders...) s.activeMakerOrders.Add(createdOrders...) s.tradeCollector.Process() return &createdOrders[0], true } func (s *Strategy) CancelAll(ctx context.Context) { var toCancel []types.Order for _, order := range s.orderStore.Orders() { if order.Status == types.OrderStatusNew || order.Status == types.OrderStatusPartiallyFilled { toCancel = append(toCancel, order) } } if len(toCancel) > 0 { if err := s.Session.Exchange.CancelOrders(ctx, toCancel...); err != nil { log.WithError(err).Errorf("cancel order error") } s.waitForTrade = false } } func (s *Strategy) GetLastPrice() fixedpoint.Value { var lastPrice fixedpoint.Value var ok bool if s.Environment.IsBackTesting() { lastPrice, ok = s.Session.LastPrice(s.Symbol) if !ok { log.Errorf("cannot get last price") return lastPrice } } else { s.lock.RLock() if s.midPrice.IsZero() { lastPrice, ok = s.Session.LastPrice(s.Symbol) if !ok { log.Errorf("cannot get last price") return lastPrice } } else { lastPrice = s.midPrice } s.lock.RUnlock() } return lastPrice } // Trading Rules: // - buy / sell the whole asset // - SL by atr (lastprice < buyprice - atr) || (lastprice > sellprice + atr) // - TP by detecting if there's a ewo pivotHigh(1,1) -> close long, or pivotLow(1,1) -> close short // - TP by ma34 +- atr * 2 // - TP by (lastprice < peak price - atr) || (lastprice > bottom price + atr) // - SL by s.Stoploss (Abs(price_diff / price) > s.Stoploss) // - entry condition on ewo(Elliott wave oscillator) Crosses ewoSignal(ma on ewo, signalWindow) // * buy signal on (crossover on previous K bar and no crossunder on latest K bar) // * sell signal on (crossunder on previous K bar and no crossunder on latest K bar) // - and filtered by the following rules: // * buy: buy signal ON, kline Close > Open, Close > ma5, Close > ma34, CCI Stochastic Buy signal // * sell: sell signal ON, kline Close < Open, Close < ma5, Close < ma34, CCI Stochastic Sell signal // - or entry when ma34 +- atr * 3 gets touched // - entry price: latestPrice +- atr / 2 (short,long), close at market price // Cancel non-fully filled orders on new signal (either in same direction or not) // // ps: kline might refer to heikinashi or normal ohlc func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { s.buyPrice = fixedpoint.Zero s.sellPrice = fixedpoint.Zero s.peakPrice = fixedpoint.Zero s.bottomPrice = fixedpoint.Zero counterTPfromPeak := 0 percentAvgTPfromPeak := 0.0 counterTPfromCCI := 0 percentAvgTPfromCCI := 0.0 counterTPfromLongShort := 0 percentAvgTPfromLongShort := 0.0 counterTPfromAtr := 0 percentAvgTPfromAtr := 0.0 counterTPfromOrder := 0 percentAvgTPfromOrder := 0.0 counterSLfromSL := 0 percentAvgSLfromSL := 0.0 counterSLfromOrder := 0 percentAvgSLfromOrder := 0.0 s.activeMakerOrders = bbgo.NewActiveOrderBook(s.Symbol) s.activeMakerOrders.BindStream(session.UserDataStream) s.orderStore = bbgo.NewOrderStore(s.Symbol) s.orderStore.BindStream(session.UserDataStream) if s.Position == nil { s.Position = types.NewPositionFromMarket(s.Market) } if s.ProfitStats == nil { s.ProfitStats = types.NewProfitStats(s.Market) } s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.Position, s.orderStore) s.tradeCollector.OnTrade(func(trade types.Trade, profit, netprofit fixedpoint.Value) { if s.Symbol != trade.Symbol { return } s.Notifiability.Notify(trade) s.ProfitStats.AddTrade(trade) if !profit.IsZero() { log.Warnf("generate profit: %v, netprofit: %v, trade: %v", profit, netprofit, trade) p := s.Position.NewProfit(trade, profit, netprofit) p.Strategy = ID p.StrategyInstanceID = s.InstanceID() s.Notify(&p) s.ProfitStats.AddProfit(p) s.Notify(&s.ProfitStats) s.Environment.RecordPosition(s.Position, trade, &p) } else { s.Environment.RecordPosition(s.Position, trade, nil) } // calculate report for the position that cannot be closed by close order (amount too small) if s.waitForTrade { price := s.entryPrice if price.IsZero() { panic("no price found") } pnlRate := trade.Price.Sub(price).Abs().Div(trade.Price).Float64() if s.Record { log.Errorf("record avg %v trade %v", price, trade) } if trade.Side == types.SideTypeBuy { if trade.Price.Compare(price) < 0 { percentAvgTPfromOrder = percentAvgTPfromOrder*float64(counterTPfromOrder) + pnlRate counterTPfromOrder += 1 percentAvgTPfromOrder /= float64(counterTPfromOrder) } else { percentAvgSLfromOrder = percentAvgSLfromOrder*float64(counterSLfromOrder) + pnlRate counterSLfromOrder += 1 percentAvgSLfromOrder /= float64(counterSLfromOrder) } } else if trade.Side == types.SideTypeSell { if trade.Price.Compare(price) > 0 { percentAvgTPfromOrder = percentAvgTPfromOrder*float64(counterTPfromOrder) + pnlRate counterTPfromOrder += 1 percentAvgTPfromOrder /= float64(counterTPfromOrder) } else { percentAvgSLfromOrder = percentAvgSLfromOrder*float64(counterSLfromOrder) + pnlRate counterSLfromOrder += 1 percentAvgSLfromOrder /= float64(counterSLfromOrder) } } else { panic(fmt.Sprintf("no sell(%v) or buy price(%v), %v", s.sellPrice, s.buyPrice, trade)) } s.waitForTrade = false } if s.Position.GetBase().Abs().Compare(s.Market.MinQuantity) >= 0 && s.Position.GetBase().Abs().Mul(trade.Price).Compare(s.Market.MinNotional) >= 0 { sign := s.Position.GetBase().Sign() if sign > 0 { log.Infof("base become positive, %v", trade) s.buyPrice = s.Position.AverageCost s.sellPrice = fixedpoint.Zero s.peakPrice = s.Position.AverageCost } else if sign == 0 { panic("not going to happen") } else { log.Infof("base become negative, %v", trade) s.buyPrice = fixedpoint.Zero s.sellPrice = s.Position.AverageCost s.bottomPrice = s.Position.AverageCost } s.entryPrice = trade.Price } else { log.Infof("base become zero, rest of base: %v", s.Position.GetBase()) if s.Position.GetBase().IsZero() { s.entryPrice = fixedpoint.Zero } s.buyPrice = fixedpoint.Zero s.sellPrice = fixedpoint.Zero s.peakPrice = fixedpoint.Zero s.bottomPrice = fixedpoint.Zero } }) s.tradeCollector.OnPositionUpdate(func(position *types.Position) { log.Infof("position changed: %s", position) s.Notify(s.Position) }) s.tradeCollector.BindStream(session.UserDataStream) store, ok := s.Session.MarketDataStore(s.Symbol) if !ok { return errors.New(fmt.Sprintf("cannot get marketdatastore of %s", s.Symbol)) } s.SetupIndicators(store) // local peak of ewo shortSig := s.ewo.Last() < s.ewo.Index(1) && s.ewo.Index(1) > s.ewo.Index(2) longSig := s.ewo.Last() > s.ewo.Index(1) && s.ewo.Index(1) < s.ewo.Index(2) sellOrderTPSL := func(price fixedpoint.Value) { lastPrice := s.GetLastPrice() base := s.Position.GetBase().Abs() if base.Mul(lastPrice).Compare(s.Market.MinNotional) < 0 || base.Compare(s.Market.MinQuantity) < 0 { return } if s.sellPrice.IsZero() { return } balances := session.GetAccount().Balances() quoteBalance := balances[s.Market.QuoteCurrency].Available atr := fixedpoint.NewFromFloat(s.atr.Last()) atrx2 := fixedpoint.NewFromFloat(s.atr.Last() * 2) buyall := false if s.bottomPrice.IsZero() || s.bottomPrice.Compare(price) > 0 { s.bottomPrice = price } takeProfit := false bottomBack := s.bottomPrice spBack := s.sellPrice reason := -1 if quoteBalance.Div(lastPrice).Compare(s.Market.MinQuantity) >= 0 && quoteBalance.Compare(s.Market.MinNotional) >= 0 { base := fixedpoint.NewFromFloat(s.ma34.Last()) // TP if lastPrice.Compare(s.sellPrice) < 0 && (longSig || (!atrx2.IsZero() && base.Sub(atrx2).Compare(lastPrice) >= 0)) { buyall = true takeProfit = true // calculate report if longSig { reason = 1 } else { reason = 2 } } if !atr.IsZero() && s.bottomPrice.Add(atr).Compare(lastPrice) <= 0 && lastPrice.Compare(s.sellPrice) < 0 { buyall = true takeProfit = true reason = 3 } // SL /*if (!atrx2.IsZero() && s.bottomPrice.Add(atrx2).Compare(lastPrice) <= 0) || lastPrice.Sub(s.bottomPrice).Div(lastPrice).Compare(s.Stoploss) > 0 { if lastPrice.Compare(s.sellPrice) < 0 { takeProfit = true } buyall = true s.bottomPrice = fixedpoint.Zero }*/ if !s.DisableShortStop && ((!atr.IsZero() && s.sellPrice.Sub(atr).Compare(lastPrice) >= 0) || lastPrice.Sub(s.sellPrice).Div(s.sellPrice).Compare(s.Stoploss) > 0) { buyall = true reason = 4 } } if buyall { log.Warnf("buyall TPSL %v %v", s.Position.GetBase(), quoteBalance) p := s.sellPrice if order, ok := s.ClosePosition(ctx); order != nil && ok { if takeProfit { log.Errorf("takeprofit buy at %v, avg %v, l: %v, atrx2: %v", lastPrice, spBack, bottomBack, atrx2) } else { log.Errorf("stoploss buy at %v, avg %v, l: %v, atrx2: %v", lastPrice, spBack, bottomBack, atrx2) } // calculate report if s.Record { log.Error("record ba") } var pnlRate float64 if takeProfit { pnlRate = p.Sub(lastPrice).Div(lastPrice).Float64() } else { pnlRate = lastPrice.Sub(p).Div(lastPrice).Float64() } switch reason { case 0: percentAvgTPfromCCI = percentAvgTPfromCCI*float64(counterTPfromCCI) + pnlRate counterTPfromCCI += 1 percentAvgTPfromCCI /= float64(counterTPfromCCI) break case 1: percentAvgTPfromLongShort = percentAvgTPfromLongShort*float64(counterTPfromLongShort) + pnlRate counterTPfromLongShort += 1 percentAvgTPfromLongShort /= float64(counterTPfromLongShort) break case 2: percentAvgTPfromAtr = percentAvgTPfromAtr*float64(counterTPfromAtr) + pnlRate counterTPfromAtr += 1 percentAvgTPfromAtr /= float64(counterTPfromAtr) break case 3: percentAvgTPfromPeak = percentAvgTPfromPeak*float64(counterTPfromPeak) + pnlRate counterTPfromPeak += 1 percentAvgTPfromPeak /= float64(counterTPfromPeak) break case 4: percentAvgSLfromSL = percentAvgSLfromSL*float64(counterSLfromSL) + pnlRate counterSLfromSL += 1 percentAvgSLfromSL /= float64(counterSLfromSL) break } } } } buyOrderTPSL := func(price fixedpoint.Value) { lastPrice := s.GetLastPrice() base := s.Position.GetBase().Abs() if base.Mul(lastPrice).Compare(s.Market.MinNotional) < 0 || base.Compare(s.Market.MinQuantity) < 0 { return } if s.buyPrice.IsZero() { return } balances := session.GetAccount().Balances() baseBalance := balances[s.Market.BaseCurrency].Available atr := fixedpoint.NewFromFloat(s.atr.Last()) atrx2 := fixedpoint.NewFromFloat(s.atr.Last() * 2) sellall := false if s.peakPrice.IsZero() || s.peakPrice.Compare(price) < 0 { s.peakPrice = price } takeProfit := false peakBack := s.peakPrice bpBack := s.buyPrice reason := -1 if baseBalance.Compare(s.Market.MinQuantity) >= 0 && baseBalance.Mul(lastPrice).Compare(s.Market.MinNotional) >= 0 { // TP base := fixedpoint.NewFromFloat(s.ma34.Last()) if lastPrice.Compare(s.buyPrice) > 0 && (shortSig || (!atrx2.IsZero() && base.Add(atrx2).Compare(lastPrice) <= 0)) { sellall = true takeProfit = true // calculate report if shortSig { reason = 1 } else { reason = 2 } } if !atr.IsZero() && s.peakPrice.Sub(atr).Compare(lastPrice) >= 0 && lastPrice.Compare(s.buyPrice) > 0 { sellall = true takeProfit = true reason = 3 } // SL /*if s.peakPrice.Sub(lastPrice).Div(s.peakPrice).Compare(s.Stoploss) > 0 || (!atrx2.IsZero() && s.peakPrice.Sub(atrx2).Compare(lastPrice) >= 0) { if lastPrice.Compare(s.buyPrice) > 0 { takeProfit = true } sellall = true s.peakPrice = fixedpoint.Zero }*/ if !s.DisableLongStop && (s.buyPrice.Sub(lastPrice).Div(s.buyPrice).Compare(s.Stoploss) > 0 || (!atr.IsZero() && s.buyPrice.Sub(atr).Compare(lastPrice) >= 0)) { sellall = true reason = 4 } } if sellall { log.Warnf("sellall TPSL %v", s.Position.GetBase()) p := s.buyPrice if order, ok := s.ClosePosition(ctx); order != nil && ok { if takeProfit { log.Errorf("takeprofit sell at %v, avg %v, h: %v, atrx2: %v", lastPrice, bpBack, peakBack, atrx2) } else { log.Errorf("stoploss sell at %v, avg %v, h: %v, atrx2: %v", lastPrice, bpBack, peakBack, atrx2) } // calculate report if s.Record { log.Error("record sa") } var pnlRate float64 if takeProfit { pnlRate = lastPrice.Sub(p).Div(p).Float64() } else { pnlRate = p.Sub(lastPrice).Div(p).Float64() } switch reason { case 0: percentAvgTPfromCCI = percentAvgTPfromCCI*float64(counterTPfromCCI) + pnlRate counterTPfromCCI += 1 percentAvgTPfromCCI /= float64(counterTPfromCCI) break case 1: percentAvgTPfromLongShort = percentAvgTPfromLongShort*float64(counterTPfromLongShort) + pnlRate counterTPfromLongShort += 1 percentAvgTPfromLongShort /= float64(counterTPfromLongShort) break case 2: percentAvgTPfromAtr = percentAvgTPfromAtr*float64(counterTPfromAtr) + pnlRate counterTPfromAtr += 1 percentAvgTPfromAtr /= float64(counterTPfromAtr) break case 3: percentAvgTPfromPeak = percentAvgTPfromPeak*float64(counterTPfromPeak) + pnlRate counterTPfromPeak += 1 percentAvgTPfromPeak /= float64(counterTPfromPeak) break case 4: percentAvgSLfromSL = percentAvgSLfromSL*float64(counterSLfromSL) + pnlRate counterSLfromSL += 1 percentAvgSLfromSL /= float64(counterSLfromSL) break } } } } // set last price by realtime book ticker update // to trigger TP/SL session.MarketDataStream.OnBookTickerUpdate(func(ticker types.BookTicker) { if s.Environment.IsBackTesting() { return } bestBid := ticker.Buy bestAsk := ticker.Sell var midPrice fixedpoint.Value if tryLock(&s.lock) { if !bestAsk.IsZero() && !bestBid.IsZero() { s.midPrice = bestAsk.Add(bestBid).Div(types.Two) } else if !bestAsk.IsZero() { s.midPrice = bestAsk } else { s.midPrice = bestBid } midPrice = s.midPrice s.lock.Unlock() } if !midPrice.IsZero() { buyOrderTPSL(midPrice) sellOrderTPSL(midPrice) // log.Debugf("best bid %v, best ask %v, mid %v", bestBid, bestAsk, midPrice) } }) getHigh := func(window types.KLineWindow) types.Series { if s.UseHeikinAshi { return s.heikinAshi.High } return window.High() } getLow := func(window types.KLineWindow) types.Series { if s.UseHeikinAshi { return s.heikinAshi.Low } return window.Low() } getClose := func(window types.KLineWindow) types.Series { if s.UseHeikinAshi { return s.heikinAshi.Close } return window.Close() } getOpen := func(window types.KLineWindow) types.Series { if s.UseHeikinAshi { return s.heikinAshi.Open } return window.Open() } store.OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow) { kline := window[len(window)-1] s.KLineStartTime = kline.StartTime s.KLineEndTime = kline.EndTime // well, only track prices on 1m if interval == types.Interval1m { if s.Environment.IsBackTesting() { buyOrderTPSL(kline.High) sellOrderTPSL(kline.Low) } } var lastPrice fixedpoint.Value var ok bool if s.Environment.IsBackTesting() { lastPrice, ok = session.LastPrice(s.Symbol) if !ok { log.Errorf("cannot get last price") return } } else { s.lock.RLock() if s.midPrice.IsZero() { lastPrice, ok = session.LastPrice(s.Symbol) if !ok { log.Errorf("cannot get last price") return } } else { lastPrice = s.midPrice } s.lock.RUnlock() } balances := session.GetAccount().Balances() baseBalance := balances[s.Market.BaseCurrency].Total() quoteBalance := balances[s.Market.QuoteCurrency].Total() atr := fixedpoint.NewFromFloat(s.atr.Last()) if !s.Environment.IsBackTesting() { log.Infof("Get last price: %v, ewo %f, ewoSig %f, ccis: %f, atr %v, kline: %v, balance[base]: %v balance[quote]: %v", lastPrice, s.ewo.Last(), s.ewoSignal.Last(), s.ccis.ma.Last(), atr, kline, baseBalance, quoteBalance) } if kline.Interval != s.Interval { return } priceHighest := types.Highest(getHigh(window), 233) priceLowest := types.Lowest(getLow(window), 233) priceChangeRate := (priceHighest - priceLowest) / priceHighest / 14 ewoHighest := types.Highest(s.ewoHistogram, 233) s.ewoChangeRate = math.Abs(s.ewoHistogram.Last() / ewoHighest * priceChangeRate) longSignal := types.CrossOver(s.ewo, s.ewoSignal) shortSignal := types.CrossUnder(s.ewo, s.ewoSignal) base := s.ma34.Last() sellLine := base + s.atr.Last()*3 buyLine := base - s.atr.Last()*3 clozes := getClose(window) opens := getOpen(window) // get trend flags bull := clozes.Last() > opens.Last() breakThrough := clozes.Last() > s.ma5.Last() && clozes.Last() > s.ma34.Last() breakDown := clozes.Last() < s.ma5.Last() && clozes.Last() < s.ma34.Last() // kline breakthrough ma5, ma34 trend up, and cci Stochastic bull IsBull := bull && breakThrough && s.ccis.BuySignal() && s.ewoChangeRate < s.EwoChangeFilterHigh && s.ewoChangeRate > s.EwoChangeFilterLow // kline downthrough ma5, ma34 trend down, and cci Stochastic bear IsBear := !bull && breakDown && s.ccis.SellSignal() && s.ewoChangeRate < s.EwoChangeFilterHigh && s.ewoChangeRate > s.EwoChangeFilterLow if !s.Environment.IsBackTesting() { log.Infof("IsBull: %v, bull: %v, longSignal[1]: %v, shortSignal: %v, lastPrice: %v", IsBull, bull, longSignal.Index(1), shortSignal.Last(), lastPrice) log.Infof("IsBear: %v, bear: %v, shortSignal[1]: %v, longSignal: %v, lastPrice: %v", IsBear, !bull, shortSignal.Index(1), longSignal.Last(), lastPrice) } if (longSignal.Index(1) && !shortSignal.Last() && IsBull) || lastPrice.Float64() <= buyLine { price := lastPrice.Sub(atr.Div(types.Two)) // if total asset (including locked) could be used to buy if quoteBalance.Div(price).Compare(s.Market.MinQuantity) >= 0 && quoteBalance.Compare(s.Market.MinNotional) >= 0 { // cancel all orders to release lock s.CancelAll(ctx) // backup, since the s.sellPrice will be cleared when doing ClosePosition sellPrice := s.sellPrice log.Errorf("ewoChangeRate %v, emv %v", s.ewoChangeRate, s.emv.Last()) // calculate report if closeOrder, _ := s.PlaceBuyOrder(ctx, price); closeOrder != nil { if s.Record { log.Error("record l") } if !sellPrice.IsZero() { if lastPrice.Compare(sellPrice) > 0 { pnlRate := lastPrice.Sub(sellPrice).Div(lastPrice).Float64() percentAvgTPfromOrder = percentAvgTPfromOrder*float64(counterTPfromOrder) + pnlRate counterTPfromOrder += 1 percentAvgTPfromOrder /= float64(counterTPfromOrder) } else { pnlRate := sellPrice.Sub(lastPrice).Div(lastPrice).Float64() percentAvgSLfromOrder = percentAvgSLfromOrder*float64(counterSLfromOrder) + pnlRate counterSLfromOrder += 1 percentAvgSLfromOrder /= float64(counterSLfromOrder) } } else { panic("no sell price") } } } } if (shortSignal.Index(1) && !longSignal.Last() && IsBear) || lastPrice.Float64() >= sellLine { price := lastPrice.Add(atr.Div(types.Two)) // if total asset (including locked) could be used to sell if baseBalance.Mul(price).Compare(s.Market.MinNotional) >= 0 && baseBalance.Compare(s.Market.MinQuantity) >= 0 { // cancel all orders to release lock s.CancelAll(ctx) // backup, since the s.buyPrice will be cleared when doing ClosePosition buyPrice := s.buyPrice log.Errorf("ewoChangeRate: %v, emv %v", s.ewoChangeRate, s.emv.Last()) // calculate report if closeOrder, _ := s.PlaceSellOrder(ctx, price); closeOrder != nil { if s.Record { log.Error("record s") } if !buyPrice.IsZero() { if lastPrice.Compare(buyPrice) > 0 { pnlRate := lastPrice.Sub(buyPrice).Div(buyPrice).Float64() percentAvgTPfromOrder = percentAvgTPfromOrder*float64(counterTPfromOrder) + pnlRate counterTPfromOrder += 1 percentAvgTPfromOrder /= float64(counterTPfromOrder) } else { pnlRate := buyPrice.Sub(lastPrice).Div(buyPrice).Float64() percentAvgSLfromOrder = percentAvgSLfromOrder*float64(counterSLfromOrder) + pnlRate counterSLfromOrder += 1 percentAvgSLfromOrder /= float64(counterSLfromOrder) } } else { panic("no buy price") } } } } }) s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() log.Infof("canceling active orders...") s.CancelAll(ctx) s.tradeCollector.Process() hiblue := color.New(color.FgHiBlue).FprintfFunc() blue := color.New(color.FgBlue).FprintfFunc() hiyellow := color.New(color.FgHiYellow).FprintfFunc() hiblue(os.Stderr, "---- Trade Report (Without Fee) ----\n") hiblue(os.Stderr, "TP:\n") blue(os.Stderr, "\tpeak / bottom with atr: %d, avg pnl rate: %f\n", counterTPfromPeak, percentAvgTPfromPeak) blue(os.Stderr, "\tCCI Stochastic: %d, avg pnl rate: %f\n", counterTPfromCCI, percentAvgTPfromCCI) blue(os.Stderr, "\tLongSignal/ShortSignal: %d, avg pnl rate: %f\n", counterTPfromLongShort, percentAvgTPfromLongShort) blue(os.Stderr, "\tma34 and Atrx2: %d, avg pnl rate: %f\n", counterTPfromAtr, percentAvgTPfromAtr) blue(os.Stderr, "\tActive Order: %d, avg pnl rate: %f\n", counterTPfromOrder, percentAvgTPfromOrder) totalTP := counterTPfromPeak + counterTPfromCCI + counterTPfromLongShort + counterTPfromAtr + counterTPfromOrder avgProfit := (float64(counterTPfromPeak)*percentAvgTPfromPeak + float64(counterTPfromCCI)*percentAvgTPfromCCI + float64(counterTPfromLongShort)*percentAvgTPfromLongShort + float64(counterTPfromAtr)*percentAvgTPfromAtr + float64(counterTPfromOrder)*percentAvgTPfromOrder) / float64(totalTP) hiblue(os.Stderr, "\tSum: %d, avg pnl rate: %f\n", totalTP, avgProfit) hiblue(os.Stderr, "SL:\n") blue(os.Stderr, "\tentry SL: %d, avg pnl rate: -%f\n", counterSLfromSL, percentAvgSLfromSL) blue(os.Stderr, "\tActive Order: %d, avg pnl rate: -%f\n", counterSLfromOrder, percentAvgSLfromOrder) totalSL := counterSLfromSL + counterSLfromOrder avgLoss := (float64(counterSLfromSL)*percentAvgSLfromSL + float64(counterSLfromOrder)*percentAvgSLfromOrder) / float64(totalSL) hiblue(os.Stderr, "\tSum: %d, avg pnl rate: -%f\n", totalSL, avgLoss) hiblue(os.Stderr, "WinRate: %f\n", float64(totalTP)/float64(totalTP+totalSL)) maString := "vwema" if s.UseSma { maString = "sma" } if s.UseEma { maString = "ema" } hiyellow(os.Stderr, "----- EWO Settings -------\n") hiyellow(os.Stderr, "General:\n") hiyellow(os.Stderr, "\tuseHeikinAshi: %v\n", s.UseHeikinAshi) hiyellow(os.Stderr, "\tstoploss: %v\n", s.Stoploss) hiyellow(os.Stderr, "\tsymbol: %s\n", s.Symbol) hiyellow(os.Stderr, "\tinterval: %s\n", s.Interval) hiyellow(os.Stderr, "\tMA type: %s\n", maString) hiyellow(os.Stderr, "\tdisableShortStop: %v\n", s.DisableShortStop) hiyellow(os.Stderr, "\tdisableLongStop: %v\n", s.DisableLongStop) hiyellow(os.Stderr, "\trecord: %v\n", s.Record) hiyellow(os.Stderr, "CCI Stochastic:\n") hiyellow(os.Stderr, "\tccistochFilterHigh: %f\n", s.FilterHigh) hiyellow(os.Stderr, "\tccistochFilterLow: %f\n", s.FilterLow) hiyellow(os.Stderr, "Ewo && Ewo Histogram:\n") hiyellow(os.Stderr, "\tsigWin: %d\n", s.SignalWindow) hiyellow(os.Stderr, "\tewoChngFilterHigh: %f\n", s.EwoChangeFilterHigh) hiyellow(os.Stderr, "\tewoChngFilterLow: %f\n", s.EwoChangeFilterLow) }) return nil }