diff --git a/doc/topics/riskcontrols.md b/doc/topics/riskcontrols.md index 90dfea94e..6a03885bd 100644 --- a/doc/topics/riskcontrols.md +++ b/doc/topics/riskcontrols.md @@ -51,7 +51,8 @@ s.circuitBreakRiskControl = riskcontrol.NewCircuitBreakRiskControl( s.Position, session.Indicators(s.Symbol).EWMA(s.CircuitBreakEMA), s.CircuitBreakLossThreshold, - s.ProfitStats) + s.ProfitStats, + 24*time.Hour) ``` Should pass in position and profit states. Also need an price EWMA to calculate unrealized profit. @@ -71,7 +72,7 @@ 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() { + if s.circuitBreakRiskControl.IsHalted(kline.EndTime) { return } diff --git a/pkg/risk/riskcontrol/circuit_break.go b/pkg/risk/riskcontrol/circuit_break.go index 753d8d236..921696e21 100644 --- a/pkg/risk/riskcontrol/circuit_break.go +++ b/pkg/risk/riskcontrol/circuit_break.go @@ -1,41 +1,68 @@ package riskcontrol import ( + "time" + log "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/indicator/v2" + indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2" "github.com/c9s/bbgo/pkg/types" ) type CircuitBreakRiskControl struct { // Since price could be fluctuated large, // use an EWMA to smooth it in running time - price *indicatorv2.EWMAStream - position *types.Position - profitStats *types.ProfitStats - lossThreshold fixedpoint.Value + price *indicatorv2.EWMAStream + position *types.Position + profitStats *types.ProfitStats + lossThreshold fixedpoint.Value + haltedDuration time.Duration + + isHalted bool + haltedAt time.Time } func NewCircuitBreakRiskControl( position *types.Position, price *indicatorv2.EWMAStream, lossThreshold fixedpoint.Value, - profitStats *types.ProfitStats) *CircuitBreakRiskControl { - + profitStats *types.ProfitStats, + haltedDuration time.Duration, +) *CircuitBreakRiskControl { return &CircuitBreakRiskControl{ - price: price, - position: position, - profitStats: profitStats, - lossThreshold: lossThreshold, + price: price, + position: position, + profitStats: profitStats, + lossThreshold: lossThreshold, + haltedDuration: haltedDuration, } } +func (c *CircuitBreakRiskControl) IsOverHaltedDuration() bool { + return time.Since(c.haltedAt) >= c.haltedDuration +} + // IsHalted returns whether we reached the circuit break condition set for this day? -func (c *CircuitBreakRiskControl) IsHalted() bool { +func (c *CircuitBreakRiskControl) IsHalted(t time.Time) bool { + if c.profitStats.IsOver24Hours() { + c.profitStats.ResetToday(t) + } + + // if we are not over the halted duration, we don't need to check the condition + if !c.IsOverHaltedDuration() { + return false + } + var unrealized = c.position.UnrealizedProfit(fixedpoint.NewFromFloat(c.price.Last(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 + + c.isHalted = unrealized.Add(c.profitStats.TodayPnL).Compare(c.lossThreshold) <= 0 + if c.isHalted { + c.haltedAt = t + } + + return c.isHalted } diff --git a/pkg/risk/riskcontrol/circuit_break_test.go b/pkg/risk/riskcontrol/circuit_break_test.go index 0e5d00dda..54f523d4b 100644 --- a/pkg/risk/riskcontrol/circuit_break_test.go +++ b/pkg/risk/riskcontrol/circuit_break_test.go @@ -2,6 +2,7 @@ package riskcontrol import ( "testing" + "time" "github.com/stretchr/testify/assert" @@ -68,11 +69,13 @@ func Test_IsHalted(t *testing.T) { }, priceEWMA, breakCondition, - &types.ProfitStats{ - TodayPnL: realizedPnL, - }, + &types.ProfitStats{}, + 24*time.Hour, ) - assert.Equal(t, tc.isHalted, riskControl.IsHalted()) + now := time.Now() + riskControl.profitStats.ResetToday(now) + riskControl.profitStats.TodayPnL = realizedPnL + assert.Equal(t, tc.isHalted, riskControl.IsHalted(now.Add(time.Hour))) }) } } diff --git a/pkg/strategy/common/strategy.go b/pkg/strategy/common/strategy.go index 5e2cd2c4d..4f159c7f6 100644 --- a/pkg/strategy/common/strategy.go +++ b/pkg/strategy/common/strategy.go @@ -2,6 +2,7 @@ package common import ( "context" + "time" log "github.com/sirupsen/logrus" @@ -83,6 +84,7 @@ func (s *Strategy) Initialize(ctx context.Context, environ *bbgo.Environment, se s.Position, session.Indicators(market.Symbol).EWMA(s.CircuitBreakEMA), s.CircuitBreakLossThreshold, - s.ProfitStats) + s.ProfitStats, + 24*time.Hour) } } diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 3abfad081..ecccefc8b 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "sync" + "time" log "github.com/sirupsen/logrus" @@ -123,7 +124,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.Position, session.Indicators(s.Symbol).EWMA(s.CircuitBreakEMA), s.CircuitBreakLossThreshold, - s.ProfitStats) + s.ProfitStats, + 24*time.Hour) } scale, err := s.LiquiditySlideRule.Scale() @@ -275,21 +277,21 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { } func (s *Strategy) placeLiquidityOrders(ctx context.Context) { - if s.circuitBreakRiskControl != nil && s.circuitBreakRiskControl.IsHalted() { + ticker, err := s.Session.Exchange.QueryTicker(ctx, s.Symbol) + if logErr(err, "unable to query ticker") { + return + } + + if s.circuitBreakRiskControl != nil && s.circuitBreakRiskControl.IsHalted(ticker.Time) { log.Warn("circuitBreakRiskControl: trading halted") return } - err := s.liquidityOrderBook.GracefulCancel(ctx, s.Session.Exchange) + err = s.liquidityOrderBook.GracefulCancel(ctx, s.Session.Exchange) if logErr(err, "unable to cancel orders") { return } - ticker, err := s.Session.Exchange.QueryTicker(ctx, s.Symbol) - if logErr(err, "unable to query ticker") { - return - } - if ticker.Buy.IsZero() && ticker.Sell.IsZero() { ticker.Sell = ticker.Last.Add(s.Market.TickSize) ticker.Buy = ticker.Last.Sub(s.Market.TickSize)