bbgo_origin/pkg/strategy/grid/strategy.go

236 lines
7.0 KiB
Go
Raw Normal View History

package grid
import (
"context"
2020-12-31 05:54:32 +00:00
"fmt"
2020-11-12 06:50:08 +00:00
"sync"
2020-10-29 13:10:13 +00:00
"github.com/sirupsen/logrus"
"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")
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
2020-11-12 06:50:08 +00:00
*bbgo.Graceful
// OrderExecutor is an interface for submitting order.
// This field will be injected automatically since it's a single exchange strategy.
bbgo.OrderExecutor
orderStore *bbgo.OrderStore
// 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
// 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-11-10 11:06:20 +00:00
UpperPrice fixedpoint.Value `json:"upperPrice"`
LowerPrice fixedpoint.Value `json:"lowerPrice"`
// Quantity is the quantity you want to submit for each order.
2020-11-11 09:55:44 +00:00
Quantity float64 `json:"quantity"`
2021-01-06 05:31:17 +00:00
// OrderAmount is used for fixed amount (dynamic quantity) if you don't want to use fixed quantity.
OrderAmount fixedpoint.Value `json:"orderAmount"`
// Long means you want to hold more base asset than the quote asset.
2020-12-31 05:54:32 +00:00
Long bool `json:"long"`
// activeOrders is the locally maintained active order book of the maker orders.
2020-11-05 06:27:22 +00:00
activeOrders *bbgo.LocalActiveOrderBook
position fixedpoint.Value
2020-11-10 11:06:20 +00:00
// any created orders for tracking trades
orders map[uint64]types.Order
}
2020-11-10 11:06:20 +00:00
func (s *Strategy) placeGridOrders(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) {
2020-12-29 10:18:32 +00:00
log.Infof("placing grid orders...")
quoteCurrency := s.Market.QuoteCurrency
balances := session.Account.Balances()
2020-11-10 11:06:20 +00:00
currentPrice, ok := session.LastPrice(s.Symbol)
if !ok {
2020-12-29 10:18:32 +00:00
log.Warn("last price not found, skipping")
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-12-31 05:54:32 +00:00
var bidOrders []types.SubmitOrder
var askOrders []types.SubmitOrder
2020-12-29 10:18:32 +00:00
baseBalance, ok := balances[s.Market.BaseCurrency]
if ok && baseBalance.Available > 0 {
log.Infof("placing sell order from %f ~ %f per grid %f", (currentPriceF + gridSize).Float64(), s.UpperPrice.Float64(), gridSize.Float64())
for price := currentPriceF + gridSize; price <= s.UpperPrice; price += gridSize {
order := types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeLimit,
Market: s.Market,
Quantity: s.Quantity,
Price: price.Float64(),
TimeInForce: "GTC",
}
2020-12-31 05:54:32 +00:00
askOrders = append(askOrders, order)
2020-11-10 11:06:20 +00:00
}
2020-12-29 10:18:32 +00:00
} else {
log.Warnf("base balance is not enough, we can't place ask orders")
2020-12-17 09:54:48 +00:00
}
2020-11-10 11:06:20 +00:00
2020-12-29 10:18:32 +00:00
quoteBalance, ok := balances[quoteCurrency]
if ok && quoteBalance.Available > 0 {
log.Infof("placing buy order from %f ~ %f per grid %f", (currentPriceF - gridSize).Float64(), s.LowerPrice.Float64(), gridSize.Float64())
for price := currentPriceF - gridSize; price >= s.LowerPrice; price -= gridSize {
order := types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeLimit,
Market: s.Market,
Quantity: s.Quantity,
Price: price.Float64(),
TimeInForce: "GTC",
}
2020-12-31 05:54:32 +00:00
bidOrders = append(bidOrders, order)
2020-11-10 11:06:20 +00:00
}
2020-12-29 10:18:32 +00:00
} else {
log.Warnf("quote balance is not enough, we can't place bid orders")
}
2020-12-31 05:54:32 +00:00
createdOrders, err := orderExecutor.SubmitOrders(context.Background(), append(bidOrders, askOrders...)...)
if err != nil {
2020-11-10 06:18:54 +00:00
log.WithError(err).Errorf("can not place orders")
return
}
2020-11-10 11:06:20 +00:00
s.activeOrders.Add(createdOrders...)
}
2020-11-10 11:06:20 +00:00
func (s *Strategy) tradeUpdateHandler(trade types.Trade) {
if trade.Symbol != s.Symbol {
return
}
if s.orderStore.Exists(trade.OrderID) {
2020-11-10 11:06:20 +00:00
log.Infof("received trade update of order %d: %+v", trade.OrderID, trade)
switch trade.Side {
case types.SideTypeBuy:
s.position.AtomicAdd(fixedpoint.NewFromFloat(trade.Quantity))
2020-11-10 11:06:20 +00:00
case types.SideTypeSell:
s.position.AtomicAdd(-fixedpoint.NewFromFloat(trade.Quantity))
2020-11-10 11:06:20 +00:00
}
}
2020-11-10 11:06:20 +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-12-31 06:29:23 +00:00
var quantity = order.Quantity
2020-11-10 11:06:20 +00:00
switch side {
case types.SideTypeSell:
price += s.ProfitSpread.Float64()
case types.SideTypeBuy:
price -= s.ProfitSpread.Float64()
2020-11-05 07:04:56 +00:00
}
2021-01-06 05:31:17 +00:00
if s.OrderAmount > 0 {
quantity = s.OrderAmount.Float64() / price
} else if s.Long {
// long = use the same amount to buy more quantity back
// the original amount
var amount = order.Price * order.Quantity
2020-12-31 05:54:32 +00:00
quantity = amount / price
}
2020-11-10 11:06:20 +00:00
submitOrder := types.SubmitOrder{
Symbol: s.Symbol,
Side: side,
Type: types.OrderTypeLimit,
2020-12-31 05:54:32 +00:00
Quantity: quantity,
2020-11-10 11:06:20 +00:00
Price: price,
TimeInForce: "GTC",
}
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-11-10 06:18:54 +00:00
2020-12-17 08:22:43 +00:00
s.orderStore.Add(createdOrders...)
2020-11-10 11:06:20 +00:00
s.activeOrders.Add(createdOrders...)
}
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"})
}
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-12-31 05:54:32 +00:00
if s.UpperPrice <= s.LowerPrice {
return fmt.Errorf("upper price (%f) should not be less than lower price (%f)", s.UpperPrice.Float64(), s.LowerPrice.Float64())
}
s.orderStore = bbgo.NewOrderStore()
s.orderStore.BindStream(session.Stream)
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.
s.activeOrders = bbgo.NewLocalActiveOrderBook()
s.activeOrders.OnFilled(s.submitReverseOrder)
s.activeOrders.BindStream(session.Stream)
2020-10-29 13:10:13 +00:00
2020-11-12 06:50:08 +00:00
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
2020-11-12 06:59:47 +00:00
defer wg.Done()
2020-11-12 06:50:08 +00:00
log.Infof("canceling active orders...")
if err := session.Exchange.CancelOrders(ctx, s.activeOrders.Orders()...); err != nil {
log.WithError(err).Errorf("cancel order error")
}
})
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
})
return nil
}