Merge pull request #1297 from c9s/narumi/reset-profit-stats

FIX: reset profit stats when over given duration in circuit break risk control
This commit is contained in:
なるみ 2023-09-05 14:00:52 +08:00 committed by GitHub
commit 9c104f5776
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 63 additions and 28 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,41 +1,68 @@
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"
) )
type CircuitBreakRiskControl struct { type CircuitBreakRiskControl struct {
// Since price could be fluctuated large, // Since price could be fluctuated large,
// use an EWMA to smooth it in running time // use an EWMA to smooth it in running time
price *indicatorv2.EWMAStream price *indicatorv2.EWMAStream
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)