Merge pull request #1722 from c9s/c9s/xmaker/add-signals

FEATURE: [xmaker] add signals
This commit is contained in:
c9s 2024-08-30 17:50:52 +08:00 committed by GitHub
commit 04bed165d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 343 additions and 32 deletions

View File

@ -38,6 +38,12 @@ var askMarginMetrics = prometheus.NewGaugeVec(
Help: "the current ask margin (dynamic)", Help: "the current ask margin (dynamic)",
}, []string{"strategy_type", "strategy_id", "exchange", "symbol"}) }, []string{"strategy_type", "strategy_id", "exchange", "symbol"})
var aggregatedSignalMetrics = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "xmaker_aggregated_signal",
Help: "",
}, []string{"strategy_type", "strategy_id", "exchange", "symbol"})
var configNumOfLayersMetrics = prometheus.NewGaugeVec( var configNumOfLayersMetrics = prometheus.NewGaugeVec(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Name: "xmaker_config_num_of_layers", Name: "xmaker_config_num_of_layers",
@ -70,6 +76,7 @@ func init() {
makerBestAskPriceMetrics, makerBestAskPriceMetrics,
bidMarginMetrics, bidMarginMetrics,
askMarginMetrics, askMarginMetrics,
aggregatedSignalMetrics,
configNumOfLayersMetrics, configNumOfLayersMetrics,
configMaxExposureMetrics, configMaxExposureMetrics,
configBidMarginMetrics, configBidMarginMetrics,

View File

@ -0,0 +1,87 @@
package xmaker
import (
"context"
"github.com/prometheus/client_golang/prometheus"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator/v2"
"github.com/c9s/bbgo/pkg/types"
)
var bollingerBandSignalMetrics = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "xmaker_bollinger_band_signal",
Help: "",
}, []string{"symbol"})
func init() {
prometheus.MustRegister(bollingerBandSignalMetrics)
}
type BollingerBandTrendSignal struct {
types.IntervalWindow
MinBandWidth float64 `json:"minBandWidth"`
MaxBandWidth float64 `json:"maxBandWidth"`
indicator *indicatorv2.BOLLStream
symbol string
lastK *types.KLine
}
func (s *BollingerBandTrendSignal) Bind(ctx context.Context, session *bbgo.ExchangeSession, symbol string) error {
if s.MaxBandWidth == 0.0 {
s.MaxBandWidth = 2.0
}
if s.MinBandWidth == 0.0 {
s.MinBandWidth = 1.0
}
s.symbol = symbol
s.indicator = session.Indicators(symbol).BOLL(s.IntervalWindow, s.MinBandWidth)
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.symbol, s.IntervalWindow.Interval, func(kline types.KLine) {
s.lastK = &kline
}))
bollingerBandSignalMetrics.WithLabelValues(s.symbol).Set(0.0)
return nil
}
func (s *BollingerBandTrendSignal) CalculateSignal(ctx context.Context) (float64, error) {
if s.lastK == nil {
return 0, nil
}
closePrice := s.lastK.Close
// when bid price is lower than the down band, then it's in the downtrend
// when ask price is higher than the up band, then it's in the uptrend
lastDownBand := fixedpoint.NewFromFloat(s.indicator.DownBand.Last(0))
lastUpBand := fixedpoint.NewFromFloat(s.indicator.UpBand.Last(0))
maxBandWidth := s.indicator.StdDev.Last(0) * s.MaxBandWidth
signal := 0.0
// if the price is inside the band, do not vote
if closePrice.Compare(lastDownBand) > 0 && closePrice.Compare(lastUpBand) < 0 {
signal = 0.0
} else if closePrice.Compare(lastDownBand) < 0 {
signal = lastDownBand.Sub(closePrice).Float64() / maxBandWidth * -2.0
} else if closePrice.Compare(lastUpBand) > 0 {
signal = closePrice.Sub(lastUpBand).Float64() / maxBandWidth * 2.0
}
log.Infof("[BollingerBandTrendSignal] %f up/down = %f/%f, close price = %f",
signal,
lastUpBand.Float64(),
lastDownBand.Float64(),
closePrice.Float64())
bollingerBandSignalMetrics.WithLabelValues(s.symbol).Set(signal)
return signal, nil
}

View File

@ -0,0 +1,68 @@
package xmaker
import (
"context"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
var orderBookSignalMetrics = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "xmaker_order_book_signal",
Help: "",
}, []string{"symbol"})
func init() {
prometheus.MustRegister(orderBookSignalMetrics)
}
type OrderBookBestPriceVolumeSignal struct {
RatioThreshold fixedpoint.Value `json:"ratioThreshold"`
MinVolume fixedpoint.Value `json:"minVolume"`
symbol string
book *types.StreamOrderBook
}
func (s *OrderBookBestPriceVolumeSignal) Bind(ctx context.Context, session *bbgo.ExchangeSession, symbol string) error {
if s.book == nil {
return errors.New("s.book can not be nil")
}
s.symbol = symbol
orderBookSignalMetrics.WithLabelValues(s.symbol).Set(0.0)
return nil
}
func (s *OrderBookBestPriceVolumeSignal) CalculateSignal(ctx context.Context) (float64, error) {
bid, ask, ok := s.book.BestBidAndAsk()
if !ok {
return 0.0, nil
}
// TODO: may use scale to define this
sumVol := bid.Volume.Add(ask.Volume)
bidRatio := bid.Volume.Div(sumVol)
askRatio := ask.Volume.Div(sumVol)
denominator := fixedpoint.One.Sub(s.RatioThreshold)
signal := 0.0
if bid.Volume.Compare(s.MinVolume) < 0 && ask.Volume.Compare(s.MinVolume) < 0 {
signal = 0.0
} else if bidRatio.Compare(s.RatioThreshold) >= 0 {
numerator := bidRatio.Sub(s.RatioThreshold)
signal = numerator.Div(denominator).Float64()
} else if askRatio.Compare(s.RatioThreshold) >= 0 {
numerator := askRatio.Sub(s.RatioThreshold)
signal = -numerator.Div(denominator).Float64()
}
log.Infof("[OrderBookBestPriceVolumeSignal] %f bid/ask = %f/%f", signal, bid.Volume.Float64(), ask.Volume.Float64())
orderBookSignalMetrics.WithLabelValues(s.symbol).Set(signal)
return signal, nil
}

View File

@ -25,14 +25,47 @@ import (
var defaultMargin = fixedpoint.NewFromFloat(0.003) var defaultMargin = fixedpoint.NewFromFloat(0.003)
var two = fixedpoint.NewFromInt(2) var two = fixedpoint.NewFromInt(2)
var lastPriceModifier = fixedpoint.NewFromFloat(1.001)
const priceUpdateTimeout = 30 * time.Second const priceUpdateTimeout = 30 * time.Second
const ID = "xmaker" const ID = "xmaker"
var log = logrus.WithField("strategy", ID) var log = logrus.WithField("strategy", ID)
type Quote struct {
BestBidPrice, BestAskPrice fixedpoint.Value
BidMargin, AskMargin fixedpoint.Value
// BidLayerPips is the price pips between each layer
BidLayerPips, AskLayerPips fixedpoint.Value
}
type SessionBinder interface {
Bind(ctx context.Context, session *bbgo.ExchangeSession, symbol string) error
}
type SignalNumber float64
const (
SignalNumberMaxLong = 2.0
SignalNumberMaxShort = -2.0
)
type SignalProvider interface {
CalculateSignal(ctx context.Context) (float64, error)
}
type KLineShapeSignal struct {
FullBodyThreshold float64 `json:"fullBodyThreshold"`
}
type SignalConfig struct {
Weight float64 `json:"weight"`
BollingerBandTrendSignal *BollingerBandTrendSignal `json:"bollingerBandTrend,omitempty"`
OrderBookBestPriceSignal *OrderBookBestPriceVolumeSignal `json:"orderBookBestPrice,omitempty"`
KLineShapeSignal *KLineShapeSignal `json:"klineShape,omitempty"`
}
func init() { func init() {
bbgo.RegisterStrategy(ID, &Strategy{}) bbgo.RegisterStrategy(ID, &Strategy{})
} }
@ -52,6 +85,10 @@ type Strategy struct {
HedgeInterval types.Duration `json:"hedgeInterval"` HedgeInterval types.Duration `json:"hedgeInterval"`
OrderCancelWaitTime types.Duration `json:"orderCancelWaitTime"` OrderCancelWaitTime types.Duration `json:"orderCancelWaitTime"`
EnableSignalMargin bool `json:"enableSignalMargin"`
SignalConfigList []SignalConfig `json:"signals"`
SignalMarginScale *bbgo.SlideRule `json:"signalMarginScale,omitempty"`
Margin fixedpoint.Value `json:"margin"` Margin fixedpoint.Value `json:"margin"`
BidMargin fixedpoint.Value `json:"bidMargin"` BidMargin fixedpoint.Value `json:"bidMargin"`
AskMargin fixedpoint.Value `json:"askMargin"` AskMargin fixedpoint.Value `json:"askMargin"`
@ -140,6 +177,8 @@ type Strategy struct {
circuitBreakerAlertLimiter *rate.Limiter circuitBreakerAlertLimiter *rate.Limiter
logger logrus.FieldLogger logger logrus.FieldLogger
metricsLabels prometheus.Labels
} }
func (s *Strategy) ID() string { func (s *Strategy) ID() string {
@ -194,18 +233,16 @@ func (s *Strategy) Initialize() error {
"strategy": ID, "strategy": ID,
"strategy_id": s.InstanceID(), "strategy_id": s.InstanceID(),
}) })
s.metricsLabels = prometheus.Labels{
"strategy_type": ID,
"strategy_id": s.InstanceID(),
"exchange": s.MakerExchange,
"symbol": s.Symbol,
}
return nil return nil
} }
type Quote struct {
BestBidPrice, BestAskPrice fixedpoint.Value
BidMargin, AskMargin fixedpoint.Value
// BidLayerPips is the price pips between each layer
BidLayerPips, AskLayerPips fixedpoint.Value
}
// 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 // 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 { func (s *Strategy) getBollingerTrend(quote *Quote) int {
// when bid price is lower than the down band, then it's in the downtrend // when bid price is lower than the down band, then it's in the downtrend
@ -213,12 +250,6 @@ func (s *Strategy) getBollingerTrend(quote *Quote) int {
lastDownBand := fixedpoint.NewFromFloat(s.boll.DownBand.Last(0)) lastDownBand := fixedpoint.NewFromFloat(s.boll.DownBand.Last(0))
lastUpBand := fixedpoint.NewFromFloat(s.boll.UpBand.Last(0)) lastUpBand := fixedpoint.NewFromFloat(s.boll.UpBand.Last(0))
s.logger.Infof("bollinger band: up/down = %f/%f, bid/ask = %f/%f",
lastUpBand.Float64(),
lastDownBand.Float64(),
quote.BestBidPrice.Float64(),
quote.BestAskPrice.Float64())
if quote.BestAskPrice.Compare(lastDownBand) < 0 { if quote.BestAskPrice.Compare(lastDownBand) < 0 {
return -1 return -1
} else if quote.BestBidPrice.Compare(lastUpBand) > 0 { } else if quote.BestBidPrice.Compare(lastUpBand) > 0 {
@ -228,6 +259,43 @@ func (s *Strategy) getBollingerTrend(quote *Quote) int {
} }
} }
func (s *Strategy) applySignalMargin(ctx context.Context, quote *Quote) error {
signal, err := s.calculateSignal(ctx)
if err != nil {
return err
}
s.logger.Infof("aggregated signal: %f", signal)
scale, err := s.SignalMarginScale.Scale()
if err != nil {
return err
}
margin := scale.Call(signal)
s.logger.Infof("signal margin: %f", margin)
marginFp := fixedpoint.NewFromFloat(margin)
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())
} else if signal > 0.0 {
quote.AskMargin = quote.AskMargin.Add(marginFp)
if signal >= 2.0 {
// quote.AskMargin = fixedpoint.Zero
}
s.logger.Infof("adjusted ask margin: %f", quote.AskMargin.Float64())
}
return nil
}
// applyBollingerMargin applies the bollinger band margin to the quote // applyBollingerMargin applies the bollinger band margin to the quote
func (s *Strategy) applyBollingerMargin( func (s *Strategy) applyBollingerMargin(
quote *Quote, quote *Quote,
@ -284,6 +352,51 @@ func (s *Strategy) applyBollingerMargin(
return nil return nil
} }
func (s *Strategy) calculateSignal(ctx context.Context) (float64, error) {
sum := 0.0
voters := 0.0
for _, signal := range s.SignalConfigList {
if signal.OrderBookBestPriceSignal != nil {
sig, err := signal.OrderBookBestPriceSignal.CalculateSignal(ctx)
if err != nil {
return 0, err
}
if sig == 0.0 {
continue
}
if signal.Weight > 0.0 {
sum += sig * signal.Weight
voters += signal.Weight
} else {
sum += sig
voters++
}
} else if signal.BollingerBandTrendSignal != nil {
sig, err := signal.BollingerBandTrendSignal.CalculateSignal(ctx)
if err != nil {
return 0, err
}
if sig == 0.0 {
continue
}
if signal.Weight > 0.0 {
sum += sig * signal.Weight
voters += signal.Weight
} else {
sum += sig
voters++
}
}
}
return sum / voters, nil
}
func (s *Strategy) updateQuote(ctx context.Context) { func (s *Strategy) updateQuote(ctx context.Context) {
if err := s.activeMakerOrders.GracefulCancel(ctx, s.makerSession.Exchange); err != nil { if err := s.activeMakerOrders.GracefulCancel(ctx, s.makerSession.Exchange); err != nil {
s.logger.Warnf("there are some %s orders not canceled, skipping placing maker orders", s.Symbol) s.logger.Warnf("there are some %s orders not canceled, skipping placing maker orders", s.Symbol)
@ -295,6 +408,14 @@ func (s *Strategy) updateQuote(ctx context.Context) {
return return
} }
signal, err := s.calculateSignal(ctx)
if err != nil {
return
}
s.logger.Infof("aggregated signal: %f", signal)
aggregatedSignalMetrics.With(s.metricsLabels).Set(signal)
if s.CircuitBreaker != nil { if s.CircuitBreaker != nil {
now := time.Now() now := time.Now()
if reason, halted := s.CircuitBreaker.IsHalted(now); halted { if reason, halted := s.CircuitBreaker.IsHalted(now); halted {
@ -496,19 +617,17 @@ func (s *Strategy) updateQuote(ctx context.Context) {
AskLayerPips: s.Pips, AskLayerPips: s.Pips,
} }
if s.EnableBollBandMargin { if s.EnableSignalMargin {
if err := s.applySignalMargin(ctx, quote); err != nil {
s.logger.WithError(err).Errorf("unable to apply signal margin")
}
} else if s.EnableBollBandMargin {
if err := s.applyBollingerMargin(quote); err != nil { if err := s.applyBollingerMargin(quote); err != nil {
log.WithError(err).Errorf("unable to apply bollinger margin") log.WithError(err).Errorf("unable to apply bollinger margin")
} }
} }
labels := prometheus.Labels{
"strategy_type": ID,
"strategy_id": s.InstanceID(),
"exchange": s.MakerExchange,
"symbol": s.Symbol,
}
bidExposureInUsd := fixedpoint.Zero bidExposureInUsd := fixedpoint.Zero
askExposureInUsd := fixedpoint.Zero askExposureInUsd := fixedpoint.Zero
bidPrice := quote.BestBidPrice bidPrice := quote.BestBidPrice
@ -522,8 +641,8 @@ func (s *Strategy) updateQuote(ctx context.Context) {
return return
} }
bidMarginMetrics.With(labels).Set(quote.BidMargin.Float64()) bidMarginMetrics.With(s.metricsLabels).Set(quote.BidMargin.Float64())
askMarginMetrics.With(labels).Set(quote.AskMargin.Float64()) askMarginMetrics.With(s.metricsLabels).Set(quote.AskMargin.Float64())
for i := 0; i < s.NumLayers; i++ { for i := 0; i < s.NumLayers; i++ {
// for maker bid orders // for maker bid orders
@ -568,7 +687,7 @@ func (s *Strategy) updateQuote(ctx context.Context) {
if i == 0 { if i == 0 {
s.logger.Infof("maker best bid price %f", bidPrice.Float64()) s.logger.Infof("maker best bid price %f", bidPrice.Float64())
makerBestBidPriceMetrics.With(labels).Set(bidPrice.Float64()) makerBestBidPriceMetrics.With(s.metricsLabels).Set(bidPrice.Float64())
} }
if makerQuota.QuoteAsset.Lock(bidQuantity.Mul(bidPrice)) && hedgeQuota.BaseAsset.Lock(bidQuantity) { if makerQuota.QuoteAsset.Lock(bidQuantity.Mul(bidPrice)) && hedgeQuota.BaseAsset.Lock(bidQuantity) {
@ -636,7 +755,7 @@ func (s *Strategy) updateQuote(ctx context.Context) {
if i == 0 { if i == 0 {
s.logger.Infof("maker best ask price %f", askPrice.Float64()) s.logger.Infof("maker best ask price %f", askPrice.Float64())
makerBestAskPriceMetrics.With(labels).Set(askPrice.Float64()) makerBestAskPriceMetrics.With(s.metricsLabels).Set(askPrice.Float64())
} }
if makerQuota.BaseAsset.Lock(askQuantity) && hedgeQuota.QuoteAsset.Lock(askQuantity.Mul(askPrice)) { if makerQuota.BaseAsset.Lock(askQuantity) && hedgeQuota.QuoteAsset.Lock(askQuantity.Mul(askPrice)) {
@ -689,8 +808,8 @@ func (s *Strategy) updateQuote(ctx context.Context) {
log.WithError(err).Errorf("unable to place maker orders: %+v", formattedOrders) log.WithError(err).Errorf("unable to place maker orders: %+v", formattedOrders)
} }
openOrderBidExposureInUsdMetrics.With(labels).Set(bidExposureInUsd.Float64()) openOrderBidExposureInUsdMetrics.With(s.metricsLabels).Set(bidExposureInUsd.Float64())
openOrderAskExposureInUsdMetrics.With(labels).Set(askExposureInUsd.Float64()) openOrderAskExposureInUsdMetrics.With(s.metricsLabels).Set(askExposureInUsd.Float64())
_ = errIdx _ = errIdx
_ = createdOrders _ = createdOrders
@ -1041,7 +1160,6 @@ func (s *Strategy) hedgeWorker(ctx context.Context) {
func (s *Strategy) CrossRun( func (s *Strategy) CrossRun(
ctx context.Context, orderExecutionRouter bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession, ctx context.Context, orderExecutionRouter bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession,
) error { ) error {
instanceID := s.InstanceID() instanceID := s.InstanceID()
// configure sessions // configure sessions
@ -1127,6 +1245,14 @@ func (s *Strategy) CrossRun(
}) })
} }
s.sourceSession.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(k types.KLine) {
s.priceSolver.Update(k.Symbol, k.Close)
feeToken := s.sourceSession.Exchange.PlatformFeeCurrency()
if feePrice, ok := s.priceSolver.ResolvePrice(feeToken, "USDT"); ok {
s.Position.SetFeeAverageCost(feeToken, feePrice)
}
}))
if s.ProfitFixerConfig != nil { if s.ProfitFixerConfig != nil {
bbgo.Notify("Fixing %s profitStats and position...", s.Symbol) bbgo.Notify("Fixing %s profitStats and position...", s.Symbol)
@ -1171,6 +1297,29 @@ func (s *Strategy) CrossRun(
s.book = types.NewStreamBook(s.Symbol, s.sourceSession.ExchangeName) s.book = types.NewStreamBook(s.Symbol, s.sourceSession.ExchangeName)
s.book.BindStream(s.sourceSession.MarketDataStream) s.book.BindStream(s.sourceSession.MarketDataStream)
if s.EnableSignalMargin {
scale, err := s.SignalMarginScale.Scale()
if err != nil {
return err
}
if solveErr := scale.Solve(); solveErr != nil {
return solveErr
}
}
for _, signalConfig := range s.SignalConfigList {
if signalConfig.OrderBookBestPriceSignal != nil {
signalConfig.OrderBookBestPriceSignal.book = s.book
if err := signalConfig.OrderBookBestPriceSignal.Bind(ctx, s.sourceSession, s.Symbol); err != nil {
return err
}
} else if signalConfig.BollingerBandTrendSignal != nil {
if err := signalConfig.BollingerBandTrendSignal.Bind(ctx, s.sourceSession, s.Symbol); err != nil {
return err
}
}
}
s.activeMakerOrders = bbgo.NewActiveOrderBook(s.Symbol) s.activeMakerOrders = bbgo.NewActiveOrderBook(s.Symbol)
s.activeMakerOrders.BindStream(s.makerSession.UserDataStream) s.activeMakerOrders.BindStream(s.makerSession.UserDataStream)