refactor basic risk controller

This commit is contained in:
c9s 2020-11-09 14:56:54 +08:00
parent ded89e099f
commit 4a2a542222
10 changed files with 209 additions and 110 deletions

View File

@ -25,7 +25,7 @@ riskControls:
# "max" is the session name that you want to configure the risk control # "max" is the session name that you want to configure the risk control
max: max:
# orderExecutors is one of the risk control # orderExecutors is one of the risk control
orderExecutors: orderExecutor:
# symbol-routed order executor # symbol-routed order executor
bySymbol: bySymbol:
BTCUSDT: BTCUSDT:

View File

@ -31,8 +31,8 @@ riskControls:
sessionBased: sessionBased:
# "max" is the session name that you want to configure the risk control # "max" is the session name that you want to configure the risk control
max: max:
# orderExecutors is one of the risk control # orderExecutor is one of the risk control
orderExecutors: orderExecutor:
# symbol-routed order executor # symbol-routed order executor
bySymbol: bySymbol:
BTCUSDT: BTCUSDT:

View File

@ -31,8 +31,8 @@ riskControls:
sessionBased: sessionBased:
# "max" is the session name that you want to configure the risk control # "max" is the session name that you want to configure the risk control
max: max:
# orderExecutors is one of the risk control # orderExecutor is one of the risk control
orderExecutors: orderExecutor:
# symbol-routed order executor # symbol-routed order executor
bySymbol: bySymbol:
BNBUSDT: BNBUSDT:

View File

@ -27,8 +27,8 @@ riskControls:
sessionBased: sessionBased:
# "max" is the session name that you want to configure the risk control # "max" is the session name that you want to configure the risk control
binance: binance:
# orderExecutors is one of the risk control # orderExecutor is one of the risk control
orderExecutors: orderExecutor:
# symbol-routed order executor # symbol-routed order executor
bySymbol: bySymbol:
BNBUSDT: BNBUSDT:

1
go.mod
View File

@ -7,6 +7,7 @@ go 1.13
require ( require (
github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/adshao/go-binance v0.0.0-20201015231210-37cee298310e 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/c9s/goose v0.0.0-20200415105707-8da682162a5b
github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect
github.com/fsnotify/fsnotify v1.4.7 github.com/fsnotify/fsnotify v1.4.7

View File

@ -2,10 +2,10 @@ package bbgo
import ( import (
"context" "context"
"fmt"
"math" "math"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types" "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...) return e.session.Exchange.SubmitOrders(ctx, formattedOrders...)
} }
type BasicRiskControlOrderExecutor struct { type BasicRiskController struct {
*ExchangeOrderExecutor Logger *logrus.Logger
MinQuoteBalance fixedpoint.Value `json:"minQuoteBalance,omitempty"` MaxOrderAmount fixedpoint.Value `json:"maxOrderAmount,omitempty"`
MaxAssetBalance fixedpoint.Value `json:"maxBaseAssetBalance,omitempty"` MinQuoteBalance fixedpoint.Value `json:"minQuoteBalance,omitempty"`
MinAssetBalance fixedpoint.Value `json:"minBaseAssetBalance,omitempty"` MaxBaseAssetBalance fixedpoint.Value `json:"maxBaseAssetBalance,omitempty"`
MaxOrderAmount fixedpoint.Value `json:"maxOrderAmount,omitempty"` MinBaseAssetBalance fixedpoint.Value `json:"minBaseAssetBalance,omitempty"`
} }
func (e *BasicRiskControlOrderExecutor) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (types.OrderSlice, error) { // ProcessOrders filters and modifies the submit order objects by:
var formattedOrders []types.SubmitOrder // 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 { for _, order := range orders {
currentPrice, ok := e.session.LastPrice(order.Symbol) lastPrice, ok := session.LastPrice(order.Symbol)
if !ok { 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 quantity := order.Quantity
balances := e.session.Account.Balances() switch order.Type {
case types.OrderTypeMarket:
price = lastPrice
}
switch order.Side { switch order.Side {
case types.SideTypeBuy: 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 quoteBalance.Available < c.MinQuoteBalance.Float64() {
if balance.Available < e.MinQuoteBalance.Float64() { c.Logger.WithError(ErrQuoteBalanceLevelTooLow).Errorf("can not place buy order, quote balance level is too low: %s < %s",
return nil, errors.Wrapf(ErrQuoteBalanceLevelTooLow, "quote balance level is too low: %s < %s", types.USD.FormatMoneyFloat64(quoteBalance.Available),
types.USD.FormatMoneyFloat64(balance.Available), types.USD.FormatMoneyFloat64(c.MinQuoteBalance.Float64()))
types.USD.FormatMoneyFloat64(e.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 { baseAssetQuota := math.Max(0, c.MaxBaseAssetBalance.Float64()-baseBalance.Available)
if e.MaxAssetBalance > 0 && baseBalance.Available > e.MaxAssetBalance.Float64() { if quantity > baseAssetQuota {
return nil, errors.Wrapf(ErrAssetBalanceLevelTooHigh, "asset balance level is too high: %f > %f", baseBalance.Available, e.MaxAssetBalance.Float64()) quantity = baseAssetQuota
}
}
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)
} }
} }
// 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: 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 the amount is too small, we should increase it.
if e.MinAssetBalance > 0 && balance.Available < e.MinAssetBalance.Float64() { quantity = adjustQuantityByMinAmount(quantity, price, market.MinNotional*1.01)
return nil, errors.Wrapf(ErrAssetBalanceLevelTooLow, "asset balance level is too low: %f > %f", balance.Available, e.MinAssetBalance.Float64())
// 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) quantity = math.Min(quantity, baseAssetBalance.Available-c.MinBaseAssetBalance.Float64())
available := balance.Available
quantity = math.Min(quantity, available)
if quantity < market.MinQuantity { if quantity < market.MinQuantity {
return nil, errors.Wrapf(ErrInsufficientAssetBalance, "insufficient asset balance: %f > minimal quantity %f", available, market.MinQuantity) c.Logger.WithError(ErrInsufficientAssetBalance).Errorf("insufficient asset balance: %f > minimal quantity %f", baseAssetBalance.Available, market.MinQuantity)
} continue
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)
} }
} }
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 // update quantity and format the order
order.Quantity = quantity order.Quantity = quantity
o, err := formatOrder(order, e.session) outOrders = append(outOrders, order)
if err != nil {
return nil, err
}
formattedOrders = append(formattedOrders, o)
} }
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...) e.notifySubmitOrders(formattedOrders...)
return e.session.Exchange.SubmitOrders(ctx, formattedOrders...) return e.session.Exchange.SubmitOrders(ctx, formattedOrders...)

View File

@ -33,8 +33,8 @@ var (
} }
if baseBalance, ok := tradingCtx.Balances[market.BaseCurrency]; ok { if baseBalance, ok := tradingCtx.Balances[market.BaseCurrency]; ok {
if util.NotZero(p.MaxAssetBalance) && 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.MaxAssetBalance) return errors.Wrapf(ErrAssetBalanceLevelTooHigh, "asset balance level is too high: %f > %f", baseBalance.Available, p.MaxBaseAssetBalance)
} }
} }
@ -55,8 +55,8 @@ var (
case types.SideTypeSell: case types.SideTypeSell:
if balance, ok := tradingCtx.Balances[market.BaseCurrency]; ok { if balance, ok := tradingCtx.Balances[market.BaseCurrency]; ok {
if util.NotZero(p.MinAssetBalance) && 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.MinAssetBalance) return errors.Wrapf(ErrAssetBalanceLevelTooLow, "asset balance level is too low: %f > %f", balance.Available, p.MinBaseAssetBalance)
} }
quantity = adjustQuantityByMinAmount(quantity, currentPrice, market.MinNotional*1.01) quantity = adjustQuantityByMinAmount(quantity, currentPrice, market.MinNotional*1.01)
@ -101,7 +101,8 @@ var (
order.QuantityString = market.FormatVolume(quantity) 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 // modify quantity for the min amount
amount := currentPrice * quantity amount := currentPrice * quantity
if amount < minAmount { if amount < minAmount {
@ -112,8 +113,8 @@ func adjustQuantityByMinAmount(quantity float64, currentPrice float64, minAmount
return quantity return quantity
} }
func adjustQuantityByMaxAmount(quantity float64, currentPrice float64, maxAmount float64) float64 { func adjustQuantityByMaxAmount(quantity float64, price float64, maxAmount float64) float64 {
amount := currentPrice * quantity amount := price * quantity
if amount > maxAmount { if amount > maxAmount {
ratio := maxAmount / amount ratio := maxAmount / amount
quantity *= ratio quantity *= ratio

View 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)
})
}
}

View File

@ -6,49 +6,40 @@ import (
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
type SymbolBasedOrderExecutor struct { type SymbolBasedRiskController struct {
BasicRiskControlOrderExecutor *BasicRiskControlOrderExecutor `json:"basic,omitempty" yaml:"basic,omitempty"` BasicRiskController *BasicRiskController `json:"basic,omitempty" yaml:"basic,omitempty"`
} }
type RiskControlOrderExecutors struct { type RiskControlOrderExecutor struct {
*ExchangeOrderExecutor *ExchangeOrderExecutor
// Symbol => Executor config // 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) { func (e *RiskControlOrderExecutor) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (retOrders types.OrderSlice, err error) {
var symbolOrders = make(map[string][]types.SubmitOrder, len(orders)) var symbolOrders = groupSubmitOrdersBySymbol(orders)
for _, order := range orders {
symbolOrders[order.Symbol] = append(symbolOrders[order.Symbol], order)
}
var retOrders []types.Order
for symbol, orders := range symbolOrders { for symbol, orders := range symbolOrders {
var err error if controller, ok := e.BySymbol[symbol]; ok && controller != nil {
var retOrders2 []types.Order orders, err = controller.BasicRiskController.ProcessOrders(e.session, orders...)
if exec, ok := e.BySymbol[symbol]; ok && exec.BasicRiskControlOrderExecutor != nil {
retOrders2, err = exec.BasicRiskControlOrderExecutor.SubmitOrders(ctx, orders...)
if err != nil { 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...) retOrders = append(retOrders, retOrders2...)
} }
return retOrders, nil return
} }
type SessionBasedRiskControl struct { type SessionBasedRiskControl struct {
OrderExecutor *RiskControlOrderExecutors `json:"orderExecutors,omitempty" yaml:"orderExecutors"` OrderExecutor *RiskControlOrderExecutor `json:"orderExecutor,omitempty" yaml:"orderExecutor"`
} }
func (control *SessionBasedRiskControl) SetBaseOrderExecutor(executor *ExchangeOrderExecutor) { func (control *SessionBasedRiskControl) SetBaseOrderExecutor(executor *ExchangeOrderExecutor) {
@ -57,16 +48,15 @@ func (control *SessionBasedRiskControl) SetBaseOrderExecutor(executor *ExchangeO
} }
control.OrderExecutor.ExchangeOrderExecutor = executor control.OrderExecutor.ExchangeOrderExecutor = executor
}
if control.OrderExecutor.BySymbol == nil { func groupSubmitOrdersBySymbol(orders []types.SubmitOrder) map[string][]types.SubmitOrder {
return 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 { return symbolOrders
if exec.BasicRiskControlOrderExecutor != nil {
exec.BasicRiskControlOrderExecutor.ExchangeOrderExecutor = executor
}
}
} }
type RiskControls struct { type RiskControls struct {

View File

@ -16,7 +16,7 @@ riskControls:
sessionBased: sessionBased:
# max is the session name that you want to configure the risk control # max is the session name that you want to configure the risk control
max: max:
orderExecutors: orderExecutor:
bySymbol: bySymbol:
BTCUSDT: BTCUSDT:
basic: basic: