Merge pull request #1327 from c9s/narumi/fix-position-risk

FIX: Fix duplicate orders caused by position risk control
This commit is contained in:
c9s 2023-10-11 15:43:26 +08:00 committed by GitHub
commit 2f65793522
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 58 additions and 24 deletions

View File

@ -1,10 +1,27 @@
--- ---
backtest:
startTime: "2023-01-01"
endTime: "2023-05-31"
symbols:
- USDCUSDT
sessions:
- max
accounts:
max:
balances:
USDC: 500.0
USDT: 500.0
exchangeStrategies: exchangeStrategies:
- on: max - on: max
fixedmaker: fixedmaker:
symbol: BTCUSDT symbol: USDCUSDT
interval: 5m interval: 5m
halfSpread: 0.05% halfSpread: 0.05%
quantity: 0.005 quantity: 15
orderType: LIMIT_MAKER orderType: LIMIT_MAKER
dryRun: true dryRun: false
positionHardLimit: 1500
maxPositionQuantity: 1500
circuitBreakLossThreshold: -0.15

View File

@ -24,6 +24,11 @@ type PositionRiskControl struct {
// only used in the ModifiedQuantity method // only used in the ModifiedQuantity method
sliceQuantity fixedpoint.Value sliceQuantity fixedpoint.Value
// activeOrderBook is used to store orders created by the risk control.
// This allows us to cancel them before submitting the position release
// orders, preventing duplicate orders.
activeOrderBook *bbgo.ActiveOrderBook
releasePositionCallbacks []func(quantity fixedpoint.Value, side types.SideType) releasePositionCallbacks []func(quantity fixedpoint.Value, side types.SideType)
} }
@ -34,26 +39,6 @@ func NewPositionRiskControl(orderExecutor bbgo.OrderExecutorExtended, hardLimit,
sliceQuantity: quantity, sliceQuantity: quantity,
} }
control.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) {
pos := orderExecutor.Position()
submitOrder := types.SubmitOrder{
Symbol: pos.Symbol,
Market: pos.Market,
Side: side,
Type: types.OrderTypeMarket,
Quantity: quantity,
}
log.Infof("RiskControl: position limit exceeded, submitting order to reduce position: %+v", submitOrder)
createdOrders, err := orderExecutor.SubmitOrders(context.Background(), submitOrder)
if err != nil {
log.WithError(err).Errorf("failed to submit orders")
return
}
log.Infof("created position release orders: %+v", createdOrders)
})
// register position update handler: check if position is over the hard limit // register position update handler: check if position is over the hard limit
orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
if fixedpoint.Compare(position.Base, hardLimit) > 0 { if fixedpoint.Compare(position.Base, hardLimit) > 0 {
@ -68,6 +53,37 @@ func NewPositionRiskControl(orderExecutor bbgo.OrderExecutorExtended, hardLimit,
return control return control
} }
func (p *PositionRiskControl) Initialize(ctx context.Context, session *bbgo.ExchangeSession) {
p.activeOrderBook = bbgo.NewActiveOrderBook("")
p.activeOrderBook.BindStream(session.UserDataStream)
p.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) {
if err := p.activeOrderBook.GracefulCancel(ctx, session.Exchange); err != nil {
log.WithError(err).Errorf("failed to cancel orders")
}
pos := p.orderExecutor.Position()
submitOrder := types.SubmitOrder{
Symbol: pos.Symbol,
Market: pos.Market,
Side: side,
Type: types.OrderTypeMarket,
Quantity: quantity,
}
log.Infof("RiskControl: position limit exceeded, submitting order to reduce position: %+v", submitOrder)
createdOrders, err := p.orderExecutor.SubmitOrders(ctx, submitOrder)
if err != nil {
log.WithError(err).Errorf("failed to submit orders")
return
}
log.Infof("created position release orders: %+v", createdOrders)
p.activeOrderBook.Add(createdOrders...)
})
}
// ModifiedQuantity returns sliceQuantity controlled by position risks // ModifiedQuantity returns sliceQuantity controlled by position risks
// For buy orders, modify sliceQuantity = min(hardLimit - position, sliceQuantity), limiting by positive position // For buy orders, modify sliceQuantity = min(hardLimit - position, sliceQuantity), limiting by positive position
// For sell orders, modify sliceQuantity = min(hardLimit - (-position), sliceQuantity), limiting by negative position // For sell orders, modify sliceQuantity = min(hardLimit - (-position), sliceQuantity), limiting by negative position

View File

@ -76,6 +76,7 @@ func (s *Strategy) Initialize(ctx context.Context, environ *bbgo.Environment, se
if !s.PositionHardLimit.IsZero() && !s.MaxPositionQuantity.IsZero() { if !s.PositionHardLimit.IsZero() && !s.MaxPositionQuantity.IsZero() {
log.Infof("positionHardLimit and maxPositionQuantity are configured, setting up PositionRiskControl...") log.Infof("positionHardLimit and maxPositionQuantity are configured, setting up PositionRiskControl...")
s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.OrderExecutor, s.PositionHardLimit, s.MaxPositionQuantity) s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.OrderExecutor, s.PositionHardLimit, s.MaxPositionQuantity)
s.positionRiskControl.Initialize(ctx, session)
} }
if !s.CircuitBreakLossThreshold.IsZero() { if !s.CircuitBreakLossThreshold.IsZero() {

View File

@ -63,7 +63,7 @@ func (s *Strategy) Validate() error {
} }
if s.HalfSpread.Float64() <= 0 { if s.HalfSpread.Float64() <= 0 {
return fmt.Errorf("halfSpreadRatio should be positive") return fmt.Errorf("halfSpread should be positive")
} }
return nil return nil
} }