mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 08:45:16 +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/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"
|
||||||
|
|
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"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/bbgo"
|
"github.com/c9s/bbgo/pkg/bbgo"
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
. "github.com/c9s/bbgo/pkg/indicator/v2"
|
. "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/strategy/common"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ID = "scmaker"
|
const ID = "scmaker"
|
||||||
|
|
||||||
var ten = fixedpoint.NewFromInt(10)
|
|
||||||
|
|
||||||
type advancedOrderCancelApi interface {
|
type advancedOrderCancelApi interface {
|
||||||
CancelAllOrders(ctx context.Context) ([]types.Order, error)
|
CancelAllOrders(ctx context.Context) ([]types.Order, error)
|
||||||
CancelOrdersBySymbol(ctx context.Context, symbol string) ([]types.Order, error)
|
CancelOrdersBySymbol(ctx context.Context, symbol string) ([]types.Order, error)
|
||||||
|
@ -62,12 +58,6 @@ type Strategy struct {
|
||||||
|
|
||||||
MinProfit fixedpoint.Value `json:"minProfit"`
|
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
|
liquidityOrderBook, adjustmentOrderBook *bbgo.ActiveOrderBook
|
||||||
book *types.StreamOrderBook
|
book *types.StreamOrderBook
|
||||||
|
|
||||||
|
@ -77,9 +67,6 @@ type Strategy struct {
|
||||||
ewma *EWMAStream
|
ewma *EWMAStream
|
||||||
boll *BOLLStream
|
boll *BOLLStream
|
||||||
intensity *IntensityStream
|
intensity *IntensityStream
|
||||||
|
|
||||||
positionRiskControl *riskcontrol.PositionRiskControl
|
|
||||||
circuitBreakRiskControl *riskcontrol.CircuitBreakRiskControl
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) ID() string {
|
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 = &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.book = types.NewStreamBook(s.Symbol)
|
s.book = types.NewStreamBook(s.Symbol)
|
||||||
s.book.BindStream(session.UserDataStream)
|
s.book.BindStream(session.MarketDataStream)
|
||||||
|
|
||||||
s.liquidityOrderBook = bbgo.NewActiveOrderBook(s.Symbol)
|
s.liquidityOrderBook = bbgo.NewActiveOrderBook(s.Symbol)
|
||||||
s.liquidityOrderBook.BindStream(session.UserDataStream)
|
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 = bbgo.NewActiveOrderBook(s.Symbol)
|
||||||
s.adjustmentOrderBook.BindStream(session.UserDataStream)
|
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()
|
scale, err := s.LiquiditySlideRule.Scale()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -174,7 +146,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
return nil
|
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 store, ok := session.MarketDataStore(symbol); ok {
|
||||||
if kLinesData, ok := store.KLinesOfInterval(interval); ok {
|
if kLinesData, ok := store.KLinesOfInterval(interval); ok {
|
||||||
for _, k := range *kLinesData {
|
for _, k := range *kLinesData {
|
||||||
|
@ -282,7 +256,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.circuitBreakRiskControl != nil && s.circuitBreakRiskControl.IsHalted(ticker.Time) {
|
if s.IsHalted(ticker.Time) {
|
||||||
log.Warn("circuitBreakRiskControl: trading halted")
|
log.Warn("circuitBreakRiskControl: trading halted")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -476,7 +450,9 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
|
||||||
log.Infof("%d liq orders are placed successfully", len(liqOrders))
|
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 {
|
switch side {
|
||||||
case types.SideTypeSell:
|
case types.SideTypeSell:
|
||||||
minProfitPrice := averageCost.Add(
|
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