2020-10-29 11:47:53 +00:00
|
|
|
package grid
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2020-11-10 11:06:20 +00:00
|
|
|
"sync/atomic"
|
2020-10-29 11:47:53 +00:00
|
|
|
|
2020-10-29 13:10:13 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
|
2020-10-29 11:47:53 +00:00
|
|
|
"github.com/c9s/bbgo/pkg/bbgo"
|
|
|
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
|
|
|
"github.com/c9s/bbgo/pkg/types"
|
|
|
|
)
|
|
|
|
|
2020-10-29 13:10:13 +00:00
|
|
|
var log = logrus.WithField("strategy", "grid")
|
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
var position int64 = 0
|
2020-10-29 11:47:53 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
// 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"`
|
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
// ProfitSpread is the fixed profit spread you want to submit the sell order
|
2020-11-10 08:56:30 +00:00
|
|
|
ProfitSpread fixedpoint.Value `json:"profitSpread"`
|
2020-11-05 07:04:56 +00:00
|
|
|
|
2020-11-02 14:14:01 +00:00
|
|
|
// GridNum is the grid number, how many orders you want to post on the orderbook.
|
2020-10-29 13:10:13 +00:00
|
|
|
GridNum int `json:"gridNumber"`
|
2020-10-29 11:47:53 +00:00
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
UpperPrice fixedpoint.Value `json:"upperPrice"`
|
|
|
|
|
|
|
|
LowerPrice fixedpoint.Value `json:"lowerPrice"`
|
|
|
|
|
2020-11-02 14:14:01 +00:00
|
|
|
// BaseQuantity is the quantity you want to submit for each order.
|
2020-10-29 11:47:53 +00:00
|
|
|
BaseQuantity float64 `json:"baseQuantity"`
|
|
|
|
|
2020-11-02 14:14:01 +00:00
|
|
|
// activeOrders is the locally maintained active order book of the maker orders.
|
2020-11-05 06:27:22 +00:00
|
|
|
activeOrders *bbgo.LocalActiveOrderBook
|
2020-10-29 11:47:53 +00:00
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
// any created orders for tracking trades
|
|
|
|
orders map[uint64]types.Order
|
2020-10-29 11:47:53 +00:00
|
|
|
}
|
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
func (s *Strategy) placeGridOrders(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) {
|
2020-10-29 11:47:53 +00:00
|
|
|
quoteCurrency := s.Market.QuoteCurrency
|
|
|
|
balances := session.Account.Balances()
|
|
|
|
|
|
|
|
balance, ok := balances[quoteCurrency]
|
2020-11-10 06:18:54 +00:00
|
|
|
if !ok || balance.Available <= 0 {
|
2020-10-29 11:47:53 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
currentPrice, ok := session.LastPrice(s.Symbol)
|
|
|
|
if !ok {
|
2020-10-31 12:36:58 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
currentPriceF := fixedpoint.NewFromFloat(currentPrice)
|
|
|
|
priceRange := s.UpperPrice - s.LowerPrice
|
|
|
|
gridSize := priceRange.Div(fixedpoint.NewFromInt(s.GridNum))
|
2020-10-29 11:47:53 +00:00
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
log.Infof("current price: %f", currentPrice)
|
|
|
|
|
|
|
|
var orders []types.SubmitOrder
|
|
|
|
for price := s.LowerPrice; price <= s.UpperPrice; price += gridSize {
|
|
|
|
var side types.SideType
|
|
|
|
if price > currentPriceF {
|
|
|
|
side = types.SideTypeSell
|
|
|
|
} else {
|
|
|
|
side = types.SideTypeBuy
|
|
|
|
}
|
|
|
|
|
|
|
|
order := types.SubmitOrder{
|
2020-10-29 13:10:13 +00:00
|
|
|
Symbol: s.Symbol,
|
2020-11-10 11:06:20 +00:00
|
|
|
Side: side,
|
2020-10-29 13:10:13 +00:00
|
|
|
Type: types.OrderTypeLimit,
|
|
|
|
Market: s.Market,
|
|
|
|
Quantity: s.BaseQuantity,
|
2020-11-10 11:06:20 +00:00
|
|
|
Price: price.Float64(),
|
2020-10-29 13:10:13 +00:00
|
|
|
TimeInForce: "GTC",
|
2020-11-10 11:06:20 +00:00
|
|
|
}
|
|
|
|
log.Infof("submitting order: %s", order.String())
|
|
|
|
orders = append(orders, order)
|
2020-10-29 11:47:53 +00:00
|
|
|
}
|
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
createdOrders, err := orderExecutor.SubmitOrders(context.Background(), orders...)
|
2020-10-29 11:47:53 +00:00
|
|
|
if err != nil {
|
2020-11-10 06:18:54 +00:00
|
|
|
log.WithError(err).Errorf("can not place orders")
|
2020-10-29 11:47:53 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
s.activeOrders.Add(createdOrders...)
|
2020-10-29 11:47:53 +00:00
|
|
|
}
|
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
func (s *Strategy) tradeUpdateHandler(trade types.Trade) {
|
|
|
|
if trade.Symbol != s.Symbol {
|
2020-10-31 12:36:58 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
if _, ok := s.orders[trade.OrderID]; ok {
|
|
|
|
log.Infof("received trade update of order %d: %+v", trade.OrderID, trade)
|
|
|
|
switch trade.Side {
|
|
|
|
case types.SideTypeBuy:
|
|
|
|
atomic.AddInt64(&position, fixedpoint.NewFromFloat(trade.Quantity).Int64())
|
|
|
|
case types.SideTypeSell:
|
|
|
|
atomic.AddInt64(&position, -fixedpoint.NewFromFloat(trade.Quantity).Int64())
|
|
|
|
}
|
2020-10-29 11:47:53 +00:00
|
|
|
}
|
2020-11-10 11:06:20 +00:00
|
|
|
}
|
2020-10-29 11:47:53 +00:00
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
func (s *Strategy) submitReverseOrder(order types.Order) {
|
|
|
|
var side = order.Side.Reverse()
|
|
|
|
var price = order.Price
|
2020-10-29 11:47:53 +00:00
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
switch side {
|
|
|
|
case types.SideTypeSell:
|
|
|
|
price += s.ProfitSpread.Float64()
|
2020-10-29 11:47:53 +00:00
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
case types.SideTypeBuy:
|
|
|
|
price -= s.ProfitSpread.Float64()
|
2020-10-29 13:10:13 +00:00
|
|
|
|
2020-11-05 07:04:56 +00:00
|
|
|
}
|
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
submitOrder := types.SubmitOrder{
|
|
|
|
Symbol: s.Symbol,
|
|
|
|
Side: side,
|
|
|
|
Type: types.OrderTypeLimit,
|
|
|
|
Quantity: order.Quantity,
|
|
|
|
Price: price,
|
|
|
|
TimeInForce: "GTC",
|
2020-10-29 11:47:53 +00:00
|
|
|
}
|
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
log.Infof("submitting reverse order: %s against %s", submitOrder.String(), order.String())
|
2020-11-05 07:04:56 +00:00
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
createdOrders, err := s.OrderExecutor.SubmitOrders(context.Background(), submitOrder)
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Errorf("can not place orders")
|
|
|
|
return
|
2020-10-29 11:47:53 +00:00
|
|
|
}
|
2020-11-10 06:18:54 +00:00
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
s.activeOrders.Add(createdOrders...)
|
2020-10-29 11:47:53 +00:00
|
|
|
}
|
|
|
|
|
2020-11-02 14:14:01 +00:00
|
|
|
func (s *Strategy) orderUpdateHandler(order types.Order) {
|
|
|
|
if order.Symbol != s.Symbol {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
log.Infof("order update: %s", order.String())
|
2020-11-05 06:27:22 +00:00
|
|
|
|
2020-11-02 14:14:01 +00:00
|
|
|
switch order.Status {
|
|
|
|
case types.OrderStatusFilled:
|
2020-11-07 07:07:06 +00:00
|
|
|
s.activeOrders.Delete(order)
|
2020-11-10 11:06:20 +00:00
|
|
|
s.submitReverseOrder(order)
|
2020-11-07 07:07:06 +00:00
|
|
|
|
|
|
|
case types.OrderStatusPartiallyFilled, types.OrderStatusNew:
|
|
|
|
s.activeOrders.Update(order)
|
2020-11-02 14:14:01 +00:00
|
|
|
|
|
|
|
case types.OrderStatusCanceled, types.OrderStatusRejected:
|
|
|
|
log.Infof("order status %s, removing %d from the active order pool...", order.Status, order.OrderID)
|
|
|
|
s.activeOrders.Delete(order)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
|
|
|
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"})
|
|
|
|
}
|
|
|
|
|
2020-10-29 11:47:53 +00:00
|
|
|
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
|
|
|
if s.GridNum == 0 {
|
2020-11-10 11:06:20 +00:00
|
|
|
s.GridNum = 10
|
2020-10-29 11:47:53 +00:00
|
|
|
}
|
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
s.orders = make(map[uint64]types.Order)
|
2020-10-29 11:47:53 +00:00
|
|
|
|
2020-10-29 13:10:13 +00:00
|
|
|
// we don't persist orders so that we can not clear the previous orders for now. just need time to support this.
|
2020-11-02 14:14:01 +00:00
|
|
|
s.activeOrders = bbgo.NewLocalActiveOrderBook()
|
2020-10-29 13:10:13 +00:00
|
|
|
|
2020-11-02 14:14:01 +00:00
|
|
|
session.Stream.OnOrderUpdate(s.orderUpdateHandler)
|
2020-11-10 11:06:20 +00:00
|
|
|
session.Stream.OnTradeUpdate(s.tradeUpdateHandler)
|
|
|
|
session.Stream.OnConnect(func() {
|
|
|
|
s.placeGridOrders(orderExecutor, session)
|
2020-11-05 07:04:56 +00:00
|
|
|
})
|
|
|
|
|
2020-10-29 11:47:53 +00:00
|
|
|
return nil
|
|
|
|
}
|