From 4e3f325bb66891bf40154639b96f5c4cf3cd4951 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 21 Mar 2021 10:44:06 +0800 Subject: [PATCH 1/9] first commit of xmaker strategy from mobydick --- pkg/strategy/xmaker/strategy.go | 457 ++++++++++++++++++++++++++++++++ 1 file changed, 457 insertions(+) create mode 100644 pkg/strategy/xmaker/strategy.go diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go new file mode 100644 index 000000000..1db940083 --- /dev/null +++ b/pkg/strategy/xmaker/strategy.go @@ -0,0 +1,457 @@ +package xmaker + +import ( + "context" + "fmt" + "hash/fnv" + "math" + "sync" + "time" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/service" + "github.com/c9s/bbgo/pkg/types" +) + +var defaultMargin = fixedpoint.NewFromFloat(0.01) + +var defaultQuantity = fixedpoint.NewFromFloat(0.001) + +const ID = "xmaker" + +const stateKey = "state-v1" + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +func (s *Strategy) ID() string { + return ID +} + +type State struct { + HedgePosition fixedpoint.Value `json:"hedgePosition"` +} + +type Strategy struct { + *bbgo.Graceful + *bbgo.Notifiability + *bbgo.Persistence + + Symbol string `json:"symbol"` + SourceExchange string `json:"sourceExchange"` + MakerExchange string `json:"makerExchange"` + + UpdateInterval types.Duration `json:"updateInterval"` + HedgeInterval types.Duration `json:"hedgeInterval"` + + Margin fixedpoint.Value `json:"margin"` + BidMargin fixedpoint.Value `json:"bidMargin"` + AskMargin fixedpoint.Value `json:"askMargin"` + Quantity fixedpoint.Value `json:"quantity"` + QuantityMultiplier fixedpoint.Value `json:"quantityMultiplier"` + DisableHedge bool `json:"disableHedge"` + + NumLayers int `json:"numLayers"` + Pips int `json:"pips"` + + makerSession *bbgo.ExchangeSession + sourceSession *bbgo.ExchangeSession + + sourceMarket types.Market + makerMarket types.Market + + state *State + + book *types.StreamOrderBook + activeMakerOrders *bbgo.LocalActiveOrderBook + + orderStore *bbgo.OrderStore + + lastPrice float64 + groupID int64 + + stopC chan struct{} +} + +func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { + sourceSession, ok := sessions[s.SourceExchange] + if !ok { + panic(fmt.Errorf("source exchange %s is not defined", s.SourceExchange)) + } + + log.Infof("subscribing %s from %s", s.Symbol, s.SourceExchange) + sourceSession.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{}) +} + +func (s *Strategy) updateQuote(ctx context.Context) { + if err := s.makerSession.Exchange.CancelOrders(ctx, s.activeMakerOrders.Orders()...); err != nil { + log.WithError(err).Errorf("can not cancel orders") + return + } + + // avoid unlock issue + time.Sleep(800 * time.Millisecond) + + sourceBook := s.book.Get() + if len(sourceBook.Bids) == 0 || len(sourceBook.Asks) == 0 { + return + } + + if valid, err := sourceBook.IsValid(); !valid { + log.WithError(err).Error("invalid order book: %v", err) + return + } + + bestBidPrice := sourceBook.Bids[0].Price + bestAskPrice := sourceBook.Asks[0].Price + log.Infof("best bid price %f, best ask price: %f", bestBidPrice.Float64(), bestAskPrice.Float64()) + + bidQuantity := s.Quantity + bidPrice := bestBidPrice.MulFloat64(1.0 - s.BidMargin.Float64()) + + askQuantity := s.Quantity + askPrice := bestAskPrice.MulFloat64(1.0 + s.AskMargin.Float64()) + + log.Infof("quote bid price: %f ask price: %f", bidPrice.Float64(), askPrice.Float64()) + + var submitOrders []types.SubmitOrder + + balances := s.makerSession.Account.Balances() + makerQuota := &bbgo.QuotaTransaction{} + if b, ok := balances[s.makerMarket.BaseCurrency]; ok { + makerQuota.BaseAsset.Add(b.Available) + } + if b, ok := balances[s.makerMarket.QuoteCurrency]; ok { + makerQuota.QuoteAsset.Add(b.Available) + } + + hedgeBalances := s.sourceSession.Account.Balances() + hedgeQuota := &bbgo.QuotaTransaction{} + if b, ok := hedgeBalances[s.sourceMarket.BaseCurrency]; ok { + hedgeQuota.BaseAsset.Add(b.Available) + } + if b, ok := hedgeBalances[s.sourceMarket.QuoteCurrency]; ok { + hedgeQuota.QuoteAsset.Add(b.Available) + } + + log.Infof("maker quota: %+v", makerQuota) + log.Infof("hedge quota: %+v", hedgeQuota) + + for i := 0; i < s.NumLayers; i++ { + // bid orders + if makerQuota.QuoteAsset.Lock(bidQuantity.Mul(bidPrice)) && hedgeQuota.BaseAsset.Lock(bidQuantity) { + // if we bought, then we need to sell the base from the hedge session + submitOrders = append(submitOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimit, + Side: types.SideTypeBuy, + Price: bidPrice.Float64(), + Quantity: bidQuantity.Float64(), + TimeInForce: "GTC", + GroupID: s.groupID, + }) + + makerQuota.Commit() + hedgeQuota.Commit() + } else { + makerQuota.Rollback() + hedgeQuota.Rollback() + } + + // ask orders + if makerQuota.BaseAsset.Lock(askQuantity) && hedgeQuota.QuoteAsset.Lock(askQuantity.Mul(askPrice)) { + // if we bought, then we need to sell the base from the hedge session + submitOrders = append(submitOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimit, + Side: types.SideTypeSell, + Price: askPrice.Float64(), + Quantity: askQuantity.Float64(), + TimeInForce: "GTC", + GroupID: s.groupID, + }) + makerQuota.Commit() + hedgeQuota.Commit() + } else { + makerQuota.Rollback() + hedgeQuota.Rollback() + } + + bidPrice -= fixedpoint.NewFromFloat(s.makerMarket.TickSize * float64(s.Pips)) + askPrice += fixedpoint.NewFromFloat(s.makerMarket.TickSize * float64(s.Pips)) + + askQuantity.Mul(s.QuantityMultiplier) + bidQuantity.Mul(s.QuantityMultiplier) + } + + if len(submitOrders) == 0 { + return + } + + makerOrderExecutor := &bbgo.ExchangeOrderExecutor{Session: s.makerSession} + makerOrders, err := makerOrderExecutor.SubmitOrders(ctx, submitOrders...) + if err != nil { + log.WithError(err).Errorf("order error: %s", err.Error()) + return + } + + s.activeMakerOrders.Add(makerOrders...) + s.orderStore.Add(makerOrders...) +} + +func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) { + side := types.SideTypeBuy + + if pos == 0 { + return + } + + quantity := pos + if pos < 0 { + side = types.SideTypeSell + quantity = -pos + } + + lastPrice := s.lastPrice + sourceBook := s.book.Get() + switch side { + + case types.SideTypeBuy: + if len(sourceBook.Asks) > 0 { + if pv, ok := sourceBook.Asks.First(); ok { + lastPrice = pv.Price.Float64() + } + } + + case types.SideTypeSell: + if len(sourceBook.Bids) > 0 { + if pv, ok := sourceBook.Bids.First(); ok { + lastPrice = pv.Price.Float64() + } + } + + } + + notional := quantity.MulFloat64(lastPrice) + if notional.Float64() <= s.sourceMarket.MinNotional { + log.Warnf("less than min notional %f, skipping", notional.Float64()) + return + } + + s.Notifiability.Notify("submitting hedge order: %s %s %f", s.Symbol, side, quantity.Float64()) + orderExecutor := &bbgo.ExchangeOrderExecutor{Session: s.sourceSession} + returnOrders, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeMarket, + Side: side, + Quantity: quantity.Float64(), + }) + + if err != nil { + log.WithError(err).Errorf("market order submit error: %s", err.Error()) + return + } + + s.orderStore.Add(returnOrders...) +} + +func (s *Strategy) handleTradeUpdate(trade types.Trade) { + log.Infof("received trade %+v", trade) + + if trade.Symbol != s.Symbol { + return + } + + if !s.orderStore.Exists(trade.OrderID) { + return + } + + q := fixedpoint.NewFromFloat(trade.Quantity) + switch trade.Side { + case types.SideTypeSell: + q = -q + + case types.SideTypeBuy: + + case types.SideTypeSelf: + // ignore self trades + + default: + log.Infof("ignore non sell/buy side trades, got: %v", trade.Side) + return + + } + + log.Infof("identified trade %d with an existing order: %d", trade.ID, trade.OrderID) + s.Notify("identified %s trade %d with an existing order: %d", trade.Symbol, trade.ID, trade.OrderID) + + s.state.HedgePosition.AtomicAdd(q) + + pos := s.state.HedgePosition.AtomicLoad() + + log.Warnf("position changed: %f", pos.Float64()) + s.Notifiability.Notify("%s position is changed to %f", s.Symbol, pos.Float64()) + + s.lastPrice = trade.Price +} + +func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error { + // configure default values + if s.UpdateInterval == 0 { + s.UpdateInterval = types.Duration(time.Second) + } + + if s.HedgeInterval == 0 { + s.HedgeInterval = types.Duration(10 * time.Second) + } + + if s.NumLayers == 0 { + s.NumLayers = 1 + } + + if s.BidMargin == 0 { + if s.Margin != 0 { + s.BidMargin = s.Margin + } else { + s.BidMargin = defaultMargin + } + } + + if s.AskMargin == 0 { + if s.Margin != 0 { + s.AskMargin = s.Margin + } else { + s.AskMargin = defaultMargin + } + } + + if s.Quantity == 0 { + s.Quantity = defaultQuantity + } + + + // configure sessions + sourceSession, ok := sessions[s.SourceExchange] + if !ok { + return fmt.Errorf("source exchange session %s is not defined", s.SourceExchange) + } + + s.sourceSession = sourceSession + + makerSession, ok := sessions[s.MakerExchange] + if !ok { + return fmt.Errorf("maker exchange session %s is not defined", s.MakerExchange) + } + + s.makerSession = makerSession + + s.sourceMarket, ok = s.sourceSession.Market(s.Symbol) + if !ok { + return fmt.Errorf("source session market %s is not defined", s.Symbol) + } + + s.makerMarket, ok = s.makerSession.Market(s.Symbol) + if !ok { + return fmt.Errorf("maker session market %s is not defined", s.Symbol) + } + + + + + // restore state + instanceID := fmt.Sprintf("%s-%s-%s", ID, s.Symbol) + s.groupID = generateGroupID(instanceID) + log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) + + var state State + + // load position + if err := s.Persistence.Load(&state, stateKey); err != nil { + if err != service.ErrPersistenceNotExists { + return err + } + + s.state = &State{} + } else { + // loaded successfully + s.state = &state + + log.Infof("state is restored: %+v", s.state) + s.Notify("position is restored => %f", s.state.HedgePosition.Float64()) + } + + s.book = types.NewStreamBook(s.Symbol) + s.book.BindStream(s.sourceSession.Stream) + + s.sourceSession.Stream.OnTradeUpdate(s.handleTradeUpdate) + s.makerSession.Stream.OnTradeUpdate(s.handleTradeUpdate) + + s.activeMakerOrders = bbgo.NewLocalActiveOrderBook() + s.activeMakerOrders.BindStream(s.makerSession.Stream) + + s.orderStore = bbgo.NewOrderStore(s.Symbol) + s.orderStore.BindStream(s.sourceSession.Stream) + s.orderStore.BindStream(s.makerSession.Stream) + + s.stopC = make(chan struct{}) + + go func() { + posTicker := time.NewTicker(s.HedgeInterval.Duration()) + defer posTicker.Stop() + + ticker := time.NewTicker(s.UpdateInterval.Duration()) + defer ticker.Stop() + for { + select { + + case <-s.stopC: + return + + case <-ctx.Done(): + return + + case <-ticker.C: + s.updateQuote(ctx) + + case <-posTicker.C: + position := s.state.HedgePosition.AtomicLoad() + abspos := math.Abs(position.Float64()) + if !s.DisableHedge && abspos > s.sourceMarket.MinQuantity { + log.Infof("found position: %f", position.Float64()) + s.Hedge(ctx, -position) + } + } + } + }() + + s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + close(s.stopC) + + if err := s.Persistence.Save(&s.state, stateKey); err != nil { + log.WithError(err).Errorf("can not save state: %+v", s.state) + } else { + log.Infof("state is saved => %+v", s.state) + s.Notify("hedge position %f is saved", s.state.HedgePosition.Float64()) + } + + if err := s.makerSession.Exchange.CancelOrders(ctx, s.activeMakerOrders.Orders()...); err != nil { + log.WithError(err).Errorf("can not cancel orders") + } + }) + + return nil +} + +func generateGroupID(s string) int64 { + h := fnv.New32a() + h.Write([]byte(s)) + return int64(h.Sum32()) +} From 6b877e13945ab31f6ca1ca22be0beae960366f20 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 21 Mar 2021 10:57:28 +0800 Subject: [PATCH 2/9] add limit maker order type --- pkg/types/order.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/types/order.go b/pkg/types/order.go index 8f985033f..927922494 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -58,6 +58,7 @@ type OrderType string const ( OrderTypeLimit OrderType = "LIMIT" + OrderTypeLimitMaker OrderType = "LIMIT_MAKER" OrderTypeMarket OrderType = "MARKET" OrderTypeStopLimit OrderType = "STOP_LIMIT" OrderTypeStopMarket OrderType = "STOP_MARKET" From 837934e69000a2b0398866a3613f2d06119f12d2 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 21 Mar 2021 11:10:41 +0800 Subject: [PATCH 3/9] add post_only order type --- pkg/exchange/max/maxapi/order.go | 75 ++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/pkg/exchange/max/maxapi/order.go b/pkg/exchange/max/maxapi/order.go index 2b21089ca..1c1c0bc2c 100644 --- a/pkg/exchange/max/maxapi/order.go +++ b/pkg/exchange/max/maxapi/order.go @@ -33,6 +33,7 @@ type OrderType string const ( OrderTypeMarket = OrderType("market") OrderTypeLimit = OrderType("limit") + OrderTypePostOnly = OrderType("post_only") OrderTypeStopLimit = OrderType("stop_limit") OrderTypeStopMarket = OrderType("stop_market") ) @@ -236,7 +237,7 @@ type OrderCancelAllRequestParams struct { Side string `json:"side,omitempty"` Market string `json:"market,omitempty"` - GroupID int64 `json:"groupID,omitempty"` + GroupID int64 `json:"groupID,omitempty"` } type OrderCancelAllRequest struct { @@ -417,62 +418,90 @@ func (s *OrderService) NewCreateMultiOrderRequest() *CreateMultiOrderRequest { return &CreateMultiOrderRequest{client: s.client} } -type CreateOrderRequestParams struct { - *PrivateRequestParams - - Market string `json:"market"` - Volume string `json:"volume"` - Price string `json:"price,omitempty"` - StopPrice string `json:"stop_price,omitempty"` - Side string `json:"side"` - OrderType string `json:"ord_type"` - ClientOrderID string `json:"client_oid,omitempty"` - GroupID string `json:"group_id,omitempty"` -} - type CreateOrderRequest struct { client *RestClient - params CreateOrderRequestParams + market *string + volume *string + price *string + stopPrice *string + side *string + orderType *string + clientOrderID *string + groupID *string } func (r *CreateOrderRequest) Market(market string) *CreateOrderRequest { - r.params.Market = market + r.market = &market return r } func (r *CreateOrderRequest) Volume(volume string) *CreateOrderRequest { - r.params.Volume = volume + r.volume = &volume return r } func (r *CreateOrderRequest) Price(price string) *CreateOrderRequest { - r.params.Price = price + r.price = &price return r } func (r *CreateOrderRequest) StopPrice(price string) *CreateOrderRequest { - r.params.StopPrice = price + r.stopPrice = &price return r } func (r *CreateOrderRequest) Side(side string) *CreateOrderRequest { - r.params.Side = side + r.side = &side return r } func (r *CreateOrderRequest) OrderType(orderType string) *CreateOrderRequest { - r.params.OrderType = orderType + r.orderType = &orderType return r } func (r *CreateOrderRequest) ClientOrderID(clientOrderID string) *CreateOrderRequest { - r.params.ClientOrderID = clientOrderID + r.clientOrderID = &clientOrderID return r } func (r *CreateOrderRequest) Do(ctx context.Context) (order *Order, err error) { - req, err := r.client.newAuthenticatedRequest("POST", "v2/orders", &r.params) + var payload = map[string]interface{}{} + + if r.market != nil { + payload["market"] = r.market + } + + if r.volume != nil { + payload["volume"] = r.volume + } + + if r.price != nil { + payload["price"] = r.price + } + + if r.stopPrice != nil { + payload["stop_price"] = r.stopPrice + } + + if r.side != nil { + payload["side"] = r.side + } + + if r.orderType != nil { + payload["ord_type"] = r.orderType + } + + if r.clientOrderID != nil { + payload["client_oid"] = r.clientOrderID + } + + if r.groupID != nil { + payload["group_id"] = r.groupID + } + + req, err := r.client.newAuthenticatedRequest("POST", "v2/orders", &payload) if err != nil { return order, errors.Wrapf(err, "order create error") } From 1f744b0fa51f66d2611e3aaa8c4a317b43c80c8a Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 21 Mar 2021 11:10:55 +0800 Subject: [PATCH 4/9] convert limit maker type to post only --- pkg/exchange/max/exchange.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index d6139d794..3610dc140 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -311,9 +311,15 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder req := e.client.OrderService.NewCreateOrderRequest(). Market(toLocalSymbol(order.Symbol)). - OrderType(string(orderType)). Side(toLocalSideType(order.Side)) + // convert limit maker to post_only + if order.Type == types.OrderTypeLimitMaker { + req.OrderType(string(maxapi.OrderTypePostOnly)) + } else { + req.OrderType(string(orderType)) + } + if len(order.ClientOrderID) > 0 { req.ClientOrderID(order.ClientOrderID) } else { @@ -331,7 +337,7 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder // set price field for limit orders switch order.Type { - case types.OrderTypeStopLimit, types.OrderTypeLimit: + case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker: if len(order.PriceString) > 0 { req.Price(order.PriceString) } else if order.Market.Symbol != "" { @@ -339,6 +345,7 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder } } + // set stop price field for limit orders switch order.Type { case types.OrderTypeStopLimit, types.OrderTypeStopMarket: From bc620601f6eea28a033512527880b046251b32b5 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 21 Mar 2021 11:13:33 +0800 Subject: [PATCH 5/9] add xmaker config files --- config/xmaker-btcusdt.yaml | 78 +++++++++++++++++++++++++++++++++++++ config/xmaker-ethusdt.yaml | 80 ++++++++++++++++++++++++++++++++++++++ config/xmaker.yaml | 78 +++++++++++++++++++++++++++++++++++++ 3 files changed, 236 insertions(+) create mode 100644 config/xmaker-btcusdt.yaml create mode 100644 config/xmaker-ethusdt.yaml create mode 100644 config/xmaker.yaml diff --git a/config/xmaker-btcusdt.yaml b/config/xmaker-btcusdt.yaml new file mode 100644 index 000000000..daf688ced --- /dev/null +++ b/config/xmaker-btcusdt.yaml @@ -0,0 +1,78 @@ +--- +notifications: + slack: + defaultChannel: "dev-bbgo" + errorChannel: "bbgo-error" + + # if you want to route channel by symbol + symbolChannels: + "^BTC": "btc" + "^ETH": "eth" + + # if you want to route channel by exchange session + sessionChannels: + max: "bbgo-max" + binance: "bbgo-binance" + + # routing rules + routing: + trade: "$symbol" + order: "$silent" + submitOrder: "$silent" + pnL: "bbgo-pnl" + +reportPnL: +- averageCostBySymbols: + - "BTCUSDT" + - "BNBUSDT" + of: binance + when: + - "@daily" + - "@hourly" + +persistence: + json: + directory: var/data + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sessions: + max: + exchange: max + envVarPrefix: max + + binance: + exchange: binance + envVarPrefix: binance + +crossExchangeStrategies: + +- mobydick: + symbol: "BTCUSDT" + sourceExchange: binance + makerExchange: max + updateInterval: 1s + + # disableHedge disables the hedge orders on the source exchange + # disableHedge: true + + hedgeInterval: 10s + + margin: 0.004 + askMargin: 0.004 + bidMargin: 0.004 + + quantity: 0.001 + quantityMultiplier: 2 + + # numLayers means how many order we want to place on each side. 3 means we want 3 bid orders and 3 ask orders + numLayers: 1 + # pips is the fraction numbers between each order. for BTC, 1 pip is 0.1, + # 0.1 pip is 0.01, here we use 10, so we will get 18000.00, 18001.00 and + # 18002.00 + pips: 10 + persistence: + type: redis + diff --git a/config/xmaker-ethusdt.yaml b/config/xmaker-ethusdt.yaml new file mode 100644 index 000000000..fc5e4d6f2 --- /dev/null +++ b/config/xmaker-ethusdt.yaml @@ -0,0 +1,80 @@ +--- +notifications: + slack: + defaultChannel: "dev-bbgo" + errorChannel: "bbgo-error" + + # if you want to route channel by symbol + symbolChannels: + "^BTC": "btc" + "^ETH": "eth" + + # if you want to route channel by exchange session + sessionChannels: + max: "bbgo-max" + binance: "bbgo-binance" + + # routing rules + routing: + trade: "$symbol" + order: "$silent" + submitOrder: "$silent" + pnL: "bbgo-pnl" + +reportPnL: +- averageCostBySymbols: + - "BTCUSDT" + - "BNBUSDT" + of: binance + when: + - "@daily" + - "@hourly" + +persistence: + json: + directory: var/data + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sessions: + max: + exchange: max + envVarPrefix: max + + binance: + exchange: binance + envVarPrefix: binance + +crossExchangeStrategies: + +- mobydick: + symbol: ETHUSDT + sourceExchange: binance + makerExchange: max + updateInterval: 1s + + # disableHedge disables the hedge orders on the source exchange + # disableHedge: true + + hedgeInterval: 10s + + margin: 0.004 + askMargin: 0.004 + bidMargin: 0.004 + + quantity: 0.1 + quantityMultiplier: 2 + + # numLayers means how many order we want to place on each side. 3 means we want 3 bid orders and 3 ask orders + numLayers: 2 + + # pips is the fraction numbers between each order. for BTC, 1 pip is 0.1, + # 0.1 pip is 0.01, here we use 10, so we will get 18000.00, 18001.00 and + # 18002.00 + pips: 10 + + persistence: + type: redis + diff --git a/config/xmaker.yaml b/config/xmaker.yaml new file mode 100644 index 000000000..daf688ced --- /dev/null +++ b/config/xmaker.yaml @@ -0,0 +1,78 @@ +--- +notifications: + slack: + defaultChannel: "dev-bbgo" + errorChannel: "bbgo-error" + + # if you want to route channel by symbol + symbolChannels: + "^BTC": "btc" + "^ETH": "eth" + + # if you want to route channel by exchange session + sessionChannels: + max: "bbgo-max" + binance: "bbgo-binance" + + # routing rules + routing: + trade: "$symbol" + order: "$silent" + submitOrder: "$silent" + pnL: "bbgo-pnl" + +reportPnL: +- averageCostBySymbols: + - "BTCUSDT" + - "BNBUSDT" + of: binance + when: + - "@daily" + - "@hourly" + +persistence: + json: + directory: var/data + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +sessions: + max: + exchange: max + envVarPrefix: max + + binance: + exchange: binance + envVarPrefix: binance + +crossExchangeStrategies: + +- mobydick: + symbol: "BTCUSDT" + sourceExchange: binance + makerExchange: max + updateInterval: 1s + + # disableHedge disables the hedge orders on the source exchange + # disableHedge: true + + hedgeInterval: 10s + + margin: 0.004 + askMargin: 0.004 + bidMargin: 0.004 + + quantity: 0.001 + quantityMultiplier: 2 + + # numLayers means how many order we want to place on each side. 3 means we want 3 bid orders and 3 ask orders + numLayers: 1 + # pips is the fraction numbers between each order. for BTC, 1 pip is 0.1, + # 0.1 pip is 0.01, here we use 10, so we will get 18000.00, 18001.00 and + # 18002.00 + pips: 10 + persistence: + type: redis + From e22a2caadeebf9f1ad600d678f53e7a0a5788663 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 21 Mar 2021 11:21:33 +0800 Subject: [PATCH 6/9] add basic risk control to the xmaker config --- config/xmaker-btcusdt.yaml | 29 ++++++++++++++++++++++++++--- config/xmaker-ethusdt.yaml | 32 +++++++++++++++++++++++--------- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/config/xmaker-btcusdt.yaml b/config/xmaker-btcusdt.yaml index daf688ced..cda539e68 100644 --- a/config/xmaker-btcusdt.yaml +++ b/config/xmaker-btcusdt.yaml @@ -41,16 +41,39 @@ persistence: sessions: max: exchange: max - envVarPrefix: max + envVarPrefix: MAX binance: exchange: binance - envVarPrefix: binance + envVarPrefix: BINANCE + +riskControls: + # This is the session-based risk controller, which let you configure different risk controller by session. + sessionBased: + # "max" is the session name that you want to configure the risk control + max: + # orderExecutor is one of the risk control + orderExecutor: + # symbol-routed order executor + bySymbol: + BTCUSDT: + # basic risk control order executor + basic: + # keep at least X USDT (keep cash) + minQuoteBalance: 100.0 + + # maximum BTC balance (don't buy too much) + maxBaseAssetBalance: 1.0 + + # minimum BTC balance (don't sell too much) + minBaseAssetBalance: 0.01 + + maxOrderAmount: 1000.0 crossExchangeStrategies: - mobydick: - symbol: "BTCUSDT" + symbol: BTCUSDT sourceExchange: binance makerExchange: max updateInterval: 1s diff --git a/config/xmaker-ethusdt.yaml b/config/xmaker-ethusdt.yaml index fc5e4d6f2..77009855d 100644 --- a/config/xmaker-ethusdt.yaml +++ b/config/xmaker-ethusdt.yaml @@ -21,15 +21,6 @@ notifications: submitOrder: "$silent" pnL: "bbgo-pnl" -reportPnL: -- averageCostBySymbols: - - "BTCUSDT" - - "BNBUSDT" - of: binance - when: - - "@daily" - - "@hourly" - persistence: json: directory: var/data @@ -47,6 +38,29 @@ sessions: exchange: binance envVarPrefix: binance +riskControls: + # This is the session-based risk controller, which let you configure different risk controller by session. + sessionBased: + # "max" is the session name that you want to configure the risk control + max: + # orderExecutor is one of the risk control + orderExecutor: + # symbol-routed order executor + bySymbol: + ETHUSDT: + # basic risk control order executor + basic: + # keep at least X USDT (keep cash) + minQuoteBalance: 100.0 + + # maximum ETH balance (don't buy too much) + maxBaseAssetBalance: 10.0 + + # minimum ETH balance (don't sell too much) + minBaseAssetBalance: 0.0 + + maxOrderAmount: 1000.0 + crossExchangeStrategies: - mobydick: From 2a067e5cb4c8ba491a600216ff3b44cb9af6f5e5 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 21 Mar 2021 11:16:15 +0800 Subject: [PATCH 7/9] add more balance check for hedging --- pkg/strategy/xmaker/strategy.go | 111 ++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 50 deletions(-) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 1db940083..4c2b5d0c2 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -120,14 +120,19 @@ func (s *Strategy) updateQuote(ctx context.Context) { log.Infof("quote bid price: %f ask price: %f", bidPrice.Float64(), askPrice.Float64()) + var disableMakerBid = false + var disableMakerAsk = false var submitOrders []types.SubmitOrder - balances := s.makerSession.Account.Balances() + // we load the balances from the account, + // however, while we're generating the orders, + // the balance may have a chance to be deducted by other strategies or manual orders submitted by the user + makerBalances := s.makerSession.Account.Balances() makerQuota := &bbgo.QuotaTransaction{} - if b, ok := balances[s.makerMarket.BaseCurrency]; ok { + if b, ok := makerBalances[s.makerMarket.BaseCurrency]; ok { makerQuota.BaseAsset.Add(b.Available) } - if b, ok := balances[s.makerMarket.QuoteCurrency]; ok { + if b, ok := makerBalances[s.makerMarket.QuoteCurrency]; ok { makerQuota.QuoteAsset.Add(b.Available) } @@ -135,59 +140,69 @@ func (s *Strategy) updateQuote(ctx context.Context) { hedgeQuota := &bbgo.QuotaTransaction{} if b, ok := hedgeBalances[s.sourceMarket.BaseCurrency]; ok { hedgeQuota.BaseAsset.Add(b.Available) + + // if the base asset balance is not enough for selling + if b.Available.Float64() <= s.sourceMarket.MinQuantity { + disableMakerBid = true + } } + if b, ok := hedgeBalances[s.sourceMarket.QuoteCurrency]; ok { hedgeQuota.QuoteAsset.Add(b.Available) + + // if the quote asset balance is not enough for buying + if b.Available.Float64() <= s.sourceMarket.MinNotional { + disableMakerAsk = true + } } - log.Infof("maker quota: %+v", makerQuota) - log.Infof("hedge quota: %+v", hedgeQuota) - for i := 0; i < s.NumLayers; i++ { - // bid orders - if makerQuota.QuoteAsset.Lock(bidQuantity.Mul(bidPrice)) && hedgeQuota.BaseAsset.Lock(bidQuantity) { - // if we bought, then we need to sell the base from the hedge session - submitOrders = append(submitOrders, types.SubmitOrder{ - Symbol: s.Symbol, - Type: types.OrderTypeLimit, - Side: types.SideTypeBuy, - Price: bidPrice.Float64(), - Quantity: bidQuantity.Float64(), - TimeInForce: "GTC", - GroupID: s.groupID, - }) + // for maker bid orders + if !disableMakerBid { + if makerQuota.QuoteAsset.Lock(bidQuantity.Mul(bidPrice)) && hedgeQuota.BaseAsset.Lock(bidQuantity) { + // if we bought, then we need to sell the base from the hedge session + submitOrders = append(submitOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimit, + Side: types.SideTypeBuy, + Price: bidPrice.Float64(), + Quantity: bidQuantity.Float64(), + TimeInForce: "GTC", + GroupID: s.groupID, + }) - makerQuota.Commit() - hedgeQuota.Commit() - } else { - makerQuota.Rollback() - hedgeQuota.Rollback() + makerQuota.Commit() + hedgeQuota.Commit() + } else { + makerQuota.Rollback() + hedgeQuota.Rollback() + } + bidPrice -= fixedpoint.NewFromFloat(s.makerMarket.TickSize * float64(s.Pips)) + bidQuantity.Mul(s.QuantityMultiplier) } - // ask orders - if makerQuota.BaseAsset.Lock(askQuantity) && hedgeQuota.QuoteAsset.Lock(askQuantity.Mul(askPrice)) { - // if we bought, then we need to sell the base from the hedge session - submitOrders = append(submitOrders, types.SubmitOrder{ - Symbol: s.Symbol, - Type: types.OrderTypeLimit, - Side: types.SideTypeSell, - Price: askPrice.Float64(), - Quantity: askQuantity.Float64(), - TimeInForce: "GTC", - GroupID: s.groupID, - }) - makerQuota.Commit() - hedgeQuota.Commit() - } else { - makerQuota.Rollback() - hedgeQuota.Rollback() + // for maker ask orders + if !disableMakerAsk { + if makerQuota.BaseAsset.Lock(askQuantity) && hedgeQuota.QuoteAsset.Lock(askQuantity.Mul(askPrice)) { + // if we bought, then we need to sell the base from the hedge session + submitOrders = append(submitOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimit, + Side: types.SideTypeSell, + Price: askPrice.Float64(), + Quantity: askQuantity.Float64(), + TimeInForce: "GTC", + GroupID: s.groupID, + }) + makerQuota.Commit() + hedgeQuota.Commit() + } else { + makerQuota.Rollback() + hedgeQuota.Rollback() + } + askPrice += fixedpoint.NewFromFloat(s.makerMarket.TickSize * float64(s.Pips)) + askQuantity.Mul(s.QuantityMultiplier) } - - bidPrice -= fixedpoint.NewFromFloat(s.makerMarket.TickSize * float64(s.Pips)) - askPrice += fixedpoint.NewFromFloat(s.makerMarket.TickSize * float64(s.Pips)) - - askQuantity.Mul(s.QuantityMultiplier) - bidQuantity.Mul(s.QuantityMultiplier) } if len(submitOrders) == 0 { @@ -335,7 +350,6 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se s.Quantity = defaultQuantity } - // configure sessions sourceSession, ok := sessions[s.SourceExchange] if !ok { @@ -361,9 +375,6 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se return fmt.Errorf("maker session market %s is not defined", s.Symbol) } - - - // restore state instanceID := fmt.Sprintf("%s-%s-%s", ID, s.Symbol) s.groupID = generateGroupID(instanceID) From 948e555f5144ac9a330726f7e24f7a1a939c6bd3 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 21 Mar 2021 12:17:13 +0800 Subject: [PATCH 8/9] doc: add section Adding New Built-in Strategy --- README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/README.md b/README.md index 822871603..a639825ef 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,76 @@ vim config/buyandhold.yaml bbgo run --config config/buyandhold.yaml ``` +## Adding New Built-in Strategy + +Fork and clone this repository, Create a directory under `pkg/strategy/newstrategy`, +write your strategy at `pkg/strategy/newstrategy/strategy.go`. + +Define a strategy struct: + +```go +package newstrategy + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +type Strategy struct { + Symbol string `json:"symbol"` + Param1 int `json:"param1"` + Param2 int `json:"param2"` + Param3 fixedpoint.Value `json:"param3"` +} +``` + +Register your strategy: + +```go +const ID = "newstrategy" + +const stateKey = "state-v1" + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} +``` + +Implement the strategy methods: + +```go +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"}) +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + // .... + return nil +} +``` + +Edit `pkg/cmd/builtin.go`, and import the package, like this: + +```go +package cmd + +// import built-in strategies +import ( + _ "github.com/c9s/bbgo/pkg/strategy/bollgrid" + _ "github.com/c9s/bbgo/pkg/strategy/buyandhold" + _ "github.com/c9s/bbgo/pkg/strategy/flashcrash" + _ "github.com/c9s/bbgo/pkg/strategy/grid" + _ "github.com/c9s/bbgo/pkg/strategy/mirrormaker" + _ "github.com/c9s/bbgo/pkg/strategy/pricealert" + _ "github.com/c9s/bbgo/pkg/strategy/support" + _ "github.com/c9s/bbgo/pkg/strategy/swing" + _ "github.com/c9s/bbgo/pkg/strategy/trailingstop" + _ "github.com/c9s/bbgo/pkg/strategy/xmaker" + _ "github.com/c9s/bbgo/pkg/strategy/xpuremaker" +) +``` + ## Write your own strategy Create your go package, and initialize the repository with `go mod` and add bbgo as a dependency: From 814a77ea39c6c418fbbfbef1fe260db2cd1bf244 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 21 Mar 2021 12:43:41 +0800 Subject: [PATCH 9/9] xmaker: improve balance checking --- config/xmaker-btcusdt.yaml | 2 +- config/xmaker-ethusdt.yaml | 6 +++--- config/xmaker.yaml | 2 +- pkg/bbgo/trader.go | 4 ++++ pkg/cmd/builtin.go | 1 + pkg/exchange/max/maxapi/order.go | 2 +- pkg/strategy/xmaker/strategy.go | 29 +++++++++++++++++++++++++---- 7 files changed, 36 insertions(+), 10 deletions(-) diff --git a/config/xmaker-btcusdt.yaml b/config/xmaker-btcusdt.yaml index cda539e68..6beb4bd52 100644 --- a/config/xmaker-btcusdt.yaml +++ b/config/xmaker-btcusdt.yaml @@ -72,7 +72,7 @@ riskControls: crossExchangeStrategies: -- mobydick: +- xmaker: symbol: BTCUSDT sourceExchange: binance makerExchange: max diff --git a/config/xmaker-ethusdt.yaml b/config/xmaker-ethusdt.yaml index 77009855d..b7faef1f4 100644 --- a/config/xmaker-ethusdt.yaml +++ b/config/xmaker-ethusdt.yaml @@ -63,11 +63,11 @@ riskControls: crossExchangeStrategies: -- mobydick: +- xmaker: symbol: ETHUSDT sourceExchange: binance makerExchange: max - updateInterval: 1s + updateInterval: 2s # disableHedge disables the hedge orders on the source exchange # disableHedge: true @@ -78,7 +78,7 @@ crossExchangeStrategies: askMargin: 0.004 bidMargin: 0.004 - quantity: 0.1 + quantity: 0.01 quantityMultiplier: 2 # numLayers means how many order we want to place on each side. 3 means we want 3 bid orders and 3 ask orders diff --git a/config/xmaker.yaml b/config/xmaker.yaml index daf688ced..9afcf296e 100644 --- a/config/xmaker.yaml +++ b/config/xmaker.yaml @@ -49,7 +49,7 @@ sessions: crossExchangeStrategies: -- mobydick: +- xmaker: symbol: "BTCUSDT" sourceExchange: binance makerExchange: max diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index 64b1891c4..64be7ca42 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -173,6 +173,8 @@ func (trader *Trader) Subscribe() { for _, strategy := range strategies { if subscriber, ok := strategy.(ExchangeSessionSubscriber); ok { subscriber.Subscribe(session) + } else { + log.Errorf("strategy %s does not implement ExchangeSessionSubscriber", strategy.ID()) } } } @@ -180,6 +182,8 @@ func (trader *Trader) Subscribe() { for _, strategy := range trader.crossExchangeStrategies { if subscriber, ok := strategy.(CrossExchangeSessionSubscriber); ok { subscriber.CrossSubscribe(trader.environment.sessions) + } else { + log.Errorf("strategy %s does not implement CrossExchangeSessionSubscriber", strategy.ID()) } } } diff --git a/pkg/cmd/builtin.go b/pkg/cmd/builtin.go index eadc5eed3..d94609985 100644 --- a/pkg/cmd/builtin.go +++ b/pkg/cmd/builtin.go @@ -11,5 +11,6 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/support" _ "github.com/c9s/bbgo/pkg/strategy/swing" _ "github.com/c9s/bbgo/pkg/strategy/trailingstop" + _ "github.com/c9s/bbgo/pkg/strategy/xmaker" _ "github.com/c9s/bbgo/pkg/strategy/xpuremaker" ) diff --git a/pkg/exchange/max/maxapi/order.go b/pkg/exchange/max/maxapi/order.go index 1c1c0bc2c..429dc1dc1 100644 --- a/pkg/exchange/max/maxapi/order.go +++ b/pkg/exchange/max/maxapi/order.go @@ -501,7 +501,7 @@ func (r *CreateOrderRequest) Do(ctx context.Context) (order *Order, err error) { payload["group_id"] = r.groupID } - req, err := r.client.newAuthenticatedRequest("POST", "v2/orders", &payload) + req, err := r.client.newAuthenticatedRequest("POST", "v2/orders", payload) if err != nil { return order, errors.Wrapf(err, "order create error") } diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 4c2b5d0c2..2b76122e2 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -82,11 +82,16 @@ type Strategy struct { func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { sourceSession, ok := sessions[s.SourceExchange] if !ok { - panic(fmt.Errorf("source exchange %s is not defined", s.SourceExchange)) + panic(fmt.Errorf("source session %s is not defined", s.SourceExchange)) } - log.Infof("subscribing %s from %s", s.Symbol, s.SourceExchange) sourceSession.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{}) + + makerSession, ok := sessions[s.MakerExchange] + if !ok { + panic(fmt.Errorf("maker session %s is not defined", s.MakerExchange)) + } + makerSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"}) } func (s *Strategy) updateQuote(ctx context.Context) { @@ -104,7 +109,7 @@ func (s *Strategy) updateQuote(ctx context.Context) { } if valid, err := sourceBook.IsValid(); !valid { - log.WithError(err).Error("invalid order book: %v", err) + log.WithError(err).Errorf("invalid order book: %v", err) return } @@ -131,9 +136,18 @@ func (s *Strategy) updateQuote(ctx context.Context) { makerQuota := &bbgo.QuotaTransaction{} if b, ok := makerBalances[s.makerMarket.BaseCurrency]; ok { makerQuota.BaseAsset.Add(b.Available) + + if b.Available.Float64() <= s.makerMarket.MinQuantity { + disableMakerAsk = true + } } + if b, ok := makerBalances[s.makerMarket.QuoteCurrency]; ok { makerQuota.QuoteAsset.Add(b.Available) + + if b.Available.Float64() <= s.makerMarket.MinNotional { + disableMakerBid = true + } } hedgeBalances := s.sourceSession.Account.Balances() @@ -141,6 +155,7 @@ func (s *Strategy) updateQuote(ctx context.Context) { if b, ok := hedgeBalances[s.sourceMarket.BaseCurrency]; ok { hedgeQuota.BaseAsset.Add(b.Available) + // to make bid orders, we need enough base asset in the foreign exchange, // if the base asset balance is not enough for selling if b.Available.Float64() <= s.sourceMarket.MinQuantity { disableMakerBid = true @@ -150,12 +165,18 @@ func (s *Strategy) updateQuote(ctx context.Context) { if b, ok := hedgeBalances[s.sourceMarket.QuoteCurrency]; ok { hedgeQuota.QuoteAsset.Add(b.Available) + // to make ask orders, we need enough quote asset in the foreign exchange, // if the quote asset balance is not enough for buying if b.Available.Float64() <= s.sourceMarket.MinNotional { disableMakerAsk = true } } + if disableMakerAsk && disableMakerBid { + log.Warn("maker is disabled due to insufficient balances") + return + } + for i := 0; i < s.NumLayers; i++ { // for maker bid orders if !disableMakerBid { @@ -376,7 +397,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se } // restore state - instanceID := fmt.Sprintf("%s-%s-%s", ID, s.Symbol) + instanceID := fmt.Sprintf("%s-%s", ID, s.Symbol) s.groupID = generateGroupID(instanceID) log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID)