Skip to content

World Cup market portrait

For market portraits, this public repository owns the PMXT acquisition, prediction-market semantics, provider-neutral story construction, and the oddsfox.market-portrait.v1 file contract. Mart contracts for bundle inputs are in Data contracts. Production bundles are private operator artifacts, not repository inputs.

Private collection, source-native schemas, sanitation implementation, and rendering do not belong here. The public API accepts only neutral, sanitized facts; it must not import a private collector package, name a private source, query a private source relation, or contain plotting and video-rendering implementation.

Acquire an approved target

Build the normal World Cup market working set, then create a review candidate:

uv run python scripts/generate_polymarket_wc2026_market_portrait_target.py \
  --fifa-match-id 104

Generation calls Gamma for fresh identities but does not consume PMXT credits. Review the ignored YAML, then authorize the resumable book-and-trade scan:

make market-portrait-live-backfill \
  TARGET_MANIFEST=/absolute/path/to/match-104.yml

The manifest must resolve exactly three literal Yes tokens for a group match, or the named home and away tokens of one advance/win market for a knockout match. Ambiguity, changed Gamma identity, unfinished adaptive windows, a total-zero trade result, invalid decimal values, or conflicting provider order blocks publication. Books and trades share the UTC-month PMXT credit counter.

Build a bundle

build_market_portrait_bundle accepts a read-only DuckDB connection, a FIFA match ID, sanitized MatchFacts, ordered sanitized FootballEvent values, an output root, and a RenderProfile. It never imports collector code, names a private upstream provider, or accepts raw provider relations. The private adapter is responsible for sanitizing and mapping source facts to these neutral types.

The output directory contains:

  • manifest.json
  • book_states.ndjson.gz
  • trades.ndjson.gz
  • story.json

JSON keys and stream order are stable, gzip uses mtime=0, and the bundle ID is derived from content. A byte-identical rerun is a verified no-op. Existing content at the same immutable path is never overwritten. The manifest records book and trade aggregate scan hashes alongside each file hash and record count.

Timeline mapping requires actual start and end timestamps for every played period. MatchFacts.kickoff_at_utc and every period boundary must be timezone-aware and sanitized. Before story construction, export verifies the kickoff against the validated match working set and verifies that each required role's published root scan window strictly contains the complete football timeline. Scheduled kickoff is a validation anchor and is never substituted for missing actual boundaries. The declared deterministic sanitizer can move equal raw timestamps independently by one microsecond, so match_ended_at may precede the final period boundary by at most two microseconds; any larger inversion blocks publication.

Each played period is tiled with half-open UTC bands [S, E). Regular bands are exactly 60 seconds; only the last band is clamped to the remaining positive period duration so that it ends at the actual boundary. The one-millisecond tolerance prevents a timestamp micro-epsilon from creating another band. Missing stoppage labels are inferred as ceil(max(0, actual duration - nominal duration - 1 ms) / 60 seconds), then combined with any explicit event stoppage by taking the greater count. An event whose labelled band has no positive source duration blocks publication.

Sanitized event scores are post-event facts. The builder derives display scores in football-timeline order, accepts only non-revoked, non-shootout Goal, Own goal, or Penalty scored events as scoring transitions, and requires each transition to add exactly one goal to one team. Non-scoring annotations receive the derived chronological score rather than trusting a possibly stale source score. A score checkpoint becomes effective at its event band's end, and the derived terminal score must agree with MatchFacts when supplied.

Event reactions are explicitly labelled minute-aligned. For an event band [S, E), before is the last observation strictly before S, and primary after is the first observation at or after E. Extended after uses the following band end. Observations cannot cross a halftime or extra-time break; missing qualifying observations are serialized as null. Shootout events receive annotations but no reaction metric. Because market observations use integer milliseconds while sanitized football boundaries may retain microseconds, both the bisect threshold for < S and the lower bound for >= E use the ceiling millisecond; the same-period upper bound uses the floor millisecond. These directional rules preserve the real datetime predicates rather than truncating them. Producer validation checks the derived band tiling, annotation mapping, score checkpoints, reaction event-role inventory, bounds, and observation predicates before publication.

The default story begins at the actual first-half boundary with elapsed minute zero. Every football band has equal video weight. Regulation flows continuously for 45 seconds; extra time extends the story to 60 seconds and a shootout adds one five-second PENS phase. Pre-match, halftime, and post-match remain zero-valued render defaults and are not emitted as timeline segments. The source clock jumps over each validated period break rather than interpolating through it.

Recovery and retention

Interrupted PMXT work resumes from terminal window leaves. Preserve a completed warehouse until the bundle is verified. Older warehouse layouts require a rebuild; see Terminology.

Keep source bundles inside the caller-managed private export root. Copy an MP4 out only after the operator completes rights and provenance review.