Skip to content

Commit

Permalink
Merge pull request #1758 from c9s/c9s/xmaker/depth-signal
Browse files Browse the repository at this point in the history
FEATURE: [xmaker] add depth ratio signal
  • Loading branch information
c9s authored Sep 30, 2024
2 parents f776914 + 3c48663 commit c7e873a
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 0 deletions.
4 changes: 4 additions & 0 deletions pkg/strategy/xmaker/signal_book.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ type OrderBookBestPriceVolumeSignal struct {
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 {
if s.book == nil {
return errors.New("s.book can not be nil")
Expand Down
80 changes: 80 additions & 0 deletions pkg/strategy/xmaker/signal_depth.go
Original file line number Diff line number Diff line change
@@ -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
}
167 changes: 167 additions & 0 deletions pkg/strategy/xmaker/signal_depth_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
8 changes: 8 additions & 0 deletions pkg/strategy/xmaker/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type SignalConfig struct {
Weight float64 `json:"weight"`
BollingerBandTrendSignal *BollingerBandTrendSignal `json:"bollingerBandTrend,omitempty"`
OrderBookBestPriceSignal *OrderBookBestPriceVolumeSignal `json:"orderBookBestPrice,omitempty"`
DepthRatioSignal *DepthRatioSignal `json:"depthRatio,omitempty"`
KLineShapeSignal *KLineShapeSignal `json:"klineShape,omitempty"`
TradeVolumeWindowSignal *TradeVolumeWindowSignal `json:"tradeVolumeWindow,omitempty"`
}
Expand Down Expand Up @@ -390,6 +391,8 @@ func (s *Strategy) aggregateSignal(ctx context.Context) (float64, error) {
var err error
if signal.OrderBookBestPriceSignal != nil {
sig, err = signal.OrderBookBestPriceSignal.CalculateSignal(ctx)
} else if signal.DepthRatioSignal != nil {
sig, err = signal.DepthRatioSignal.CalculateSignal(ctx)
} else if signal.BollingerBandTrendSignal != nil {
sig, err = signal.BollingerBandTrendSignal.CalculateSignal(ctx)
} else if signal.TradeVolumeWindowSignal != nil {
Expand Down Expand Up @@ -1547,6 +1550,11 @@ func (s *Strategy) CrossRun(
if err := signalConfig.OrderBookBestPriceSignal.Bind(ctx, s.sourceSession, s.Symbol); err != nil {
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 {
if err := signalConfig.BollingerBandTrendSignal.Bind(ctx, s.sourceSession, s.Symbol); err != nil {
return err
Expand Down
22 changes: 22 additions & 0 deletions pkg/types/price_volume_slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,28 @@ func (slice PriceVolumeSlice) AverageDepthPriceByQuote(requiredDepthInQuote fixe
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
func (slice PriceVolumeSlice) AverageDepthPrice(requiredQuantity fixedpoint.Value) fixedpoint.Value {
// rest quantity
Expand Down

0 comments on commit c7e873a

Please sign in to comment.