diff --git a/config/scmaker.yaml b/config/scmaker.yaml index 12e40fead..b73515347 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -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 diff --git a/doc/topics/riskcontrols.md b/doc/topics/riskcontrols.md index 8f548cc1a..90dfea94e 100644 --- a/doc/topics/riskcontrols.md +++ b/doc/topics/riskcontrols.md @@ -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: diff --git a/pkg/indicator/float64series.go b/pkg/indicator/float64series.go index c198e8e8d..8035acb96 100644 --- a/pkg/indicator/float64series.go +++ b/pkg/indicator/float64series.go @@ -67,5 +67,7 @@ func (f *Float64Series) Bind(source Float64Source, target Float64Calculator) { } } - f.Subscribe(source, c) + if source != nil { + f.Subscribe(source, c) + } } diff --git a/pkg/risk/riskcontrol/circuit_break.go b/pkg/risk/riskcontrol/circuit_break.go index db528b3a8..a89833ed8 100644 --- a/pkg/risk/riskcontrol/circuit_break.go +++ b/pkg/risk/riskcontrol/circuit_break.go @@ -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 } diff --git a/pkg/risk/riskcontrol/circuit_break_test.go b/pkg/risk/riskcontrol/circuit_break_test.go index 8d2252634..1ee01c59b 100644 --- a/pkg/risk/riskcontrol/circuit_break_test.go +++ b/pkg/risk/riskcontrol/circuit_break_test.go @@ -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 diff --git a/pkg/risk/riskcontrol/position.go b/pkg/risk/riskcontrol/position.go index 44022c55d..447e037ce 100644 --- a/pkg/risk/riskcontrol/position.go +++ b/pkg/risk/riskcontrol/position.go @@ -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) { +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) } diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 38056b630..df1e775cc 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -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,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 @@ -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