liquiditymaker: use order generator

This commit is contained in:
c9s 2023-11-08 17:54:01 +08:00
parent 533907894e
commit cc5c033af7
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
4 changed files with 142 additions and 168 deletions

View File

@ -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

View File

@ -25,6 +25,7 @@ import (
_ "github.com/c9s/bbgo/pkg/strategy/irr" _ "github.com/c9s/bbgo/pkg/strategy/irr"
_ "github.com/c9s/bbgo/pkg/strategy/kline" _ "github.com/c9s/bbgo/pkg/strategy/kline"
_ "github.com/c9s/bbgo/pkg/strategy/linregmaker" _ "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/marketcap"
_ "github.com/c9s/bbgo/pkg/strategy/pivotshort" _ "github.com/c9s/bbgo/pkg/strategy/pivotshort"
_ "github.com/c9s/bbgo/pkg/strategy/pricealert" _ "github.com/c9s/bbgo/pkg/strategy/pricealert"

View File

@ -42,7 +42,7 @@ func TestLiquidityOrderGenerator(t *testing.T) {
assert.InDelta(t, 1.0, scale.Call(1.0), 0.00001) assert.InDelta(t, 1.0, scale.Call(1.0), 0.00001)
assert.InDelta(t, 4.0, scale.Call(30.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) { t.Run("ask orders", func(t *testing.T) {
orders := g.Generate(types.SideTypeSell, totalAmount, Number(2.0), Number(2.04), 30, scale) 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) assert.InDelta(t, totalAmount.Float64(), totalQuoteQuantity.Float64(), 1.0)
AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{ AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{
{Side: types.SideTypeSell, Price: Number("2.0000"), Quantity: Number("1513.40")}, {Side: types.SideTypeSell, Price: Number("2.0000"), Quantity: Number("151.34")},
{Side: types.SideTypeSell, Price: Number("2.0013"), Quantity: Number("1587.50")}, {Side: types.SideTypeSell, Price: Number("2.0013"), Quantity: Number("158.75")},
{Side: types.SideTypeSell, Price: Number("2.0027"), Quantity: Number("1665.23")}, {Side: types.SideTypeSell, Price: Number("2.0027"), Quantity: Number("166.52")},
{Side: types.SideTypeSell, Price: Number("2.0041"), Quantity: Number("1746.77")}, {Side: types.SideTypeSell, Price: Number("2.0041"), Quantity: Number("174.67")},
{Side: types.SideTypeSell, Price: Number("2.0055"), Quantity: Number("1832.30")}, {Side: types.SideTypeSell, Price: Number("2.0055"), Quantity: Number("183.23")},
{Side: types.SideTypeSell, Price: Number("2.0068"), Quantity: Number("1922.02")}, {Side: types.SideTypeSell, Price: Number("2.0068"), Quantity: Number("192.20")},
{Side: types.SideTypeSell, Price: Number("2.0082"), Quantity: Number("2016.13")}, {Side: types.SideTypeSell, Price: Number("2.0082"), Quantity: Number("201.61")},
{Side: types.SideTypeSell, Price: Number("2.0096"), Quantity: Number("2114.85")}, {Side: types.SideTypeSell, Price: Number("2.0096"), Quantity: Number("211.48")},
{Side: types.SideTypeSell, Price: Number("2.0110"), Quantity: Number("2218.40")}, {Side: types.SideTypeSell, Price: Number("2.0110"), Quantity: Number("221.84")},
{Side: types.SideTypeSell, Price: Number("2.0124"), Quantity: Number("2327.02")}, {Side: types.SideTypeSell, Price: Number("2.0124"), Quantity: Number("232.70")},
{Side: types.SideTypeSell, Price: Number("2.0137"), Quantity: Number("2440.96")}, {Side: types.SideTypeSell, Price: Number("2.0137"), Quantity: Number("244.09")},
{Side: types.SideTypeSell, Price: Number("2.0151"), Quantity: Number("2560.48")}, {Side: types.SideTypeSell, Price: Number("2.0151"), Quantity: Number("256.04")},
{Side: types.SideTypeSell, Price: Number("2.0165"), Quantity: Number("2685.86")}, {Side: types.SideTypeSell, Price: Number("2.0165"), Quantity: Number("268.58")},
{Side: types.SideTypeSell, Price: Number("2.0179"), Quantity: Number("2817.37")}, {Side: types.SideTypeSell, Price: Number("2.0179"), Quantity: Number("281.73")},
{Side: types.SideTypeSell, Price: Number("2.0193"), Quantity: Number("2955.32")}, {Side: types.SideTypeSell, Price: Number("2.0193"), Quantity: Number("295.53")},
}, orders[0:15]) }, orders[0:15])
AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{ AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{
{Side: types.SideTypeSell, Price: Number("2.0386"), Quantity: Number("5771.04")}, {Side: types.SideTypeSell, Price: Number("2.0386"), Quantity: Number("577.10")},
{Side: types.SideTypeSell, Price: Number("2.0399"), Quantity: Number("6053.62")}, {Side: types.SideTypeSell, Price: Number("2.0399"), Quantity: Number("605.36")},
}, orders[28:30]) }, orders[28:30])
}) })
@ -89,26 +89,26 @@ func TestLiquidityOrderGenerator(t *testing.T) {
assert.InDelta(t, totalAmount.Float64(), totalQuoteQuantity.Float64(), 1.0) assert.InDelta(t, totalAmount.Float64(), totalQuoteQuantity.Float64(), 1.0)
AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{ AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{
{Side: types.SideTypeBuy, Price: Number("2.0000"), Quantity: Number("1551.37")}, {Side: types.SideTypeBuy, Price: Number("2.0000"), Quantity: Number("155.13")},
{Side: types.SideTypeBuy, Price: Number("1.9986"), Quantity: Number("1627.33")}, {Side: types.SideTypeBuy, Price: Number("1.9986"), Quantity: Number("162.73")},
{Side: types.SideTypeBuy, Price: Number("1.9972"), Quantity: Number("1707.01")}, {Side: types.SideTypeBuy, Price: Number("1.9972"), Quantity: Number("170.70")},
{Side: types.SideTypeBuy, Price: Number("1.9958"), Quantity: Number("1790.59")}, {Side: types.SideTypeBuy, Price: Number("1.9958"), Quantity: Number("179.05")},
{Side: types.SideTypeBuy, Price: Number("1.9944"), Quantity: Number("1878.27")}, {Side: types.SideTypeBuy, Price: Number("1.9944"), Quantity: Number("187.82")},
{Side: types.SideTypeBuy, Price: Number("1.9931"), Quantity: Number("1970.24")}, {Side: types.SideTypeBuy, Price: Number("1.9931"), Quantity: Number("197.02")},
{Side: types.SideTypeBuy, Price: Number("1.9917"), Quantity: Number("2066.71")}, {Side: types.SideTypeBuy, Price: Number("1.9917"), Quantity: Number("206.67")},
{Side: types.SideTypeBuy, Price: Number("1.9903"), Quantity: Number("2167.91")}, {Side: types.SideTypeBuy, Price: Number("1.9903"), Quantity: Number("216.79")},
{Side: types.SideTypeBuy, Price: Number("1.9889"), Quantity: Number("2274.06")}, {Side: types.SideTypeBuy, Price: Number("1.9889"), Quantity: Number("227.40")},
{Side: types.SideTypeBuy, Price: Number("1.9875"), Quantity: Number("2385.40")}, {Side: types.SideTypeBuy, Price: Number("1.9875"), Quantity: Number("238.54")},
{Side: types.SideTypeBuy, Price: Number("1.9862"), Quantity: Number("2502.20")}, {Side: types.SideTypeBuy, Price: Number("1.9862"), Quantity: Number("250.22")},
{Side: types.SideTypeBuy, Price: Number("1.9848"), Quantity: Number("2624.72")}, {Side: types.SideTypeBuy, Price: Number("1.9848"), Quantity: Number("262.47")},
{Side: types.SideTypeBuy, Price: Number("1.9834"), Quantity: Number("2753.24")}, {Side: types.SideTypeBuy, Price: Number("1.9834"), Quantity: Number("275.32")},
{Side: types.SideTypeBuy, Price: Number("1.9820"), Quantity: Number("2888.05")}, {Side: types.SideTypeBuy, Price: Number("1.9820"), Quantity: Number("288.80")},
{Side: types.SideTypeBuy, Price: Number("1.9806"), Quantity: Number("3029.46")}, {Side: types.SideTypeBuy, Price: Number("1.9806"), Quantity: Number("302.94")},
}, orders[0:15]) }, orders[0:15])
AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{ AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{
{Side: types.SideTypeBuy, Price: Number("1.9613"), Quantity: Number("5915.83")}, {Side: types.SideTypeBuy, Price: Number("1.9613"), Quantity: Number("591.58")},
{Side: types.SideTypeBuy, Price: Number("1.9600"), Quantity: Number("6205.49")}, {Side: types.SideTypeBuy, Price: Number("1.9600"), Quantity: Number("620.54")},
}, orders[28:30]) }, orders[28:30])
}) })
} }

View File

@ -3,7 +3,6 @@ package liquiditymaker
import ( import (
"context" "context"
"fmt" "fmt"
"math"
"sync" "sync"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -46,13 +45,11 @@ type Strategy struct {
NumOfLiquidityLayers int `json:"numOfLiquidityLayers"` NumOfLiquidityLayers int `json:"numOfLiquidityLayers"`
LiquiditySlideRule *bbgo.SlideRule `json:"liquidityScale"` LiquiditySlideRule *bbgo.SlideRule `json:"liquidityScale"`
LiquidityLayerTickSize fixedpoint.Value `json:"liquidityLayerTickSize"`
LiquiditySkew fixedpoint.Value `json:"liquiditySkew"`
LiquidityPriceRange fixedpoint.Value `json:"liquidityPriceRange"` LiquidityPriceRange fixedpoint.Value `json:"liquidityPriceRange"`
AskLiquidityAmount fixedpoint.Value `json:"askLiquidityAmount"` AskLiquidityAmount fixedpoint.Value `json:"askLiquidityAmount"`
BidLiquidityAmount fixedpoint.Value `json:"bidLiquidityAmount"` BidLiquidityAmount fixedpoint.Value `json:"bidLiquidityAmount"`
UseLastTradePrice bool `json:"useLastTradePrice"`
Spread fixedpoint.Value `json:"spread"` Spread fixedpoint.Value `json:"spread"`
MaxPrice fixedpoint.Value `json:"maxPrice"` MaxPrice fixedpoint.Value `json:"maxPrice"`
MinPrice fixedpoint.Value `json:"minPrice"` MinPrice fixedpoint.Value `json:"minPrice"`
@ -65,6 +62,8 @@ type Strategy struct {
book *types.StreamOrderBook book *types.StreamOrderBook
liquidityScale bbgo.Scale liquidityScale bbgo.Scale
orderGenerator *LiquidityOrderGenerator
} }
func (s *Strategy) ID() string { 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 = &common.Strategy{}
s.Strategy.Initialize(ctx, s.Environment, session, s.Market, ID, s.InstanceID()) 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 = types.NewStreamBook(s.Symbol)
s.book.BindStream(session.MarketDataStream) s.book.BindStream(session.MarketDataStream)
@ -209,6 +213,11 @@ func (s *Strategy) placeAdjustmentOrders(ctx context.Context) {
} }
func (s *Strategy) placeLiquidityOrders(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) ticker, err := s.Session.Exchange.QueryTicker(ctx, s.Symbol)
if logErr(err, "unable to query ticker") { if logErr(err, "unable to query ticker") {
return return
@ -219,11 +228,14 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
return return
} }
err = s.liquidityOrderBook.GracefulCancel(ctx, s.Session.Exchange) if _, err := s.Session.UpdateAccount(ctx); err != nil {
if logErr(err, "unable to cancel orders") { logErr(err, "unable to update account")
return return
} }
baseBal, _ := s.Session.Account.Balance(s.Market.BaseCurrency)
quoteBal, _ := s.Session.Account.Balance(s.Market.QuoteCurrency)
if ticker.Buy.IsZero() && ticker.Sell.IsZero() { if ticker.Buy.IsZero() && ticker.Sell.IsZero() {
ticker.Sell = ticker.Last.Add(s.Market.TickSize) ticker.Sell = ticker.Last.Add(s.Market.TickSize)
ticker.Buy = ticker.Last.Sub(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) ticker.Sell = ticker.Buy.Add(s.Market.TickSize)
} }
if _, err := s.Session.UpdateAccount(ctx); err != nil { log.Infof("ticker: %+v", ticker)
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 lastTradedPrice := ticker.Last
midPrice := ticker.Sell.Add(ticker.Buy).Div(fixedpoint.Two) midPrice := ticker.Sell.Add(ticker.Buy).Div(fixedpoint.Two)
currentSpread := ticker.Sell.Sub(ticker.Buy) currentSpread := ticker.Sell.Sub(ticker.Buy)
tickSize := fixedpoint.Max(s.LiquidityLayerTickSize, s.Market.TickSize)
sideSpread := s.Spread.Div(fixedpoint.Two) 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)) ask1Price := midPrice.Mul(fixedpoint.One.Add(sideSpread))
bid1Price := midPrice.Mul(fixedpoint.One.Sub(sideSpread)) bid1Price := midPrice.Mul(fixedpoint.One.Sub(sideSpread))
askLastPrice := midPrice.Mul(fixedpoint.One.Add(s.LiquidityPriceRange)) askLastPrice := midPrice.Mul(fixedpoint.One.Add(s.LiquidityPriceRange))
bidLastPrice := midPrice.Mul(fixedpoint.One.Sub(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(), ask1Price.Float64(), askLastPrice.Float64(),
bid1Price.Float64(), bidLastPrice.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 availableBase := baseBal.Available
availableQuote := quoteBal.Available availableQuote := quoteBal.Available
makerQuota := &bbgo.QuotaTransaction{}
makerQuota.QuoteAsset.Add(availableQuote)
makerQuota.BaseAsset.Add(availableBase)
log.Infof("balances before liq orders: %s, %s", log.Infof("balances before liq orders: %s, %s",
baseBal.String(), baseBal.String(),
quoteBal.String()) quoteBal.String())
@ -319,79 +285,32 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
} }
} }
askX := availableBase.Float64() / askSum bidOrders := s.orderGenerator.Generate(types.SideTypeBuy,
bidX := availableQuote.Float64() / (bidSum * (fixedpoint.Sum(bidPrices).Float64())) fixedpoint.Min(s.BidLiquidityAmount, quoteBal.Available),
bid1Price,
bidLastPrice,
s.NumOfLiquidityLayers,
s.liquidityScale)
askX = math.Trunc(askX*1e8) / 1e8 askOrders := s.orderGenerator.Generate(types.SideTypeSell,
bidX = math.Trunc(bidX*1e8) / 1e8 s.AskLiquidityAmount,
ask1Price,
askLastPrice,
s.NumOfLiquidityLayers,
s.liquidityScale)
var liqOrders []types.SubmitOrder orderForms := append(bidOrders, askOrders...)
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()) createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, orderForms...)
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") { if logErr(err, "unable to place liquidity orders") {
return return
} }
s.liquidityOrderBook.Add(createdOrders...) 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( func profitProtectedPrice(