mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 06:53:52 +00:00
Merge pull request #1795 from c9s/c9s/liqmaker/metrics-and-tests
IMPROVE: add test helper for price side quantity assertion
This commit is contained in:
commit
7f905a2956
21
pkg/dbg/orders.go
Normal file
21
pkg/dbg/orders.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package dbg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
types2 "github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DebugSubmitOrders(logger logrus.FieldLogger, submitOrders []types2.SubmitOrder) {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("SubmitOrders[\n")
|
||||||
|
for i, order := range submitOrders {
|
||||||
|
sb.WriteString(fmt.Sprintf("%3d) ", i+1) + order.String() + "\n")
|
||||||
|
}
|
||||||
|
sb.WriteString("] End of SubmitOrders")
|
||||||
|
|
||||||
|
logger.Info(sb.String())
|
||||||
|
}
|
|
@ -6,10 +6,11 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/bbgo"
|
"github.com/c9s/bbgo/pkg/bbgo"
|
||||||
"github.com/c9s/bbgo/pkg/exchange/retry"
|
"github.com/c9s/bbgo/pkg/exchange/retry"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var recoverSinceLimit = time.Date(2024, time.January, 29, 12, 0, 0, 0, time.Local)
|
var recoverSinceLimit = time.Date(2024, time.January, 29, 12, 0, 0, 0, time.Local)
|
||||||
|
@ -65,7 +66,7 @@ func recoverState(ctx context.Context, maxOrderCount int, currentRound Round, or
|
||||||
|
|
||||||
// dca stop at take-profit order stage
|
// dca stop at take-profit order stage
|
||||||
if len(currentRound.TakeProfitOrders) > 0 {
|
if len(currentRound.TakeProfitOrders) > 0 {
|
||||||
openedOrders, cancelledOrders, filledOrders, unexpectedOrders := classifyOrders(currentRound.TakeProfitOrders)
|
openedOrders, cancelledOrders, filledOrders, unexpectedOrders := types.ClassifyOrdersByStatus(currentRound.TakeProfitOrders)
|
||||||
|
|
||||||
if len(unexpectedOrders) > 0 {
|
if len(unexpectedOrders) > 0 {
|
||||||
return None, fmt.Errorf("there is unexpected status in orders %+v", unexpectedOrders)
|
return None, fmt.Errorf("there is unexpected status in orders %+v", unexpectedOrders)
|
||||||
|
@ -96,7 +97,7 @@ func recoverState(ctx context.Context, maxOrderCount int, currentRound Round, or
|
||||||
}
|
}
|
||||||
|
|
||||||
// collect open-position orders' status
|
// collect open-position orders' status
|
||||||
openedOrders, cancelledOrders, filledOrders, unexpectedOrders := classifyOrders(currentRound.OpenPositionOrders)
|
openedOrders, cancelledOrders, filledOrders, unexpectedOrders := types.ClassifyOrdersByStatus(currentRound.OpenPositionOrders)
|
||||||
if len(unexpectedOrders) > 0 {
|
if len(unexpectedOrders) > 0 {
|
||||||
return None, fmt.Errorf("there is unexpected status of orders %+v", unexpectedOrders)
|
return None, fmt.Errorf("there is unexpected status of orders %+v", unexpectedOrders)
|
||||||
}
|
}
|
||||||
|
@ -124,7 +125,9 @@ func recoverState(ctx context.Context, maxOrderCount int, currentRound Round, or
|
||||||
return OpenPositionOrdersCancelling, nil
|
return OpenPositionOrdersCancelling, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func recoverPosition(ctx context.Context, position *types.Position, currentRound Round, queryService types.ExchangeOrderQueryService) error {
|
func recoverPosition(
|
||||||
|
ctx context.Context, position *types.Position, currentRound Round, queryService types.ExchangeOrderQueryService,
|
||||||
|
) error {
|
||||||
if position == nil {
|
if position == nil {
|
||||||
return fmt.Errorf("position is nil, please check it")
|
return fmt.Errorf("position is nil, please check it")
|
||||||
}
|
}
|
||||||
|
@ -191,20 +194,3 @@ func recoverStartTimeOfNextRound(ctx context.Context, currentRound Round, coolDo
|
||||||
|
|
||||||
return startTimeOfNextRound
|
return startTimeOfNextRound
|
||||||
}
|
}
|
||||||
|
|
||||||
func classifyOrders(orders []types.Order) (opened, cancelled, filled, unexpected []types.Order) {
|
|
||||||
for _, order := range orders {
|
|
||||||
switch order.Status {
|
|
||||||
case types.OrderStatusNew, types.OrderStatusPartiallyFilled:
|
|
||||||
opened = append(opened, order)
|
|
||||||
case types.OrderStatusFilled:
|
|
||||||
filled = append(filled, order)
|
|
||||||
case types.OrderStatusCanceled:
|
|
||||||
cancelled = append(cancelled, order)
|
|
||||||
default:
|
|
||||||
unexpected = append(unexpected, order)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return opened, cancelled, filled, unexpected
|
|
||||||
}
|
|
||||||
|
|
|
@ -165,7 +165,7 @@ func Test_classifyOrders(t *testing.T) {
|
||||||
types.Order{Status: types.OrderStatusCanceled},
|
types.Order{Status: types.OrderStatusCanceled},
|
||||||
}
|
}
|
||||||
|
|
||||||
opened, cancelled, filled, unexpected := classifyOrders(orders)
|
opened, cancelled, filled, unexpected := types.ClassifyOrdersByStatus(orders)
|
||||||
assert.Equal(t, 3, len(opened))
|
assert.Equal(t, 3, len(opened))
|
||||||
assert.Equal(t, 4, len(cancelled))
|
assert.Equal(t, 4, len(cancelled))
|
||||||
assert.Equal(t, 2, len(filled))
|
assert.Equal(t, 2, len(filled))
|
||||||
|
|
|
@ -46,6 +46,14 @@ func (g *LiquidityOrderGenerator) Generate(
|
||||||
g.logger = logger
|
g.logger = logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g.logger.Infof("generating %s orders with total amount %s from price %s to price %s with %d layers",
|
||||||
|
side,
|
||||||
|
totalAmount.String(),
|
||||||
|
startPrice.String(),
|
||||||
|
endPrice.String(),
|
||||||
|
numLayers,
|
||||||
|
)
|
||||||
|
|
||||||
layerSpread := endPrice.Sub(startPrice).Div(fixedpoint.NewFromInt(int64(numLayers - 1)))
|
layerSpread := endPrice.Sub(startPrice).Div(fixedpoint.NewFromInt(int64(numLayers - 1)))
|
||||||
switch side {
|
switch side {
|
||||||
case types.SideTypeSell:
|
case types.SideTypeSell:
|
||||||
|
@ -59,6 +67,8 @@ func (g *LiquidityOrderGenerator) Generate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g.logger.Infof("side %s layer spread: %s", side, layerSpread.String())
|
||||||
|
|
||||||
quantityBase := 0.0
|
quantityBase := 0.0
|
||||||
var layerPrices []fixedpoint.Value
|
var layerPrices []fixedpoint.Value
|
||||||
var layerScales []float64
|
var layerScales []float64
|
||||||
|
|
|
@ -111,4 +111,48 @@ func TestLiquidityOrderGenerator(t *testing.T) {
|
||||||
{Side: types.SideTypeBuy, Price: Number("1.9600"), Quantity: Number("620.54")},
|
{Side: types.SideTypeBuy, Price: Number("1.9600"), Quantity: Number("620.54")},
|
||||||
}, orders[28:30])
|
}, orders[28:30])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("bid orders 2", func(t *testing.T) {
|
||||||
|
orders := g.Generate(types.SideTypeBuy, Number(1000.0), Number(0.29), Number(0.20), 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, 1000.0, totalQuoteQuantity.Float64(), 1.0)
|
||||||
|
|
||||||
|
AssertOrdersPriceSideQuantityFromText(t, `
|
||||||
|
BUY,0.2899,65.41
|
||||||
|
BUY,0.2868,68.61
|
||||||
|
BUY,0.2837,71.97
|
||||||
|
BUY,0.2806,75.5
|
||||||
|
BUY,0.2775,79.2
|
||||||
|
BUY,0.2744,83.07
|
||||||
|
BUY,0.2713,87.14
|
||||||
|
BUY,0.2682,91.41
|
||||||
|
BUY,0.2651,95.88
|
||||||
|
BUY,0.262,100.58
|
||||||
|
BUY,0.2589,105.5
|
||||||
|
BUY,0.2558,110.67
|
||||||
|
BUY,0.2527,116.09
|
||||||
|
BUY,0.2496,121.77
|
||||||
|
BUY,0.2465,127.74
|
||||||
|
BUY,0.2434,133.99
|
||||||
|
BUY,0.2403,140.55
|
||||||
|
BUY,0.2372,147.44
|
||||||
|
BUY,0.2341,154.65
|
||||||
|
BUY,0.231,162.23
|
||||||
|
BUY,0.2279,170.17
|
||||||
|
BUY,0.2248,178.5
|
||||||
|
BUY,0.2217,187.24
|
||||||
|
BUY,0.2186,196.41
|
||||||
|
BUY,0.2155,206.03
|
||||||
|
BUY,0.2124,216.12
|
||||||
|
BUY,0.2093,226.7
|
||||||
|
BUY,0.2062,237.8
|
||||||
|
BUY,0.2031,249.44
|
||||||
|
BUY,0.2,261.66
|
||||||
|
`, orders)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
22
pkg/strategy/liquiditymaker/metrics.go
Normal file
22
pkg/strategy/liquiditymaker/metrics.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package liquiditymaker
|
||||||
|
|
||||||
|
import "github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
|
var openOrderBidExposureInUsdMetrics = prometheus.NewGaugeVec(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "liqmaker_open_order_bid_exposure_in_usd",
|
||||||
|
Help: "",
|
||||||
|
}, []string{"strategy_type", "strategy_id", "exchange", "symbol"})
|
||||||
|
|
||||||
|
var openOrderAskExposureInUsdMetrics = prometheus.NewGaugeVec(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "liqmaker_open_order_ask_exposure_in_usd",
|
||||||
|
Help: "",
|
||||||
|
}, []string{"strategy_type", "strategy_id", "exchange", "symbol"})
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
prometheus.MustRegister(
|
||||||
|
openOrderBidExposureInUsdMetrics,
|
||||||
|
openOrderAskExposureInUsdMetrics,
|
||||||
|
)
|
||||||
|
}
|
|
@ -5,9 +5,11 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
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/dbg"
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2"
|
indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2"
|
||||||
"github.com/c9s/bbgo/pkg/strategy/common"
|
"github.com/c9s/bbgo/pkg/strategy/common"
|
||||||
|
@ -79,7 +81,8 @@ type Strategy struct {
|
||||||
|
|
||||||
orderGenerator *LiquidityOrderGenerator
|
orderGenerator *LiquidityOrderGenerator
|
||||||
|
|
||||||
logger log.FieldLogger
|
logger log.FieldLogger
|
||||||
|
metricsLabels prometheus.Labels
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) Initialize() error {
|
func (s *Strategy) Initialize() error {
|
||||||
|
@ -87,9 +90,19 @@ func (s *Strategy) Initialize() error {
|
||||||
s.Strategy = &common.Strategy{}
|
s.Strategy = &common.Strategy{}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger = log.WithField("strategy", ID).WithFields(log.Fields{
|
s.logger = log.WithFields(log.Fields{
|
||||||
"symbol": s.Symbol,
|
"symbol": s.Symbol,
|
||||||
|
"strategy": ID,
|
||||||
|
"strategy_id": s.InstanceID(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
s.metricsLabels = prometheus.Labels{
|
||||||
|
"strategy_type": ID,
|
||||||
|
"strategy_id": s.InstanceID(),
|
||||||
|
"exchange": string(s.Session.Exchange.Name()),
|
||||||
|
"symbol": s.Symbol,
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -406,6 +419,8 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var bidExposureInUsd = fixedpoint.Zero
|
||||||
|
var askExposureInUsd = fixedpoint.Zero
|
||||||
var orderForms []types.SubmitOrder
|
var orderForms []types.SubmitOrder
|
||||||
if placeBid {
|
if placeBid {
|
||||||
bidOrders := s.orderGenerator.Generate(types.SideTypeBuy,
|
bidOrders := s.orderGenerator.Generate(types.SideTypeBuy,
|
||||||
|
@ -415,6 +430,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
|
||||||
s.NumOfLiquidityLayers,
|
s.NumOfLiquidityLayers,
|
||||||
s.liquidityScale)
|
s.liquidityScale)
|
||||||
|
|
||||||
|
bidExposureInUsd = sumOrderQuoteQuantity(bidOrders)
|
||||||
orderForms = append(orderForms, bidOrders...)
|
orderForms = append(orderForms, bidOrders...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -427,9 +443,12 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
|
||||||
s.liquidityScale)
|
s.liquidityScale)
|
||||||
|
|
||||||
askOrders = filterAskOrders(askOrders, baseBal.Available)
|
askOrders = filterAskOrders(askOrders, baseBal.Available)
|
||||||
|
askExposureInUsd = sumOrderQuoteQuantity(askOrders)
|
||||||
orderForms = append(orderForms, askOrders...)
|
orderForms = append(orderForms, askOrders...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbg.DebugSubmitOrders(s.logger, orderForms)
|
||||||
|
|
||||||
createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, orderForms...)
|
createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, orderForms...)
|
||||||
if util.LogErr(err, "unable to place liquidity orders") {
|
if util.LogErr(err, "unable to place liquidity orders") {
|
||||||
return
|
return
|
||||||
|
@ -437,6 +456,9 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
|
||||||
|
|
||||||
s.liquidityOrderBook.Add(createdOrders...)
|
s.liquidityOrderBook.Add(createdOrders...)
|
||||||
|
|
||||||
|
openOrderBidExposureInUsdMetrics.With(s.metricsLabels).Set(bidExposureInUsd.Float64())
|
||||||
|
openOrderAskExposureInUsdMetrics.With(s.metricsLabels).Set(askExposureInUsd.Float64())
|
||||||
|
|
||||||
s.logger.Infof("%d liq orders are placed successfully", len(orderForms))
|
s.logger.Infof("%d liq orders are placed successfully", len(orderForms))
|
||||||
for _, o := range createdOrders {
|
for _, o := range createdOrders {
|
||||||
s.logger.Infof("liq order: %+v", o)
|
s.logger.Infof("liq order: %+v", o)
|
||||||
|
@ -461,6 +483,14 @@ func profitProtectedPrice(
|
||||||
return price
|
return price
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sumOrderQuoteQuantity(orders []types.SubmitOrder) fixedpoint.Value {
|
||||||
|
sum := fixedpoint.Zero
|
||||||
|
for _, order := range orders {
|
||||||
|
sum = sum.Add(order.Price.Mul(order.Quantity))
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
func filterAskOrders(askOrders []types.SubmitOrder, available fixedpoint.Value) (out []types.SubmitOrder) {
|
func filterAskOrders(askOrders []types.SubmitOrder, available fixedpoint.Value) (out []types.SubmitOrder) {
|
||||||
usedBase := fixedpoint.Zero
|
usedBase := fixedpoint.Zero
|
||||||
for _, askOrder := range askOrders {
|
for _, askOrder := range askOrders {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package testhelper
|
package testhelper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -28,6 +30,55 @@ type PriceSideQuantityAssert struct {
|
||||||
Quantity fixedpoint.Value
|
Quantity fixedpoint.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParsePriceSideQuantityAssertions(text string) []PriceSideQuantityAssert {
|
||||||
|
var asserts []PriceSideQuantityAssert
|
||||||
|
lines := strings.Split(text, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cols := strings.SplitN(line, ",", 3)
|
||||||
|
if len(cols) < 3 {
|
||||||
|
panic(fmt.Errorf("column length should be 3, got %d", len(cols)))
|
||||||
|
}
|
||||||
|
|
||||||
|
side := strings.TrimSpace(cols[0])
|
||||||
|
price := fixedpoint.MustNewFromString(strings.TrimSpace(cols[1]))
|
||||||
|
quantity := fixedpoint.MustNewFromString(strings.TrimSpace(cols[2]))
|
||||||
|
asserts = append(asserts, PriceSideQuantityAssert{
|
||||||
|
Price: price,
|
||||||
|
Side: types.SideType(side),
|
||||||
|
Quantity: quantity,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return asserts
|
||||||
|
}
|
||||||
|
|
||||||
|
func AssertOrdersPriceSideQuantityFromText(
|
||||||
|
t *testing.T, text string, orders []types.SubmitOrder,
|
||||||
|
) {
|
||||||
|
asserts := ParsePriceSideQuantityAssertions(text)
|
||||||
|
assert.Equalf(t, len(asserts), len(orders), "expecting %d orders", len(asserts))
|
||||||
|
for i, a := range asserts {
|
||||||
|
order := orders[i]
|
||||||
|
assert.Equalf(t, a.Price.Float64(), order.Price.Float64(), "order #%d price should be %f", i+1, a.Price.Float64())
|
||||||
|
assert.Equalf(t, a.Quantity.Float64(), order.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)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Failed() {
|
||||||
|
actualInText := "Actual Orders:\n"
|
||||||
|
for _, order := range orders {
|
||||||
|
actualInText += fmt.Sprintf("%s,%s,%s\n", order.Side, order.Price.String(), order.Quantity.String())
|
||||||
|
}
|
||||||
|
t.Log(actualInText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// AssertOrdersPriceSide asserts the orders with the given price and side (slice)
|
// AssertOrdersPriceSide asserts the orders with the given price and side (slice)
|
||||||
func AssertOrdersPriceSideQuantity(
|
func AssertOrdersPriceSideQuantity(
|
||||||
t *testing.T, asserts []PriceSideQuantityAssert, orders []types.SubmitOrder,
|
t *testing.T, asserts []PriceSideQuantityAssert, orders []types.SubmitOrder,
|
||||||
|
|
18
pkg/types/orders.go
Normal file
18
pkg/types/orders.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
func ClassifyOrdersByStatus(orders []Order) (opened, cancelled, filled, unexpected []Order) {
|
||||||
|
for _, order := range orders {
|
||||||
|
switch order.Status {
|
||||||
|
case OrderStatusNew, OrderStatusPartiallyFilled:
|
||||||
|
opened = append(opened, order)
|
||||||
|
case OrderStatusFilled:
|
||||||
|
filled = append(filled, order)
|
||||||
|
case OrderStatusCanceled:
|
||||||
|
cancelled = append(cancelled, order)
|
||||||
|
default:
|
||||||
|
unexpected = append(unexpected, order)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return opened, cancelled, filled, unexpected
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user