mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-15 03:23:52 +00:00
Merge pull request #1758 from c9s/c9s/xmaker/depth-signal
FEATURE: [xmaker] add depth ratio signal
This commit is contained in:
commit
c7e873abbb
|
@ -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")
|
||||||
|
|
80
pkg/strategy/xmaker/signal_depth.go
Normal file
80
pkg/strategy/xmaker/signal_depth.go
Normal 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
|
||||||
|
}
|
167
pkg/strategy/xmaker/signal_depth_test.go
Normal file
167
pkg/strategy/xmaker/signal_depth_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user