StatsHub scorer pipeline · Stage 4

What does today_events.py actually do?

It finds upcoming fixtures, gathers auditable player and Bet365 inputs, estimates each player’s conditional scoring probability, adjusts that estimate for the match environment, and prints only candidates that pass a conservative value policy.

Bet365 onlyserial HTTPraw snapshotsno automatic betsuncalibrated baseline

The one-line mental model

fixture → valid inputs → player xG/minutes model → team-market context → Poisson probability → EV filter → audited output

The script is a candidate finder, not a betting executor. It never places a bet and it does not claim that a displayed EV is calibrated profit.

End-to-end flow

fixturesby datevalidatedquotes/historyplayerexpectationteam-marketcontextprobabilityand EV qualifyingcandidates + audit
data/model stagedecision/output

1 · Choose the fixture set

1

Date window

The positional argument is a day offset: 0 means today, 1 tomorrow, through 6. The script calls /event/by-date, then keeps events with odds available.

./today_events.py 1 --snapshot-dir snapshots
Failure is visible: if the event list cannot be loaded, the script raises an error instead of publishing an empty ranking.

2 · Build trustworthy inputs

Competition history

Resolves the fixture’s exact tournament and season, then filters performance rows to finished matches before kickoff for the correct team and competition.

Player quotes

Matches players by ID first, uses unique names only as a fallback, and accepts valid Bet365 anytime-scorer prices only.

Freshness

Rejects missing, stale, future, malformed, or timezone-less quote timestamps. Unknown settlement rules remain warnings.

Lineups

Fetches a starting XI only when the provider explicitly says the lineup is confirmed. Otherwise historical participation is used and labelled uncertain/likely.

Snapshots

Stores compressed raw responses, request attempts, hashes, warnings, candidate rows, policy, and summary metadata under a unique run directory.

Retry safety

Requests are serial. HTTP 429/5xx and transient transport failures get bounded retries with preserved attempt-level evidence.

3 · Estimate the player’s goal intensity

For each quoted player, Stage 2 creates a regularized estimate rather than multiplying goals, odds, and arbitrary position bonuses.

Recent xG rate

Uses recency weights with a five-match half-life. Non-penalty xG is preferred; missing xG is not silently converted to zero.

Empirical prior

Uses competition/position priors with leave-one-player-out logic. A 450-minute prior shrinks small samples toward broader evidence.

Playing time

Estimates appearance probability and minutes conditional on appearing. Confirmed starters and non-starters use different historical minutes.

unadjusted λ if playing = regularized xG/90 × estimated minutes if playing / 90

This λ is an expected goal count, not yet a probability of scoring.

4 · Adjust for the match environment

Stage 4 avoids the removed hand-written forward/midfielder/defender multipliers. It reads paired Bet365 team-total over/under markets for the exact team name.

De-vig the market

For each half-goal line, normalizes 1/Over and 1/Under to remove the two-way overround.

Fit team goals

Inverts the fair over probability to a Poisson team-goal mean. Requires at least two complete half-goal lines and rejects inconsistent fits.

Compare history

Compares the market mean with recency-weighted historical team xG, then shrinks the ratio toward neutral using a reliability factor.

raw ratio = market team goals / historical team xG
context multiplier = exp(reliability × log(raw ratio))
contextual λ = unadjusted λ × context multiplier
No manufactured boost: missing, sparse, non-positive, or implausible context rejects the candidate instead of defaulting to a favourable multiplier.

5 · Convert λ into value

For a player who appears, scoring is modelled with a Poisson baseline:

P(score | plays) = 1 − exp(−λ)
fair odds = 1 / P(score | plays)
edge = P(score | plays) − 1 / Bet365 odds
EV if settled = P(score | plays) × odds − 1
EV per placed stake = P(plays) × EV if settled

Uncertain participation is kept explicit:

P(win) = P(plays) × P(score | plays)
P(lose) = P(plays) × (1 − P(score | plays))
P(void) = 1 − P(plays)

The default policy requires EV per placed stake ≥ 5%, prior share ≤ 65%, medium/high quality, likely or confirmed starter status, complete xG history, valid context, and no unsupported settlement rule.

6 · Read the output

🎯 Qualifying anytime-scorer value candidates
• Player (F) - Score if plays: 42.3% | Fair: 2.37 | Bet365: 4.333 | Edge: +19.2% | EV/bet: +83.2% | Play: 100% | win/lose/void: 42.3%/57.7%/0.0% | λ if plays: 0.550 (context 1.40x; market/historical team goals 1.36/0.89) | Quality: medium | Settlement: non_runner_void_assumed

Probability vs fair

“Score if plays” is conditional. “Fair” is the decimal price corresponding to that conditional probability.

EV is an estimate

A high EV means the model’s point estimate exceeds the market price. It is not a confidence level and has not passed calibration yet.

Warnings matter

settlement_rules_unknown means non-runner void/refund is assumed, not verified. Quote timestamps may be response-level rather than bookmaker-level.

What it does not do

Does not place bets

It prints and publishes text only. There is no staking or execution logic.

Does not prove profitability

Stage 5 is collecting untouched pre-kickoff predictions and post-match outcomes for calibration and paper tracking.

Does not know every rule

Bet365 non-runner settlement and team-market quote timestamps remain incomplete inputs.

Does not use concurrency

Prior experiments caused HTTP 429s and incomplete results. The current retry policy is bounded and serial.

Audit trail

Every candidate—including rejected ones—is written to candidates.jsonl with the player/event IDs, selected quote, model output, match context, decision reasons, and raw-source links. Stage 5 additionally hashes the candidate file and separates model revisions by source and policy fingerprints.

today_events.py → snapshots/<UTC-run-id>/
├── run.json # policy, source hashes, HTTP policy
├── requests.jsonl # requests + every retry attempt
├── *.response.gz # original provider bodies
├── warnings.jsonl # coverage/data warnings
├── candidates.jsonl # accepted and rejected decisions
└── summary.json # completion, counts, file hash