mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-25 16:25:16 +00:00
Merge pull request #1219 from c9s/feature/scmaker-with-risk-control
FEATURE: [scmaker] integrate risk control
This commit is contained in:
commit
f6ad784583
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -67,5 +67,7 @@ func (f *Float64Series) Bind(source Float64Source, target Float64Calculator) {
|
|||
}
|
||||
}
|
||||
|
||||
f.Subscribe(source, c)
|
||||
if source != nil {
|
||||
f.Subscribe(source, c)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user