mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 06:53:52 +00:00
Merge pull request #1401 from c9s/strategy/liquidity-maker
STRATEGY: add liquidity maker
This commit is contained in:
commit
4c701676a0
54
config/liquiditymaker.yaml
Normal file
54
config/liquiditymaker.yaml
Normal 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
|
|
@ -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"
|
||||
|
|
96
pkg/strategy/liquiditymaker/generator.go
Normal file
96
pkg/strategy/liquiditymaker/generator.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
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) + ....)
|
||||
// f = totalLiquidityAmount / (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(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)
|
||||
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("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("577.10")},
|
||||
{Side: types.SideTypeSell, Price: Number("2.0399"), Quantity: Number("605.36")},
|
||||
}, 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("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("591.58")},
|
||||
{Side: types.SideTypeBuy, Price: Number("1.9600"), Quantity: Number("620.54")},
|
||||
}, orders[28:30])
|
||||
})
|
||||
}
|
378
pkg/strategy/liquiditymaker/strategy.go
Normal file
378
pkg/strategy/liquiditymaker/strategy.go
Normal file
|
@ -0,0 +1,378 @@
|
|||
package liquiditymaker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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"`
|
||||
LiquidityPriceRange fixedpoint.Value `json:"liquidityPriceRange"`
|
||||
AskLiquidityAmount fixedpoint.Value `json:"askLiquidityAmount"`
|
||||
BidLiquidityAmount fixedpoint.Value `json:"bidLiquidityAmount"`
|
||||
|
||||
UseLastTradePrice bool `json:"useLastTradePrice"`
|
||||
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
|
||||
|
||||
orderGenerator *LiquidityOrderGenerator
|
||||
}
|
||||
|
||||
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.orderGenerator = &LiquidityOrderGenerator{
|
||||
Symbol: s.Symbol,
|
||||
Market: s.Market,
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
if s.IsHalted(ticker.Time) {
|
||||
log.Warn("circuitBreakRiskControl: trading halted")
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
log.Infof("ticker: %+v", ticker)
|
||||
|
||||
lastTradedPrice := ticker.Last
|
||||
midPrice := ticker.Sell.Add(ticker.Buy).Div(fixedpoint.Two)
|
||||
currentSpread := ticker.Sell.Sub(ticker.Buy)
|
||||
sideSpread := s.Spread.Div(fixedpoint.Two)
|
||||
|
||||
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(),
|
||||
ask1Price.Float64(), askLastPrice.Float64(),
|
||||
bid1Price.Float64(), bidLastPrice.Float64())
|
||||
|
||||
availableBase := baseBal.Available
|
||||
availableQuote := quoteBal.Available
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
bidOrders := s.orderGenerator.Generate(types.SideTypeBuy,
|
||||
fixedpoint.Min(s.BidLiquidityAmount, quoteBal.Available),
|
||||
bid1Price,
|
||||
bidLastPrice,
|
||||
s.NumOfLiquidityLayers,
|
||||
s.liquidityScale)
|
||||
|
||||
askOrders := s.orderGenerator.Generate(types.SideTypeSell,
|
||||
s.AskLiquidityAmount,
|
||||
ask1Price,
|
||||
askLastPrice,
|
||||
s.NumOfLiquidityLayers,
|
||||
s.liquidityScale)
|
||||
|
||||
askOrders = filterAskOrders(askOrders, baseBal.Available)
|
||||
|
||||
orderForms := append(bidOrders, askOrders...)
|
||||
|
||||
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(orderForms))
|
||||
for _, o := range createdOrders {
|
||||
log.Infof("liq order: %+v", o)
|
||||
}
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,22 +5,18 @@ 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"
|
||||
)
|
||||
|
||||
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)
|
||||
|
@ -62,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
|
||||
|
||||
|
@ -77,9 +67,6 @@ type Strategy struct {
|
|||
ewma *EWMAStream
|
||||
boll *BOLLStream
|
||||
intensity *IntensityStream
|
||||
|
||||
positionRiskControl *riskcontrol.PositionRiskControl
|
||||
circuitBreakRiskControl *riskcontrol.CircuitBreakRiskControl
|
||||
}
|
||||
|
||||
func (s *Strategy) ID() string {
|
||||
|
@ -100,12 +87,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)
|
||||
|
@ -113,21 +100,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
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
|
||||
|
@ -174,7 +146,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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -476,7 +450,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(
|
||||
|
|
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