diff --git a/config/drift.yaml b/config/drift.yaml index 399a1f9f8..c5c684b56 100644 --- a/config/drift.yaml +++ b/config/drift.yaml @@ -12,6 +12,13 @@ sessions: envVarPrefix: binance heikinAshi: false + # Drift strategy intends to place buy/sell orders as much value mas it could be. To exchanges that requires to + # calculate fees before placing limit orders (e.g. FTX Pro), make sure the fee rate is configured correctly and + # enable modifyOrderAmountForFee to prevent order rejection. + makerFeeRate: 0.0002 + takerFeeRate: 0.0007 + modifyOrderAmountForFee: false + exchangeStrategies: - on: binance diff --git a/config/driftBTC.yaml b/config/driftBTC.yaml index df47faebf..c199e0b0c 100644 --- a/config/driftBTC.yaml +++ b/config/driftBTC.yaml @@ -12,6 +12,13 @@ sessions: envVarPrefix: binance heikinAshi: false + # Drift strategy intends to place buy/sell orders as much value mas it could be. To exchanges that requires to + # calculate fees before placing limit orders (e.g. FTX Pro), make sure the fee rate is configured correctly and + # enable modifyOrderAmountForFee to prevent order rejection. + makerFeeRate: 0.0002 + takerFeeRate: 0.0007 + modifyOrderAmountForFee: false + exchangeStrategies: - on: binance diff --git a/pkg/bbgo/config_test.go b/pkg/bbgo/config_test.go index 723290431..13ad37ed2 100644 --- a/pkg/bbgo/config_test.go +++ b/pkg/bbgo/config_test.go @@ -82,16 +82,25 @@ func TestLoadConfig(t *testing.T) { assert.Equal(t, map[string]interface{}{ "sessions": map[string]interface{}{ "max": map[string]interface{}{ - "exchange": "max", - "envVarPrefix": "MAX", - "takerFeeRate": 0., - "makerFeeRate": 0., + "exchange": "max", + "envVarPrefix": "MAX", + "takerFeeRate": 0., + "makerFeeRate": 0., + "modifyOrderAmountForFee": false, }, "binance": map[string]interface{}{ - "exchange": "binance", - "envVarPrefix": "BINANCE", - "takerFeeRate": 0., - "makerFeeRate": 0., + "exchange": "binance", + "envVarPrefix": "BINANCE", + "takerFeeRate": 0., + "makerFeeRate": 0., + "modifyOrderAmountForFee": false, + }, + "ftx": map[string]interface{}{ + "exchange": "ftx", + "envVarPrefix": "FTX", + "takerFeeRate": 0., + "makerFeeRate": 0., + "modifyOrderAmountForFee": true, }, }, "build": map[string]interface{}{ diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index aa86fe06f..1e59f602e 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -39,9 +39,10 @@ type ExchangeSession struct { SubAccount string `json:"subAccount,omitempty" yaml:"subAccount,omitempty"` // Withdrawal is used for enabling withdrawal functions - Withdrawal bool `json:"withdrawal,omitempty" yaml:"withdrawal,omitempty"` - MakerFeeRate fixedpoint.Value `json:"makerFeeRate" yaml:"makerFeeRate"` - TakerFeeRate fixedpoint.Value `json:"takerFeeRate" yaml:"takerFeeRate"` + Withdrawal bool `json:"withdrawal,omitempty" yaml:"withdrawal,omitempty"` + MakerFeeRate fixedpoint.Value `json:"makerFeeRate" yaml:"makerFeeRate"` + TakerFeeRate fixedpoint.Value `json:"takerFeeRate" yaml:"takerFeeRate"` + ModifyOrderAmountForFee bool `json:"modifyOrderAmountForFee" yaml:"modifyOrderAmountForFee"` PublicOnly bool `json:"publicOnly,omitempty" yaml:"publicOnly"` Margin bool `json:"margin,omitempty" yaml:"margin"` @@ -202,6 +203,16 @@ func (session *ExchangeSession) Init(ctx context.Context, environ *Environment) } } + if session.ModifyOrderAmountForFee { + amountProtectExchange, ok := session.Exchange.(types.ExchangeAmountFeeProtect) + if !ok { + return fmt.Errorf("exchange %s does not support order amount protection", session.ExchangeName.String()) + } + + fees := types.ExchangeFee{MakerFeeRate: session.MakerFeeRate, TakerFeeRate: session.TakerFeeRate} + amountProtectExchange.SetModifyOrderAmountForFee(fees) + } + if session.UseHeikinAshi { session.MarketDataStream = &types.HeikinAshiStream{ StandardStreamEmitter: session.MarketDataStream.(types.StandardStreamEmitter), diff --git a/pkg/bbgo/testdata/strategy.yaml b/pkg/bbgo/testdata/strategy.yaml index e04e630e1..9c1ecc482 100644 --- a/pkg/bbgo/testdata/strategy.yaml +++ b/pkg/bbgo/testdata/strategy.yaml @@ -5,11 +5,19 @@ sessions: envVarPrefix: MAX takerFeeRate: 0 makerFeeRate: 0 + modifyOrderAmountForFee: false binance: exchange: binance envVarPrefix: BINANCE takerFeeRate: 0 makerFeeRate: 0 + modifyOrderAmountForFee: false + ftx: + exchange: ftx + envVarPrefix: FTX + takerFeeRate: 0 + makerFeeRate: 0 + modifyOrderAmountForFee: true exchangeStrategies: - on: ["binance"] diff --git a/pkg/exchange/ftx/exchange.go b/pkg/exchange/ftx/exchange.go index 194ff4b52..4f62cc052 100644 --- a/pkg/exchange/ftx/exchange.go +++ b/pkg/exchange/ftx/exchange.go @@ -37,9 +37,10 @@ var marketDataLimiter = rate.NewLimiter(rate.Every(500*time.Millisecond), 2) type Exchange struct { client *ftxapi.RestClient - key, secret string - subAccount string - restEndpoint *url.URL + key, secret string + subAccount string + restEndpoint *url.URL + orderAmountReduceFactor fixedpoint.Value } type MarketTicker struct { @@ -91,8 +92,9 @@ func NewExchange(key, secret string, subAccount string) *Exchange { restEndpoint: u, key: key, // pragma: allowlist nextline secret - secret: secret, - subAccount: subAccount, + secret: secret, + subAccount: subAccount, + orderAmountReduceFactor: fixedpoint.One, } } @@ -221,6 +223,13 @@ func (e *Exchange) DefaultFeeRates() types.ExchangeFee { } } +// SetModifyOrderAmountForFee protects the limit buy orders by reducing amount with taker fee. +// The amount is recalculated before submit: submit_amount = original_amount / (1 + taker_fee_rate) . +// This prevents balance exceeding error while closing position without spot margin enabled. +func (e *Exchange) SetModifyOrderAmountForFee(feeRate types.ExchangeFee) { + e.orderAmountReduceFactor = fixedpoint.One.Add(feeRate.TakerFeeRate) +} + // resolution field in api // window length in seconds. options: 15, 60, 300, 900, 3600, 14400, 86400, or any multiple of 86400 up to 30*86400 var supportedIntervals = map[types.Interval]int{ @@ -406,11 +415,17 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder logrus.WithError(err).Error("type error") } + submitQuantity := so.Quantity + switch orderType { + case ftxapi.OrderTypeLimit, ftxapi.OrderTypeStopLimit: + submitQuantity = so.Quantity.Div(e.orderAmountReduceFactor) + } + req := e.client.NewPlaceOrderRequest() req.Market(toLocalSymbol(TrimUpperString(so.Symbol))) req.OrderType(orderType) req.Side(ftxapi.Side(TrimLowerString(string(so.Side)))) - req.Size(so.Quantity) + req.Size(submitQuantity) switch so.Type { case types.OrderTypeLimit, types.OrderTypeLimitMaker: diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index 8ce73c283..243c663d3 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -107,6 +107,10 @@ type ExchangeDefaultFeeRates interface { DefaultFeeRates() ExchangeFee } +type ExchangeAmountFeeProtect interface { + SetModifyOrderAmountForFee(ExchangeFee) +} + type ExchangeTradeHistoryService interface { QueryTrades(ctx context.Context, symbol string, options *TradeQueryOptions) ([]Trade, error) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) (orders []Order, err error)