diff --git a/pkg/dbg/orders.go b/pkg/dbg/orders.go new file mode 100644 index 000000000..38c6f66a8 --- /dev/null +++ b/pkg/dbg/orders.go @@ -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()) +} diff --git a/pkg/strategy/dca2/recover.go b/pkg/strategy/dca2/recover.go index b3c0c2093..c9889a946 100644 --- a/pkg/strategy/dca2/recover.go +++ b/pkg/strategy/dca2/recover.go @@ -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) @@ -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) @@ -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) } @@ -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") } @@ -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 -} diff --git a/pkg/strategy/dca2/recover_test.go b/pkg/strategy/dca2/recover_test.go index c26205c2d..3c09937e9 100644 --- a/pkg/strategy/dca2/recover_test.go +++ b/pkg/strategy/dca2/recover_test.go @@ -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)) diff --git a/pkg/strategy/liquiditymaker/generator.go b/pkg/strategy/liquiditymaker/generator.go index 6f8c066e8..069fc0e3c 100644 --- a/pkg/strategy/liquiditymaker/generator.go +++ b/pkg/strategy/liquiditymaker/generator.go @@ -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: @@ -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 diff --git a/pkg/strategy/liquiditymaker/generator_test.go b/pkg/strategy/liquiditymaker/generator_test.go index 995f33bd7..a1113cf44 100644 --- a/pkg/strategy/liquiditymaker/generator_test.go +++ b/pkg/strategy/liquiditymaker/generator_test.go @@ -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) + }) } diff --git a/pkg/strategy/liquiditymaker/metrics.go b/pkg/strategy/liquiditymaker/metrics.go new file mode 100644 index 000000000..7dfccf3a2 --- /dev/null +++ b/pkg/strategy/liquiditymaker/metrics.go @@ -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, + ) +} diff --git a/pkg/strategy/liquiditymaker/strategy.go b/pkg/strategy/liquiditymaker/strategy.go index 778335651..2921e3dd2 100644 --- a/pkg/strategy/liquiditymaker/strategy.go +++ b/pkg/strategy/liquiditymaker/strategy.go @@ -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" @@ -79,7 +81,8 @@ type Strategy struct { orderGenerator *LiquidityOrderGenerator - logger log.FieldLogger + logger log.FieldLogger + metricsLabels prometheus.Labels } func (s *Strategy) Initialize() error { @@ -87,9 +90,19 @@ func (s *Strategy) Initialize() error { 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 } @@ -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, @@ -415,6 +430,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { s.NumOfLiquidityLayers, s.liquidityScale) + bidExposureInUsd = sumOrderQuoteQuantity(bidOrders) orderForms = append(orderForms, bidOrders...) } @@ -427,9 +443,12 @@ 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 @@ -437,6 +456,9 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) { 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) @@ -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 { diff --git a/pkg/testing/testhelper/assert_priceside.go b/pkg/testing/testhelper/assert_priceside.go index 0d7f74df1..03e2fd7a1 100644 --- a/pkg/testing/testhelper/assert_priceside.go +++ b/pkg/testing/testhelper/assert_priceside.go @@ -1,6 +1,8 @@ package testhelper import ( + "fmt" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -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, diff --git a/pkg/types/orders.go b/pkg/types/orders.go new file mode 100644 index 000000000..34078dd29 --- /dev/null +++ b/pkg/types/orders.go @@ -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 +}