diff --git a/config/liquiditymaker.yaml b/config/liquiditymaker.yaml new file mode 100644 index 000000000..85288d5b9 --- /dev/null +++ b/config/liquiditymaker.yaml @@ -0,0 +1,54 @@ +sessions: + max: + exchange: max + envVarPrefix: max + makerFeeRate: 0% + takerFeeRate: 0.025% + +#services: +# googleSpreadSheet: +# jsonTokenFile: ".credentials/google-cloud/service-account-json-token.json" +# spreadSheetId: "YOUR_SPREADSHEET_ID" + +exchangeStrategies: +- on: max + liquiditymaker: + symbol: &symbol USDTTWD + + ## adjustmentUpdateInterval is the interval for adjusting position + adjustmentUpdateInterval: 1m + + ## liquidityUpdateInterval is the interval for updating liquidity orders + liquidityUpdateInterval: 1h + + numOfLiquidityLayers: 30 + askLiquidityAmount: 20_000.0 + bidLiquidityAmount: 20_000.0 + liquidityPriceRange: 2% + useLastTradePrice: true + spread: 1.1% + + liquidityScale: + exp: + domain: [1, 30] + range: [1, 4] + + ## maxExposure controls how much balance should be used for placing the maker orders + maxExposure: 200_000 + minProfit: 0.01% + + +backtest: + sessions: + - max + startTime: "2023-05-20" + endTime: "2023-06-01" + symbols: + - *symbol + account: + max: + makerFeeRate: 0.0% + takerFeeRate: 0.025% + balances: + USDT: 5000 + TWD: 150_000 diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index d868e926a..867c72dc2 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -25,6 +25,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/irr" _ "github.com/c9s/bbgo/pkg/strategy/kline" _ "github.com/c9s/bbgo/pkg/strategy/linregmaker" + _ "github.com/c9s/bbgo/pkg/strategy/liquiditymaker" _ "github.com/c9s/bbgo/pkg/strategy/marketcap" _ "github.com/c9s/bbgo/pkg/strategy/pivotshort" _ "github.com/c9s/bbgo/pkg/strategy/pricealert" diff --git a/pkg/strategy/liquiditymaker/generator_test.go b/pkg/strategy/liquiditymaker/generator_test.go index d56700f6e..995f33bd7 100644 --- a/pkg/strategy/liquiditymaker/generator_test.go +++ b/pkg/strategy/liquiditymaker/generator_test.go @@ -42,7 +42,7 @@ func TestLiquidityOrderGenerator(t *testing.T) { assert.InDelta(t, 1.0, scale.Call(1.0), 0.00001) assert.InDelta(t, 4.0, scale.Call(30.0), 0.00001) - totalAmount := Number(200_000.0) + totalAmount := Number(20_000.0) t.Run("ask orders", func(t *testing.T) { orders := g.Generate(types.SideTypeSell, totalAmount, Number(2.0), Number(2.04), 30, scale) @@ -55,26 +55,26 @@ func TestLiquidityOrderGenerator(t *testing.T) { assert.InDelta(t, totalAmount.Float64(), totalQuoteQuantity.Float64(), 1.0) AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{ - {Side: types.SideTypeSell, Price: Number("2.0000"), Quantity: Number("1513.40")}, - {Side: types.SideTypeSell, Price: Number("2.0013"), Quantity: Number("1587.50")}, - {Side: types.SideTypeSell, Price: Number("2.0027"), Quantity: Number("1665.23")}, - {Side: types.SideTypeSell, Price: Number("2.0041"), Quantity: Number("1746.77")}, - {Side: types.SideTypeSell, Price: Number("2.0055"), Quantity: Number("1832.30")}, - {Side: types.SideTypeSell, Price: Number("2.0068"), Quantity: Number("1922.02")}, - {Side: types.SideTypeSell, Price: Number("2.0082"), Quantity: Number("2016.13")}, - {Side: types.SideTypeSell, Price: Number("2.0096"), Quantity: Number("2114.85")}, - {Side: types.SideTypeSell, Price: Number("2.0110"), Quantity: Number("2218.40")}, - {Side: types.SideTypeSell, Price: Number("2.0124"), Quantity: Number("2327.02")}, - {Side: types.SideTypeSell, Price: Number("2.0137"), Quantity: Number("2440.96")}, - {Side: types.SideTypeSell, Price: Number("2.0151"), Quantity: Number("2560.48")}, - {Side: types.SideTypeSell, Price: Number("2.0165"), Quantity: Number("2685.86")}, - {Side: types.SideTypeSell, Price: Number("2.0179"), Quantity: Number("2817.37")}, - {Side: types.SideTypeSell, Price: Number("2.0193"), Quantity: Number("2955.32")}, + {Side: types.SideTypeSell, Price: Number("2.0000"), Quantity: Number("151.34")}, + {Side: types.SideTypeSell, Price: Number("2.0013"), Quantity: Number("158.75")}, + {Side: types.SideTypeSell, Price: Number("2.0027"), Quantity: Number("166.52")}, + {Side: types.SideTypeSell, Price: Number("2.0041"), Quantity: Number("174.67")}, + {Side: types.SideTypeSell, Price: Number("2.0055"), Quantity: Number("183.23")}, + {Side: types.SideTypeSell, Price: Number("2.0068"), Quantity: Number("192.20")}, + {Side: types.SideTypeSell, Price: Number("2.0082"), Quantity: Number("201.61")}, + {Side: types.SideTypeSell, Price: Number("2.0096"), Quantity: Number("211.48")}, + {Side: types.SideTypeSell, Price: Number("2.0110"), Quantity: Number("221.84")}, + {Side: types.SideTypeSell, Price: Number("2.0124"), Quantity: Number("232.70")}, + {Side: types.SideTypeSell, Price: Number("2.0137"), Quantity: Number("244.09")}, + {Side: types.SideTypeSell, Price: Number("2.0151"), Quantity: Number("256.04")}, + {Side: types.SideTypeSell, Price: Number("2.0165"), Quantity: Number("268.58")}, + {Side: types.SideTypeSell, Price: Number("2.0179"), Quantity: Number("281.73")}, + {Side: types.SideTypeSell, Price: Number("2.0193"), Quantity: Number("295.53")}, }, orders[0:15]) AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{ - {Side: types.SideTypeSell, Price: Number("2.0386"), Quantity: Number("5771.04")}, - {Side: types.SideTypeSell, Price: Number("2.0399"), Quantity: Number("6053.62")}, + {Side: types.SideTypeSell, Price: Number("2.0386"), Quantity: Number("577.10")}, + {Side: types.SideTypeSell, Price: Number("2.0399"), Quantity: Number("605.36")}, }, orders[28:30]) }) @@ -89,26 +89,26 @@ func TestLiquidityOrderGenerator(t *testing.T) { assert.InDelta(t, totalAmount.Float64(), totalQuoteQuantity.Float64(), 1.0) AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{ - {Side: types.SideTypeBuy, Price: Number("2.0000"), Quantity: Number("1551.37")}, - {Side: types.SideTypeBuy, Price: Number("1.9986"), Quantity: Number("1627.33")}, - {Side: types.SideTypeBuy, Price: Number("1.9972"), Quantity: Number("1707.01")}, - {Side: types.SideTypeBuy, Price: Number("1.9958"), Quantity: Number("1790.59")}, - {Side: types.SideTypeBuy, Price: Number("1.9944"), Quantity: Number("1878.27")}, - {Side: types.SideTypeBuy, Price: Number("1.9931"), Quantity: Number("1970.24")}, - {Side: types.SideTypeBuy, Price: Number("1.9917"), Quantity: Number("2066.71")}, - {Side: types.SideTypeBuy, Price: Number("1.9903"), Quantity: Number("2167.91")}, - {Side: types.SideTypeBuy, Price: Number("1.9889"), Quantity: Number("2274.06")}, - {Side: types.SideTypeBuy, Price: Number("1.9875"), Quantity: Number("2385.40")}, - {Side: types.SideTypeBuy, Price: Number("1.9862"), Quantity: Number("2502.20")}, - {Side: types.SideTypeBuy, Price: Number("1.9848"), Quantity: Number("2624.72")}, - {Side: types.SideTypeBuy, Price: Number("1.9834"), Quantity: Number("2753.24")}, - {Side: types.SideTypeBuy, Price: Number("1.9820"), Quantity: Number("2888.05")}, - {Side: types.SideTypeBuy, Price: Number("1.9806"), Quantity: Number("3029.46")}, + {Side: types.SideTypeBuy, Price: Number("2.0000"), Quantity: Number("155.13")}, + {Side: types.SideTypeBuy, Price: Number("1.9986"), Quantity: Number("162.73")}, + {Side: types.SideTypeBuy, Price: Number("1.9972"), Quantity: Number("170.70")}, + {Side: types.SideTypeBuy, Price: Number("1.9958"), Quantity: Number("179.05")}, + {Side: types.SideTypeBuy, Price: Number("1.9944"), Quantity: Number("187.82")}, + {Side: types.SideTypeBuy, Price: Number("1.9931"), Quantity: Number("197.02")}, + {Side: types.SideTypeBuy, Price: Number("1.9917"), Quantity: Number("206.67")}, + {Side: types.SideTypeBuy, Price: Number("1.9903"), Quantity: Number("216.79")}, + {Side: types.SideTypeBuy, Price: Number("1.9889"), Quantity: Number("227.40")}, + {Side: types.SideTypeBuy, Price: Number("1.9875"), Quantity: Number("238.54")}, + {Side: types.SideTypeBuy, Price: Number("1.9862"), Quantity: Number("250.22")}, + {Side: types.SideTypeBuy, Price: Number("1.9848"), Quantity: Number("262.47")}, + {Side: types.SideTypeBuy, Price: Number("1.9834"), Quantity: Number("275.32")}, + {Side: types.SideTypeBuy, Price: Number("1.9820"), Quantity: Number("288.80")}, + {Side: types.SideTypeBuy, Price: Number("1.9806"), Quantity: Number("302.94")}, }, orders[0:15]) AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{ - {Side: types.SideTypeBuy, Price: Number("1.9613"), Quantity: Number("5915.83")}, - {Side: types.SideTypeBuy, Price: Number("1.9600"), Quantity: Number("6205.49")}, + {Side: types.SideTypeBuy, Price: Number("1.9613"), Quantity: Number("591.58")}, + {Side: types.SideTypeBuy, Price: Number("1.9600"), Quantity: Number("620.54")}, }, orders[28:30]) }) } diff --git a/pkg/strategy/liquiditymaker/strategy.go b/pkg/strategy/liquiditymaker/strategy.go index 6dcb8ece0..07a6517e7 100644 --- a/pkg/strategy/liquiditymaker/strategy.go +++ b/pkg/strategy/liquiditymaker/strategy.go @@ -3,7 +3,6 @@ package liquiditymaker import ( "context" "fmt" - "math" "sync" log "github.com/sirupsen/logrus" @@ -44,18 +43,16 @@ type Strategy struct { AdjustmentUpdateInterval types.Interval `json:"adjustmentUpdateInterval"` - NumOfLiquidityLayers int `json:"numOfLiquidityLayers"` - LiquiditySlideRule *bbgo.SlideRule `json:"liquidityScale"` - LiquidityLayerTickSize fixedpoint.Value `json:"liquidityLayerTickSize"` - LiquiditySkew fixedpoint.Value `json:"liquiditySkew"` - LiquidityPriceRange fixedpoint.Value `json:"liquidityPriceRange"` + NumOfLiquidityLayers int `json:"numOfLiquidityLayers"` + LiquiditySlideRule *bbgo.SlideRule `json:"liquidityScale"` + LiquidityPriceRange fixedpoint.Value `json:"liquidityPriceRange"` + AskLiquidityAmount fixedpoint.Value `json:"askLiquidityAmount"` + BidLiquidityAmount fixedpoint.Value `json:"bidLiquidityAmount"` - AskLiquidityAmount fixedpoint.Value `json:"askLiquidityAmount"` - BidLiquidityAmount fixedpoint.Value `json:"bidLiquidityAmount"` - - Spread fixedpoint.Value `json:"spread"` - MaxPrice fixedpoint.Value `json:"maxPrice"` - MinPrice fixedpoint.Value `json:"minPrice"` + UseLastTradePrice bool `json:"useLastTradePrice"` + Spread fixedpoint.Value `json:"spread"` + MaxPrice fixedpoint.Value `json:"maxPrice"` + MinPrice fixedpoint.Value `json:"minPrice"` MaxExposure fixedpoint.Value `json:"maxExposure"` @@ -65,6 +62,8 @@ type Strategy struct { book *types.StreamOrderBook liquidityScale bbgo.Scale + + orderGenerator *LiquidityOrderGenerator } func (s *Strategy) ID() string { @@ -85,6 +84,11 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. s.Strategy = &common.Strategy{} s.Strategy.Initialize(ctx, s.Environment, session, s.Market, ID, s.InstanceID()) + s.orderGenerator = &LiquidityOrderGenerator{ + Symbol: s.Symbol, + Market: s.Market, + } + s.book = types.NewStreamBook(s.Symbol) s.book.BindStream(session.MarketDataStream) @@ -209,6 +213,11 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) { } func (s *Strategy) placeLiquidityOrders(ctx context.Context) { + 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 @@ -219,11 +228,14 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { return } - err = s.liquidityOrderBook.GracefulCancel(ctx, s.Session.Exchange) - if logErr(err, "unable to cancel orders") { + 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) + if ticker.Buy.IsZero() && ticker.Sell.IsZero() { ticker.Sell = ticker.Last.Add(s.Market.TickSize) ticker.Buy = ticker.Last.Sub(s.Market.TickSize) @@ -233,78 +245,32 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { ticker.Sell = ticker.Buy.Add(s.Market.TickSize) } - 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) + log.Infof("ticker: %+v", ticker) lastTradedPrice := ticker.Last midPrice := ticker.Sell.Add(ticker.Buy).Div(fixedpoint.Two) currentSpread := ticker.Sell.Sub(ticker.Buy) - tickSize := fixedpoint.Max(s.LiquidityLayerTickSize, s.Market.TickSize) sideSpread := s.Spread.Div(fixedpoint.Two) - log.Infof("current: spread: %f lastTradedPrice: %f midPrice: %f", currentSpread.Float64(), lastTradedPrice.Float64(), midPrice.Float64()) + if s.UseLastTradePrice { + midPrice = lastTradedPrice + } + + log.Infof("current spread: %f lastTradedPrice: %f midPrice: %f", currentSpread.Float64(), lastTradedPrice.Float64(), midPrice.Float64()) ask1Price := midPrice.Mul(fixedpoint.One.Add(sideSpread)) bid1Price := midPrice.Mul(fixedpoint.One.Sub(sideSpread)) askLastPrice := midPrice.Mul(fixedpoint.One.Add(s.LiquidityPriceRange)) bidLastPrice := midPrice.Mul(fixedpoint.One.Sub(s.LiquidityPriceRange)) - log.Infof("wanted side spread: %f askRange: %f ~ %f bidRange: %f ~ %f", sideSpread.Float64(), + log.Infof("wanted side spread: %f askRange: %f ~ %f bidRange: %f ~ %f", + sideSpread.Float64(), ask1Price.Float64(), askLastPrice.Float64(), bid1Price.Float64(), bidLastPrice.Float64()) - askLayerSpread := askLastPrice.Sub(ask1Price).Div(fixedpoint.NewFromInt(int64(s.NumOfLiquidityLayers))) - bidLayerSpread := bid1Price.Sub(bidLastPrice).Div(fixedpoint.NewFromInt(int64(s.NumOfLiquidityLayers))) - - if askLayerSpread.Compare(tickSize) < 0 { - askLayerSpread = tickSize - } - - if bidLayerSpread.Compare(tickSize) < 0 { - bidLayerSpread = tickSize - } - - sum := s.liquidityScale.Sum(1.0) - askSum := sum - bidSum := sum - log.Infof("liquidity sum: %f / %f", askSum, bidSum) - - skew := s.LiquiditySkew.Float64() - useSkew := !s.LiquiditySkew.IsZero() - if useSkew { - askSum = sum / skew - bidSum = sum * skew - log.Infof("adjusted liqudity skew: %f / %f", askSum, bidSum) - } - - var bidPrices []fixedpoint.Value - var askPrices []fixedpoint.Value - - // calculate and collect prices - for i := 0; i <= s.NumOfLiquidityLayers; i++ { - fi := fixedpoint.NewFromInt(int64(i)) - bidPrice := bid1Price.Sub(bidLayerSpread.Mul(fi)) - askPrice := ask1Price.Add(askLayerSpread.Mul(fi)) - - bidPrice = s.Market.TruncatePrice(bidPrice) - askPrice = s.Market.TruncatePrice(askPrice) - - bidPrices = append(bidPrices, bidPrice) - askPrices = append(askPrices, askPrice) - } - availableBase := baseBal.Available availableQuote := quoteBal.Available - makerQuota := &bbgo.QuotaTransaction{} - makerQuota.QuoteAsset.Add(availableQuote) - makerQuota.BaseAsset.Add(availableBase) - log.Infof("balances before liq orders: %s, %s", baseBal.String(), quoteBal.String()) @@ -319,79 +285,32 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { } } - askX := availableBase.Float64() / askSum - bidX := availableQuote.Float64() / (bidSum * (fixedpoint.Sum(bidPrices).Float64())) + bidOrders := s.orderGenerator.Generate(types.SideTypeBuy, + fixedpoint.Min(s.BidLiquidityAmount, quoteBal.Available), + bid1Price, + bidLastPrice, + s.NumOfLiquidityLayers, + s.liquidityScale) - askX = math.Trunc(askX*1e8) / 1e8 - bidX = math.Trunc(bidX*1e8) / 1e8 + askOrders := s.orderGenerator.Generate(types.SideTypeSell, + s.AskLiquidityAmount, + ask1Price, + askLastPrice, + s.NumOfLiquidityLayers, + s.liquidityScale) - 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] + orderForms := append(bidOrders, askOrders...) - log.Infof("liqudity layer #%d %f/%f = %f/%f", i, askPrice.Float64(), bidPrice.Float64(), askQuantity.Float64(), bidQuantity.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 := bidQuantity.Mul(bidPrice) - - if s.Market.IsDustQuantity(bidQuantity, bidPrice) || !makerQuota.QuoteAsset.Lock(quoteQuantity) { - placeBuy = false - } - - if s.Market.IsDustQuantity(askQuantity, askPrice) || !makerQuota.BaseAsset.Lock(askQuantity) { - placeSell = false - } - - if placeBuy { - liqOrders = append(liqOrders, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Type: types.OrderTypeLimitMaker, - Quantity: bidQuantity, - 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: askQuantity, - Price: askPrice, - Market: s.Market, - TimeInForce: types.TimeInForceGTC, - }) - } - } - - makerQuota.Commit() - - createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, liqOrders...) + createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, orderForms...) if logErr(err, "unable to place liquidity orders") { return } s.liquidityOrderBook.Add(createdOrders...) - log.Infof("%d liq orders are placed successfully", len(liqOrders)) + log.Infof("%d liq orders are placed successfully", len(orderForms)) + for _, o := range createdOrders { + log.Infof("liq order: %+v", o) + } } func profitProtectedPrice(