diff --git a/config/elliottwave.yaml b/config/elliottwave.yaml new file mode 100644 index 000000000..b0d004475 --- /dev/null +++ b/config/elliottwave.yaml @@ -0,0 +1,116 @@ +--- +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 + elliottwave: + symbol: BTCUSDT + # kline interval for indicators + interval: 3m + stoploss: 0.15% + windowATR: 14 + windowQuick: 3 + windowSlow: 19 + source: hl2 + pendingMinutes: 8 + + + # 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.0006, 0.0008, 0.0012, 0.0017, 0.01] + #trailingActivationRatio: [] + #trailingCallbackRate: [] + #trailingCallbackRate: [0.002, 0.01, 0.1] + #trailingCallbackRate: [0.0004, 0.0009, 0.018] + trailingCallbackRate: [0.0001, 0.0002, 0.0003, 0.0006, 0.0049] + + #exits: + # - roiStopLoss: + # percentage: 0.35% + #- roiTakeProfit: + # percentage: 0.7% + #- protectiveStopLoss: + # activationRatio: 0.5% + # stopLossRatio: 0.2% + # placeStopOrder: false + #- trailingStop: + # callbackRate: 0.3% + # activationRatio is relative to the average cost, + # when side is buy, 1% means lower 1% than the average cost. + # when side is sell, 1% means higher 1% than the average cost. + # activationRatio: 0.7% + # minProfit uses the position ROI to calculate the profit ratio + + # minProfit: 1.5% + # interval: 1m + # side: sell + # closePosition: 100% + + #- trailingStop: + # callbackRate: 0.3% + # activationRatio is relative to the average cost, + # when side is buy, 1% means lower 1% than the average cost. + # when side is sell, 1% means higher 1% than the average cost. + # activationRatio: 0.7% + # minProfit uses the position ROI to calculate the profit ratio + + # minProfit: 1.5% + # interval: 1m + # side: buy + # closePosition: 100% + #- 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: + - BTCUSDT + +backtest: + startTime: "2022-08-30" + endTime: "2022-09-30" + symbols: + - BTCUSDT + sessions: [binance] + accounts: + binance: + makerFeeRate: 0.000 + #takerFeeRate: 0.000 + balances: + BTC: 0 + USDT: 5000 diff --git a/pkg/bbgo/activeorderbook.go b/pkg/bbgo/activeorderbook.go index 75d28ecc7..84732cf72 100644 --- a/pkg/bbgo/activeorderbook.go +++ b/pkg/bbgo/activeorderbook.go @@ -3,6 +3,7 @@ package bbgo import ( "context" "encoding/json" + "fmt" "time" log "github.com/sirupsen/logrus" @@ -40,6 +41,30 @@ func (b *ActiveOrderBook) BindStream(stream types.Stream) { stream.OnOrderUpdate(b.orderUpdateHandler) } +func (b *ActiveOrderBook) waitClear(ctx context.Context, order types.Order, waitTime, timeout time.Duration) (bool, error) { + if !b.Exists(order) { + return true, nil + } + + timeoutC := time.After(timeout) + for { + time.Sleep(waitTime) + clear := !b.Exists(order) + select { + case <-timeoutC: + return clear, nil + + case <-ctx.Done(): + return clear, ctx.Err() + + default: + if clear { + return clear, nil + } + } + } +} + func (b *ActiveOrderBook) waitAllClear(ctx context.Context, waitTime, timeout time.Duration) (bool, error) { numOfOrders := b.NumOfOrders() clear := numOfOrders == 0 @@ -67,6 +92,55 @@ func (b *ActiveOrderBook) waitAllClear(ctx context.Context, waitTime, timeout ti } } +// Cancel cancels the given order from activeOrderBook gracefully +func (b *ActiveOrderBook) Cancel(ctx context.Context, ex types.Exchange, order types.Order) error { + if !b.Exists(order) { + return fmt.Errorf("cannot find %v in orderbook", order) + } + // optimize order cancel for back-testing + if IsBackTesting { + return ex.CancelOrders(context.Background(), order) + } + log.Debugf("[ActiveOrderBook] gracefully cancelling %s order...", order.OrderID) + waitTime := CancelOrderWaitTime + + startTime := time.Now() + // ensure order is cancelled + for { + // Some orders in the variable are not created on the server side yet, + // If we cancel these orders directly, we will get an unsent order error + // We wait here for a while for server to create these orders. + // time.Sleep(SentOrderWaitTime) + + // since ctx might be canceled, we should use background context here + + if err := ex.CancelOrders(context.Background(), order); err != nil { + log.WithError(err).Errorf("[ActiveORderBook] can not cancel %s order", order.OrderID) + } + log.Debugf("[ActiveOrderBook] waiting %s for %s order to be cancelled...", waitTime, order.OrderID) + clear, err := b.waitClear(ctx, order, waitTime, 5*time.Second) + if clear || err != nil { + break + } + b.Print() + + openOrders, err := ex.QueryOpenOrders(ctx, order.Symbol) + if err != nil { + log.WithError(err).Errorf("can not query %s open orders", order.Symbol) + continue + } + + openOrderStore := NewOrderStore(order.Symbol) + openOrderStore.Add(openOrders...) + // if it's not on the order book (open orders), we should remove it from our local side + if !openOrderStore.Exists(order.OrderID) { + b.Remove(order) + } + } + log.Debugf("[ActiveOrderBook] %s order is cancelled successfully in %s", order.OrderID, b.Symbol, time.Since(startTime)) + return nil +} + // GracefulCancel cancels the active orders gracefully func (b *ActiveOrderBook) GracefulCancel(ctx context.Context, ex types.Exchange) error { // optimize order cancel for back-testing diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index d8d8347ff..33b119d11 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -144,6 +144,20 @@ func (e *GeneralOrderExecutor) GracefulCancelActiveOrderBook(ctx context.Context return nil } +func (e *GeneralOrderExecutor) Cancel(ctx context.Context, order types.Order) error { + if e.activeMakerOrders.NumOfOrders() == 0 { + return nil + } + if err := e.activeMakerOrders.Cancel(ctx, e.session.Exchange, order); err != nil { + // Retry once + if err = e.activeMakerOrders.Cancel(ctx, e.session.Exchange, order); err != nil { + return fmt.Errorf("cancel order error: %w", err) + } + } + e.tradeCollector.Process() + return nil +} + // GracefulCancel cancels all active maker orders func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context) error { return e.GracefulCancelActiveOrderBook(ctx, e.activeMakerOrders) diff --git a/pkg/bbgo/serialmarketdatastore.go b/pkg/bbgo/serialmarketdatastore.go new file mode 100644 index 000000000..3bcf6b645 --- /dev/null +++ b/pkg/bbgo/serialmarketdatastore.go @@ -0,0 +1,69 @@ +package bbgo + +import ( + "time" + + "github.com/c9s/bbgo/pkg/types" +) + +type SerialMarketDataStore struct { + *MarketDataStore + KLines map[types.Interval]*types.KLine + Subscription []types.Interval +} + +func NewSerialMarketDataStore(symbol string) *SerialMarketDataStore { + return &SerialMarketDataStore{ + MarketDataStore: NewMarketDataStore(symbol), + KLines: make(map[types.Interval]*types.KLine), + Subscription: []types.Interval{}, + } +} + +func (store *SerialMarketDataStore) Subscribe(interval types.Interval) { + // dedup + for _, i := range store.Subscription { + if i == interval { + return + } + } + store.Subscription = append(store.Subscription, interval) +} + +func (store *SerialMarketDataStore) BindStream(stream types.Stream) { + stream.OnKLineClosed(store.handleKLineClosed) +} + +func (store *SerialMarketDataStore) handleKLineClosed(kline types.KLine) { + store.AddKLine(kline) +} + +func (store *SerialMarketDataStore) AddKLine(kline types.KLine) { + if kline.Symbol != store.Symbol { + return + } + // only consumes kline1m + if kline.Interval != types.Interval1m { + return + } + // endtime in minutes + timestamp := kline.StartTime.Time().Add(time.Minute) + for _, val := range store.Subscription { + k, ok := store.KLines[val] + if !ok { + k = &types.KLine{} + k.Set(&kline) + k.Interval = val + k.Closed = false + store.KLines[val] = k + } else { + k.Merge(&kline) + k.Closed = false + } + if timestamp.Truncate(val.Duration()) == timestamp { + k.Closed = true + store.MarketDataStore.AddKLine(*k) + delete(store.KLines, val) + } + } +} diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index 136be0a8a..8a1e73c60 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -7,6 +7,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/bollmaker" _ "github.com/c9s/bbgo/pkg/strategy/dca" _ "github.com/c9s/bbgo/pkg/strategy/drift" + _ "github.com/c9s/bbgo/pkg/strategy/elliottwave" _ "github.com/c9s/bbgo/pkg/strategy/emastop" _ "github.com/c9s/bbgo/pkg/strategy/etf" _ "github.com/c9s/bbgo/pkg/strategy/ewoDgtrd" diff --git a/pkg/strategy/elliottwave/ewo.go b/pkg/strategy/elliottwave/ewo.go new file mode 100644 index 000000000..ff6b7cf2e --- /dev/null +++ b/pkg/strategy/elliottwave/ewo.go @@ -0,0 +1,25 @@ +package elliottwave + +import "github.com/c9s/bbgo/pkg/indicator" + +type ElliottWave struct { + maSlow *indicator.SMA + maQuick *indicator.SMA +} + +func (s *ElliottWave) Index(i int) float64 { + return s.maQuick.Index(i)/s.maSlow.Index(i) - 1.0 +} + +func (s *ElliottWave) Last() float64 { + return s.maQuick.Last()/s.maSlow.Last() - 1.0 +} + +func (s *ElliottWave) Length() int { + return s.maSlow.Length() +} + +func (s *ElliottWave) Update(v float64) { + s.maSlow.Update(v) + s.maQuick.Update(v) +} diff --git a/pkg/strategy/elliottwave/strategy.go b/pkg/strategy/elliottwave/strategy.go new file mode 100644 index 000000000..b22c7e412 --- /dev/null +++ b/pkg/strategy/elliottwave/strategy.go @@ -0,0 +1,495 @@ +package elliottwave + +import ( + "bytes" + "context" + "errors" + "fmt" + "math" + "os" + "sync" + "time" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/strategy" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" + "github.com/sirupsen/logrus" +) + +const ID = "elliottwave" + +var log = logrus.WithField("strategy", ID) +var Two fixedpoint.Value = fixedpoint.NewFromInt(2) +var Three fixedpoint.Value = fixedpoint.NewFromInt(3) +var Four fixedpoint.Value = fixedpoint.NewFromInt(4) +var Delta fixedpoint.Value = fixedpoint.NewFromFloat(0.00001) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type SourceFunc func(*types.KLine) fixedpoint.Value + +type Strategy struct { + Symbol string `json:"symbol"` + + bbgo.StrategyController + types.Market + strategy.SourceSelector + Session *bbgo.ExchangeSession + + Interval types.Interval `json:"interval"` + Stoploss fixedpoint.Value `json:"stoploss"` + WindowATR int `json:"windowATR"` + WindowQuick int `json:"windowQuick"` + WindowSlow int `json:"windowSlow"` + PendingMinutes int `json:"pendingMinutes"` + + *bbgo.Environment + *bbgo.GeneralOrderExecutor + *types.Position `persistence:"position"` + *types.ProfitStats `persistence:"profit_stats"` + *types.TradeStats `persistence:"trade_stats"` + + ewo *ElliottWave + atr *indicator.ATR + + getLastPrice func() fixedpoint.Value + + // for smart cancel + orderPendingCounter map[uint64]int + startTime time.Time + minutesCounter int + + // for position + buyPrice float64 `persistence:"buy_price"` + sellPrice float64 `persistence:"sell_price"` + highestPrice float64 `persistence:"highest_price"` + lowestPrice float64 `persistence:"lowest_price"` + + TrailingCallbackRate []float64 `json:"trailingCallbackRate"` + TrailingActivationRatio []float64 `json:"trailingActivationRatio"` + ExitMethods bbgo.ExitMethodSet `json:"exits"` + + midPrice fixedpoint.Value + lock sync.RWMutex `ignore:"true"` +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s:%v", ID, s.Symbol, bbgo.IsBackTesting) +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: types.Interval1m, + }) + if !bbgo.IsBackTesting { + session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{}) + } + s.ExitMethods.SetAndSubscribe(session, s) +} + +func (s *Strategy) CurrentPosition() *types.Position { + return s.Position +} + +func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { + order := s.Position.NewMarketCloseOrder(percentage) + if order == nil { + return nil + } + 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 + } + for { + if s.Market.IsDustQuantity(order.Quantity, price) { + return nil + } + _, err := s.GeneralOrderExecutor.SubmitOrders(ctx, *order) + if err != nil { + order.Quantity = order.Quantity.Mul(fixedpoint.One.Sub(Delta)) + continue + } + return nil + } + +} + +func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error { + maSlow := &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.WindowSlow}} + maQuick := &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.WindowQuick}} + s.ewo = &ElliottWave{ + maSlow, maQuick, + } + s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.WindowATR}} + klines, ok := store.KLinesOfInterval(s.Interval) + klineLength := len(*klines) + if !ok || klineLength == 0 { + return errors.New("klines not exists") + } + s.startTime = (*klines)[klineLength-1].EndTime.Time() + + for _, kline := range *klines { + source := s.GetSource(&kline).Float64() + s.ewo.Update(source) + s.atr.PushK(kline) + } + return nil +} + +// FIXME: stdevHigh +func (s *Strategy) smartCancel(ctx context.Context, pricef float64) int { + nonTraded := s.GeneralOrderExecutor.ActiveMakerOrders().Orders() + if len(nonTraded) > 0 { + left := 0 + for _, order := range nonTraded { + toCancel := false + if s.minutesCounter-s.orderPendingCounter[order.OrderID] >= s.PendingMinutes { + toCancel = true + } else if order.Side == types.SideTypeBuy { + if order.Price.Float64()+s.atr.Last()*2 <= pricef { + toCancel = true + } + } else if order.Side == types.SideTypeSell { + // 75% of the probability + if order.Price.Float64()-s.atr.Last()*2 >= pricef { + toCancel = true + } + } else { + panic("not supported side for the order") + } + if toCancel { + err := s.GeneralOrderExecutor.Cancel(ctx, order) + if err == nil { + delete(s.orderPendingCounter, order.OrderID) + } else { + log.WithError(err).Errorf("failed to cancel %v", order.OrderID) + } + log.Warnf("cancel %v", order.OrderID) + } else { + left += 1 + } + } + return left + } + return len(nonTraded) +} + +func (s *Strategy) trailingCheck(price float64, direction string) bool { + if s.highestPrice > 0 && s.highestPrice < price { + s.highestPrice = price + } + if s.lowestPrice > 0 && s.lowestPrice < price { + s.lowestPrice = price + } + isShort := direction == "short" + for i := len(s.TrailingCallbackRate) - 1; i >= 0; i-- { + trailingCallbackRate := s.TrailingCallbackRate[i] + trailingActivationRatio := s.TrailingActivationRatio[i] + if isShort { + if (s.sellPrice-s.lowestPrice)/s.lowestPrice > trailingActivationRatio { + return (price-s.lowestPrice)/s.lowestPrice > trailingCallbackRate + } + } else { + if (s.highestPrice-s.buyPrice)/s.buyPrice > trailingActivationRatio { + return (s.highestPrice-price)/price > trailingCallbackRate + } + } + } + return false +} + +func (s *Strategy) initTickerFunctions() { + if s.IsBackTesting() { + s.getLastPrice = func() fixedpoint.Value { + lastPrice, ok := s.Session.LastPrice(s.Symbol) + if !ok { + log.Error("cannot get lastprice") + } + return lastPrice + } + } else { + s.Session.MarketDataStream.OnBookTickerUpdate(func(ticker types.BookTicker) { + bestBid := ticker.Buy + bestAsk := ticker.Sell + if !util.TryLock(&s.lock) { + return + } + if !bestAsk.IsZero() && !bestBid.IsZero() { + s.midPrice = bestAsk.Add(bestBid).Div(Two) + } else if !bestAsk.IsZero() { + s.midPrice = bestAsk + } else if !bestBid.IsZero() { + s.midPrice = bestBid + } + s.lock.Unlock() + }) + s.getLastPrice = func() (lastPrice fixedpoint.Value) { + var ok bool + s.lock.RLock() + defer s.lock.RUnlock() + if s.midPrice.IsZero() { + lastPrice, ok = s.Session.LastPrice(s.Symbol) + if !ok { + log.Error("cannot get lastprice") + return lastPrice + } + } else { + lastPrice = s.midPrice + } + return lastPrice + } + } +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + instanceID := s.InstanceID() + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + if s.TradeStats == nil { + s.TradeStats = types.NewTradeStats(s.Symbol) + } + // StrategyController + s.Status = types.StrategyStatusRunning + // Get source function from config input + s.SourceSelector.Init() + s.OnSuspend(func() { + _ = s.GeneralOrderExecutor.GracefulCancel(ctx) + }) + s.OnEmergencyStop(func() { + _ = s.GeneralOrderExecutor.GracefulCancel(ctx) + _ = s.ClosePosition(ctx, fixedpoint.One) + }) + s.GeneralOrderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.GeneralOrderExecutor.BindEnvironment(s.Environment) + s.GeneralOrderExecutor.BindProfitStats(s.ProfitStats) + s.GeneralOrderExecutor.BindTradeStats(s.TradeStats) + s.GeneralOrderExecutor.TradeCollector().OnPositionUpdate(func(p *types.Position) { + bbgo.Sync(s) + }) + s.GeneralOrderExecutor.Bind() + + s.orderPendingCounter = make(map[uint64]int) + s.minutesCounter = 0 + + for _, method := range s.ExitMethods { + method.Bind(session, s.GeneralOrderExecutor) + } + s.GeneralOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _profit, _netProfit fixedpoint.Value) { + if s.Position.IsDust(trade.Price) { + s.buyPrice = 0 + s.sellPrice = 0 + s.highestPrice = 0 + s.lowestPrice = 0 + } else if s.Position.IsLong() { + s.buyPrice = trade.Price.Float64() + s.sellPrice = 0 + s.highestPrice = s.buyPrice + s.lowestPrice = 0 + } else { + s.sellPrice = trade.Price.Float64() + s.buyPrice = 0 + s.highestPrice = 0 + s.lowestPrice = s.sellPrice + } + }) + s.initTickerFunctions() + + startTime := s.Environment.StartTime() + s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, startTime)) + s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, startTime)) + + st, _ := session.MarketDataStore(s.Symbol) + store := bbgo.NewSerialMarketDataStore(s.Symbol) + klines, ok := st.KLinesOfInterval(types.Interval1m) + if !ok { + panic("cannot get 1m history") + } + // event trigger order: s.Interval => Interval1m + store.Subscribe(s.Interval) + store.Subscribe(types.Interval1m) + for _, kline := range *klines { + store.AddKLine(kline) + } + store.OnKLineClosed(func(kline types.KLine) { + s.minutesCounter = int(kline.StartTime.Time().Sub(s.startTime).Minutes()) + if kline.Interval == types.Interval1m { + s.klineHandler1m(ctx, kline) + } else if kline.Interval == s.Interval { + s.klineHandler(ctx, kline) + } + }) + store.BindStream(session.MarketDataStream) + if err := s.initIndicators(store); err != nil { + log.WithError(err).Errorf("initIndicator failed") + return nil + } + + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + var buffer bytes.Buffer + for _, daypnl := range s.TradeStats.IntervalProfits[types.Interval1d].GetNonProfitableIntervals() { + fmt.Fprintf(&buffer, "%s\n", daypnl) + } + fmt.Fprintln(&buffer, s.TradeStats.BriefString()) + os.Stdout.Write(buffer.Bytes()) + wg.Done() + }) + return nil +} + +func (s *Strategy) klineHandler1m(ctx context.Context, kline types.KLine) { + if s.Status != types.StrategyStatusRunning { + return + } + + stoploss := s.Stoploss.Float64() + price := s.getLastPrice() + pricef := price.Float64() + + numPending := s.smartCancel(ctx, pricef) + if numPending > 0 { + log.Infof("pending orders: %d, exit", numPending) + return + } + lowf := math.Min(kline.Low.Float64(), pricef) + highf := math.Max(kline.High.Float64(), pricef) + if s.lowestPrice > 0 && lowf < s.lowestPrice { + s.lowestPrice = lowf + } + if s.highestPrice > 0 && highf > s.highestPrice { + s.highestPrice = highf + } + exitShortCondition := s.sellPrice > 0 && (s.sellPrice*(1.+stoploss) <= highf || + s.trailingCheck(highf, "short")) + exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= lowf || + s.trailingCheck(lowf, "long")) + + if exitShortCondition || exitLongCondition { + _ = s.ClosePosition(ctx, fixedpoint.One) + } +} + +func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { + source := s.GetSource(&kline) + sourcef := source.Float64() + s.ewo.Update(sourcef) + s.atr.PushK(kline) + + if s.Status != types.StrategyStatusRunning { + return + } + + stoploss := s.Stoploss.Float64() + price := s.getLastPrice() + pricef := price.Float64() + lowf := math.Min(kline.Low.Float64(), pricef) + highf := math.Min(kline.High.Float64(), pricef) + + s.smartCancel(ctx, pricef) + + ewo := types.Array(s.ewo, 3) + shortCondition := ewo[0] < ewo[1] && ewo[1] > ewo[2] + longCondition := ewo[0] > ewo[1] && ewo[1] < ewo[2] + + exitShortCondition := s.sellPrice > 0 && !shortCondition && s.sellPrice*(1.+stoploss) <= highf || s.trailingCheck(highf, "short") + exitLongCondition := s.buyPrice > 0 && !longCondition && s.buyPrice*(1.-stoploss) >= lowf || s.trailingCheck(lowf, "long") + + if exitShortCondition || exitLongCondition { + if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + return + } + s.ClosePosition(ctx, fixedpoint.One) + } + if longCondition { + if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + return + } + if source.Compare(price) > 0 { + source = price + sourcef = source.Float64() + } + balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() + quoteBalance, ok := balances[s.Market.QuoteCurrency] + if !ok { + log.Errorf("unable to get quoteCurrency") + return + } + if s.Market.IsDustQuantity( + quoteBalance.Available.Div(source), source) { + return + } + quantity := quoteBalance.Available.Div(source) + createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Price: source, + Quantity: quantity, + Tag: "long", + }) + if err != nil { + log.WithError(err).Errorf("cannot place buy order") + log.Errorf("%v %v %v", quoteBalance, source, kline) + return + } + s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + return + } + if shortCondition { + if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { + log.WithError(err).Errorf("cannot cancel orders") + return + } + if source.Compare(price) < 0 { + source = price + sourcef = price.Float64() + } + balances := s.GeneralOrderExecutor.Session().GetAccount().Balances() + baseBalance, ok := balances[s.Market.BaseCurrency] + if !ok { + log.Errorf("unable to get baseCurrency") + return + } + if s.Market.IsDustQuantity(baseBalance.Available, source) { + return + } + quantity := baseBalance.Available + createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimit, + Price: source, + Quantity: quantity, + Tag: "short", + }) + if err != nil { + log.WithError(err).Errorf("cannot place sell order") + return + } + s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + return + } +} diff --git a/pkg/strategy/source.go b/pkg/strategy/source.go new file mode 100644 index 000000000..433f0a6c2 --- /dev/null +++ b/pkg/strategy/source.go @@ -0,0 +1,50 @@ +package strategy + +import ( + "strings" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + log "github.com/sirupsen/logrus" +) + +type SourceFunc func(*types.KLine) fixedpoint.Value + +var Four fixedpoint.Value = fixedpoint.NewFromInt(4) +var Three fixedpoint.Value = fixedpoint.NewFromInt(3) +var Two fixedpoint.Value = fixedpoint.NewFromInt(2) + +type SourceSelector struct { + Source string `json:"source,omitempty"` + getSource SourceFunc +} + +func (s *SourceSelector) Init() { + switch strings.ToLower(s.Source) { + case "close": + s.getSource = func(kline *types.KLine) fixedpoint.Value { return kline.Close } + case "high": + s.getSource = func(kline *types.KLine) fixedpoint.Value { return kline.High } + case "low": + s.getSource = func(kline *types.KLine) fixedpoint.Value { return kline.Low } + case "hl2": + s.getSource = func(kline *types.KLine) fixedpoint.Value { return kline.High.Add(kline.Low).Div(Two) } + case "hlc3": + s.getSource = func(kline *types.KLine) fixedpoint.Value { + return kline.High.Add(kline.Low).Add(kline.Close).Div(Three) + } + case "ohlc4": + s.getSource = func(kline *types.KLine) fixedpoint.Value { + return kline.High.Add(kline.Low).Add(kline.Close).Add(kline.Open).Div(Four) + } + case "open": + s.getSource = func(kline *types.KLine) fixedpoint.Value { return kline.Open } + default: + log.Infof("source not set: %s, use hl2 by default", s.Source) + s.getSource = func(kline *types.KLine) fixedpoint.Value { return kline.High.Add(kline.Low).Div(Two) } + } +} + +func (s *SourceSelector) GetSource(kline *types.KLine) fixedpoint.Value { + return s.getSource(kline) +} diff --git a/pkg/types/interval.go b/pkg/types/interval.go index 8d3958afc..099aefa9c 100644 --- a/pkg/types/interval.go +++ b/pkg/types/interval.go @@ -41,6 +41,7 @@ func (s IntervalSlice) StringSlice() (slice []string) { } var Interval1m = Interval("1m") +var Interval3m = Interval("3m") var Interval5m = Interval("5m") var Interval15m = Interval("15m") var Interval30m = Interval("30m") @@ -57,6 +58,7 @@ var Interval1mo = Interval("1mo") var SupportedIntervals = map[Interval]int{ Interval1m: 1, + Interval3m: 3, Interval5m: 5, Interval15m: 15, Interval30m: 30, diff --git a/pkg/types/kline.go b/pkg/types/kline.go index 9cb4bcad6..936ca2b47 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -91,6 +91,20 @@ func (k *KLine) Set(o *KLine) { k.Closed = o.Closed } +func (k *KLine) Merge(o *KLine) { + k.EndTime = o.EndTime + k.Close = o.Close + k.High = fixedpoint.Max(k.High, o.High) + k.Low = fixedpoint.Min(k.Low, o.Low) + k.Volume = k.Volume.Add(o.Volume) + k.QuoteVolume = k.QuoteVolume.Add(o.QuoteVolume) + k.TakerBuyBaseAssetVolume = k.TakerBuyBaseAssetVolume.Add(o.TakerBuyBaseAssetVolume) + k.TakerBuyQuoteAssetVolume = k.TakerBuyQuoteAssetVolume.Add(o.TakerBuyQuoteAssetVolume) + k.LastTradeID = o.LastTradeID + k.NumberOfTrades += o.NumberOfTrades + k.Closed = o.Closed +} + func (k KLine) GetStartTime() Time { return k.StartTime } diff --git a/pkg/types/pca.go b/pkg/types/pca.go new file mode 100644 index 000000000..9ecc00a6c --- /dev/null +++ b/pkg/types/pca.go @@ -0,0 +1,58 @@ +package types + +import ( + "fmt" + "gonum.org/v1/gonum/mat" +) + +type PCA struct { + svd *mat.SVD +} + +func (pca *PCA) FitTransform(x []SeriesExtend, lookback, feature int) ([]SeriesExtend, error) { + if err := pca.Fit(x, lookback); err != nil { + return nil, err + } + return pca.Transform(x, lookback, feature), nil +} + +func (pca *PCA) Fit(x []SeriesExtend, lookback int) error { + vec := make([]float64, lookback*len(x)) + for i, xx := range x { + mean := xx.Mean(lookback) + for j := 0; j < lookback; j++ { + vec[i+j*i] = xx.Index(j) - mean + } + } + pca.svd = &mat.SVD{} + diffMatrix := mat.NewDense(lookback, len(x), vec) + if ok := pca.svd.Factorize(diffMatrix, mat.SVDThin); !ok { + return fmt.Errorf("Unable to factorize") + } + return nil +} + +func (pca *PCA) Transform(x []SeriesExtend, lookback int, features int) (result []SeriesExtend) { + result = make([]SeriesExtend, features) + vTemp := new(mat.Dense) + pca.svd.VTo(vTemp) + var ret mat.Dense + vec := make([]float64, lookback*len(x)) + for i, xx := range x { + for j := 0; j < lookback; j++ { + vec[i+j*i] = xx.Index(j) + } + } + newX := mat.NewDense(lookback, len(x), vec) + ret.Mul(newX, vTemp) + newMatrix := mat.NewDense(lookback, features, nil) + newMatrix.Copy(&ret) + for i := 0; i < features; i++ { + queue := NewQueue(lookback) + for j := 0; j < lookback; j++ { + queue.Update(newMatrix.At(lookback-j-1, i)) + } + result[i] = queue + } + return result +}