2020-10-29 11:47:53 +00:00
|
|
|
package grid
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2020-12-31 05:54:32 +00:00
|
|
|
"fmt"
|
2021-02-07 03:37:24 +00:00
|
|
|
"hash/fnv"
|
2020-11-12 06:50:08 +00:00
|
|
|
"sync"
|
2020-10-29 11:47:53 +00:00
|
|
|
|
2021-03-16 10:39:18 +00:00
|
|
|
"github.com/pkg/errors"
|
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"
|
2021-02-16 08:30:01 +00:00
|
|
|
"github.com/c9s/bbgo/pkg/service"
|
2020-10-29 11:47:53 +00:00
|
|
|
"github.com/c9s/bbgo/pkg/types"
|
|
|
|
)
|
|
|
|
|
2021-02-03 01:08:05 +00:00
|
|
|
const ID = "grid"
|
|
|
|
|
|
|
|
var log = logrus.WithField("strategy", ID)
|
2020-10-29 13:10:13 +00:00
|
|
|
|
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.
|
2021-02-03 01:08:05 +00:00
|
|
|
bbgo.RegisterStrategy(ID, &Strategy{})
|
2020-10-29 11:47:53 +00:00
|
|
|
}
|
|
|
|
|
2021-03-17 17:14:56 +00:00
|
|
|
// State is the grid snapshot
|
|
|
|
type State struct {
|
2021-03-16 10:39:18 +00:00
|
|
|
Orders []types.SubmitOrder `json:"orders,omitempty"`
|
|
|
|
FilledBuyGrids map[fixedpoint.Value]struct{} `json:"filledBuyGrids"`
|
|
|
|
FilledSellGrids map[fixedpoint.Value]struct{} `json:"filledSellGrids"`
|
2021-03-17 16:46:25 +00:00
|
|
|
Position *bbgo.Position `json:"position,omitempty"`
|
2021-03-18 09:20:21 +00:00
|
|
|
|
|
|
|
ArbitrageProfit fixedpoint.Value `json:"arbitrageProfit"`
|
|
|
|
|
|
|
|
// any created orders for tracking trades
|
|
|
|
// [source Order ID] -> arbitrage order
|
|
|
|
ArbitrageOrders map[uint64]types.Order `json:"arbitrageOrders"`
|
2021-03-16 10:39:18 +00:00
|
|
|
}
|
|
|
|
|
2020-10-29 11:47:53 +00:00
|
|
|
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.
|
2021-02-02 18:26:41 +00:00
|
|
|
*bbgo.Notifiability `json:"-" yaml:"-"`
|
2020-10-29 11:47:53 +00:00
|
|
|
|
2021-02-02 18:26:41 +00:00
|
|
|
*bbgo.Graceful `json:"-" yaml:"-"`
|
2020-11-12 06:50:08 +00:00
|
|
|
|
2021-03-16 10:39:18 +00:00
|
|
|
*bbgo.Persistence
|
|
|
|
|
2020-10-29 11:47:53 +00:00
|
|
|
// OrderExecutor is an interface for submitting order.
|
|
|
|
// This field will be injected automatically since it's a single exchange strategy.
|
2021-02-02 18:26:41 +00:00
|
|
|
bbgo.OrderExecutor `json:"-" yaml:"-"`
|
2020-12-17 07:52:53 +00:00
|
|
|
|
2020-10-29 11:47:53 +00:00
|
|
|
// 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.
|
2021-02-02 18:26:41 +00:00
|
|
|
types.Market `json:"-" yaml:"-"`
|
2020-10-29 11:47:53 +00:00
|
|
|
|
2021-02-16 08:30:01 +00:00
|
|
|
TradeService *service.TradeService `json:"-" yaml:"-"`
|
|
|
|
|
2020-10-29 11:47:53 +00:00
|
|
|
// These fields will be filled from the config file (it translates YAML to JSON)
|
2021-02-02 18:26:41 +00:00
|
|
|
Symbol string `json:"symbol" yaml:"symbol"`
|
2020-10-29 11:47:53 +00:00
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
// ProfitSpread is the fixed profit spread you want to submit the sell order
|
2021-02-02 18:26:41 +00:00
|
|
|
ProfitSpread fixedpoint.Value `json:"profitSpread" yaml:"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.
|
2021-02-02 18:26:41 +00:00
|
|
|
GridNum int `json:"gridNumber" yaml:"gridNumber"`
|
2020-10-29 11:47:53 +00:00
|
|
|
|
2021-02-02 18:26:41 +00:00
|
|
|
UpperPrice fixedpoint.Value `json:"upperPrice" yaml:"upperPrice"`
|
2020-11-10 11:06:20 +00:00
|
|
|
|
2021-02-02 18:26:41 +00:00
|
|
|
LowerPrice fixedpoint.Value `json:"lowerPrice" yaml:"lowerPrice"`
|
2020-11-10 11:06:20 +00:00
|
|
|
|
2020-11-11 09:55:16 +00:00
|
|
|
// Quantity is the quantity you want to submit for each order.
|
2021-03-15 18:18:17 +00:00
|
|
|
Quantity fixedpoint.Value `json:"quantity,omitempty"`
|
|
|
|
|
|
|
|
// ScaleQuantity helps user to define the quantity by price scale or volume scale
|
2021-02-28 06:51:24 +00:00
|
|
|
ScaleQuantity *bbgo.PriceVolumeScale `json:"scaleQuantity,omitempty"`
|
2020-10-29 11:47:53 +00:00
|
|
|
|
2021-02-02 18:26:41 +00:00
|
|
|
// FixedAmount is used for fixed amount (dynamic quantity) if you don't want to use fixed quantity.
|
|
|
|
FixedAmount fixedpoint.Value `json:"amount,omitempty" yaml:"amount"`
|
2021-01-06 05:31:17 +00:00
|
|
|
|
2021-03-15 18:18:17 +00:00
|
|
|
// Side is the initial maker orders side. defaults to "both"
|
|
|
|
Side types.SideType `json:"side" yaml:"side"`
|
|
|
|
|
|
|
|
// CatchUp let the maker grid catch up with the price change.
|
|
|
|
CatchUp bool `json:"catchUp" yaml:"catchUp"`
|
|
|
|
|
2021-01-06 05:31:17 +00:00
|
|
|
// Long means you want to hold more base asset than the quote asset.
|
2021-02-02 18:26:41 +00:00
|
|
|
Long bool `json:"long,omitempty" yaml:"long,omitempty"`
|
|
|
|
|
2021-03-17 17:14:56 +00:00
|
|
|
state *State
|
2021-03-15 18:18:17 +00:00
|
|
|
|
|
|
|
// orderStore is used to store all the created orders, so that we can filter the trades.
|
2021-02-02 18:26:41 +00:00
|
|
|
orderStore *bbgo.OrderStore
|
2020-12-31 05:54:32 +00:00
|
|
|
|
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
|
|
|
|
2021-03-15 18:18:17 +00:00
|
|
|
// groupID is the group ID used for the strategy instance for canceling orders
|
2021-02-07 03:37:24 +00:00
|
|
|
groupID int64
|
2020-10-29 11:47:53 +00:00
|
|
|
}
|
|
|
|
|
2021-02-03 01:08:05 +00:00
|
|
|
func (s *Strategy) ID() string {
|
|
|
|
return ID
|
|
|
|
}
|
|
|
|
|
2021-02-07 03:37:24 +00:00
|
|
|
func (s *Strategy) generateGridSellOrders(session *bbgo.ExchangeSession) ([]types.SubmitOrder, error) {
|
|
|
|
currentPriceFloat, ok := session.LastPrice(s.Symbol)
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("%s last price not found, skipping", s.Symbol)
|
|
|
|
}
|
|
|
|
|
|
|
|
currentPrice := fixedpoint.NewFromFloat(currentPriceFloat)
|
2021-03-17 16:46:25 +00:00
|
|
|
if currentPrice > s.UpperPrice {
|
|
|
|
return nil, fmt.Errorf("current price %f is higher than upper price %f", currentPrice.Float64(), s.UpperPrice.Float64())
|
2021-02-07 03:37:24 +00:00
|
|
|
}
|
|
|
|
|
2021-03-17 16:46:25 +00:00
|
|
|
priceRange := s.UpperPrice - s.LowerPrice
|
2021-02-07 03:37:24 +00:00
|
|
|
numGrids := fixedpoint.NewFromInt(s.GridNum)
|
|
|
|
gridSpread := priceRange.Div(numGrids)
|
2021-03-17 16:46:25 +00:00
|
|
|
|
|
|
|
// find the nearest grid price from the current price
|
|
|
|
startPrice := fixedpoint.Max(
|
|
|
|
s.LowerPrice,
|
|
|
|
s.UpperPrice-(s.UpperPrice-currentPrice).Div(gridSpread).Floor().Mul(gridSpread))
|
2021-02-07 03:37:24 +00:00
|
|
|
|
|
|
|
if startPrice > s.UpperPrice {
|
|
|
|
return nil, fmt.Errorf("current price %f exceeded the upper price boundary %f",
|
|
|
|
currentPrice.Float64(),
|
|
|
|
s.UpperPrice.Float64())
|
|
|
|
}
|
2020-12-29 10:18:32 +00:00
|
|
|
|
2020-10-29 11:47:53 +00:00
|
|
|
balances := session.Account.Balances()
|
2021-02-07 03:37:24 +00:00
|
|
|
baseBalance, ok := balances[s.Market.BaseCurrency]
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("base balance %s not found", s.Market.BaseCurrency)
|
|
|
|
}
|
2020-10-29 11:47:53 +00:00
|
|
|
|
2021-02-07 03:37:24 +00:00
|
|
|
if baseBalance.Available == 0 {
|
|
|
|
return nil, fmt.Errorf("base balance %s is zero: %+v", s.Market.BaseCurrency, baseBalance)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Infof("placing grid sell orders from %f ~ %f, grid spread %f",
|
2021-02-08 05:21:22 +00:00
|
|
|
startPrice.Float64(),
|
2021-02-07 03:37:24 +00:00
|
|
|
s.UpperPrice.Float64(),
|
|
|
|
gridSpread.Float64())
|
|
|
|
|
|
|
|
var orders []types.SubmitOrder
|
2021-03-03 06:55:11 +00:00
|
|
|
for price := startPrice; price <= s.UpperPrice; price += gridSpread {
|
2021-02-28 03:57:25 +00:00
|
|
|
var quantity fixedpoint.Value
|
|
|
|
if s.Quantity > 0 {
|
|
|
|
quantity = s.Quantity
|
|
|
|
} else if s.ScaleQuantity != nil {
|
|
|
|
qf, err := s.ScaleQuantity.Scale(price.Float64(), 0)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
quantity = fixedpoint.NewFromFloat(qf)
|
|
|
|
} else if s.FixedAmount > 0 {
|
2021-02-07 03:37:24 +00:00
|
|
|
quantity = s.FixedAmount.Div(price)
|
|
|
|
}
|
|
|
|
|
|
|
|
// quoteQuantity := price.Mul(quantity)
|
|
|
|
if baseBalance.Available < quantity {
|
2021-02-28 03:57:25 +00:00
|
|
|
return orders, fmt.Errorf("base balance %s %f is not enough, stop generating sell orders",
|
|
|
|
baseBalance.Currency,
|
|
|
|
baseBalance.Available.Float64())
|
2021-02-07 03:37:24 +00:00
|
|
|
}
|
|
|
|
|
2021-03-17 17:14:56 +00:00
|
|
|
if _, filled := s.state.FilledSellGrids[price]; filled {
|
2021-03-15 18:21:46 +00:00
|
|
|
log.Debugf("sell grid at price %f is already filled, skipping", price.Float64())
|
2021-03-15 18:18:17 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2021-02-07 03:37:24 +00:00
|
|
|
orders = append(orders, types.SubmitOrder{
|
|
|
|
Symbol: s.Symbol,
|
|
|
|
Side: types.SideTypeSell,
|
|
|
|
Type: types.OrderTypeLimit,
|
|
|
|
Market: s.Market,
|
|
|
|
Quantity: quantity.Float64(),
|
|
|
|
Price: price.Float64(),
|
|
|
|
TimeInForce: "GTC",
|
|
|
|
GroupID: s.groupID,
|
|
|
|
})
|
|
|
|
baseBalance.Available -= quantity
|
2021-03-15 18:18:17 +00:00
|
|
|
|
2021-03-17 17:14:56 +00:00
|
|
|
s.state.FilledSellGrids[price] = struct{}{}
|
2021-02-07 03:37:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return orders, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Strategy) generateGridBuyOrders(session *bbgo.ExchangeSession) ([]types.SubmitOrder, error) {
|
2021-03-15 18:18:17 +00:00
|
|
|
// session.Exchange.QueryTicker()
|
2021-02-07 03:37:24 +00:00
|
|
|
currentPriceFloat, ok := session.LastPrice(s.Symbol)
|
2020-11-10 11:06:20 +00:00
|
|
|
if !ok {
|
2021-02-07 03:37:24 +00:00
|
|
|
return nil, fmt.Errorf("%s last price not found, skipping", s.Symbol)
|
2020-10-31 12:36:58 +00:00
|
|
|
}
|
|
|
|
|
2021-02-07 03:37:24 +00:00
|
|
|
currentPrice := fixedpoint.NewFromFloat(currentPriceFloat)
|
2021-03-17 16:46:25 +00:00
|
|
|
if currentPrice < s.LowerPrice {
|
|
|
|
return nil, fmt.Errorf("current price %f is lower than the lower price %f", currentPrice.Float64(), s.LowerPrice.Float64())
|
2021-02-07 03:37:24 +00:00
|
|
|
}
|
2020-10-29 11:47:53 +00:00
|
|
|
|
2021-03-17 16:46:25 +00:00
|
|
|
priceRange := s.UpperPrice - s.LowerPrice
|
2021-02-07 03:37:24 +00:00
|
|
|
numGrids := fixedpoint.NewFromInt(s.GridNum)
|
|
|
|
gridSpread := priceRange.Div(numGrids)
|
2021-03-17 16:46:25 +00:00
|
|
|
|
|
|
|
// Find the nearest grid price for placing buy orders:
|
|
|
|
// buyRange = currentPrice - lowerPrice
|
|
|
|
// numOfBuyGrids = Floor(buyRange / gridSpread)
|
|
|
|
// startPrice = lowerPrice + numOfBuyGrids * gridSpread
|
|
|
|
// priceOfBuyOrder1 = startPrice
|
|
|
|
// priceOfBuyOrder2 = startPrice - gridSpread
|
|
|
|
// priceOfBuyOrder3 = startPrice - gridSpread * 2
|
|
|
|
startPrice := fixedpoint.Min(
|
|
|
|
s.UpperPrice,
|
|
|
|
s.LowerPrice+(currentPrice-s.LowerPrice).Div(gridSpread).Floor().Mul(gridSpread))
|
2020-12-29 10:18:32 +00:00
|
|
|
|
2021-02-07 03:37:24 +00:00
|
|
|
if startPrice < s.LowerPrice {
|
|
|
|
return nil, fmt.Errorf("current price %f exceeded the lower price boundary %f",
|
|
|
|
currentPrice.Float64(),
|
|
|
|
s.UpperPrice.Float64())
|
|
|
|
}
|
|
|
|
|
|
|
|
balances := session.Account.Balances()
|
|
|
|
balance, ok := balances[s.Market.QuoteCurrency]
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("quote balance %s not found", s.Market.QuoteCurrency)
|
|
|
|
}
|
|
|
|
|
|
|
|
if balance.Available == 0 {
|
|
|
|
return nil, fmt.Errorf("quote balance %s is zero: %+v", s.Market.QuoteCurrency, balance)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Infof("placing grid buy orders from %f to %f, grid spread %f",
|
2021-03-17 17:14:56 +00:00
|
|
|
startPrice.Float64(),
|
2021-02-07 03:37:24 +00:00
|
|
|
s.LowerPrice.Float64(),
|
|
|
|
gridSpread.Float64())
|
|
|
|
|
|
|
|
var orders []types.SubmitOrder
|
2021-03-03 06:55:11 +00:00
|
|
|
for price := startPrice; s.LowerPrice <= price; price -= gridSpread {
|
2021-02-28 03:57:25 +00:00
|
|
|
var quantity fixedpoint.Value
|
|
|
|
if s.Quantity > 0 {
|
|
|
|
quantity = s.Quantity
|
|
|
|
} else if s.ScaleQuantity != nil {
|
|
|
|
qf, err := s.ScaleQuantity.Scale(price.Float64(), 0)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
quantity = fixedpoint.NewFromFloat(qf)
|
|
|
|
} else if s.FixedAmount > 0 {
|
2021-02-07 03:37:24 +00:00
|
|
|
quantity = s.FixedAmount.Div(price)
|
|
|
|
}
|
|
|
|
|
|
|
|
quoteQuantity := price.Mul(quantity)
|
|
|
|
if balance.Available < quoteQuantity {
|
2021-02-28 03:57:25 +00:00
|
|
|
return orders, fmt.Errorf("quote balance %s %f is not enough for %f, stop generating buy orders",
|
|
|
|
balance.Currency,
|
2021-02-07 03:37:24 +00:00
|
|
|
balance.Available.Float64(),
|
|
|
|
quoteQuantity.Float64())
|
2020-11-10 11:06:20 +00:00
|
|
|
}
|
2021-02-07 03:37:24 +00:00
|
|
|
|
2021-03-17 17:14:56 +00:00
|
|
|
if _, filled := s.state.FilledBuyGrids[price]; filled {
|
2021-03-15 18:21:46 +00:00
|
|
|
log.Debugf("buy grid at price %f is already filled, skipping", price.Float64())
|
2021-03-15 18:18:17 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2021-02-07 03:37:24 +00:00
|
|
|
orders = append(orders, types.SubmitOrder{
|
|
|
|
Symbol: s.Symbol,
|
|
|
|
Side: types.SideTypeBuy,
|
|
|
|
Type: types.OrderTypeLimit,
|
|
|
|
Market: s.Market,
|
|
|
|
Quantity: quantity.Float64(),
|
|
|
|
Price: price.Float64(),
|
|
|
|
TimeInForce: "GTC",
|
|
|
|
GroupID: s.groupID,
|
|
|
|
})
|
|
|
|
balance.Available -= quoteQuantity
|
2021-03-15 18:18:17 +00:00
|
|
|
|
2021-03-17 17:14:56 +00:00
|
|
|
s.state.FilledBuyGrids[price] = struct{}{}
|
2021-02-07 03:37:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return orders, nil
|
|
|
|
}
|
|
|
|
|
2021-03-15 18:18:17 +00:00
|
|
|
func (s *Strategy) placeGridSellOrders(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
|
|
|
orderForms, err := s.generateGridSellOrders(session)
|
2021-02-07 03:37:24 +00:00
|
|
|
if err != nil {
|
2021-03-15 18:18:17 +00:00
|
|
|
return err
|
2021-02-07 03:37:24 +00:00
|
|
|
}
|
2021-03-15 18:18:17 +00:00
|
|
|
|
|
|
|
if len(orderForms) > 0 {
|
|
|
|
createdOrders, err := orderExecutor.SubmitOrders(context.Background(), orderForms...)
|
2021-02-07 03:37:24 +00:00
|
|
|
if err != nil {
|
2021-03-15 18:18:17 +00:00
|
|
|
return err
|
2020-11-10 11:06:20 +00:00
|
|
|
}
|
2021-03-15 18:18:17 +00:00
|
|
|
|
|
|
|
s.activeOrders.Add(createdOrders...)
|
2020-10-29 11:47:53 +00:00
|
|
|
}
|
|
|
|
|
2021-03-15 18:18:17 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Strategy) placeGridBuyOrders(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
|
|
|
orderForms, err := s.generateGridBuyOrders(session)
|
2020-10-29 11:47:53 +00:00
|
|
|
if err != nil {
|
2021-03-15 18:18:17 +00:00
|
|
|
return err
|
2020-10-29 11:47:53 +00:00
|
|
|
}
|
|
|
|
|
2021-03-15 18:18:17 +00:00
|
|
|
if len(orderForms) > 0 {
|
|
|
|
createdOrders, err := orderExecutor.SubmitOrders(context.Background(), orderForms...)
|
2021-02-07 03:37:24 +00:00
|
|
|
if err != nil {
|
2021-03-15 18:18:17 +00:00
|
|
|
return err
|
2021-02-07 03:37:24 +00:00
|
|
|
} else {
|
2021-03-15 18:18:17 +00:00
|
|
|
s.activeOrders.Add(createdOrders...)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Strategy) placeGridOrders(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) {
|
2021-03-17 17:14:56 +00:00
|
|
|
log.Infof("placing grid orders on side %s...", s.Side)
|
2021-03-15 18:18:17 +00:00
|
|
|
|
|
|
|
switch s.Side {
|
|
|
|
|
|
|
|
case types.SideTypeBuy:
|
|
|
|
if err := s.placeGridBuyOrders(orderExecutor, session); err != nil {
|
|
|
|
log.Warn(err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
case types.SideTypeSell:
|
|
|
|
if err := s.placeGridSellOrders(orderExecutor, session); err != nil {
|
|
|
|
log.Warn(err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
case types.SideTypeBoth:
|
|
|
|
if err := s.placeGridSellOrders(orderExecutor, session); err != nil {
|
|
|
|
log.Warn(err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := s.placeGridBuyOrders(orderExecutor, session); err != nil {
|
|
|
|
log.Warn(err.Error())
|
2021-02-07 03:37:24 +00:00
|
|
|
}
|
|
|
|
}
|
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-12-17 07:52:53 +00:00
|
|
|
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)
|
2021-03-15 18:18:17 +00:00
|
|
|
|
2021-03-16 02:58:51 +00:00
|
|
|
if s.TradeService != nil {
|
2021-03-16 06:07:47 +00:00
|
|
|
if err := s.TradeService.Mark(context.Background(), trade.ID, ID); err != nil {
|
|
|
|
log.WithError(err).Error("trade mark error")
|
2021-03-16 02:58:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-15 18:18:17 +00:00
|
|
|
if trade.Side == types.SideTypeSelf {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-03-17 17:14:56 +00:00
|
|
|
profit, madeProfit := s.state.Position.AddTrade(trade)
|
2021-03-15 18:18:17 +00:00
|
|
|
if madeProfit {
|
2021-03-18 09:20:21 +00:00
|
|
|
s.Notify("average cost profit: %f", profit.Float64())
|
2020-11-10 11:06:20 +00:00
|
|
|
}
|
2020-10-29 11:47:53 +00:00
|
|
|
}
|
2020-11-10 11:06:20 +00:00
|
|
|
}
|
2020-10-29 11:47:53 +00:00
|
|
|
|
2021-03-18 09:20:21 +00:00
|
|
|
func (s *Strategy) handleFilledOrder(filledOrder types.Order) {
|
|
|
|
// generate arbitrage order
|
|
|
|
var side = filledOrder.Side.Reverse()
|
|
|
|
var price = filledOrder.Price
|
|
|
|
var quantity = filledOrder.Quantity
|
2020-12-31 06:29:23 +00:00
|
|
|
|
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-02-02 18:26:41 +00:00
|
|
|
if s.FixedAmount > 0 {
|
|
|
|
quantity = s.FixedAmount.Float64() / price
|
2021-01-06 05:31:17 +00:00
|
|
|
} else if s.Long {
|
|
|
|
// long = use the same amount to buy more quantity back
|
|
|
|
// the original amount
|
2021-03-18 09:20:21 +00:00
|
|
|
var amount = filledOrder.Price * filledOrder.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",
|
2021-02-07 03:37:24 +00:00
|
|
|
GroupID: s.groupID,
|
2020-10-29 11:47:53 +00:00
|
|
|
}
|
|
|
|
|
2021-03-18 09:20:21 +00:00
|
|
|
log.Infof("submitting arbitrage order: %s against filled order %s", submitOrder.String(), filledOrder.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
|
|
|
|
2021-03-18 09:20:21 +00:00
|
|
|
// create one-way link from the newly created orders
|
|
|
|
for _, o := range createdOrders {
|
|
|
|
s.state.ArbitrageOrders[o.OrderID] = filledOrder
|
|
|
|
}
|
|
|
|
|
2020-12-17 08:22:43 +00:00
|
|
|
s.orderStore.Add(createdOrders...)
|
2020-11-10 11:06:20 +00:00
|
|
|
s.activeOrders.Add(createdOrders...)
|
2021-03-18 09:20:21 +00:00
|
|
|
|
|
|
|
// calculate arbitrage profit
|
2021-03-18 09:48:05 +00:00
|
|
|
// TODO: apply fee rate here
|
2021-03-18 09:20:21 +00:00
|
|
|
if s.Long {
|
|
|
|
switch filledOrder.Side {
|
|
|
|
case types.SideTypeSell:
|
|
|
|
if buyOrder, ok := s.state.ArbitrageOrders[filledOrder.OrderID]; ok {
|
|
|
|
// use base asset quantity here
|
|
|
|
baseProfit := buyOrder.Quantity - filledOrder.Quantity
|
|
|
|
s.state.ArbitrageProfit += fixedpoint.NewFromFloat(baseProfit)
|
|
|
|
s.Notify("grid arbitrage profit %f %s", baseProfit, s.Market.BaseCurrency)
|
|
|
|
}
|
|
|
|
|
|
|
|
case types.SideTypeBuy:
|
|
|
|
if sellOrder, ok := s.state.ArbitrageOrders[filledOrder.OrderID]; ok {
|
|
|
|
// use base asset quantity here
|
|
|
|
baseProfit := filledOrder.Quantity - sellOrder.Quantity
|
|
|
|
s.state.ArbitrageProfit += fixedpoint.NewFromFloat(baseProfit)
|
|
|
|
s.Notify("grid arbitrage profit %f %s", baseProfit, s.Market.BaseCurrency)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if !s.Long && s.Quantity > 0 {
|
|
|
|
switch filledOrder.Side {
|
|
|
|
case types.SideTypeSell:
|
|
|
|
if buyOrder, ok := s.state.ArbitrageOrders[filledOrder.OrderID]; ok {
|
|
|
|
// use base asset quantity here
|
|
|
|
quoteProfit := (filledOrder.Quantity * filledOrder.Price) - (buyOrder.Quantity * buyOrder.Price)
|
|
|
|
s.state.ArbitrageProfit += fixedpoint.NewFromFloat(quoteProfit)
|
|
|
|
s.Notify("grid arbitrage profit %f %s", quoteProfit, s.Market.BaseCurrency)
|
|
|
|
}
|
|
|
|
case types.SideTypeBuy:
|
|
|
|
if sellOrder, ok := s.state.ArbitrageOrders[filledOrder.OrderID]; ok {
|
|
|
|
// use base asset quantity here
|
|
|
|
quoteProfit := (sellOrder.Quantity * sellOrder.Price) - (filledOrder.Quantity * filledOrder.Price)
|
|
|
|
s.state.ArbitrageProfit += fixedpoint.NewFromFloat(quoteProfit)
|
|
|
|
s.Notify("grid arbitrage profit %f %s", quoteProfit, s.Market.QuoteCurrency)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-10-29 11:47:53 +00:00
|
|
|
}
|
|
|
|
|
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 {
|
2021-02-16 08:30:01 +00:00
|
|
|
// do some basic validation
|
2020-10-29 11:47:53 +00:00
|
|
|
if s.GridNum == 0 {
|
2020-11-10 11:06:20 +00:00
|
|
|
s.GridNum = 10
|
2020-10-29 11:47:53 +00:00
|
|
|
}
|
|
|
|
|
2021-03-15 18:18:17 +00:00
|
|
|
if s.Side == "" {
|
|
|
|
s.Side = types.SideTypeBoth
|
|
|
|
}
|
|
|
|
|
2021-03-17 16:46:25 +00:00
|
|
|
if s.UpperPrice == 0 {
|
|
|
|
return errors.New("upperPrice can not be zero, you forgot to set?")
|
|
|
|
}
|
|
|
|
|
|
|
|
if s.LowerPrice == 0 {
|
|
|
|
return errors.New("lowerPrice can not be zero, you forgot to set?")
|
|
|
|
}
|
|
|
|
|
2020-12-31 05:54:32 +00:00
|
|
|
if s.UpperPrice <= s.LowerPrice {
|
2021-03-17 16:46:25 +00:00
|
|
|
return fmt.Errorf("upperPrice (%f) should not be less than or equal to lowerPrice (%f)", s.UpperPrice.Float64(), s.LowerPrice.Float64())
|
2020-12-31 05:54:32 +00:00
|
|
|
}
|
|
|
|
|
2021-03-17 17:14:56 +00:00
|
|
|
var stateLoaded = false
|
2021-03-16 10:39:18 +00:00
|
|
|
if s.Persistence != nil {
|
2021-03-17 17:14:56 +00:00
|
|
|
var state State
|
|
|
|
if err := s.Persistence.Load(&state, ID, s.Symbol, "state"); err != nil {
|
2021-03-16 10:39:18 +00:00
|
|
|
if err != service.ErrPersistenceNotExists {
|
2021-03-17 17:14:56 +00:00
|
|
|
return errors.Wrapf(err, "state load error")
|
2021-03-16 10:39:18 +00:00
|
|
|
}
|
|
|
|
} else {
|
2021-03-17 17:14:56 +00:00
|
|
|
log.Infof("grid state loaded")
|
|
|
|
stateLoaded = true
|
|
|
|
s.state = &state
|
2021-03-16 10:39:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-17 17:14:56 +00:00
|
|
|
if s.state == nil {
|
|
|
|
position, ok := session.Position(s.Symbol)
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("position not found")
|
|
|
|
}
|
2021-03-15 18:18:17 +00:00
|
|
|
|
2021-03-17 17:14:56 +00:00
|
|
|
s.state = &State{
|
|
|
|
FilledBuyGrids: make(map[fixedpoint.Value]struct{}),
|
|
|
|
FilledSellGrids: make(map[fixedpoint.Value]struct{}),
|
2021-03-18 09:20:21 +00:00
|
|
|
ArbitrageOrders: make(map[uint64]types.Order),
|
2021-03-17 17:14:56 +00:00
|
|
|
Position: position,
|
|
|
|
}
|
2021-02-16 08:30:01 +00:00
|
|
|
}
|
|
|
|
|
2021-03-18 09:20:21 +00:00
|
|
|
if s.state.ArbitrageOrders == nil {
|
|
|
|
s.state.ArbitrageOrders = make(map[uint64]types.Order)
|
|
|
|
}
|
|
|
|
|
2021-03-17 17:14:56 +00:00
|
|
|
s.Notify("current position %+v", s.state.Position)
|
2021-03-15 18:18:17 +00:00
|
|
|
|
2021-02-07 03:37:24 +00:00
|
|
|
instanceID := fmt.Sprintf("grid-%s-%d", s.Symbol, s.GridNum)
|
|
|
|
s.groupID = generateGroupID(instanceID)
|
|
|
|
log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID)
|
|
|
|
|
2021-01-21 06:51:37 +00:00
|
|
|
s.orderStore = bbgo.NewOrderStore(s.Symbol)
|
2020-12-17 07:52:53 +00:00
|
|
|
s.orderStore.BindStream(session.Stream)
|
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()
|
2021-03-18 09:20:21 +00:00
|
|
|
s.activeOrders.OnFilled(s.handleFilledOrder)
|
2020-12-17 08:29:00 +00:00
|
|
|
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()
|
|
|
|
|
2021-03-16 10:39:18 +00:00
|
|
|
if s.Persistence != nil {
|
2021-03-17 17:14:56 +00:00
|
|
|
log.Infof("backing up grid state...")
|
2021-03-16 10:39:18 +00:00
|
|
|
submitOrders := s.activeOrders.Backup()
|
2021-03-17 17:14:56 +00:00
|
|
|
s.state.Orders = submitOrders
|
|
|
|
if err := s.Persistence.Save(s.state, ID, s.Symbol, "snapshot"); err != nil {
|
2021-03-16 10:39:18 +00:00
|
|
|
log.WithError(err).Error("can not save active order backups")
|
|
|
|
} else {
|
|
|
|
log.Infof("active order snapshot saved")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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")
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2021-03-15 18:18:17 +00:00
|
|
|
if s.CatchUp {
|
|
|
|
session.Stream.OnKLineClosed(func(kline types.KLine) {
|
|
|
|
log.Infof("catchUp mode is enabled, updating grid orders...")
|
|
|
|
// update grid
|
|
|
|
s.placeGridOrders(orderExecutor, session)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-11-10 11:06:20 +00:00
|
|
|
session.Stream.OnTradeUpdate(s.tradeUpdateHandler)
|
2021-03-15 10:04:55 +00:00
|
|
|
session.Stream.OnStart(func() {
|
2021-03-17 17:14:56 +00:00
|
|
|
if stateLoaded && len(s.state.Orders) > 0 {
|
|
|
|
createdOrders, err := orderExecutor.SubmitOrders(ctx, s.state.Orders...)
|
2021-03-16 10:39:18 +00:00
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Error("active orders restore error")
|
|
|
|
}
|
|
|
|
s.activeOrders.Add(createdOrders...)
|
|
|
|
s.orderStore.Add(createdOrders...)
|
|
|
|
} else {
|
|
|
|
s.placeGridOrders(orderExecutor, session)
|
|
|
|
}
|
2020-11-05 07:04:56 +00:00
|
|
|
})
|
|
|
|
|
2020-10-29 11:47:53 +00:00
|
|
|
return nil
|
|
|
|
}
|
2021-02-07 03:37:24 +00:00
|
|
|
|
|
|
|
func generateGroupID(s string) int64 {
|
|
|
|
h := fnv.New32a()
|
|
|
|
h.Write([]byte(s))
|
|
|
|
return int64(h.Sum32())
|
|
|
|
}
|