Skip to content

Commit 7f905a2

Browse files
authored
Merge pull request #1795 from c9s/c9s/liqmaker/metrics-and-tests
IMPROVE: add test helper for price side quantity assertion
2 parents f4df9a0 + 74cc361 commit 7f905a2

File tree

9 files changed

+207
-25
lines changed

9 files changed

+207
-25
lines changed

pkg/dbg/orders.go

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package dbg
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/sirupsen/logrus"
8+
9+
types2 "github.com/c9s/bbgo/pkg/types"
10+
)
11+
12+
func DebugSubmitOrders(logger logrus.FieldLogger, submitOrders []types2.SubmitOrder) {
13+
var sb strings.Builder
14+
sb.WriteString("SubmitOrders[\n")
15+
for i, order := range submitOrders {
16+
sb.WriteString(fmt.Sprintf("%3d) ", i+1) + order.String() + "\n")
17+
}
18+
sb.WriteString("] End of SubmitOrders")
19+
20+
logger.Info(sb.String())
21+
}

pkg/strategy/dca2/recover.go

+7-21
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import (
66
"strconv"
77
"time"
88

9+
"github.com/pkg/errors"
10+
911
"github.com/c9s/bbgo/pkg/bbgo"
1012
"github.com/c9s/bbgo/pkg/exchange/retry"
1113
"github.com/c9s/bbgo/pkg/types"
12-
"github.com/pkg/errors"
1314
)
1415

1516
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
6566

6667
// dca stop at take-profit order stage
6768
if len(currentRound.TakeProfitOrders) > 0 {
68-
openedOrders, cancelledOrders, filledOrders, unexpectedOrders := classifyOrders(currentRound.TakeProfitOrders)
69+
openedOrders, cancelledOrders, filledOrders, unexpectedOrders := types.ClassifyOrdersByStatus(currentRound.TakeProfitOrders)
6970

7071
if len(unexpectedOrders) > 0 {
7172
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
9697
}
9798

9899
// collect open-position orders' status
99-
openedOrders, cancelledOrders, filledOrders, unexpectedOrders := classifyOrders(currentRound.OpenPositionOrders)
100+
openedOrders, cancelledOrders, filledOrders, unexpectedOrders := types.ClassifyOrdersByStatus(currentRound.OpenPositionOrders)
100101
if len(unexpectedOrders) > 0 {
101102
return None, fmt.Errorf("there is unexpected status of orders %+v", unexpectedOrders)
102103
}
@@ -124,7 +125,9 @@ func recoverState(ctx context.Context, maxOrderCount int, currentRound Round, or
124125
return OpenPositionOrdersCancelling, nil
125126
}
126127

127-
func recoverPosition(ctx context.Context, position *types.Position, currentRound Round, queryService types.ExchangeOrderQueryService) error {
128+
func recoverPosition(
129+
ctx context.Context, position *types.Position, currentRound Round, queryService types.ExchangeOrderQueryService,
130+
) error {
128131
if position == nil {
129132
return fmt.Errorf("position is nil, please check it")
130133
}
@@ -191,20 +194,3 @@ func recoverStartTimeOfNextRound(ctx context.Context, currentRound Round, coolDo
191194

192195
return startTimeOfNextRound
193196
}
194-
195-
func classifyOrders(orders []types.Order) (opened, cancelled, filled, unexpected []types.Order) {
196-
for _, order := range orders {
197-
switch order.Status {
198-
case types.OrderStatusNew, types.OrderStatusPartiallyFilled:
199-
opened = append(opened, order)
200-
case types.OrderStatusFilled:
201-
filled = append(filled, order)
202-
case types.OrderStatusCanceled:
203-
cancelled = append(cancelled, order)
204-
default:
205-
unexpected = append(unexpected, order)
206-
}
207-
}
208-
209-
return opened, cancelled, filled, unexpected
210-
}

pkg/strategy/dca2/recover_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ func Test_classifyOrders(t *testing.T) {
165165
types.Order{Status: types.OrderStatusCanceled},
166166
}
167167

168-
opened, cancelled, filled, unexpected := classifyOrders(orders)
168+
opened, cancelled, filled, unexpected := types.ClassifyOrdersByStatus(orders)
169169
assert.Equal(t, 3, len(opened))
170170
assert.Equal(t, 4, len(cancelled))
171171
assert.Equal(t, 2, len(filled))

pkg/strategy/liquiditymaker/generator.go

+10
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ func (g *LiquidityOrderGenerator) Generate(
4646
g.logger = logger
4747
}
4848

49+
g.logger.Infof("generating %s orders with total amount %s from price %s to price %s with %d layers",
50+
side,
51+
totalAmount.String(),
52+
startPrice.String(),
53+
endPrice.String(),
54+
numLayers,
55+
)
56+
4957
layerSpread := endPrice.Sub(startPrice).Div(fixedpoint.NewFromInt(int64(numLayers - 1)))
5058
switch side {
5159
case types.SideTypeSell:
@@ -59,6 +67,8 @@ func (g *LiquidityOrderGenerator) Generate(
5967
}
6068
}
6169

70+
g.logger.Infof("side %s layer spread: %s", side, layerSpread.String())
71+
6272
quantityBase := 0.0
6373
var layerPrices []fixedpoint.Value
6474
var layerScales []float64

pkg/strategy/liquiditymaker/generator_test.go

+44
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,48 @@ func TestLiquidityOrderGenerator(t *testing.T) {
111111
{Side: types.SideTypeBuy, Price: Number("1.9600"), Quantity: Number("620.54")},
112112
}, orders[28:30])
113113
})
114+
115+
t.Run("bid orders 2", func(t *testing.T) {
116+
orders := g.Generate(types.SideTypeBuy, Number(1000.0), Number(0.29), Number(0.20), 30, scale)
117+
assert.Len(t, orders, 30)
118+
119+
totalQuoteQuantity := fixedpoint.NewFromInt(0)
120+
for _, o := range orders {
121+
totalQuoteQuantity = totalQuoteQuantity.Add(o.Quantity.Mul(o.Price))
122+
}
123+
assert.InDelta(t, 1000.0, totalQuoteQuantity.Float64(), 1.0)
124+
125+
AssertOrdersPriceSideQuantityFromText(t, `
126+
BUY,0.2899,65.41
127+
BUY,0.2868,68.61
128+
BUY,0.2837,71.97
129+
BUY,0.2806,75.5
130+
BUY,0.2775,79.2
131+
BUY,0.2744,83.07
132+
BUY,0.2713,87.14
133+
BUY,0.2682,91.41
134+
BUY,0.2651,95.88
135+
BUY,0.262,100.58
136+
BUY,0.2589,105.5
137+
BUY,0.2558,110.67
138+
BUY,0.2527,116.09
139+
BUY,0.2496,121.77
140+
BUY,0.2465,127.74
141+
BUY,0.2434,133.99
142+
BUY,0.2403,140.55
143+
BUY,0.2372,147.44
144+
BUY,0.2341,154.65
145+
BUY,0.231,162.23
146+
BUY,0.2279,170.17
147+
BUY,0.2248,178.5
148+
BUY,0.2217,187.24
149+
BUY,0.2186,196.41
150+
BUY,0.2155,206.03
151+
BUY,0.2124,216.12
152+
BUY,0.2093,226.7
153+
BUY,0.2062,237.8
154+
BUY,0.2031,249.44
155+
BUY,0.2,261.66
156+
`, orders)
157+
})
114158
}
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package liquiditymaker
2+
3+
import "github.com/prometheus/client_golang/prometheus"
4+
5+
var openOrderBidExposureInUsdMetrics = prometheus.NewGaugeVec(
6+
prometheus.GaugeOpts{
7+
Name: "liqmaker_open_order_bid_exposure_in_usd",
8+
Help: "",
9+
}, []string{"strategy_type", "strategy_id", "exchange", "symbol"})
10+
11+
var openOrderAskExposureInUsdMetrics = prometheus.NewGaugeVec(
12+
prometheus.GaugeOpts{
13+
Name: "liqmaker_open_order_ask_exposure_in_usd",
14+
Help: "",
15+
}, []string{"strategy_type", "strategy_id", "exchange", "symbol"})
16+
17+
func init() {
18+
prometheus.MustRegister(
19+
openOrderBidExposureInUsdMetrics,
20+
openOrderAskExposureInUsdMetrics,
21+
)
22+
}

pkg/strategy/liquiditymaker/strategy.go

+33-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import (
55
"fmt"
66
"sync"
77

8+
"github.com/prometheus/client_golang/prometheus"
89
log "github.com/sirupsen/logrus"
910

1011
"github.com/c9s/bbgo/pkg/bbgo"
12+
"github.com/c9s/bbgo/pkg/dbg"
1113
"github.com/c9s/bbgo/pkg/fixedpoint"
1214
indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2"
1315
"github.com/c9s/bbgo/pkg/strategy/common"
@@ -79,17 +81,28 @@ type Strategy struct {
7981

8082
orderGenerator *LiquidityOrderGenerator
8183

82-
logger log.FieldLogger
84+
logger log.FieldLogger
85+
metricsLabels prometheus.Labels
8386
}
8487

8588
func (s *Strategy) Initialize() error {
8689
if s.Strategy == nil {
8790
s.Strategy = &common.Strategy{}
8891
}
8992

90-
s.logger = log.WithField("strategy", ID).WithFields(log.Fields{
91-
"symbol": s.Symbol,
93+
s.logger = log.WithFields(log.Fields{
94+
"symbol": s.Symbol,
95+
"strategy": ID,
96+
"strategy_id": s.InstanceID(),
9297
})
98+
99+
s.metricsLabels = prometheus.Labels{
100+
"strategy_type": ID,
101+
"strategy_id": s.InstanceID(),
102+
"exchange": string(s.Session.Exchange.Name()),
103+
"symbol": s.Symbol,
104+
}
105+
93106
return nil
94107
}
95108

@@ -406,6 +419,8 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
406419
}
407420
}
408421

422+
var bidExposureInUsd = fixedpoint.Zero
423+
var askExposureInUsd = fixedpoint.Zero
409424
var orderForms []types.SubmitOrder
410425
if placeBid {
411426
bidOrders := s.orderGenerator.Generate(types.SideTypeBuy,
@@ -415,6 +430,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
415430
s.NumOfLiquidityLayers,
416431
s.liquidityScale)
417432

433+
bidExposureInUsd = sumOrderQuoteQuantity(bidOrders)
418434
orderForms = append(orderForms, bidOrders...)
419435
}
420436

@@ -427,16 +443,22 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
427443
s.liquidityScale)
428444

429445
askOrders = filterAskOrders(askOrders, baseBal.Available)
446+
askExposureInUsd = sumOrderQuoteQuantity(askOrders)
430447
orderForms = append(orderForms, askOrders...)
431448
}
432449

450+
dbg.DebugSubmitOrders(s.logger, orderForms)
451+
433452
createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, orderForms...)
434453
if util.LogErr(err, "unable to place liquidity orders") {
435454
return
436455
}
437456

438457
s.liquidityOrderBook.Add(createdOrders...)
439458

459+
openOrderBidExposureInUsdMetrics.With(s.metricsLabels).Set(bidExposureInUsd.Float64())
460+
openOrderAskExposureInUsdMetrics.With(s.metricsLabels).Set(askExposureInUsd.Float64())
461+
440462
s.logger.Infof("%d liq orders are placed successfully", len(orderForms))
441463
for _, o := range createdOrders {
442464
s.logger.Infof("liq order: %+v", o)
@@ -461,6 +483,14 @@ func profitProtectedPrice(
461483
return price
462484
}
463485

486+
func sumOrderQuoteQuantity(orders []types.SubmitOrder) fixedpoint.Value {
487+
sum := fixedpoint.Zero
488+
for _, order := range orders {
489+
sum = sum.Add(order.Price.Mul(order.Quantity))
490+
}
491+
return sum
492+
}
493+
464494
func filterAskOrders(askOrders []types.SubmitOrder, available fixedpoint.Value) (out []types.SubmitOrder) {
465495
usedBase := fixedpoint.Zero
466496
for _, askOrder := range askOrders {

pkg/testing/testhelper/assert_priceside.go

+51
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package testhelper
22

33
import (
4+
"fmt"
5+
"strings"
46
"testing"
57

68
"github.com/stretchr/testify/assert"
@@ -28,6 +30,55 @@ type PriceSideQuantityAssert struct {
2830
Quantity fixedpoint.Value
2931
}
3032

33+
func ParsePriceSideQuantityAssertions(text string) []PriceSideQuantityAssert {
34+
var asserts []PriceSideQuantityAssert
35+
lines := strings.Split(text, "\n")
36+
for _, line := range lines {
37+
line = strings.TrimSpace(line)
38+
if line == "" {
39+
continue
40+
}
41+
42+
cols := strings.SplitN(line, ",", 3)
43+
if len(cols) < 3 {
44+
panic(fmt.Errorf("column length should be 3, got %d", len(cols)))
45+
}
46+
47+
side := strings.TrimSpace(cols[0])
48+
price := fixedpoint.MustNewFromString(strings.TrimSpace(cols[1]))
49+
quantity := fixedpoint.MustNewFromString(strings.TrimSpace(cols[2]))
50+
asserts = append(asserts, PriceSideQuantityAssert{
51+
Price: price,
52+
Side: types.SideType(side),
53+
Quantity: quantity,
54+
})
55+
}
56+
57+
return asserts
58+
}
59+
60+
func AssertOrdersPriceSideQuantityFromText(
61+
t *testing.T, text string, orders []types.SubmitOrder,
62+
) {
63+
asserts := ParsePriceSideQuantityAssertions(text)
64+
assert.Equalf(t, len(asserts), len(orders), "expecting %d orders", len(asserts))
65+
for i, a := range asserts {
66+
order := orders[i]
67+
assert.Equalf(t, a.Price.Float64(), order.Price.Float64(), "order #%d price should be %f", i+1, a.Price.Float64())
68+
assert.Equalf(t, a.Quantity.Float64(), order.Quantity.Float64(), "order #%d quantity should be %f", i+1, a.Quantity.Float64())
69+
assert.Equalf(t, a.Side, orders[i].Side, "order at price %f should be %s", a.Price.Float64(), a.Side)
70+
71+
}
72+
73+
if t.Failed() {
74+
actualInText := "Actual Orders:\n"
75+
for _, order := range orders {
76+
actualInText += fmt.Sprintf("%s,%s,%s\n", order.Side, order.Price.String(), order.Quantity.String())
77+
}
78+
t.Log(actualInText)
79+
}
80+
}
81+
3182
// AssertOrdersPriceSide asserts the orders with the given price and side (slice)
3283
func AssertOrdersPriceSideQuantity(
3384
t *testing.T, asserts []PriceSideQuantityAssert, orders []types.SubmitOrder,

pkg/types/orders.go

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package types
2+
3+
func ClassifyOrdersByStatus(orders []Order) (opened, cancelled, filled, unexpected []Order) {
4+
for _, order := range orders {
5+
switch order.Status {
6+
case OrderStatusNew, OrderStatusPartiallyFilled:
7+
opened = append(opened, order)
8+
case OrderStatusFilled:
9+
filled = append(filled, order)
10+
case OrderStatusCanceled:
11+
cancelled = append(cancelled, order)
12+
default:
13+
unexpected = append(unexpected, order)
14+
}
15+
}
16+
17+
return opened, cancelled, filled, unexpected
18+
}

0 commit comments

Comments
 (0)