Skip to content

Commit ae2fbd0

Browse files
authored
mm: add volume minimums and sanity checks for oracle rates (#2937)
* add volume minimums and sanity checks for oracle rates * tame bond asset warning
1 parent 8ecde88 commit ae2fbd0

5 files changed

Lines changed: 84 additions & 30 deletions

File tree

client/core/bond.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,7 @@ func (c *Core) rotateBonds(ctx context.Context) {
706706

707707
bondCfg := c.dexBondConfig(dc, now)
708708
if len(bondCfg.bondAssets) == 0 {
709-
if !dc.IsDown() {
709+
if !dc.IsDown() && dc.config() != nil {
710710
dc.log.Meter("no-bond-assets", time.Minute*10).Warnf("Zero bond assets reported for apparently connected DCRDEX server")
711711
}
712712
continue

client/mm/mm_basic.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"math"
1111
"sync"
1212
"sync/atomic"
13+
"time"
1314

1415
"decred.org/dcrdex/client/core"
1516
"decred.org/dcrdex/dex"
@@ -165,13 +166,30 @@ func (b *basicMMCalculatorImpl) basisPrice() uint64 {
165166
oracleRate := b.msgRate(b.oracle.getMarketPrice(b.baseID, b.quoteID))
166167
b.log.Tracef("oracle rate = %s", b.fmtRate(oracleRate))
167168

168-
if oracleRate == 0 {
169-
oracleRate = b.core.ExchangeRateFromFiatSources()
170-
if oracleRate == 0 {
169+
rateFromFiat := b.core.ExchangeRateFromFiatSources()
170+
if rateFromFiat == 0 {
171+
b.log.Meter("basisPrice_nofiat_"+b.market.name, time.Hour).Warn(
172+
"No fiat-based rate estimate(s) available for sanity check for %s", b.market.name,
173+
)
174+
if oracleRate == 0 { // steppedRate(0, x) => x, so we have to handle this.
171175
return 0
172176
}
173-
174-
b.log.Tracef("using fiat rate = %s", b.fmtRate(oracleRate))
177+
return steppedRate(oracleRate, b.rateStep)
178+
}
179+
if oracleRate == 0 {
180+
b.log.Meter("basisPrice_nooracle_"+b.market.name, time.Hour).Infof(
181+
"No oracle rate available. Using fiat-derived basis rate = %s for %s", b.fmtRate(rateFromFiat), b.market.name,
182+
)
183+
return steppedRate(rateFromFiat, b.rateStep)
184+
}
185+
mismatch := math.Abs((float64(oracleRate) - float64(rateFromFiat)) / float64(oracleRate))
186+
const maxOracleFiatMismatch = 0.05
187+
if mismatch > maxOracleFiatMismatch {
188+
b.log.Meter("basisPrice_sanity_fail+"+b.market.name, time.Minute*20).Warnf(
189+
"Oracle rate sanity check failed for %s. oracle rate = %s, rate from fiat = %s",
190+
b.market.name, b.market.fmtRate(oracleRate), b.market.fmtRate(rateFromFiat),
191+
)
192+
return 0
175193
}
176194

177195
return steppedRate(oracleRate, b.rateStep)

client/mm/mm_basic_test.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,15 @@ func TestBasisPrice(t *testing.T) {
4545
{
4646
name: "oracle price",
4747
oraclePrice: 2000,
48-
fiatRate: 1000,
48+
fiatRate: 1900,
4949
exp: 2000,
5050
},
51+
{
52+
name: "failed sanity check",
53+
oraclePrice: 2000,
54+
fiatRate: 1850, // mismatch > 5%
55+
exp: 0,
56+
},
5157
{
5258
name: "no oracle price",
5359
oraclePrice: 0,

client/mm/mm_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,7 @@ func newTBotCEXAdaptor() *tBotCexAdaptor {
556556

557557
var _ botCexAdaptor = (*tBotCexAdaptor)(nil)
558558

559-
var tLogger = dex.StdOutLogger("mm_TEST", dex.LevelTrace)
559+
var tLogger = dex.StdOutLogger("mm_TEST", dex.LevelInfo)
560560

561561
func (c *tBotCexAdaptor) CEXBalance(assetID uint32) (*BotBalance, error) {
562562
return c.balances[assetID], c.balanceErr

client/mm/price_oracle.go

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ import (
2424
const (
2525
oraclePriceExpiration = time.Minute * 10
2626
oracleRecheckInterval = time.Minute * 3
27+
28+
// If the total USD volume of all oracles is less than
29+
// minimumUSDVolumeForOraclesAvg, the oracles will be ignored for
30+
// pricing averages.
31+
minimumUSDVolumeForOraclesAvg = 100_000
2732
)
2833

2934
// MarketReport contains a market's rates on various exchanges and the fiat
@@ -265,26 +270,36 @@ func fetchMarketPrice(ctx context.Context, baseID, quoteID uint32, log dex.Logge
265270
return 0, nil, err
266271
}
267272

268-
price, err := oracleAverage(oracles, log)
273+
price, usdVolume, err := oracleAverage(oracles, log)
274+
if err != nil {
275+
return 0, nil, err
276+
}
277+
if usdVolume < minimumUSDVolumeForOraclesAvg {
278+
log.Meter("oracle_low_volume_"+b.Symbol+"_"+q.Symbol, 12*time.Hour).Infof(
279+
"Rejecting oracle average price for %s. not enough volume (%.2f USD < %.2f)",
280+
b.Symbol+"_"+q.Symbol, usdVolume, float32(minimumUSDVolumeForOraclesAvg),
281+
)
282+
return 0, oracles, nil
283+
}
269284
return price, oracles, err
270285
}
271286

272-
func oracleAverage(mkts []*OracleReport, log dex.Logger) (float64, error) {
273-
var weightedSum, usdVolume float64
287+
func oracleAverage(mkts []*OracleReport, log dex.Logger) (rate, usdVolume float64, _ error) {
288+
var weightedSum float64
274289
var n int
275290
for _, mkt := range mkts {
276291
n++
277292
weightedSum += mkt.USDVol * (mkt.BestBuy + mkt.BestSell) / 2
278293
usdVolume += mkt.USDVol
279294
}
280295
if usdVolume == 0 {
281-
return 0, nil // No markets have data. OK.
296+
return 0, 0, nil // No markets have data. OK.
282297
}
283298

284-
rate := weightedSum / usdVolume
299+
rate = weightedSum / usdVolume
285300
// TODO: Require a minimum USD volume?
286301
log.Tracef("marketAveragedPrice: price calculated from %d markets: rate = %f, USD volume = %f", n, rate, usdVolume)
287-
return rate, nil
302+
return rate, usdVolume, nil
288303
}
289304

290305
func getRates(ctx context.Context, url string, thing any) (err error) {
@@ -325,7 +340,7 @@ func spread(ctx context.Context, addr string, baseSymbol, quoteSymbol string, lo
325340
}
326341
sell, buy, err = s(ctx, baseSymbol, quoteSymbol, log)
327342
if err != nil {
328-
log.Errorf("Error getting spread from %q: %v", addr, err)
343+
log.Meter("spread_"+addr, time.Hour*12).Errorf("Error getting spread from %q: %v", addr, err)
329344
return 0, 0
330345
}
331346
return sell, buy
@@ -351,7 +366,8 @@ func oracleMarketReport(ctx context.Context, b, q *fiatrates.CoinpaprikaAsset, l
351366
QuoteCurrencyID string `json:"quote_currency_id"`
352367
MarketURL string `json:"market_url"`
353368
LastUpdated time.Time `json:"last_updated"`
354-
TrustScore string `json:"trust_score"`
369+
TrustScore string `json:"trust_score"` // TrustScore appears to be deprecated?
370+
Outlier bool `json:"outlier"`
355371
Quotes map[string]*coinpapQuote `json:"quotes"`
356372
}
357373

@@ -375,7 +391,7 @@ func oracleMarketReport(ctx context.Context, b, q *fiatrates.CoinpaprikaAsset, l
375391

376392
// Create filter for desirable matches.
377393
marketMatches := func(mkt *coinpapMarket) bool {
378-
if mkt.TrustScore != "high" {
394+
if mkt.TrustScore != "high" || mkt.Outlier {
379395
return false
380396
}
381397

@@ -441,35 +457,53 @@ func oracleMarketReport(ctx context.Context, b, q *fiatrates.CoinpaprikaAsset, l
441457
type Spreader func(ctx context.Context, baseSymbol, quoteSymbol string, log dex.Logger) (sell, buy float64, err error)
442458

443459
var spreaders = map[string]Spreader{
444-
"binance.com": fetchBinanceSpread,
460+
"binance.com": fetchBinanceGlobalSpread,
461+
"binance.us": fetchBinanceUSSpread,
445462
"coinbase.com": fetchCoinbaseSpread,
446463
"bittrex.com": fetchBittrexSpread,
447464
"hitbtc.com": fetchHitBTCSpread,
448465
"exmo.com": fetchEXMOSpread,
449466
}
450467

451-
var binanceGlobalIs451 atomic.Bool
468+
var binanceGlobalIs451, binanceUSIs451 atomic.Bool
469+
470+
func fetchBinanceGlobalSpread(ctx context.Context, baseSymbol, quoteSymbol string, log dex.Logger) (sell, buy float64, err error) {
471+
if binanceGlobalIs451.Load() {
472+
return 0, 0, nil
473+
}
474+
return fetchBinanceSpread(ctx, baseSymbol, quoteSymbol, false, log)
475+
}
452476

453-
func fetchBinanceSpread(ctx context.Context, baseSymbol, quoteSymbol string, log dex.Logger) (sell, buy float64, err error) {
477+
func fetchBinanceUSSpread(ctx context.Context, baseSymbol, quoteSymbol string, log dex.Logger) (sell, buy float64, err error) {
478+
if binanceUSIs451.Load() {
479+
return 0, 0, nil
480+
}
481+
return fetchBinanceSpread(ctx, baseSymbol, quoteSymbol, true, log)
482+
}
483+
484+
func fetchBinanceSpread(ctx context.Context, baseSymbol, quoteSymbol string, isUS bool, log dex.Logger) (sell, buy float64, err error) {
454485
slug := fmt.Sprintf("%s%s", strings.ToUpper(baseSymbol), strings.ToUpper(quoteSymbol))
455486
var url string
456-
var isGlobal bool
457-
if binanceGlobalIs451.Load() {
487+
if isUS {
458488
url = fmt.Sprintf("https://api.binance.us/api/v3/ticker/bookTicker?symbol=%s", slug)
459489
} else {
460-
isGlobal = true
461490
url = fmt.Sprintf("https://api.binance.com/api/v3/ticker/bookTicker?symbol=%s", slug)
462491
}
463492

464493
var resp struct {
465494
BidPrice float64 `json:"bidPrice,string"`
466495
AskPrice float64 `json:"askPrice,string"`
467496
}
497+
468498
code, err := getHTTPWithCode(ctx, url, &resp)
469499
if err != nil {
470-
if isGlobal && code == http.StatusUnavailableForLegalReasons && binanceGlobalIs451.CompareAndSwap(false, true) {
471-
log.Info("Binance Global responded with a 451. Oracle will use Binance U.S.")
472-
return fetchBinanceSpread(ctx, baseSymbol, quoteSymbol, log)
500+
if code == http.StatusUnavailableForLegalReasons {
501+
if isUS && binanceUSIs451.CompareAndSwap(false, true) {
502+
log.Debugf("Binance U.S. responded with a 451. Disabling")
503+
} else if !isUS && binanceGlobalIs451.CompareAndSwap(false, true) {
504+
log.Debugf("Binance Global responded with a 451. Disabling")
505+
}
506+
return 0, 0, nil
473507
}
474508
return 0, 0, err
475509
}
@@ -554,7 +588,3 @@ func fetchEXMOSpread(ctx context.Context, baseSymbol, quoteSymbol string, _ dex.
554588

555589
return mkt.AskTop, mkt.BidTop, nil
556590
}
557-
558-
func shortSymbol(symbol string) string {
559-
return strings.Split(symbol, ".")[0]
560-
}

0 commit comments

Comments
 (0)