mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-21 14:33:53 +00:00
Merge pull request #1836 from c9s/c9s/xmaker-optimize
IMPROVE: [xmaker] add more improvements
This commit is contained in:
commit
7b391b15f1
|
@ -64,7 +64,7 @@ func TestDepthBuffer_CorruptedUpdateAtTheBeginning(t *testing.T) {
|
|||
{Price: itov(99), Volume: itov(1)},
|
||||
},
|
||||
}, snapshotFinalID, nil
|
||||
})
|
||||
}, time.Millisecond*5)
|
||||
|
||||
resetC := make(chan struct{}, 1)
|
||||
|
||||
|
@ -104,7 +104,7 @@ func TestDepthBuffer_ConcurrentRun(t *testing.T) {
|
|||
{Price: itov(99), Volume: itov(1)},
|
||||
},
|
||||
}, snapshotFinalID, nil
|
||||
})
|
||||
}, time.Millisecond*5)
|
||||
|
||||
readyCnt := 0
|
||||
resetCnt := 0
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
package xmaker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
@ -14,12 +17,14 @@ import (
|
|||
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
"github.com/c9s/bbgo/pkg/core"
|
||||
"github.com/c9s/bbgo/pkg/dynamic"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2"
|
||||
"github.com/c9s/bbgo/pkg/pricesolver"
|
||||
"github.com/c9s/bbgo/pkg/profile/timeprofile"
|
||||
"github.com/c9s/bbgo/pkg/risk/circuitbreaker"
|
||||
"github.com/c9s/bbgo/pkg/strategy/common"
|
||||
"github.com/c9s/bbgo/pkg/style"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
"github.com/c9s/bbgo/pkg/util"
|
||||
"github.com/c9s/bbgo/pkg/util/timejitter"
|
||||
|
@ -109,6 +114,29 @@ func init() {
|
|||
bbgo.RegisterStrategy(ID, &Strategy{})
|
||||
}
|
||||
|
||||
type SignalMargin struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Scale *bbgo.SlideRule `json:"scale,omitempty"`
|
||||
Threshold float64 `json:"threshold,omitempty"`
|
||||
}
|
||||
|
||||
type DelayedHedge struct {
|
||||
// EnableDelayHedge enables the delay hedge feature
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
// MaxDelayDuration is the maximum delay duration to hedge the position
|
||||
MaxDelayDuration types.Duration `json:"maxDelay"`
|
||||
|
||||
// FixedDelayDuration is the fixed delay duration
|
||||
FixedDelayDuration types.Duration `json:"fixedDelay"`
|
||||
|
||||
// SignalThreshold is the signal threshold to trigger the delay hedge
|
||||
SignalThreshold float64 `json:"signalThreshold"`
|
||||
|
||||
// DynamicDelayScale is the dynamic delay scale
|
||||
DynamicDelayScale *bbgo.SlideRule `json:"dynamicDelayScale,omitempty"`
|
||||
}
|
||||
|
||||
type Strategy struct {
|
||||
Environment *bbgo.Environment
|
||||
|
||||
|
@ -128,11 +156,18 @@ type Strategy struct {
|
|||
|
||||
EnableSignalMargin bool `json:"enableSignalMargin"`
|
||||
SignalConfigList []SignalConfig `json:"signals"`
|
||||
SignalMarginScale *bbgo.SlideRule `json:"signalMarginScale,omitempty"`
|
||||
|
||||
SignalReverseSideMargin *SignalMargin `json:"signalReverseSideMargin,omitempty"`
|
||||
SignalTrendSideMarginDiscount *SignalMargin `json:"signalTrendSideMarginDiscount,omitempty"`
|
||||
|
||||
// Margin is the default margin for the quote
|
||||
Margin fixedpoint.Value `json:"margin"`
|
||||
BidMargin fixedpoint.Value `json:"bidMargin"`
|
||||
AskMargin fixedpoint.Value `json:"askMargin"`
|
||||
|
||||
// MinMargin is the minimum margin protection for signal margin
|
||||
MinMargin *fixedpoint.Value `json:"minMargin"`
|
||||
|
||||
UseDepthPrice bool `json:"useDepthPrice"`
|
||||
DepthQuantity fixedpoint.Value `json:"depthQuantity"`
|
||||
SourceDepthLevel types.Depth `json:"sourceDepthLevel"`
|
||||
|
@ -143,6 +178,8 @@ type Strategy struct {
|
|||
MaxDelayHedgeDuration types.Duration `json:"maxHedgeDelayDuration"`
|
||||
DelayHedgeSignalThreshold float64 `json:"delayHedgeSignalThreshold"`
|
||||
|
||||
DelayedHedge *DelayedHedge `json:"delayedHedge,omitempty"`
|
||||
|
||||
EnableBollBandMargin bool `json:"enableBollBandMargin"`
|
||||
BollBandInterval types.Interval `json:"bollBandInterval"`
|
||||
BollBandMargin fixedpoint.Value `json:"bollBandMargin"`
|
||||
|
@ -179,6 +216,8 @@ type Strategy struct {
|
|||
|
||||
RecoverTradeScanPeriod types.Duration `json:"recoverTradeScanPeriod"`
|
||||
|
||||
MaxQuoteQuotaRatio fixedpoint.Value `json:"maxQuoteQuotaRatio,omitempty"`
|
||||
|
||||
NumLayers int `json:"numLayers"`
|
||||
|
||||
// Pips is the pips of the layer prices
|
||||
|
@ -317,9 +356,49 @@ func (s *Strategy) Initialize() error {
|
|||
"exchange": s.MakerExchange,
|
||||
"symbol": s.Symbol,
|
||||
}
|
||||
|
||||
if s.SignalReverseSideMargin != nil && s.SignalReverseSideMargin.Scale != nil {
|
||||
scale, err := s.SignalReverseSideMargin.Scale.Scale()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if solveErr := scale.Solve(); solveErr != nil {
|
||||
return solveErr
|
||||
}
|
||||
}
|
||||
|
||||
if s.SignalTrendSideMarginDiscount != nil && s.SignalTrendSideMarginDiscount.Scale != nil {
|
||||
scale, err := s.SignalTrendSideMarginDiscount.Scale.Scale()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if solveErr := scale.Solve(); solveErr != nil {
|
||||
return solveErr
|
||||
}
|
||||
}
|
||||
|
||||
if s.DelayedHedge != nil && s.DelayedHedge.DynamicDelayScale != nil {
|
||||
if scale, _ := s.DelayedHedge.DynamicDelayScale.Scale(); scale != nil {
|
||||
if err := scale.Solve(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) PrintConfig(f io.Writer, pretty bool, withColor ...bool) {
|
||||
var tableStyle *table.Style
|
||||
if pretty {
|
||||
tableStyle = style.NewDefaultTableStyle()
|
||||
}
|
||||
|
||||
dynamic.PrintConfig(s, f, tableStyle, len(withColor) > 0 && withColor[0], dynamic.DefaultWhiteList()...)
|
||||
}
|
||||
|
||||
// getBollingerTrend returns -1 when the price is in the downtrend, 1 when the price is in the uptrend, 0 when the price is in the band
|
||||
func (s *Strategy) getBollingerTrend(quote *Quote) int {
|
||||
// when bid price is lower than the down band, then it's in the downtrend
|
||||
|
@ -377,32 +456,65 @@ func (s *Strategy) applySignalMargin(ctx context.Context, quote *Quote) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
scale, err := s.SignalMarginScale.Scale()
|
||||
signalAbs := math.Abs(signal)
|
||||
|
||||
var trendSideMarginDiscount, reverseSideMargin float64
|
||||
var trendSideMarginDiscountFp, reverseSideMarginFp fixedpoint.Value
|
||||
if s.SignalTrendSideMarginDiscount != nil && s.SignalTrendSideMarginDiscount.Enabled {
|
||||
trendSideMarginScale, err := s.SignalTrendSideMarginDiscount.Scale.Scale()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
margin := scale.Call(math.Abs(signal))
|
||||
if signalAbs > s.SignalTrendSideMarginDiscount.Threshold {
|
||||
// trendSideMarginDiscount is the discount for the trend side margin
|
||||
trendSideMarginDiscount = trendSideMarginScale.Call(math.Abs(signal))
|
||||
trendSideMarginDiscountFp = fixedpoint.NewFromFloat(trendSideMarginDiscount)
|
||||
|
||||
s.logger.Infof("signal margin: %f", margin)
|
||||
if signal > 0.0 {
|
||||
quote.BidMargin = quote.BidMargin.Sub(trendSideMarginDiscountFp)
|
||||
} else if signal < 0.0 {
|
||||
quote.AskMargin = quote.AskMargin.Sub(trendSideMarginDiscountFp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
marginFp := fixedpoint.NewFromFloat(margin)
|
||||
if s.SignalReverseSideMargin != nil && s.SignalReverseSideMargin.Enabled {
|
||||
reverseSideMarginScale, err := s.SignalReverseSideMargin.Scale.Scale()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if signalAbs > s.SignalReverseSideMargin.Threshold {
|
||||
reverseSideMargin = reverseSideMarginScale.Call(math.Abs(signal))
|
||||
reverseSideMarginFp = fixedpoint.NewFromFloat(reverseSideMargin)
|
||||
if signal < 0.0 {
|
||||
quote.BidMargin = quote.BidMargin.Add(marginFp)
|
||||
if signal <= -2.0 {
|
||||
// quote.BidMargin = fixedpoint.Zero
|
||||
}
|
||||
|
||||
s.logger.Infof("adjusted bid margin: %f", quote.BidMargin.Float64())
|
||||
quote.BidMargin = quote.BidMargin.Add(reverseSideMarginFp)
|
||||
} else if signal > 0.0 {
|
||||
quote.AskMargin = quote.AskMargin.Add(marginFp)
|
||||
if signal >= 2.0 {
|
||||
// quote.AskMargin = fixedpoint.Zero
|
||||
quote.AskMargin = quote.AskMargin.Add(reverseSideMarginFp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Infof("adjusted ask margin: %f", quote.AskMargin.Float64())
|
||||
s.logger.Infof("signal margin params: signal = %f, reverseSideMargin = %f, trendSideMarginDiscount = %f", signal, reverseSideMargin, trendSideMarginDiscount)
|
||||
|
||||
s.logger.Infof("calculated signal margin: signal = %f, askMargin = %s, bidMargin = %s",
|
||||
signal,
|
||||
quote.AskMargin,
|
||||
quote.BidMargin,
|
||||
)
|
||||
|
||||
if s.MinMargin != nil {
|
||||
quote.AskMargin = fixedpoint.Max(*s.MinMargin, quote.AskMargin)
|
||||
quote.BidMargin = fixedpoint.Max(*s.MinMargin, quote.BidMargin)
|
||||
}
|
||||
|
||||
s.logger.Infof("final signal margin: signal = %f, askMargin = %s, bidMargin = %s",
|
||||
signal,
|
||||
quote.AskMargin,
|
||||
quote.BidMargin,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -696,7 +808,14 @@ func (s *Strategy) updateQuote(ctx context.Context) error {
|
|||
|
||||
if b, ok := makerBalances[s.makerMarket.QuoteCurrency]; ok {
|
||||
if b.Available.Compare(s.makerMarket.MinNotional) > 0 {
|
||||
if s.MaxQuoteQuotaRatio.Sign() > 0 {
|
||||
quoteAvailable := b.Available.Mul(s.MaxQuoteQuotaRatio)
|
||||
makerQuota.QuoteAsset.Add(quoteAvailable)
|
||||
} else {
|
||||
// use all quote balances as much as possible
|
||||
makerQuota.QuoteAsset.Add(b.Available)
|
||||
}
|
||||
|
||||
} else {
|
||||
disableMakerBid = true
|
||||
s.logger.Infof("%s maker bid disabled: insufficient quote balance %s", s.Symbol, b.String())
|
||||
|
@ -835,7 +954,7 @@ func (s *Strategy) updateQuote(ctx context.Context) error {
|
|||
}
|
||||
|
||||
if disableMakerAsk && disableMakerBid {
|
||||
log.Warnf("%s bid/ask maker is disabled due to insufficient balances", s.Symbol)
|
||||
log.Warnf("%s bid/ask maker is disabled", s.Symbol)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1178,14 +1297,16 @@ func AdjustHedgeQuantityWithAvailableBalance(
|
|||
return market.TruncateQuantity(quantity)
|
||||
}
|
||||
|
||||
func (s *Strategy) canDelayHedge(side types.SideType, pos fixedpoint.Value) bool {
|
||||
if !s.EnableDelayHedge {
|
||||
// canDelayHedge returns true if the hedge can be delayed
|
||||
func (s *Strategy) canDelayHedge(hedgeSide types.SideType, pos fixedpoint.Value) bool {
|
||||
if s.DelayedHedge == nil || !s.DelayedHedge.Enabled {
|
||||
return false
|
||||
}
|
||||
|
||||
signal := s.lastAggregatedSignal.Get()
|
||||
|
||||
if math.Abs(signal) < s.DelayHedgeSignalThreshold {
|
||||
signalAbs := math.Abs(signal)
|
||||
if signalAbs < s.DelayedHedge.SignalThreshold {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -1195,8 +1316,21 @@ func (s *Strategy) canDelayHedge(side types.SideType, pos fixedpoint.Value) bool
|
|||
return false
|
||||
}
|
||||
|
||||
if (signal > 0 && side == types.SideTypeSell) || (signal < 0 && side == types.SideTypeBuy) {
|
||||
if period < s.MaxDelayHedgeDuration.Duration() {
|
||||
var maxDelay = s.DelayedHedge.MaxDelayDuration.Duration()
|
||||
var delay = s.DelayedHedge.FixedDelayDuration.Duration()
|
||||
|
||||
if s.DelayedHedge.DynamicDelayScale != nil {
|
||||
if scale, _ := s.DelayedHedge.DynamicDelayScale.Scale(); scale != nil {
|
||||
delay = time.Duration(scale.Call(signalAbs)) * time.Millisecond
|
||||
}
|
||||
}
|
||||
|
||||
if delay > maxDelay {
|
||||
delay = maxDelay
|
||||
}
|
||||
|
||||
if (signal > 0 && hedgeSide == types.SideTypeSell) || (signal < 0 && hedgeSide == types.SideTypeBuy) {
|
||||
if period < delay {
|
||||
s.logger.Infof("delay hedge enabled, signal %f is strong enough, waiting for the next tick to hedge %s quantity (max period %s)", signal, pos, s.MaxDelayHedgeDuration.Duration().String())
|
||||
|
||||
delayHedgeCounterMetrics.With(s.metricsLabels).Inc()
|
||||
|
@ -1363,6 +1497,7 @@ func (s *Strategy) Defaults() error {
|
|||
if s.BollBandMarginFactor.IsZero() {
|
||||
s.BollBandMarginFactor = fixedpoint.One
|
||||
}
|
||||
|
||||
if s.BollBandMargin.IsZero() {
|
||||
s.BollBandMargin = fixedpoint.NewFromFloat(0.001)
|
||||
}
|
||||
|
@ -1410,6 +1545,42 @@ func (s *Strategy) Defaults() error {
|
|||
s.CircuitBreaker.SetMetricsInfo(ID, s.InstanceID(), s.Symbol)
|
||||
}
|
||||
|
||||
if s.EnableSignalMargin {
|
||||
if s.SignalReverseSideMargin.Scale == nil {
|
||||
s.SignalReverseSideMargin.Scale = &bbgo.SlideRule{
|
||||
ExpScale: &bbgo.ExponentialScale{
|
||||
Domain: [2]float64{0, 2.0},
|
||||
Range: [2]float64{0.00010, 0.00500},
|
||||
},
|
||||
QuadraticScale: nil,
|
||||
}
|
||||
}
|
||||
|
||||
if s.SignalTrendSideMarginDiscount.Scale == nil {
|
||||
s.SignalTrendSideMarginDiscount.Scale = &bbgo.SlideRule{
|
||||
ExpScale: &bbgo.ExponentialScale{
|
||||
Domain: [2]float64{0, 2.0},
|
||||
Range: [2]float64{0.00010, 0.00500},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if s.SignalTrendSideMarginDiscount.Threshold == 0.0 {
|
||||
s.SignalTrendSideMarginDiscount.Threshold = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
if s.DelayedHedge != nil {
|
||||
// default value protection for delayed hedge
|
||||
if s.DelayedHedge.MaxDelayDuration == 0 {
|
||||
s.DelayedHedge.MaxDelayDuration = types.Duration(3 * time.Second)
|
||||
}
|
||||
|
||||
if s.DelayedHedge.SignalThreshold == 0.0 {
|
||||
s.DelayedHedge.SignalThreshold = 0.5
|
||||
}
|
||||
}
|
||||
|
||||
// circuitBreakerAlertLimiter is for CircuitBreaker alerts
|
||||
s.circuitBreakerAlertLimiter = rate.NewLimiter(rate.Every(3*time.Minute), 2)
|
||||
s.reportProfitStatsRateLimiter = rate.NewLimiter(rate.Every(3*time.Minute), 1)
|
||||
|
@ -1579,6 +1750,10 @@ func (s *Strategy) CrossRun(
|
|||
) error {
|
||||
instanceID := s.InstanceID()
|
||||
|
||||
configWriter := bytes.NewBuffer(nil)
|
||||
s.PrintConfig(configWriter, true, false)
|
||||
s.logger.Infof("config: %s", configWriter.String())
|
||||
|
||||
// configure sessions
|
||||
sourceSession, ok := sessions[s.SourceExchange]
|
||||
if !ok {
|
||||
|
@ -1761,13 +1936,17 @@ func (s *Strategy) CrossRun(
|
|||
if s.EnableSignalMargin {
|
||||
s.logger.Infof("signal margin is enabled")
|
||||
|
||||
scale, err := s.SignalMarginScale.Scale()
|
||||
if err != nil {
|
||||
return err
|
||||
if s.SignalReverseSideMargin == nil || s.SignalReverseSideMargin.Scale == nil {
|
||||
return errors.New("signalReverseSideMarginScale can not be nil when signal margin is enabled")
|
||||
}
|
||||
|
||||
if solveErr := scale.Solve(); solveErr != nil {
|
||||
return solveErr
|
||||
if s.SignalTrendSideMarginDiscount == nil || s.SignalTrendSideMarginDiscount.Scale == nil {
|
||||
return errors.New("signalTrendSideMarginScale can not be nil when signal margin is enabled")
|
||||
}
|
||||
|
||||
scale, err := s.SignalReverseSideMargin.Scale.Scale()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
minAdditionalMargin := scale.Call(0.0)
|
||||
|
|
|
@ -81,6 +81,9 @@ func bindMockMarketDataStream(mockStream *mocks.MockStream, stream *types.Standa
|
|||
mockStream.EXPECT().OnConnect(Catch(func(x any) {
|
||||
stream.OnConnect(x.(func()))
|
||||
})).AnyTimes()
|
||||
mockStream.EXPECT().OnDisconnect(Catch(func(x any) {
|
||||
stream.OnDisconnect(x.(func()))
|
||||
})).AnyTimes()
|
||||
}
|
||||
|
||||
func bindMockUserDataStream(mockStream *mocks.MockStream, stream *types.StandardStream) {
|
||||
|
|
|
@ -212,6 +212,10 @@ func (sb *StreamOrderBook) updateMetrics(t time.Time) {
|
|||
}
|
||||
|
||||
func (sb *StreamOrderBook) BindStream(stream Stream) {
|
||||
stream.OnDisconnect(func() {
|
||||
sb.Reset()
|
||||
})
|
||||
|
||||
stream.OnBookSnapshot(func(book SliceOrderBook) {
|
||||
if sb.MutexOrderBook.Symbol != book.Symbol {
|
||||
return
|
||||
|
|
Loading…
Reference in New Issue
Block a user