Merge pull request #1758 from c9s/c9s/xmaker/depth-signal

FEATURE: [xmaker] add depth ratio signal
This commit is contained in:
c9s 2024-09-30 17:44:43 +08:00 committed by GitHub
commit c7e873abbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 281 additions and 0 deletions

View File

@ -29,6 +29,10 @@ type OrderBookBestPriceVolumeSignal struct {
book *types.StreamOrderBook book *types.StreamOrderBook
} }
func (s *OrderBookBestPriceVolumeSignal) BindStreamBook(book *types.StreamOrderBook) {
s.book = book
}
func (s *OrderBookBestPriceVolumeSignal) Bind(ctx context.Context, session *bbgo.ExchangeSession, symbol string) error { func (s *OrderBookBestPriceVolumeSignal) Bind(ctx context.Context, session *bbgo.ExchangeSession, symbol string) error {
if s.book == nil { if s.book == nil {
return errors.New("s.book can not be nil") return errors.New("s.book can not be nil")

View File

@ -0,0 +1,80 @@
package xmaker
import (
"context"
"math"
"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 depthRatioSignalMetrics = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "xmaker_depth_ratio_signal",
Help: "",
}, []string{"symbol"})
func init() {
prometheus.MustRegister(depthRatioSignalMetrics)
}
type DepthRatioSignal struct {
// PriceRange, 2% depth ratio means 2% price range from the mid price
PriceRange fixedpoint.Value `json:"priceRange"`
MinRatio float64 `json:"minRatio"`
symbol string
book *types.StreamOrderBook
}
func (s *DepthRatioSignal) BindStreamBook(book *types.StreamOrderBook) {
s.book = book
}
func (s *DepthRatioSignal) 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 *DepthRatioSignal) CalculateSignal(ctx context.Context) (float64, error) {
bid, ask, ok := s.book.BestBidAndAsk()
if !ok {
return 0.0, nil
}
midPrice := bid.Price.Add(ask.Price).Div(fixedpoint.Two)
asks := s.book.SideBook(types.SideTypeSell)
bids := s.book.SideBook(types.SideTypeBuy)
asksInRange := asks.InPriceRange(midPrice, types.SideTypeSell, s.PriceRange)
bidsInRange := bids.InPriceRange(midPrice, types.SideTypeBuy, s.PriceRange)
askDepthQuote := asksInRange.SumDepthInQuote()
bidDepthQuote := bidsInRange.SumDepthInQuote()
var signal = 0.0
depthRatio := bidDepthQuote.Div(askDepthQuote.Add(bidDepthQuote))
// convert ratio into -2.0 and 2.0
signal = depthRatio.Sub(fixedpoint.NewFromFloat(0.5)).Float64() * 4.0
// ignore noise
if math.Abs(signal) < s.MinRatio {
signal = 0.0
}
log.Infof("[DepthRatioSignal] %f bid/ask = %f/%f", signal, bidDepthQuote.Float64(), askDepthQuote.Float64())
depthRatioSignalMetrics.WithLabelValues(s.symbol).Set(signal)
return signal, nil
}

View File

@ -0,0 +1,167 @@
package xmaker
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
. "github.com/c9s/bbgo/pkg/testing/testhelper"
)
func TestDepthRatioSignal_CalculateSignal(t *testing.T) {
type fields struct {
PriceRange fixedpoint.Value
MinRatio float64
symbol string
book *types.StreamOrderBook
}
type args struct {
ctx context.Context
bids, asks types.PriceVolumeSlice
}
tests := []struct {
name string
fields fields
args args
want float64
wantErr assert.ErrorAssertionFunc
}{
{
name: "medium short",
fields: fields{
PriceRange: fixedpoint.NewFromFloat(0.02),
MinRatio: 0.01,
symbol: "BTCUSDT",
},
args: args{
ctx: context.Background(),
asks: PriceVolumeSliceFromText(`
19310,1.0
19320,0.2
19330,0.3
19340,0.4
19350,0.5
`),
bids: PriceVolumeSliceFromText(`
19300,0.1
19290,0.2
19280,0.3
19270,0.4
19260,0.5
`),
},
want: -0.4641,
wantErr: assert.NoError,
},
{
name: "strong short",
fields: fields{
PriceRange: fixedpoint.NewFromFloat(0.02),
MinRatio: 0.01,
symbol: "BTCUSDT",
},
args: args{
ctx: context.Background(),
asks: PriceVolumeSliceFromText(`
19310,10.0
19320,0.2
19330,0.3
19340,0.4
19350,0.5
`),
bids: PriceVolumeSliceFromText(`
19300,0.1
19290,0.1
19280,0.1
19270,0.1
19260,0.1
`),
},
want: -1.8322,
wantErr: assert.NoError,
},
{
name: "strong long",
fields: fields{
PriceRange: fixedpoint.NewFromFloat(0.02),
MinRatio: 0.01,
symbol: "BTCUSDT",
},
args: args{
ctx: context.Background(),
asks: PriceVolumeSliceFromText(`
19310,0.1
19320,0.1
19330,0.1
19340,0.1
19350,0.1
`),
bids: PriceVolumeSliceFromText(`
19300,10.0
19290,0.1
19280,0.1
19270,0.1
19260,0.1
`),
},
want: 1.81623,
wantErr: assert.NoError,
},
{
name: "normal",
fields: fields{
PriceRange: fixedpoint.NewFromFloat(0.02),
MinRatio: 0.01,
symbol: "BTCUSDT",
},
args: args{
ctx: context.Background(),
asks: PriceVolumeSliceFromText(`
19310,0.1
19320,0.2
19330,0.3
19340,0.4
19350,0.5
`),
bids: PriceVolumeSliceFromText(`
19300,0.1
19290,0.2
19280,0.3
19270,0.4
19260,0.5
`),
},
want: 0,
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &DepthRatioSignal{
PriceRange: tt.fields.PriceRange,
MinRatio: tt.fields.MinRatio,
symbol: tt.fields.symbol,
book: types.NewStreamBook("BTCUSDT", types.ExchangeBinance),
}
s.book.Load(types.SliceOrderBook{
Symbol: "BTCUSDT",
Bids: tt.args.bids,
Asks: tt.args.asks,
})
got, err := s.CalculateSignal(tt.args.ctx)
if !tt.wantErr(t, err, fmt.Sprintf("CalculateSignal(%v)", tt.args.ctx)) {
return
}
assert.InDeltaf(t, tt.want, got, 0.001, "CalculateSignal(%v)", tt.args.ctx)
})
}
}

View File

@ -64,6 +64,7 @@ type SignalConfig struct {
Weight float64 `json:"weight"` Weight float64 `json:"weight"`
BollingerBandTrendSignal *BollingerBandTrendSignal `json:"bollingerBandTrend,omitempty"` BollingerBandTrendSignal *BollingerBandTrendSignal `json:"bollingerBandTrend,omitempty"`
OrderBookBestPriceSignal *OrderBookBestPriceVolumeSignal `json:"orderBookBestPrice,omitempty"` OrderBookBestPriceSignal *OrderBookBestPriceVolumeSignal `json:"orderBookBestPrice,omitempty"`
DepthRatioSignal *DepthRatioSignal `json:"depthRatio,omitempty"`
KLineShapeSignal *KLineShapeSignal `json:"klineShape,omitempty"` KLineShapeSignal *KLineShapeSignal `json:"klineShape,omitempty"`
TradeVolumeWindowSignal *TradeVolumeWindowSignal `json:"tradeVolumeWindow,omitempty"` TradeVolumeWindowSignal *TradeVolumeWindowSignal `json:"tradeVolumeWindow,omitempty"`
} }
@ -390,6 +391,8 @@ func (s *Strategy) aggregateSignal(ctx context.Context) (float64, error) {
var err error var err error
if signal.OrderBookBestPriceSignal != nil { if signal.OrderBookBestPriceSignal != nil {
sig, err = signal.OrderBookBestPriceSignal.CalculateSignal(ctx) sig, err = signal.OrderBookBestPriceSignal.CalculateSignal(ctx)
} else if signal.DepthRatioSignal != nil {
sig, err = signal.DepthRatioSignal.CalculateSignal(ctx)
} else if signal.BollingerBandTrendSignal != nil { } else if signal.BollingerBandTrendSignal != nil {
sig, err = signal.BollingerBandTrendSignal.CalculateSignal(ctx) sig, err = signal.BollingerBandTrendSignal.CalculateSignal(ctx)
} else if signal.TradeVolumeWindowSignal != nil { } else if signal.TradeVolumeWindowSignal != nil {
@ -1547,6 +1550,11 @@ func (s *Strategy) CrossRun(
if err := signalConfig.OrderBookBestPriceSignal.Bind(ctx, s.sourceSession, s.Symbol); err != nil { if err := signalConfig.OrderBookBestPriceSignal.Bind(ctx, s.sourceSession, s.Symbol); err != nil {
return err return err
} }
} else if signalConfig.DepthRatioSignal != nil {
signalConfig.DepthRatioSignal.book = s.sourceBook
if err := signalConfig.DepthRatioSignal.Bind(ctx, s.sourceSession, s.Symbol); err != nil {
return err
}
} else if signalConfig.BollingerBandTrendSignal != nil { } else if signalConfig.BollingerBandTrendSignal != nil {
if err := signalConfig.BollingerBandTrendSignal.Bind(ctx, s.sourceSession, s.Symbol); err != nil { if err := signalConfig.BollingerBandTrendSignal.Bind(ctx, s.sourceSession, s.Symbol); err != nil {
return err return err

View File

@ -280,6 +280,28 @@ func (slice PriceVolumeSlice) AverageDepthPriceByQuote(requiredDepthInQuote fixe
return totalQuoteAmount.Div(totalQuantity) return totalQuoteAmount.Div(totalQuantity)
} }
func (slice PriceVolumeSlice) InPriceRange(midPrice fixedpoint.Value, side SideType, r fixedpoint.Value) (sub PriceVolumeSlice) {
switch side {
case SideTypeSell:
boundaryPrice := midPrice.Add(midPrice.Mul(r))
for _, pv := range slice {
if pv.Price.Compare(boundaryPrice) <= 0 {
sub = append(sub, pv)
}
}
case SideTypeBuy:
boundaryPrice := midPrice.Sub(midPrice.Mul(r))
for _, pv := range slice {
if pv.Price.Compare(boundaryPrice) >= 0 {
sub = append(sub, pv)
}
}
}
return sub
}
// AverageDepthPrice uses the required total quantity to calculate the corresponding price // AverageDepthPrice uses the required total quantity to calculate the corresponding price
func (slice PriceVolumeSlice) AverageDepthPrice(requiredQuantity fixedpoint.Value) fixedpoint.Value { func (slice PriceVolumeSlice) AverageDepthPrice(requiredQuantity fixedpoint.Value) fixedpoint.Value {
// rest quantity // rest quantity