mirror of
https://github.com/c9s/bbgo.git
synced 2024-09-20 08:11:08 +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"`
|
||||
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"`
|
||||
|
|
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