mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-25 08:15:15 +00:00
Add two risk controls for strategies: postion and circuit break.
This commit is contained in:
parent
9254fb3b18
commit
9a98c4995e
87
doc/topics/riskcontrols.md
Normal file
87
doc/topics/riskcontrols.md
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
# Risk Control
|
||||||
|
------------
|
||||||
|
|
||||||
|
### 1. Introduction
|
||||||
|
|
||||||
|
Two types of risk controls for strategies is created:
|
||||||
|
- Position-limit Risk Control (pkg/risk/riskcontrol/position.go)
|
||||||
|
- Circuit-break Risk Control (pkg/risk/riskcontrol/circuit_break.go)
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Strategy should provide OnReleasePosition callback, which will be called when position (positive or negative) is over hard limit.
|
||||||
|
|
||||||
|
Modify quantity before submitting orders:
|
||||||
|
|
||||||
|
```
|
||||||
|
buyQuantity, sellQuantity := s.positionRiskControl.ModifiedQuantity(s.Position.Base)
|
||||||
|
```
|
||||||
|
|
||||||
|
It calculates buy and sell quantity shrinking by hard limit and position.
|
||||||
|
|
||||||
|
### 3. Circuit-Break Risk Control
|
||||||
|
|
||||||
|
Initialization
|
||||||
|
```
|
||||||
|
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
|
||||||
|
```
|
||||||
|
Circuit break condition should be non-greater than zero.
|
||||||
|
|
||||||
|
Check for circuit break before submitting orders:
|
||||||
|
```
|
||||||
|
// Circuit break when accumulated losses are over break condition
|
||||||
|
if s.circuitBreakRiskControl.IsHalted() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitOrders, err := s.generateSubmitOrders(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("failed to generate submit orders")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Infof("submit orders: %+v", submitOrders)
|
||||||
|
|
||||||
|
if s.DryRun {
|
||||||
|
log.Infof("dry run, not submitting orders")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice that if there are multiple place to submit orders, it is recommended to check in one place in Strategy.Run() and re-use that flag before submitting orders. That can avoid duplicated logs generated from IsHalted().
|
40
pkg/risk/riskcontrol/circuit_break.go
Normal file
40
pkg/risk/riskcontrol/circuit_break.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package riskcontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCircuitBreakRiskControl(
|
||||||
|
position *types.Position,
|
||||||
|
price *indicator.EWMA,
|
||||||
|
breakCondition fixedpoint.Value,
|
||||||
|
profitStats *types.ProfitStats) *CircuitBreakRiskControl {
|
||||||
|
|
||||||
|
return &CircuitBreakRiskControl{
|
||||||
|
price: price,
|
||||||
|
position: position,
|
||||||
|
profitStats: profitStats,
|
||||||
|
breakCondition: breakCondition,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
79
pkg/risk/riskcontrol/circuit_break_test.go
Normal file
79
pkg/risk/riskcontrol/circuit_break_test.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package riskcontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/indicator"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_IsHalted(t *testing.T) {
|
||||||
|
|
||||||
|
var (
|
||||||
|
price = 30000.00
|
||||||
|
realizedPnL = fixedpoint.NewFromFloat(-100.0)
|
||||||
|
breakCondition = fixedpoint.NewFromFloat(-500.00)
|
||||||
|
)
|
||||||
|
|
||||||
|
window := types.IntervalWindow{Window: 30, Interval: types.Interval1m}
|
||||||
|
priceEWMA := &indicator.EWMA{IntervalWindow: window}
|
||||||
|
priceEWMA.Update(price)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
position fixedpoint.Value
|
||||||
|
averageCost fixedpoint.Value
|
||||||
|
isHalted bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "PositivePositionReachBreakCondition",
|
||||||
|
position: fixedpoint.NewFromFloat(10.0),
|
||||||
|
averageCost: fixedpoint.NewFromFloat(30040.0),
|
||||||
|
isHalted: true,
|
||||||
|
}, {
|
||||||
|
name: "PositivePositionOverBreakCondition",
|
||||||
|
position: fixedpoint.NewFromFloat(10.0),
|
||||||
|
averageCost: fixedpoint.NewFromFloat(30050.0),
|
||||||
|
isHalted: true,
|
||||||
|
}, {
|
||||||
|
name: "PositivePositionUnderBreakCondition",
|
||||||
|
position: fixedpoint.NewFromFloat(10.0),
|
||||||
|
averageCost: fixedpoint.NewFromFloat(30030.0),
|
||||||
|
isHalted: false,
|
||||||
|
}, {
|
||||||
|
name: "NegativePositionReachBreakCondition",
|
||||||
|
position: fixedpoint.NewFromFloat(-10.0),
|
||||||
|
averageCost: fixedpoint.NewFromFloat(29960.0),
|
||||||
|
isHalted: true,
|
||||||
|
}, {
|
||||||
|
name: "NegativePositionOverBreakCondition",
|
||||||
|
position: fixedpoint.NewFromFloat(-10.0),
|
||||||
|
averageCost: fixedpoint.NewFromFloat(29950.0),
|
||||||
|
isHalted: true,
|
||||||
|
}, {
|
||||||
|
name: "NegativePositionUnderBreakCondition",
|
||||||
|
position: fixedpoint.NewFromFloat(-10.0),
|
||||||
|
averageCost: fixedpoint.NewFromFloat(29970.0),
|
||||||
|
isHalted: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var riskControl = NewCircuitBreakRiskControl(
|
||||||
|
&types.Position{
|
||||||
|
Base: tc.position,
|
||||||
|
AverageCost: tc.averageCost,
|
||||||
|
},
|
||||||
|
priceEWMA,
|
||||||
|
breakCondition,
|
||||||
|
&types.ProfitStats{
|
||||||
|
TodayPnL: realizedPnL,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert.Equal(t, tc.isHalted, riskControl.IsHalted())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
42
pkg/risk/riskcontrol/position.go
Normal file
42
pkg/risk/riskcontrol/position.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package riskcontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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
|
||||||
|
type PositionRiskControl struct {
|
||||||
|
hardLimit fixedpoint.Value
|
||||||
|
quantity fixedpoint.Value
|
||||||
|
|
||||||
|
releasePositionCallbacks []func(quantity fixedpoint.Value, side types.SideType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPositionRiskControl(hardLimit, quantity fixedpoint.Value, tradeCollector *bbgo.TradeCollector) *PositionRiskControl {
|
||||||
|
p := &PositionRiskControl{
|
||||||
|
hardLimit: hardLimit,
|
||||||
|
quantity: quantity,
|
||||||
|
}
|
||||||
|
// register position update handler: check if position is over hardlimit
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
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) {
|
||||||
|
return fixedpoint.Min(p.hardLimit.Sub(position), p.quantity),
|
||||||
|
fixedpoint.Min(p.hardLimit.Add(position), p.quantity)
|
||||||
|
}
|
92
pkg/risk/riskcontrol/position_test.go
Normal file
92
pkg/risk/riskcontrol/position_test.go
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
package riskcontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/bbgo"
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_ModifiedQuantity(t *testing.T) {
|
||||||
|
|
||||||
|
riskControl := NewPositionRiskControl(fixedpoint.NewFromInt(10), fixedpoint.NewFromInt(2), &bbgo.TradeCollector{})
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
position fixedpoint.Value
|
||||||
|
buyQuantity fixedpoint.Value
|
||||||
|
sellQuantity fixedpoint.Value
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "BuyOverHardLimit",
|
||||||
|
position: fixedpoint.NewFromInt(9),
|
||||||
|
buyQuantity: fixedpoint.NewFromInt(1),
|
||||||
|
sellQuantity: fixedpoint.NewFromInt(2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SellOverHardLimit",
|
||||||
|
position: fixedpoint.NewFromInt(-9),
|
||||||
|
buyQuantity: fixedpoint.NewFromInt(2),
|
||||||
|
sellQuantity: fixedpoint.NewFromInt(1),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
buyQuantity, sellQuantity := riskControl.ModifiedQuantity(tc.position)
|
||||||
|
assert.Equal(t, tc.buyQuantity, buyQuantity)
|
||||||
|
assert.Equal(t, tc.sellQuantity, sellQuantity)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReleasePositionCallbacks(t *testing.T) {
|
||||||
|
|
||||||
|
var position fixedpoint.Value
|
||||||
|
|
||||||
|
tradeCollector := &bbgo.TradeCollector{}
|
||||||
|
riskControl := NewPositionRiskControl(fixedpoint.NewFromInt(10), fixedpoint.NewFromInt(2), tradeCollector)
|
||||||
|
riskControl.OnReleasePosition(func(quantity fixedpoint.Value, side types.SideType) {
|
||||||
|
if side == types.SideTypeBuy {
|
||||||
|
position = position.Add(quantity)
|
||||||
|
} else {
|
||||||
|
position = position.Sub(quantity)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
position fixedpoint.Value
|
||||||
|
resultPosition fixedpoint.Value
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "PostivePositionWithinLimit",
|
||||||
|
position: fixedpoint.NewFromInt(8),
|
||||||
|
resultPosition: fixedpoint.NewFromInt(8),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NegativePositionWithinLimit",
|
||||||
|
position: fixedpoint.NewFromInt(-8),
|
||||||
|
resultPosition: fixedpoint.NewFromInt(-8),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PostivePositionOverLimit",
|
||||||
|
position: fixedpoint.NewFromInt(11),
|
||||||
|
resultPosition: fixedpoint.NewFromInt(10),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NegativePositionOverLimit",
|
||||||
|
position: fixedpoint.NewFromInt(-11),
|
||||||
|
resultPosition: fixedpoint.NewFromInt(-10),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
position = tc.position
|
||||||
|
tradeCollector.EmitPositionUpdate(&types.Position{Base: tc.position})
|
||||||
|
assert.Equal(t, tc.resultPosition, position)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
18
pkg/risk/riskcontrol/positionriskcontrol_callbacks.go
Normal file
18
pkg/risk/riskcontrol/positionriskcontrol_callbacks.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// Code generated by "callbackgen -type PositionRiskControl"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package riskcontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *PositionRiskControl) OnReleasePosition(cb func(quantity fixedpoint.Value, side types.SideType)) {
|
||||||
|
p.releasePositionCallbacks = append(p.releasePositionCallbacks, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PositionRiskControl) EmitReleasePosition(quantity fixedpoint.Value, side types.SideType) {
|
||||||
|
for _, cb := range p.releasePositionCallbacks {
|
||||||
|
cb(quantity, side)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user