Skip to content

Commit 75b9bc5

Browse files
authored
Merge pull request #2385 from c9s/c9s/xmaker/bal-based-quoting
FEATURE: [xmaker] integrate balance weight quoting
2 parents 23da40f + 46105f8 commit 75b9bc5

File tree

7 files changed

+674
-150
lines changed

7 files changed

+674
-150
lines changed

pkg/strategy/xmaker/hedgemarket.go

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,50 @@ func (c *HedgeMarketConfig) Defaults() error {
103103
return nil
104104
}
105105

106+
// ResolveQuotingDepthInQuote returns the quoting depth in quote currency.
107+
// Priority:
108+
// 1. If QuotingDepthInQuote is configured (> 0), return it directly.
109+
// 2. Else if QuotingDepth (base) is configured and latestPrice > 0, convert
110+
// base depth to quote via base * latestPrice.
111+
// 3. Otherwise return zero.
112+
func (c *HedgeMarketConfig) ResolveQuotingDepthInQuote(latestPrice fixedpoint.Value) fixedpoint.Value {
113+
if c == nil {
114+
return fixedpoint.Zero
115+
}
116+
117+
if c.QuotingDepthInQuote.Sign() > 0 {
118+
return c.QuotingDepthInQuote
119+
}
120+
121+
if c.QuotingDepth.Sign() > 0 && latestPrice.Sign() > 0 {
122+
return c.QuotingDepth.Mul(latestPrice)
123+
}
124+
125+
return fixedpoint.Zero
126+
}
127+
128+
// ResolveQuotingDepthInBase returns the quoting depth in base currency.
129+
// Priority:
130+
// 1. If QuotingDepth (base) is configured (> 0), return it directly.
131+
// 2. Else if QuotingDepthInQuote is configured and latestPrice > 0, convert
132+
// quote depth to base via quote / latestPrice.
133+
// 3. Otherwise return zero.
134+
func (c *HedgeMarketConfig) ResolveQuotingDepthInBase(latestPrice fixedpoint.Value) fixedpoint.Value {
135+
if c == nil {
136+
return fixedpoint.Zero
137+
}
138+
139+
if c.QuotingDepth.Sign() > 0 {
140+
return c.QuotingDepth
141+
}
142+
143+
if c.QuotingDepthInQuote.Sign() > 0 && latestPrice.Sign() > 0 {
144+
return c.QuotingDepthInQuote.Div(latestPrice)
145+
}
146+
147+
return fixedpoint.Zero
148+
}
149+
106150
func InitializeHedgeMarketFromConfig(
107151
c *HedgeMarketConfig,
108152
sessions map[string]*bbgo.ExchangeSession,
@@ -459,9 +503,44 @@ func (m *HedgeMarket) GetQuotePriceBySessionBalances() (bid, ask fixedpoint.Valu
459503
// fetch available balances once
460504
baseAvail, quoteAvail := m.GetBaseQuoteAvailableBalances()
461505

506+
// determine a latest price for converting depths between base and quote
507+
bestBid := m.bidPricer(0, fixedpoint.Zero)
508+
bestAsk := m.askPricer(0, fixedpoint.Zero)
509+
latest := fixedpoint.Zero
510+
if bestBid.Sign() > 0 && bestAsk.Sign() > 0 {
511+
latest = bestBid.Add(bestAsk).Div(fixedpoint.NewFromFloat(2))
512+
} else if bestBid.Sign() > 0 {
513+
latest = bestBid
514+
} else if bestAsk.Sign() > 0 {
515+
latest = bestAsk
516+
}
517+
518+
// resolve configured quoting depths in both currencies
519+
cfgQuoteDepth := m.HedgeMarketConfig.ResolveQuotingDepthInQuote(latest)
520+
cfgBaseDepth := m.HedgeMarketConfig.ResolveQuotingDepthInBase(latest)
521+
522+
// use min(balance, configuredDepth) per requirement
523+
baseDepthToUse := baseAvail
524+
if cfgBaseDepth.Sign() > 0 && baseAvail.Compare(cfgBaseDepth) > 0 {
525+
baseDepthToUse = cfgBaseDepth
526+
}
527+
528+
quoteDepthToUse := quoteAvail
529+
if cfgQuoteDepth.Sign() > 0 && quoteAvail.Compare(cfgQuoteDepth) > 0 {
530+
quoteDepthToUse = cfgQuoteDepth
531+
}
532+
462533
// compute raw prices using side-specific depth definitions
463-
rawBid := m.depthBook.PriceAtDepth(types.SideTypeBuy, baseAvail)
464-
rawAsk := m.depthBook.PriceAtQuoteDepth(types.SideTypeSell, quoteAvail)
534+
rawBid := m.depthBook.PriceAtDepth(types.SideTypeBuy, baseDepthToUse)
535+
rawAsk := m.depthBook.PriceAtQuoteDepth(types.SideTypeSell, quoteDepthToUse)
536+
537+
// fallback to best prices if depth-based prices are not available
538+
if rawBid.IsZero() {
539+
rawBid = bestBid
540+
}
541+
if rawAsk.IsZero() {
542+
rawAsk = bestAsk
543+
}
465544

466545
// Apply fee according to PriceFeeMode
467546
feeRate := m.priceFeeRate()
@@ -472,8 +551,12 @@ func (m *HedgeMarket) GetQuotePriceBySessionBalances() (bid, ask fixedpoint.Valu
472551
if bid.IsZero() || ask.IsZero() {
473552
bids := m.book.SideBook(types.SideTypeBuy)
474553
asks := m.book.SideBook(types.SideTypeSell)
475-
m.logger.Warnf("no valid bid/ask price from session balances for %s, baseAvail=%s, quoteAvail=%s, bids: %v, asks: %v",
476-
m.SymbolSelector, baseAvail.String(), quoteAvail.String(), bids, asks)
554+
m.logger.Warnf("no valid bid/ask price from session balances for %s, baseAvail=%s, quoteAvail=%s, cfgBaseDepth=%s, cfgQuoteDepth=%s, usedBaseDepth=%s, usedQuoteDepth=%s, bids: %v, asks: %v",
555+
m.SymbolSelector,
556+
baseAvail.String(), quoteAvail.String(),
557+
cfgBaseDepth.String(), cfgQuoteDepth.String(),
558+
baseDepthToUse.String(), quoteDepthToUse.String(),
559+
bids, asks)
477560
}
478561

479562
// store prices as snapshot
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package xmaker
2+
3+
import (
4+
"testing"
5+
6+
"github.com/c9s/bbgo/pkg/fixedpoint"
7+
)
8+
9+
func TestResolveQuotingDepthInQuote(t *testing.T) {
10+
price := fixedpoint.NewFromFloat(30000)
11+
12+
t.Run("prefer in-quote when set", func(t *testing.T) {
13+
c := &HedgeMarketConfig{
14+
QuotingDepthInQuote: fixedpoint.NewFromFloat(500),
15+
QuotingDepth: fixedpoint.NewFromFloat(2), // should be ignored
16+
}
17+
got := c.ResolveQuotingDepthInQuote(price)
18+
want := fixedpoint.NewFromFloat(500)
19+
if got.Compare(want) != 0 {
20+
t.Fatalf("got %s, want %s", got.String(), want.String())
21+
}
22+
})
23+
24+
t.Run("derive in-quote from base when only base set", func(t *testing.T) {
25+
c := &HedgeMarketConfig{
26+
QuotingDepth: fixedpoint.NewFromFloat(2),
27+
}
28+
got := c.ResolveQuotingDepthInQuote(price)
29+
want := fixedpoint.NewFromFloat(60000)
30+
if got.Compare(want) != 0 {
31+
t.Fatalf("got %s, want %s", got.String(), want.String())
32+
}
33+
})
34+
35+
t.Run("zero when no config", func(t *testing.T) {
36+
c := &HedgeMarketConfig{}
37+
got := c.ResolveQuotingDepthInQuote(price)
38+
if !got.IsZero() {
39+
t.Fatalf("expected zero, got %s", got.String())
40+
}
41+
})
42+
43+
t.Run("zero when price is zero and only base set", func(t *testing.T) {
44+
c := &HedgeMarketConfig{QuotingDepth: fixedpoint.NewFromFloat(1)}
45+
got := c.ResolveQuotingDepthInQuote(fixedpoint.Zero)
46+
if !got.IsZero() {
47+
t.Fatalf("expected zero, got %s", got.String())
48+
}
49+
})
50+
}
51+
52+
func TestResolveQuotingDepthInBase(t *testing.T) {
53+
price := fixedpoint.NewFromFloat(25000)
54+
55+
t.Run("prefer base when set", func(t *testing.T) {
56+
c := &HedgeMarketConfig{
57+
QuotingDepth: fixedpoint.NewFromFloat(3),
58+
QuotingDepthInQuote: fixedpoint.NewFromFloat(1000), // ignored
59+
}
60+
got := c.ResolveQuotingDepthInBase(price)
61+
want := fixedpoint.NewFromFloat(3)
62+
if got.Compare(want) != 0 {
63+
t.Fatalf("got %s, want %s", got.String(), want.String())
64+
}
65+
})
66+
67+
t.Run("derive base from in-quote when only in-quote set", func(t *testing.T) {
68+
c := &HedgeMarketConfig{
69+
QuotingDepthInQuote: fixedpoint.NewFromFloat(50000),
70+
}
71+
got := c.ResolveQuotingDepthInBase(price)
72+
want := fixedpoint.NewFromFloat(2)
73+
if got.Compare(want) != 0 {
74+
t.Fatalf("got %s, want %s", got.String(), want.String())
75+
}
76+
})
77+
78+
t.Run("zero when no config", func(t *testing.T) {
79+
c := &HedgeMarketConfig{}
80+
got := c.ResolveQuotingDepthInBase(price)
81+
if !got.IsZero() {
82+
t.Fatalf("expected zero, got %s", got.String())
83+
}
84+
})
85+
86+
t.Run("zero when price is zero and only in-quote set", func(t *testing.T) {
87+
c := &HedgeMarketConfig{QuotingDepthInQuote: fixedpoint.NewFromFloat(1)}
88+
got := c.ResolveQuotingDepthInBase(fixedpoint.Zero)
89+
if !got.IsZero() {
90+
t.Fatalf("expected zero, got %s", got.String())
91+
}
92+
})
93+
}

pkg/strategy/xmaker/metrics.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ var directionAlignedWeight = prometheus.NewGaugeVec(
143143
[]string{"strategy_type", "strategy_id", "exchange", "symbol"},
144144
)
145145

146+
var splitHedgeBidHigherThanAskCounter = prometheus.NewCounterVec(
147+
prometheus.CounterOpts{
148+
Name: "xmaker_split_hedge_bid_higher_than_ask_total",
149+
Help: "The total number of times the split hedge bid price was higher than the ask price",
150+
},
151+
[]string{"strategy_type", "strategy_id", "exchange", "symbol"},
152+
)
153+
146154
func init() {
147155
prometheus.MustRegister(
148156
openOrderBidExposureInUsdMetrics,
@@ -166,5 +174,6 @@ func init() {
166174
divergenceD2,
167175
directionMean,
168176
directionAlignedWeight,
177+
splitHedgeBidHigherThanAskCounter,
169178
)
170179
}

pkg/strategy/xmaker/splithedge.go

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -347,14 +347,19 @@ func (h *SplitHedge) Stop(shutdownCtx context.Context) error {
347347
// - Ask side is weighted by base currency available balance of each hedge market session.
348348
// - Bid side is weighted by quote currency available balance of each hedge market session.
349349
//
350+
// For margin accounts (mkt.session.Margin is true):
351+
// - The debt quota (borrowing capacity) is included in the weight.
352+
// - Bid weight = quoteAvail + debtQuota.
353+
// - Ask weight = baseAvail + (debtQuota / askPrice).
354+
//
350355
// Per-market prices are obtained by calling HedgeMarket.GetQuotePriceBySessionBalances(),
351356
// which already considers the market's current book and the session balances as depth.
352357
//
353358
// If the total weight for a side is zero (e.g., no base balances for ask weighting), the
354359
// corresponding returned price will be zero.
355-
func (h *SplitHedge) GetBalanceWeightedQuotePrice() (bid, ask fixedpoint.Value) {
360+
func (h *SplitHedge) GetBalanceWeightedQuotePrice() (bid, ask fixedpoint.Value, ret bool) {
356361
if h == nil || len(h.hedgeMarketInstances) == 0 {
357-
return fixedpoint.Zero, fixedpoint.Zero
362+
return fixedpoint.Zero, fixedpoint.Zero, false
358363
}
359364

360365
var (
@@ -371,22 +376,40 @@ func (h *SplitHedge) GetBalanceWeightedQuotePrice() (bid, ask fixedpoint.Value)
371376
// Skip markets that have no book-derived prices (but still allow zero balances to contribute zero weight)
372377
b, a := mkt.GetQuotePriceBySessionBalances()
373378

374-
if !a.IsZero() && baseAvail.Sign() > 0 {
375-
sumWeightedAsk = sumWeightedAsk.Add(a.Mul(baseAvail))
376-
sumBaseWeight = sumBaseWeight.Add(baseAvail)
377-
} else if baseAvail.Sign() > 0 && a.IsZero() {
379+
if h.logger != nil {
380+
h.logger.Infof("splitHedge market ticker: %s ask / bid = %s / %s", name, a.String(), b.String())
381+
}
382+
383+
bidWeight := quoteAvail
384+
askWeight := baseAvail
385+
386+
if mkt.session.Margin {
387+
debtQuota := mkt.getDebtQuota()
388+
if debtQuota.Sign() > 0 {
389+
bidWeight = bidWeight.Add(debtQuota)
390+
391+
if !a.IsZero() {
392+
askWeight = askWeight.Add(debtQuota.Div(a))
393+
}
394+
}
395+
}
396+
397+
if !a.IsZero() && askWeight.Sign() > 0 {
398+
sumWeightedAsk = sumWeightedAsk.Add(a.Mul(askWeight))
399+
sumBaseWeight = sumBaseWeight.Add(askWeight)
400+
} else if askWeight.Sign() > 0 && a.IsZero() {
378401
// helpful diagnostics for missing price with weight
379402
if h.logger != nil {
380-
h.logger.Warnf("splitHedge: zero ask price from market %s despite positive base balance %s", name, baseAvail.String())
403+
h.logger.Warnf("splitHedge: zero ask price from market %s despite positive base balance %s", name, askWeight.String())
381404
}
382405
}
383406

384-
if !b.IsZero() && quoteAvail.Sign() > 0 {
385-
sumWeightedBid = sumWeightedBid.Add(b.Mul(quoteAvail))
386-
sumQuoteWeight = sumQuoteWeight.Add(quoteAvail)
387-
} else if quoteAvail.Sign() > 0 && b.IsZero() {
407+
if !b.IsZero() && bidWeight.Sign() > 0 {
408+
sumWeightedBid = sumWeightedBid.Add(b.Mul(bidWeight))
409+
sumQuoteWeight = sumQuoteWeight.Add(bidWeight)
410+
} else if bidWeight.Sign() > 0 && b.IsZero() {
388411
if h.logger != nil {
389-
h.logger.Warnf("splitHedge: zero bid price from market %s despite positive quote balance %s", name, quoteAvail.String())
412+
h.logger.Warnf("splitHedge: zero bid price from market %s despite positive quote balance %s", name, bidWeight.String())
390413
}
391414
}
392415
}
@@ -409,5 +432,5 @@ func (h *SplitHedge) GetBalanceWeightedQuotePrice() (bid, ask fixedpoint.Value)
409432
}
410433
}
411434

412-
return bid, ask
435+
return bid, ask, true
413436
}

0 commit comments

Comments
 (0)