mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-14 19:13: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
|
||||
}
|
||||
|
||||
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")
|
||||
|
|
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"`
|
||||
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"`
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user