mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-25 16:25:16 +00:00
refactor basic risk controller
This commit is contained in:
parent
ded89e099f
commit
4a2a542222
|
@ -25,7 +25,7 @@ riskControls:
|
|||
# "max" is the session name that you want to configure the risk control
|
||||
max:
|
||||
# orderExecutors is one of the risk control
|
||||
orderExecutors:
|
||||
orderExecutor:
|
||||
# symbol-routed order executor
|
||||
bySymbol:
|
||||
BTCUSDT:
|
||||
|
|
|
@ -31,8 +31,8 @@ riskControls:
|
|||
sessionBased:
|
||||
# "max" is the session name that you want to configure the risk control
|
||||
max:
|
||||
# orderExecutors is one of the risk control
|
||||
orderExecutors:
|
||||
# orderExecutor is one of the risk control
|
||||
orderExecutor:
|
||||
# symbol-routed order executor
|
||||
bySymbol:
|
||||
BTCUSDT:
|
||||
|
|
|
@ -31,8 +31,8 @@ riskControls:
|
|||
sessionBased:
|
||||
# "max" is the session name that you want to configure the risk control
|
||||
max:
|
||||
# orderExecutors is one of the risk control
|
||||
orderExecutors:
|
||||
# orderExecutor is one of the risk control
|
||||
orderExecutor:
|
||||
# symbol-routed order executor
|
||||
bySymbol:
|
||||
BNBUSDT:
|
||||
|
|
|
@ -27,8 +27,8 @@ riskControls:
|
|||
sessionBased:
|
||||
# "max" is the session name that you want to configure the risk control
|
||||
binance:
|
||||
# orderExecutors is one of the risk control
|
||||
orderExecutors:
|
||||
# orderExecutor is one of the risk control
|
||||
orderExecutor:
|
||||
# symbol-routed order executor
|
||||
bySymbol:
|
||||
BNBUSDT:
|
||||
|
|
1
go.mod
1
go.mod
|
@ -7,6 +7,7 @@ go 1.13
|
|||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0
|
||||
github.com/adshao/go-binance v0.0.0-20201015231210-37cee298310e
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869
|
||||
github.com/c9s/goose v0.0.0-20200415105707-8da682162a5b
|
||||
github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
|
|
|
@ -2,10 +2,10 @@ package bbgo
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
|
@ -71,97 +71,161 @@ func (e *ExchangeOrderExecutor) SubmitOrders(ctx context.Context, orders ...type
|
|||
return e.session.Exchange.SubmitOrders(ctx, formattedOrders...)
|
||||
}
|
||||
|
||||
type BasicRiskControlOrderExecutor struct {
|
||||
*ExchangeOrderExecutor
|
||||
type BasicRiskController struct {
|
||||
Logger *logrus.Logger
|
||||
|
||||
MinQuoteBalance fixedpoint.Value `json:"minQuoteBalance,omitempty"`
|
||||
MaxAssetBalance fixedpoint.Value `json:"maxBaseAssetBalance,omitempty"`
|
||||
MinAssetBalance fixedpoint.Value `json:"minBaseAssetBalance,omitempty"`
|
||||
MaxOrderAmount fixedpoint.Value `json:"maxOrderAmount,omitempty"`
|
||||
MaxOrderAmount fixedpoint.Value `json:"maxOrderAmount,omitempty"`
|
||||
MinQuoteBalance fixedpoint.Value `json:"minQuoteBalance,omitempty"`
|
||||
MaxBaseAssetBalance fixedpoint.Value `json:"maxBaseAssetBalance,omitempty"`
|
||||
MinBaseAssetBalance fixedpoint.Value `json:"minBaseAssetBalance,omitempty"`
|
||||
}
|
||||
|
||||
func (e *BasicRiskControlOrderExecutor) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (types.OrderSlice, error) {
|
||||
var formattedOrders []types.SubmitOrder
|
||||
// ProcessOrders filters and modifies the submit order objects by:
|
||||
// 1. Increase the quantity by the minimal requirement
|
||||
// 2. Decrease the quantity by risk controls
|
||||
// 3. If the quantity does not meet minimal requirement, we should ignore the submit order.
|
||||
func (c *BasicRiskController) ProcessOrders(session *ExchangeSession, orders ...types.SubmitOrder) (outOrders []types.SubmitOrder, err error) {
|
||||
balances := session.Account.Balances()
|
||||
|
||||
accumulativeQuoteAmount := 0.0
|
||||
accumulativeBaseSellQuantity := 0.0
|
||||
for _, order := range orders {
|
||||
currentPrice, ok := e.session.LastPrice(order.Symbol)
|
||||
lastPrice, ok := session.LastPrice(order.Symbol)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("the last price of symbol %q is not found", order.Symbol)
|
||||
c.Logger.Errorf("the last price of symbol %q is not found", order.Symbol)
|
||||
continue
|
||||
}
|
||||
|
||||
market := order.Market
|
||||
market, ok := session.Market(order.Symbol)
|
||||
if !ok {
|
||||
c.Logger.Errorf("the market config of symbol %q is not found", order.Symbol)
|
||||
continue
|
||||
}
|
||||
|
||||
price := order.Price
|
||||
quantity := order.Quantity
|
||||
balances := e.session.Account.Balances()
|
||||
switch order.Type {
|
||||
case types.OrderTypeMarket:
|
||||
price = lastPrice
|
||||
}
|
||||
|
||||
switch order.Side {
|
||||
case types.SideTypeBuy:
|
||||
// Critical conditions for placing buy orders
|
||||
quoteBalance, ok := balances[market.QuoteCurrency]
|
||||
if !ok {
|
||||
c.Logger.Errorf("can not place buy order, quote balance %s not found", market.QuoteCurrency)
|
||||
continue
|
||||
}
|
||||
|
||||
if balance, ok := balances[market.QuoteCurrency]; ok {
|
||||
if balance.Available < e.MinQuoteBalance.Float64() {
|
||||
return nil, errors.Wrapf(ErrQuoteBalanceLevelTooLow, "quote balance level is too low: %s < %s",
|
||||
types.USD.FormatMoneyFloat64(balance.Available),
|
||||
types.USD.FormatMoneyFloat64(e.MinQuoteBalance.Float64()))
|
||||
if quoteBalance.Available < c.MinQuoteBalance.Float64() {
|
||||
c.Logger.WithError(ErrQuoteBalanceLevelTooLow).Errorf("can not place buy order, quote balance level is too low: %s < %s",
|
||||
types.USD.FormatMoneyFloat64(quoteBalance.Available),
|
||||
types.USD.FormatMoneyFloat64(c.MinQuoteBalance.Float64()))
|
||||
continue
|
||||
}
|
||||
|
||||
// Increase the quantity if the amount is not enough,
|
||||
// this is the only increase op, later we will decrease the quantity if it meets the criteria
|
||||
quantity = adjustQuantityByMinAmount(quantity, price, market.MinAmount*1.01)
|
||||
|
||||
if c.MaxOrderAmount > 0 {
|
||||
quantity = adjustQuantityByMaxAmount(quantity, price, c.MaxOrderAmount.Float64())
|
||||
}
|
||||
|
||||
quoteAssetQuota := math.Max(0.0, quoteBalance.Available-c.MinQuoteBalance.Float64())
|
||||
if quoteAssetQuota < market.MinAmount {
|
||||
c.Logger.WithError(ErrInsufficientQuoteBalance).Errorf("can not place buy order, insufficient quote balance: quota %f < min amount %f", quoteAssetQuota, market.MinAmount)
|
||||
continue
|
||||
}
|
||||
|
||||
quantity = adjustQuantityByMaxAmount(quantity, price, quoteAssetQuota)
|
||||
|
||||
// if MaxBaseAssetBalance is enabled, we should check the current base asset balance
|
||||
if baseBalance, hasBaseAsset := balances[market.BaseCurrency]; hasBaseAsset && c.MaxBaseAssetBalance > 0 {
|
||||
if baseBalance.Available > c.MaxBaseAssetBalance.Float64() {
|
||||
c.Logger.WithError(ErrAssetBalanceLevelTooHigh).Errorf("should not place buy order, asset balance level is too high: %f > %f", baseBalance.Available, c.MaxBaseAssetBalance.Float64())
|
||||
continue
|
||||
}
|
||||
|
||||
if baseBalance, ok := balances[market.BaseCurrency]; ok {
|
||||
if e.MaxAssetBalance > 0 && baseBalance.Available > e.MaxAssetBalance.Float64() {
|
||||
return nil, errors.Wrapf(ErrAssetBalanceLevelTooHigh, "asset balance level is too high: %f > %f", baseBalance.Available, e.MaxAssetBalance.Float64())
|
||||
}
|
||||
}
|
||||
|
||||
available := math.Max(0.0, balance.Available-e.MinQuoteBalance.Float64())
|
||||
if available < market.MinAmount {
|
||||
return nil, errors.Wrapf(ErrInsufficientQuoteBalance, "insufficient quote balance: %f < min amount %f", available, market.MinAmount)
|
||||
}
|
||||
|
||||
quantity = adjustQuantityByMinAmount(quantity, currentPrice, market.MinAmount*1.01)
|
||||
quantity = adjustQuantityByMaxAmount(quantity, currentPrice, available)
|
||||
amount := quantity * currentPrice
|
||||
if amount < market.MinAmount {
|
||||
return nil, fmt.Errorf("amount too small: %f < min amount %f", amount, market.MinAmount)
|
||||
baseAssetQuota := math.Max(0, c.MaxBaseAssetBalance.Float64()-baseBalance.Available)
|
||||
if quantity > baseAssetQuota {
|
||||
quantity = baseAssetQuota
|
||||
}
|
||||
}
|
||||
|
||||
// if the amount is still too small, we should skip it.
|
||||
notional := quantity * lastPrice
|
||||
if notional < market.MinAmount {
|
||||
c.Logger.Errorf("can not place buy order, quote amount too small: notional %f < min amount %f", notional, market.MinAmount)
|
||||
continue
|
||||
}
|
||||
|
||||
accumulativeQuoteAmount += notional
|
||||
|
||||
case types.SideTypeSell:
|
||||
// Critical conditions for placing SELL orders
|
||||
baseAssetBalance, ok := balances[market.BaseCurrency]
|
||||
if !ok {
|
||||
c.Logger.Errorf("can not place sell order, no base asset balance %s", market.BaseCurrency)
|
||||
continue
|
||||
}
|
||||
|
||||
if balance, ok := balances[market.BaseCurrency]; ok {
|
||||
if e.MinAssetBalance > 0 && balance.Available < e.MinAssetBalance.Float64() {
|
||||
return nil, errors.Wrapf(ErrAssetBalanceLevelTooLow, "asset balance level is too low: %f > %f", balance.Available, e.MinAssetBalance.Float64())
|
||||
// if the amount is too small, we should increase it.
|
||||
quantity = adjustQuantityByMinAmount(quantity, price, market.MinNotional*1.01)
|
||||
|
||||
// we should not SELL too much
|
||||
quantity = math.Min(quantity, baseAssetBalance.Available)
|
||||
|
||||
if c.MinBaseAssetBalance > 0 {
|
||||
if baseAssetBalance.Available < c.MinBaseAssetBalance.Float64() {
|
||||
c.Logger.WithError(ErrAssetBalanceLevelTooLow).Errorf("asset balance level is too low: %f > %f", baseAssetBalance.Available, c.MinBaseAssetBalance.Float64())
|
||||
continue
|
||||
}
|
||||
|
||||
quantity = adjustQuantityByMinAmount(quantity, currentPrice, market.MinNotional*1.01)
|
||||
|
||||
available := balance.Available
|
||||
quantity = math.Min(quantity, available)
|
||||
quantity = math.Min(quantity, baseAssetBalance.Available-c.MinBaseAssetBalance.Float64())
|
||||
if quantity < market.MinQuantity {
|
||||
return nil, errors.Wrapf(ErrInsufficientAssetBalance, "insufficient asset balance: %f > minimal quantity %f", available, market.MinQuantity)
|
||||
}
|
||||
|
||||
notional := quantity * currentPrice
|
||||
if notional < market.MinNotional {
|
||||
return nil, fmt.Errorf("notional %f < min notional: %f", notional, market.MinNotional)
|
||||
}
|
||||
|
||||
if quantity < market.MinLot {
|
||||
return nil, fmt.Errorf("quantity %f less than min lot %f", quantity, market.MinLot)
|
||||
}
|
||||
|
||||
notional = quantity * currentPrice
|
||||
if notional < market.MinNotional {
|
||||
return nil, fmt.Errorf("notional %f < min notional: %f", notional, market.MinNotional)
|
||||
c.Logger.WithError(ErrInsufficientAssetBalance).Errorf("insufficient asset balance: %f > minimal quantity %f", baseAssetBalance.Available, market.MinQuantity)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if c.MaxOrderAmount > 0 {
|
||||
quantity = adjustQuantityByMaxAmount(quantity, price, c.MaxOrderAmount.Float64())
|
||||
}
|
||||
|
||||
notional := quantity * lastPrice
|
||||
if notional < market.MinNotional {
|
||||
c.Logger.Errorf("can not place sell order, notional %f < min notional: %f", notional, market.MinNotional)
|
||||
continue
|
||||
}
|
||||
|
||||
if quantity < market.MinLot {
|
||||
c.Logger.Errorf("can not place sell order, quantity %f is less than the minimal lot %f", quantity, market.MinLot)
|
||||
continue
|
||||
}
|
||||
|
||||
accumulativeBaseSellQuantity += quantity
|
||||
}
|
||||
|
||||
// update quantity and format the order
|
||||
order.Quantity = quantity
|
||||
o, err := formatOrder(order, e.session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
formattedOrders = append(formattedOrders, o)
|
||||
outOrders = append(outOrders, order)
|
||||
}
|
||||
|
||||
return outOrders, nil
|
||||
}
|
||||
|
||||
type BasicRiskControlOrderExecutor struct {
|
||||
*ExchangeOrderExecutor
|
||||
|
||||
BasicRiskController
|
||||
}
|
||||
|
||||
func (e *BasicRiskControlOrderExecutor) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (types.OrderSlice, error) {
|
||||
orders, _ = e.BasicRiskController.ProcessOrders(e.session, orders...)
|
||||
formattedOrders, _ := formatOrders(orders, e.session)
|
||||
|
||||
e.notifySubmitOrders(formattedOrders...)
|
||||
|
||||
return e.session.Exchange.SubmitOrders(ctx, formattedOrders...)
|
||||
|
|
|
@ -33,8 +33,8 @@ var (
|
|||
}
|
||||
|
||||
if baseBalance, ok := tradingCtx.Balances[market.BaseCurrency]; ok {
|
||||
if util.NotZero(p.MaxAssetBalance) && baseBalance.Available > p.MaxAssetBalance {
|
||||
return errors.Wrapf(ErrAssetBalanceLevelTooHigh, "asset balance level is too high: %f > %f", baseBalance.Available, p.MaxAssetBalance)
|
||||
if util.NotZero(p.MaxBaseAssetBalance) && baseBalance.Available > p.MaxBaseAssetBalance {
|
||||
return errors.Wrapf(ErrAssetBalanceLevelTooHigh, "asset balance level is too high: %f > %f", baseBalance.Available, p.MaxBaseAssetBalance)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,8 +55,8 @@ var (
|
|||
case types.SideTypeSell:
|
||||
|
||||
if balance, ok := tradingCtx.Balances[market.BaseCurrency]; ok {
|
||||
if util.NotZero(p.MinAssetBalance) && balance.Available < p.MinAssetBalance {
|
||||
return errors.Wrapf(ErrAssetBalanceLevelTooLow, "asset balance level is too low: %f > %f", balance.Available, p.MinAssetBalance)
|
||||
if util.NotZero(p.MinBaseAssetBalance) && balance.Available < p.MinBaseAssetBalance {
|
||||
return errors.Wrapf(ErrAssetBalanceLevelTooLow, "asset balance level is too low: %f > %f", balance.Available, p.MinBaseAssetBalance)
|
||||
}
|
||||
|
||||
quantity = adjustQuantityByMinAmount(quantity, currentPrice, market.MinNotional*1.01)
|
||||
|
@ -101,7 +101,8 @@ var (
|
|||
order.QuantityString = market.FormatVolume(quantity)
|
||||
*/
|
||||
|
||||
func adjustQuantityByMinAmount(quantity float64, currentPrice float64, minAmount float64) float64 {
|
||||
// adjustQuantityByMinAmount adjusts the quantity to make the amount greater than the given minAmount
|
||||
func adjustQuantityByMinAmount(quantity, currentPrice, minAmount float64) float64 {
|
||||
// modify quantity for the min amount
|
||||
amount := currentPrice * quantity
|
||||
if amount < minAmount {
|
||||
|
@ -112,8 +113,8 @@ func adjustQuantityByMinAmount(quantity float64, currentPrice float64, minAmount
|
|||
return quantity
|
||||
}
|
||||
|
||||
func adjustQuantityByMaxAmount(quantity float64, currentPrice float64, maxAmount float64) float64 {
|
||||
amount := currentPrice * quantity
|
||||
func adjustQuantityByMaxAmount(quantity float64, price float64, maxAmount float64) float64 {
|
||||
amount := price * quantity
|
||||
if amount > maxAmount {
|
||||
ratio := maxAmount / amount
|
||||
quantity *= ratio
|
||||
|
|
43
pkg/bbgo/order_processor_test.go
Normal file
43
pkg/bbgo/order_processor_test.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package bbgo
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAdjustQuantityByMinAmount(t *testing.T) {
|
||||
type args struct {
|
||||
quantity, price, minAmount float64
|
||||
}
|
||||
type testcase struct {
|
||||
name string
|
||||
args args
|
||||
wanted float64
|
||||
}
|
||||
|
||||
tests := []testcase{
|
||||
{
|
||||
name: "amount too small",
|
||||
args: args{0.1, 10.0, 10.0},
|
||||
wanted: 1.0,
|
||||
},
|
||||
{
|
||||
name: "amount equals to min amount",
|
||||
args: args{1.0, 10.0, 10.0},
|
||||
wanted: 1.0,
|
||||
},
|
||||
{
|
||||
name: "amount is greater than min amount",
|
||||
args: args{2.0, 10.0, 10.0},
|
||||
wanted: 2.0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
q := adjustQuantityByMinAmount(test.args.quantity, test.args.price, test.args.minAmount)
|
||||
assert.Equal(t, test.wanted, q)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -6,49 +6,40 @@ import (
|
|||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
type SymbolBasedOrderExecutor struct {
|
||||
BasicRiskControlOrderExecutor *BasicRiskControlOrderExecutor `json:"basic,omitempty" yaml:"basic,omitempty"`
|
||||
type SymbolBasedRiskController struct {
|
||||
BasicRiskController *BasicRiskController `json:"basic,omitempty" yaml:"basic,omitempty"`
|
||||
}
|
||||
|
||||
type RiskControlOrderExecutors struct {
|
||||
type RiskControlOrderExecutor struct {
|
||||
*ExchangeOrderExecutor
|
||||
|
||||
// Symbol => Executor config
|
||||
BySymbol map[string]*SymbolBasedOrderExecutor `json:"bySymbol,omitempty" yaml:"bySymbol,omitempty"`
|
||||
BySymbol map[string]*SymbolBasedRiskController `json:"bySymbol,omitempty" yaml:"bySymbol,omitempty"`
|
||||
}
|
||||
|
||||
func (e *RiskControlOrderExecutors) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (types.OrderSlice, error) {
|
||||
var symbolOrders = make(map[string][]types.SubmitOrder, len(orders))
|
||||
for _, order := range orders {
|
||||
symbolOrders[order.Symbol] = append(symbolOrders[order.Symbol], order)
|
||||
}
|
||||
|
||||
var retOrders []types.Order
|
||||
|
||||
func (e *RiskControlOrderExecutor) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (retOrders types.OrderSlice, err error) {
|
||||
var symbolOrders = groupSubmitOrdersBySymbol(orders)
|
||||
for symbol, orders := range symbolOrders {
|
||||
var err error
|
||||
var retOrders2 []types.Order
|
||||
if exec, ok := e.BySymbol[symbol]; ok && exec.BasicRiskControlOrderExecutor != nil {
|
||||
retOrders2, err = exec.BasicRiskControlOrderExecutor.SubmitOrders(ctx, orders...)
|
||||
if controller, ok := e.BySymbol[symbol]; ok && controller != nil {
|
||||
orders, err = controller.BasicRiskController.ProcessOrders(e.session, orders...)
|
||||
if err != nil {
|
||||
return retOrders, err
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
retOrders2, err = e.ExchangeOrderExecutor.SubmitOrders(ctx, orders...)
|
||||
if err != nil {
|
||||
return retOrders, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
retOrders2, err := e.ExchangeOrderExecutor.SubmitOrders(ctx, orders...)
|
||||
if err != nil {
|
||||
return retOrders, err
|
||||
}
|
||||
|
||||
retOrders = append(retOrders, retOrders2...)
|
||||
}
|
||||
|
||||
return retOrders, nil
|
||||
return
|
||||
}
|
||||
|
||||
type SessionBasedRiskControl struct {
|
||||
OrderExecutor *RiskControlOrderExecutors `json:"orderExecutors,omitempty" yaml:"orderExecutors"`
|
||||
OrderExecutor *RiskControlOrderExecutor `json:"orderExecutor,omitempty" yaml:"orderExecutor"`
|
||||
}
|
||||
|
||||
func (control *SessionBasedRiskControl) SetBaseOrderExecutor(executor *ExchangeOrderExecutor) {
|
||||
|
@ -57,16 +48,15 @@ func (control *SessionBasedRiskControl) SetBaseOrderExecutor(executor *ExchangeO
|
|||
}
|
||||
|
||||
control.OrderExecutor.ExchangeOrderExecutor = executor
|
||||
}
|
||||
|
||||
if control.OrderExecutor.BySymbol == nil {
|
||||
return
|
||||
func groupSubmitOrdersBySymbol(orders []types.SubmitOrder) map[string][]types.SubmitOrder {
|
||||
var symbolOrders = make(map[string][]types.SubmitOrder, len(orders))
|
||||
for _, order := range orders {
|
||||
symbolOrders[order.Symbol] = append(symbolOrders[order.Symbol], order)
|
||||
}
|
||||
|
||||
for _, exec := range control.OrderExecutor.BySymbol {
|
||||
if exec.BasicRiskControlOrderExecutor != nil {
|
||||
exec.BasicRiskControlOrderExecutor.ExchangeOrderExecutor = executor
|
||||
}
|
||||
}
|
||||
return symbolOrders
|
||||
}
|
||||
|
||||
type RiskControls struct {
|
||||
|
|
2
pkg/bbgo/testdata/order_executor.yaml
vendored
2
pkg/bbgo/testdata/order_executor.yaml
vendored
|
@ -16,7 +16,7 @@ riskControls:
|
|||
sessionBased:
|
||||
# max is the session name that you want to configure the risk control
|
||||
max:
|
||||
orderExecutors:
|
||||
orderExecutor:
|
||||
bySymbol:
|
||||
BTCUSDT:
|
||||
basic:
|
||||
|
|
Loading…
Reference in New Issue
Block a user