bbgo_origin/pkg/strategy/xmaker/strategy.go

903 lines
26 KiB
Go
Raw Normal View History

package xmaker
import (
"context"
"fmt"
"sync"
"time"
2021-12-26 04:10:10 +00:00
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
2022-01-09 03:33:34 +00:00
"golang.org/x/time/rate"
2021-12-26 04:10:10 +00:00
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
2021-05-30 06:46:48 +00:00
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
)
2021-05-17 13:33:55 +00:00
var defaultMargin = fixedpoint.NewFromFloat(0.003)
var Two = fixedpoint.NewFromInt(2)
2022-01-12 06:33:55 +00:00
const priceUpdateTimeout = 30 * time.Second
2022-01-12 03:55:45 +00:00
const ID = "xmaker"
const stateKey = "state-v1"
var log = logrus.WithField("strategy", ID)
func init() {
bbgo.RegisterStrategy(ID, &Strategy{})
}
type Strategy struct {
*bbgo.Graceful
*bbgo.Notifiability
*bbgo.Persistence
2022-03-11 13:27:45 +00:00
Environment *bbgo.Environment
2021-05-30 06:46:48 +00:00
Symbol string `json:"symbol"`
// SourceExchange session name
SourceExchange string `json:"sourceExchange"`
2021-05-30 06:46:48 +00:00
// MakerExchange session name
MakerExchange string `json:"makerExchange"`
2021-05-09 13:14:51 +00:00
UpdateInterval types.Duration `json:"updateInterval"`
HedgeInterval types.Duration `json:"hedgeInterval"`
2021-05-09 10:55:56 +00:00
OrderCancelWaitTime types.Duration `json:"orderCancelWaitTime"`
2021-06-06 18:49:44 +00:00
Margin fixedpoint.Value `json:"margin"`
BidMargin fixedpoint.Value `json:"bidMargin"`
AskMargin fixedpoint.Value `json:"askMargin"`
UseDepthPrice bool `json:"useDepthPrice"`
2022-01-11 14:47:40 +00:00
DepthQuantity fixedpoint.Value `json:"depthQuantity"`
2021-05-09 18:52:41 +00:00
EnableBollBandMargin bool `json:"enableBollBandMargin"`
BollBandInterval types.Interval `json:"bollBandInterval"`
BollBandMargin fixedpoint.Value `json:"bollBandMargin"`
BollBandMarginFactor fixedpoint.Value `json:"bollBandMarginFactor"`
StopHedgeQuoteBalance fixedpoint.Value `json:"stopHedgeQuoteBalance"`
StopHedgeBaseBalance fixedpoint.Value `json:"stopHedgeBaseBalance"`
2021-05-09 18:52:41 +00:00
// Quantity is used for fixed quantity of the first layer
Quantity fixedpoint.Value `json:"quantity"`
// QuantityMultiplier is the factor that multiplies the quantity of the previous layer
QuantityMultiplier fixedpoint.Value `json:"quantityMultiplier"`
// QuantityScale helps user to define the quantity by layer scale
QuantityScale *bbgo.LayerScale `json:"quantityScale,omitempty"`
// MaxExposurePosition defines the unhedged quantity of stop
2021-04-04 03:14:09 +00:00
MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"`
DisableHedge bool `json:"disableHedge"`
2021-05-28 17:31:13 +00:00
NotifyTrade bool `json:"notifyTrade"`
NumLayers int `json:"numLayers"`
// Pips is the pips of the layer prices
2021-05-17 15:55:09 +00:00
Pips fixedpoint.Value `json:"pips"`
// --------------------------------
// private field
2021-12-26 04:10:10 +00:00
makerSession, sourceSession *bbgo.ExchangeSession
2021-12-26 04:10:10 +00:00
makerMarket, sourceMarket types.Market
// boll is the BOLLINGER indicator we used for predicting the price.
boll *indicator.BOLL
state *State
2022-05-05 07:05:38 +00:00
// persistence fields
Position *types.Position `json:"position,omitempty" persistence:"position"`
ProfitStats *ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
CoveredPosition fixedpoint.Value `json:"coveredPosition,omitempty" persistence:"covered_position"`
book *types.StreamOrderBook
activeMakerOrders *bbgo.ActiveOrderBook
2022-01-11 14:47:40 +00:00
hedgeErrorLimiter *rate.Limiter
2022-01-10 04:25:13 +00:00
hedgeErrorRateReservation *rate.Reservation
2022-01-09 03:33:34 +00:00
2021-12-26 04:10:10 +00:00
orderStore *bbgo.OrderStore
tradeCollector *bbgo.TradeCollector
2022-01-12 06:33:55 +00:00
askPriceHeartBeat, bidPriceHeartBeat types.PriceHeartBeat
2022-01-12 04:14:51 +00:00
lastPrice fixedpoint.Value
2021-03-22 09:27:07 +00:00
groupID uint32
stopC chan struct{}
}
2021-05-09 12:03:06 +00:00
func (s *Strategy) ID() string {
return ID
}
2022-05-05 07:05:38 +00:00
func (s *Strategy) InstanceID() string {
return fmt.Sprintf("%s:%s", ID, s.Symbol)
}
func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) {
sourceSession, ok := sessions[s.SourceExchange]
if !ok {
2021-03-21 04:43:41 +00:00
panic(fmt.Errorf("source session %s is not defined", s.SourceExchange))
}
sourceSession.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{})
sourceSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"})
2021-05-17 13:33:55 +00:00
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"})
}
2021-05-10 16:58:11 +00:00
func aggregatePrice(pvs types.PriceVolumeSlice, requiredQuantity fixedpoint.Value) (price fixedpoint.Value) {
q := requiredQuantity
totalAmount := fixedpoint.Zero
2021-05-10 16:58:11 +00:00
if len(pvs) == 0 {
price = fixedpoint.Zero
2021-05-10 16:58:11 +00:00
return price
} else if pvs[0].Volume.Compare(requiredQuantity) >= 0 {
2021-05-10 16:58:11 +00:00
return pvs[0].Price
}
for i := 0; i < len(pvs); i++ {
pv := pvs[i]
if pv.Volume.Compare(q) >= 0 {
totalAmount = totalAmount.Add(q.Mul(pv.Price))
2021-05-10 16:58:11 +00:00
break
}
2021-05-10 16:10:49 +00:00
q = q.Sub(pv.Volume)
totalAmount = totalAmount.Add(pv.Volume.Mul(pv.Price))
2021-05-10 16:58:11 +00:00
}
price = totalAmount.Div(requiredQuantity)
return price
2021-05-10 16:10:49 +00:00
}
func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.OrderExecutionRouter) {
if err := s.activeMakerOrders.GracefulCancel(ctx, s.makerSession.Exchange); err != nil {
log.Warnf("there are some %s orders not canceled, skipping placing maker orders", s.Symbol)
s.activeMakerOrders.Print()
return
}
2022-06-05 10:12:26 +00:00
if s.activeMakerOrders.NumOfOrders() > 0 {
return
}
2021-06-07 09:02:24 +00:00
bestBid, bestAsk, hasPrice := s.book.BestBidAndAsk()
if !hasPrice {
return
}
2021-12-26 04:10:10 +00:00
// use mid-price for the last price
s.lastPrice = bestBid.Price.Add(bestAsk.Price).Div(Two)
2021-12-26 04:10:10 +00:00
bookLastUpdateTime := s.book.LastUpdateTime()
if _, err := s.bidPriceHeartBeat.Update(bestBid, priceUpdateTimeout); err != nil {
log.WithError(err).Errorf("quote update error, %s price not updating, order book last update: %s ago",
s.Symbol,
time.Since(bookLastUpdateTime))
2022-01-12 04:14:51 +00:00
return
2022-01-12 03:55:45 +00:00
}
if _, err := s.askPriceHeartBeat.Update(bestAsk, priceUpdateTimeout); err != nil {
log.WithError(err).Errorf("quote update error, %s price not updating, order book last update: %s ago",
s.Symbol,
time.Since(bookLastUpdateTime))
2022-01-12 04:14:51 +00:00
return
2022-01-12 03:55:45 +00:00
}
2022-01-13 03:09:32 +00:00
sourceBook := s.book.CopyDepth(10)
if valid, err := sourceBook.IsValid(); !valid {
2021-06-07 09:02:24 +00:00
log.WithError(err).Errorf("%s invalid copied order book, skip quoting: %v", s.Symbol, err)
return
}
2021-03-21 03:16:15 +00:00
var disableMakerBid = false
var disableMakerAsk = false
2021-05-10 16:10:49 +00:00
// check maker's balance quota
// we load the balances from the account while we're generating the orders,
2021-03-21 03:16:15 +00:00
// the balance may have a chance to be deducted by other strategies or manual orders submitted by the user
makerBalances := s.makerSession.GetAccount().Balances()
makerQuota := &bbgo.QuotaTransaction{}
2021-03-21 03:16:15 +00:00
if b, ok := makerBalances[s.makerMarket.BaseCurrency]; ok {
if b.Available.Compare(s.makerMarket.MinQuantity) > 0 {
2021-05-10 16:10:49 +00:00
makerQuota.BaseAsset.Add(b.Available)
} else {
2021-03-21 04:43:41 +00:00
disableMakerAsk = true
}
}
2021-03-21 04:43:41 +00:00
2021-03-21 03:16:15 +00:00
if b, ok := makerBalances[s.makerMarket.QuoteCurrency]; ok {
if b.Available.Compare(s.makerMarket.MinNotional) > 0 {
2021-05-10 16:10:49 +00:00
makerQuota.QuoteAsset.Add(b.Available)
} else {
2021-04-04 03:14:09 +00:00
disableMakerBid = true
}
}
hedgeBalances := s.sourceSession.GetAccount().Balances()
hedgeQuota := &bbgo.QuotaTransaction{}
if b, ok := hedgeBalances[s.sourceMarket.BaseCurrency]; ok {
2021-03-21 04:43:41 +00:00
// to make bid orders, we need enough base asset in the foreign exchange,
2021-03-21 03:16:15 +00:00
// if the base asset balance is not enough for selling
if s.StopHedgeBaseBalance.Sign() > 0 {
minAvailable := s.StopHedgeBaseBalance.Add(s.sourceMarket.MinQuantity)
if b.Available.Compare(minAvailable) > 0 {
hedgeQuota.BaseAsset.Add(b.Available.Sub(minAvailable))
} else {
log.Warnf("%s maker bid disabled: insufficient base balance %s", s.Symbol, b.String())
disableMakerBid = true
}
} else if b.Available.Compare(s.sourceMarket.MinQuantity) > 0 {
2021-05-10 16:10:49 +00:00
hedgeQuota.BaseAsset.Add(b.Available)
} else {
2021-05-11 04:53:32 +00:00
log.Warnf("%s maker bid disabled: insufficient base balance %s", s.Symbol, b.String())
2021-03-21 03:16:15 +00:00
disableMakerBid = true
}
}
2021-03-21 03:16:15 +00:00
if b, ok := hedgeBalances[s.sourceMarket.QuoteCurrency]; ok {
2021-03-21 04:43:41 +00:00
// to make ask orders, we need enough quote asset in the foreign exchange,
2021-03-21 03:16:15 +00:00
// if the quote asset balance is not enough for buying
if s.StopHedgeQuoteBalance.Sign() > 0 {
minAvailable := s.StopHedgeQuoteBalance.Add(s.sourceMarket.MinNotional)
if b.Available.Compare(minAvailable) > 0 {
hedgeQuota.QuoteAsset.Add(b.Available.Sub(minAvailable))
} else {
log.Warnf("%s maker ask disabled: insufficient quote balance %s", s.Symbol, b.String())
disableMakerAsk = true
}
} else if b.Available.Compare(s.sourceMarket.MinNotional) > 0 {
2021-05-10 16:10:49 +00:00
hedgeQuota.QuoteAsset.Add(b.Available)
} else {
2021-05-11 04:53:32 +00:00
log.Warnf("%s maker ask disabled: insufficient quote balance %s", s.Symbol, b.String())
2021-03-21 03:16:15 +00:00
disableMakerAsk = true
}
}
2021-05-10 16:10:49 +00:00
// if max exposure position is configured, we should not:
// 1. place bid orders when we already bought too much
// 2. place ask orders when we already sold too much
if s.MaxExposurePosition.Sign() > 0 {
2022-05-05 07:05:38 +00:00
pos := s.Position.GetBase()
if pos.Compare(s.MaxExposurePosition.Neg()) > 0 {
2021-05-10 16:10:49 +00:00
// stop sell if we over-sell
disableMakerAsk = true
} else if pos.Compare(s.MaxExposurePosition) > 0 {
2021-05-10 16:10:49 +00:00
// stop buy if we over buy
disableMakerBid = true
}
}
2021-03-21 04:43:41 +00:00
if disableMakerAsk && disableMakerBid {
2021-05-17 13:33:55 +00:00
log.Warnf("%s bid/ask maker is disabled due to insufficient balances", s.Symbol)
2021-03-21 04:43:41 +00:00
return
}
2021-06-06 18:23:07 +00:00
bestBidPrice := bestBid.Price
bestAskPrice := bestAsk.Price
log.Infof("%s book ticker: best ask / best bid = %v / %v", s.Symbol, bestAskPrice, bestBidPrice)
2021-05-10 16:10:49 +00:00
var submitOrders []types.SubmitOrder
2021-05-10 16:58:11 +00:00
var accumulativeBidQuantity, accumulativeAskQuantity fixedpoint.Value
2021-05-10 16:10:49 +00:00
var bidQuantity = s.Quantity
var askQuantity = s.Quantity
var bidMargin = s.BidMargin
var askMargin = s.AskMargin
2021-05-17 15:55:09 +00:00
var pips = s.Pips
if s.EnableBollBandMargin {
lastDownBand := fixedpoint.NewFromFloat(s.boll.LastDownBand())
lastUpBand := fixedpoint.NewFromFloat(s.boll.LastUpBand())
if lastUpBand.IsZero() || lastDownBand.IsZero() {
log.Warnf("bollinger band value is zero, skipping")
return
}
log.Infof("bollinger band: up/down = %f/%f", lastUpBand.Float64(), lastDownBand.Float64())
// when bid price is lower than the down band, then it's in the downtrend
// when ask price is higher than the up band, then it's in the uptrend
if bestBidPrice.Compare(lastDownBand) < 0 {
// ratio here should be greater than 1.00
ratio := lastDownBand.Div(bestBidPrice)
// so that the original bid margin can be multiplied by 1.x
bollMargin := s.BollBandMargin.Mul(ratio).Mul(s.BollBandMarginFactor)
log.Infof("%s bollband downtrend: adjusting ask margin %v + %v = %v",
s.Symbol,
askMargin,
bollMargin,
askMargin.Add(bollMargin))
askMargin = askMargin.Add(bollMargin)
pips = pips.Mul(ratio)
}
if bestAskPrice.Compare(lastUpBand) > 0 {
// ratio here should be greater than 1.00
ratio := bestAskPrice.Div(lastUpBand)
// so that the original bid margin can be multiplied by 1.x
bollMargin := s.BollBandMargin.Mul(ratio).Mul(s.BollBandMarginFactor)
log.Infof("%s bollband uptrend adjusting bid margin %v + %v = %v",
s.Symbol,
bidMargin,
bollMargin,
bidMargin.Add(bollMargin))
bidMargin = bidMargin.Add(bollMargin)
pips = pips.Mul(ratio)
}
}
2021-06-06 18:49:44 +00:00
bidPrice := bestBidPrice
askPrice := bestAskPrice
for i := 0; i < s.NumLayers; i++ {
2021-03-21 03:16:15 +00:00
// for maker bid orders
if !disableMakerBid {
2021-05-09 18:52:41 +00:00
if s.QuantityScale != nil {
qf, err := s.QuantityScale.Scale(i + 1)
if err != nil {
log.WithError(err).Errorf("quantityScale error")
return
}
log.Infof("%s scaling bid #%d quantity to %f", s.Symbol, i+1, qf)
2021-05-09 18:52:41 +00:00
// override the default bid quantity
bidQuantity = fixedpoint.NewFromFloat(qf)
}
accumulativeBidQuantity = accumulativeBidQuantity.Add(bidQuantity)
2021-06-06 18:49:44 +00:00
if s.UseDepthPrice {
if s.DepthQuantity.Sign() > 0 {
2022-01-11 14:47:40 +00:00
bidPrice = aggregatePrice(sourceBook.SideBook(types.SideTypeBuy), s.DepthQuantity)
} else {
bidPrice = aggregatePrice(sourceBook.SideBook(types.SideTypeBuy), accumulativeBidQuantity)
}
2021-06-06 18:49:44 +00:00
}
2021-05-17 15:55:09 +00:00
bidPrice = bidPrice.Mul(fixedpoint.One.Sub(bidMargin))
if i > 0 && pips.Sign() > 0 {
bidPrice = bidPrice.Sub(pips.Mul(fixedpoint.NewFromInt(int64(i)).
Mul(s.makerMarket.TickSize)))
}
2021-05-10 16:58:11 +00:00
2021-03-21 03:16:15 +00:00
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,
Quantity: bidQuantity,
2022-02-18 05:52:13 +00:00
TimeInForce: types.TimeInForceGTC,
2021-03-21 03:16:15 +00:00
GroupID: s.groupID,
})
makerQuota.Commit()
hedgeQuota.Commit()
} else {
makerQuota.Rollback()
hedgeQuota.Rollback()
}
2021-05-09 18:52:41 +00:00
if s.QuantityMultiplier.Sign() > 0 {
2021-05-09 18:52:41 +00:00
bidQuantity = bidQuantity.Mul(s.QuantityMultiplier)
}
}
2021-03-21 03:16:15 +00:00
// for maker ask orders
if !disableMakerAsk {
2021-05-09 18:52:41 +00:00
if s.QuantityScale != nil {
qf, err := s.QuantityScale.Scale(i + 1)
if err != nil {
log.WithError(err).Errorf("quantityScale error")
return
}
log.Infof("%s scaling ask #%d quantity to %f", s.Symbol, i+1, qf)
2021-05-09 18:52:41 +00:00
// override the default bid quantity
askQuantity = fixedpoint.NewFromFloat(qf)
}
accumulativeAskQuantity = accumulativeAskQuantity.Add(askQuantity)
2021-05-10 16:58:11 +00:00
2021-06-06 18:49:44 +00:00
if s.UseDepthPrice {
if s.DepthQuantity.Sign() > 0 {
2022-01-11 14:47:40 +00:00
askPrice = aggregatePrice(sourceBook.SideBook(types.SideTypeSell), s.DepthQuantity)
} else {
askPrice = aggregatePrice(sourceBook.SideBook(types.SideTypeSell), accumulativeAskQuantity)
}
2021-06-06 18:49:44 +00:00
}
askPrice = askPrice.Mul(fixedpoint.One.Add(askMargin))
if i > 0 && pips.Sign() > 0 {
askPrice = askPrice.Add(pips.Mul(fixedpoint.NewFromInt(int64(i)).Mul(s.makerMarket.TickSize)))
}
2021-05-09 18:52:41 +00:00
2021-03-21 03:16:15 +00:00
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,
Market: s.makerMarket,
2021-03-21 03:16:15 +00:00
Type: types.OrderTypeLimit,
Side: types.SideTypeSell,
Price: askPrice,
Quantity: askQuantity,
2022-02-18 05:52:13 +00:00
TimeInForce: types.TimeInForceGTC,
2021-03-21 03:16:15 +00:00
GroupID: s.groupID,
})
makerQuota.Commit()
hedgeQuota.Commit()
} else {
makerQuota.Rollback()
hedgeQuota.Rollback()
}
2021-05-09 18:52:41 +00:00
if s.QuantityMultiplier.Sign() > 0 {
2021-05-09 18:52:41 +00:00
askQuantity = askQuantity.Mul(s.QuantityMultiplier)
}
}
}
if len(submitOrders) == 0 {
2021-05-17 13:33:55 +00:00
log.Warnf("no orders generated")
return
}
makerOrders, err := orderExecutionRouter.SubmitOrdersTo(ctx, s.MakerExchange, submitOrders...)
if err != nil {
log.WithError(err).Errorf("order error: %s", err.Error())
return
}
s.activeMakerOrders.Add(makerOrders...)
s.orderStore.Add(makerOrders...)
}
var lastPriceModifier = fixedpoint.NewFromFloat(1.001)
var minGap = fixedpoint.NewFromFloat(1.02)
2021-05-30 06:46:48 +00:00
func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) {
side := types.SideTypeBuy
if pos.IsZero() {
return
}
quantity := pos.Abs()
2021-05-30 06:46:48 +00:00
if pos.Sign() < 0 {
side = types.SideTypeSell
}
lastPrice := s.lastPrice
sourceBook := s.book.CopyDepth(1)
switch side {
case types.SideTypeBuy:
if bestAsk, ok := sourceBook.BestAsk(); ok {
lastPrice = bestAsk.Price
}
case types.SideTypeSell:
if bestBid, ok := sourceBook.BestBid(); ok {
lastPrice = bestBid.Price
}
}
notional := quantity.Mul(lastPrice)
if notional.Compare(s.sourceMarket.MinNotional) <= 0 {
log.Warnf("%s %v less than min notional, skipping hedge", s.Symbol, notional)
return
}
// adjust quantity according to the balances
account := s.sourceSession.GetAccount()
switch side {
case types.SideTypeBuy:
// check quote quantity
2021-05-10 12:22:33 +00:00
if quote, ok := account.Balance(s.sourceMarket.QuoteCurrency); ok {
if quote.Available.Compare(notional) < 0 {
// adjust price to higher 0.1%, so that we can ensure that the order can be executed
quantity = bbgo.AdjustQuantityByMaxAmount(quantity, lastPrice.Mul(lastPriceModifier), quote.Available)
quantity = s.sourceMarket.TruncateQuantity(quantity)
}
}
case types.SideTypeSell:
// check quote quantity
2021-05-10 12:22:33 +00:00
if base, ok := account.Balance(s.sourceMarket.BaseCurrency); ok {
if base.Available.Compare(quantity) < 0 {
quantity = base.Available
}
}
}
// truncate quantity for the supported precision
quantity = s.sourceMarket.TruncateQuantity(quantity)
if notional.Compare(s.sourceMarket.MinNotional.Mul(minGap)) <= 0 {
log.Warnf("the adjusted amount %v is less than minimal notional %v, skipping hedge", notional, s.sourceMarket.MinNotional)
2022-01-09 03:33:34 +00:00
return
}
if quantity.Compare(s.sourceMarket.MinQuantity.Mul(minGap)) <= 0 {
log.Warnf("the adjusted quantity %v is less than minimal quantity %v, skipping hedge", quantity, s.sourceMarket.MinQuantity)
return
}
2022-01-10 04:25:13 +00:00
if s.hedgeErrorRateReservation != nil {
if !s.hedgeErrorRateReservation.OK() {
return
}
2022-01-11 14:48:28 +00:00
s.Notify("Hit hedge error rate limit, waiting...")
2022-01-10 04:25:13 +00:00
time.Sleep(s.hedgeErrorRateReservation.Delay())
s.hedgeErrorRateReservation = nil
}
log.Infof("submitting %s hedge order %s %v", s.Symbol, side.String(), quantity)
s.Notifiability.Notify("Submitting %s hedge order %s %v", s.Symbol, side.String(), quantity)
orderExecutor := &bbgo.ExchangeOrderExecutor{Session: s.sourceSession}
returnOrders, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Market: s.sourceMarket,
Symbol: s.Symbol,
Type: types.OrderTypeMarket,
Side: side,
Quantity: quantity,
})
if err != nil {
2022-01-10 04:25:13 +00:00
s.hedgeErrorRateReservation = s.hedgeErrorLimiter.Reserve()
log.WithError(err).Errorf("market order submit error: %s", err.Error())
return
}
2021-05-30 06:46:48 +00:00
// if it's selling, than we should add positive position
if side == types.SideTypeSell {
2022-05-05 07:05:38 +00:00
s.CoveredPosition = s.CoveredPosition.Add(quantity)
2021-05-30 06:46:48 +00:00
} else {
2022-05-05 07:05:38 +00:00
s.CoveredPosition = s.CoveredPosition.Add(quantity.Neg())
2021-05-30 06:46:48 +00:00
}
s.orderStore.Add(returnOrders...)
}
2021-05-09 18:52:41 +00:00
func (s *Strategy) Validate() error {
if s.Quantity.IsZero() || s.QuantityScale == nil {
2021-05-09 18:52:41 +00:00
return errors.New("quantity or quantityScale can not be empty")
}
if !s.QuantityMultiplier.IsZero() && s.QuantityMultiplier.Sign() < 0 {
2021-05-09 18:52:41 +00:00
return errors.New("quantityMultiplier can not be a negative number")
}
if len(s.Symbol) == 0 {
return errors.New("symbol is required")
}
return nil
}
2021-05-22 09:44:20 +00:00
func (s *Strategy) LoadState() error {
var state State
// load position
2022-05-05 07:05:38 +00:00
if err := s.Persistence.Load(&state, ID, s.Symbol, stateKey); err == nil {
2021-05-22 09:44:20 +00:00
s.state = &state
}
return nil
}
func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error {
if s.BollBandInterval == "" {
s.BollBandInterval = types.Interval1m
}
if s.BollBandMarginFactor.IsZero() {
s.BollBandMarginFactor = fixedpoint.One
}
if s.BollBandMargin.IsZero() {
s.BollBandMargin = fixedpoint.NewFromFloat(0.001)
}
// 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.IsZero() {
if !s.Margin.IsZero() {
s.BidMargin = s.Margin
} else {
s.BidMargin = defaultMargin
}
}
if s.AskMargin.IsZero() {
if !s.Margin.IsZero() {
s.AskMargin = s.Margin
} else {
s.AskMargin = defaultMargin
}
}
2022-01-11 14:47:40 +00:00
s.hedgeErrorLimiter = rate.NewLimiter(rate.Every(1*time.Minute), 1)
2022-01-09 03:33:34 +00:00
// 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)
}
standardIndicatorSet, ok := s.sourceSession.StandardIndicatorSet(s.Symbol)
if !ok {
return fmt.Errorf("%s standard indicator set not found", s.Symbol)
}
s.boll = standardIndicatorSet.BOLL(types.IntervalWindow{
Interval: s.BollBandInterval,
Window: 21,
}, 1.0)
if store, ok := s.sourceSession.MarketDataStore(s.Symbol); ok {
if klines, ok2 := store.KLinesOfInterval(s.BollBandInterval); ok2 {
s.boll.Update(*klines)
}
}
// restore state
2022-05-05 07:05:38 +00:00
instanceID := s.InstanceID()
s.groupID = util.FNV32(instanceID)
log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID)
2021-05-22 09:44:20 +00:00
if err := s.LoadState(); err != nil {
return err
2022-05-05 07:05:38 +00:00
}
if s.Position == nil {
if s.state != nil && s.state.Position != nil {
s.Position = s.state.Position
} else {
s.Position = types.NewPositionFromMarket(s.makerMarket)
}
// force update for legacy code
s.Position.Market = s.makerMarket
}
2022-06-13 04:04:35 +00:00
s.Notify("xmaker: %s position is restored", s.Symbol, s.Position)
2022-05-05 07:05:38 +00:00
if s.ProfitStats == nil {
if s.state != nil {
p2 := s.state.ProfitStats
s.ProfitStats = &p2
} else {
s.ProfitStats = &ProfitStats{
ProfitStats: types.NewProfitStats(s.makerMarket),
MakerExchange: s.makerSession.ExchangeName,
}
}
}
if s.CoveredPosition.IsZero() {
if s.state != nil && !s.CoveredPosition.IsZero() {
s.CoveredPosition = s.state.CoveredPosition
}
}
if s.makerSession.MakerFeeRate.Sign() > 0 || s.makerSession.TakerFeeRate.Sign() > 0 {
2022-05-05 07:05:38 +00:00
s.Position.SetExchangeFeeRate(types.ExchangeName(s.MakerExchange), types.ExchangeFee{
2021-05-16 09:58:51 +00:00
MakerFeeRate: s.makerSession.MakerFeeRate,
TakerFeeRate: s.makerSession.TakerFeeRate,
})
}
if s.sourceSession.MakerFeeRate.Sign() > 0 || s.sourceSession.TakerFeeRate.Sign() > 0 {
2022-05-05 07:05:38 +00:00
s.Position.SetExchangeFeeRate(types.ExchangeName(s.SourceExchange), types.ExchangeFee{
2021-05-16 09:58:51 +00:00
MakerFeeRate: s.sourceSession.MakerFeeRate,
TakerFeeRate: s.sourceSession.TakerFeeRate,
})
}
s.book = types.NewStreamBook(s.Symbol)
2021-05-27 19:13:50 +00:00
s.book.BindStream(s.sourceSession.MarketDataStream)
s.activeMakerOrders = bbgo.NewActiveOrderBook(s.Symbol)
s.activeMakerOrders.BindStream(s.makerSession.UserDataStream)
s.orderStore = bbgo.NewOrderStore(s.Symbol)
s.orderStore.BindStream(s.sourceSession.UserDataStream)
s.orderStore.BindStream(s.makerSession.UserDataStream)
2022-05-05 07:05:38 +00:00
s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.Position, s.orderStore)
if s.NotifyTrade {
2022-03-11 13:27:45 +00:00
s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) {
s.Notifiability.Notify(trade)
})
}
2022-03-11 13:27:45 +00:00
s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) {
2021-12-26 04:10:10 +00:00
c := trade.PositionChange()
if trade.Exchange == s.sourceSession.ExchangeName {
2022-05-05 07:05:38 +00:00
s.CoveredPosition = s.CoveredPosition.Add(c)
}
2022-05-05 07:05:38 +00:00
s.ProfitStats.AddTrade(trade)
2021-12-26 04:10:10 +00:00
2022-03-11 13:27:45 +00:00
if profit.Compare(fixedpoint.Zero) == 0 {
2022-05-05 07:05:38 +00:00
s.Environment.RecordPosition(s.Position, trade, nil)
2022-03-11 13:27:45 +00:00
} else {
log.Infof("%s generated profit: %v", s.Symbol, profit)
2022-05-05 07:05:38 +00:00
p := s.Position.NewProfit(trade, profit, netProfit)
2022-03-11 13:27:45 +00:00
p.Strategy = ID
p.StrategyInstanceID = instanceID
s.Notify(&p)
2022-05-05 07:05:38 +00:00
s.ProfitStats.AddProfit(p)
2022-03-11 13:27:45 +00:00
2022-05-05 07:05:38 +00:00
s.Environment.RecordPosition(s.Position, trade, &p)
2021-12-26 04:10:10 +00:00
}
})
2022-03-11 13:27:45 +00:00
2021-12-26 04:10:10 +00:00
s.tradeCollector.OnPositionUpdate(func(position *types.Position) {
s.Notifiability.Notify(position)
})
s.tradeCollector.OnRecover(func(trade types.Trade) {
s.Notifiability.Notify("Recover trade", trade)
})
s.tradeCollector.BindStream(s.sourceSession.UserDataStream)
s.tradeCollector.BindStream(s.makerSession.UserDataStream)
2021-12-26 04:10:10 +00:00
s.stopC = make(chan struct{})
go func() {
posTicker := time.NewTicker(util.MillisecondsJitter(s.HedgeInterval.Duration(), 200))
defer posTicker.Stop()
quoteTicker := time.NewTicker(util.MillisecondsJitter(s.UpdateInterval.Duration(), 200))
2021-05-09 12:03:06 +00:00
defer quoteTicker.Stop()
reportTicker := time.NewTicker(time.Hour)
defer reportTicker.Stop()
2022-01-06 17:03:12 +00:00
tradeScanInterval := 20 * time.Minute
tradeScanTicker := time.NewTicker(tradeScanInterval)
defer tradeScanTicker.Stop()
defer func() {
if err := s.activeMakerOrders.GracefulCancel(context.Background(), s.makerSession.Exchange); err != nil {
log.WithError(err).Errorf("can not cancel %s orders", s.Symbol)
}
}()
for {
select {
case <-s.stopC:
2021-05-09 18:17:19 +00:00
log.Warnf("%s maker goroutine stopped, due to the stop signal", s.Symbol)
return
case <-ctx.Done():
2021-05-09 18:17:19 +00:00
log.Warnf("%s maker goroutine stopped, due to the cancelled context", s.Symbol)
return
2021-05-09 12:03:06 +00:00
case <-quoteTicker.C:
s.updateQuote(ctx, orderExecutionRouter)
case <-reportTicker.C:
2022-05-05 07:05:38 +00:00
s.Notifiability.Notify(&s.ProfitStats)
2022-01-06 17:03:12 +00:00
case <-tradeScanTicker.C:
log.Infof("scanning trades from %s ago...", tradeScanInterval)
startTime := time.Now().Add(-tradeScanInterval)
if err := s.tradeCollector.Recover(ctx, s.sourceSession.Exchange.(types.ExchangeTradeHistoryService), s.Symbol, startTime); err != nil {
2022-01-06 17:03:12 +00:00
log.WithError(err).Errorf("query trades error")
}
case <-posTicker.C:
// For positive position and positive covered position:
// uncover position = +5 - +3 (covered position) = 2
//
// For positive position and negative covered position:
// uncover position = +5 - (-3) (covered position) = 8
//
// meaning we bought 5 on MAX and sent buy order with 3 on binance
//
// For negative position:
2021-05-30 06:46:48 +00:00
// uncover position = -5 - -3 (covered position) = -2
s.tradeCollector.Process()
2022-05-05 07:05:38 +00:00
position := s.Position.GetBase()
2022-05-05 07:05:38 +00:00
uncoverPosition := position.Sub(s.CoveredPosition)
absPos := uncoverPosition.Abs()
if !s.DisableHedge && absPos.Compare(s.sourceMarket.MinQuantity) > 0 {
log.Infof("%s base position %v coveredPosition: %v uncoverPosition: %v",
2021-12-31 07:26:51 +00:00
s.Symbol,
position,
2022-05-05 07:05:38 +00:00
s.CoveredPosition,
uncoverPosition,
2021-12-31 07:26:51 +00:00
)
s.Hedge(ctx, uncoverPosition.Neg())
}
}
}
}()
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
close(s.stopC)
2021-05-22 09:44:20 +00:00
// wait for the quoter to stop
2021-05-09 18:17:19 +00:00
time.Sleep(s.UpdateInterval.Duration())
2021-05-09 11:04:44 +00:00
shutdownCtx, cancelShutdown := context.WithTimeout(context.TODO(), time.Minute)
defer cancelShutdown()
if err := s.activeMakerOrders.GracefulCancel(shutdownCtx, s.makerSession.Exchange); err != nil {
log.WithError(err).Errorf("graceful cancel error")
}
2022-05-05 07:05:38 +00:00
s.Notify("%s: %s position", ID, s.Symbol, s.Position)
})
return nil
}