🚧 SportsPerp is currently live on devnet. Mainnet target: before Jun 12, 2026 (World Cup kickoff).
The OBV IndexReal-Time vs Post-Match

Real-Time vs Post-Match

Our data partner publishes OBV post-match only. The canonical numbers — what professional clubs and national teams consume — are derived after the match ends and full event data is processed. For trading an in-play market, that’s too slow.

SportsPerp runs a parallel real-time pipeline that estimates OBV live during matches, then reconciles with the official post-match value once it’s published. This page explains how.

The three regimes

A market in SportsPerp exists in one of three states at any moment:

RegimeWhenIndex sourceTrader impact
DormantBetween matchesLast batch cycle’s value (unchanged)Price moves only when form/results shift over multiple matchdays
LiveFrom kick-off to final whistleBaseline + real-time OBV overlayPrice moves on every meaningful event (goals, key passes, big saves)
ReconcilingFull-time → T+4h4-hour EMA blend of live estimate and official OBVPrice smoothly converges to the partner’s post-match authoritative value

The transitions between these regimes are the critical design points.

Live regime: the real-time OBV overlay

When a match kicks off, the engine’s live-processor subscribes to the live event feed (GraphQL subscriptions) for that match. Live events come back keyed by live entity ids, which live in a different ID space from the canonical REST ids roster.json is keyed on. The id-bridge translates live → canonical ids fail-closed — any unmapped id drops the event and increments a counter rather than mis-attributing it. The translation is per-match: live-side ids are not guaranteed stable across matches for the same entity. V2 of the bridge is the current production setting and is verified periodically by a systemd timer on the production host.

After translation, every event is annotated with an OBV delta via the obv-engine Python sidecar (XGBoost PV-GF / PV-GA models, deployed on Hetzner port 8100), reachable over HTTP at POST /api/live-obv/matches/{id}/{start,events,end} and gated by the ENABLE_REALTIME_OBV env var. Authoritative team & player totals plus a per-(category × {net, gf, ga}) cross-tab are mirrored into engine/obv-store.ts.

The live index uses a pinned z-score population — the mean and standard deviation from the last batch cycle’s season stats are frozen and reused. Only teams and players actually on the pitch move; every other market’s index is mathematically invariant during the live window. This is a deliberate design choice:

  • Without pinning: one live match’s events would shift the league’s mean/stdev, which would re-z-score every other team/player — creating ghost price movement in markets whose underlying players aren’t even playing. Unacceptable.
  • With pinning: playing teams’ indices move cleanly in response to what happens on the pitch. Non-playing markets don’t move at all. The math is a clean delta, not a re-baseline.

See engine/live-index-overlay.ts for the implementation.

Tiered fallback chain

Live pipelines fail. The obv-engine sidecar can time out, the GraphQL connection can drop, or the upstream event stream can lag. SportsPerp handles this with a three-tier source-priority fallback (fallback-chain.ts):

TierLabelSourceUsed when
1authoritativePer-event OBV deltas from the Python XGBoost sidecarSidecar is healthy and responding within SLA
2aggregatedRunning season-rate estimate scaled by elapsed timeSidecar unreachable but live event stream is working
3heuristicSignal reconstructed from shots, goals, and major defensive actions in the streamGraphQL live stream partially degraded
4frozenLast-known index value, flagged staleAll live sources down — oracle continues to tick but with liveness=false

Each tick carries the source tier in its metadata. Traders and integrators can see which tier is driving the current price — critical for risk management and for any downstream strategy that should throttle during degraded data quality.

Reconciling regime: the 4-hour EMA blend

When full-time is reached and the data partner publishes the official OBV for the match (typically ~30 minutes after the final whistle), the engine doesn’t snap the index to the official value. That would create a discontinuous jump that is both:

  • Bad UX — the index just leapt 3% for no live reason
  • Bad for positions — a jump across a liquidation threshold would trigger spurious liquidations

Instead, the engine smoothly blends from the live estimate to the official value over 4 hours using an exponential moving average:

α(t) = 1 − exp(−λ × t)

blended(t) = α(t) × official  +  (1 − α(t)) × live_estimate

where  λ = ln(20) / 4h ≈ 7.45 × 10⁻⁵ per second

This gives a specific convergence schedule:

Time after officialα (blend weight)What the index is mostly showing
0 min0.00100% live estimate
30 min0.3333% official, 67% live
1 hour0.5353% official, 47% live
2 hours0.7878% official — dominated by official
4 hours0.9595% official — converged
8 hours0.998Effectively fully converged

The constants are defined in engine/types.ts: EMA_LAMBDA = Math.log(20) / (4 * 3600).

Single-update cap

Blending alone isn’t enough. If the live estimate diverged sharply from the official value (rare but possible), even a gradual EMA blend could move the index faster than is comfortable for leveraged traders.

SportsPerp caps any single blended update at 3% of the previous value (EMA_MAX_CHANGE_FRACTION = 0.03):

const maxChange = Math.max(1.0, Math.abs(previousValue) * 0.03);
if (Math.abs(blended − previousValue) > maxChange) {
  blended = previousValue + sign(blended − previousValue) × maxChange;
}

This means a large live-vs-official gap resolves over multiple crank cycles rather than a single jump. Combined with the 5-minute crank cadence, the worst-case single-tick move is bounded; sustained convergence unfolds over the 4-hour window. See engine/ema-blender.ts.

Why this matters for traders

Three practical consequences:

  1. Live matches are the high-signal window. Most price discovery happens during live matches when the overlay is active. Between matches, indices drift slowly on form updates from completed fixtures.
  2. Post-match settlement is smooth. A position you hold through a match will not be jolted by a sudden snap-to-official at full-time. The index you see at 90+5 and the index you see at T+4h are connected by a continuous curve, not a step function.
  3. Data-source transparency. Every tick is labeled with its tier (authoritative / aggregated / heuristic / frozen) and its blend progress (for post-match reconciling positions). Integrators and sophisticated traders can gate their own strategies on these flags.

Further reading

  • Composite Index Design — the underlying formula that both live and batch paths feed.
  • Data Pipeline — the REST and live-event sources that drive each regime.
  • Mark Price — the 150-second EMA blend of oracle and vAMM that actually settles your PnL.