mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
Merge pull request #1212 from randych521/randy/feat/riskcontrol-for-mm
FEATURE: add risk controls for strategies
This commit is contained in:
commit
8a89408f0f
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