From 2c842e54e8f5513ed98aee2eb2f966fee171349a Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 1 Nov 2023 17:01:04 +0800 Subject: [PATCH 1/6] scmaker: fix scmaker stream book binding --- pkg/strategy/scmaker/strategy.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index ecccefc8b..d9eb8d7aa 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -19,8 +19,6 @@ import ( const ID = "scmaker" -var ten = fixedpoint.NewFromInt(10) - type advancedOrderCancelApi interface { CancelAllOrders(ctx context.Context) ([]types.Order, error) CancelOrdersBySymbol(ctx context.Context, symbol string) ([]types.Order, error) @@ -100,12 +98,12 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } } -func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { +func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { s.Strategy = &common.Strategy{} s.Strategy.Initialize(ctx, s.Environment, session, s.Market, ID, s.InstanceID()) s.book = types.NewStreamBook(s.Symbol) - s.book.BindStream(session.UserDataStream) + s.book.BindStream(session.MarketDataStream) s.liquidityOrderBook = bbgo.NewActiveOrderBook(s.Symbol) s.liquidityOrderBook.BindStream(session.UserDataStream) @@ -174,7 +172,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return nil } -func (s *Strategy) preloadKLines(inc *KLineStream, session *bbgo.ExchangeSession, symbol string, interval types.Interval) { +func (s *Strategy) preloadKLines( + inc *KLineStream, session *bbgo.ExchangeSession, symbol string, interval types.Interval, +) { if store, ok := session.MarketDataStore(symbol); ok { if kLinesData, ok := store.KLinesOfInterval(interval); ok { for _, k := range *kLinesData { @@ -476,7 +476,9 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { log.Infof("%d liq orders are placed successfully", len(liqOrders)) } -func profitProtectedPrice(side types.SideType, averageCost, price, feeRate, minProfit fixedpoint.Value) fixedpoint.Value { +func profitProtectedPrice( + side types.SideType, averageCost, price, feeRate, minProfit fixedpoint.Value, +) fixedpoint.Value { switch side { case types.SideTypeSell: minProfitPrice := averageCost.Add( From d2dab58193009367a73977c77d770b23cee25f86 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 1 Nov 2023 17:05:10 +0800 Subject: [PATCH 2/6] scmaker: clean up scmaker risk control --- pkg/strategy/scmaker/strategy.go | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/pkg/strategy/scmaker/strategy.go b/pkg/strategy/scmaker/strategy.go index d9eb8d7aa..79e3f8956 100644 --- a/pkg/strategy/scmaker/strategy.go +++ b/pkg/strategy/scmaker/strategy.go @@ -5,14 +5,12 @@ import ( "fmt" "math" "sync" - "time" log "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" . "github.com/c9s/bbgo/pkg/indicator/v2" - "github.com/c9s/bbgo/pkg/risk/riskcontrol" "github.com/c9s/bbgo/pkg/strategy/common" "github.com/c9s/bbgo/pkg/types" ) @@ -60,12 +58,6 @@ type Strategy struct { MinProfit fixedpoint.Value `json:"minProfit"` - // risk related parameters - PositionHardLimit fixedpoint.Value `json:"positionHardLimit"` - MaxPositionQuantity fixedpoint.Value `json:"maxPositionQuantity"` - CircuitBreakLossThreshold fixedpoint.Value `json:"circuitBreakLossThreshold"` - CircuitBreakEMA types.IntervalWindow `json:"circuitBreakEMA"` - liquidityOrderBook, adjustmentOrderBook *bbgo.ActiveOrderBook book *types.StreamOrderBook @@ -75,9 +67,6 @@ type Strategy struct { ewma *EWMAStream boll *BOLLStream intensity *IntensityStream - - positionRiskControl *riskcontrol.PositionRiskControl - circuitBreakRiskControl *riskcontrol.CircuitBreakRiskControl } func (s *Strategy) ID() string { @@ -111,21 +100,6 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. s.adjustmentOrderBook = bbgo.NewActiveOrderBook(s.Symbol) s.adjustmentOrderBook.BindStream(session.UserDataStream) - if !s.PositionHardLimit.IsZero() && !s.MaxPositionQuantity.IsZero() { - log.Infof("positionHardLimit and maxPositionQuantity are configured, setting up PositionRiskControl...") - s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.OrderExecutor, s.PositionHardLimit, s.MaxPositionQuantity) - } - - if !s.CircuitBreakLossThreshold.IsZero() { - log.Infof("circuitBreakLossThreshold is configured, setting up CircuitBreakRiskControl...") - s.circuitBreakRiskControl = riskcontrol.NewCircuitBreakRiskControl( - s.Position, - session.Indicators(s.Symbol).EWMA(s.CircuitBreakEMA), - s.CircuitBreakLossThreshold, - s.ProfitStats, - 24*time.Hour) - } - scale, err := s.LiquiditySlideRule.Scale() if err != nil { return err @@ -282,7 +256,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { return } - if s.circuitBreakRiskControl != nil && s.circuitBreakRiskControl.IsHalted(ticker.Time) { + if s.IsHalted(ticker.Time) { log.Warn("circuitBreakRiskControl: trading halted") return } From dda2cfb73de27f6597cf924bf6fbfdf4b92ea47a Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 2 Nov 2023 11:59:47 +0800 Subject: [PATCH 3/6] liquiditymaker: first commit --- pkg/strategy/liquiditymaker/strategy.go | 440 ++++++++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 pkg/strategy/liquiditymaker/strategy.go diff --git a/pkg/strategy/liquiditymaker/strategy.go b/pkg/strategy/liquiditymaker/strategy.go new file mode 100644 index 000000000..fcd8f17de --- /dev/null +++ b/pkg/strategy/liquiditymaker/strategy.go @@ -0,0 +1,440 @@ +package liquiditymaker + +import ( + "context" + "fmt" + "math" + "sync" + + log "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + . "github.com/c9s/bbgo/pkg/indicator/v2" + "github.com/c9s/bbgo/pkg/strategy/common" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "liquiditymaker" + +type advancedOrderCancelApi interface { + CancelAllOrders(ctx context.Context) ([]types.Order, error) + CancelOrdersBySymbol(ctx context.Context, symbol string) ([]types.Order, error) +} + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +// Strategy is the strategy struct of LiquidityMaker +// liquidity maker does not care about the current price, it tries to place liquidity orders (limit maker orders) +// around the current mid price +// liquidity maker's target: +// - place enough total liquidity amount on the order book, for example, 20k USDT value liquidity on both sell and buy +// - ensure the spread by placing the orders from the mid price (or the last trade price) +type Strategy struct { + *common.Strategy + + Environment *bbgo.Environment + Market types.Market + + Symbol string `json:"symbol"` + + LiquidityUpdateInterval types.Interval `json:"liquidityUpdateInterval"` + + 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"` + + Spread fixedpoint.Value `json:"spread"` + MaxPrice fixedpoint.Value `json:"maxPrice"` + MinPrice fixedpoint.Value `json:"minPrice"` + + MaxExposure fixedpoint.Value `json:"maxExposure"` + + MinProfit fixedpoint.Value `json:"minProfit"` + + liquidityOrderBook, adjustmentOrderBook *bbgo.ActiveOrderBook + book *types.StreamOrderBook + + liquidityScale bbgo.Scale +} + +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}) +} + +func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + s.Strategy = &common.Strategy{} + s.Strategy.Initialize(ctx, s.Environment, session, s.Market, ID, s.InstanceID()) + + s.book = types.NewStreamBook(s.Symbol) + s.book.BindStream(session.MarketDataStream) + + s.liquidityOrderBook = bbgo.NewActiveOrderBook(s.Symbol) + s.liquidityOrderBook.BindStream(session.UserDataStream) + + s.adjustmentOrderBook = bbgo.NewActiveOrderBook(s.Symbol) + s.adjustmentOrderBook.BindStream(session.UserDataStream) + + scale, err := s.LiquiditySlideRule.Scale() + if err != nil { + return err + } + + if err := scale.Solve(); err != nil { + return err + } + + if cancelApi, ok := session.Exchange.(advancedOrderCancelApi); ok { + _, _ = cancelApi.CancelOrdersBySymbol(ctx, s.Symbol) + } + + s.liquidityScale = scale + + session.UserDataStream.OnStart(func() { + s.placeLiquidityOrders(ctx) + }) + + session.MarketDataStream.OnKLineClosed(func(k types.KLine) { + if k.Interval == s.AdjustmentUpdateInterval { + s.placeAdjustmentOrders(ctx) + } + + if k.Interval == s.LiquidityUpdateInterval { + s.placeLiquidityOrders(ctx) + } + }) + + bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + if err := s.liquidityOrderBook.GracefulCancel(ctx, s.Session.Exchange); err != nil { + logErr(err, "unable to cancel liquidity orders") + } + + if err := s.adjustmentOrderBook.GracefulCancel(ctx, s.Session.Exchange); err != nil { + logErr(err, "unable to cancel adjustment orders") + } + }) + + return nil +} + +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 + } + + 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 + + posSize := s.Position.Base.Abs() + tickSize := s.Market.TickSize + + if s.Position.IsShort() { + 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) + + 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(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) { + ticker, err := s.Session.Exchange.QueryTicker(ctx, s.Symbol) + if logErr(err, "unable to query ticker") { + return + } + + if s.IsHalted(ticker.Time) { + log.Warn("circuitBreakRiskControl: trading halted") + return + } + + err = s.liquidityOrderBook.GracefulCancel(ctx, s.Session.Exchange) + if logErr(err, "unable to cancel orders") { + return + } + + if ticker.Buy.IsZero() && ticker.Sell.IsZero() { + ticker.Sell = ticker.Last.Add(s.Market.TickSize) + ticker.Buy = ticker.Last.Sub(s.Market.TickSize) + } else if ticker.Buy.IsZero() { + ticker.Buy = ticker.Sell.Sub(s.Market.TickSize) + } else if ticker.Sell.IsZero() { + 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) + + 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()) + + 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(), + 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()) + + 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() / askSum + bidX := availableQuote.Float64() / (bidSum * (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("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...) + if logErr(err, "unable to place liquidity orders") { + return + } + + s.liquidityOrderBook.Add(createdOrders...) + log.Infof("%d liq orders are placed successfully", len(liqOrders)) +} + +func profitProtectedPrice( + side types.SideType, averageCost, price, feeRate, minProfit fixedpoint.Value, +) fixedpoint.Value { + switch side { + case types.SideTypeSell: + 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) + + } + return price +} + +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 +} + +func preloadKLines( + inc *KLineStream, session *bbgo.ExchangeSession, symbol string, interval types.Interval, +) { + if store, ok := session.MarketDataStore(symbol); ok { + if kLinesData, ok := store.KLinesOfInterval(interval); ok { + for _, k := range *kLinesData { + inc.EmitUpdate(k) + } + } + } +} From 533907894e954c8c42a2b39486e93b596f025283 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 8 Nov 2023 15:37:12 +0800 Subject: [PATCH 4/6] liquiditymaker: implement order generator --- pkg/strategy/liquiditymaker/generator.go | 95 +++++++++++++++ pkg/strategy/liquiditymaker/generator_test.go | 114 ++++++++++++++++++ pkg/strategy/liquiditymaker/strategy.go | 3 + pkg/testing/testhelper/assert_priceside.go | 58 +++++++++ pkg/testing/testhelper/number.go | 18 +++ 5 files changed, 288 insertions(+) create mode 100644 pkg/strategy/liquiditymaker/generator.go create mode 100644 pkg/strategy/liquiditymaker/generator_test.go create mode 100644 pkg/testing/testhelper/assert_priceside.go create mode 100644 pkg/testing/testhelper/number.go diff --git a/pkg/strategy/liquiditymaker/generator.go b/pkg/strategy/liquiditymaker/generator.go new file mode 100644 index 000000000..2b19b3f48 --- /dev/null +++ b/pkg/strategy/liquiditymaker/generator.go @@ -0,0 +1,95 @@ +package liquiditymaker + +import ( + log "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// input: liquidityOrderGenerator( +// +// totalLiquidityAmount, +// startPrice, +// endPrice, +// numLayers, +// quantityScale) +// +// when side == sell +// +// priceAsk1 * scale(1) * f = amount1 +// priceAsk2 * scale(2) * f = amount2 +// priceAsk3 * scale(3) * f = amount3 +// +// totalLiquidityAmount = priceAsk1 * scale(1) * f + priceAsk2 * scale(2) * f + priceAsk3 * scale(3) * f + .... +// totalLiquidityAmount = f * (priceAsk1 * scale(1) + priceAsk2 * scale(2) + priceAsk3 * scale(3) + ....) +// +// when side == buy +// +// priceBid1 * scale(1) * f = amount1 +type LiquidityOrderGenerator struct { + Symbol string + Market types.Market + + logger log.FieldLogger +} + +func (g *LiquidityOrderGenerator) Generate( + side types.SideType, totalAmount, startPrice, endPrice fixedpoint.Value, numLayers int, scale bbgo.Scale, +) (orders []types.SubmitOrder) { + + if g.logger == nil { + logger := log.New() + logger.SetLevel(log.ErrorLevel) + g.logger = logger + } + + layerSpread := endPrice.Sub(startPrice).Div(fixedpoint.NewFromInt(int64(numLayers - 1))) + switch side { + case types.SideTypeSell: + if layerSpread.Compare(g.Market.TickSize) < 0 { + layerSpread = g.Market.TickSize + } + + case types.SideTypeBuy: + if layerSpread.Compare(g.Market.TickSize.Neg()) > 0 { + layerSpread = g.Market.TickSize.Neg() + } + } + + quantityBase := 0.0 + var layerPrices []fixedpoint.Value + var layerScales []float64 + for i := 0; i < numLayers; i++ { + fi := fixedpoint.NewFromInt(int64(i)) + layerPrice := g.Market.TruncatePrice(startPrice.Add(layerSpread.Mul(fi))) + layerPrices = append(layerPrices, layerPrice) + + layerScale := scale.Call(float64(i + 1)) + layerScales = append(layerScales, layerScale) + + quantityBase += layerPrice.Float64() * layerScale + } + + factor := totalAmount.Float64() / quantityBase + + g.logger.Infof("liquidity amount base: %f, factor: %f", quantityBase, factor) + + for i := 0; i < numLayers; i++ { + price := layerPrices[i] + s := layerScales[i] + + quantity := factor * s + orders = append(orders, types.SubmitOrder{ + Symbol: g.Symbol, + Price: price, + Type: types.OrderTypeLimitMaker, + Quantity: g.Market.TruncateQuantity(fixedpoint.NewFromFloat(quantity)), + Side: side, + Market: g.Market, + }) + } + + return orders +} diff --git a/pkg/strategy/liquiditymaker/generator_test.go b/pkg/strategy/liquiditymaker/generator_test.go new file mode 100644 index 000000000..d56700f6e --- /dev/null +++ b/pkg/strategy/liquiditymaker/generator_test.go @@ -0,0 +1,114 @@ +//go:build !dnum + +package liquiditymaker + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + . "github.com/c9s/bbgo/pkg/testing/testhelper" + "github.com/c9s/bbgo/pkg/types" +) + +func newTestMarket() types.Market { + return types.Market{ + BaseCurrency: "XML", + QuoteCurrency: "USDT", + TickSize: Number(0.0001), + StepSize: Number(0.01), + PricePrecision: 4, + VolumePrecision: 8, + MinNotional: Number(8.0), + MinQuantity: Number(40.0), + } +} + +func TestLiquidityOrderGenerator(t *testing.T) { + g := &LiquidityOrderGenerator{ + Symbol: "XMLUSDT", + Market: newTestMarket(), + } + + scale := &bbgo.ExponentialScale{ + Domain: [2]float64{1.0, 30.0}, + Range: [2]float64{1.0, 4.0}, + } + + err := scale.Solve() + assert.NoError(t, err) + 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) + + t.Run("ask orders", func(t *testing.T) { + orders := g.Generate(types.SideTypeSell, totalAmount, Number(2.0), Number(2.04), 30, scale) + assert.Len(t, orders, 30) + + totalQuoteQuantity := fixedpoint.NewFromInt(0) + for _, o := range orders { + totalQuoteQuantity = totalQuoteQuantity.Add(o.Quantity.Mul(o.Price)) + } + 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")}, + }, 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")}, + }, orders[28:30]) + }) + + t.Run("bid orders", func(t *testing.T) { + orders := g.Generate(types.SideTypeBuy, totalAmount, Number(2.0), Number(1.96), 30, scale) + assert.Len(t, orders, 30) + + totalQuoteQuantity := fixedpoint.NewFromInt(0) + for _, o := range orders { + totalQuoteQuantity = totalQuoteQuantity.Add(o.Quantity.Mul(o.Price)) + } + 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")}, + }, 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")}, + }, orders[28:30]) + }) +} diff --git a/pkg/strategy/liquiditymaker/strategy.go b/pkg/strategy/liquiditymaker/strategy.go index fcd8f17de..6dcb8ece0 100644 --- a/pkg/strategy/liquiditymaker/strategy.go +++ b/pkg/strategy/liquiditymaker/strategy.go @@ -50,6 +50,9 @@ type Strategy struct { LiquiditySkew fixedpoint.Value `json:"liquiditySkew"` LiquidityPriceRange fixedpoint.Value `json:"liquidityPriceRange"` + 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"` diff --git a/pkg/testing/testhelper/assert_priceside.go b/pkg/testing/testhelper/assert_priceside.go new file mode 100644 index 000000000..7c45cdd9d --- /dev/null +++ b/pkg/testing/testhelper/assert_priceside.go @@ -0,0 +1,58 @@ +package testhelper + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type PriceSideAssert struct { + Price fixedpoint.Value + Side types.SideType +} + +// AssertOrdersPriceSide asserts the orders with the given price and side (slice) +func AssertOrdersPriceSide(t *testing.T, asserts []PriceSideAssert, orders []types.SubmitOrder) { + for i, a := range asserts { + assert.Equalf(t, a.Price, orders[i].Price, "order #%d price should be %f", i+1, a.Price.Float64()) + assert.Equalf(t, a.Side, orders[i].Side, "order at price %f should be %s", a.Price.Float64(), a.Side) + } +} + +type PriceSideQuantityAssert struct { + Price fixedpoint.Value + Side types.SideType + Quantity fixedpoint.Value +} + +// AssertOrdersPriceSide asserts the orders with the given price and side (slice) +func AssertOrdersPriceSideQuantity( + t *testing.T, asserts []PriceSideQuantityAssert, orders []types.SubmitOrder, +) { + assert.Equalf(t, len(orders), len(asserts), "expecting %d orders", len(asserts)) + + var assertPrices, orderPrices fixedpoint.Slice + var assertPricesFloat, orderPricesFloat []float64 + for _, a := range asserts { + assertPrices = append(assertPrices, a.Price) + assertPricesFloat = append(assertPricesFloat, a.Price.Float64()) + } + + for _, o := range orders { + orderPrices = append(orderPrices, o.Price) + orderPricesFloat = append(orderPricesFloat, o.Price.Float64()) + } + + if !assert.Equalf(t, assertPricesFloat, orderPricesFloat, "assert prices") { + return + } + + for i, a := range asserts { + assert.Equalf(t, a.Price.Float64(), orders[i].Price.Float64(), "order #%d price should be %f", i+1, a.Price.Float64()) + assert.Equalf(t, a.Quantity.Float64(), orders[i].Quantity.Float64(), "order #%d quantity should be %f", i+1, a.Quantity.Float64()) + assert.Equalf(t, a.Side, orders[i].Side, "order at price %f should be %s", a.Price.Float64(), a.Side) + } +} diff --git a/pkg/testing/testhelper/number.go b/pkg/testing/testhelper/number.go new file mode 100644 index 000000000..e57659a01 --- /dev/null +++ b/pkg/testing/testhelper/number.go @@ -0,0 +1,18 @@ +package testhelper + +import "github.com/c9s/bbgo/pkg/fixedpoint" + +func Number(a interface{}) fixedpoint.Value { + switch v := a.(type) { + case string: + return fixedpoint.MustNewFromString(v) + case int: + return fixedpoint.NewFromInt(int64(v)) + case int64: + return fixedpoint.NewFromInt(int64(v)) + case float64: + return fixedpoint.NewFromFloat(v) + } + + return fixedpoint.Zero +} From cc5c033af76c2dbd341140b8328c88c3f95bed6c Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 8 Nov 2023 17:54:01 +0800 Subject: [PATCH 5/6] liquiditymaker: use order generator --- config/liquiditymaker.yaml | 54 +++++ pkg/cmd/strategy/builtin.go | 1 + pkg/strategy/liquiditymaker/generator_test.go | 70 +++---- pkg/strategy/liquiditymaker/strategy.go | 185 +++++------------- 4 files changed, 142 insertions(+), 168 deletions(-) create mode 100644 config/liquiditymaker.yaml 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( From 3563c0b98601066aeefb0300ff02d6955ef84805 Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 8 Nov 2023 20:19:05 +0800 Subject: [PATCH 6/6] liquiditymaker: filterAskOrders by base balance --- pkg/strategy/liquiditymaker/generator.go | 1 + pkg/strategy/liquiditymaker/strategy.go | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/pkg/strategy/liquiditymaker/generator.go b/pkg/strategy/liquiditymaker/generator.go index 2b19b3f48..d21d79b83 100644 --- a/pkg/strategy/liquiditymaker/generator.go +++ b/pkg/strategy/liquiditymaker/generator.go @@ -24,6 +24,7 @@ import ( // // totalLiquidityAmount = priceAsk1 * scale(1) * f + priceAsk2 * scale(2) * f + priceAsk3 * scale(3) * f + .... // totalLiquidityAmount = f * (priceAsk1 * scale(1) + priceAsk2 * scale(2) + priceAsk3 * scale(3) + ....) +// f = totalLiquidityAmount / (priceAsk1 * scale(1) + priceAsk2 * scale(2) + priceAsk3 * scale(3) + ....) // // when side == buy // diff --git a/pkg/strategy/liquiditymaker/strategy.go b/pkg/strategy/liquiditymaker/strategy.go index 07a6517e7..9d90e8fed 100644 --- a/pkg/strategy/liquiditymaker/strategy.go +++ b/pkg/strategy/liquiditymaker/strategy.go @@ -299,6 +299,8 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { s.NumOfLiquidityLayers, s.liquidityScale) + askOrders = filterAskOrders(askOrders, baseBal.Available) + orderForms := append(bidOrders, askOrders...) createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, orderForms...) @@ -331,6 +333,20 @@ func profitProtectedPrice( return price } +func filterAskOrders(askOrders []types.SubmitOrder, available fixedpoint.Value) (out []types.SubmitOrder) { + usedBase := fixedpoint.Zero + for _, askOrder := range askOrders { + if usedBase.Add(askOrder.Quantity).Compare(available) > 0 { + return out + } + + usedBase = usedBase.Add(askOrder.Quantity) + out = append(out, askOrder) + } + + return out +} + func logErr(err error, msgAndArgs ...interface{}) bool { if err == nil { return false