xmaker: add signal providers

This commit is contained in:
c9s 2024-08-30 15:44:55 +08:00
parent d9fb9ff3e0
commit 9ebab4f4f7
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
4 changed files with 289 additions and 29 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 finalSignalMetrics = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "xmaker_final_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,
finalSignalMetrics,
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))
log.Infof("bollinger band: up/down = %f/%f, close price = %f",
lastUpBand.Float64(),
lastDownBand.Float64(),
closePrice.Float64())
// if the price is inside the band, do not vote
if closePrice.Compare(lastDownBand) > 0 && closePrice.Compare(lastUpBand) < 0 {
return 0.0, nil
}
maxBandWidth := s.indicator.StdDev.Last(0) * s.MaxBandWidth
signal := 0.0
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("bollinger signal: %f", signal)
return signal, nil
}

View File

@ -0,0 +1,70 @@
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
}
if bid.Volume.Compare(s.MinVolume) < 0 && ask.Volume.Compare(s.MinVolume) < 0 {
return 0.0, nil
}
log.Infof("OrderBookBestPriceVolumeSignal: bid/ask = %f/%f", bid.Volume.Float64(), ask.Volume.Float64())
// 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 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()
}
orderBookSignalMetrics.WithLabelValues(s.symbol).Set(signal)
return signal, nil
}

View File

@ -31,6 +31,41 @@ 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{})
} }
@ -50,6 +85,8 @@ type Strategy struct {
HedgeInterval types.Duration `json:"hedgeInterval"` HedgeInterval types.Duration `json:"hedgeInterval"`
OrderCancelWaitTime types.Duration `json:"orderCancelWaitTime"` OrderCancelWaitTime types.Duration `json:"orderCancelWaitTime"`
SignalConfigList []SignalConfig `json:"signals"`
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"`
@ -138,6 +175,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 {
@ -192,18 +231,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
@ -211,12 +248,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 {
@ -282,6 +313,43 @@ 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 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 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)
@ -293,6 +361,15 @@ func (s *Strategy) updateQuote(ctx context.Context) {
return return
} }
signal, err := s.calculateSignal(ctx)
if err != nil {
return
}
s.logger.Infof("Final signal: %f", signal)
finalSignalMetrics.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 {
@ -500,13 +577,6 @@ func (s *Strategy) updateQuote(ctx context.Context) {
} }
} }
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
@ -520,8 +590,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
@ -566,7 +636,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) {
@ -634,7 +704,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)) {
@ -687,8 +757,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
@ -1039,7 +1109,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
@ -1125,6 +1194,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)
@ -1169,6 +1246,25 @@ 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)
for _, signalConfig := range s.SignalConfigList {
var sigAny any
switch {
case signalConfig.OrderBookBestPriceSignal != nil:
sig := signalConfig.OrderBookBestPriceSignal
sig.book = s.book
sigAny = sig
case signalConfig.BollingerBandTrendSignal != nil:
}
if sigAny != nil {
if binder, ok := sigAny.(SessionBinder); ok {
binder.Bind(ctx, s.sourceSession, s.Symbol)
}
}
}
s.activeMakerOrders = bbgo.NewActiveOrderBook(s.Symbol) s.activeMakerOrders = bbgo.NewActiveOrderBook(s.Symbol)
s.activeMakerOrders.BindStream(s.makerSession.UserDataStream) s.activeMakerOrders.BindStream(s.makerSession.UserDataStream)