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:
c9s 2024-10-28 16:16:05 +08:00 committed by GitHub
commit 7f905a2956
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 207 additions and 25 deletions

21
pkg/dbg/orders.go Normal file
View 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())
}

View File

@ -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
}

View File

@ -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))

View File

@ -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

View File

@ -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)
})
} }

View 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,
)
}

View File

@ -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 {

View File

@ -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
View 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
}