Skip to content

Commit

Permalink
all: refactor risk control and integrate risk control into scmaker
Browse files Browse the repository at this point in the history
  • Loading branch information
c9s committed Jul 3, 2023
1 parent 3052dd5 commit ae3f371
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 52 deletions.
18 changes: 18 additions & 0 deletions config/scmaker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 33 additions & 29 deletions doc/topics/riskcontrols.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion pkg/indicator/float64series.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,7 @@ func (f *Float64Series) Bind(source Float64Source, target Float64Calculator) {
}
}

f.Subscribe(source, c)
if source != nil {
f.Subscribe(source, c)
}
}
31 changes: 16 additions & 15 deletions pkg/risk/riskcontrol/circuit_break.go
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 2 additions & 2 deletions pkg/risk/riskcontrol/circuit_break_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 8 additions & 5 deletions pkg/risk/riskcontrol/position.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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) {
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)
}
45 changes: 45 additions & 0 deletions pkg/strategy/scmaker/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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"`

Expand All @@ -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 {
Expand Down Expand Up @@ -126,6 +136,36 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.ProfitStats = types.NewProfitStats(s.Market)
}

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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit ae3f371

Please sign in to comment.