Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: [xmaker] add depth ratio signal #1758

Merged
merged 2 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading