From ab52dd6349dab3e3d7c3df6f1be39d81ca4b8d4a Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 21:11:58 +0800 Subject: [PATCH 01/49] funding: filter kline event with types.KLineWith --- pkg/strategy/funding/strategy.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/funding/strategy.go b/pkg/strategy/funding/strategy.go index 58361e963..d056a8fb7 100644 --- a/pkg/strategy/funding/strategy.go +++ b/pkg/strategy/funding/strategy.go @@ -127,11 +127,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { // skip k-lines from other symbols - if kline.Symbol != s.Symbol { - return - } for _, detection := range s.SupportDetection { var lastMA = ma.Last() @@ -195,6 +192,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.Notify(kline) } } - }) + })) return nil } From 12b9775eb3310e69f030cc0adf59ba2d571e2090 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 21:17:33 +0800 Subject: [PATCH 02/49] rename funding to xfunding --- pkg/cmd/strategy/builtin.go | 2 +- .../{funding => xfunding}/strategy.go | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) rename pkg/strategy/{funding => xfunding}/strategy.go (92%) diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index edfdce475..50e1ec8bb 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -16,7 +16,6 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/fixedmaker" _ "github.com/c9s/bbgo/pkg/strategy/flashcrash" _ "github.com/c9s/bbgo/pkg/strategy/fmaker" - _ "github.com/c9s/bbgo/pkg/strategy/funding" _ "github.com/c9s/bbgo/pkg/strategy/grid" _ "github.com/c9s/bbgo/pkg/strategy/grid2" _ "github.com/c9s/bbgo/pkg/strategy/harmonic" @@ -38,6 +37,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/trendtrader" _ "github.com/c9s/bbgo/pkg/strategy/wall" _ "github.com/c9s/bbgo/pkg/strategy/xbalance" + _ "github.com/c9s/bbgo/pkg/strategy/xfunding" _ "github.com/c9s/bbgo/pkg/strategy/xgap" _ "github.com/c9s/bbgo/pkg/strategy/xmaker" _ "github.com/c9s/bbgo/pkg/strategy/xnav" diff --git a/pkg/strategy/funding/strategy.go b/pkg/strategy/xfunding/strategy.go similarity index 92% rename from pkg/strategy/funding/strategy.go rename to pkg/strategy/xfunding/strategy.go index d056a8fb7..407fcc6f0 100644 --- a/pkg/strategy/funding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -1,4 +1,4 @@ -package funding +package xfunding import ( "context" @@ -14,7 +14,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -const ID = "funding" +const ID = "xfunding" var log = logrus.WithField("strategy", ID) @@ -66,6 +66,11 @@ func (s *Strategy) ID() string { return ID } +func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { + // TODO implement me + panic("implement me") +} + func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { // session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{}) @@ -99,11 +104,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } - premiumIndex, err := session.Exchange.(*binance.Exchange).QueryPremiumIndex(ctx, s.Symbol) - if err != nil { - log.Error("exchange does not support funding rate api") - } - var ma types.Float64Indicator for _, detection := range s.SupportDetection { @@ -124,10 +124,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se Window: detection.MovingAverageIntervalWindow.Window, }) } - } session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { + premiumIndex, err := session.Exchange.(*binance.Exchange).QueryPremiumIndex(ctx, s.Symbol) + if err != nil { + log.Error("exchange does not support funding rate api") + } + // skip k-lines from other symbols for _, detection := range s.SupportDetection { var lastMA = ma.Last() @@ -195,3 +199,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se })) return nil } + +func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error { + // TODO implement me + panic("implement me") +} From b2c0a343f33a26ae9b03409ec8c8934502a2ffbe Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 21:17:45 +0800 Subject: [PATCH 03/49] rename funding config file --- config/{funding.yaml => xfunding.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename config/{funding.yaml => xfunding.yaml} (100%) diff --git a/config/funding.yaml b/config/xfunding.yaml similarity index 100% rename from config/funding.yaml rename to config/xfunding.yaml From e93d13e425bc0b82c6dd6b2a361ada7d47eb9867 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 21:36:42 +0800 Subject: [PATCH 04/49] xfunding: implement CrossRun --- pkg/strategy/xfunding/strategy.go | 64 ++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 407fcc6f0..fa511e611 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -3,6 +3,7 @@ package xfunding import ( "context" "errors" + "fmt" "strings" "github.com/sirupsen/logrus" @@ -26,6 +27,8 @@ func init() { } type Strategy struct { + Environment *bbgo.Environment + // These fields will be filled from the config file (it translates YAML to JSON) Symbol string `json:"symbol"` Market types.Market `json:"-"` @@ -34,9 +37,8 @@ type Strategy struct { // Interval types.Interval `json:"interval"` FundingRate *struct { - High fixedpoint.Value `json:"high"` - Neutral fixedpoint.Value `json:"neutral"` - DiffThreshold fixedpoint.Value `json:"diffThreshold"` + High fixedpoint.Value `json:"high"` + Neutral fixedpoint.Value `json:"neutral"` } `json:"fundingRate"` SupportDetection []struct { @@ -60,6 +62,19 @@ type Strategy struct { MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"` } `json:"supportDetection"` + + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + + SpotPosition *types.Position `persistence:"spot_position"` + FuturesPosition *types.Position `persistence:"futures_position"` + + spotSession, futuresSession *bbgo.ExchangeSession + + spotOrderExecutor, futuresOrderExecutor bbgo.OrderExecutor + spotMarket, futuresMarket types.Market + + SpotSession string `json:"spotSession"` + FuturesSession string `json:"futuresSession"` } func (s *Strategy) ID() string { @@ -96,6 +111,10 @@ func (s *Strategy) Validate() error { return nil } +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s-%s", ID, s.Symbol) +} + func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { standardIndicatorSet := session.StandardIndicatorSet(s.Symbol) @@ -201,6 +220,41 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error { - // TODO implement me - panic("implement me") + instanceID := s.InstanceID() + + // TODO: add safety check + s.spotSession = sessions[s.SpotSession] + s.futuresSession = sessions[s.FuturesSession] + + s.spotMarket, _ = s.spotSession.Market(s.Symbol) + s.futuresMarket, _ = s.futuresSession.Market(s.Symbol) + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + if s.FuturesPosition == nil { + s.FuturesPosition = types.NewPositionFromMarket(s.futuresMarket) + } + + if s.SpotPosition == nil { + s.SpotPosition = types.NewPositionFromMarket(s.spotMarket) + } + + s.spotOrderExecutor = s.allocateOrderExecutor(ctx, s.spotSession, instanceID, s.SpotPosition) + s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition) + return nil +} + +func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.ExchangeSession, instanceID string, position *types.Position) *bbgo.GeneralOrderExecutor { + orderExecutor := bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, position) + orderExecutor.BindEnvironment(s.Environment) + orderExecutor.Bind() + orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _, _ fixedpoint.Value) { + s.ProfitStats.AddTrade(trade) + }) + orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + return orderExecutor } From b881aea2282c3f4411350f5b66d494e36bf110b4 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 21:38:56 +0800 Subject: [PATCH 05/49] add position action --- pkg/strategy/xfunding/strategy.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index fa511e611..44b3cab84 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -17,6 +17,12 @@ import ( const ID = "xfunding" +const ( + PositionNoOp = iota + PositionOpening + PositionClosing +) + var log = logrus.WithField("strategy", ID) func init() { From d6c430a4b4d8f66ea5d6abe65de9cee0fddbfc49 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 21:42:06 +0800 Subject: [PATCH 06/49] xfunding: implement CrossSubscribe --- pkg/strategy/xfunding/strategy.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 44b3cab84..834cf4884 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -17,8 +17,10 @@ import ( const ID = "xfunding" +type PositionAction int + const ( - PositionNoOp = iota + PositionNoOp PositionAction = iota PositionOpening PositionClosing ) @@ -81,6 +83,9 @@ type Strategy struct { SpotSession string `json:"spotSession"` FuturesSession string `json:"futuresSession"` + + // positionAction is default to NoOp + positionAction PositionAction } func (s *Strategy) ID() string { @@ -88,8 +93,17 @@ func (s *Strategy) ID() string { } func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { - // TODO implement me - panic("implement me") + // TODO: add safety check + spotSession := sessions[s.SpotSession] + futuresSession := sessions[s.FuturesSession] + + spotSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: types.Interval1m, + }) + + futuresSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: types.Interval1m, + }) } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { From e607fc19acc252defa22b61e92181cde0ef92eb6 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 21:42:44 +0800 Subject: [PATCH 07/49] xfunding: check spotSession, futuresSession names --- pkg/strategy/xfunding/strategy.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 834cf4884..1879d626d 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -128,6 +128,14 @@ func (s *Strategy) Validate() error { return errors.New("symbol is required") } + if len(s.SpotSession) == 0 { + return errors.New("spotSession name is required") + } + + if len(s.FuturesSession) == 0 { + return errors.New("futuresSession name is required") + } + return nil } From 98b0ffa510a8a98cf904989baf86ab249d2980fb Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 22:01:59 +0800 Subject: [PATCH 08/49] all: add more futures channel types --- pkg/strategy/xfunding/strategy.go | 6 ------ pkg/types/channel.go | 17 ++++++++++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 1879d626d..055d2c676 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -107,12 +107,6 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - // session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{}) - - // session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ - // Interval: string(s.Interval), - // }) - for _, detection := range s.SupportDetection { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ Interval: detection.Interval, diff --git a/pkg/types/channel.go b/pkg/types/channel.go index 8b9b48e0f..b9aeb5460 100644 --- a/pkg/types/channel.go +++ b/pkg/types/channel.go @@ -2,8 +2,15 @@ package types type Channel string -var BookChannel = Channel("book") -var KLineChannel = Channel("kline") -var BookTickerChannel = Channel("bookticker") -var MarketTradeChannel = Channel("trade") -var AggTradeChannel = Channel("aggTrade") +const ( + BookChannel = Channel("book") + KLineChannel = Channel("kline") + BookTickerChannel = Channel("bookTicker") + MarketTradeChannel = Channel("trade") + AggTradeChannel = Channel("aggTrade") + + // channels for futures + MarkPriceChannel = Channel("markPrice") + LiquidationOrderChannel = Channel("liquidationOrder") + ContractChannel = Channel("contract") +) From 3c69ccc25a56c36195dbe406da8625b6a96cea6c Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 22:04:02 +0800 Subject: [PATCH 09/49] types: update channel names --- pkg/types/channel.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/types/channel.go b/pkg/types/channel.go index b9aeb5460..2e85b9236 100644 --- a/pkg/types/channel.go +++ b/pkg/types/channel.go @@ -10,7 +10,10 @@ const ( AggTradeChannel = Channel("aggTrade") // channels for futures - MarkPriceChannel = Channel("markPrice") + MarkPriceChannel = Channel("markPrice") + LiquidationOrderChannel = Channel("liquidationOrder") - ContractChannel = Channel("contract") + + // ContractInfoChannel is the contract info provided by the exchange + ContractInfoChannel = Channel("contractInfo") ) From 6265ad248ee0d76ea4d2455255d61b57b167c555 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 22:15:01 +0800 Subject: [PATCH 10/49] xfunding: add premium checker --- pkg/strategy/xfunding/strategy.go | 102 ++++++++---------------------- 1 file changed, 27 insertions(+), 75 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 055d2c676..29e62bb3d 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -44,10 +44,12 @@ type Strategy struct { MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"` // Interval types.Interval `json:"interval"` - FundingRate *struct { - High fixedpoint.Value `json:"high"` - Neutral fixedpoint.Value `json:"neutral"` - } `json:"fundingRate"` + // ShortFundingRate is the funding rate range for short positions + // TODO: right now we don't support negative funding rate (long position) since it's rarer + ShortFundingRate *struct { + High fixedpoint.Value `json:"high"` + Low fixedpoint.Value `json:"low"` + } `json:"shortFundingRate"` SupportDetection []struct { Interval types.Interval `json:"interval"` @@ -86,6 +88,7 @@ type Strategy struct { // positionAction is default to NoOp positionAction PositionAction + positionType types.PositionType } func (s *Strategy) ID() string { @@ -167,77 +170,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { - premiumIndex, err := session.Exchange.(*binance.Exchange).QueryPremiumIndex(ctx, s.Symbol) - if err != nil { - log.Error("exchange does not support funding rate api") - } - - // skip k-lines from other symbols - for _, detection := range s.SupportDetection { - var lastMA = ma.Last() - - closePrice := kline.GetClose() - closePriceF := closePrice.Float64() - // skip if the closed price is under the moving average - if closePriceF < lastMA { - log.Infof("skip %s closed price %v < last ma %f", s.Symbol, closePrice, lastMA) - return - } - - fundingRate := premiumIndex.LastFundingRate - - if fundingRate.Compare(s.FundingRate.High) >= 0 { - bbgo.Notify("%s funding rate %s is too high! threshold %s", - s.Symbol, - fundingRate.Percentage(), - s.FundingRate.High.Percentage(), - ) - } else { - log.Infof("skip funding rate is too low") - return - } - - prettyBaseVolume := s.Market.BaseCurrencyFormatter() - prettyQuoteVolume := s.Market.QuoteCurrencyFormatter() - - if detection.MinVolume.Sign() > 0 && kline.Volume.Compare(detection.MinVolume) > 0 { - bbgo.Notify("Detected %s %s resistance base volume %s > min base volume %s, quote volume %s", - s.Symbol, detection.Interval.String(), - prettyBaseVolume.FormatMoney(kline.Volume.Trunc()), - prettyBaseVolume.FormatMoney(detection.MinVolume.Trunc()), - prettyQuoteVolume.FormatMoney(kline.QuoteVolume.Trunc()), - ) - bbgo.Notify(kline) - - baseBalance, ok := session.GetAccount().Balance(s.Market.BaseCurrency) - if !ok { - return - } - - if baseBalance.Available.Sign() > 0 && baseBalance.Total().Compare(s.MaxExposurePosition) < 0 { - log.Infof("opening a short position") - _, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: kline.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeMarket, - Quantity: s.Quantity, - }) - if err != nil { - log.WithError(err).Error("submit order error") - } - } - } else if detection.MinQuoteVolume.Sign() > 0 && kline.QuoteVolume.Compare(detection.MinQuoteVolume) > 0 { - bbgo.Notify("Detected %s %s resistance quote volume %s > min quote volume %s, base volume %s", - s.Symbol, detection.Interval.String(), - prettyQuoteVolume.FormatMoney(kline.QuoteVolume.Trunc()), - prettyQuoteVolume.FormatMoney(detection.MinQuoteVolume.Trunc()), - prettyBaseVolume.FormatMoney(kline.Volume.Trunc()), - ) - bbgo.Notify(kline) - } - } - })) return nil } @@ -265,6 +197,26 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.spotOrderExecutor = s.allocateOrderExecutor(ctx, s.spotSession, instanceID, s.SpotPosition) s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition) + + s.futuresSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { + premiumIndex, err := s.futuresSession.Exchange.(*binance.Exchange).QueryPremiumIndex(ctx, s.Symbol) + if err != nil { + log.WithError(err).Error("premium index query error") + return + } + + fundingRate := premiumIndex.LastFundingRate + + if s.ShortFundingRate != nil { + if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { + s.positionAction = PositionOpening + s.positionType = types.PositionShort + } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { + s.positionAction = PositionClosing + } + } + })) + return nil } From dc5e0cbcc24c77e0c7ebb33c336efd52fe83499d Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 22:15:24 +0800 Subject: [PATCH 11/49] xfunding: solve lint error --- pkg/strategy/xfunding/strategy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 29e62bb3d..7dcf100a5 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -169,6 +169,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se }) } } + _ = ma return nil } From 928f668fecf21d85b23436310f068a656e3fd137 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 22 Mar 2023 22:17:37 +0800 Subject: [PATCH 12/49] xfunding: pull out premium check to detectPremiumIndex --- pkg/strategy/xfunding/strategy.go | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 7dcf100a5..4fb91a94a 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -206,21 +206,24 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return } - fundingRate := premiumIndex.LastFundingRate - - if s.ShortFundingRate != nil { - if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { - s.positionAction = PositionOpening - s.positionType = types.PositionShort - } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { - s.positionAction = PositionClosing - } - } + s.detectPremiumIndex(premiumIndex) })) return nil } +func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) { + fundingRate := premiumIndex.LastFundingRate + if s.ShortFundingRate != nil { + if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { + s.positionAction = PositionOpening + s.positionType = types.PositionShort + } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { + s.positionAction = PositionClosing + } + } +} + func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.ExchangeSession, instanceID string, position *types.Position) *bbgo.GeneralOrderExecutor { orderExecutor := bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, position) orderExecutor.BindEnvironment(s.Environment) From 684f6c6e1dd20e493ffac4651f7b33022063d584 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 00:23:51 +0800 Subject: [PATCH 13/49] xfunding: document spot trade handler --- pkg/strategy/xfunding/strategy.go | 133 ++++++++++++++++++++++++++---- 1 file changed, 119 insertions(+), 14 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 4fb91a94a..9958ef485 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -38,11 +38,15 @@ type Strategy struct { Environment *bbgo.Environment // These fields will be filled from the config file (it translates YAML to JSON) - Symbol string `json:"symbol"` - Market types.Market `json:"-"` - Quantity fixedpoint.Value `json:"quantity,omitempty"` - MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"` - // Interval types.Interval `json:"interval"` + Symbol string `json:"symbol"` + Market types.Market `json:"-"` + Quantity fixedpoint.Value `json:"quantity,omitempty"` + + // IncrementalQuoteQuantity is used for opening position incrementally with a small fixed quote quantity + // for example, 100usdt per order + IncrementalQuoteQuantity fixedpoint.Value `json:"incrementalQuoteQuantity"` + + QuoteInvestment fixedpoint.Value `json:"quoteInvestment"` // ShortFundingRate is the funding rate range for short positions // TODO: right now we don't support negative funding rate (long position) since it's rarer @@ -80,7 +84,7 @@ type Strategy struct { spotSession, futuresSession *bbgo.ExchangeSession - spotOrderExecutor, futuresOrderExecutor bbgo.OrderExecutor + spotOrderExecutor, futuresOrderExecutor *bbgo.GeneralOrderExecutor spotMarket, futuresMarket types.Market SpotSession string `json:"spotSession"` @@ -88,7 +92,12 @@ type Strategy struct { // positionAction is default to NoOp positionAction PositionAction - positionType types.PositionType + + // positionType is the futures position type + // currently we only support short position for the positive funding rate + positionType types.PositionType + + usedQuoteInvestment fixedpoint.Value } func (s *Strategy) ID() string { @@ -133,6 +142,10 @@ func (s *Strategy) Validate() error { return errors.New("futuresSession name is required") } + if s.QuoteInvestment.IsZero() { + return errors.New("quoteInvestment can not be zero") + } + return nil } @@ -143,11 +156,6 @@ func (s *Strategy) InstanceID() string { func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { standardIndicatorSet := session.StandardIndicatorSet(s.Symbol) - if !session.Futures { - log.Error("futures not enabled in config for this strategy") - return nil - } - var ma types.Float64Indicator for _, detection := range s.SupportDetection { @@ -177,7 +185,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error { instanceID := s.InstanceID() - // TODO: add safety check + s.usedQuoteInvestment = fixedpoint.Zero s.spotSession = sessions[s.SpotSession] s.futuresSession = sessions[s.FuturesSession] @@ -197,10 +205,48 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } s.spotOrderExecutor = s.allocateOrderExecutor(ctx, s.spotSession, instanceID, s.SpotPosition) + s.spotOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + // we act differently on the spot account + // when opening a position, we place orders on the spot account first, then the futures account, + // and we need to accumulate the used quote amount + // + // when closing a position, we place orders on the futures account first, then the spot account + // we need to close the position according to its base quantity instead of quote quantity + if s.positionType == types.PositionShort { + switch s.positionAction { + case PositionOpening: + if trade.Side != types.SideTypeSell { + log.Errorf("unexpected trade side: %+v, expecting SELL trade", trade) + return + } + + // TODO: add mutex lock for this modification + s.usedQuoteInvestment = s.usedQuoteInvestment.Add(trade.QuoteQuantity) + if s.usedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { + s.positionAction = PositionNoOp + + // 1) if we have trade, try to query the balance and transfer the balance to the futures wallet account + + // 2) transferred successfully, sync futures position + + // 3) compare spot position and futures position, increase the position size until they are the same size + } + + case PositionClosing: + if trade.Side != types.SideTypeBuy { + log.Errorf("unexpected trade side: %+v, expecting BUY trade", trade) + return + } + + } + } + }) + s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition) + binanceExchange := s.futuresSession.Exchange.(*binance.Exchange) s.futuresSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { - premiumIndex, err := s.futuresSession.Exchange.(*binance.Exchange).QueryPremiumIndex(ctx, s.Symbol) + premiumIndex, err := binanceExchange.QueryPremiumIndex(ctx, s.Symbol) if err != nil { log.WithError(err).Error("premium index query error") return @@ -209,9 +255,68 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.detectPremiumIndex(premiumIndex) })) + s.spotSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(k types.KLine) { + // TODO: use go routine and time.Ticker + s.triggerPositionAction(ctx) + })) + return nil } +func (s *Strategy) syncSpotPosition(ctx context.Context) { + ticker, err := s.spotSession.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + log.WithError(err).Errorf("can not query ticker") + return + } + + if s.positionType != types.PositionShort { + log.Errorf("funding long position type is not supported") + return + } + + switch s.positionAction { + + case PositionClosing: + + case PositionOpening: + if s.usedQuoteInvestment.IsZero() || s.usedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { + // stop + return + } + + leftQuote := s.QuoteInvestment.Sub(s.usedQuoteInvestment) + orderPrice := ticker.Sell + orderQuantity := fixedpoint.Min(s.IncrementalQuoteQuantity, leftQuote).Div(orderPrice) + orderQuantity = fixedpoint.Max(orderQuantity, s.spotMarket.MinQuantity) + createdOrders, err := s.spotOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Quantity: orderQuantity, + Price: orderPrice, + Market: s.spotMarket, + TimeInForce: types.TimeInForceGTC, + }) + if err != nil { + log.WithError(err).Errorf("can not submit order") + return + } + + log.Infof("created orders: %+v", createdOrders) + } +} + +func (s *Strategy) triggerPositionAction(ctx context.Context) { + switch s.positionAction { + case PositionOpening: + s.syncSpotPosition(ctx) + + case PositionClosing: + + } +} + func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) { fundingRate := premiumIndex.LastFundingRate if s.ShortFundingRate != nil { From 6668d683e169a87cda80f61b9365f970706186bb Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 00:40:20 +0800 Subject: [PATCH 14/49] xfunding: adjust quoteInvestment according to the quote balance --- pkg/strategy/xfunding/strategy.go | 53 ++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 9958ef485..00ba6e971 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -186,12 +186,26 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order instanceID := s.InstanceID() s.usedQuoteInvestment = fixedpoint.Zero + s.spotSession = sessions[s.SpotSession] s.futuresSession = sessions[s.FuturesSession] s.spotMarket, _ = s.spotSession.Market(s.Symbol) s.futuresMarket, _ = s.futuresSession.Market(s.Symbol) + // adjust QuoteInvestment + if b, ok := s.spotSession.Account.Balance(s.spotMarket.QuoteCurrency); ok { + originalQuoteInvestment := s.QuoteInvestment + s.QuoteInvestment = fixedpoint.Min(b.Available, s.QuoteInvestment) + + if originalQuoteInvestment.Compare(s.QuoteInvestment) != 0 { + log.Infof("adjusted quoteInvestment from %s to %s according to the balance", + originalQuoteInvestment.String(), + s.QuoteInvestment.String(), + ) + } + } + if s.ProfitStats == nil { s.ProfitStats = types.NewProfitStats(s.Market) } @@ -204,6 +218,10 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.SpotPosition = types.NewPositionFromMarket(s.spotMarket) } + binanceFutures := s.futuresSession.Exchange.(*binance.Exchange) + binanceSpot := s.spotSession.Exchange.(*binance.Exchange) + _ = binanceSpot + s.spotOrderExecutor = s.allocateOrderExecutor(ctx, s.spotSession, instanceID, s.SpotPosition) s.spotOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { // we act differently on the spot account @@ -224,14 +242,15 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.usedQuoteInvestment = s.usedQuoteInvestment.Add(trade.QuoteQuantity) if s.usedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { s.positionAction = PositionNoOp - - // 1) if we have trade, try to query the balance and transfer the balance to the futures wallet account - - // 2) transferred successfully, sync futures position - - // 3) compare spot position and futures position, increase the position size until they are the same size } + // 1) if we have trade, try to query the balance and transfer the balance to the futures wallet account + // balances, err := binanceSpot.QueryAccountBalances(ctx) + + // 2) transferred successfully, sync futures position + + // 3) compare spot position and futures position, increase the position size until they are the same size + case PositionClosing: if trade.Side != types.SideTypeBuy { log.Errorf("unexpected trade side: %+v, expecting BUY trade", trade) @@ -244,9 +263,8 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition) - binanceExchange := s.futuresSession.Exchange.(*binance.Exchange) s.futuresSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { - premiumIndex, err := binanceExchange.QueryPremiumIndex(ctx, s.Symbol) + premiumIndex, err := binanceFutures.QueryPremiumIndex(ctx, s.Symbol) if err != nil { log.WithError(err).Error("premium index query error") return @@ -263,6 +281,25 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return nil } +// TODO: replace type binance.Exchange with an interface +func (s *Strategy) transferIn(ctx context.Context, ex *binance.Exchange, trade types.Trade) error { + balances, err := ex.QueryAccountBalances(ctx) + if err != nil { + return err + } + + b, ok := balances[s.spotMarket.BaseCurrency] + if !ok { + return nil + } + + // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity + if b.Available.Compare(trade.Quantity) >= 0 { + + } + return nil +} + func (s *Strategy) syncSpotPosition(ctx context.Context) { ticker, err := s.spotSession.Exchange.QueryTicker(ctx, s.Symbol) if err != nil { From c632e6efac50d5777af03dc1729237cd64fd3e8f Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 00:54:37 +0800 Subject: [PATCH 15/49] binance: add binance futures_transfer_request --- .../binanceapi/futures_transfer_request.go | 33 ++++ .../futures_transfer_request_requestgen.go | 178 ++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 pkg/exchange/binance/binanceapi/futures_transfer_request.go create mode 100644 pkg/exchange/binance/binanceapi/futures_transfer_request_requestgen.go diff --git a/pkg/exchange/binance/binanceapi/futures_transfer_request.go b/pkg/exchange/binance/binanceapi/futures_transfer_request.go new file mode 100644 index 000000000..ee726f74a --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_transfer_request.go @@ -0,0 +1,33 @@ +package binanceapi + +import "github.com/c9s/requestgen" + +type FuturesTransferType int + +const ( + FuturesTransferSpotToUsdtFutures FuturesTransferType = 1 + FuturesTransferUsdtFuturesToSpot FuturesTransferType = 2 + + FuturesTransferSpotToCoinFutures FuturesTransferType = 3 + FuturesTransferCoinFuturesToSpot FuturesTransferType = 4 +) + +type FuturesTransferResponse struct { + TranId int64 `json:"tranId"` +} + +//go:generate requestgen -method POST -url "/sapi/v1/futures/transfer" -type FuturesTransferRequest -responseType .FuturesTransferResponse +type FuturesTransferRequest struct { + client requestgen.AuthenticatedAPIClient + + asset string `param:"asset"` + + // amount is a decimal in string format + amount string `param:"amount"` + + transferType FuturesTransferType `param:"type"` +} + +func (c *RestClient) NewFuturesTransferRequest() *FuturesTransferRequest { + return &FuturesTransferRequest{client: c} +} diff --git a/pkg/exchange/binance/binanceapi/futures_transfer_request_requestgen.go b/pkg/exchange/binance/binanceapi/futures_transfer_request_requestgen.go new file mode 100644 index 000000000..50f4b46e9 --- /dev/null +++ b/pkg/exchange/binance/binanceapi/futures_transfer_request_requestgen.go @@ -0,0 +1,178 @@ +// Code generated by "requestgen -method POST -url /sapi/v1/futures/transfer -type FuturesTransferRequest -responseType .FuturesTransferResponse"; DO NOT EDIT. + +package binanceapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (f *FuturesTransferRequest) Asset(asset string) *FuturesTransferRequest { + f.asset = asset + return f +} + +func (f *FuturesTransferRequest) Amount(amount string) *FuturesTransferRequest { + f.amount = amount + return f +} + +func (f *FuturesTransferRequest) TransferType(transferType FuturesTransferType) *FuturesTransferRequest { + f.transferType = transferType + return f +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (f *FuturesTransferRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (f *FuturesTransferRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check asset field -> json key asset + asset := f.asset + + // assign parameter of asset + params["asset"] = asset + // check amount field -> json key amount + amount := f.amount + + // assign parameter of amount + params["amount"] = amount + // check transferType field -> json key type + transferType := f.transferType + + // TEMPLATE check-valid-values + switch transferType { + case FuturesTransferSpotToUsdtFutures, FuturesTransferUsdtFuturesToSpot, FuturesTransferSpotToCoinFutures, FuturesTransferCoinFuturesToSpot: + params["type"] = transferType + + default: + return nil, fmt.Errorf("type value %v is invalid", transferType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of transferType + params["type"] = transferType + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (f *FuturesTransferRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := f.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (f *FuturesTransferRequest) GetParametersJSON() ([]byte, error) { + params, err := f.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (f *FuturesTransferRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (f *FuturesTransferRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (f *FuturesTransferRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (f *FuturesTransferRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (f *FuturesTransferRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := f.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +func (f *FuturesTransferRequest) Do(ctx context.Context) (*FuturesTransferResponse, error) { + + params, err := f.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + apiURL := "/sapi/v1/futures/transfer" + + req, err := f.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := f.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse FuturesTransferResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + return &apiResponse, nil +} From 6848e11e8a5002a814deb4b0ca01c6eff6df76b0 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 00:55:00 +0800 Subject: [PATCH 16/49] binance: implement TransferFuturesAsset --- pkg/exchange/binance/exchange.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index aea4e1459..16dbfd39e 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -367,6 +367,22 @@ func (e *Exchange) QueryMarginBorrowHistory(ctx context.Context, asset string) e return nil } +func (e *Exchange) TransferFuturesAsset(ctx context.Context, asset string, amount fixedpoint.Value, io int) error { + req := e.client2.NewFuturesTransferRequest() + req.Asset(asset) + req.Amount(amount.String()) + + if io > 0 { // int + req.TransferType(binanceapi.FuturesTransferSpotToUsdtFutures) + } else if io < 0 { // out + req.TransferType(binanceapi.FuturesTransferUsdtFuturesToSpot) + } + + resp, err := req.Do(ctx) + log.Debugf("futures transfer %s %s, transaction = %+v", amount.String(), asset, resp) + return err +} + // transferCrossMarginAccountAsset transfer asset to the cross margin account or to the main account func (e *Exchange) transferCrossMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io int) error { req := e.client.NewMarginTransferService() From 6ca85b175aaac6c34283221b2a91a512104b502f Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 00:56:28 +0800 Subject: [PATCH 17/49] xfunding: adjust quote investment according to the fee rate --- pkg/strategy/xfunding/strategy.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 00ba6e971..859aea333 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -196,7 +196,10 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order // adjust QuoteInvestment if b, ok := s.spotSession.Account.Balance(s.spotMarket.QuoteCurrency); ok { originalQuoteInvestment := s.QuoteInvestment - s.QuoteInvestment = fixedpoint.Min(b.Available, s.QuoteInvestment) + + // adjust available quote with the fee rate + available := b.Available.Mul(fixedpoint.NewFromFloat(1.0 - (0.01 * 0.075))) + s.QuoteInvestment = fixedpoint.Min(available, s.QuoteInvestment) if originalQuoteInvestment.Compare(s.QuoteInvestment) != 0 { log.Infof("adjusted quoteInvestment from %s to %s according to the balance", From 6797069a40032016c3cf1eff4a550e4f621e1aea Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 02:42:05 +0800 Subject: [PATCH 18/49] binanceapi: fix payload encode format --- pkg/exchange/binance/binanceapi/client.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/exchange/binance/binanceapi/client.go b/pkg/exchange/binance/binanceapi/client.go index 015713129..b9722380f 100644 --- a/pkg/exchange/binance/binanceapi/client.go +++ b/pkg/exchange/binance/binanceapi/client.go @@ -233,6 +233,14 @@ func castPayload(payload interface{}) ([]byte, error) { case []byte: return v, nil + case map[string]interface{}: + var params = url.Values{} + for a, b := range v { + params.Add(a, fmt.Sprintf("%v", b)) + } + + return []byte(params.Encode()), nil + default: body, err := json.Marshal(v) return body, err From 487fbf868111f2beef1468d1ef3d91cab9256405 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 02:42:26 +0800 Subject: [PATCH 19/49] binance: implement TransferFuturesAccountAsset api --- pkg/exchange/binance/exchange.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 16dbfd39e..9880b6d2d 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -367,33 +367,38 @@ func (e *Exchange) QueryMarginBorrowHistory(ctx context.Context, asset string) e return nil } -func (e *Exchange) TransferFuturesAsset(ctx context.Context, asset string, amount fixedpoint.Value, io int) error { +func (e *Exchange) TransferFuturesAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { req := e.client2.NewFuturesTransferRequest() req.Asset(asset) req.Amount(amount.String()) - if io > 0 { // int + if io == types.TransferIn { req.TransferType(binanceapi.FuturesTransferSpotToUsdtFutures) - } else if io < 0 { // out + } else if io == types.TransferOut { req.TransferType(binanceapi.FuturesTransferUsdtFuturesToSpot) + } else { + return fmt.Errorf("unexpected transfer direction: %d given", io) } resp, err := req.Do(ctx) - log.Debugf("futures transfer %s %s, transaction = %+v", amount.String(), asset, resp) + log.Infof("futures transfer %s %s, transaction = %+v, err = %+v", amount.String(), asset, resp, err) return err } // transferCrossMarginAccountAsset transfer asset to the cross margin account or to the main account -func (e *Exchange) transferCrossMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io int) error { +func (e *Exchange) transferCrossMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error { req := e.client.NewMarginTransferService() req.Asset(asset) req.Amount(amount.String()) - if io > 0 { // in + if io == types.TransferIn { req.Type(binance.MarginTransferTypeToMargin) - } else if io < 0 { // out + } else if io == types.TransferOut { req.Type(binance.MarginTransferTypeToMain) + } else { + return fmt.Errorf("unexpected transfer direction: %d given", io) } + resp, err := req.Do(ctx) if err != nil { return err From 161dc7dc647dcf3906cc2ac5e0566099089adec2 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 09:04:49 +0800 Subject: [PATCH 20/49] types: add transfer direction --- pkg/types/transfer.go | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 pkg/types/transfer.go diff --git a/pkg/types/transfer.go b/pkg/types/transfer.go new file mode 100644 index 000000000..640ef9977 --- /dev/null +++ b/pkg/types/transfer.go @@ -0,0 +1,8 @@ +package types + +type TransferDirection int + +const ( + TransferIn TransferDirection = 1 + TransferOut TransferDirection = -1 +) From 2a927dc34d1c0fd2aa1fac37bebbc781ddb04b32 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 09:20:44 +0800 Subject: [PATCH 21/49] interact: reduce info logs --- pkg/interact/interact.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/interact/interact.go b/pkg/interact/interact.go index 820979cfe..c9358b9ac 100644 --- a/pkg/interact/interact.go +++ b/pkg/interact/interact.go @@ -247,9 +247,9 @@ func (it *Interact) Start(ctx context.Context) error { } for _, custom := range it.customInteractions { - log.Infof("checking %T custom interaction...", custom) + log.Debugf("checking %T custom interaction...", custom) if initializer, ok := custom.(Initializer); ok { - log.Infof("initializing %T custom interaction...", custom) + log.Debugf("initializing %T custom interaction...", custom) if err := initializer.Initialize(); err != nil { return err } From a838b4991aadecd6eadb694a70c81f6836a51cb2 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 12:51:52 +0800 Subject: [PATCH 22/49] bbgo: refactor order executor with max retries --- pkg/bbgo/order_execution.go | 39 ++++++++++++------------------ pkg/bbgo/order_executor_general.go | 14 ++++++++++- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index 761dc74e1..4ac34b72a 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -54,7 +54,7 @@ func (e *ExchangeOrderExecutionRouter) SubmitOrdersTo(ctx context.Context, sessi return nil, err } - createdOrders, _, err := BatchPlaceOrder(ctx, es.Exchange, formattedOrders...) + createdOrders, _, err := BatchPlaceOrder(ctx, es.Exchange, nil, formattedOrders...) return createdOrders, err } @@ -94,7 +94,7 @@ func (e *ExchangeOrderExecutor) SubmitOrders(ctx context.Context, orders ...type log.Infof("submitting order: %s", order.String()) } - createdOrders, _, err := BatchPlaceOrder(ctx, e.Session.Exchange, formattedOrders...) + createdOrders, _, err := BatchPlaceOrder(ctx, e.Session.Exchange, nil, formattedOrders...) return createdOrders, err } @@ -297,10 +297,13 @@ func (c *BasicRiskController) ProcessOrders(session *ExchangeSession, orders ... return outOrders, nil } +type OrderCallback func(order types.Order) + // BatchPlaceOrder -func BatchPlaceOrder(ctx context.Context, exchange types.Exchange, submitOrders ...types.SubmitOrder) (types.OrderSlice, []int, error) { +func BatchPlaceOrder(ctx context.Context, exchange types.Exchange, orderCallback OrderCallback, submitOrders ...types.SubmitOrder) (types.OrderSlice, []int, error) { var createdOrders types.OrderSlice var err error + var errIndexes []int for i, submitOrder := range submitOrders { createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrder) @@ -309,6 +312,11 @@ func BatchPlaceOrder(ctx context.Context, exchange types.Exchange, submitOrders errIndexes = append(errIndexes, i) } else if createdOrder != nil { createdOrder.Tag = submitOrder.Tag + + if orderCallback != nil { + orderCallback(*createdOrder) + } + createdOrders = append(createdOrders, *createdOrder) } } @@ -316,8 +324,6 @@ func BatchPlaceOrder(ctx context.Context, exchange types.Exchange, submitOrders return createdOrders, errIndexes, err } -type OrderCallback func(order types.Order) - // BatchRetryPlaceOrder places the orders and retries the failed orders func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx []int, orderCallback OrderCallback, logger log.FieldLogger, submitOrders ...types.SubmitOrder) (types.OrderSlice, []int, error) { if logger == nil { @@ -329,26 +335,12 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ // if the errIdx is nil, then we should iterate all the submit orders // allocate a variable for new error index - var errIdxNext []int if len(errIdx) == 0 { - for i, submitOrder := range submitOrders { - createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrder) - if err2 != nil { - werr = multierr.Append(werr, err2) - errIdxNext = append(errIdxNext, i) - } else if createdOrder != nil { - // if the order is successfully created, than we should copy the order tag - createdOrder.Tag = submitOrder.Tag - - if orderCallback != nil { - orderCallback(*createdOrder) - } - - createdOrders = append(createdOrders, *createdOrder) - } + var err2 error + createdOrders, errIdx, err2 = BatchPlaceOrder(ctx, exchange, orderCallback, submitOrders...) + if err2 != nil { + werr = multierr.Append(werr, err2) } - - errIdx = errIdxNext } timeoutCtx, cancelTimeout := context.WithTimeout(ctx, DefaultSubmitOrderRetryTimeout) @@ -359,6 +351,7 @@ func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx [ // set backoff max retries to 101 because https://ja.wikipedia.org/wiki/101%E5%9B%9E%E7%9B%AE%E3%81%AE%E3%83%97%E3%83%AD%E3%83%9D%E3%83%BC%E3%82%BA backoffMaxRetries := uint64(101) + var errIdxNext []int batchRetryOrder: for retryRound := 0; len(errIdx) > 0 && retryRound < 10; retryRound++ { // sleep for 200 millisecond between each retry diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 219955c15..db97ae7e0 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -40,6 +40,7 @@ type GeneralOrderExecutor struct { marginBaseMaxBorrowable, marginQuoteMaxBorrowable fixedpoint.Value + maxRetries uint disableNotify bool closing int64 } @@ -73,6 +74,10 @@ func (e *GeneralOrderExecutor) DisableNotify() { e.disableNotify = true } +func (e *GeneralOrderExecutor) SetMaxRetries(maxRetries uint) { + e.maxRetries = maxRetries +} + func (e *GeneralOrderExecutor) startMarginAssetUpdater(ctx context.Context) { marginService, ok := e.session.Exchange.(types.MarginBorrowRepayService) if !ok { @@ -194,10 +199,12 @@ func (e *GeneralOrderExecutor) FastSubmitOrders(ctx context.Context, submitOrder if err != nil { return nil, err } - createdOrders, errIdx, err := BatchPlaceOrder(ctx, e.session.Exchange, formattedOrders...) + + createdOrders, errIdx, err := BatchPlaceOrder(ctx, e.session.Exchange, nil, formattedOrders...) if len(errIdx) > 0 { return nil, err } + if IsBackTesting { e.orderStore.Add(createdOrders...) e.activeMakerOrders.Add(createdOrders...) @@ -229,6 +236,11 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders .. e.tradeCollector.Process() } + if e.maxRetries == 0 { + createdOrders, _, err := BatchPlaceOrder(ctx, e.session.Exchange, orderCreateCallback, formattedOrders...) + return createdOrders, err + } + createdOrders, _, err := BatchRetryPlaceOrder(ctx, e.session.Exchange, nil, orderCreateCallback, e.logger, formattedOrders...) return createdOrders, err } From 20cd73e6ad92a97c1793a8f69c6a317632903224 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 12:58:10 +0800 Subject: [PATCH 23/49] xfunding: fix transfer and refactoring more methods --- config/xfunding.yaml | 30 ++- go.mod | 1 + go.sum | 1 + pkg/strategy/drift/strategy.go | 4 +- .../xfunding/positionaction_string.go | 25 ++ pkg/strategy/xfunding/strategy.go | 243 ++++++++++++++---- pkg/strategy/xgap/strategy.go | 2 +- pkg/util/backoff/generic.go | 18 ++ 8 files changed, 259 insertions(+), 65 deletions(-) create mode 100644 pkg/strategy/xfunding/positionaction_string.go create mode 100644 pkg/util/backoff/generic.go diff --git a/config/xfunding.yaml b/config/xfunding.yaml index 9f7e7352b..0714a91f9 100644 --- a/config/xfunding.yaml +++ b/config/xfunding.yaml @@ -12,20 +12,22 @@ notifications: sessions: binance: exchange: binance - envVarPrefix: binance + envVarPrefix: BINANCE + + binance_futures: + exchange: binance + envVarPrefix: BINANCE futures: true -exchangeStrategies: -- on: binance - funding: +crossExchangeStrategies: + +- xfunding: + spotSession: binance + futuresSession: binance_futures symbol: ETHUSDT - quantity: 0.0001 - fundingRate: - high: 0.01% - supportDetection: - - interval: 1m - movingAverageType: EMA - movingAverageIntervalWindow: - interval: 15m - window: 60 - minVolume: 8_000 + leverage: 1.0 + incrementalQuoteQuantity: 11 + quoteInvestment: 110 + shortFundingRate: + high: 0.000% + low: -0.01% diff --git a/go.mod b/go.mod index ee4b893c3..10a84c69f 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b github.com/cenkalti/backoff/v4 v4.2.0 github.com/cheggaaa/pb/v3 v3.0.8 + github.com/cloudflare/cfssl v0.0.0-20190808011637-b1ec8c586c2a github.com/codingconcepts/env v0.0.0-20200821220118-a8fbf8d84482 github.com/ethereum/go-ethereum v1.10.23 github.com/evanphx/json-patch/v5 v5.6.0 diff --git a/go.sum b/go.sum index 3e90668ad..294026040 100644 --- a/go.sum +++ b/go.sum @@ -103,6 +103,7 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cfssl v0.0.0-20190808011637-b1ec8c586c2a h1:ym8P2+ZvUvVtpLzy8wFLLvdggUIU31mvldvxixQQI2o= github.com/cloudflare/cfssl v0.0.0-20190808011637-b1ec8c586c2a/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index 403ee1914..b3519442e 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -177,7 +177,7 @@ func (s *Strategy) SubmitOrder(ctx context.Context, submitOrder types.SubmitOrde if err != nil { return nil, err } - createdOrders, errIdx, err := bbgo.BatchPlaceOrder(ctx, s.Session.Exchange, formattedOrder) + createdOrders, errIdx, err := bbgo.BatchPlaceOrder(ctx, s.Session.Exchange, nil, formattedOrder) if len(errIdx) > 0 { return nil, err } @@ -539,7 +539,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter s.atr.PushK(kline) atr := s.atr.Last() - price := kline.Close //s.getLastPrice() + price := kline.Close // s.getLastPrice() pricef := price.Float64() lowf := math.Min(kline.Low.Float64(), pricef) highf := math.Max(kline.High.Float64(), pricef) diff --git a/pkg/strategy/xfunding/positionaction_string.go b/pkg/strategy/xfunding/positionaction_string.go new file mode 100644 index 000000000..6aba4acf8 --- /dev/null +++ b/pkg/strategy/xfunding/positionaction_string.go @@ -0,0 +1,25 @@ +// Code generated by "stringer -type=PositionAction"; DO NOT EDIT. + +package xfunding + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[PositionNoOp-0] + _ = x[PositionOpening-1] + _ = x[PositionClosing-2] +} + +const _PositionAction_name = "PositionNoOpPositionOpeningPositionClosing" + +var _PositionAction_index = [...]uint8{0, 12, 27, 42} + +func (i PositionAction) String() string { + if i < 0 || i >= PositionAction(len(_PositionAction_index)-1) { + return "PositionAction(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _PositionAction_name[_PositionAction_index[i]:_PositionAction_index[i+1]] +} diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 859aea333..4b61e55d9 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -5,11 +5,13 @@ import ( "errors" "fmt" "strings" + "time" "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/exchange/binance" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/util/backoff" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/types" @@ -17,6 +19,7 @@ import ( const ID = "xfunding" +//go:generate stringer -type=PositionAction type PositionAction int const ( @@ -34,13 +37,18 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } +// Strategy is the xfunding fee strategy +// Right now it only supports short position in the USDT futures account. +// When opening the short position, it uses spot account to buy inventory, then transfer the inventory to the futures account as collateral assets. type Strategy struct { Environment *bbgo.Environment // These fields will be filled from the config file (it translates YAML to JSON) - Symbol string `json:"symbol"` - Market types.Market `json:"-"` - Quantity fixedpoint.Value `json:"quantity,omitempty"` + Symbol string `json:"symbol"` + Market types.Market `json:"-"` + + // Leverage is the leverage of the futures position + Leverage fixedpoint.Value `json:"leverage,omitempty"` // IncrementalQuoteQuantity is used for opening position incrementally with a small fixed quote quantity // for example, 100usdt per order @@ -129,6 +137,11 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } } +func (s *Strategy) Defaults() error { + s.Leverage = fixedpoint.One + return nil +} + func (s *Strategy) Validate() error { if len(s.Symbol) == 0 { return errors.New("symbol is required") @@ -236,8 +249,8 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order if s.positionType == types.PositionShort { switch s.positionAction { case PositionOpening: - if trade.Side != types.SideTypeSell { - log.Errorf("unexpected trade side: %+v, expecting SELL trade", trade) + if trade.Side != types.SideTypeBuy { + log.Errorf("unexpected trade side: %+v, expecting BUY trade", trade) return } @@ -248,15 +261,20 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } // 1) if we have trade, try to query the balance and transfer the balance to the futures wallet account - // balances, err := binanceSpot.QueryAccountBalances(ctx) + // TODO: handle missing trades here. If the process crashed during the transfer, how to recover? + if err := backoff.RetryGeneric(ctx, func() error { + return s.transferIn(ctx, binanceSpot, trade) + }); err != nil { + log.WithError(err).Errorf("transfer in retry failed") + return + } // 2) transferred successfully, sync futures position - - // 3) compare spot position and futures position, increase the position size until they are the same size + // compare spot position and futures position, increase the position size until they are the same size case PositionClosing: - if trade.Side != types.SideTypeBuy { - log.Errorf("unexpected trade side: %+v, expecting BUY trade", trade) + if trade.Side != types.SideTypeSell { + log.Errorf("unexpected trade side: %+v, expecting SELL trade", trade) return } @@ -267,42 +285,167 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition) s.futuresSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { - premiumIndex, err := binanceFutures.QueryPremiumIndex(ctx, s.Symbol) - if err != nil { - log.WithError(err).Error("premium index query error") - return + // s.queryAndDetectPremiumIndex(ctx, binanceFutures) + })) + + go func() { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + + case <-ticker.C: + s.queryAndDetectPremiumIndex(ctx, binanceFutures) + + } } + }() - s.detectPremiumIndex(premiumIndex) - })) - - s.spotSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(k types.KLine) { - // TODO: use go routine and time.Ticker - s.triggerPositionAction(ctx) - })) + // TODO: use go routine and time.Ticker to trigger spot sync and futures sync + /* + s.spotSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(k types.KLine) { + })) + */ return nil } +func (s *Strategy) queryAndDetectPremiumIndex(ctx context.Context, binanceFutures *binance.Exchange) { + premiumIndex, err := binanceFutures.QueryPremiumIndex(ctx, s.Symbol) + if err != nil { + log.WithError(err).Error("premium index query error") + return + } + + log.Infof("premiumIndex: %+v", premiumIndex) + + if changed := s.detectPremiumIndex(premiumIndex); changed { + log.Infof("position action: %s %s", s.positionType, s.positionAction.String()) + s.triggerPositionAction(ctx) + } +} + // TODO: replace type binance.Exchange with an interface func (s *Strategy) transferIn(ctx context.Context, ex *binance.Exchange, trade types.Trade) error { + currency := s.spotMarket.BaseCurrency + + // base asset needs BUY trades + if trade.Side == types.SideTypeSell { + return nil + } + balances, err := ex.QueryAccountBalances(ctx) if err != nil { return err } - b, ok := balances[s.spotMarket.BaseCurrency] + b, ok := balances[currency] if !ok { - return nil + return fmt.Errorf("%s balance not found", currency) } - // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity + // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here if b.Available.Compare(trade.Quantity) >= 0 { - + log.Infof("transfering futures account asset %s %s", trade.Quantity, currency) + if err := ex.TransferFuturesAccountAsset(ctx, currency, trade.Quantity, types.TransferIn); err != nil { + log.WithError(err).Errorf("spot-to-futures transfer error") + return err + } } + return nil } +func (s *Strategy) triggerPositionAction(ctx context.Context) { + switch s.positionAction { + case PositionOpening: + s.syncSpotPosition(ctx) + s.syncFuturesPosition(ctx) + case PositionClosing: + s.syncFuturesPosition(ctx) + s.syncSpotPosition(ctx) + } +} + +func (s *Strategy) syncFuturesPosition(ctx context.Context) { + _ = s.futuresOrderExecutor.GracefulCancel(ctx) + + ticker, err := s.futuresSession.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + log.WithError(err).Errorf("can not query ticker") + return + } + + switch s.positionAction { + + case PositionClosing: + + case PositionOpening: + + if s.positionType != types.PositionShort { + return + } + + spotBase := s.SpotPosition.GetBase() // should be positive base quantity here + futuresBase := s.FuturesPosition.GetBase() // should be negative base quantity here + + if spotBase.IsZero() { + // skip when spot base is zero + return + } + + log.Infof("position comparision: %s (spot) <=> %s (futures)", spotBase.String(), futuresBase.String()) + + if futuresBase.Sign() > 0 { + // unexpected error + log.Errorf("unexpected futures position (got positive, expecting negative)") + return + } + + // compare with the spot position and increase the position + quoteValue, err := bbgo.CalculateQuoteQuantity(ctx, s.futuresSession, s.futuresMarket.QuoteCurrency, s.Leverage) + if err != nil { + log.WithError(err).Errorf("can not calculate futures account quote value") + return + } + log.Infof("calculated futures account quote value = %s", quoteValue.String()) + + if spotBase.Sign() > 0 && futuresBase.Neg().Compare(spotBase) < 0 { + orderPrice := ticker.Sell + diffQuantity := spotBase.Sub(futuresBase.Neg().Mul(s.Leverage)) + + log.Infof("position diff quantity: %s", diffQuantity.String()) + + orderQuantity := fixedpoint.Max(diffQuantity, s.futuresMarket.MinQuantity) + orderQuantity = bbgo.AdjustQuantityByMinAmount(orderQuantity, orderPrice, s.futuresMarket.MinNotional) + if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { + log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) + return + } + + createdOrders, err := s.futuresOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Quantity: orderQuantity, + Price: orderPrice, + Market: s.futuresMarket, + // TimeInForce: types.TimeInForceGTC, + }) + + if err != nil { + log.WithError(err).Errorf("can not submit order") + return + } + + log.Infof("created orders: %+v", createdOrders) + } + } +} + func (s *Strategy) syncSpotPosition(ctx context.Context) { ticker, err := s.spotSession.Exchange.QueryTicker(ctx, s.Symbol) if err != nil { @@ -318,26 +461,32 @@ func (s *Strategy) syncSpotPosition(ctx context.Context) { switch s.positionAction { case PositionClosing: + // TODO: compare with the futures position and reduce the position case PositionOpening: - if s.usedQuoteInvestment.IsZero() || s.usedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { - // stop + if s.usedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { return } leftQuote := s.QuoteInvestment.Sub(s.usedQuoteInvestment) - orderPrice := ticker.Sell + orderPrice := ticker.Buy orderQuantity := fixedpoint.Min(s.IncrementalQuoteQuantity, leftQuote).Div(orderPrice) orderQuantity = fixedpoint.Max(orderQuantity, s.spotMarket.MinQuantity) - createdOrders, err := s.spotOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeLimitMaker, - Quantity: orderQuantity, - Price: orderPrice, - Market: s.spotMarket, - TimeInForce: types.TimeInForceGTC, - }) + + _ = s.spotOrderExecutor.GracefulCancel(ctx) + + submitOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Quantity: orderQuantity, + Price: orderPrice, + Market: s.spotMarket, + } + + log.Infof("placing spot order: %+v", submitOrder) + + createdOrders, err := s.spotOrderExecutor.SubmitOrders(ctx, submitOrder) if err != nil { log.WithError(err).Errorf("can not submit order") return @@ -347,30 +496,28 @@ func (s *Strategy) syncSpotPosition(ctx context.Context) { } } -func (s *Strategy) triggerPositionAction(ctx context.Context) { - switch s.positionAction { - case PositionOpening: - s.syncSpotPosition(ctx) - - case PositionClosing: - - } -} - -func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) { +func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed bool) { fundingRate := premiumIndex.LastFundingRate + + log.Infof("last %s funding rate: %s", s.Symbol, fundingRate.Percentage()) + if s.ShortFundingRate != nil { if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { s.positionAction = PositionOpening s.positionType = types.PositionShort + changed = true } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { s.positionAction = PositionClosing + changed = true } } + + return changed } func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.ExchangeSession, instanceID string, position *types.Position) *bbgo.GeneralOrderExecutor { orderExecutor := bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, position) + orderExecutor.SetMaxRetries(0) orderExecutor.BindEnvironment(s.Environment) orderExecutor.Bind() orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _, _ fixedpoint.Value) { diff --git a/pkg/strategy/xgap/strategy.go b/pkg/strategy/xgap/strategy.go index fd0b67f60..efa8b6445 100644 --- a/pkg/strategy/xgap/strategy.go +++ b/pkg/strategy/xgap/strategy.go @@ -331,7 +331,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se s.tradingMarket.MinNotional.Mul(NotionModifier).Div(price)) } - createdOrders, _, err := bbgo.BatchPlaceOrder(ctx, tradingSession.Exchange, types.SubmitOrder{ + createdOrders, _, err := bbgo.BatchPlaceOrder(ctx, tradingSession.Exchange, nil, types.SubmitOrder{ Symbol: s.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeLimit, diff --git a/pkg/util/backoff/generic.go b/pkg/util/backoff/generic.go new file mode 100644 index 000000000..f7303d336 --- /dev/null +++ b/pkg/util/backoff/generic.go @@ -0,0 +1,18 @@ +package backoff + +import ( + "context" + + "github.com/cenkalti/backoff/v4" +) + +var MaxRetries uint64 = 101 + +func RetryGeneric(ctx context.Context, op backoff.Operation) (err error) { + err = backoff.Retry(op, backoff.WithContext( + backoff.WithMaxRetries( + backoff.NewExponentialBackOff(), + MaxRetries), + ctx)) + return err +} From 80c30d15a0bac5e98eaecb86e49541ab261ecddd Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 13:02:22 +0800 Subject: [PATCH 24/49] xfunding: correct method names --- pkg/strategy/xfunding/strategy.go | 114 ++++++++++++++++-------------- 1 file changed, 60 insertions(+), 54 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 4b61e55d9..2da6c934c 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -362,14 +362,17 @@ func (s *Strategy) transferIn(ctx context.Context, ex *binance.Exchange, trade t func (s *Strategy) triggerPositionAction(ctx context.Context) { switch s.positionAction { case PositionOpening: - s.syncSpotPosition(ctx) + s.increaseSpotPosition(ctx) s.syncFuturesPosition(ctx) case PositionClosing: - s.syncFuturesPosition(ctx) + s.reduceFuturesPosition(ctx) s.syncSpotPosition(ctx) } } +func (s *Strategy) reduceFuturesPosition(ctx context.Context) {} + +// syncFuturesPosition syncs the futures position with the given spot position func (s *Strategy) syncFuturesPosition(ctx context.Context) { _ = s.futuresOrderExecutor.GracefulCancel(ctx) @@ -380,73 +383,76 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { } switch s.positionAction { - case PositionClosing: + return + case PositionOpening, PositionNoOp: + } - case PositionOpening: + if s.positionType != types.PositionShort { + return + } - if s.positionType != types.PositionShort { + spotBase := s.SpotPosition.GetBase() // should be positive base quantity here + futuresBase := s.FuturesPosition.GetBase() // should be negative base quantity here + + if spotBase.IsZero() { + // skip when spot base is zero + return + } + + log.Infof("position comparision: %s (spot) <=> %s (futures)", spotBase.String(), futuresBase.String()) + + if futuresBase.Sign() > 0 { + // unexpected error + log.Errorf("unexpected futures position (got positive, expecting negative)") + return + } + + // compare with the spot position and increase the position + quoteValue, err := bbgo.CalculateQuoteQuantity(ctx, s.futuresSession, s.futuresMarket.QuoteCurrency, s.Leverage) + if err != nil { + log.WithError(err).Errorf("can not calculate futures account quote value") + return + } + log.Infof("calculated futures account quote value = %s", quoteValue.String()) + + if spotBase.Sign() > 0 && futuresBase.Neg().Compare(spotBase) < 0 { + orderPrice := ticker.Sell + diffQuantity := spotBase.Sub(futuresBase.Neg().Mul(s.Leverage)) + + log.Infof("position diff quantity: %s", diffQuantity.String()) + + orderQuantity := fixedpoint.Max(diffQuantity, s.futuresMarket.MinQuantity) + orderQuantity = bbgo.AdjustQuantityByMinAmount(orderQuantity, orderPrice, s.futuresMarket.MinNotional) + if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { + log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) return } - spotBase := s.SpotPosition.GetBase() // should be positive base quantity here - futuresBase := s.FuturesPosition.GetBase() // should be negative base quantity here + createdOrders, err := s.futuresOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Quantity: orderQuantity, + Price: orderPrice, + Market: s.futuresMarket, + // TimeInForce: types.TimeInForceGTC, + }) - if spotBase.IsZero() { - // skip when spot base is zero - return - } - - log.Infof("position comparision: %s (spot) <=> %s (futures)", spotBase.String(), futuresBase.String()) - - if futuresBase.Sign() > 0 { - // unexpected error - log.Errorf("unexpected futures position (got positive, expecting negative)") - return - } - - // compare with the spot position and increase the position - quoteValue, err := bbgo.CalculateQuoteQuantity(ctx, s.futuresSession, s.futuresMarket.QuoteCurrency, s.Leverage) if err != nil { - log.WithError(err).Errorf("can not calculate futures account quote value") + log.WithError(err).Errorf("can not submit order") return } - log.Infof("calculated futures account quote value = %s", quoteValue.String()) - if spotBase.Sign() > 0 && futuresBase.Neg().Compare(spotBase) < 0 { - orderPrice := ticker.Sell - diffQuantity := spotBase.Sub(futuresBase.Neg().Mul(s.Leverage)) - - log.Infof("position diff quantity: %s", diffQuantity.String()) - - orderQuantity := fixedpoint.Max(diffQuantity, s.futuresMarket.MinQuantity) - orderQuantity = bbgo.AdjustQuantityByMinAmount(orderQuantity, orderPrice, s.futuresMarket.MinNotional) - if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { - log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) - return - } - - createdOrders, err := s.futuresOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeLimitMaker, - Quantity: orderQuantity, - Price: orderPrice, - Market: s.futuresMarket, - // TimeInForce: types.TimeInForceGTC, - }) - - if err != nil { - log.WithError(err).Errorf("can not submit order") - return - } - - log.Infof("created orders: %+v", createdOrders) - } + log.Infof("created orders: %+v", createdOrders) } } func (s *Strategy) syncSpotPosition(ctx context.Context) { + +} + +func (s *Strategy) increaseSpotPosition(ctx context.Context) { ticker, err := s.spotSession.Exchange.QueryTicker(ctx, s.Symbol) if err != nil { log.WithError(err).Errorf("can not query ticker") From 16608619cac4bb48af12e525b25ccce49d357b29 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 13:07:54 +0800 Subject: [PATCH 25/49] xfunding: fix sync guard --- pkg/strategy/xfunding/strategy.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 2da6c934c..7f07d6bdf 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -374,11 +374,7 @@ func (s *Strategy) reduceFuturesPosition(ctx context.Context) {} // syncFuturesPosition syncs the futures position with the given spot position func (s *Strategy) syncFuturesPosition(ctx context.Context) { - _ = s.futuresOrderExecutor.GracefulCancel(ctx) - - ticker, err := s.futuresSession.Exchange.QueryTicker(ctx, s.Symbol) - if err != nil { - log.WithError(err).Errorf("can not query ticker") + if s.positionType != types.PositionShort { return } @@ -388,7 +384,11 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { case PositionOpening, PositionNoOp: } - if s.positionType != types.PositionShort { + _ = s.futuresOrderExecutor.GracefulCancel(ctx) + + ticker, err := s.futuresSession.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + log.WithError(err).Errorf("can not query ticker") return } From b7edc38dc795f40941a8f3c82c1a5678cc0d3040 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 13:14:59 +0800 Subject: [PATCH 26/49] xfunding: record pending transfer --- pkg/strategy/xfunding/strategy.go | 62 +++++++++++++++++++------------ 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 7f07d6bdf..b0654e79e 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -85,27 +85,30 @@ type Strategy struct { MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"` } `json:"supportDetection"` - ProfitStats *types.ProfitStats `persistence:"profit_stats"` - - SpotPosition *types.Position `persistence:"spot_position"` - FuturesPosition *types.Position `persistence:"futures_position"` - - spotSession, futuresSession *bbgo.ExchangeSession - - spotOrderExecutor, futuresOrderExecutor *bbgo.GeneralOrderExecutor - spotMarket, futuresMarket types.Market - SpotSession string `json:"spotSession"` FuturesSession string `json:"futuresSession"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + SpotPosition *types.Position `persistence:"spot_position"` + FuturesPosition *types.Position `persistence:"futures_position"` + + State *State `persistence:"state"` + + spotSession, futuresSession *bbgo.ExchangeSession + spotOrderExecutor, futuresOrderExecutor *bbgo.GeneralOrderExecutor + spotMarket, futuresMarket types.Market + // positionAction is default to NoOp positionAction PositionAction // positionType is the futures position type // currently we only support short position for the positive funding rate positionType types.PositionType +} - usedQuoteInvestment fixedpoint.Value +type State struct { + PendingBaseTransfer fixedpoint.Value `json:"pendingBaseTransfer"` + UsedQuoteInvestment fixedpoint.Value `json:"usedQuoteInvestment"` } func (s *Strategy) ID() string { @@ -198,8 +201,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error { instanceID := s.InstanceID() - s.usedQuoteInvestment = fixedpoint.Zero - s.spotSession = sessions[s.SpotSession] s.futuresSession = sessions[s.FuturesSession] @@ -234,6 +235,13 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.SpotPosition = types.NewPositionFromMarket(s.spotMarket) } + if s.State == nil { + s.State = &State{ + PendingBaseTransfer: fixedpoint.Zero, + UsedQuoteInvestment: fixedpoint.Zero, + } + } + binanceFutures := s.futuresSession.Exchange.(*binance.Exchange) binanceSpot := s.spotSession.Exchange.(*binance.Exchange) _ = binanceSpot @@ -255,8 +263,8 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } // TODO: add mutex lock for this modification - s.usedQuoteInvestment = s.usedQuoteInvestment.Add(trade.QuoteQuantity) - if s.usedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { + s.State.UsedQuoteInvestment = s.State.UsedQuoteInvestment.Add(trade.QuoteQuantity) + if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { s.positionAction = PositionNoOp } @@ -265,7 +273,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order if err := backoff.RetryGeneric(ctx, func() error { return s.transferIn(ctx, binanceSpot, trade) }); err != nil { - log.WithError(err).Errorf("transfer in retry failed") + log.WithError(err).Errorf("spot-to-futures transfer in retry failed") return } @@ -348,14 +356,20 @@ func (s *Strategy) transferIn(ctx context.Context, ex *binance.Exchange, trade t } // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here - if b.Available.Compare(trade.Quantity) >= 0 { - log.Infof("transfering futures account asset %s %s", trade.Quantity, currency) - if err := ex.TransferFuturesAccountAsset(ctx, currency, trade.Quantity, types.TransferIn); err != nil { - log.WithError(err).Errorf("spot-to-futures transfer error") - return err - } + if b.Available.Compare(trade.Quantity) < 0 { + log.Infof("adding to pending base transfer: %s %s", trade.Quantity, currency) + s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(trade.Quantity) + return nil } + amount := s.State.PendingBaseTransfer.Add(trade.Quantity) + + log.Infof("transfering futures account asset %s %s", amount, currency) + if err := ex.TransferFuturesAccountAsset(ctx, currency, amount, types.TransferIn); err != nil { + return err + } + + s.State.PendingBaseTransfer = fixedpoint.Zero return nil } @@ -470,11 +484,11 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { // TODO: compare with the futures position and reduce the position case PositionOpening: - if s.usedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { + if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { return } - leftQuote := s.QuoteInvestment.Sub(s.usedQuoteInvestment) + leftQuote := s.QuoteInvestment.Sub(s.State.UsedQuoteInvestment) orderPrice := ticker.Buy orderQuantity := fixedpoint.Min(s.IncrementalQuoteQuantity, leftQuote).Div(orderPrice) orderQuantity = fixedpoint.Max(orderQuantity, s.spotMarket.MinQuantity) From 8b87a8706b595bbdfb4011c467d728aa20cdb4d3 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 14:46:02 +0800 Subject: [PATCH 27/49] xfunding: add state for recording TotalBaseTransfer --- config/grid2-max.yaml | 3 ++- pkg/strategy/xfunding/strategy.go | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index 596d0069f..3c55294d8 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -11,7 +11,8 @@ notifications: sessions: max: exchange: max - envVarPrefix: max + envVarPrefix: MAX + # example command: # godotenv -f .env.local -- go run ./cmd/bbgo backtest --config config/grid2-max.yaml --base-asset-baseline diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index b0654e79e..977b2bdd1 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -108,6 +108,7 @@ type Strategy struct { type State struct { PendingBaseTransfer fixedpoint.Value `json:"pendingBaseTransfer"` + TotalBaseTransfer fixedpoint.Value `json:"totalBaseTransfer"` UsedQuoteInvestment fixedpoint.Value `json:"usedQuoteInvestment"` } @@ -238,6 +239,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order if s.State == nil { s.State = &State{ PendingBaseTransfer: fixedpoint.Zero, + TotalBaseTransfer: fixedpoint.Zero, UsedQuoteInvestment: fixedpoint.Zero, } } @@ -364,12 +366,25 @@ func (s *Strategy) transferIn(ctx context.Context, ex *binance.Exchange, trade t amount := s.State.PendingBaseTransfer.Add(trade.Quantity) + pos := s.SpotPosition.GetBase() + rest := pos.Sub(s.State.TotalBaseTransfer) + + if rest.Sign() < 0 { + return nil + } + + amount = fixedpoint.Min(rest, amount) + log.Infof("transfering futures account asset %s %s", amount, currency) if err := ex.TransferFuturesAccountAsset(ctx, currency, amount, types.TransferIn); err != nil { return err } + // reset pending transfer s.State.PendingBaseTransfer = fixedpoint.Zero + + // record the transfer in the total base transfer + s.State.TotalBaseTransfer = s.State.TotalBaseTransfer.Add(amount) return nil } From 44850e48e88a10e3db8b7c7fec37e644e9c763b3 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 14:48:24 +0800 Subject: [PATCH 28/49] xfunding: add mutex protection --- pkg/strategy/xfunding/strategy.go | 78 ++++++++++++++++--------------- 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 977b2bdd1..3be8fefdc 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "strings" + "sync" "time" "github.com/sirupsen/logrus" @@ -93,6 +94,7 @@ type Strategy struct { FuturesPosition *types.Position `persistence:"futures_position"` State *State `persistence:"state"` + mu sync.Mutex spotSession, futuresSession *bbgo.ExchangeSession spotOrderExecutor, futuresOrderExecutor *bbgo.GeneralOrderExecutor @@ -264,7 +266,9 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return } - // TODO: add mutex lock for this modification + s.mu.Lock() + defer s.mu.Unlock() + s.State.UsedQuoteInvestment = s.State.UsedQuoteInvestment.Add(trade.QuoteQuantity) if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { s.positionAction = PositionNoOp @@ -492,43 +496,43 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { log.Errorf("funding long position type is not supported") return } - - switch s.positionAction { - - case PositionClosing: - // TODO: compare with the futures position and reduce the position - - case PositionOpening: - if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { - return - } - - leftQuote := s.QuoteInvestment.Sub(s.State.UsedQuoteInvestment) - orderPrice := ticker.Buy - orderQuantity := fixedpoint.Min(s.IncrementalQuoteQuantity, leftQuote).Div(orderPrice) - orderQuantity = fixedpoint.Max(orderQuantity, s.spotMarket.MinQuantity) - - _ = s.spotOrderExecutor.GracefulCancel(ctx) - - submitOrder := types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Type: types.OrderTypeLimitMaker, - Quantity: orderQuantity, - Price: orderPrice, - Market: s.spotMarket, - } - - log.Infof("placing spot order: %+v", submitOrder) - - createdOrders, err := s.spotOrderExecutor.SubmitOrders(ctx, submitOrder) - if err != nil { - log.WithError(err).Errorf("can not submit order") - return - } - - log.Infof("created orders: %+v", createdOrders) + if s.positionAction != PositionOpening { + return } + + s.mu.Lock() + defer s.mu.Unlock() + + if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { + return + } + + leftQuota := s.QuoteInvestment.Sub(s.State.UsedQuoteInvestment) + + orderPrice := ticker.Buy + orderQuantity := fixedpoint.Min(s.IncrementalQuoteQuantity, leftQuota).Div(orderPrice) + orderQuantity = fixedpoint.Max(orderQuantity, s.spotMarket.MinQuantity) + + _ = s.spotOrderExecutor.GracefulCancel(ctx) + + submitOrder := types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Quantity: orderQuantity, + Price: orderPrice, + Market: s.spotMarket, + } + + log.Infof("placing spot order: %+v", submitOrder) + + createdOrders, err := s.spotOrderExecutor.SubmitOrders(ctx, submitOrder) + if err != nil { + log.WithError(err).Errorf("can not submit order") + return + } + + log.Infof("created orders: %+v", createdOrders) } func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed bool) { From 018e281627800890401cc3cb7ee4006d4cff0913 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 16:14:30 +0800 Subject: [PATCH 29/49] types: add AdjustQuantityByMinNotional to types.Market --- pkg/types/market.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pkg/types/market.go b/pkg/types/market.go index b7a44db92..6417cf79a 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -136,6 +136,18 @@ func (m Market) CanonicalizeVolume(val fixedpoint.Value) float64 { return math.Trunc(p*val.Float64()) / p } +// AdjustQuantityByMinNotional adjusts the quantity to make the amount greater than the given minAmount +func (m Market) AdjustQuantityByMinNotional(quantity, currentPrice fixedpoint.Value) fixedpoint.Value { + // modify quantity for the min amount + amount := currentPrice.Mul(quantity) + if amount.Compare(m.MinNotional) < 0 { + ratio := m.MinNotional.Div(amount) + return quantity.Mul(ratio) + } + + return quantity +} + type MarketMap map[string]Market func (m MarketMap) Add(market Market) { From c3ca5b75acd4fa9913d815df5c6c491e4978667c Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 16:47:57 +0800 Subject: [PATCH 30/49] types: add minNotionalSealant to adjust quantity method --- pkg/types/market.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/types/market.go b/pkg/types/market.go index 6417cf79a..87eaa6301 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -136,12 +136,14 @@ func (m Market) CanonicalizeVolume(val fixedpoint.Value) float64 { return math.Trunc(p*val.Float64()) / p } +var minNotionalSealant = fixedpoint.NewFromFloat(1.0001) + // AdjustQuantityByMinNotional adjusts the quantity to make the amount greater than the given minAmount func (m Market) AdjustQuantityByMinNotional(quantity, currentPrice fixedpoint.Value) fixedpoint.Value { // modify quantity for the min amount amount := currentPrice.Mul(quantity) if amount.Compare(m.MinNotional) < 0 { - ratio := m.MinNotional.Div(amount) + ratio := m.MinNotional.Mul(minNotionalSealant).Div(amount) return quantity.Mul(ratio) } From 02c28a07cc3cd7aac3b300a61c2da49f3d259409 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 17:35:54 +0800 Subject: [PATCH 31/49] types: fix AdjustQuantityByMinNotional by round up the quantity --- pkg/types/market.go | 21 +++++++++++++---- pkg/types/market_test.go | 50 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/pkg/types/market.go b/pkg/types/market.go index 87eaa6301..99b2f9efb 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -2,6 +2,7 @@ package types import ( "math" + "strconv" "github.com/leekchan/accounting" @@ -59,7 +60,14 @@ func (m Market) IsDustQuantity(quantity, price fixedpoint.Value) bool { // TruncateQuantity uses the step size to truncate floating number, in order to avoid the rounding issue func (m Market) TruncateQuantity(quantity fixedpoint.Value) fixedpoint.Value { - return fixedpoint.MustNewFromString(m.FormatQuantity(quantity)) + var ts = m.StepSize.Float64() + var prec = int(math.Round(math.Log10(ts) * -1.0)) + var pow10 = math.Pow10(prec) + + qf := math.Trunc(quantity.Float64() * pow10) + qf = qf / pow10 + qs := strconv.FormatFloat(qf, 'f', prec, 64) + return fixedpoint.MustNewFromString(qs) } func (m Market) TruncatePrice(price fixedpoint.Value) fixedpoint.Value { @@ -136,15 +144,18 @@ func (m Market) CanonicalizeVolume(val fixedpoint.Value) float64 { return math.Trunc(p*val.Float64()) / p } -var minNotionalSealant = fixedpoint.NewFromFloat(1.0001) - // AdjustQuantityByMinNotional adjusts the quantity to make the amount greater than the given minAmount func (m Market) AdjustQuantityByMinNotional(quantity, currentPrice fixedpoint.Value) fixedpoint.Value { // modify quantity for the min amount + quantity = m.TruncateQuantity(quantity) amount := currentPrice.Mul(quantity) if amount.Compare(m.MinNotional) < 0 { - ratio := m.MinNotional.Mul(minNotionalSealant).Div(amount) - return quantity.Mul(ratio) + ratio := m.MinNotional.Div(amount) + quantity = quantity.Mul(ratio) + + ts := m.StepSize.Float64() + prec := int(math.Round(math.Log10(ts) * -1.0)) + return quantity.Round(prec, fixedpoint.Up) } return quantity diff --git a/pkg/types/market_test.go b/pkg/types/market_test.go index 809e60b0d..493c3a21d 100644 --- a/pkg/types/market_test.go +++ b/pkg/types/market_test.go @@ -191,3 +191,53 @@ func Test_formatQuantity(t *testing.T) { }) } } + +func TestMarket_TruncateQuantity(t *testing.T) { + market := Market{ + StepSize: fixedpoint.NewFromFloat(0.0001), + } + + testCases := []struct { + input string + expect string + }{ + {"0.00573961", "0.0057"}, + {"0.00579961", "0.0057"}, + {"0.0057", "0.0057"}, + } + + for _, testCase := range testCases { + q := fixedpoint.MustNewFromString(testCase.input) + q2 := market.TruncateQuantity(q) + assert.Equalf(t, testCase.expect, q2.String(), "input: %s stepSize: %s", testCase.input, market.StepSize.String()) + } + +} + +func TestMarket_AdjustQuantityByMinNotional(t *testing.T) { + + market := Market{ + Symbol: "ETHUSDT", + StepSize: fixedpoint.NewFromFloat(0.0001), + MinQuantity: fixedpoint.NewFromFloat(0.0001), + MinNotional: fixedpoint.NewFromFloat(10.0), + VolumePrecision: 8, + PricePrecision: 2, + } + + // Quantity:0.00573961 Price:1750.99 + testCases := []struct { + input string + expect string + }{ + {"0.00573961", "0.0058"}, + } + + price := fixedpoint.NewFromFloat(1750.99) + for _, testCase := range testCases { + q := fixedpoint.MustNewFromString(testCase.input) + q2 := market.AdjustQuantityByMinNotional(q, price) + assert.Equalf(t, testCase.expect, q2.String(), "input: %s stepSize: %s", testCase.input, market.StepSize.String()) + assert.False(t, market.IsDustQuantity(q2, price)) + } +} From 1b5126c9a1d6ff858f66c40e64c74fcfbd874644 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 17:36:30 +0800 Subject: [PATCH 32/49] xfunding: add mutex --- config/xfunding.yaml | 6 ++++ pkg/bbgo/order_processor.go | 2 +- pkg/strategy/xfunding/strategy.go | 54 +++++++++++++++++++++++-------- 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/config/xfunding.yaml b/config/xfunding.yaml index 0714a91f9..b39712659 100644 --- a/config/xfunding.yaml +++ b/config/xfunding.yaml @@ -9,6 +9,12 @@ notifications: orderUpdate: true submitOrder: true +persistence: + redis: + host: 127.0.0.1 + port: 6379 + db: 1 + sessions: binance: exchange: binance diff --git a/pkg/bbgo/order_processor.go b/pkg/bbgo/order_processor.go index edf3844f1..0a5a06d7a 100644 --- a/pkg/bbgo/order_processor.go +++ b/pkg/bbgo/order_processor.go @@ -33,7 +33,7 @@ func AdjustQuantityByMinAmount(quantity, currentPrice, minAmount fixedpoint.Valu amount := currentPrice.Mul(quantity) if amount.Compare(minAmount) < 0 { ratio := minAmount.Div(amount) - quantity = quantity.Mul(ratio) + return quantity.Mul(ratio) } return quantity diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 3be8fefdc..1f8163d1b 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -94,7 +94,9 @@ type Strategy struct { FuturesPosition *types.Position `persistence:"futures_position"` State *State `persistence:"state"` - mu sync.Mutex + + // mu is used for locking state + mu sync.Mutex spotSession, futuresSession *bbgo.ExchangeSession spotOrderExecutor, futuresOrderExecutor *bbgo.GeneralOrderExecutor @@ -246,6 +248,9 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } } + log.Infof("loaded spot position: %s", s.SpotPosition.String()) + log.Infof("loaded futures position: %s", s.FuturesPosition.String()) + binanceFutures := s.futuresSession.Exchange.(*binance.Exchange) binanceSpot := s.spotSession.Exchange.(*binance.Exchange) _ = binanceSpot @@ -428,7 +433,7 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { spotBase := s.SpotPosition.GetBase() // should be positive base quantity here futuresBase := s.FuturesPosition.GetBase() // should be negative base quantity here - if spotBase.IsZero() { + if spotBase.IsZero() || spotBase.Sign() < 0 { // skip when spot base is zero return } @@ -449,14 +454,25 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { } log.Infof("calculated futures account quote value = %s", quoteValue.String()) - if spotBase.Sign() > 0 && futuresBase.Neg().Compare(spotBase) < 0 { + // max futures base position (without negative sign) + maxFuturesBasePosition := fixedpoint.Min( + spotBase.Mul(s.Leverage), + s.State.TotalBaseTransfer.Mul(s.Leverage)) + + // if - futures position < max futures position, increase it + if futuresBase.Neg().Compare(maxFuturesBasePosition) < 0 { orderPrice := ticker.Sell - diffQuantity := spotBase.Sub(futuresBase.Neg().Mul(s.Leverage)) + diffQuantity := maxFuturesBasePosition.Sub(futuresBase.Neg()) + + if diffQuantity.Sign() < 0 { + log.Errorf("unexpected negative position diff: %s", diffQuantity.String()) + return + } log.Infof("position diff quantity: %s", diffQuantity.String()) orderQuantity := fixedpoint.Max(diffQuantity, s.futuresMarket.MinQuantity) - orderQuantity = bbgo.AdjustQuantityByMinAmount(orderQuantity, orderPrice, s.futuresMarket.MinNotional) + orderQuantity = s.futuresMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) return @@ -486,12 +502,6 @@ func (s *Strategy) syncSpotPosition(ctx context.Context) { } func (s *Strategy) increaseSpotPosition(ctx context.Context) { - ticker, err := s.spotSession.Exchange.QueryTicker(ctx, s.Symbol) - if err != nil { - log.WithError(err).Errorf("can not query ticker") - return - } - if s.positionType != types.PositionShort { log.Errorf("funding long position type is not supported") return @@ -507,13 +517,27 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { return } + _ = s.spotOrderExecutor.GracefulCancel(ctx) + + ticker, err := s.spotSession.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + log.WithError(err).Errorf("can not query ticker") + return + } + leftQuota := s.QuoteInvestment.Sub(s.State.UsedQuoteInvestment) orderPrice := ticker.Buy orderQuantity := fixedpoint.Min(s.IncrementalQuoteQuantity, leftQuota).Div(orderPrice) - orderQuantity = fixedpoint.Max(orderQuantity, s.spotMarket.MinQuantity) - _ = s.spotOrderExecutor.GracefulCancel(ctx) + log.Infof("initial spot order quantity %s", orderQuantity.String()) + + orderQuantity = fixedpoint.Max(orderQuantity, s.spotMarket.MinQuantity) + orderQuantity = s.spotMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) + + if s.spotMarket.IsDustQuantity(orderQuantity, orderPrice) { + return + } submitOrder := types.SubmitOrder{ Symbol: s.Symbol, @@ -542,6 +566,10 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed if s.ShortFundingRate != nil { if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { + + log.Infof("funding rate %s is higher than the High threshold %s, start opening position...", + fundingRate.Percentage(), s.ShortFundingRate.High.Percentage()) + s.positionAction = PositionOpening s.positionType = types.PositionShort changed = true From 7ba7eb8be7315e20288a903a345e031fde9c811d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 18:09:16 +0800 Subject: [PATCH 33/49] xfunding: implement reduceFuturesPosition --- pkg/strategy/xfunding/strategy.go | 68 +++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 1f8163d1b..b4f93456a 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -408,7 +408,57 @@ func (s *Strategy) triggerPositionAction(ctx context.Context) { } } -func (s *Strategy) reduceFuturesPosition(ctx context.Context) {} +func (s *Strategy) reduceFuturesPosition(ctx context.Context) { + switch s.positionAction { + case PositionOpening, PositionNoOp: + return + } + + futuresBase := s.FuturesPosition.GetBase() // should be negative base quantity here + + if futuresBase.Sign() > 0 { + // unexpected error + log.Errorf("unexpected futures position (got positive, expecting negative)") + return + } + + _ = s.futuresOrderExecutor.GracefulCancel(ctx) + + ticker, err := s.futuresSession.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + log.WithError(err).Errorf("can not query ticker") + return + } + + if futuresBase.Compare(fixedpoint.Zero) < 0 { + orderPrice := ticker.Sell + + orderQuantity := futuresBase.Abs() + orderQuantity = fixedpoint.Max(orderQuantity, s.futuresMarket.MinQuantity) + orderQuantity = s.futuresMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) + if s.futuresMarket.IsDustQuantity(orderQuantity, orderPrice) { + log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.futuresMarket) + return + } + + createdOrders, err := s.futuresOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Quantity: orderQuantity, + Price: orderPrice, + Market: s.futuresMarket, + ReduceOnly: true, + }) + + if err != nil { + log.WithError(err).Errorf("can not submit order") + return + } + + log.Infof("created orders: %+v", createdOrders) + } +} // syncFuturesPosition syncs the futures position with the given spot position func (s *Strategy) syncFuturesPosition(ctx context.Context) { @@ -422,14 +472,6 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { case PositionOpening, PositionNoOp: } - _ = s.futuresOrderExecutor.GracefulCancel(ctx) - - ticker, err := s.futuresSession.Exchange.QueryTicker(ctx, s.Symbol) - if err != nil { - log.WithError(err).Errorf("can not query ticker") - return - } - spotBase := s.SpotPosition.GetBase() // should be positive base quantity here futuresBase := s.FuturesPosition.GetBase() // should be negative base quantity here @@ -446,6 +488,14 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { return } + _ = s.futuresOrderExecutor.GracefulCancel(ctx) + + ticker, err := s.futuresSession.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + log.WithError(err).Errorf("can not query ticker") + return + } + // compare with the spot position and increase the position quoteValue, err := bbgo.CalculateQuoteQuantity(ctx, s.futuresSession, s.futuresMarket.QuoteCurrency, s.Leverage) if err != nil { From b5f69e7f45d25b7553609f7c4dbbd6477658919b Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 18:18:30 +0800 Subject: [PATCH 34/49] xfunding: reset stats when direction changed --- pkg/strategy/xfunding/strategy.go | 58 +++------------- pkg/strategy/xfunding/transfer.go | 112 ++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 50 deletions(-) create mode 100644 pkg/strategy/xfunding/transfer.go diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index b4f93456a..c1ee02ded 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -347,56 +347,6 @@ func (s *Strategy) queryAndDetectPremiumIndex(ctx context.Context, binanceFuture } } -// TODO: replace type binance.Exchange with an interface -func (s *Strategy) transferIn(ctx context.Context, ex *binance.Exchange, trade types.Trade) error { - currency := s.spotMarket.BaseCurrency - - // base asset needs BUY trades - if trade.Side == types.SideTypeSell { - return nil - } - - balances, err := ex.QueryAccountBalances(ctx) - if err != nil { - return err - } - - b, ok := balances[currency] - if !ok { - return fmt.Errorf("%s balance not found", currency) - } - - // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here - if b.Available.Compare(trade.Quantity) < 0 { - log.Infof("adding to pending base transfer: %s %s", trade.Quantity, currency) - s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(trade.Quantity) - return nil - } - - amount := s.State.PendingBaseTransfer.Add(trade.Quantity) - - pos := s.SpotPosition.GetBase() - rest := pos.Sub(s.State.TotalBaseTransfer) - - if rest.Sign() < 0 { - return nil - } - - amount = fixedpoint.Min(rest, amount) - - log.Infof("transfering futures account asset %s %s", amount, currency) - if err := ex.TransferFuturesAccountAsset(ctx, currency, amount, types.TransferIn); err != nil { - return err - } - - // reset pending transfer - s.State.PendingBaseTransfer = fixedpoint.Zero - - // record the transfer in the total base transfer - s.State.TotalBaseTransfer = s.State.TotalBaseTransfer.Add(amount) - return nil -} - func (s *Strategy) triggerPositionAction(ctx context.Context) { switch s.positionAction { case PositionOpening: @@ -622,9 +572,17 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed s.positionAction = PositionOpening s.positionType = types.PositionShort + + // reset the transfer stats + s.State.PendingBaseTransfer = fixedpoint.Zero + s.State.TotalBaseTransfer = fixedpoint.Zero changed = true } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { s.positionAction = PositionClosing + + // reset the transfer stats + s.State.PendingBaseTransfer = fixedpoint.Zero + s.State.TotalBaseTransfer = fixedpoint.Zero changed = true } } diff --git a/pkg/strategy/xfunding/transfer.go b/pkg/strategy/xfunding/transfer.go new file mode 100644 index 000000000..00a34afdc --- /dev/null +++ b/pkg/strategy/xfunding/transfer.go @@ -0,0 +1,112 @@ +package xfunding + +import ( + "context" + "fmt" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type FuturesTransfer interface { + TransferFuturesAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io types.TransferDirection) error + QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) +} + +func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, trade types.Trade) error { + currency := s.spotMarket.BaseCurrency + + // base asset needs BUY trades + if trade.Side == types.SideTypeBuy { + return nil + } + + balances, err := ex.QueryAccountBalances(ctx) + if err != nil { + return err + } + + b, ok := balances[currency] + if !ok { + return fmt.Errorf("%s balance not found", currency) + } + + // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here + if b.Available.Compare(trade.Quantity) < 0 { + log.Infof("adding to pending base transfer: %s %s", trade.Quantity, currency) + s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(trade.Quantity) + return nil + } + + amount := s.State.PendingBaseTransfer.Add(trade.Quantity) + + pos := s.SpotPosition.GetBase() + rest := pos.Sub(s.State.TotalBaseTransfer) + + if rest.Sign() < 0 { + return nil + } + + amount = fixedpoint.Min(rest, amount) + + log.Infof("transfering out futures account asset %s %s", amount, currency) + if err := ex.TransferFuturesAccountAsset(ctx, currency, amount, types.TransferOut); err != nil { + return err + } + + // reset pending transfer + s.State.PendingBaseTransfer = fixedpoint.Zero + + // record the transfer in the total base transfer + s.State.TotalBaseTransfer = s.State.TotalBaseTransfer.Add(amount) + return nil +} + +func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, trade types.Trade) error { + currency := s.spotMarket.BaseCurrency + + // base asset needs BUY trades + if trade.Side == types.SideTypeSell { + return nil + } + + balances, err := ex.QueryAccountBalances(ctx) + if err != nil { + return err + } + + b, ok := balances[currency] + if !ok { + return fmt.Errorf("%s balance not found", currency) + } + + // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here + if b.Available.Compare(trade.Quantity) < 0 { + log.Infof("adding to pending base transfer: %s %s", trade.Quantity, currency) + s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(trade.Quantity) + return nil + } + + amount := s.State.PendingBaseTransfer.Add(trade.Quantity) + + pos := s.SpotPosition.GetBase() + rest := pos.Sub(s.State.TotalBaseTransfer) + + if rest.Sign() < 0 { + return nil + } + + amount = fixedpoint.Min(rest, amount) + + log.Infof("transfering in futures account asset %s %s", amount, currency) + if err := ex.TransferFuturesAccountAsset(ctx, currency, amount, types.TransferIn); err != nil { + return err + } + + // reset pending transfer + s.State.PendingBaseTransfer = fixedpoint.Zero + + // record the transfer in the total base transfer + s.State.TotalBaseTransfer = s.State.TotalBaseTransfer.Add(amount) + return nil +} From a933f90cc877ae55a188872570929f8ae2a79d4d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 18:19:30 +0800 Subject: [PATCH 35/49] xfunding: log low funding fee --- pkg/strategy/xfunding/strategy.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index c1ee02ded..369413926 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -578,6 +578,10 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed s.State.TotalBaseTransfer = fixedpoint.Zero changed = true } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { + + log.Infof("funding rate %s is lower than the Low threshold %s, start closing position...", + fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage()) + s.positionAction = PositionClosing // reset the transfer stats From aba80398d924e56294a2763b1c8492798418ff7a Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 22:36:35 +0800 Subject: [PATCH 36/49] xfunding: add MinHoldingPeriod support --- pkg/strategy/xfunding/strategy.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 369413926..c8d284676 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -57,6 +57,8 @@ type Strategy struct { QuoteInvestment fixedpoint.Value `json:"quoteInvestment"` + MinHoldingPeriod types.Duration `json:"minHoldingPeriod"` + // ShortFundingRate is the funding rate range for short positions // TODO: right now we don't support negative funding rate (long position) since it's rarer ShortFundingRate *struct { @@ -111,6 +113,7 @@ type Strategy struct { } type State struct { + PositionStartTime time.Time `json:"positionStartTime"` PendingBaseTransfer fixedpoint.Value `json:"pendingBaseTransfer"` TotalBaseTransfer fixedpoint.Value `json:"totalBaseTransfer"` UsedQuoteInvestment fixedpoint.Value `json:"usedQuoteInvestment"` @@ -146,7 +149,14 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } func (s *Strategy) Defaults() error { - s.Leverage = fixedpoint.One + if s.Leverage.IsZero() { + s.Leverage = fixedpoint.One + } + + if s.MinHoldingPeriod == 0 { + s.MinHoldingPeriod = types.Duration(3 * 24 * time.Hour) + } + return nil } @@ -574,6 +584,7 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed s.positionType = types.PositionShort // reset the transfer stats + s.State.PositionStartTime = premiumIndex.Time s.State.PendingBaseTransfer = fixedpoint.Zero s.State.TotalBaseTransfer = fixedpoint.Zero changed = true @@ -582,6 +593,12 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed log.Infof("funding rate %s is lower than the Low threshold %s, start closing position...", fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage()) + holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime) + if holdingPeriod < time.Duration(s.MinHoldingPeriod) { + log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod) + return + } + s.positionAction = PositionClosing // reset the transfer stats From 3624dd03389c2c26a579fa7ad2774b3ed66ec43d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 22:54:42 +0800 Subject: [PATCH 37/49] xfunding: implement close position transfer --- pkg/strategy/xfunding/strategy.go | 136 +++++++++++--------- pkg/strategy/xfunding/transfer.go | 39 +++--- pkg/util/backoff/{generic.go => general.go} | 2 +- 3 files changed, 100 insertions(+), 77 deletions(-) rename pkg/util/backoff/{generic.go => general.go} (80%) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index c8d284676..b11517705 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -273,45 +273,60 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order // // when closing a position, we place orders on the futures account first, then the spot account // we need to close the position according to its base quantity instead of quote quantity - if s.positionType == types.PositionShort { - switch s.positionAction { - case PositionOpening: - if trade.Side != types.SideTypeBuy { - log.Errorf("unexpected trade side: %+v, expecting BUY trade", trade) - return - } - - s.mu.Lock() - defer s.mu.Unlock() - - s.State.UsedQuoteInvestment = s.State.UsedQuoteInvestment.Add(trade.QuoteQuantity) - if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { - s.positionAction = PositionNoOp - } - - // 1) if we have trade, try to query the balance and transfer the balance to the futures wallet account - // TODO: handle missing trades here. If the process crashed during the transfer, how to recover? - if err := backoff.RetryGeneric(ctx, func() error { - return s.transferIn(ctx, binanceSpot, trade) - }); err != nil { - log.WithError(err).Errorf("spot-to-futures transfer in retry failed") - return - } - - // 2) transferred successfully, sync futures position - // compare spot position and futures position, increase the position size until they are the same size - - case PositionClosing: - if trade.Side != types.SideTypeSell { - log.Errorf("unexpected trade side: %+v, expecting SELL trade", trade) - return - } + if s.positionType != types.PositionShort { + return + } + switch s.positionAction { + case PositionOpening: + if trade.Side != types.SideTypeBuy { + log.Errorf("unexpected trade side: %+v, expecting BUY trade", trade) + return } + + s.mu.Lock() + defer s.mu.Unlock() + + s.State.UsedQuoteInvestment = s.State.UsedQuoteInvestment.Add(trade.QuoteQuantity) + if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { + s.positionAction = PositionNoOp + } + + // if we have trade, try to query the balance and transfer the balance to the futures wallet account + // TODO: handle missing trades here. If the process crashed during the transfer, how to recover? + if err := backoff.RetryGeneral(ctx, func() error { + return s.transferIn(ctx, binanceSpot, s.spotMarket.BaseCurrency, trade) + }); err != nil { + log.WithError(err).Errorf("spot-to-futures transfer in retry failed") + return + } + + case PositionClosing: + if trade.Side != types.SideTypeSell { + log.Errorf("unexpected trade side: %+v, expecting SELL trade", trade) + return + } + } }) s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition) + s.futuresOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + if s.positionType != types.PositionShort { + return + } + + switch s.positionAction { + case PositionClosing: + if err := backoff.RetryGeneral(ctx, func() error { + return s.transferOut(ctx, binanceSpot, s.spotMarket.BaseCurrency, trade) + }); err != nil { + log.WithError(err).Errorf("spot-to-futures transfer in retry failed") + return + } + + } + }) s.futuresSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { // s.queryAndDetectPremiumIndex(ctx, binanceFutures) @@ -421,6 +436,8 @@ func (s *Strategy) reduceFuturesPosition(ctx context.Context) { } // syncFuturesPosition syncs the futures position with the given spot position +// when the spot is transferred successfully, sync futures position +// compare spot position and futures position, increase the position size until they are the same size func (s *Strategy) syncFuturesPosition(ctx context.Context) { if s.positionType != types.PositionShort { return @@ -495,7 +512,6 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { Quantity: orderQuantity, Price: orderPrice, Market: s.futuresMarket, - // TimeInForce: types.TimeInForceGTC, }) if err != nil { @@ -574,38 +590,40 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed log.Infof("last %s funding rate: %s", s.Symbol, fundingRate.Percentage()) - if s.ShortFundingRate != nil { - if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { + if s.ShortFundingRate == nil { + return changed + } - log.Infof("funding rate %s is higher than the High threshold %s, start opening position...", - fundingRate.Percentage(), s.ShortFundingRate.High.Percentage()) + if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { - s.positionAction = PositionOpening - s.positionType = types.PositionShort + log.Infof("funding rate %s is higher than the High threshold %s, start opening position...", + fundingRate.Percentage(), s.ShortFundingRate.High.Percentage()) - // reset the transfer stats - s.State.PositionStartTime = premiumIndex.Time - s.State.PendingBaseTransfer = fixedpoint.Zero - s.State.TotalBaseTransfer = fixedpoint.Zero - changed = true - } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { + s.positionAction = PositionOpening + s.positionType = types.PositionShort - log.Infof("funding rate %s is lower than the Low threshold %s, start closing position...", - fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage()) + // reset the transfer stats + s.State.PositionStartTime = premiumIndex.Time + s.State.PendingBaseTransfer = fixedpoint.Zero + s.State.TotalBaseTransfer = fixedpoint.Zero + changed = true + } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { - holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime) - if holdingPeriod < time.Duration(s.MinHoldingPeriod) { - log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod) - return - } + log.Infof("funding rate %s is lower than the Low threshold %s, start closing position...", + fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage()) - s.positionAction = PositionClosing - - // reset the transfer stats - s.State.PendingBaseTransfer = fixedpoint.Zero - s.State.TotalBaseTransfer = fixedpoint.Zero - changed = true + holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime) + if holdingPeriod < time.Duration(s.MinHoldingPeriod) { + log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod) + return } + + s.positionAction = PositionClosing + + // reset the transfer stats + s.State.PendingBaseTransfer = fixedpoint.Zero + s.State.TotalBaseTransfer = fixedpoint.Zero + changed = true } return changed diff --git a/pkg/strategy/xfunding/transfer.go b/pkg/strategy/xfunding/transfer.go index 00a34afdc..14e5abb9d 100644 --- a/pkg/strategy/xfunding/transfer.go +++ b/pkg/strategy/xfunding/transfer.go @@ -13,15 +13,13 @@ type FuturesTransfer interface { QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) } -func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, trade types.Trade) error { - currency := s.spotMarket.BaseCurrency - +func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, currency string, trade types.Trade) error { // base asset needs BUY trades if trade.Side == types.SideTypeBuy { return nil } - balances, err := ex.QueryAccountBalances(ctx) + balances, err := s.futuresSession.Exchange.QueryAccountBalances(ctx) if err != nil { return err } @@ -31,16 +29,23 @@ func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, trade ty return fmt.Errorf("%s balance not found", currency) } + quantity := trade.Quantity + + if s.Leverage.Compare(fixedpoint.One) > 0 { + // de-leverage and get the collateral base quantity for transfer + quantity = quantity.Div(s.Leverage) + } + // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here - if b.Available.Compare(trade.Quantity) < 0 { - log.Infof("adding to pending base transfer: %s %s", trade.Quantity, currency) - s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(trade.Quantity) + if b.Available.IsZero() || b.Available.Compare(quantity) < 0 { + log.Infof("adding to pending base transfer: %s %s", quantity, currency) + s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return nil } - amount := s.State.PendingBaseTransfer.Add(trade.Quantity) + amount := s.State.PendingBaseTransfer.Add(quantity) - pos := s.SpotPosition.GetBase() + pos := s.FuturesPosition.GetBase().Abs().Div(s.Leverage) rest := pos.Sub(s.State.TotalBaseTransfer) if rest.Sign() < 0 { @@ -62,15 +67,14 @@ func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, trade ty return nil } -func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, trade types.Trade) error { - currency := s.spotMarket.BaseCurrency +func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, currency string, trade types.Trade) error { // base asset needs BUY trades if trade.Side == types.SideTypeSell { return nil } - balances, err := ex.QueryAccountBalances(ctx) + balances, err := s.spotSession.Exchange.QueryAccountBalances(ctx) if err != nil { return err } @@ -81,15 +85,16 @@ func (s *Strategy) transferIn(ctx context.Context, ex FuturesTransfer, trade typ } // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here - if b.Available.Compare(trade.Quantity) < 0 { - log.Infof("adding to pending base transfer: %s %s", trade.Quantity, currency) - s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(trade.Quantity) + quantity := trade.Quantity + if b.Available.Compare(quantity) < 0 { + log.Infof("adding to pending base transfer: %s %s", quantity, currency) + s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return nil } - amount := s.State.PendingBaseTransfer.Add(trade.Quantity) + amount := s.State.PendingBaseTransfer.Add(quantity) - pos := s.SpotPosition.GetBase() + pos := s.SpotPosition.GetBase().Abs() rest := pos.Sub(s.State.TotalBaseTransfer) if rest.Sign() < 0 { diff --git a/pkg/util/backoff/generic.go b/pkg/util/backoff/general.go similarity index 80% rename from pkg/util/backoff/generic.go rename to pkg/util/backoff/general.go index f7303d336..968acd8ef 100644 --- a/pkg/util/backoff/generic.go +++ b/pkg/util/backoff/general.go @@ -8,7 +8,7 @@ import ( var MaxRetries uint64 = 101 -func RetryGeneric(ctx context.Context, op backoff.Operation) (err error) { +func RetryGeneral(ctx context.Context, op backoff.Operation) (err error) { err = backoff.Retry(op, backoff.WithContext( backoff.WithMaxRetries( backoff.NewExponentialBackOff(), From e016892a7016af91418830e1da42adc84da27c7f Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 22:57:13 +0800 Subject: [PATCH 38/49] xfunding: pull out startClosingPosition, startOpeningPosition method --- pkg/strategy/xfunding/strategy.go | 32 +++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index b11517705..932a97d00 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -599,13 +599,7 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed log.Infof("funding rate %s is higher than the High threshold %s, start opening position...", fundingRate.Percentage(), s.ShortFundingRate.High.Percentage()) - s.positionAction = PositionOpening - s.positionType = types.PositionShort - - // reset the transfer stats - s.State.PositionStartTime = premiumIndex.Time - s.State.PendingBaseTransfer = fixedpoint.Zero - s.State.TotalBaseTransfer = fixedpoint.Zero + s.startOpeningPosition(types.PositionShort, premiumIndex.Time) changed = true } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { @@ -618,17 +612,31 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed return } - s.positionAction = PositionClosing - - // reset the transfer stats - s.State.PendingBaseTransfer = fixedpoint.Zero - s.State.TotalBaseTransfer = fixedpoint.Zero + s.startClosingPosition() changed = true } return changed } +func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { + s.positionAction = PositionOpening + s.positionType = pt + + // reset the transfer stats + s.State.PositionStartTime = t + s.State.PendingBaseTransfer = fixedpoint.Zero + s.State.TotalBaseTransfer = fixedpoint.Zero +} + +func (s *Strategy) startClosingPosition() { + s.positionAction = PositionClosing + + // reset the transfer stats + s.State.PendingBaseTransfer = fixedpoint.Zero + s.State.TotalBaseTransfer = fixedpoint.Zero +} + func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.ExchangeSession, instanceID string, position *types.Position) *bbgo.GeneralOrderExecutor { orderExecutor := bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, position) orderExecutor.SetMaxRetries(0) From 108bb5deeb68fa21414432e447e2bad66e93b63a Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 23 Mar 2023 22:58:42 +0800 Subject: [PATCH 39/49] xfunding: add guard condition for starting and stopping --- pkg/strategy/xfunding/strategy.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 932a97d00..3590564ad 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -620,6 +620,10 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed } func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { + if s.positionAction == PositionOpening { + return + } + s.positionAction = PositionOpening s.positionType = pt @@ -630,6 +634,10 @@ func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { } func (s *Strategy) startClosingPosition() { + if s.positionAction == PositionClosing { + return + } + s.positionAction = PositionClosing // reset the transfer stats From c1fbbbe4000062184233c8d9ffc39f5d56fb764a Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 00:36:28 +0800 Subject: [PATCH 40/49] xfunding: move position state to state struct --- .../xfunding/positionaction_string.go | 25 --------- pkg/strategy/xfunding/positionstate_string.go | 25 +++++++++ pkg/strategy/xfunding/strategy.go | 52 ++++++++++++------- 3 files changed, 58 insertions(+), 44 deletions(-) delete mode 100644 pkg/strategy/xfunding/positionaction_string.go create mode 100644 pkg/strategy/xfunding/positionstate_string.go diff --git a/pkg/strategy/xfunding/positionaction_string.go b/pkg/strategy/xfunding/positionaction_string.go deleted file mode 100644 index 6aba4acf8..000000000 --- a/pkg/strategy/xfunding/positionaction_string.go +++ /dev/null @@ -1,25 +0,0 @@ -// Code generated by "stringer -type=PositionAction"; DO NOT EDIT. - -package xfunding - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[PositionNoOp-0] - _ = x[PositionOpening-1] - _ = x[PositionClosing-2] -} - -const _PositionAction_name = "PositionNoOpPositionOpeningPositionClosing" - -var _PositionAction_index = [...]uint8{0, 12, 27, 42} - -func (i PositionAction) String() string { - if i < 0 || i >= PositionAction(len(_PositionAction_index)-1) { - return "PositionAction(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _PositionAction_name[_PositionAction_index[i]:_PositionAction_index[i+1]] -} diff --git a/pkg/strategy/xfunding/positionstate_string.go b/pkg/strategy/xfunding/positionstate_string.go new file mode 100644 index 000000000..c227a848b --- /dev/null +++ b/pkg/strategy/xfunding/positionstate_string.go @@ -0,0 +1,25 @@ +// Code generated by "stringer -type=PositionState"; DO NOT EDIT. + +package xfunding + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[PositionNoOp-0] + _ = x[PositionOpening-1] + _ = x[PositionClosing-2] +} + +const _PositionState_name = "PositionNoOpPositionOpeningPositionClosing" + +var _PositionState_index = [...]uint8{0, 12, 27, 42} + +func (i PositionState) String() string { + if i < 0 || i >= PositionState(len(_PositionState_index)-1) { + return "PositionState(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _PositionState_name[_PositionState_index[i]:_PositionState_index[i+1]] +} diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 3590564ad..84bc1de72 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -20,11 +20,15 @@ import ( const ID = "xfunding" -//go:generate stringer -type=PositionAction -type PositionAction int +// Position State Transitions: +// NoOp -> Opening | Closing +// Opening -> NoOp -> Closing +// Closing -> NoOp -> Opening +//go:generate stringer -type=PositionState +type PositionState int const ( - PositionNoOp PositionAction = iota + PositionNoOp PositionState = iota PositionOpening PositionClosing ) @@ -104,16 +108,17 @@ type Strategy struct { spotOrderExecutor, futuresOrderExecutor *bbgo.GeneralOrderExecutor spotMarket, futuresMarket types.Market - // positionAction is default to NoOp - positionAction PositionAction - // positionType is the futures position type // currently we only support short position for the positive funding rate positionType types.PositionType } type State struct { - PositionStartTime time.Time `json:"positionStartTime"` + PositionStartTime time.Time `json:"positionStartTime"` + + // PositionState is default to NoOp + PositionState PositionState + PendingBaseTransfer fixedpoint.Value `json:"pendingBaseTransfer"` TotalBaseTransfer fixedpoint.Value `json:"totalBaseTransfer"` UsedQuoteInvestment fixedpoint.Value `json:"usedQuoteInvestment"` @@ -252,6 +257,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order if s.State == nil { s.State = &State{ + PositionState: PositionNoOp, PendingBaseTransfer: fixedpoint.Zero, TotalBaseTransfer: fixedpoint.Zero, UsedQuoteInvestment: fixedpoint.Zero, @@ -277,7 +283,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return } - switch s.positionAction { + switch s.State.PositionState { case PositionOpening: if trade.Side != types.SideTypeBuy { log.Errorf("unexpected trade side: %+v, expecting BUY trade", trade) @@ -289,7 +295,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.State.UsedQuoteInvestment = s.State.UsedQuoteInvestment.Add(trade.QuoteQuantity) if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { - s.positionAction = PositionNoOp + s.State.PositionState = PositionNoOp } // if we have trade, try to query the balance and transfer the balance to the futures wallet account @@ -316,7 +322,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order return } - switch s.positionAction { + switch s.State.PositionState { case PositionClosing: if err := backoff.RetryGeneral(ctx, func() error { return s.transferOut(ctx, binanceSpot, s.spotMarket.BaseCurrency, trade) @@ -367,13 +373,13 @@ func (s *Strategy) queryAndDetectPremiumIndex(ctx context.Context, binanceFuture log.Infof("premiumIndex: %+v", premiumIndex) if changed := s.detectPremiumIndex(premiumIndex); changed { - log.Infof("position action: %s %s", s.positionType, s.positionAction.String()) + log.Infof("position action: %s %s", s.positionType, s.State.PositionState.String()) s.triggerPositionAction(ctx) } } func (s *Strategy) triggerPositionAction(ctx context.Context) { - switch s.positionAction { + switch s.State.PositionState { case PositionOpening: s.increaseSpotPosition(ctx) s.syncFuturesPosition(ctx) @@ -384,7 +390,7 @@ func (s *Strategy) triggerPositionAction(ctx context.Context) { } func (s *Strategy) reduceFuturesPosition(ctx context.Context) { - switch s.positionAction { + switch s.State.PositionState { case PositionOpening, PositionNoOp: return } @@ -443,7 +449,7 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { return } - switch s.positionAction { + switch s.State.PositionState { case PositionClosing: return case PositionOpening, PositionNoOp: @@ -532,7 +538,8 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { log.Errorf("funding long position type is not supported") return } - if s.positionAction != PositionOpening { + + if s.State.PositionState != PositionOpening { return } @@ -540,6 +547,8 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { defer s.mu.Unlock() if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { + // stop increase the position + s.State.PositionState = PositionNoOp return } @@ -586,6 +595,10 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { } func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed bool) { + if s.State.PositionState != PositionNoOp { + return changed + } + fundingRate := premiumIndex.LastFundingRate log.Infof("last %s funding rate: %s", s.Symbol, fundingRate.Percentage()) @@ -620,11 +633,12 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed } func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { - if s.positionAction == PositionOpening { + // we should only open a new position when there is no op on the position + if s.State.PositionState != PositionNoOp { return } - s.positionAction = PositionOpening + s.State.PositionState = PositionOpening s.positionType = pt // reset the transfer stats @@ -634,11 +648,11 @@ func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { } func (s *Strategy) startClosingPosition() { - if s.positionAction == PositionClosing { + if s.State.PositionState != PositionNoOp { return } - s.positionAction = PositionClosing + s.State.PositionState = PositionClosing // reset the transfer stats s.State.PendingBaseTransfer = fixedpoint.Zero From 62e6b232ed34e42b9dd45f0f80d19a252ec89874 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 00:52:36 +0800 Subject: [PATCH 41/49] xfunding: refactor and refine PositionState checking --- pkg/strategy/xfunding/positionstate_string.go | 9 +- pkg/strategy/xfunding/strategy.go | 82 +++++++++---------- 2 files changed, 45 insertions(+), 46 deletions(-) diff --git a/pkg/strategy/xfunding/positionstate_string.go b/pkg/strategy/xfunding/positionstate_string.go index c227a848b..67eb94805 100644 --- a/pkg/strategy/xfunding/positionstate_string.go +++ b/pkg/strategy/xfunding/positionstate_string.go @@ -8,14 +8,15 @@ func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} - _ = x[PositionNoOp-0] + _ = x[PositionClosed-0] _ = x[PositionOpening-1] - _ = x[PositionClosing-2] + _ = x[PositionReady-2] + _ = x[PositionClosing-3] } -const _PositionState_name = "PositionNoOpPositionOpeningPositionClosing" +const _PositionState_name = "PositionClosedPositionOpeningPositionReadyPositionClosing" -var _PositionState_index = [...]uint8{0, 12, 27, 42} +var _PositionState_index = [...]uint8{0, 14, 29, 42, 57} func (i PositionState) String() string { if i < 0 || i >= PositionState(len(_PositionState_index)-1) { diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 84bc1de72..ef4520cc1 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -21,15 +21,16 @@ import ( const ID = "xfunding" // Position State Transitions: -// NoOp -> Opening | Closing -// Opening -> NoOp -> Closing -// Closing -> NoOp -> Opening +// NoOp -> Opening +// Opening -> Ready -> Closing +// Closing -> Closed -> Opening //go:generate stringer -type=PositionState type PositionState int const ( - PositionNoOp PositionState = iota + PositionClosed PositionState = iota PositionOpening + PositionReady PositionClosing ) @@ -142,16 +143,7 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { }) } -func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - for _, detection := range s.SupportDetection { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ - Interval: detection.Interval, - }) - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ - Interval: detection.MovingAverageIntervalWindow.Interval, - }) - } -} +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {} func (s *Strategy) Defaults() error { if s.Leverage.IsZero() { @@ -257,7 +249,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order if s.State == nil { s.State = &State{ - PositionState: PositionNoOp, + PositionState: PositionClosed, PendingBaseTransfer: fixedpoint.Zero, TotalBaseTransfer: fixedpoint.Zero, UsedQuoteInvestment: fixedpoint.Zero, @@ -295,7 +287,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.State.UsedQuoteInvestment = s.State.UsedQuoteInvestment.Add(trade.QuoteQuantity) if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { - s.State.PositionState = PositionNoOp + s.State.PositionState = PositionClosed } // if we have trade, try to query the balance and transfer the balance to the futures wallet account @@ -391,7 +383,7 @@ func (s *Strategy) triggerPositionAction(ctx context.Context) { func (s *Strategy) reduceFuturesPosition(ctx context.Context) { switch s.State.PositionState { - case PositionOpening, PositionNoOp: + case PositionOpening, PositionClosed: return } @@ -452,7 +444,7 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { switch s.State.PositionState { case PositionClosing: return - case PositionOpening, PositionNoOp: + case PositionOpening, PositionClosed: } spotBase := s.SpotPosition.GetBase() // should be positive base quantity here @@ -539,16 +531,16 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { return } + s.mu.Lock() + defer s.mu.Unlock() + if s.State.PositionState != PositionOpening { return } - s.mu.Lock() - defer s.mu.Unlock() - if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { // stop increase the position - s.State.PositionState = PositionNoOp + s.State.PositionState = PositionReady return } @@ -595,7 +587,7 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { } func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed bool) { - if s.State.PositionState != PositionNoOp { + if s.State.PositionState != PositionClosed { return changed } @@ -607,34 +599,39 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed return changed } - if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { + switch s.State.PositionState { - log.Infof("funding rate %s is higher than the High threshold %s, start opening position...", - fundingRate.Percentage(), s.ShortFundingRate.High.Percentage()) + case PositionClosed: + if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { + log.Infof("funding rate %s is higher than the High threshold %s, start opening position...", + fundingRate.Percentage(), s.ShortFundingRate.High.Percentage()) - s.startOpeningPosition(types.PositionShort, premiumIndex.Time) - changed = true - } else if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { - - log.Infof("funding rate %s is lower than the Low threshold %s, start closing position...", - fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage()) - - holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime) - if holdingPeriod < time.Duration(s.MinHoldingPeriod) { - log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod) - return + s.startOpeningPosition(types.PositionShort, premiumIndex.Time) + changed = true } - s.startClosingPosition() - changed = true + case PositionReady: + if fundingRate.Compare(s.ShortFundingRate.Low) <= 0 { + log.Infof("funding rate %s is lower than the Low threshold %s, start closing position...", + fundingRate.Percentage(), s.ShortFundingRate.Low.Percentage()) + + holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime) + if holdingPeriod < time.Duration(s.MinHoldingPeriod) { + log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod) + return + } + + s.startClosingPosition() + changed = true + } } return changed } func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { - // we should only open a new position when there is no op on the position - if s.State.PositionState != PositionNoOp { + // only open a new position when there is no position + if s.State.PositionState != PositionClosed { return } @@ -648,7 +645,8 @@ func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { } func (s *Strategy) startClosingPosition() { - if s.State.PositionState != PositionNoOp { + // we can't close a position that is not ready + if s.State.PositionState != PositionReady { return } From 209fb102face897a56236e7e097f703a2e34286f Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 01:57:43 +0800 Subject: [PATCH 42/49] xfunding: add stringer support on PremiumIndex --- config/xfunding.yaml | 1 + pkg/strategy/xfunding/strategy.go | 49 ++++++++++++++++++------------- pkg/types/premiumindex.go | 5 ++++ 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/config/xfunding.yaml b/config/xfunding.yaml index b39712659..047d278bd 100644 --- a/config/xfunding.yaml +++ b/config/xfunding.yaml @@ -37,3 +37,4 @@ crossExchangeStrategies: shortFundingRate: high: 0.000% low: -0.01% + reset: true diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index ef4520cc1..1b3036652 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -43,6 +43,24 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } +type State struct { + PositionStartTime time.Time `json:"positionStartTime"` + + // PositionState is default to NoOp + PositionState PositionState + + PendingBaseTransfer fixedpoint.Value `json:"pendingBaseTransfer"` + TotalBaseTransfer fixedpoint.Value `json:"totalBaseTransfer"` + UsedQuoteInvestment fixedpoint.Value `json:"usedQuoteInvestment"` +} + +func (s *State) Reset() { + s.PositionState = PositionClosed + s.PendingBaseTransfer = fixedpoint.Zero + s.TotalBaseTransfer = fixedpoint.Zero + s.UsedQuoteInvestment = fixedpoint.Zero +} + // Strategy is the xfunding fee strategy // Right now it only supports short position in the USDT futures account. // When opening the short position, it uses spot account to buy inventory, then transfer the inventory to the futures account as collateral assets. @@ -95,6 +113,7 @@ type Strategy struct { SpotSession string `json:"spotSession"` FuturesSession string `json:"futuresSession"` + Reset bool `json:"reset"` ProfitStats *types.ProfitStats `persistence:"profit_stats"` SpotPosition *types.Position `persistence:"spot_position"` @@ -114,17 +133,6 @@ type Strategy struct { positionType types.PositionType } -type State struct { - PositionStartTime time.Time `json:"positionStartTime"` - - // PositionState is default to NoOp - PositionState PositionState - - PendingBaseTransfer fixedpoint.Value `json:"pendingBaseTransfer"` - TotalBaseTransfer fixedpoint.Value `json:"totalBaseTransfer"` - UsedQuoteInvestment fixedpoint.Value `json:"usedQuoteInvestment"` -} - func (s *Strategy) ID() string { return ID } @@ -235,19 +243,19 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } } - if s.ProfitStats == nil { + if s.ProfitStats == nil || s.Reset { s.ProfitStats = types.NewProfitStats(s.Market) } - if s.FuturesPosition == nil { + if s.FuturesPosition == nil || s.Reset { s.FuturesPosition = types.NewPositionFromMarket(s.futuresMarket) } - if s.SpotPosition == nil { + if s.SpotPosition == nil || s.Reset { s.SpotPosition = types.NewPositionFromMarket(s.spotMarket) } - if s.State == nil { + if s.State == nil || s.Reset { s.State = &State{ PositionState: PositionClosed, PendingBaseTransfer: fixedpoint.Zero, @@ -365,12 +373,13 @@ func (s *Strategy) queryAndDetectPremiumIndex(ctx context.Context, binanceFuture log.Infof("premiumIndex: %+v", premiumIndex) if changed := s.detectPremiumIndex(premiumIndex); changed { - log.Infof("position action: %s %s", s.positionType, s.State.PositionState.String()) - s.triggerPositionAction(ctx) + log.Infof("position state changed: %s %s", s.positionType, s.State.PositionState.String()) } + + s.sync(ctx) } -func (s *Strategy) triggerPositionAction(ctx context.Context) { +func (s *Strategy) sync(ctx context.Context) { switch s.State.PositionState { case PositionOpening: s.increaseSpotPosition(ctx) @@ -441,10 +450,8 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { return } - switch s.State.PositionState { - case PositionClosing: + if s.State.PositionState != PositionOpening { return - case PositionOpening, PositionClosed: } spotBase := s.SpotPosition.GetBase() // should be positive base quantity here diff --git a/pkg/types/premiumindex.go b/pkg/types/premiumindex.go index c9ffcd0aa..5b2e4094c 100644 --- a/pkg/types/premiumindex.go +++ b/pkg/types/premiumindex.go @@ -1,6 +1,7 @@ package types import ( + "fmt" "time" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -13,3 +14,7 @@ type PremiumIndex struct { NextFundingTime time.Time `json:"nextFundingTime"` Time time.Time `json:"time"` } + +func (i *PremiumIndex) String() string { + return fmt.Sprintf("%s %s %s %s NEXT: %s", i.Symbol, i.MarkPrice.String(), i.LastFundingRate.Percentage(), i.Time, i.NextFundingTime) +} From f3049de2baf0d31ce71935d99aa5d50ceafa0479 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 02:09:49 +0800 Subject: [PATCH 43/49] all: improve logging --- config/xfunding.yaml | 11 ++++++++--- pkg/strategy/xfunding/strategy.go | 12 +++++++----- pkg/types/premiumindex.go | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/config/xfunding.yaml b/config/xfunding.yaml index 047d278bd..3d80c528d 100644 --- a/config/xfunding.yaml +++ b/config/xfunding.yaml @@ -32,9 +32,14 @@ crossExchangeStrategies: futuresSession: binance_futures symbol: ETHUSDT leverage: 1.0 - incrementalQuoteQuantity: 11 - quoteInvestment: 110 + incrementalQuoteQuantity: 20 + quoteInvestment: 50 + shortFundingRate: - high: 0.000% + ## when funding rate is higher than this high value, the strategy will start buying spot and opening a short position + high: 0.001% + ## when funding rate is lower than this low value, the strategy will start closing futures position and sell the spot low: -0.01% + + ## reset will reset the spot/futures positions, the transfer stats and the position state. reset: true diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 1b3036652..430c2e419 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -349,7 +349,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order case <-ticker.C: s.queryAndDetectPremiumIndex(ctx, binanceFutures) - + s.sync(ctx) } } }() @@ -370,13 +370,11 @@ func (s *Strategy) queryAndDetectPremiumIndex(ctx context.Context, binanceFuture return } - log.Infof("premiumIndex: %+v", premiumIndex) + log.Info(premiumIndex) if changed := s.detectPremiumIndex(premiumIndex); changed { - log.Infof("position state changed: %s %s", s.positionType, s.State.PositionState.String()) + log.Infof("position state changed to -> %s %s", s.positionType, s.State.PositionState.String()) } - - s.sync(ctx) } func (s *Strategy) sync(ctx context.Context) { @@ -548,6 +546,8 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { // stop increase the position s.State.PositionState = PositionReady + + s.startClosingPosition() return } @@ -642,6 +642,7 @@ func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { return } + log.Infof("startOpeningPosition") s.State.PositionState = PositionOpening s.positionType = pt @@ -657,6 +658,7 @@ func (s *Strategy) startClosingPosition() { return } + log.Infof("startClosingPosition") s.State.PositionState = PositionClosing // reset the transfer stats diff --git a/pkg/types/premiumindex.go b/pkg/types/premiumindex.go index 5b2e4094c..0dd3bc52d 100644 --- a/pkg/types/premiumindex.go +++ b/pkg/types/premiumindex.go @@ -16,5 +16,5 @@ type PremiumIndex struct { } func (i *PremiumIndex) String() string { - return fmt.Sprintf("%s %s %s %s NEXT: %s", i.Symbol, i.MarkPrice.String(), i.LastFundingRate.Percentage(), i.Time, i.NextFundingTime) + return fmt.Sprintf("PremiumIndex | %s | %.4f | %s | %s | NEXT FUNDING TIME: %s", i.Symbol, i.MarkPrice.Float64(), i.LastFundingRate.Percentage(), i.Time, i.NextFundingTime) } From 84313dbdf9535ccf352d40fe3da0e16197a30769 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 02:52:13 +0800 Subject: [PATCH 44/49] xfunding: refactor state functions and fix transfer out --- pkg/strategy/xfunding/strategy.go | 72 ++++++++++++++++++------------- pkg/strategy/xfunding/transfer.go | 48 +++++++++++++-------- 2 files changed, 71 insertions(+), 49 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 430c2e419..c71652191 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -291,12 +291,8 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } s.mu.Lock() - defer s.mu.Unlock() - s.State.UsedQuoteInvestment = s.State.UsedQuoteInvestment.Add(trade.QuoteQuantity) - if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { - s.State.PositionState = PositionClosed - } + s.mu.Unlock() // if we have trade, try to query the balance and transfer the balance to the futures wallet account // TODO: handle missing trades here. If the process crashed during the transfer, how to recover? @@ -318,11 +314,14 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition) s.futuresOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + + log.Infof("futures trade: %v", trade) + if s.positionType != types.PositionShort { return } - switch s.State.PositionState { + switch s.getPositionState() { case PositionClosing: if err := backoff.RetryGeneral(ctx, func() error { return s.transferOut(ctx, binanceSpot, s.spotMarket.BaseCurrency, trade) @@ -378,7 +377,7 @@ func (s *Strategy) queryAndDetectPremiumIndex(ctx context.Context, binanceFuture } func (s *Strategy) sync(ctx context.Context) { - switch s.State.PositionState { + switch s.getPositionState() { case PositionOpening: s.increaseSpotPosition(ctx) s.syncFuturesPosition(ctx) @@ -389,8 +388,7 @@ func (s *Strategy) sync(ctx context.Context) { } func (s *Strategy) reduceFuturesPosition(ctx context.Context) { - switch s.State.PositionState { - case PositionOpening, PositionClosed: + if s.notPositionState(PositionClosing) { return } @@ -411,8 +409,7 @@ func (s *Strategy) reduceFuturesPosition(ctx context.Context) { } if futuresBase.Compare(fixedpoint.Zero) < 0 { - orderPrice := ticker.Sell - + orderPrice := ticker.Buy orderQuantity := futuresBase.Abs() orderQuantity = fixedpoint.Max(orderQuantity, s.futuresMarket.MinQuantity) orderQuantity = s.futuresMarket.AdjustQuantityByMinNotional(orderQuantity, orderPrice) @@ -448,7 +445,7 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { return } - if s.State.PositionState != PositionOpening { + if s.notPositionState(PositionOpening) { return } @@ -539,14 +536,15 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { s.mu.Lock() defer s.mu.Unlock() - if s.State.PositionState != PositionOpening { + if s.notPositionState(PositionOpening) { return } if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { // stop increase the position - s.State.PositionState = PositionReady + s.setPositionState(PositionReady) + // DEBUG CODE - triggering closing position automatically s.startClosingPosition() return } @@ -593,20 +591,16 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { log.Infof("created orders: %+v", createdOrders) } -func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed bool) { - if s.State.PositionState != PositionClosed { - return changed - } - +func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) bool { fundingRate := premiumIndex.LastFundingRate log.Infof("last %s funding rate: %s", s.Symbol, fundingRate.Percentage()) if s.ShortFundingRate == nil { - return changed + return false } - switch s.State.PositionState { + switch s.getPositionState() { case PositionClosed: if fundingRate.Compare(s.ShortFundingRate.High) >= 0 { @@ -614,7 +608,7 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed fundingRate.Percentage(), s.ShortFundingRate.High.Percentage()) s.startOpeningPosition(types.PositionShort, premiumIndex.Time) - changed = true + return true } case PositionReady: @@ -625,25 +619,26 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) (changed holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime) if holdingPeriod < time.Duration(s.MinHoldingPeriod) { log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod) - return + return false } s.startClosingPosition() - changed = true + return true } } - return changed + return false } func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { // only open a new position when there is no position - if s.State.PositionState != PositionClosed { + if s.notPositionState(PositionClosed) { return } log.Infof("startOpeningPosition") - s.State.PositionState = PositionOpening + s.setPositionState(PositionOpening) + s.positionType = pt // reset the transfer stats @@ -654,16 +649,33 @@ func (s *Strategy) startOpeningPosition(pt types.PositionType, t time.Time) { func (s *Strategy) startClosingPosition() { // we can't close a position that is not ready - if s.State.PositionState != PositionReady { + if s.notPositionState(PositionReady) { return } log.Infof("startClosingPosition") - s.State.PositionState = PositionClosing + s.setPositionState(PositionClosing) // reset the transfer stats s.State.PendingBaseTransfer = fixedpoint.Zero - s.State.TotalBaseTransfer = fixedpoint.Zero +} + +func (s *Strategy) setPositionState(state PositionState) { + origState := s.State.PositionState + s.State.PositionState = state + log.Infof("position state transition: %s -> %s", origState.String(), state.String()) +} + +func (s *Strategy) isPositionState(state PositionState) bool { + return s.State.PositionState == state +} + +func (s *Strategy) getPositionState() PositionState { + return s.State.PositionState +} + +func (s *Strategy) notPositionState(state PositionState) bool { + return s.State.PositionState != state } func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.ExchangeSession, instanceID string, position *types.Position) *bbgo.GeneralOrderExecutor { diff --git a/pkg/strategy/xfunding/transfer.go b/pkg/strategy/xfunding/transfer.go index 14e5abb9d..12bcd2b6b 100644 --- a/pkg/strategy/xfunding/transfer.go +++ b/pkg/strategy/xfunding/transfer.go @@ -15,44 +15,54 @@ type FuturesTransfer interface { func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, currency string, trade types.Trade) error { // base asset needs BUY trades - if trade.Side == types.SideTypeBuy { + if trade.Side != types.SideTypeBuy { return nil } + // if transfer done + if s.State.TotalBaseTransfer.IsZero() { + return nil + } + + // de-leverage and get the collateral base quantity for transfer + quantity := trade.Quantity + quantity = quantity.Div(s.Leverage) + balances, err := s.futuresSession.Exchange.QueryAccountBalances(ctx) if err != nil { + log.Infof("adding to pending base transfer: %s %s + %s", quantity.String(), currency, s.State.PendingBaseTransfer.String()) + s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return err } b, ok := balances[currency] if !ok { + log.Infof("adding to pending base transfer: %s %s + %s", quantity.String(), currency, s.State.PendingBaseTransfer.String()) + s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return fmt.Errorf("%s balance not found", currency) } - quantity := trade.Quantity + // add the previous pending base transfer and the current trade quantity + amount := s.State.PendingBaseTransfer.Add(quantity) - if s.Leverage.Compare(fixedpoint.One) > 0 { - // de-leverage and get the collateral base quantity for transfer - quantity = quantity.Div(s.Leverage) - } + // try to transfer more if we enough balance + amount = fixedpoint.Min(amount, b.Available) + + // we can only transfer the rest quota (total base transfer) + amount = fixedpoint.Min(s.State.TotalBaseTransfer, amount) // TODO: according to the fee, we might not be able to get enough balance greater than the trade quantity, we can adjust the quantity here - if b.Available.IsZero() || b.Available.Compare(quantity) < 0 { - log.Infof("adding to pending base transfer: %s %s", quantity, currency) + if amount.IsZero() { + log.Infof("adding to pending base transfer: %s %s + %s ", quantity.String(), currency, s.State.PendingBaseTransfer.String()) s.State.PendingBaseTransfer = s.State.PendingBaseTransfer.Add(quantity) return nil } - amount := s.State.PendingBaseTransfer.Add(quantity) + // de-leverage and get the collateral base quantity + collateralBase := s.FuturesPosition.GetBase().Abs().Div(s.Leverage) + _ = collateralBase - pos := s.FuturesPosition.GetBase().Abs().Div(s.Leverage) - rest := pos.Sub(s.State.TotalBaseTransfer) - - if rest.Sign() < 0 { - return nil - } - - amount = fixedpoint.Min(rest, amount) + // if s.State.TotalBaseTransfer.Compare(collateralBase) log.Infof("transfering out futures account asset %s %s", amount, currency) if err := ex.TransferFuturesAccountAsset(ctx, currency, amount, types.TransferOut); err != nil { @@ -62,8 +72,8 @@ func (s *Strategy) transferOut(ctx context.Context, ex FuturesTransfer, currency // reset pending transfer s.State.PendingBaseTransfer = fixedpoint.Zero - // record the transfer in the total base transfer - s.State.TotalBaseTransfer = s.State.TotalBaseTransfer.Add(amount) + // reduce the transfer in the total base transfer + s.State.TotalBaseTransfer = s.State.TotalBaseTransfer.Sub(amount) return nil } From 0f21c1fd8f423f106e3fc65784475583bbb42504 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 03:19:03 +0800 Subject: [PATCH 45/49] xfunding: fix Warnf format --- pkg/strategy/xfunding/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index c71652191..a62a4db69 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -618,7 +618,7 @@ func (s *Strategy) detectPremiumIndex(premiumIndex *types.PremiumIndex) bool { holdingPeriod := premiumIndex.Time.Sub(s.State.PositionStartTime) if holdingPeriod < time.Duration(s.MinHoldingPeriod) { - log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod) + log.Warnf("position holding period %s is less than %s, skip closing", holdingPeriod, s.MinHoldingPeriod.Duration()) return false } From 1517076f6d72d592b0977b71bb52c4eabc2d547b Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 03:14:04 +0800 Subject: [PATCH 46/49] xfunding: implement syncSpotPosition --- config/xfunding.yaml | 2 +- pkg/strategy/xfunding/strategy.go | 85 +++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/config/xfunding.yaml b/config/xfunding.yaml index 3d80c528d..2b65cd167 100644 --- a/config/xfunding.yaml +++ b/config/xfunding.yaml @@ -42,4 +42,4 @@ crossExchangeStrategies: low: -0.01% ## reset will reset the spot/futures positions, the transfer stats and the position state. - reset: true + # reset: true diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index a62a4db69..02414a1a3 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -524,7 +524,92 @@ func (s *Strategy) syncFuturesPosition(ctx context.Context) { } func (s *Strategy) syncSpotPosition(ctx context.Context) { + if s.positionType != types.PositionShort { + return + } + if s.notPositionState(PositionClosing) { + return + } + + spotBase := s.SpotPosition.GetBase() // should be positive base quantity here + futuresBase := s.FuturesPosition.GetBase() // should be negative base quantity here + + if spotBase.IsZero() { + s.setPositionState(PositionClosed) + return + } + + // skip short spot position + if spotBase.Sign() < 0 { + return + } + + log.Infof("spot/futures positions: %s (spot) <=> %s (futures)", spotBase.String(), futuresBase.String()) + + if futuresBase.Sign() > 0 { + // unexpected error + log.Errorf("unexpected futures position (got positive, expecting negative)") + return + } + + _ = s.futuresOrderExecutor.GracefulCancel(ctx) + + ticker, err := s.spotSession.Exchange.QueryTicker(ctx, s.Symbol) + if err != nil { + log.WithError(err).Errorf("can not query ticker") + return + } + + if s.SpotPosition.IsDust(ticker.Sell) { + dust := s.SpotPosition.GetBase().Abs() + cost := s.SpotPosition.AverageCost + + log.Warnf("spot dust loss: %f %s (average cost = %f)", dust.Float64(), s.spotMarket.BaseCurrency, cost.Float64()) + + s.SpotPosition.Reset() + + s.setPositionState(PositionClosed) + return + } + + // spot pos size > futures pos size ==> reduce spot position + if spotBase.Compare(futuresBase.Neg()) > 0 { + diffQuantity := spotBase.Sub(futuresBase.Neg()) + + if diffQuantity.Sign() < 0 { + log.Errorf("unexpected negative position diff: %s", diffQuantity.String()) + return + } + + orderPrice := ticker.Sell + orderQuantity := diffQuantity + if b, ok := s.spotSession.Account.Balance(s.spotMarket.BaseCurrency); ok { + orderQuantity = fixedpoint.Min(b.Available, orderQuantity) + } + + // avoid increase the order size + if s.spotMarket.IsDustQuantity(orderQuantity, orderPrice) { + log.Infof("skip futures order with dust quantity %s, market = %+v", orderQuantity.String(), s.spotMarket) + return + } + + createdOrders, err := s.spotOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Quantity: orderQuantity, + Price: orderPrice, + Market: s.futuresMarket, + }) + + if err != nil { + log.WithError(err).Errorf("can not submit spot order") + return + } + + log.Infof("created spot orders: %+v", createdOrders) + } } func (s *Strategy) increaseSpotPosition(ctx context.Context) { From 4669692b8db9f620442dda265172781d6292edbd Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 03:16:51 +0800 Subject: [PATCH 47/49] xfunding: remove debug log and test code --- pkg/strategy/xfunding/strategy.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 02414a1a3..2c2dfc72e 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -314,9 +314,6 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order s.futuresOrderExecutor = s.allocateOrderExecutor(ctx, s.futuresSession, instanceID, s.FuturesPosition) s.futuresOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { - - log.Infof("futures trade: %v", trade) - if s.positionType != types.PositionShort { return } @@ -630,7 +627,7 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { s.setPositionState(PositionReady) // DEBUG CODE - triggering closing position automatically - s.startClosingPosition() + // s.startClosingPosition() return } From ea7af708f91a11437ced2adeb77a547e5ceb32e3 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 14:28:36 +0800 Subject: [PATCH 48/49] add mutex lock --- pkg/strategy/xfunding/strategy.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 2c2dfc72e..15f29d2ac 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -615,13 +615,12 @@ func (s *Strategy) increaseSpotPosition(ctx context.Context) { return } - s.mu.Lock() - defer s.mu.Unlock() - if s.notPositionState(PositionOpening) { return } + s.mu.Lock() + defer s.mu.Unlock() if s.State.UsedQuoteInvestment.Compare(s.QuoteInvestment) >= 0 { // stop increase the position s.setPositionState(PositionReady) @@ -743,13 +742,18 @@ func (s *Strategy) startClosingPosition() { } func (s *Strategy) setPositionState(state PositionState) { + s.mu.Lock() origState := s.State.PositionState s.State.PositionState = state + s.mu.Unlock() log.Infof("position state transition: %s -> %s", origState.String(), state.String()) } func (s *Strategy) isPositionState(state PositionState) bool { - return s.State.PositionState == state + s.mu.Lock() + ret := s.State.PositionState == state + s.mu.Unlock() + return ret } func (s *Strategy) getPositionState() PositionState { @@ -757,7 +761,10 @@ func (s *Strategy) getPositionState() PositionState { } func (s *Strategy) notPositionState(state PositionState) bool { - return s.State.PositionState != state + s.mu.Lock() + ret := s.State.PositionState != state + s.mu.Unlock() + return ret } func (s *Strategy) allocateOrderExecutor(ctx context.Context, session *bbgo.ExchangeSession, instanceID string, position *types.Position) *bbgo.GeneralOrderExecutor { From feec194843f5cdfbd7b6ea60a6e2c71ed07127c0 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 24 Mar 2023 14:37:18 +0800 Subject: [PATCH 49/49] binance: improve transfer logs --- pkg/exchange/binance/exchange.go | 9 ++++++++- pkg/strategy/xfunding/strategy.go | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 9880b6d2d..4e56853b2 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -381,7 +381,14 @@ func (e *Exchange) TransferFuturesAccountAsset(ctx context.Context, asset string } resp, err := req.Do(ctx) - log.Infof("futures transfer %s %s, transaction = %+v, err = %+v", amount.String(), asset, resp, err) + + switch io { + case types.TransferIn: + log.Infof("internal transfer (spot) => (futures) %s %s, transaction = %+v, err = %+v", amount.String(), asset, resp, err) + case types.TransferOut: + log.Infof("internal transfer (futures) => (spot) %s %s, transaction = %+v, err = %+v", amount.String(), asset, resp, err) + } + return err } diff --git a/pkg/strategy/xfunding/strategy.go b/pkg/strategy/xfunding/strategy.go index 15f29d2ac..77e662d73 100644 --- a/pkg/strategy/xfunding/strategy.go +++ b/pkg/strategy/xfunding/strategy.go @@ -162,6 +162,8 @@ func (s *Strategy) Defaults() error { s.MinHoldingPeriod = types.Duration(3 * 24 * time.Hour) } + s.positionType = types.PositionShort + return nil }