Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IMPROVE: add test helper for price side quantity assertion #1795

Merged
merged 5 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions pkg/dbg/orders.go
Original file line number Diff line number Diff line change
@@ -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())
}
28 changes: 7 additions & 21 deletions pkg/strategy/dca2/recover.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import (
"strconv"
"time"

"github.com/pkg/errors"

"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/exchange/retry"
"github.com/c9s/bbgo/pkg/types"
"github.com/pkg/errors"
)

var recoverSinceLimit = time.Date(2024, time.January, 29, 12, 0, 0, 0, time.Local)
Expand Down Expand Up @@ -65,7 +66,7 @@ func recoverState(ctx context.Context, maxOrderCount int, currentRound Round, or

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

if len(unexpectedOrders) > 0 {
return None, fmt.Errorf("there is unexpected status in orders %+v", unexpectedOrders)
Expand Down Expand Up @@ -96,7 +97,7 @@ func recoverState(ctx context.Context, maxOrderCount int, currentRound Round, or
}

// collect open-position orders' status
openedOrders, cancelledOrders, filledOrders, unexpectedOrders := classifyOrders(currentRound.OpenPositionOrders)
openedOrders, cancelledOrders, filledOrders, unexpectedOrders := types.ClassifyOrdersByStatus(currentRound.OpenPositionOrders)
if len(unexpectedOrders) > 0 {
return None, fmt.Errorf("there is unexpected status of orders %+v", unexpectedOrders)
}
Expand Down Expand Up @@ -124,7 +125,9 @@ func recoverState(ctx context.Context, maxOrderCount int, currentRound Round, or
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 {
return fmt.Errorf("position is nil, please check it")
}
Expand Down Expand Up @@ -191,20 +194,3 @@ func recoverStartTimeOfNextRound(ctx context.Context, currentRound Round, coolDo

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
}
2 changes: 1 addition & 1 deletion pkg/strategy/dca2/recover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ func Test_classifyOrders(t *testing.T) {
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, 4, len(cancelled))
assert.Equal(t, 2, len(filled))
Expand Down
10 changes: 10 additions & 0 deletions pkg/strategy/liquiditymaker/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ func (g *LiquidityOrderGenerator) Generate(
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)))
switch side {
case types.SideTypeSell:
Expand All @@ -59,6 +67,8 @@ func (g *LiquidityOrderGenerator) Generate(
}
}

g.logger.Infof("side %s layer spread: %s", side, layerSpread.String())

quantityBase := 0.0
var layerPrices []fixedpoint.Value
var layerScales []float64
Expand Down
44 changes: 44 additions & 0 deletions pkg/strategy/liquiditymaker/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,48 @@ func TestLiquidityOrderGenerator(t *testing.T) {
{Side: types.SideTypeBuy, Price: Number("1.9600"), Quantity: Number("620.54")},
}, 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 changes: 22 additions & 0 deletions pkg/strategy/liquiditymaker/metrics.go
Original file line number Diff line number Diff line change
@@ -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,
)
}
36 changes: 33 additions & 3 deletions pkg/strategy/liquiditymaker/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
"fmt"
"sync"

"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"

"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/dbg"
"github.com/c9s/bbgo/pkg/fixedpoint"
indicatorv2 "github.com/c9s/bbgo/pkg/indicator/v2"
"github.com/c9s/bbgo/pkg/strategy/common"
Expand Down Expand Up @@ -79,17 +81,28 @@ type Strategy struct {

orderGenerator *LiquidityOrderGenerator

logger log.FieldLogger
logger log.FieldLogger
metricsLabels prometheus.Labels
}

func (s *Strategy) Initialize() error {
if s.Strategy == nil {
s.Strategy = &common.Strategy{}
}

s.logger = log.WithField("strategy", ID).WithFields(log.Fields{
"symbol": s.Symbol,
s.logger = log.WithFields(log.Fields{
"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
}

Expand Down Expand Up @@ -406,6 +419,8 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
}
}

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

bidExposureInUsd = sumOrderQuoteQuantity(bidOrders)
orderForms = append(orderForms, bidOrders...)
}

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

askOrders = filterAskOrders(askOrders, baseBal.Available)
askExposureInUsd = sumOrderQuoteQuantity(askOrders)
orderForms = append(orderForms, askOrders...)
}

dbg.DebugSubmitOrders(s.logger, orderForms)

createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, orderForms...)
if util.LogErr(err, "unable to place liquidity orders") {
return
}

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))
for _, o := range createdOrders {
s.logger.Infof("liq order: %+v", o)
Expand All @@ -461,6 +483,14 @@ func profitProtectedPrice(
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) {
usedBase := fixedpoint.Zero
for _, askOrder := range askOrders {
Expand Down
51 changes: 51 additions & 0 deletions pkg/testing/testhelper/assert_priceside.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package testhelper

import (
"fmt"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -28,6 +30,55 @@ type PriceSideQuantityAssert struct {
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)
func AssertOrdersPriceSideQuantity(
t *testing.T, asserts []PriceSideQuantityAssert, orders []types.SubmitOrder,
Expand Down
18 changes: 18 additions & 0 deletions pkg/types/orders.go
Original file line number Diff line number Diff line change
@@ -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