diff --git a/go.mod b/go.mod index e459de352a..aa15fb8140 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/x-cray/logrus-prefixed-formatter v0.5.2 golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect golang.org/x/time v0.0.0-20191024005414-555d28b269f0 - gonum.org/v1/gonum v0.8.1 // indirect + gonum.org/v1/gonum v0.8.1 gopkg.in/yaml.v2 v2.3.0 // indirect gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c ) diff --git a/pkg/bbgo/environment.go b/pkg/bbgo/environment.go index 22b11e1db8..478ecec184 100644 --- a/pkg/bbgo/environment.go +++ b/pkg/bbgo/environment.go @@ -158,7 +158,7 @@ func (environ *Environment) Init(ctx context.Context) (err error) { session.marketDataStores[kline.Symbol].AddKLine(kline) }) - session.Stream.OnTrade(func(trade types.Trade) { + session.Stream.OnTradeUpdate(func(trade types.Trade) { // append trades session.Trades[trade.Symbol] = append(session.Trades[trade.Symbol], trade) diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index a4dcae2fba..fd5c9c2c22 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -106,7 +106,7 @@ func (trader *Trader) Run(ctx context.Context) error { for sessionName := range trader.environment.sessions { var session = trader.environment.sessions[sessionName] if trader.tradeReporter != nil { - session.Stream.OnTrade(func(trade types.Trade) { + session.Stream.OnTradeUpdate(func(trade types.Trade) { trader.tradeReporter.Report(trade) }) } @@ -290,7 +290,7 @@ func (trader *OrderExecutor) RunStrategy(ctx context.Context, strategy SingleExc trader.reportPnL() }) - stream.OnTrade(func(trade *types.Trade) { + stream.OnTradeUpdate(func(trade *types.Trade) { trader.NotifyTrade(trade) trader.ProfitAndLossCalculator.AddTrade(*trade) _, err := trader.Context.StockManager.AddTrades([]types.Trade{*trade}) diff --git a/pkg/exchange/binance/stream.go b/pkg/exchange/binance/stream.go index 75489ba0db..39e502a35e 100644 --- a/pkg/exchange/binance/stream.go +++ b/pkg/exchange/binance/stream.go @@ -137,7 +137,7 @@ func NewStream(client *binance.Client) *Stream { break } - stream.EmitTrade(*trade) + stream.EmitTradeUpdate(*trade) } }) diff --git a/pkg/exchange/max/stream.go b/pkg/exchange/max/stream.go index 5dbfcc07a5..519fefe639 100644 --- a/pkg/exchange/max/stream.go +++ b/pkg/exchange/max/stream.go @@ -38,7 +38,7 @@ func NewStream(key, secret string) *Stream { return } - stream.EmitTrade(*trade) + stream.EmitTradeUpdate(*trade) } }) diff --git a/pkg/indicator/boll.go b/pkg/indicator/boll.go index 0206a46c89..f579dc6360 100644 --- a/pkg/indicator/boll.go +++ b/pkg/indicator/boll.go @@ -74,14 +74,18 @@ func (inc *BOLL) calculateAndUpdate(kLines []types.KLine) { var std = stat.StdDev(prices, nil) inc.StdDev.Push(std) - var upBand = sma + inc.K*std + var band = inc.K * std + + var upBand = sma + band inc.UpBand.Push(upBand) - var downBand = sma - inc.K*std + var downBand = sma - band inc.DownBand.Push(downBand) // update end time inc.EndTime = kLines[index].EndTime + + inc.EmitUpdate(sma, upBand, downBand) } func (inc *BOLL) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { diff --git a/pkg/indicator/boll_callbacks.go b/pkg/indicator/boll_callbacks.go new file mode 100644 index 0000000000..cafe0bcade --- /dev/null +++ b/pkg/indicator/boll_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type BOLL"; DO NOT EDIT. + +package indicator + +import () + +func (inc *BOLL) OnUpdate(cb func(sma float64, upBand float64, downBand float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *BOLL) EmitUpdate(sma float64, upBand float64, downBand float64) { + for _, cb := range inc.updateCallbacks { + cb(sma, upBand, downBand) + } +} diff --git a/pkg/strategy/grid/strategy.go b/pkg/strategy/grid/strategy.go new file mode 100644 index 0000000000..ec8357d808 --- /dev/null +++ b/pkg/strategy/grid/strategy.go @@ -0,0 +1,200 @@ +package grid + +import ( + "context" + "time" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +// The indicators (SMA and EWMA) that we want to use are returning float64 data. +type Float64Indicator interface { + Last() float64 +} + +func init() { + // Register the pointer of the strategy struct, + // so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON) + // Note: built-in strategies need to imported manually in the bbgo cmd package. + bbgo.RegisterStrategy("grid", &Strategy{}) +} + +type Strategy struct { + // The notification system will be injected into the strategy automatically. + // This field will be injected automatically since it's a single exchange strategy. + *bbgo.Notifiability + + // OrderExecutor is an interface for submitting order. + // This field will be injected automatically since it's a single exchange strategy. + bbgo.OrderExecutor + + // if Symbol string field is defined, bbgo will know it's a symbol-based strategy + // The following embedded fields will be injected with the corresponding instances. + + // MarketDataStore is a pointer only injection field. public trades, k-lines (candlestick) + // and order book updates are maintained in the market data store. + // This field will be injected automatically since we defined the Symbol field. + *bbgo.MarketDataStore + + // StandardIndicatorSet contains the standard indicators of a market (symbol) + // This field will be injected automatically since we defined the Symbol field. + *bbgo.StandardIndicatorSet + + // Market stores the configuration of the market, for example, VolumePrecision, PricePrecision, MinLotSize... etc + // This field will be injected automatically since we defined the Symbol field. + types.Market + + // These fields will be filled from the config file (it translates YAML to JSON) + Symbol string `json:"symbol"` + + Interval types.Interval `json:"interval"` + + // GridPips is the pips of grid, e.g., 0.001 + GridPips fixedpoint.Value `json:"gridPips"` + + // GridNum is the grid number (order numbers) + GridNum int `json:"gridNum"` + + BaseQuantity float64 `json:"baseQuantity"` + + activeBidOrders map[uint64]types.Order + activeAskOrders map[uint64]types.Order + + boll *indicator.BOLL +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + // currently we need the 1m kline to update the last close price and indicators + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval.String()}) +} + +func (s *Strategy) updateBidOrders(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) { + quoteCurrency := s.Market.QuoteCurrency + balances := session.Account.Balances() + + balance, ok := balances[quoteCurrency] + if !ok || balance.Available <= 0.0 { + return + } + + var numOrders = s.GridNum - len(s.activeBidOrders) + if numOrders <= 0 { + return + } + + var upBand = s.boll.LastUpBand() + var startPrice = upBand + + var submitOrders []types.SubmitOrder + for i := 0 ; i < numOrders ; i++ { + submitOrders = append(submitOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Market: s.Market, + Quantity: s.BaseQuantity, + Price: startPrice, + TimeInForce: "GTC", + }) + + startPrice -= s.GridPips.Float64() + } + + orders, err := orderExecutor.SubmitOrders(context.Background(), submitOrders...) + if err != nil { + return + } + + for _, order := range orders { + s.activeBidOrders[order.OrderID] = order + } +} + +func (s *Strategy) updateAskOrders(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) { + baseCurrency := s.Market.BaseCurrency + balances := session.Account.Balances() + + balance, ok := balances[baseCurrency] + if !ok || balance.Available <= 0.0 { + return + } + + var numOrders = s.GridNum - len(s.activeAskOrders) + if numOrders <= 0 { + return + } + + var downBand = s.boll.LastDownBand() + var startPrice = downBand + + var submitOrders []types.SubmitOrder + for i := 0 ; i < numOrders ; i++ { + submitOrders = append(submitOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimit, + Market: s.Market, + Quantity: s.BaseQuantity, + Price: startPrice, + TimeInForce: "GTC", + }) + + startPrice += s.GridPips.Float64() + } + + orders, err := orderExecutor.SubmitOrders(context.Background(), submitOrders...) + if err != nil { + return + } + + for _, order := range orders { + s.activeAskOrders[order.OrderID] = order + } +} + +func (s *Strategy) updateOrders(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) { + if len(s.activeBidOrders) < s.GridNum { + s.updateBidOrders(orderExecutor, session) + } + + if len(s.activeAskOrders) < s.GridNum { + s.updateAskOrders(orderExecutor, session) + } +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + // 1. we don't persist orders so that we can not clear the previous orders for now. just need time to support this. + if s.GridNum == 0 { + s.GridNum = 2 + } + + s.activeBidOrders = make(map[uint64]types.Order) + s.activeAskOrders = make(map[uint64]types.Order) + s.boll = s.StandardIndicatorSet.GetBOLL(types.IntervalWindow{ + Interval: s.Interval, + Window: 21, + }) + + // session.Stream.OnOrderUpdate(func) + + go func() { + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + + case <-ticker.C: + // see if we have enough balances and then we create limit orders on the up band and the down band. + s.updateOrders(orderExecutor, session) + } + } + }() + + return nil +}