liquiditymaker: implement order generator

This commit is contained in:
c9s 2023-11-08 15:37:12 +08:00
parent dda2cfb73d
commit 533907894e
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
5 changed files with 288 additions and 0 deletions

View File

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

View File

@ -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])
})
}

View File

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

View File

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

View File

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