mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
liquiditymaker: implement order generator
This commit is contained in:
parent
dda2cfb73d
commit
533907894e
95
pkg/strategy/liquiditymaker/generator.go
Normal file
95
pkg/strategy/liquiditymaker/generator.go
Normal 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
|
||||||
|
}
|
114
pkg/strategy/liquiditymaker/generator_test.go
Normal file
114
pkg/strategy/liquiditymaker/generator_test.go
Normal 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])
|
||||||
|
})
|
||||||
|
}
|
|
@ -50,6 +50,9 @@ type Strategy struct {
|
||||||
LiquiditySkew fixedpoint.Value `json:"liquiditySkew"`
|
LiquiditySkew fixedpoint.Value `json:"liquiditySkew"`
|
||||||
LiquidityPriceRange fixedpoint.Value `json:"liquidityPriceRange"`
|
LiquidityPriceRange fixedpoint.Value `json:"liquidityPriceRange"`
|
||||||
|
|
||||||
|
AskLiquidityAmount fixedpoint.Value `json:"askLiquidityAmount"`
|
||||||
|
BidLiquidityAmount fixedpoint.Value `json:"bidLiquidityAmount"`
|
||||||
|
|
||||||
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"`
|
||||||
|
|
58
pkg/testing/testhelper/assert_priceside.go
Normal file
58
pkg/testing/testhelper/assert_priceside.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
18
pkg/testing/testhelper/number.go
Normal file
18
pkg/testing/testhelper/number.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user