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,
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
}

View File

@ -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
}

View File

@ -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)))
})
}
}

View File

@ -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)
}
}

View File

@ -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)