Merge pull request #1219 from c9s/feature/scmaker-with-risk-control

FEATURE: [scmaker] integrate risk control
This commit is contained in:
c9s 2023-07-03 17:50:16 +08:00 committed by GitHub
commit f6ad784583
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 135 additions and 62 deletions

View File

@ -23,8 +23,26 @@ exchangeStrategies:
domain: [0, 9]
range: [1, 4]
## maxExposure controls how much balance should be used for placing the maker orders
maxExposure: 10_000
## circuitBreakEMA is used for calculating the price for circuitBreak
circuitBreakEMA:
interval: 1m
window: 14
## circuitBreakLossThreshold is the maximum loss threshold for realized+unrealized PnL
circuitBreakLossThreshold: -10.0
## positionHardLimit is the maximum position limit
positionHardLimit: 500.0
## maxPositionQuantity is the maximum quantity per order that could be controlled in positionHardLimit,
## this parameter is used with positionHardLimit togerther
maxPositionQuantity: 10.0
midPriceEMA:
interval: 1h
window: 99

View File

@ -10,22 +10,26 @@ Two types of risk controls for strategies is created:
### 2. Position-Limit Risk Control
Initialization:
```
s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.HardLimit, s.Quantity, s.orderExecutor.TradeCollector())
s.positionRiskControl.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) {
createdOrders, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
Market: s.Market,
Side: side,
Type: types.OrderTypeMarket,
Quantity: quantity,
})
if err != nil {
log.WithError(err).Errorf("failed to submit orders")
return
}
log.Infof("created orders: %+v", createdOrders)
```go
s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.PositionHardLimit, s.MaxPositionQuantity, s.orderExecutor.TradeCollector())
s.positionRiskControl.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) {
createdOrders, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
Market: s.Market,
Side: side,
Type: types.OrderTypeMarket,
Quantity: quantity,
})
if err != nil {
log.WithError(err).Errorf("failed to submit orders")
return
}
log.Infof("created position release orders: %+v", createdOrders)
})
```
Strategy should provide OnReleasePosition callback, which will be called when position (positive or negative) is over hard limit.
@ -41,27 +45,27 @@ It calculates buy and sell quantity shrinking by hard limit and position.
### 3. Circuit-Break Risk Control
Initialization
```go
s.circuitBreakRiskControl = riskcontrol.NewCircuitBreakRiskControl(
s.Position,
session.Indicators(s.Symbol).EWMA(s.CircuitBreakEMA),
s.CircuitBreakLossThreshold,
s.ProfitStats)
```
s.circuitBreakRiskControl = riskcontrol.NewCircuitBreakRiskControl(
s.Position,
session.StandardIndicatorSet(s.Symbol).EWMA(
types.IntervalWindow{
Window: EWMAWindow,
Interval: types.Interval1m,
}),
s.CircuitBreakCondition,
s.ProfitStats)
```
Should pass in position and profit states. Also need an price EWMA to calculate unrealized profit.
Validate parameters:
```
if s.CircuitBreakCondition.Float64() > 0 {
return fmt.Errorf("circuitBreakCondition should be non-positive")
}
return nil
if s.CircuitBreakLossThreshold.Float64() > 0 {
return fmt.Errorf("circuitBreakLossThreshold should be non-positive")
}
return nil
```
Circuit break condition should be non-greater than zero.
Check for circuit break before submitting orders:

View File

@ -67,5 +67,7 @@ func (f *Float64Series) Bind(source Float64Source, target Float64Calculator) {
}
}
f.Subscribe(source, c)
if source != nil {
f.Subscribe(source, c)
}
}

View File

@ -1,40 +1,41 @@
package riskcontrol
import (
log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
log "github.com/sirupsen/logrus"
)
type CircuitBreakRiskControl struct {
// Since price could be fluctuated large,
// use an EWMA to smooth it in running time
price *indicator.EWMA
position *types.Position
profitStats *types.ProfitStats
breakCondition fixedpoint.Value
price *indicator.EWMAStream
position *types.Position
profitStats *types.ProfitStats
lossThreshold fixedpoint.Value
}
func NewCircuitBreakRiskControl(
position *types.Position,
price *indicator.EWMA,
breakCondition fixedpoint.Value,
price *indicator.EWMAStream,
lossThreshold fixedpoint.Value,
profitStats *types.ProfitStats) *CircuitBreakRiskControl {
return &CircuitBreakRiskControl{
price: price,
position: position,
profitStats: profitStats,
breakCondition: breakCondition,
price: price,
position: position,
profitStats: profitStats,
lossThreshold: lossThreshold,
}
}
// IsHalted returns whether we reached the circuit break condition set for this day?
func (c *CircuitBreakRiskControl) IsHalted() bool {
var unrealized = c.position.UnrealizedProfit(fixedpoint.NewFromFloat(c.price.Last(0)))
log.Infof("[CircuitBreakRiskControl] Realized P&L = %v, Unrealized P&L = %v\n",
c.profitStats.TodayPnL,
unrealized)
return unrealized.Add(c.profitStats.TodayPnL).Compare(c.breakCondition) <= 0
log.Infof("[CircuitBreakRiskControl] realized PnL = %f, unrealized PnL = %f\n",
c.profitStats.TodayPnL.Float64(),
unrealized.Float64())
return unrealized.Add(c.profitStats.TodayPnL).Compare(c.lossThreshold) <= 0
}

View File

@ -19,8 +19,8 @@ func Test_IsHalted(t *testing.T) {
)
window := types.IntervalWindow{Window: 30, Interval: types.Interval1m}
priceEWMA := &indicator.EWMA{IntervalWindow: window}
priceEWMA.Update(price)
priceEWMA := indicator.EWMA2(nil, window.Window)
priceEWMA.PushAndEmit(price)
cases := []struct {
name string

View File

@ -1,10 +1,11 @@
package riskcontrol
import (
log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
log "github.com/sirupsen/logrus"
)
//go:generate callbackgen -type PositionRiskControl
@ -20,23 +21,25 @@ func NewPositionRiskControl(hardLimit, quantity fixedpoint.Value, tradeCollector
hardLimit: hardLimit,
quantity: quantity,
}
// register position update handler: check if position is over hardlimit
// register position update handler: check if position is over the hard limit
tradeCollector.OnPositionUpdate(func(position *types.Position) {
if fixedpoint.Compare(position.Base, hardLimit) > 0 {
log.Infof("Position %v is over hardlimit %v, releasing:\n", position.Base, hardLimit)
log.Infof("position %f is over hardlimit %f, releasing position...", position.Base.Float64(), hardLimit.Float64())
p.EmitReleasePosition(position.Base.Sub(hardLimit), types.SideTypeSell)
} else if fixedpoint.Compare(position.Base, hardLimit.Neg()) < 0 {
log.Infof("Position %v is over hardlimit %v, releasing:\n", position.Base, hardLimit)
log.Infof("position %f is over hardlimit %f, releasing position...", position.Base.Float64(), hardLimit.Float64())
p.EmitReleasePosition(position.Base.Neg().Sub(hardLimit), types.SideTypeBuy)
}
})
return p
}
// ModifiedQuantity returns quantity controlled by position risks
// For buy orders, mod quantity = min(hardlimit - position, quanity), limiting by positive position
// For sell orders, mod quantity = min(hardlimit - (-position), quanity), limiting by negative position
func (p *PositionRiskControl) ModifiedQuantity(position fixedpoint.Value) (buyQuanity, sellQuantity fixedpoint.Value) {
// For buy orders, mod quantity = min(hardLimit - position, quantity), limiting by positive position
// For sell orders, mod quantity = min(hardLimit - (-position), quantity), limiting by negative position
func (p *PositionRiskControl) ModifiedQuantity(position fixedpoint.Value) (buyQuantity, sellQuantity fixedpoint.Value) {
return fixedpoint.Min(p.hardLimit.Sub(position), p.quantity),
fixedpoint.Min(p.hardLimit.Add(position), p.quantity)
}

View File

@ -11,6 +11,7 @@ import (
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/risk/riskcontrol"
"github.com/c9s/bbgo/pkg/types"
)
@ -57,6 +58,12 @@ type Strategy struct {
MinProfit fixedpoint.Value `json:"minProfit"`
// risk related parameters
PositionHardLimit fixedpoint.Value `json:"positionHardLimit"`
MaxPositionQuantity fixedpoint.Value `json:"maxPositionQuantity"`
CircuitBreakLossThreshold fixedpoint.Value `json:"circuitBreakLossThreshold"`
CircuitBreakEMA types.IntervalWindow `json:"circuitBreakEMA"`
Position *types.Position `json:"position,omitempty" persistence:"position"`
ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
@ -71,6 +78,9 @@ type Strategy struct {
ewma *indicator.EWMAStream
boll *indicator.BOLLStream
intensity *IntensityStream
positionRiskControl *riskcontrol.PositionRiskControl
circuitBreakRiskControl *riskcontrol.CircuitBreakRiskControl
}
func (s *Strategy) ID() string {
@ -126,6 +136,44 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.ProfitStats = types.NewProfitStats(s.Market)
}
s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)
s.orderExecutor.BindEnvironment(s.Environment)
s.orderExecutor.BindProfitStats(s.ProfitStats)
s.orderExecutor.Bind()
s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
bbgo.Sync(ctx, s)
})
if !s.PositionHardLimit.IsZero() && !s.MaxPositionQuantity.IsZero() {
log.Infof("positionHardLimit and maxPositionQuantity are configured, setting up PositionRiskControl...")
s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.PositionHardLimit, s.MaxPositionQuantity, s.orderExecutor.TradeCollector())
s.positionRiskControl.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) {
createdOrders, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
Market: s.Market,
Side: side,
Type: types.OrderTypeMarket,
Quantity: quantity,
})
if err != nil {
log.WithError(err).Errorf("failed to submit orders")
return
}
log.Infof("created position release orders: %+v", createdOrders)
})
}
if !s.CircuitBreakLossThreshold.IsZero() {
log.Infof("circuitBreakLossThreshold is configured, setting up CircuitBreakRiskControl...")
s.circuitBreakRiskControl = riskcontrol.NewCircuitBreakRiskControl(
s.Position,
session.Indicators(s.Symbol).EWMA(s.CircuitBreakEMA),
s.CircuitBreakLossThreshold,
s.ProfitStats)
}
scale, err := s.LiquiditySlideRule.Scale()
if err != nil {
return err
@ -141,14 +189,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.liquidityScale = scale
s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)
s.orderExecutor.BindEnvironment(s.Environment)
s.orderExecutor.BindProfitStats(s.ProfitStats)
s.orderExecutor.Bind()
s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
bbgo.Sync(ctx, s)
})
s.initializeMidPriceEMA(session)
s.initializePriceRangeBollinger(session)
s.initializeIntensityIndicator(session)
@ -283,6 +323,11 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) {
}
func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
if s.circuitBreakRiskControl != nil && s.circuitBreakRiskControl.IsHalted() {
log.Warn("circuitBreakRiskControl: trading halted")
return
}
err := s.liquidityOrderBook.GracefulCancel(ctx, s.session.Exchange)
if logErr(err, "unable to cancel orders") {
return