Skip to content

Commit e7fd90e

Browse files
authored
Merge pull request #1708 from c9s/c9s/xmaker/integrate-circuitbreaker
FEATURE: [xmaker] integrate circuit breaker
2 parents 92ad80f + 6ef8aa6 commit e7fd90e

File tree

3 files changed

+51
-9
lines changed

3 files changed

+51
-9
lines changed

config/xmaker.yaml

+10-3
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ crossExchangeStrategies:
5959
# 0.1 pip is 0.01, here we use 10, so we will get 18000.00, 18001.00 and
6060
# 18002.00
6161
pips: 10
62-
persistence:
63-
type: redis
64-
62+
circuitBreaker:
63+
maximumConsecutiveTotalLoss: 36.0
64+
maximumConsecutiveLossTimes: 10
65+
maximumLossPerRound: 15.0
66+
maximumTotalLoss: 80.0
67+
ignoreConsecutiveDustLoss: true
68+
consecutiveDustLossThreshold: 0.003
69+
haltDuration: "30m"
70+
maximumHaltTimes: 2
71+
maximumHaltTimesExceededPanic: true

pkg/risk/circuitbreaker/basic.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,17 @@ type BasicCircuitBreaker struct {
116116
}
117117

118118
func NewBasicCircuitBreaker(strategyID, strategyInstance string) *BasicCircuitBreaker {
119-
return &BasicCircuitBreaker{
119+
b := &BasicCircuitBreaker{
120120
MaximumConsecutiveLossTimes: 8,
121121
MaximumHaltTimes: 3,
122122
MaximumHaltTimesExceededPanic: false,
123-
HaltDuration: types.Duration(30 * time.Minute),
123+
HaltDuration: types.Duration(1 * time.Hour),
124124
strategyID: strategyID,
125125
strategyInstance: strategyInstance,
126126
metricsLabels: prometheus.Labels{"strategy": strategyID, "strategyInstance": strategyInstance},
127127
}
128+
b.updateMetrics()
129+
return b
128130
}
129131

130132
func (b *BasicCircuitBreaker) getMetricsLabels() prometheus.Labels {
@@ -221,21 +223,21 @@ func (b *BasicCircuitBreaker) reset() {
221223
b.updateMetrics()
222224
}
223225

224-
func (b *BasicCircuitBreaker) IsHalted(now time.Time) bool {
226+
func (b *BasicCircuitBreaker) IsHalted(now time.Time) (string, bool) {
225227
b.mu.Lock()
226228
defer b.mu.Unlock()
227229

228230
if !b.halted {
229-
return false
231+
return "", false
230232
}
231233

232234
// check if it's an expired halt
233235
if now.After(b.haltTo) {
234236
b.reset()
235-
return false
237+
return "", false
236238
}
237239

238-
return true
240+
return b.haltReason, true
239241
}
240242

241243
func (b *BasicCircuitBreaker) halt(now time.Time, reason string) {

pkg/strategy/xmaker/strategy.go

+33
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@ import (
1414
"github.com/c9s/bbgo/pkg/core"
1515
"github.com/c9s/bbgo/pkg/fixedpoint"
1616
"github.com/c9s/bbgo/pkg/indicator"
17+
"github.com/c9s/bbgo/pkg/risk/circuitbreaker"
1718
"github.com/c9s/bbgo/pkg/types"
1819
"github.com/c9s/bbgo/pkg/util"
1920
)
2021

2122
var defaultMargin = fixedpoint.NewFromFloat(0.003)
2223
var Two = fixedpoint.NewFromInt(2)
2324

25+
// circuitBreakerAlertLimiter is for CircuitBreaker alerts
26+
var circuitBreakerAlertLimiter = rate.NewLimiter(rate.Every(3*time.Minute), 2)
27+
2428
const priceUpdateTimeout = 30 * time.Second
2529

2630
const ID = "xmaker"
@@ -98,6 +102,8 @@ type Strategy struct {
98102

99103
state *State
100104

105+
CircuitBreaker *circuitbreaker.BasicCircuitBreaker `json:"circuitBreaker"`
106+
101107
// persistence fields
102108
Position *types.Position `json:"position,omitempty" persistence:"position"`
103109
ProfitStats *ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
@@ -187,6 +193,19 @@ func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.Or
187193
return
188194
}
189195

196+
if s.CircuitBreaker != nil {
197+
now := time.Now()
198+
if reason, halted := s.CircuitBreaker.IsHalted(now); halted {
199+
log.Warnf("[arbWorker] strategy is halted, reason: %s", reason)
200+
201+
if circuitBreakerAlertLimiter.AllowN(now, 1) {
202+
bbgo.Notify("Strategy is halted, reason: %s", reason)
203+
}
204+
205+
return
206+
}
207+
}
208+
190209
bestBid, bestAsk, hasPrice := s.book.BestBidAndAsk()
191210
if !hasPrice {
192211
return
@@ -570,6 +589,8 @@ func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) {
570589

571590
log.Infof("submitting %s hedge order %s %v", s.Symbol, side.String(), quantity)
572591
bbgo.Notify("Submitting %s hedge order %s %v", s.Symbol, side.String(), quantity)
592+
593+
// TODO: improve order executor
573594
orderExecutor := &bbgo.ExchangeOrderExecutor{Session: s.sourceSession}
574595
returnOrders, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
575596
Market: s.sourceMarket,
@@ -630,6 +651,14 @@ func (s *Strategy) tradeRecover(ctx context.Context) {
630651
}
631652
}
632653

654+
func (s *Strategy) Defaults() error {
655+
if s.CircuitBreaker == nil {
656+
s.CircuitBreaker = circuitbreaker.NewBasicCircuitBreaker(ID, s.InstanceID())
657+
}
658+
659+
return nil
660+
}
661+
633662
func (s *Strategy) Validate() error {
634663
if s.Quantity.IsZero() || s.QuantityScale == nil {
635664
return errors.New("quantity or quantityScale can not be empty")
@@ -813,6 +842,10 @@ func (s *Strategy) CrossRun(
813842
s.ProfitStats.AddProfit(p)
814843

815844
s.Environment.RecordPosition(s.Position, trade, &p)
845+
846+
if s.CircuitBreaker != nil {
847+
s.CircuitBreaker.RecordProfit(profit, trade.Time.Time())
848+
}
816849
}
817850
})
818851

0 commit comments

Comments
 (0)