fix: reset profit stats when over given duration in circuit break risk control

This commit is contained in:
narumi 2023-08-31 14:50:50 +08:00 committed by なるみ
parent 52412d9ead
commit 57198cc6b0
6 changed files with 64 additions and 34 deletions

View File

@ -51,7 +51,8 @@ s.circuitBreakRiskControl = riskcontrol.NewCircuitBreakRiskControl(
s.Position, s.Position,
session.Indicators(s.Symbol).EWMA(s.CircuitBreakEMA), session.Indicators(s.Symbol).EWMA(s.CircuitBreakEMA),
s.CircuitBreakLossThreshold, 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. 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: Check for circuit break before submitting orders:
``` ```
// Circuit break when accumulated losses are over break condition // Circuit break when accumulated losses are over break condition
if s.circuitBreakRiskControl.IsHalted() { if s.circuitBreakRiskControl.IsHalted(kline.EndTime) {
return return
} }

View File

@ -1,10 +1,12 @@
package riskcontrol package riskcontrol
import ( import (
"time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/fixedpoint" "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" "github.com/c9s/bbgo/pkg/types"
) )
@ -15,27 +17,52 @@ type CircuitBreakRiskControl struct {
position *types.Position position *types.Position
profitStats *types.ProfitStats profitStats *types.ProfitStats
lossThreshold fixedpoint.Value lossThreshold fixedpoint.Value
haltedDuration time.Duration
isHalted bool
haltedAt time.Time
} }
func NewCircuitBreakRiskControl( func NewCircuitBreakRiskControl(
position *types.Position, position *types.Position,
price *indicatorv2.EWMAStream, price *indicatorv2.EWMAStream,
lossThreshold fixedpoint.Value, lossThreshold fixedpoint.Value,
profitStats *types.ProfitStats) *CircuitBreakRiskControl { profitStats *types.ProfitStats,
haltedDuration time.Duration,
) *CircuitBreakRiskControl {
return &CircuitBreakRiskControl{ return &CircuitBreakRiskControl{
price: price, price: price,
position: position, position: position,
profitStats: profitStats, profitStats: profitStats,
lossThreshold: lossThreshold, 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? // 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))) var unrealized = c.position.UnrealizedProfit(fixedpoint.NewFromFloat(c.price.Last(0)))
log.Infof("[CircuitBreakRiskControl] realized PnL = %f, unrealized PnL = %f\n", log.Infof("[CircuitBreakRiskControl] realized PnL = %f, unrealized PnL = %f\n",
c.profitStats.TodayPnL.Float64(), c.profitStats.TodayPnL.Float64(),
unrealized.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
} }

View File

@ -2,6 +2,7 @@ package riskcontrol
import ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -68,11 +69,13 @@ func Test_IsHalted(t *testing.T) {
}, },
priceEWMA, priceEWMA,
breakCondition, breakCondition,
&types.ProfitStats{ &types.ProfitStats{},
TodayPnL: realizedPnL, 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)))
}) })
} }
} }

View File

@ -2,6 +2,7 @@ package common
import ( import (
"context" "context"
"time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -83,6 +84,7 @@ func (s *Strategy) Initialize(ctx context.Context, environ *bbgo.Environment, se
s.Position, s.Position,
session.Indicators(market.Symbol).EWMA(s.CircuitBreakEMA), session.Indicators(market.Symbol).EWMA(s.CircuitBreakEMA),
s.CircuitBreakLossThreshold, s.CircuitBreakLossThreshold,
s.ProfitStats) s.ProfitStats,
24*time.Hour)
} }
} }

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"math" "math"
"sync" "sync"
"time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -123,7 +124,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.Position, s.Position,
session.Indicators(s.Symbol).EWMA(s.CircuitBreakEMA), session.Indicators(s.Symbol).EWMA(s.CircuitBreakEMA),
s.CircuitBreakLossThreshold, s.CircuitBreakLossThreshold,
s.ProfitStats) s.ProfitStats,
24*time.Hour)
} }
scale, err := s.LiquiditySlideRule.Scale() scale, err := s.LiquiditySlideRule.Scale()
@ -275,21 +277,21 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) {
} }
func (s *Strategy) placeLiquidityOrders(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") log.Warn("circuitBreakRiskControl: trading halted")
return return
} }
err := s.liquidityOrderBook.GracefulCancel(ctx, s.Session.Exchange) err = s.liquidityOrderBook.GracefulCancel(ctx, s.Session.Exchange)
if logErr(err, "unable to cancel orders") { if logErr(err, "unable to cancel orders") {
return 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() { if ticker.Buy.IsZero() && ticker.Sell.IsZero() {
ticker.Sell = ticker.Last.Add(s.Market.TickSize) ticker.Sell = ticker.Last.Add(s.Market.TickSize)
ticker.Buy = ticker.Last.Sub(s.Market.TickSize) ticker.Buy = ticker.Last.Sub(s.Market.TickSize)

View File

@ -240,14 +240,9 @@ func (s *ProfitStats) AddTrade(trade Trade) {
s.AccumulatedVolume = s.AccumulatedVolume.Add(trade.Quantity) s.AccumulatedVolume = s.AccumulatedVolume.Add(trade.Quantity)
} }
// IsOver checks if the since time is over given duration
func (s *ProfitStats) IsOver(d time.Duration) bool {
return time.Since(time.Unix(s.TodaySince, 0)) >= d
}
// IsOver24Hours checks if the since time is over 24 hours // IsOver24Hours checks if the since time is over 24 hours
func (s *ProfitStats) IsOver24Hours() bool { func (s *ProfitStats) IsOver24Hours() bool {
return s.IsOver(24 * time.Hour) return time.Since(time.Unix(s.TodaySince, 0)) >= 24*time.Hour
} }
func (s *ProfitStats) ResetToday(t time.Time) { func (s *ProfitStats) ResetToday(t time.Time) {