From 88a49e6e1965200b13c7b462ae3e535b14020bfb Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Tue, 9 Jun 2026 12:45:22 +0200 Subject: [PATCH] feat(peg): add OHLC per-minute gauges + peg_deviation_worst_bps headline Switches stablecoin-peg leaderboard from per-minute median to per-minute max deviation so sub-minute depeg wicks no longer get smoothed away. New metrics alongside existing peg_deviation_bps: - peg_deviation_worst_bps (PRIMARY): per-minute max |price-1| in bps - peg_minute_max_bps / _min_bps / _open_bps / _close_bps: full OHLC bar YAML provider queries swapped to peg_deviation_worst_bps. Legacy peg_deviation_bps gauge retained for backward compat. Methodology recommendation from Coinpaprika data team review. --- benchmarks/stablecoin-peg.yml | 40 ++++++++------- .../stablecoin-peg/cmd/script/aggregator.go | 42 +++++++++++++++ .../stablecoin-peg/cmd/script/metrics.go | 51 ++++++++++++++++++- 3 files changed, 115 insertions(+), 18 deletions(-) diff --git a/benchmarks/stablecoin-peg.yml b/benchmarks/stablecoin-peg.yml index e92ed949..66f27b57 100644 --- a/benchmarks/stablecoin-peg.yml +++ b/benchmarks/stablecoin-peg.yml @@ -57,7 +57,8 @@ abstract: | methodology: - "Sources per stable, USDC: Binance USDC/USDT (USDT-anchored, secondary), Kraken USDCUSD, Bitstamp usdcusd. USDT: Kraken USDTUSD, Bitstamp usdtusd. FDUSD: Binance FDUSDUSDT (USDT-anchored, secondary). USDe: Binance USDEUSDT (USDT-anchored, secondary). DAI: Curve 3pool `get_dy` forward (USDC → DAI) and reverse (DAI → USDC)." - "Cadence: 5 seconds for CEX REST tickers, 12 seconds for Curve `get_dy` on-chain (matches Ethereum block time). Per-venue samples are grouped into 60-second buckets for the liquidity-weighted median aggregation." - - "Aggregation: per-minute liquidity-weighted median across USD-quoted venues (`Quote = QuoteUSD`). Weight is 24 h USD volume estimate. The aggregated price drives `peg_deviation_bps` and `peg_deviation_bps_histogram`; the percentile metric is computed on this series, not on raw per-venue samples." + - "Aggregation: per-minute liquidity-weighted median across USD-quoted venues (`Quote = QuoteUSD`). Weight is 24 h USD volume estimate. The aggregated price drives `peg_deviation_bps` (per-minute median, retained for backward compatibility) and `peg_deviation_bps_histogram`." + - "Primary headline: `peg_deviation_worst_bps` — the per-minute MAX |price − $1.00| across every venue sample in the 60-second bucket, in basis points. The leaderboard ranks stables on `quantile_over_time(0.99, peg_deviation_worst_bps[24h])`. Switching from per-minute median to per-minute max surfaces the sub-minute depeg wicks (e.g. a 5-second print to $0.92 on one venue) that the median would smooth into invisibility — matching the OHLC convention used by every TradFi reference rate. The companion gauges `peg_minute_min_bps`, `peg_minute_open_bps` and `peg_minute_close_bps` expose the full bar shape so consumers can reconstruct the within-minute distribution." - "Cross-venue gap (OCB-only number): `max(price across USD-quoted venues), min(price)`, per minute, in basis points. Surfaces the windows where Coinbase quotes $1.0003 while Kraken quotes $0.9978. Stored as `peg_cross_venue_gap_bps`." - "USDT-anchored secondary metric: Binance USDC/USDT, FDUSD/USDT and USDE/USDT are exposed on `peg_deviation_usdt_anchored_bps{venue}` separately so the USD-anchored primary leaderboard is not contaminated by USDT's own peg deviation." - "Time outside band: total seconds in the trailing 24 h during which the per-minute aggregated price fell outside [0.995, 1.005] (±50 bps). Split into `peg_time_below_peg_24h_seconds` (< 0.995) and `peg_time_above_peg_24h_seconds` (> 1.005) because Circle redemption only clears above-peg, so the direction tells you which failure mode is active." @@ -94,7 +95,12 @@ prometheus: # Real metrics emitted by the stablecoin-peg harness: # peg_raw_price{stable, venue, quote} gauge # peg_aggregated_price_usd{stable} gauge -# peg_deviation_bps{stable} gauge +# peg_deviation_bps{stable} gauge (legacy median) +# peg_deviation_worst_bps{stable} gauge (PRIMARY, per-minute MAX) +# peg_minute_max_bps{stable} gauge +# peg_minute_min_bps{stable} gauge +# peg_minute_open_bps{stable} gauge +# peg_minute_close_bps{stable} gauge # peg_deviation_bps_histogram{stable} histogram # peg_cross_venue_gap_bps{stable} gauge # peg_cross_venue_gap_bps_histogram{stable} histogram @@ -118,39 +124,39 @@ providers: tag: Circle, USD-redeemed primary, audited on Kraken + Bitstamp formula: "Median |aggregated_price − $1.00| in bps, where aggregated_price is the per-minute liquidity-weighted median of USDCUSD ticks on Kraken and Bitstamp." queries: - p50: quantile_over_time(0.50, peg_deviation_bps{stable="usdc"}[24h]) - p90: quantile_over_time(0.90, peg_deviation_bps{stable="usdc"}[24h]) - p99: quantile_over_time(0.99, peg_deviation_bps{stable="usdc"}[24h]) - mean: avg_over_time(peg_deviation_bps{stable="usdc"}[24h]) + p50: quantile_over_time(0.50, peg_deviation_worst_bps{stable="usdc"}[24h]) + p90: quantile_over_time(0.90, peg_deviation_worst_bps{stable="usdc"}[24h]) + p99: quantile_over_time(0.99, peg_deviation_worst_bps{stable="usdc"}[24h]) + mean: avg_over_time(peg_deviation_worst_bps{stable="usdc"}[24h]) success: avg_over_time(peg_source_health{stable="usdc"}[24h]) sample_size: sum(increase(peg_source_call_total{stable="usdc"}[24h])) - series: peg_deviation_bps{stable="usdc"} + series: peg_deviation_worst_bps{stable="usdc"} - slug: usdt name: USDT tag: Tether, dominant CEX pair currency, audited on Kraken + Bitstamp formula: "Median |aggregated_price − $1.00| in bps, where aggregated_price is the per-minute liquidity-weighted median of USDTUSD ticks on Kraken and Bitstamp." queries: - p50: quantile_over_time(0.50, peg_deviation_bps{stable="usdt"}[24h]) - p90: quantile_over_time(0.90, peg_deviation_bps{stable="usdt"}[24h]) - p99: quantile_over_time(0.99, peg_deviation_bps{stable="usdt"}[24h]) - mean: avg_over_time(peg_deviation_bps{stable="usdt"}[24h]) + p50: quantile_over_time(0.50, peg_deviation_worst_bps{stable="usdt"}[24h]) + p90: quantile_over_time(0.90, peg_deviation_worst_bps{stable="usdt"}[24h]) + p99: quantile_over_time(0.99, peg_deviation_worst_bps{stable="usdt"}[24h]) + mean: avg_over_time(peg_deviation_worst_bps{stable="usdt"}[24h]) success: avg_over_time(peg_source_health{stable="usdt"}[24h]) sample_size: sum(increase(peg_source_call_total{stable="usdt"}[24h])) - series: peg_deviation_bps{stable="usdt"} + series: peg_deviation_worst_bps{stable="usdt"} - slug: dai name: DAI tag: MakerDAO, audited via Curve 3pool get_dy (on-chain only) formula: "Median |dai_price − $1.00| in bps, derived from Curve 3pool get_dy forward (USDC → DAI) and reverse (DAI → USDC) every 12s over 24h." queries: - p50: quantile_over_time(0.50, peg_deviation_bps{stable="dai"}[24h]) - p90: quantile_over_time(0.90, peg_deviation_bps{stable="dai"}[24h]) - p99: quantile_over_time(0.99, peg_deviation_bps{stable="dai"}[24h]) - mean: avg_over_time(peg_deviation_bps{stable="dai"}[24h]) + p50: quantile_over_time(0.50, peg_deviation_worst_bps{stable="dai"}[24h]) + p90: quantile_over_time(0.90, peg_deviation_worst_bps{stable="dai"}[24h]) + p99: quantile_over_time(0.99, peg_deviation_worst_bps{stable="dai"}[24h]) + mean: avg_over_time(peg_deviation_worst_bps{stable="dai"}[24h]) success: avg_over_time(peg_source_health{stable="dai"}[24h]) sample_size: sum(increase(peg_source_call_total{stable="dai"}[24h])) - series: peg_deviation_bps{stable="dai"} + series: peg_deviation_worst_bps{stable="dai"} # FDUSD and USDe are USDT-anchored stables with no deep USD-quoted # venue, so they have no signal on the primary `peg_deviation_bps` diff --git a/harnesses/stablecoin-peg/cmd/script/aggregator.go b/harnesses/stablecoin-peg/cmd/script/aggregator.go index 22a57279..88416a06 100644 --- a/harnesses/stablecoin-peg/cmd/script/aggregator.go +++ b/harnesses/stablecoin-peg/cmd/script/aggregator.go @@ -184,6 +184,48 @@ func (a *Aggregator) closeMinuteLocked(closedMinute time.Time) { pegDeviationBps.WithLabelValues(stable).Set(devBps) pegDeviationHist.WithLabelValues(stable).Observe(devBps) + // OHLC pass: compute open/high/low/close on the per-minute + // |price-1| series across every venue sample in the bucket, + // in chronological order. The median we computed above is + // what the bench reports as the "typical" value within the + // minute; the high (worst) is what a depeg detector or a + // stress-aware integrator needs to see. A 5-second print to + // $0.92 is invisible in the median, undeniable in the high. + var ( + firstTime, lastTime time.Time + firstDev, lastDev float64 + maxDev float64 + minDev = math.MaxFloat64 + haveAny bool + ) + for _, samples := range byVenue { + for _, s := range samples { + dev := math.Abs(s.price-1.0) * 10000 + if !haveAny || s.receivedAt.Before(firstTime) { + firstTime = s.receivedAt + firstDev = dev + } + if !haveAny || s.receivedAt.After(lastTime) { + lastTime = s.receivedAt + lastDev = dev + } + if dev > maxDev { + maxDev = dev + } + if dev < minDev { + minDev = dev + } + haveAny = true + } + } + if haveAny { + pegDeviationWorstBps.WithLabelValues(stable).Set(maxDev) + pegMinuteMaxBps.WithLabelValues(stable).Set(maxDev) + pegMinuteMinBps.WithLabelValues(stable).Set(minDev) + pegMinuteOpenBps.WithLabelValues(stable).Set(firstDev) + pegMinuteCloseBps.WithLabelValues(stable).Set(lastDev) + } + // Cross-venue gap = max - min in bps. Only meaningful with // ≥2 venues in the same minute. if len(venuePrices) >= 2 { diff --git a/harnesses/stablecoin-peg/cmd/script/metrics.go b/harnesses/stablecoin-peg/cmd/script/metrics.go index 9898940b..96aeb71a 100644 --- a/harnesses/stablecoin-peg/cmd/script/metrics.go +++ b/harnesses/stablecoin-peg/cmd/script/metrics.go @@ -42,7 +42,56 @@ var ( pegDeviationBps = promauto.NewGaugeVec( prometheus.GaugeOpts{ Name: "peg_deviation_bps", - Help: "Current absolute deviation from $1.00 in basis points, computed from the per-minute aggregated price. Drives p99 over 24h via quantile_over_time.", + Help: "Per-minute median absolute deviation from $1.00 in basis points (legacy headline, retained for backward compatibility). The new primary leaderboard ranks on peg_deviation_worst_bps which captures the per-minute MAX, not the median.", + }, + []string{"stable"}, + ) + + // THE new primary headline. Per-minute MAX deviation across + // every venue's samples within the bucket — surfaces the worst + // case the bench observed during the minute. A 5-second depeg + // to $0.92 is invisible in the median (which averages it with + // 11 healthy samples in the same minute) but pops out in this + // gauge. Matches what TradFi reference rates publish as the + // "high" of an OHLC bar. Methodology recommendation from + // Coinpaprika data team. + pegDeviationWorstBps = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "peg_deviation_worst_bps", + Help: "Per-minute MAX |price - $1.00| across every venue sample within the bucket, in basis points. Captures sub-minute depeg wicks that the median smooths away. Primary leaderboard metric.", + }, + []string{"stable"}, + ) + + // Companion OHLC gauges for the same per-minute bucket. open + // and close are deviation in bps of the first / last sample; + // max and min are the bucket's extremes. Together they expose + // the bar shape that a single median value collapses. + pegMinuteMaxBps = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "peg_minute_max_bps", + Help: "MAX |price - $1.00| in bps observed within the most-recently-closed 60s bucket across all venues. Identical to peg_deviation_worst_bps; exposed under both names for legibility.", + }, + []string{"stable"}, + ) + pegMinuteMinBps = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "peg_minute_min_bps", + Help: "MIN |price - $1.00| in bps observed within the most-recently-closed 60s bucket across all venues. Floor of the bar.", + }, + []string{"stable"}, + ) + pegMinuteOpenBps = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "peg_minute_open_bps", + Help: "|first_sample_price - $1.00| in bps for the most-recently-closed 60s bucket. Open of the bar.", + }, + []string{"stable"}, + ) + pegMinuteCloseBps = promauto.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "peg_minute_close_bps", + Help: "|last_sample_price - $1.00| in bps for the most-recently-closed 60s bucket. Close of the bar.", }, []string{"stable"}, )