From ed51eff2423b9b564883b58b140d2d3940a6dfe5 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 4 Sep 2024 14:59:10 +0800 Subject: [PATCH 1/6] max: drop unused function --- pkg/exchange/max/convert.go | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index 7c23f9b69..85cab17e4 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -247,29 +247,6 @@ func toGlobalTradeV3(t v3.Trade) ([]types.Trade, error) { return trades, nil } -func toGlobalTradeV2(t max.Trade) (*types.Trade, error) { - isMargin := t.WalletType == max.WalletTypeMargin - side := toGlobalSideType(t.Side) - return &types.Trade{ - ID: t.ID, - OrderID: t.OrderID, - Price: t.Price, - Symbol: toGlobalSymbol(t.Market), - Exchange: types.ExchangeMax, - Quantity: t.Volume, - Side: side, - IsBuyer: t.IsBuyer(), - IsMaker: t.IsMaker(), - Fee: t.Fee, - FeeCurrency: toGlobalCurrency(t.FeeCurrency), - QuoteQuantity: t.Funds, - Time: types.Time(t.CreatedAt), - IsMargin: isMargin, - IsIsolated: false, - IsFutures: false, - }, nil -} - func toGlobalDepositStatus(a max.DepositState) types.DepositStatus { switch a { From a2f8fe5f72df418793ee79fbed9208cc003988e5 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 4 Sep 2024 14:59:58 +0800 Subject: [PATCH 2/6] max: add v3 DepositStateFailed state --- pkg/exchange/max/maxapi/account.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/exchange/max/maxapi/account.go b/pkg/exchange/max/maxapi/account.go index ccf4c165b..7120a2b08 100644 --- a/pkg/exchange/max/maxapi/account.go +++ b/pkg/exchange/max/maxapi/account.go @@ -119,6 +119,7 @@ const ( // v3 states DepositStateProcessing DepositState = "processing" + DepositStateFailed DepositState = "failed" DepositStateDone DepositState = "done" ) From 2527c0c7b75c7976dcb3dc0d4742a9ef4e69332a Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 4 Sep 2024 15:00:37 +0800 Subject: [PATCH 3/6] max: convert v3 DepositStateFailed into rejected --- pkg/exchange/max/convert.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index 85cab17e4..ca36a41f8 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -262,6 +262,9 @@ func toGlobalDepositStatus(a max.DepositState) types.DepositStatus { case max.DepositStateAccepted: return types.DepositSuccess + case max.DepositStateFailed: // v3 state + return types.DepositRejected + case max.DepositStateProcessing: // v3 states return types.DepositPending From ba73eeaad19f075d2d3ccc6f1be437b1f07cb1b1 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 4 Sep 2024 15:59:21 +0800 Subject: [PATCH 4/6] xmaker: add TradeVolumeWindowSignal --- pkg/strategy/xmaker/signal_trade.go | 102 +++++++++++++++++++++++ pkg/strategy/xmaker/signal_trade_test.go | 55 ++++++++++++ pkg/strategy/xmaker/strategy.go | 65 +++++++-------- pkg/types/price_volume_slice.go | 7 ++ 4 files changed, 196 insertions(+), 33 deletions(-) create mode 100644 pkg/strategy/xmaker/signal_trade.go create mode 100644 pkg/strategy/xmaker/signal_trade_test.go diff --git a/pkg/strategy/xmaker/signal_trade.go b/pkg/strategy/xmaker/signal_trade.go new file mode 100644 index 000000000..6e67d3595 --- /dev/null +++ b/pkg/strategy/xmaker/signal_trade.go @@ -0,0 +1,102 @@ +package xmaker + +import ( + "context" + "time" + + "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 tradeVolumeWindowSignalMetrics = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "xmaker_trade_volume_window_signal", + Help: "", + }, []string{"symbol"}) + +func init() { + prometheus.MustRegister(tradeVolumeWindowSignalMetrics) +} + +type TradeVolumeWindowSignal struct { + Threshold fixedpoint.Value `json:"threshold"` + Window types.Duration `json:"window"` + + trades []types.Trade + symbol string +} + +func (s *TradeVolumeWindowSignal) handleTrade(trade types.Trade) { + s.trades = append(s.trades, trade) +} + +func (s *TradeVolumeWindowSignal) Bind(ctx context.Context, session *bbgo.ExchangeSession, symbol string) error { + s.symbol = symbol + + if s.Window == 0 { + s.Window = types.Duration(time.Minute) + } + + if s.Threshold.IsZero() { + s.Threshold = fixedpoint.NewFromFloat(0.7) + } + + session.MarketDataStream.OnMarketTrade(s.handleTrade) + return nil +} + +func (s *TradeVolumeWindowSignal) filterTrades(now time.Time) []types.Trade { + startTime := now.Add(-time.Duration(s.Window)) + startIdx := 0 + + for idx, td := range s.trades { + // skip trades before the start time + if td.Time.Before(startTime) { + continue + } + + startIdx = idx + break + } + + s.trades = s.trades[startIdx:] + return s.trades +} + +func (s *TradeVolumeWindowSignal) calculateTradeVolume(trades []types.Trade) (buyVolume, sellVolume float64) { + for _, td := range trades { + if td.IsBuyer { + buyVolume += td.Quantity.Float64() + } else { + sellVolume += td.Quantity.Float64() + } + } + + return buyVolume, sellVolume +} + +func (s *TradeVolumeWindowSignal) CalculateSignal(ctx context.Context) (float64, error) { + now := time.Now() + trades := s.filterTrades(now) + buyVolume, sellVolume := s.calculateTradeVolume(trades) + totalVolume := buyVolume + sellVolume + + threshold := s.Threshold.Float64() + buyRatio := buyVolume / totalVolume + sellRatio := sellVolume / totalVolume + + sig := 0.0 + if buyRatio > threshold { + sig = (buyRatio - threshold) / 2.0 + } else if sellRatio > threshold { + sig = -(sellRatio - threshold) / 2.0 + } + + log.Infof("[TradeVolumeWindowSignal] sig: %f buy/sell = %f/%f", sig, buyVolume, sellVolume) + + tradeVolumeWindowSignalMetrics.WithLabelValues(s.symbol).Set(sig) + return sig, nil +} diff --git a/pkg/strategy/xmaker/signal_trade_test.go b/pkg/strategy/xmaker/signal_trade_test.go new file mode 100644 index 000000000..d99e31ef3 --- /dev/null +++ b/pkg/strategy/xmaker/signal_trade_test.go @@ -0,0 +1,55 @@ +package xmaker + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + + . "github.com/c9s/bbgo/pkg/testing/testhelper" +) + +var tradeId = 0 + +func Trade(symbol string, side types.SideType, price, quantity fixedpoint.Value, t time.Time) types.Trade { + tradeId++ + return types.Trade{ + ID: uint64(tradeId), + Symbol: symbol, + Side: side, + Price: price, + IsBuyer: side == types.SideTypeBuy, + Quantity: quantity, + Time: types.Time(t), + } +} + +func TestMarketTradeWindowSignal(t *testing.T) { + now := time.Now() + symbol := "BTCUSDT" + sig := &TradeVolumeWindowSignal{ + symbol: symbol, + Threshold: fixedpoint.NewFromFloat(0.65), + Window: types.Duration(time.Minute), + } + + sig.trades = []types.Trade{ + Trade(symbol, types.SideTypeBuy, Number(18000.0), Number(1.0), now.Add(-2*time.Minute)), + Trade(symbol, types.SideTypeSell, Number(18000.0), Number(0.5), now.Add(-2*time.Second)), + Trade(symbol, types.SideTypeBuy, Number(18000.0), Number(1.0), now.Add(-1*time.Second)), + } + + ctx := context.Background() + sigNum, err := sig.CalculateSignal(ctx) + if assert.NoError(t, err) { + // buy ratio: 1/1.5 = 0.6666666666666666 + // sell ratio: 0.5/1.5 = 0.3333333333333333 + assert.InDelta(t, 0.0083333, sigNum, 0.0001) + } + + assert.Len(t, sig.trades, 2) +} diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 6a33825df..943fd9185 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -65,6 +65,7 @@ type SignalConfig struct { BollingerBandTrendSignal *BollingerBandTrendSignal `json:"bollingerBandTrend,omitempty"` OrderBookBestPriceSignal *OrderBookBestPriceVolumeSignal `json:"orderBookBestPrice,omitempty"` KLineShapeSignal *KLineShapeSignal `json:"klineShape,omitempty"` + TradeVolumeWindowSignal *TradeVolumeWindowSignal `json:"tradeVolumeWindow,omitempty"` } func init() { @@ -205,7 +206,14 @@ func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { if !ok { panic(fmt.Errorf("maker session %s is not defined", s.MakerExchange)) } + makerSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"}) + + for _, sig := range s.SignalConfigList { + if sig.TradeVolumeWindowSignal != nil { + sourceSession.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) + } + } } func aggregatePrice(pvs types.PriceVolumeSlice, requiredQuantity fixedpoint.Value) (price fixedpoint.Value) { @@ -363,42 +371,33 @@ func (s *Strategy) calculateSignal(ctx context.Context) (float64, error) { sum := 0.0 voters := 0.0 for _, signal := range s.SignalConfigList { + var sig float64 + var err error if signal.OrderBookBestPriceSignal != nil { - sig, err := signal.OrderBookBestPriceSignal.CalculateSignal(ctx) - if err != nil { - return 0, err - } - - if sig == 0.0 { - continue - } - - if signal.Weight > 0.0 { - sum += sig * signal.Weight - voters += signal.Weight - } else { - sum += sig - voters++ - } - + sig, err = signal.OrderBookBestPriceSignal.CalculateSignal(ctx) } else if signal.BollingerBandTrendSignal != nil { - sig, err := signal.BollingerBandTrendSignal.CalculateSignal(ctx) - if err != nil { - return 0, err - } - - if sig == 0.0 { - continue - } - - if signal.Weight > 0.0 { - sum += sig * signal.Weight - voters += signal.Weight - } else { - sum += sig - voters++ - } + sig, err = signal.BollingerBandTrendSignal.CalculateSignal(ctx) + } else if signal.TradeVolumeWindowSignal != nil { + sig, err = signal.TradeVolumeWindowSignal.CalculateSignal(ctx) } + + if err != nil { + return 0, err + } else if sig == 0.0 { + continue + } + + if signal.Weight > 0.0 { + sum += sig * signal.Weight + voters += signal.Weight + } else { + sum += sig + voters++ + } + } + + if sum == 0.0 { + return 0.0, nil } return sum / voters, nil diff --git a/pkg/types/price_volume_slice.go b/pkg/types/price_volume_slice.go index 5ca022dbb..4ffcda467 100644 --- a/pkg/types/price_volume_slice.go +++ b/pkg/types/price_volume_slice.go @@ -13,6 +13,13 @@ type PriceVolume struct { Price, Volume fixedpoint.Value } +func NewPriceVolume(p, v fixedpoint.Value) PriceVolume { + return PriceVolume{ + Price: p, + Volume: v, + } +} + func (p PriceVolume) InQuote() fixedpoint.Value { return p.Price.Mul(p.Volume) } From 656112de454f5245ecda8a9856493e8ef9f46227 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 4 Sep 2024 16:07:28 +0800 Subject: [PATCH 5/6] xmaker: call signalConfig.TradeVolumeWindowSignal.Bind --- pkg/strategy/xmaker/signal_trade.go | 15 ++++++++++++--- pkg/strategy/xmaker/strategy.go | 4 ++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/xmaker/signal_trade.go b/pkg/strategy/xmaker/signal_trade.go index 6e67d3595..bb3db5f9c 100644 --- a/pkg/strategy/xmaker/signal_trade.go +++ b/pkg/strategy/xmaker/signal_trade.go @@ -2,6 +2,7 @@ package xmaker import ( "context" + "sync" "time" "github.com/prometheus/client_golang/prometheus" @@ -27,10 +28,14 @@ type TradeVolumeWindowSignal struct { trades []types.Trade symbol string + + mu sync.Mutex } func (s *TradeVolumeWindowSignal) handleTrade(trade types.Trade) { + s.mu.Lock() s.trades = append(s.trades, trade) + s.mu.Unlock() } func (s *TradeVolumeWindowSignal) Bind(ctx context.Context, session *bbgo.ExchangeSession, symbol string) error { @@ -52,6 +57,9 @@ func (s *TradeVolumeWindowSignal) filterTrades(now time.Time) []types.Trade { startTime := now.Add(-time.Duration(s.Window)) startIdx := 0 + s.mu.Lock() + defer s.mu.Unlock() + for idx, td := range s.trades { // skip trades before the start time if td.Time.Before(startTime) { @@ -62,8 +70,9 @@ func (s *TradeVolumeWindowSignal) filterTrades(now time.Time) []types.Trade { break } - s.trades = s.trades[startIdx:] - return s.trades + trades := s.trades[startIdx:] + s.trades = trades + return trades } func (s *TradeVolumeWindowSignal) calculateTradeVolume(trades []types.Trade) (buyVolume, sellVolume float64) { @@ -95,7 +104,7 @@ func (s *TradeVolumeWindowSignal) CalculateSignal(ctx context.Context) (float64, sig = -(sellRatio - threshold) / 2.0 } - log.Infof("[TradeVolumeWindowSignal] sig: %f buy/sell = %f/%f", sig, buyVolume, sellVolume) + log.Infof("[TradeVolumeWindowSignal] %f buy/sell = %f/%f", sig, buyVolume, sellVolume) tradeVolumeWindowSignalMetrics.WithLabelValues(s.symbol).Set(sig) return sig, nil diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 943fd9185..de97f691d 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -1373,6 +1373,10 @@ func (s *Strategy) CrossRun( if err := signalConfig.BollingerBandTrendSignal.Bind(ctx, s.sourceSession, s.Symbol); err != nil { return err } + } else if signalConfig.TradeVolumeWindowSignal != nil { + if err := signalConfig.TradeVolumeWindowSignal.Bind(ctx, s.sourceSession, s.Symbol); err != nil { + return err + } } } From 9fc3a1b44a64ceaf8feb83c9ed558b751fbe7e5a Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 4 Sep 2024 16:09:58 +0800 Subject: [PATCH 6/6] xmaker: rename to aggTradeVolume --- pkg/strategy/xmaker/signal_trade.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/xmaker/signal_trade.go b/pkg/strategy/xmaker/signal_trade.go index bb3db5f9c..876cb3090 100644 --- a/pkg/strategy/xmaker/signal_trade.go +++ b/pkg/strategy/xmaker/signal_trade.go @@ -75,7 +75,7 @@ func (s *TradeVolumeWindowSignal) filterTrades(now time.Time) []types.Trade { return trades } -func (s *TradeVolumeWindowSignal) calculateTradeVolume(trades []types.Trade) (buyVolume, sellVolume float64) { +func (s *TradeVolumeWindowSignal) aggTradeVolume(trades []types.Trade) (buyVolume, sellVolume float64) { for _, td := range trades { if td.IsBuyer { buyVolume += td.Quantity.Float64() @@ -87,10 +87,10 @@ func (s *TradeVolumeWindowSignal) calculateTradeVolume(trades []types.Trade) (bu return buyVolume, sellVolume } -func (s *TradeVolumeWindowSignal) CalculateSignal(ctx context.Context) (float64, error) { +func (s *TradeVolumeWindowSignal) CalculateSignal(_ context.Context) (float64, error) { now := time.Now() trades := s.filterTrades(now) - buyVolume, sellVolume := s.calculateTradeVolume(trades) + buyVolume, sellVolume := s.aggTradeVolume(trades) totalVolume := buyVolume + sellVolume threshold := s.Threshold.Float64()