From ea3b1cc937628c32c283d50bedac08d4b43702ef Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 16:39:44 +0800 Subject: [PATCH 01/20] binance: fix logrus call --- pkg/exchange/binance/convert.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/exchange/binance/convert.go b/pkg/exchange/binance/convert.go index be2383689..7586770d4 100644 --- a/pkg/exchange/binance/convert.go +++ b/pkg/exchange/binance/convert.go @@ -46,11 +46,11 @@ func toGlobalMarket(symbol binance.Symbol) types.Market { } if market.MinNotional.IsZero() { - log.Warn("binance market %s minNotional is zero", market.Symbol) + log.Warnf("binance market %s minNotional is zero", market.Symbol) } if market.MinQuantity.IsZero() { - log.Warn("binance market %s minQuantity is zero", market.Symbol) + log.Warnf("binance market %s minQuantity is zero", market.Symbol) } return market From 9d9f898f17a2c5fa63a1a11d14b4b88873d4145e Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 9 Jun 2023 13:18:08 +0800 Subject: [PATCH 02/20] indicator: use pointer for float64series --- pkg/indicator/float64series.go | 4 ++-- pkg/indicator/v2_atrp.go | 6 ++++-- pkg/indicator/v2_cma.go | 2 +- pkg/indicator/v2_cross.go | 2 +- pkg/indicator/v2_ewma.go | 2 +- pkg/indicator/v2_macd_test.go | 2 +- pkg/indicator/v2_multiply.go | 2 +- pkg/indicator/v2_pivothigh.go | 2 +- pkg/indicator/v2_pivotlow.go | 2 +- pkg/indicator/v2_price.go | 17 +++++++++-------- pkg/indicator/v2_rma.go | 2 +- pkg/indicator/v2_rsi.go | 2 +- pkg/indicator/v2_rsi_test.go | 2 +- pkg/indicator/v2_sma.go | 2 +- pkg/indicator/v2_stddev.go | 2 +- pkg/indicator/v2_subtract.go | 2 +- pkg/indicator/v2_tr.go | 2 +- 17 files changed, 29 insertions(+), 26 deletions(-) diff --git a/pkg/indicator/float64series.go b/pkg/indicator/float64series.go index 821c17666..c198e8e8d 100644 --- a/pkg/indicator/float64series.go +++ b/pkg/indicator/float64series.go @@ -11,8 +11,8 @@ type Float64Series struct { slice floats.Slice } -func NewFloat64Series(v ...float64) Float64Series { - s := Float64Series{} +func NewFloat64Series(v ...float64) *Float64Series { + s := &Float64Series{} s.slice = v s.SeriesBase.Series = s.slice return s diff --git a/pkg/indicator/v2_atrp.go b/pkg/indicator/v2_atrp.go index 6261c1cb3..bb9b982d3 100644 --- a/pkg/indicator/v2_atrp.go +++ b/pkg/indicator/v2_atrp.go @@ -1,11 +1,13 @@ package indicator type ATRPStream struct { - Float64Series + *Float64Series } func ATRP2(source KLineSubscription, window int) *ATRPStream { - s := &ATRPStream{} + s := &ATRPStream{ + Float64Series: NewFloat64Series(), + } tr := TR2(source) atr := RMA2(tr, window, true) atr.OnUpdate(func(x float64) { diff --git a/pkg/indicator/v2_cma.go b/pkg/indicator/v2_cma.go index f3c485aff..9bc2c8994 100644 --- a/pkg/indicator/v2_cma.go +++ b/pkg/indicator/v2_cma.go @@ -1,7 +1,7 @@ package indicator type CMAStream struct { - Float64Series + *Float64Series } func CMA2(source Float64Source) *CMAStream { diff --git a/pkg/indicator/v2_cross.go b/pkg/indicator/v2_cross.go index 084130fdb..835e87c30 100644 --- a/pkg/indicator/v2_cross.go +++ b/pkg/indicator/v2_cross.go @@ -13,7 +13,7 @@ const ( // CrossStream subscribes 2 upstreams, and calculate the cross signal type CrossStream struct { - Float64Series + *Float64Series a, b floats.Slice } diff --git a/pkg/indicator/v2_ewma.go b/pkg/indicator/v2_ewma.go index 1be654f85..45a15d7ca 100644 --- a/pkg/indicator/v2_ewma.go +++ b/pkg/indicator/v2_ewma.go @@ -1,7 +1,7 @@ package indicator type EWMAStream struct { - Float64Series + *Float64Series window int multiplier float64 diff --git a/pkg/indicator/v2_macd_test.go b/pkg/indicator/v2_macd_test.go index 74617089a..0fb9b647a 100644 --- a/pkg/indicator/v2_macd_test.go +++ b/pkg/indicator/v2_macd_test.go @@ -41,7 +41,7 @@ func Test_MACD2(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - prices := &PriceStream{} + prices := ClosePrices(nil) macd := MACD2(prices, 12, 26, 9) for _, k := range tt.kLines { prices.EmitUpdate(k.Close.Float64()) diff --git a/pkg/indicator/v2_multiply.go b/pkg/indicator/v2_multiply.go index 24f647005..ee76bdc00 100644 --- a/pkg/indicator/v2_multiply.go +++ b/pkg/indicator/v2_multiply.go @@ -3,7 +3,7 @@ package indicator import "github.com/c9s/bbgo/pkg/datatype/floats" type MultiplyStream struct { - Float64Series + *Float64Series a, b floats.Slice } diff --git a/pkg/indicator/v2_pivothigh.go b/pkg/indicator/v2_pivothigh.go index eb0352a73..9e4143816 100644 --- a/pkg/indicator/v2_pivothigh.go +++ b/pkg/indicator/v2_pivothigh.go @@ -5,7 +5,7 @@ import ( ) type PivotHighStream struct { - Float64Series + *Float64Series rawValues floats.Slice window, rightWindow int } diff --git a/pkg/indicator/v2_pivotlow.go b/pkg/indicator/v2_pivotlow.go index 47d76308f..1fa78e054 100644 --- a/pkg/indicator/v2_pivotlow.go +++ b/pkg/indicator/v2_pivotlow.go @@ -5,7 +5,7 @@ import ( ) type PivotLowStream struct { - Float64Series + *Float64Series rawValues floats.Slice window, rightWindow int } diff --git a/pkg/indicator/v2_price.go b/pkg/indicator/v2_price.go index d95976dda..6e4c61844 100644 --- a/pkg/indicator/v2_price.go +++ b/pkg/indicator/v2_price.go @@ -11,22 +11,23 @@ type KLineSubscription interface { } type PriceStream struct { - Float64Series + *Float64Series mapper KLineValueMapper } func Price(source KLineSubscription, mapper KLineValueMapper) *PriceStream { s := &PriceStream{ - mapper: mapper, + Float64Series: NewFloat64Series(), + mapper: mapper, } - s.SeriesBase.Series = s.slice - - source.AddSubscriber(func(k types.KLine) { - v := s.mapper(k) - s.PushAndEmit(v) - }) + if source != nil { + source.AddSubscriber(func(k types.KLine) { + v := s.mapper(k) + s.PushAndEmit(v) + }) + } return s } diff --git a/pkg/indicator/v2_rma.go b/pkg/indicator/v2_rma.go index 17650d9d9..0464ad96d 100644 --- a/pkg/indicator/v2_rma.go +++ b/pkg/indicator/v2_rma.go @@ -2,7 +2,7 @@ package indicator type RMAStream struct { // embedded structs - Float64Series + *Float64Series // config fields Adjust bool diff --git a/pkg/indicator/v2_rsi.go b/pkg/indicator/v2_rsi.go index 81e439320..11cf984c9 100644 --- a/pkg/indicator/v2_rsi.go +++ b/pkg/indicator/v2_rsi.go @@ -2,7 +2,7 @@ package indicator type RSIStream struct { // embedded structs - Float64Series + *Float64Series // config fields window int diff --git a/pkg/indicator/v2_rsi_test.go b/pkg/indicator/v2_rsi_test.go index 533a89e1a..311624024 100644 --- a/pkg/indicator/v2_rsi_test.go +++ b/pkg/indicator/v2_rsi_test.go @@ -67,7 +67,7 @@ func Test_RSI2(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // RSI2() - prices := &PriceStream{} + prices := ClosePrices(nil) rsi := RSI2(prices, tt.window) t.Logf("data length: %d", len(tt.values)) diff --git a/pkg/indicator/v2_sma.go b/pkg/indicator/v2_sma.go index 641cdb3d2..b6a79277c 100644 --- a/pkg/indicator/v2_sma.go +++ b/pkg/indicator/v2_sma.go @@ -3,7 +3,7 @@ package indicator import "github.com/c9s/bbgo/pkg/types" type SMAStream struct { - Float64Series + *Float64Series window int rawValues *types.Queue } diff --git a/pkg/indicator/v2_stddev.go b/pkg/indicator/v2_stddev.go index 28a5a4d07..9e465f970 100644 --- a/pkg/indicator/v2_stddev.go +++ b/pkg/indicator/v2_stddev.go @@ -3,7 +3,7 @@ package indicator import "github.com/c9s/bbgo/pkg/types" type StdDevStream struct { - Float64Series + *Float64Series rawValues *types.Queue diff --git a/pkg/indicator/v2_subtract.go b/pkg/indicator/v2_subtract.go index 7ccde2bf6..33a191730 100644 --- a/pkg/indicator/v2_subtract.go +++ b/pkg/indicator/v2_subtract.go @@ -6,7 +6,7 @@ import ( // SubtractStream subscribes 2 upstream data, and then subtract these 2 values type SubtractStream struct { - Float64Series + *Float64Series a, b floats.Slice i int diff --git a/pkg/indicator/v2_tr.go b/pkg/indicator/v2_tr.go index 05c6350e2..98f4bdc7b 100644 --- a/pkg/indicator/v2_tr.go +++ b/pkg/indicator/v2_tr.go @@ -9,7 +9,7 @@ import ( // This TRStream calculates the ATR first type TRStream struct { // embedded struct - Float64Series + *Float64Series // private states previousClose float64 From 295ae95da6410e0be00257b2b142922119197552 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 9 Jun 2023 18:36:54 +0800 Subject: [PATCH 03/20] indicator: implement bollinger indicator --- pkg/indicator/v2_boll.go | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 pkg/indicator/v2_boll.go diff --git a/pkg/indicator/v2_boll.go b/pkg/indicator/v2_boll.go new file mode 100644 index 000000000..a1f1e3197 --- /dev/null +++ b/pkg/indicator/v2_boll.go @@ -0,0 +1,52 @@ +package indicator + +type BollStream struct { + // the band series + *Float64Series + + UpBand, DownBand *Float64Series + + window int + k float64 + + SMA *SMAStream + StdDev *StdDevStream +} + +// BOOL2 is bollinger indicator +// the data flow: +// +// priceSource -> +// +// -> calculate SMA +// -> calculate stdDev -> calculate bandWidth -> get latest SMA -> upBand, downBand +func BOLL2(source Float64Source, window int, k float64) *BollStream { + // bind these indicators before our main calculator + sma := SMA2(source, window) + stdDev := StdDev2(source, window) + + s := &BollStream{ + Float64Series: NewFloat64Series(), + UpBand: NewFloat64Series(), + DownBand: NewFloat64Series(), + window: window, + k: k, + SMA: sma, + StdDev: stdDev, + } + s.Bind(source, s) + + // on band update + s.Float64Series.OnUpdate(func(band float64) { + mid := s.SMA.Last(0) + s.UpBand.PushAndEmit(mid + band) + s.DownBand.PushAndEmit(mid - band) + }) + return s +} + +func (s *BollStream) Calculate(v float64) float64 { + stdDev := s.StdDev.Last(0) + band := stdDev * s.k + return band +} From 0a5f31a80f0c0f9d1acd63ecd1829c9b849e8491 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 9 Jun 2023 18:38:12 +0800 Subject: [PATCH 04/20] indicator: rename BollStream to BOLLStream --- pkg/indicator/v2_boll.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/indicator/v2_boll.go b/pkg/indicator/v2_boll.go index a1f1e3197..ee0dfd15b 100644 --- a/pkg/indicator/v2_boll.go +++ b/pkg/indicator/v2_boll.go @@ -1,6 +1,6 @@ package indicator -type BollStream struct { +type BOLLStream struct { // the band series *Float64Series @@ -20,12 +20,12 @@ type BollStream struct { // // -> calculate SMA // -> calculate stdDev -> calculate bandWidth -> get latest SMA -> upBand, downBand -func BOLL2(source Float64Source, window int, k float64) *BollStream { +func BOLL2(source Float64Source, window int, k float64) *BOLLStream { // bind these indicators before our main calculator sma := SMA2(source, window) stdDev := StdDev2(source, window) - s := &BollStream{ + s := &BOLLStream{ Float64Series: NewFloat64Series(), UpBand: NewFloat64Series(), DownBand: NewFloat64Series(), @@ -45,7 +45,7 @@ func BOLL2(source Float64Source, window int, k float64) *BollStream { return s } -func (s *BollStream) Calculate(v float64) float64 { +func (s *BOLLStream) Calculate(v float64) float64 { stdDev := s.StdDev.Last(0) band := stdDev * s.k return band From e529a3271d87b3abcb0ed369c5a527751af5460a Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 12 Jun 2023 17:38:17 +0800 Subject: [PATCH 05/20] indicator: fix ewma2 initial value --- pkg/indicator/v2_ewma.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/indicator/v2_ewma.go b/pkg/indicator/v2_ewma.go index 45a15d7ca..16feb9901 100644 --- a/pkg/indicator/v2_ewma.go +++ b/pkg/indicator/v2_ewma.go @@ -19,6 +19,10 @@ func EWMA2(source Float64Source, window int) *EWMAStream { func (s *EWMAStream) Calculate(v float64) float64 { last := s.slice.Last(0) + if last == 0.0 { + return v + } + m := s.multiplier return (1.0-m)*last + m*v } From fded41b0ea433087f764e51c3e57df76ea20966a Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 16:22:25 +0800 Subject: [PATCH 06/20] indicator: fix macd test case since we changed the ewma default value --- pkg/indicator/v2_macd_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/indicator/v2_macd_test.go b/pkg/indicator/v2_macd_test.go index 0fb9b647a..6f86d0ffa 100644 --- a/pkg/indicator/v2_macd_test.go +++ b/pkg/indicator/v2_macd_test.go @@ -35,7 +35,7 @@ func Test_MACD2(t *testing.T) { { name: "random_case", kLines: buildKLines(input), - want: 0.7967670223776384, + want: 0.7740187187598249, }, } From 0482ade44a10f7111fec20cd32b03d30848708a2 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 12 Jun 2023 16:01:40 +0800 Subject: [PATCH 07/20] backtest: adjust best bid/ask price with tick size --- pkg/backtest/exchange.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/backtest/exchange.go b/pkg/backtest/exchange.go index 5faa41289..b358973ee 100644 --- a/pkg/backtest/exchange.go +++ b/pkg/backtest/exchange.go @@ -12,18 +12,18 @@ for each kline, the backtest engine: There are 2 ways that a strategy could work with backtest engine: -1. the strategy receives kline from the market data stream, and then it submits the order by the given market data to the backtest engine. - backtest engine receives the order and then pushes the trade and order updates to the user data stream. + 1. the strategy receives kline from the market data stream, and then it submits the order by the given market data to the backtest engine. + backtest engine receives the order and then pushes the trade and order updates to the user data stream. - the strategy receives the trade and update its position. + the strategy receives the trade and update its position. -2. the strategy places the orders when it starts. (like grid) the strategy then receives the order updates and then submit a new order - by its order update message. + 2. the strategy places the orders when it starts. (like grid) the strategy then receives the order updates and then submit a new order + by its order update message. We need to ensure that: -1. if the strategy submits the order from the market data stream, since it's a separate goroutine, the strategy should block the backtest engine - to process the trades before the next kline is published. + 1. if the strategy submits the order from the market data stream, since it's a separate goroutine, the strategy should block the backtest engine + to process the trades before the next kline is published. */ package backtest @@ -270,8 +270,8 @@ func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticke Open: kline.Open, High: kline.High, Low: kline.Low, - Buy: kline.Close, - Sell: kline.Close, + Buy: kline.Close.Sub(matching.Market.TickSize), + Sell: kline.Close.Add(matching.Market.TickSize), }, nil } From a28081a5d284119c39244d8e68839777e0a582d0 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 12:22:43 +0800 Subject: [PATCH 08/20] xalign: add more checks --- pkg/strategy/xalign/strategy.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index aa30d7412..d50c9f114 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -236,9 +236,9 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se orderBook := bbgo.NewActiveOrderBook("") orderBook.BindStream(session.UserDataStream) + s.orderBooks[sessionName] = orderBook s.sessions[sessionName] = session - s.orderBooks[sessionName] = orderBook } bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { @@ -313,6 +313,7 @@ func (s *Strategy) align(ctx context.Context, sessions map[string]*bbgo.Exchange } else { log.Errorf("orderbook %s not found", selectedSession.Name) } + s.orderBooks[selectedSession.Name].Add(*createdOrder) } } } From 40f8283616e12b5371f4c49b6219371967f702d8 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 13 Jun 2023 12:22:59 +0800 Subject: [PATCH 09/20] scmaker: basic prototype --- config/scmaker.yaml | 50 +++++ pkg/cmd/strategy/builtin.go | 1 + pkg/strategy/scmaker/intensity.go | 44 ++++ pkg/strategy/scmaker/strategy.go | 326 ++++++++++++++++++++++++++++++ pkg/types/position.go | 7 +- 5 files changed, 427 insertions(+), 1 deletion(-) create mode 100644 config/scmaker.yaml create mode 100644 pkg/strategy/scmaker/intensity.go create mode 100644 pkg/strategy/scmaker/strategy.go diff --git a/config/scmaker.yaml b/config/scmaker.yaml new file mode 100644 index 000000000..144de5c55 --- /dev/null +++ b/config/scmaker.yaml @@ -0,0 +1,50 @@ +sessions: + binance: + exchange: max + envVarPrefix: max + + +exchangeStrategies: +- on: max + scmaker: + symbol: USDCUSDT + + ## adjustmentUpdateInterval is the interval for adjusting position + adjustmentUpdateInterval: 1m + + ## liquidityUpdateInterval is the interval for updating liquidity orders + liquidityUpdateInterval: 1h + + midPriceEMA: + interval: 1h + window: 99 + + ## priceRangeBollinger is used for the liquidity price range + priceRangeBollinger: + interval: 1h + window: 10 + k: 1.0 + + numOfLiquidityLayers: 10 + + liquidityLayerTick: 0.01 + + strengthInterval: 1m + + liquidityScale: + exp: + domain: [0, 10] + range: [100, 500] + +backtest: + sessions: + - max + startTime: "2023-05-01" + endTime: "2023-06-01" + symbols: + - USDCUSDT + account: + max: + balances: + USDC: 5000 + USDT: 5000 diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index 0a76cfb4e..995f9f5b8 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -29,6 +29,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/rebalance" _ "github.com/c9s/bbgo/pkg/strategy/rsmaker" _ "github.com/c9s/bbgo/pkg/strategy/schedule" + _ "github.com/c9s/bbgo/pkg/strategy/scmaker" _ "github.com/c9s/bbgo/pkg/strategy/skeleton" _ "github.com/c9s/bbgo/pkg/strategy/supertrend" _ "github.com/c9s/bbgo/pkg/strategy/support" diff --git a/pkg/strategy/scmaker/intensity.go b/pkg/strategy/scmaker/intensity.go new file mode 100644 index 000000000..b4f3cb383 --- /dev/null +++ b/pkg/strategy/scmaker/intensity.go @@ -0,0 +1,44 @@ +package scmaker + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type IntensityStream struct { + *indicator.Float64Series + + Buy, Sell *indicator.RMAStream + window int +} + +func Intensity(source indicator.KLineSubscription, window int) *IntensityStream { + s := &IntensityStream{ + Float64Series: indicator.NewFloat64Series(), + window: window, + + Buy: indicator.RMA2(indicator.NewFloat64Series(), window, false), + Sell: indicator.RMA2(indicator.NewFloat64Series(), window, false), + } + + threshold := fixedpoint.NewFromFloat(100.0) + source.AddSubscriber(func(k types.KLine) { + volume := k.Volume.Float64() + + // ignore zero volume events or <= 10usd events + if volume == 0.0 || k.Close.Mul(k.Volume).Compare(threshold) <= 0 { + return + } + + c := k.Close.Compare(k.Open) + if c > 0 { + s.Buy.PushAndEmit(volume) + } else if c < 0 { + s.Sell.PushAndEmit(volume) + } + s.Float64Series.PushAndEmit(k.High.Sub(k.Low).Float64()) + }) + + return s +} diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go new file mode 100644 index 000000000..ade173865 --- /dev/null +++ b/pkg/strategy/scmaker/strategy.go @@ -0,0 +1,326 @@ +package scmaker + +import ( + "context" + "fmt" + "math" + + log "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "scmaker" + +var ten = fixedpoint.NewFromInt(10) + +type BollingerConfig struct { + Interval types.Interval `json:"interval"` + Window int `json:"window"` + K float64 `json:"k"` +} + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +// scmaker is a stable coin market maker +type Strategy struct { + Environment *bbgo.Environment + Market types.Market + + Symbol string `json:"symbol"` + + NumOfLiquidityLayers int `json:"numOfLiquidityLayers"` + + LiquidityUpdateInterval types.Interval `json:"liquidityUpdateInterval"` + PriceRangeBollinger *BollingerConfig `json:"priceRangeBollinger"` + StrengthInterval types.Interval `json:"strengthInterval"` + + AdjustmentUpdateInterval types.Interval `json:"adjustmentUpdateInterval"` + + MidPriceEMA *types.IntervalWindow `json:"midPriceEMA"` + LiquiditySlideRule *bbgo.SlideRule `json:"liquidityScale"` + + Position *types.Position `json:"position,omitempty" persistence:"position"` + ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + liquidityOrderBook, adjustmentOrderBook *bbgo.ActiveOrderBook + book *types.StreamOrderBook + + liquidityScale bbgo.Scale + + // indicators + ewma *indicator.EWMAStream + boll *indicator.BOLLStream + intensity *IntensityStream +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.AdjustmentUpdateInterval}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.LiquidityUpdateInterval}) + + if s.MidPriceEMA != nil { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.MidPriceEMA.Interval}) + } +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + instanceID := s.InstanceID() + + s.session = session + s.book = types.NewStreamBook(s.Symbol) + s.book.BindStream(session.UserDataStream) + + s.liquidityOrderBook = bbgo.NewActiveOrderBook(s.Symbol) + s.adjustmentOrderBook = bbgo.NewActiveOrderBook(s.Symbol) + + // If position is nil, we need to allocate a new position for calculation + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + // Always update the position fields + s.Position.Strategy = ID + s.Position.StrategyInstanceID = instanceID + + if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 { + s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{ + MakerFeeRate: s.session.MakerFeeRate, + TakerFeeRate: s.session.TakerFeeRate, + }) + } + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + scale, err := s.LiquiditySlideRule.Scale() + if err != nil { + return err + } + + if err := scale.Solve(); err != nil { + return err + } + + s.liquidityScale = scale + + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.Bind() + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + + s.initializeMidPriceEMA(session) + s.initializePriceRangeBollinger(session) + s.initializeIntensityIndicator(session) + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.AdjustmentUpdateInterval, func(k types.KLine) { + s.placeAdjustmentOrders(ctx) + })) + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.LiquidityUpdateInterval, func(k types.KLine) { + s.placeLiquidityOrders(ctx) + })) + + return nil +} + +func (s *Strategy) initializeMidPriceEMA(session *bbgo.ExchangeSession) { + kLines := indicator.KLines(session.MarketDataStream, s.Symbol, s.MidPriceEMA.Interval) + s.ewma = indicator.EWMA2(indicator.ClosePrices(kLines), s.MidPriceEMA.Window) +} + +func (s *Strategy) initializeIntensityIndicator(session *bbgo.ExchangeSession) { + kLines := indicator.KLines(session.MarketDataStream, s.Symbol, s.StrengthInterval) + s.intensity = Intensity(kLines, 10) +} + +func (s *Strategy) initializePriceRangeBollinger(session *bbgo.ExchangeSession) { + kLines := indicator.KLines(session.MarketDataStream, s.Symbol, s.PriceRangeBollinger.Interval) + closePrices := indicator.ClosePrices(kLines) + s.boll = indicator.BOLL2(closePrices, s.PriceRangeBollinger.Window, s.PriceRangeBollinger.K) +} + +func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { + if s.Position.IsDust() { + return + } +} + +func (s *Strategy) placeLiquidityOrders(ctx context.Context) { + _ = s.liquidityOrderBook.GracefulCancel(ctx, s.session.Exchange) + + ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) + if logErr(err, "unable to query ticker") { + return + } + + baseBal, _ := s.session.Account.Balance(s.Market.BaseCurrency) + quoteBal, _ := s.session.Account.Balance(s.Market.QuoteCurrency) + + spread := ticker.Sell.Sub(ticker.Buy) + _ = spread + + midPriceEMA := s.ewma.Last(0) + midPrice := fixedpoint.NewFromFloat(midPriceEMA) + + makerQuota := &bbgo.QuotaTransaction{} + makerQuota.QuoteAsset.Add(quoteBal.Available) + makerQuota.BaseAsset.Add(baseBal.Available) + + bandWidth := s.boll.Last(0) + _ = bandWidth + + log.Infof("mid price ema: %f boll band width: %f", midPriceEMA, bandWidth) + + var liqOrders []types.SubmitOrder + for i := 0; i <= s.NumOfLiquidityLayers; i++ { + fi := fixedpoint.NewFromInt(int64(i)) + quantity := fixedpoint.NewFromFloat(s.liquidityScale.Call(float64(i))) + bidPrice := midPrice.Sub(s.Market.TickSize.Mul(fi)) + askPrice := midPrice.Add(s.Market.TickSize.Mul(fi)) + if i == 0 { + bidPrice = ticker.Buy + askPrice = ticker.Sell + } + + log.Infof("layer #%d %f/%f = %f", i, askPrice.Float64(), bidPrice.Float64(), quantity.Float64()) + + placeBuy := true + placeSell := true + averageCost := s.Position.AverageCost + // when long position, do not place sell orders below the average cost + if !s.Position.IsDust() { + if s.Position.IsLong() && askPrice.Compare(averageCost) < 0 { + placeSell = false + } + + if s.Position.IsShort() && bidPrice.Compare(averageCost) > 0 { + placeBuy = false + } + } + + quoteQuantity := quantity.Mul(bidPrice) + + if !makerQuota.QuoteAsset.Lock(quoteQuantity) { + placeBuy = false + } + + if !makerQuota.BaseAsset.Lock(quantity) { + placeSell = false + } + + if placeBuy { + liqOrders = append(liqOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimitMaker, + Quantity: quantity, + Price: bidPrice, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + }) + } + + if placeSell { + liqOrders = append(liqOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Quantity: quantity, + Price: askPrice, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + }) + } + } + + _, err = s.orderExecutor.SubmitOrders(ctx, liqOrders...) + logErr(err, "unable to place liquidity orders") +} + +func (s *Strategy) generateOrders(symbol string, side types.SideType, price, priceTick, baseQuantity fixedpoint.Value, numOrders int) (orders []types.SubmitOrder) { + var expBase = fixedpoint.Zero + + switch side { + case types.SideTypeBuy: + if priceTick.Sign() > 0 { + priceTick = priceTick.Neg() + } + + case types.SideTypeSell: + if priceTick.Sign() < 0 { + priceTick = priceTick.Neg() + } + } + + decdigits := priceTick.Abs().NumIntDigits() + step := priceTick.Abs().MulExp(-decdigits + 1) + + for i := 0; i < numOrders; i++ { + quantityExp := fixedpoint.NewFromFloat(math.Exp(expBase.Float64())) + volume := baseQuantity.Mul(quantityExp) + amount := volume.Mul(price) + // skip order less than 10usd + if amount.Compare(ten) < 0 { + log.Warnf("amount too small (< 10usd). price=%s volume=%s amount=%s", + price.String(), volume.String(), amount.String()) + continue + } + + orders = append(orders, types.SubmitOrder{ + Symbol: symbol, + Side: side, + Type: types.OrderTypeLimit, + Price: price, + Quantity: volume, + }) + + log.Infof("%s order: %s @ %s", side, volume.String(), price.String()) + + if len(orders) >= numOrders { + break + } + + price = price.Add(priceTick) + expBase = expBase.Add(step) + } + + return orders +} + +func logErr(err error, msgAndArgs ...interface{}) bool { + if err == nil { + return false + } + + if len(msgAndArgs) == 0 { + log.WithError(err).Error(err.Error()) + } else if len(msgAndArgs) == 1 { + msg := msgAndArgs[0].(string) + log.WithError(err).Error(msg) + } else if len(msgAndArgs) > 1 { + msg := msgAndArgs[0].(string) + log.WithError(err).Errorf(msg, msgAndArgs[1:]...) + } + + return true +} diff --git a/pkg/types/position.go b/pkg/types/position.go index bd428fe43..6cee34e75 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -170,7 +170,12 @@ func (p *Position) NewMarketCloseOrder(percentage fixedpoint.Value) *SubmitOrder } } -func (p *Position) IsDust(price fixedpoint.Value) bool { +func (p *Position) IsDust(a ...fixedpoint.Value) bool { + price := p.AverageCost + if len(a) > 0 { + price = a[0] + } + base := p.Base.Abs() return p.Market.IsDustQuantity(base, price) } From aa4f998382261bac69b9def4b9b7af114925e7c3 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 15:16:32 +0800 Subject: [PATCH 10/20] bbgo: add scale Sum method --- pkg/bbgo/scale.go | 70 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/pkg/bbgo/scale.go b/pkg/bbgo/scale.go index da5a5bc52..207a42589 100644 --- a/pkg/bbgo/scale.go +++ b/pkg/bbgo/scale.go @@ -12,6 +12,7 @@ type Scale interface { Formula() string FormulaOf(x float64) string Call(x float64) (y float64) + Sum(step float64) float64 } func init() { @@ -21,6 +22,7 @@ func init() { _ = Scale(&QuadraticScale{}) } +// f(x) := ab^x // y := ab^x // shift xs[0] to 0 (x - h) // a = y1 @@ -56,6 +58,14 @@ func (s *ExponentialScale) Solve() error { return nil } +func (s *ExponentialScale) Sum(step float64) float64 { + sum := 0.0 + for x := s.Domain[0]; x <= s.Domain[1]; x += step { + sum += s.Call(x) + } + return sum +} + func (s *ExponentialScale) String() string { return s.Formula() } @@ -100,6 +110,14 @@ func (s *LogarithmicScale) Call(x float64) (y float64) { return y } +func (s *LogarithmicScale) Sum(step float64) float64 { + sum := 0.0 + for x := s.Domain[0]; x <= s.Domain[1]; x += step { + sum += s.Call(x) + } + return sum +} + func (s *LogarithmicScale) String() string { return s.Formula() } @@ -158,6 +176,14 @@ func (s *LinearScale) Call(x float64) (y float64) { return y } +func (s *LinearScale) Sum(step float64) float64 { + sum := 0.0 + for x := s.Domain[0]; x <= s.Domain[1]; x += step { + sum += s.Call(x) + } + return sum +} + func (s *LinearScale) String() string { return s.Formula() } @@ -201,6 +227,14 @@ func (s *QuadraticScale) Call(x float64) (y float64) { return y } +func (s *QuadraticScale) Sum(step float64) float64 { + sum := 0.0 + for x := s.Domain[0]; x <= s.Domain[1]; x += step { + sum += s.Call(x) + } + return sum +} + func (s *QuadraticScale) String() string { return s.Formula() } @@ -266,18 +300,20 @@ func (rule *SlideRule) Scale() (Scale, error) { // LayerScale defines the scale DSL for maker layers, e.g., // // quantityScale: -// byLayer: -// exp: -// domain: [1, 5] -// range: [0.01, 1.0] +// +// byLayer: +// exp: +// domain: [1, 5] +// range: [0.01, 1.0] // // and // // quantityScale: -// byLayer: -// linear: -// domain: [1, 3] -// range: [0.01, 1.0] +// +// byLayer: +// linear: +// domain: [1, 3] +// range: [0.01, 1.0] type LayerScale struct { LayerRule *SlideRule `json:"byLayer"` } @@ -303,18 +339,20 @@ func (s *LayerScale) Scale(layer int) (quantity float64, err error) { // PriceVolumeScale defines the scale DSL for strategy, e.g., // // quantityScale: -// byPrice: -// exp: -// domain: [10_000, 50_000] -// range: [0.01, 1.0] +// +// byPrice: +// exp: +// domain: [10_000, 50_000] +// range: [0.01, 1.0] // // and // // quantityScale: -// byVolume: -// linear: -// domain: [10_000, 50_000] -// range: [0.01, 1.0] +// +// byVolume: +// linear: +// domain: [10_000, 50_000] +// range: [0.01, 1.0] type PriceVolumeScale struct { ByPriceRule *SlideRule `json:"byPrice"` ByVolumeRule *SlideRule `json:"byVolume"` From b8597a1803fbca7d8329bc8f8bc75dfdad0ae877 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 15:16:48 +0800 Subject: [PATCH 11/20] scmaker: calculate balance quantity --- config/scmaker.yaml | 2 +- go.sum | 1 + pkg/strategy/scmaker/strategy.go | 49 ++++++++++++++++++++++++-------- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/config/scmaker.yaml b/config/scmaker.yaml index 144de5c55..efc3eb945 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -34,7 +34,7 @@ exchangeStrategies: liquidityScale: exp: domain: [0, 10] - range: [100, 500] + range: [1, 4] backtest: sessions: diff --git a/go.sum b/go.sum index f9c5077c3..f8c6b72d2 100644 --- a/go.sum +++ b/go.sum @@ -671,6 +671,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM= github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index ade173865..44666b817 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -27,7 +27,7 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } -// scmaker is a stable coin market maker +// Strategy scmaker is a stable coin market maker type Strategy struct { Environment *bbgo.Environment Market types.Market @@ -191,18 +191,43 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { log.Infof("mid price ema: %f boll band width: %f", midPriceEMA, bandWidth) - var liqOrders []types.SubmitOrder + n := s.liquidityScale.Sum(1.0) + + var bidPrices []fixedpoint.Value + var askPrices []fixedpoint.Value + + // calculate and collect prices for i := 0; i <= s.NumOfLiquidityLayers; i++ { fi := fixedpoint.NewFromInt(int64(i)) - quantity := fixedpoint.NewFromFloat(s.liquidityScale.Call(float64(i))) - bidPrice := midPrice.Sub(s.Market.TickSize.Mul(fi)) - askPrice := midPrice.Add(s.Market.TickSize.Mul(fi)) if i == 0 { - bidPrice = ticker.Buy - askPrice = ticker.Sell + bidPrices = append(bidPrices, ticker.Buy) + askPrices = append(askPrices, ticker.Sell) + } else if i == s.NumOfLiquidityLayers { + bwf := fixedpoint.NewFromFloat(bandWidth) + bidPrices = append(bidPrices, midPrice.Add(-bwf)) + askPrices = append(askPrices, midPrice.Add(bwf)) + } else { + bidPrice := midPrice.Sub(s.Market.TickSize.Mul(fi)) + askPrice := midPrice.Add(s.Market.TickSize.Mul(fi)) + bidPrices = append(bidPrices, bidPrice) + askPrices = append(askPrices, askPrice) } + } - log.Infof("layer #%d %f/%f = %f", i, askPrice.Float64(), bidPrice.Float64(), quantity.Float64()) + askX := baseBal.Available.Float64() / n + bidX := quoteBal.Available.Float64() / (n * (fixedpoint.Sum(bidPrices).Float64())) + + askX = math.Trunc(askX*1e8) / 1e8 + bidX = math.Trunc(bidX*1e8) / 1e8 + + var liqOrders []types.SubmitOrder + for i := 0; i <= s.NumOfLiquidityLayers; i++ { + bidQuantity := fixedpoint.NewFromFloat(s.liquidityScale.Call(float64(i)) * bidX) + askQuantity := fixedpoint.NewFromFloat(s.liquidityScale.Call(float64(i)) * askX) + bidPrice := bidPrices[i] + askPrice := askPrices[i] + + log.Infof("layer #%d %f/%f = %f/%f", i, askPrice.Float64(), bidPrice.Float64(), askQuantity.Float64(), bidQuantity.Float64()) placeBuy := true placeSell := true @@ -218,13 +243,13 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { } } - quoteQuantity := quantity.Mul(bidPrice) + quoteQuantity := bidQuantity.Mul(bidPrice) if !makerQuota.QuoteAsset.Lock(quoteQuantity) { placeBuy = false } - if !makerQuota.BaseAsset.Lock(quantity) { + if !makerQuota.BaseAsset.Lock(askQuantity) { placeSell = false } @@ -233,7 +258,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { Symbol: s.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeLimitMaker, - Quantity: quantity, + Quantity: bidQuantity, Price: bidPrice, Market: s.Market, TimeInForce: types.TimeInForceGTC, @@ -245,7 +270,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { Symbol: s.Symbol, Side: types.SideTypeSell, Type: types.OrderTypeLimitMaker, - Quantity: quantity, + Quantity: askQuantity, Price: askPrice, Market: s.Market, TimeInForce: types.TimeInForceGTC, From f426d151a82a5e169bd48419474fb7782f0f5021 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 16:20:37 +0800 Subject: [PATCH 12/20] scmaker: final version --- config/scmaker.yaml | 9 +- pkg/strategy/scmaker/strategy.go | 179 ++++++++++++++++++++----------- 2 files changed, 125 insertions(+), 63 deletions(-) diff --git a/config/scmaker.yaml b/config/scmaker.yaml index efc3eb945..5cc656849 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -2,7 +2,8 @@ sessions: binance: exchange: max envVarPrefix: max - + makerFeeRate: 0% + takerFeeRate: 0.025% exchangeStrategies: - on: max @@ -31,6 +32,8 @@ exchangeStrategies: strengthInterval: 1m + minProfit: 0% + liquidityScale: exp: domain: [0, 10] @@ -39,12 +42,14 @@ exchangeStrategies: backtest: sessions: - max - startTime: "2023-05-01" + startTime: "2023-05-20" endTime: "2023-06-01" symbols: - USDCUSDT account: max: + makerFeeRate: 0.0% + takerFeeRate: 0.025% balances: USDC: 5000 USDT: 5000 diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 44666b817..65ac66234 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -45,6 +45,8 @@ type Strategy struct { MidPriceEMA *types.IntervalWindow `json:"midPriceEMA"` LiquiditySlideRule *bbgo.SlideRule `json:"liquidityScale"` + MinProfit fixedpoint.Value `json:"minProfit"` + Position *types.Position `json:"position,omitempty" persistence:"position"` ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` @@ -98,6 +100,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.Position.Strategy = ID s.Position.StrategyInstanceID = instanceID + // if anyone of the fee rate is defined, this assumes that both are defined. + // so that zero maker fee could be applied if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 { s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{ MakerFeeRate: s.session.MakerFeeRate, @@ -132,13 +136,15 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.initializePriceRangeBollinger(session) s.initializeIntensityIndicator(session) - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.AdjustmentUpdateInterval, func(k types.KLine) { - s.placeAdjustmentOrders(ctx) - })) + session.MarketDataStream.OnKLineClosed(func(k types.KLine) { + if k.Interval == s.AdjustmentUpdateInterval { + s.placeAdjustmentOrders(ctx) + } - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.LiquidityUpdateInterval, func(k types.KLine) { - s.placeLiquidityOrders(ctx) - })) + if k.Interval == s.LiquidityUpdateInterval { + s.placeLiquidityOrders(ctx) + } + }) return nil } @@ -160,9 +166,67 @@ func (s *Strategy) initializePriceRangeBollinger(session *bbgo.ExchangeSession) } func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { + _ = s.adjustmentOrderBook.GracefulCancel(ctx, s.session.Exchange) + if s.Position.IsDust() { return } + + ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) + if logErr(err, "unable to query ticker") { + return + } + + baseBal, _ := s.session.Account.Balance(s.Market.BaseCurrency) + quoteBal, _ := s.session.Account.Balance(s.Market.QuoteCurrency) + + var adjOrders []types.SubmitOrder + + var posSize = s.Position.Base.Abs() + + if s.Position.IsShort() { + price := profitProtectedPrice(types.SideTypeBuy, s.Position.AverageCost, ticker.Sell.Add(-s.Market.TickSize), s.session.MakerFeeRate, s.MinProfit) + quoteQuantity := fixedpoint.Min(price.Mul(posSize), quoteBal.Available) + bidQuantity := quoteQuantity.Div(price) + + if s.Market.IsDustQuantity(bidQuantity, price) { + return + } + + adjOrders = append(adjOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimitMaker, + Side: types.SideTypeBuy, + Price: price, + Quantity: bidQuantity, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + }) + } else if s.Position.IsLong() { + price := profitProtectedPrice(types.SideTypeSell, s.Position.AverageCost, ticker.Buy.Add(s.Market.TickSize), s.session.MakerFeeRate, s.MinProfit) + askQuantity := fixedpoint.Min(posSize, baseBal.Available) + + if s.Market.IsDustQuantity(askQuantity, price) { + return + } + + adjOrders = append(adjOrders, types.SubmitOrder{ + Symbol: s.Symbol, + Type: types.OrderTypeLimitMaker, + Side: types.SideTypeSell, + Price: price, + Quantity: askQuantity, + Market: s.Market, + TimeInForce: types.TimeInForceGTC, + }) + } + + createdOrders, err := s.orderExecutor.SubmitOrders(ctx, adjOrders...) + if logErr(err, "unable to place liquidity orders") { + return + } + + s.adjustmentOrderBook.Add(createdOrders...) } func (s *Strategy) placeLiquidityOrders(ctx context.Context) { @@ -177,7 +241,6 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { quoteBal, _ := s.session.Account.Balance(s.Market.QuoteCurrency) spread := ticker.Sell.Sub(ticker.Buy) - _ = spread midPriceEMA := s.ewma.Last(0) midPrice := fixedpoint.NewFromFloat(midPriceEMA) @@ -187,9 +250,8 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { makerQuota.BaseAsset.Add(baseBal.Available) bandWidth := s.boll.Last(0) - _ = bandWidth - log.Infof("mid price ema: %f boll band width: %f", midPriceEMA, bandWidth) + log.Infof("spread: %f mid price ema: %f boll band width: %f", spread.Float64(), midPriceEMA, bandWidth) n := s.liquidityScale.Sum(1.0) @@ -214,8 +276,31 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { } } - askX := baseBal.Available.Float64() / n - bidX := quoteBal.Available.Float64() / (n * (fixedpoint.Sum(bidPrices).Float64())) + availableBase := baseBal.Available + availableQuote := quoteBal.Available + + /* + log.Infof("available balances: %f %s, %f %s", + availableBase.Float64(), s.Market.BaseCurrency, + availableQuote.Float64(), s.Market.QuoteCurrency) + */ + + log.Infof("balances before liq orders: %s, %s", + baseBal.String(), + quoteBal.String()) + + if !s.Position.IsDust() { + if s.Position.IsLong() { + availableBase = availableBase.Sub(s.Position.Base) + availableBase = s.Market.RoundDownQuantityByPrecision(availableBase) + } else if s.Position.IsShort() { + posSizeInQuote := s.Position.Base.Mul(ticker.Sell) + availableQuote = availableQuote.Sub(posSizeInQuote) + } + } + + askX := availableBase.Float64() / n + bidX := availableQuote.Float64() / (n * (fixedpoint.Sum(bidPrices).Float64())) askX = math.Trunc(askX*1e8) / 1e8 bidX = math.Trunc(bidX*1e8) / 1e8 @@ -227,7 +312,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { bidPrice := bidPrices[i] askPrice := askPrices[i] - log.Infof("layer #%d %f/%f = %f/%f", i, askPrice.Float64(), bidPrice.Float64(), askQuantity.Float64(), bidQuantity.Float64()) + log.Infof("liqudity layer #%d %f/%f = %f/%f", i, askPrice.Float64(), bidPrice.Float64(), askQuantity.Float64(), bidQuantity.Float64()) placeBuy := true placeSell := true @@ -245,11 +330,11 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { quoteQuantity := bidQuantity.Mul(bidPrice) - if !makerQuota.QuoteAsset.Lock(quoteQuantity) { + if s.Market.IsDustQuantity(bidQuantity, bidPrice) || !makerQuota.QuoteAsset.Lock(quoteQuantity) { placeBuy = false } - if !makerQuota.BaseAsset.Lock(askQuantity) { + if s.Market.IsDustQuantity(askQuantity, askPrice) || !makerQuota.BaseAsset.Lock(askQuantity) { placeSell = false } @@ -278,58 +363,30 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { } } - _, err = s.orderExecutor.SubmitOrders(ctx, liqOrders...) - logErr(err, "unable to place liquidity orders") + makerQuota.Commit() + + createdOrders, err := s.orderExecutor.SubmitOrders(ctx, liqOrders...) + if logErr(err, "unable to place liquidity orders") { + return + } + + s.liquidityOrderBook.Add(createdOrders...) } -func (s *Strategy) generateOrders(symbol string, side types.SideType, price, priceTick, baseQuantity fixedpoint.Value, numOrders int) (orders []types.SubmitOrder) { - var expBase = fixedpoint.Zero - +func profitProtectedPrice(side types.SideType, averageCost, price, feeRate, minProfit fixedpoint.Value) fixedpoint.Value { switch side { - case types.SideTypeBuy: - if priceTick.Sign() > 0 { - priceTick = priceTick.Neg() - } - case types.SideTypeSell: - if priceTick.Sign() < 0 { - priceTick = priceTick.Neg() - } + minProfitPrice := averageCost.Add( + averageCost.Mul(feeRate.Add(minProfit))) + return fixedpoint.Max(minProfitPrice, price) + + case types.SideTypeBuy: + minProfitPrice := averageCost.Sub( + averageCost.Mul(feeRate.Add(minProfit))) + return fixedpoint.Min(minProfitPrice, price) + } - - decdigits := priceTick.Abs().NumIntDigits() - step := priceTick.Abs().MulExp(-decdigits + 1) - - for i := 0; i < numOrders; i++ { - quantityExp := fixedpoint.NewFromFloat(math.Exp(expBase.Float64())) - volume := baseQuantity.Mul(quantityExp) - amount := volume.Mul(price) - // skip order less than 10usd - if amount.Compare(ten) < 0 { - log.Warnf("amount too small (< 10usd). price=%s volume=%s amount=%s", - price.String(), volume.String(), amount.String()) - continue - } - - orders = append(orders, types.SubmitOrder{ - Symbol: symbol, - Side: side, - Type: types.OrderTypeLimit, - Price: price, - Quantity: volume, - }) - - log.Infof("%s order: %s @ %s", side, volume.String(), price.String()) - - if len(orders) >= numOrders { - break - } - - price = price.Add(priceTick) - expBase = expBase.Add(step) - } - - return orders + return price } func logErr(err error, msgAndArgs ...interface{}) bool { From 81aa46e46bf82331638d81b4095a5f72e1996bd9 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 16:30:55 +0800 Subject: [PATCH 13/20] config: adjust liquidityScale --- config/scmaker.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/scmaker.yaml b/config/scmaker.yaml index 5cc656849..4db747e3b 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -36,7 +36,7 @@ exchangeStrategies: liquidityScale: exp: - domain: [0, 10] + domain: [0, 9] range: [1, 4] backtest: From 68c3c96b102c2f929866f47dd0276d327abe71a0 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 16:55:32 +0800 Subject: [PATCH 14/20] scmaker: fix balance lock and active order book update issue --- config/scmaker.yaml | 2 +- pkg/strategy/scmaker/strategy.go | 31 +++++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/config/scmaker.yaml b/config/scmaker.yaml index 4db747e3b..dbbaa25b3 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -28,7 +28,7 @@ exchangeStrategies: numOfLiquidityLayers: 10 - liquidityLayerTick: 0.01 + liquidityLayerTick: 0.001 strengthInterval: 1m diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 65ac66234..9a26b21fa 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -44,6 +44,7 @@ type Strategy struct { MidPriceEMA *types.IntervalWindow `json:"midPriceEMA"` LiquiditySlideRule *bbgo.SlideRule `json:"liquidityScale"` + LiquidityLayerTick fixedpoint.Value `json:"liquidityLayerTick"` MinProfit fixedpoint.Value `json:"minProfit"` @@ -89,7 +90,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.book.BindStream(session.UserDataStream) s.liquidityOrderBook = bbgo.NewActiveOrderBook(s.Symbol) + s.liquidityOrderBook.BindStream(session.UserDataStream) + s.adjustmentOrderBook = bbgo.NewActiveOrderBook(s.Symbol) + s.adjustmentOrderBook.BindStream(session.UserDataStream) // If position is nil, we need to allocate a new position for calculation if s.Position == nil { @@ -177,15 +181,21 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { return } + if _, err := s.session.UpdateAccount(ctx); err != nil { + logErr(err, "unable to update account") + return + } + baseBal, _ := s.session.Account.Balance(s.Market.BaseCurrency) quoteBal, _ := s.session.Account.Balance(s.Market.QuoteCurrency) var adjOrders []types.SubmitOrder - var posSize = s.Position.Base.Abs() + posSize := s.Position.Base.Abs() + tickSize := s.Market.TickSize if s.Position.IsShort() { - price := profitProtectedPrice(types.SideTypeBuy, s.Position.AverageCost, ticker.Sell.Add(-s.Market.TickSize), s.session.MakerFeeRate, s.MinProfit) + price := profitProtectedPrice(types.SideTypeBuy, s.Position.AverageCost, ticker.Sell.Add(-tickSize), s.session.MakerFeeRate, s.MinProfit) quoteQuantity := fixedpoint.Min(price.Mul(posSize), quoteBal.Available) bidQuantity := quoteQuantity.Div(price) @@ -203,7 +213,7 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { TimeInForce: types.TimeInForceGTC, }) } else if s.Position.IsLong() { - price := profitProtectedPrice(types.SideTypeSell, s.Position.AverageCost, ticker.Buy.Add(s.Market.TickSize), s.session.MakerFeeRate, s.MinProfit) + price := profitProtectedPrice(types.SideTypeSell, s.Position.AverageCost, ticker.Buy.Add(tickSize), s.session.MakerFeeRate, s.MinProfit) askQuantity := fixedpoint.Min(posSize, baseBal.Available) if s.Market.IsDustQuantity(askQuantity, price) { @@ -230,17 +240,26 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { } func (s *Strategy) placeLiquidityOrders(ctx context.Context) { - _ = s.liquidityOrderBook.GracefulCancel(ctx, s.session.Exchange) + err := s.liquidityOrderBook.GracefulCancel(ctx, s.session.Exchange) + if logErr(err, "unable to cancel orders") { + return + } ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) if logErr(err, "unable to query ticker") { return } + if _, err := s.session.UpdateAccount(ctx); err != nil { + logErr(err, "unable to update account") + return + } + baseBal, _ := s.session.Account.Balance(s.Market.BaseCurrency) quoteBal, _ := s.session.Account.Balance(s.Market.QuoteCurrency) spread := ticker.Sell.Sub(ticker.Buy) + tickSize := fixedpoint.Max(s.LiquidityLayerTick, s.Market.TickSize) midPriceEMA := s.ewma.Last(0) midPrice := fixedpoint.NewFromFloat(midPriceEMA) @@ -269,8 +288,8 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { bidPrices = append(bidPrices, midPrice.Add(-bwf)) askPrices = append(askPrices, midPrice.Add(bwf)) } else { - bidPrice := midPrice.Sub(s.Market.TickSize.Mul(fi)) - askPrice := midPrice.Add(s.Market.TickSize.Mul(fi)) + bidPrice := midPrice.Sub(tickSize.Mul(fi)) + askPrice := midPrice.Add(tickSize.Mul(fi)) bidPrices = append(bidPrices, bidPrice) askPrices = append(askPrices, askPrice) } From 372028ebe64ebe6fcc9f3dbfd9b7fa8f77d15ba2 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 16:58:44 +0800 Subject: [PATCH 15/20] scmaker: truncate price with price precision --- pkg/strategy/scmaker/strategy.go | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 9a26b21fa..d817b0c3e 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -280,19 +280,24 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { // calculate and collect prices for i := 0; i <= s.NumOfLiquidityLayers; i++ { fi := fixedpoint.NewFromInt(int64(i)) - if i == 0 { - bidPrices = append(bidPrices, ticker.Buy) - askPrices = append(askPrices, ticker.Sell) - } else if i == s.NumOfLiquidityLayers { + + bidPrice := ticker.Buy + askPrice := ticker.Sell + + if i == s.NumOfLiquidityLayers { bwf := fixedpoint.NewFromFloat(bandWidth) - bidPrices = append(bidPrices, midPrice.Add(-bwf)) - askPrices = append(askPrices, midPrice.Add(bwf)) - } else { - bidPrice := midPrice.Sub(tickSize.Mul(fi)) - askPrice := midPrice.Add(tickSize.Mul(fi)) - bidPrices = append(bidPrices, bidPrice) - askPrices = append(askPrices, askPrice) + bidPrice = midPrice.Add(-bwf) + askPrice = midPrice.Add(bwf) + } else if i > 0 { + bidPrice = midPrice.Sub(tickSize.Mul(fi)) + askPrice = midPrice.Add(tickSize.Mul(fi)) } + + bidPrice = s.Market.TruncatePrice(bidPrice) + askPrice = s.Market.TruncatePrice(askPrice) + + bidPrices = append(bidPrices, bidPrice) + askPrices = append(askPrices, askPrice) } availableBase := baseBal.Available From 8344193e81fa06546bb35fc54b5a7dd82dad4a52 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 17:00:47 +0800 Subject: [PATCH 16/20] scmaker: rename liquidityLayerTick to liquidityLayerTickSize --- config/scmaker.yaml | 2 +- pkg/strategy/scmaker/strategy.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/config/scmaker.yaml b/config/scmaker.yaml index dbbaa25b3..a0b849850 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -28,7 +28,7 @@ exchangeStrategies: numOfLiquidityLayers: 10 - liquidityLayerTick: 0.001 + liquidityLayerTickSize: 0.0001 strengthInterval: 1m diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index d817b0c3e..cb7fa41ca 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -42,9 +42,9 @@ type Strategy struct { AdjustmentUpdateInterval types.Interval `json:"adjustmentUpdateInterval"` - MidPriceEMA *types.IntervalWindow `json:"midPriceEMA"` - LiquiditySlideRule *bbgo.SlideRule `json:"liquidityScale"` - LiquidityLayerTick fixedpoint.Value `json:"liquidityLayerTick"` + MidPriceEMA *types.IntervalWindow `json:"midPriceEMA"` + LiquiditySlideRule *bbgo.SlideRule `json:"liquidityScale"` + LiquidityLayerTickSize fixedpoint.Value `json:"liquidityLayerTickSize"` MinProfit fixedpoint.Value `json:"minProfit"` @@ -195,7 +195,7 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { tickSize := s.Market.TickSize if s.Position.IsShort() { - price := profitProtectedPrice(types.SideTypeBuy, s.Position.AverageCost, ticker.Sell.Add(-tickSize), s.session.MakerFeeRate, s.MinProfit) + price := profitProtectedPrice(types.SideTypeBuy, s.Position.AverageCost, ticker.Sell.Add(tickSize.Neg()), s.session.MakerFeeRate, s.MinProfit) quoteQuantity := fixedpoint.Min(price.Mul(posSize), quoteBal.Available) bidQuantity := quoteQuantity.Div(price) @@ -259,7 +259,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { quoteBal, _ := s.session.Account.Balance(s.Market.QuoteCurrency) spread := ticker.Sell.Sub(ticker.Buy) - tickSize := fixedpoint.Max(s.LiquidityLayerTick, s.Market.TickSize) + tickSize := fixedpoint.Max(s.LiquidityLayerTickSize, s.Market.TickSize) midPriceEMA := s.ewma.Last(0) midPrice := fixedpoint.NewFromFloat(midPriceEMA) @@ -286,7 +286,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { if i == s.NumOfLiquidityLayers { bwf := fixedpoint.NewFromFloat(bandWidth) - bidPrice = midPrice.Add(-bwf) + bidPrice = midPrice.Add(bwf.Neg()) askPrice = midPrice.Add(bwf) } else if i > 0 { bidPrice = midPrice.Sub(tickSize.Mul(fi)) From 5f3ab4776d28261f0b704adc7fc13553636f831a Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 17:05:58 +0800 Subject: [PATCH 17/20] config/scmaker: adjust minProfit ratio --- config/scmaker.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/scmaker.yaml b/config/scmaker.yaml index a0b849850..bc009f3b6 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -32,7 +32,7 @@ exchangeStrategies: strengthInterval: 1m - minProfit: 0% + minProfit: 0.004% liquidityScale: exp: From 148869d46b254d157cb4f92536ae6283442ffea8 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 14 Jun 2023 17:31:01 +0800 Subject: [PATCH 18/20] scmaker: clean up --- pkg/strategy/scmaker/strategy.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index cb7fa41ca..99bfbfc15 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -303,12 +303,6 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { availableBase := baseBal.Available availableQuote := quoteBal.Available - /* - log.Infof("available balances: %f %s, %f %s", - availableBase.Float64(), s.Market.BaseCurrency, - availableQuote.Float64(), s.Market.QuoteCurrency) - */ - log.Infof("balances before liq orders: %s, %s", baseBal.String(), quoteBal.String()) From 73726b91c782ef1912cfd944386cd9550f83d49d Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Jun 2023 13:47:21 +0800 Subject: [PATCH 19/20] scmaker: check ticker price and adjust liq order prices --- pkg/strategy/scmaker/strategy.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index 99bfbfc15..291072c2b 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -289,8 +289,17 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { bidPrice = midPrice.Add(bwf.Neg()) askPrice = midPrice.Add(bwf) } else if i > 0 { - bidPrice = midPrice.Sub(tickSize.Mul(fi)) - askPrice = midPrice.Add(tickSize.Mul(fi)) + sp := tickSize.Mul(fi) + bidPrice = midPrice.Sub(sp) + askPrice = midPrice.Add(sp) + + if bidPrice.Compare(ticker.Buy) < 0 { + bidPrice = ticker.Buy.Sub(sp) + } + + if askPrice.Compare(ticker.Sell) > 0 { + askPrice = ticker.Sell.Add(sp) + } } bidPrice = s.Market.TruncatePrice(bidPrice) From eb464cb5954fecd038a8740a356eea846065ec95 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 15 Jun 2023 14:07:35 +0800 Subject: [PATCH 20/20] config/scmaker: adjust minProfit --- config/scmaker.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/scmaker.yaml b/config/scmaker.yaml index bc009f3b6..49be4bacd 100644 --- a/config/scmaker.yaml +++ b/config/scmaker.yaml @@ -32,7 +32,7 @@ exchangeStrategies: strengthInterval: 1m - minProfit: 0.004% + minProfit: 0.01% liquidityScale: exp: