OpenAthlete
AGPLv3 endurance training platform: EU-oriented hosting, transparent CTL/ATL/TSB logic in code, self-hosting, and full export. Built in Grenoble.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
Game predictions and player prop projections for MLB, NFL, NBA and NHL,
rebuilt every fifteen minutes by GitHub Actions and served as a static site.
Live site: open index.html (or the per-sport pages mlb.html, nfl.html,
nba.html, nhl.html).查看完整 README ↗Game predictions and player prop projections for MLB, NFL, NBA and NHL, rebuilt every fifteen minutes by GitHub Actions and served as a static site.
Live site: open index.html (or the per-sport pages mlb.html, nfl.html,
nba.html, nhl.html).
Laid out like a sportsbook. A row of sport pills across the top, then four sections. Featured is the lobby: what is in progress, the strongest game picks, the prop plays with the biggest edge, and how yesterday went, each pointing at the section that holds the rest. Games is the full board, with a rail for live, today, tomorrow, the next seven days or everything scheduled. Player Props is the whole slate's board, with a rail for open, in-progress, graded or all, and under it the market categories a book groups its props by — passing, rushing, receiving, touchdowns in football; batting and pitching in baseball. Open a game and the same category chips narrow that game's props. Results holds the past, with a rail for the recent games and the full track record.
Every section has one rail deciding what it is about, a category strip where
markets need grouping, and a fold-away Filters bar for narrowing further.
Every count on a rail or chip is the number of rows that choice will show.
Addresses say exactly what is on screen, so #mlb/games/live and
#mlb/results/track are worth sharing, and the older #mlb/today,
#mlb/upcoming and #mlb/record links still land in the right place.
Game predictions. Every scheduled game gets a win probability, a confidence tier, and a plain-language breakdown of what is driving the pick — Elo edge, recent form, scoring margin, rest, home/road splits, head-to-head.
Player props. Open a game, pick a side, pick a player, and you get their most-shopped markets: hits, total bases, home runs, RBIs and strikeouts in baseball; points, rebounds, assists and threes in basketball; passing, rushing and receiving lines in football; shots, points and saves in hockey. The line is the sportsbook consensus (the median where several books post the market, via The Odds API) and the projection is the site's own: the player's season rate adjusted for the opponent, the expected game script, home/away and the injury report, corrected by what the graded ledger has learned. Each prop shows both, a likely range, and the over/under probability of that projection against the book's line. A market the books have not posted yet stays blank and fills in on a later run. Once the game ends every prop is marked correct or incorrect against the box score. Football boards cover offensive players only. A league-wide Player Props board ranks the whole slate.
Predictions are frozen at kickoff. The first forecast published for a game is written to a ledger and is what the site shows from then on, so a finished game's probability never drifts and the favourite can never quietly change to the team that won.
Honest accuracy reporting. Results shows the verified record: games whose forecast was published before they started. Exhibition games are excluded everywhere, and so are games the site first saw after the final whistle. Track record adds the breakdown by confidence, sport, team, player and prop type, and, under "Under the hood", out-of-sample accuracy, Brier score, log loss and a calibration table.
ESPN ──▶ ingest ──▶ archive + ratings + model ──▶ predictions (frozen)
└──▶ injuries + player pool ──▶ props (graded) ──▶ static site
Everything is standard-library Python; there is nothing to install and no R.
sportspred/ingest.py) pulls standings, per-team statistics
and the schedule window from ESPN into a per-league CSV, computes the
standings-model prior, and snapshots every team's standings line for the
day. The window is set by SP_LOOKBACK_DAYS and SP_FORWARD_DAYS. A feed
outage keeps the previous CSV rather than blanking the site.sportspred/learn.py) folds each window's completed games
into history/<league>_games.csv. The ingest only ever sees a few weeks;
the archive is what gives the ratings a season-long memory.sportspred/ratings.py) replays the archive through Elo with a
margin-of-victory multiplier and between-season regression. Ratings are
always pre-game.sportspred/features.py) builds one vector per game from
games that finished before it: Elo edge, season-to-date Pythagorean, recent
form and scoring margin, home/road split, rest, back-to-backs,
head-to-head.sportspred/model.py) trains a ridge-penalised logistic
regression, picks the penalty and the Elo/model blend by walk-forward
validation, and recalibrates with Platt scaling.sportspred/props.py) turns each player's season line into a
per-game rate, adjusts it for the opponent, the projected game environment
and the injury report, and prices it against the sportsbook line
(sportspred/odds.py) with a Poisson, negative binomial or normal
distribution. In baseball the two probable starters' ERAs also shift the
game's standings prior.history/<league>_ledger.csv and
never rewritten. Later runs read it back rather than recomputing.sportspred/render.py) writes a data file per league plus a
few kilobytes of HTML shell.Every game line carries three numbers, the way a sportsbook writes them: who wins, by how much, and how high-scoring it will be.
The total comes from scoring rates (sportspred/scores.py). Each team
carries what it has scored and conceded per game, pulled toward the league
average by ten games of prior so a hot fortnight does not run away with it,
and an expected score is the league average scaled by how good this attack is
and how leaky that defence is. Scoring is far noisier than winning, though,
and in some sports there is almost nothing to know: against simply guessing
the league average, the estimate is worth about 3% on NFL totals and 0.3% on
baseball ones, where run scoring at the team-game level is close to
irreducible. So the published number is a blend of the estimate and the
league average, and each league's blend is fitted walk-forward on its own
archive. A league where the estimate earns its keep gets it in full; a league
where it does not falls back toward the average, which is the honest answer;
a league with no record yet publishes no total at all rather than putting the
same league-average number on every card.
The spread is not a second model. Subtracting the two expected scores gives a margin, and it is a reasonable one, but it is another opinion about who wins — and on the archive it disagreed with the win-probability model about the favourite in 15% of NFL games and 30% of baseball ones. A card whose spread says Seattle and whose moneyline says San Francisco is the site arguing with itself. So the spread is derived from the win probability, which cannot contradict it: if margins in a league scatter by fourteen points, a 62% favourite is favoured by about four and a quarter. That is also how a book moves between a moneyline and a spread, and on the archive it was slightly the more accurate of the two (10.07 against 10.22 points of average error in the NFL, 3.33 against 3.34 in baseball). The expected scores shown on the card are rebuilt from the published total and spread, so all three numbers always add up — and the build gate refuses a card where they do not.
Neither number can have a hit rate, and the site does not pretend otherwise. Our own number is the line, so a game landing over it is not us being right or wrong; over enough games an honest total is cleared about half the time. What gets reported instead is how far off it has been, which way it leans, and the error of the league average it is measured against. Both are frozen at kickoff alongside the moneyline, for the same reason: they are built from season scoring rates, and once a result lands in those rates the number would drift.
The model is refit on every run and the standings model behind it is recomputed from current standings. Left alone, that means a finished game's probability keeps moving — and once the result is in the standings, the model can end up naming the winner as the team it favoured all along. A prediction that changes after the fact is not a prediction, so the first one published is the one the site is held to.
Exhibition games are dropped from the ratings, the training data, the archive,
the results list and the accuracy figures. They are played by rosters that will
not take the field once the season starts, so a preseason result is not
evidence about anybody. They still appear on the schedule, badged
PRESEASON. Season type comes from ESPN, with a per-league date rule as a
fallback for older rows.
Games the site first saw after they finished are shown but never counted. Their pick was made with standings that already contained the result, so it would score near-perfectly and prove nothing. That is what the backtest figure is for instead.
The previous model reported about 58.6% on MLB. That figure was measured by scoring past games with end-of-season standings — it knew how the season turned out. Rebuilt so that each game is scored only with information that existed beforehand, the honest out-of-sample number is lower. Nothing got worse; the measurement got truthful. The published pick is an ensemble of the learned model and the standings model, and the Model tab reports what it actually achieved.
Three mechanisms, all persisted to the repository so every run starts from what the last one learned:
history/<league>_ledger.csv before kickoff and graded afterwards. This
is the only measurement in the system that hindsight cannot flatter. Once it
holds enough graded games, it — not a fixed schedule — sets how far to lean
on the learned model versus the standings model, and recalibrates the output.sportspred/optimize.py). Every run walks the
neighbourhood of the settings in use — the form window and how fast older
games fade, which signals feed the form model (each core one can be
dropped, each extra one added), and the rating engine's speed — scoring
each candidate by walk-forward log loss, plus one random combination seeded
by the hour so a season of runs explores settings coordinate descent would
never reach. The winner is the challenger the rule above judges. On the
props side, once 150 graded props are on file the ledger fits a
recalibration of prop probabilities and a blend speed against the
sportsbook (how many games a player needs before our number outweighs the
book's), each adopted only when it lowers held-out log loss. The Track
Record page's "Under the hood" panel shows what has been tried and kept.history/<league>_props.csv and graded against the final box score. Once a
market has thirty graded props, its projections are corrected for
systematic bias and for outcomes scattering more or less than assumed,
phasing in with sample size. The Model tab shows the hit rate per market.history/<league>_team_stats.csv records every
team's standings line each day, so historical games can eventually be paired
with the standings as they stood that morning — the leak-free version of a
season-statistics model.The line shown is the median across books of each book's main line (the two-sided point with the most balanced prices). A line posted by a single book that sits far from our projection (more than 60% of the number away, or two and a half standard deviations) is an alternate line, not the market, and the prop stays blank until a real line appears. Lines are fetched within 36 hours of kickoff for the daily sports and refreshed every four hours; football lines are fetched from five days out and refreshed daily until the game is within 36 hours, since the books post them early in the week.
Confidence tiers are the same for game picks and props: 66% and up is high, 57% to 65% medium, anything closer to a coin flip low.
Every player the books have posted a line for makes the board, up to twelve a side. Each market has a minimum projection below which the site will not invent a line of its own (a receiver averaging half a catch gets no receptions prop), but that minimum is a relevance filter, not a judgement: where a real line exists it is priced whatever the projection, because the market has already decided the player is worth pricing. A player who has not taken the field, with no previous season to lean on, is still left off.
Every run reads its own output back before anything is pushed
(sportspred/verify.py). It checks that each page loads the stylesheet and
script that were built with it, that every league payload carries the blocks
the page reads, that every game has a pick, a date and an identity, that
every prop's lean is on the same side as its own projection, that no
projection exceeds what a player could post, that a blank prop really has no
line and a priced one has a real one, and that every market sits in exactly
one category chip. A failure exits the build non-zero and the workflow
publishes nothing, so a bad build costs one skipped refresh instead of
showing wrong numbers until somebody notices. The skip_verify workflow
input forces a publish if the gate is ever wrong.
The checks run against the freshly built output, never against what is already on disk: the test suite runs before the pipeline, so judging the previous build there would mean the first change that adds a new block would fail the tests, stop the run, and leave the site unable to rebuild out of it.
The gate above judges the data. scripts/check_pages.py judges the pages it
is poured into: it opens the built site in a real browser at desktop and
phone width, visits every league and section, and fails if a page throws,
renders nothing, shows undefined where a value should be, scrolls
sideways, claims a count on a rail that the list underneath does not match,
or opens a section somewhere other than its own front door. That last one is
the check that would have caught the Results tab remembering Track record
and so showing no finished games at all.
python3 scripts/check_pages.py # the site in this directory
python3 scripts/check_pages.py --dir out # a site built somewhere else
It needs Playwright and a browser, which the hourly refresh deliberately does not — that build is standard-library Python with nothing to install — so it is run by hand, or by the daily review, after a change to the front end. Without Playwright it says so and exits 0.
docs/RUNBOOK.md is the operating manual: the five promises the site makes
to a reader, how the pieces fit together, the instruments, the standing
conditions that look like faults but are not, and every failure mode that
has actually happened with its cause and its fix.
scripts/health_check.py is the instrument it points at first. The build
gate asks whether a build holds together; this asks the different question —
is what readers are looking at right now current, correct, and still
honouring those promises. It checks that the boards are fresh, that no pick
was first recorded after its game started, that finished games can actually
be reached in Results, that the ledgers hold no duplicates or verdicts
without results, that a board for a game under way is frozen, that no market
is being published which can never be scored, and that the refresh is still
pushing.
python3 scripts/health_check.py # everything
python3 scripts/health_check.py mlb nfl # only these leagues
Findings come at two levels: a PROBLEM is something a reader would be wrong to trust, and exits non-zero; a WATCH is worth knowing and not worth waking anyone for. Its first check is whether the checkout matches what is published, because every other check reads the working tree — a stale checkout reports a stale site, which is the easiest way to spend an hour chasing a fault that does not exist.
The site-doctor agent (.claude/agents/site-doctor.md) does this
end to end: it reads the runbook, runs the instruments, diagnoses, fixes,
tests and confirms, and is deliberately scoped to breakage rather than to
features or model accuracy.
A refresh takes about five minutes. If a change to the site's code lands
while one is running, that run's pages were produced by the older code, and
pushing them would overwrite the new ones; that happened once and left the
pages a revision behind until the next refresh. .github/scripts/publish.sh
now checks, before publishing, whether main has gained any code, and if it
has, rebuilds on top of it while carrying over the ledgers and model state
the run produced.
Two refreshes can also overlap and both try to push. Rebasing one over the
other conflicted on every generated file they had both written, and the
conflict left the checkout detached mid-rebase, so all four retries died and
the refresh published nothing. Publishing no longer rebases: this run's
pages and payloads are the fresher build and simply win, and only the
ledgers are merged, by scripts/merge_history.py. That matters because a
pick is written once, when it is first published, and never rewritten — a
row only the other run has is one this run cannot reproduce, and losing it
would mean republishing that pick later, possibly after kickoff, which is
the one thing the ledger exists to prevent. A board the other run froze also
wins over an open one, for the same reason.
A statistics feed occasionally sends a corrupt row (a hitter at sixteen hits
a game). Any prop priced from such a row stays in the ledger as published,
but it is void wherever a verdict is counted or shown: the record, the
per-market and per-player tables, the corrections and the calibration. The
page shows it as "VOID · bad feed data", the way a book voids a bet on a bad
line. The cap that catches these rows (PER_GAME_MAX) also stops new ones
from being published.
The site is rebuilt every fifteen minutes, but the page itself keeps up between rebuilds. While games are on it polls ESPN's public scoreboard and box score feeds every half minute and shows the score and clock on each game, the number so far against the line for every chosen prop (how much is still needed, or that it has already cleared), and an "on track" count per player and per game. The moment a game goes final the page grades the game pick and every prop from the final box score and moves the game to Results, tagged "confirming" until the next rebuild writes the same verdict to the ledger. Every ten minutes it also re-fetches the data files, so a rebuild shows up without a reload. Nothing about a pick moves: lines, leans and probabilities were frozen at kickoff; only the outcome fills in.
python3 run_pipeline.py # every league, with player props
python3 run_pipeline.py mlb nfl # only these leagues
python3 run_pipeline.py --no-props # skip the player-statistics fetch
python3 run_pipeline.py --no-tune # reuse the stored Elo parameters
python3 -m unittest discover -s tests # the test suite
Only the standard library is required.
Sportsbook lines need an API key from The Odds API:
set ODDS_API_KEY locally, and add it as a repository secret of the same
name so the refresh can use it. Lines are fetched for games within
36 hours of kickoff and re-fetched every ODDS_REFRESH_HOURS (default 4)
so newly posted players fill in; each fetch costs one credit per market,
and the client stops with 40 credits in reserve. Without a key the board
prices against lines derived from each player's season baseline and says
so.
A wider one-off backfill of past results:
SP_LOOKBACK_DAYS=120 python3 run_pipeline.py mlb
| Path | What it is |
|---|---|
run_pipeline.py | Entry point: model, props, site |
sportspred/ | The engine (see the pipeline steps above) |
sportspred/scores.py | Expected scores, and the total and spread that follow |
assets/app.css, assets/app.js | Shared front end, cached across all four sports |
data/<league>.js | Per-league payload the page renders from — one JSON object behind a window.SP_DATA[...] = assignment |
data/<league>-history.js | Older graded games, loaded only when Results is opened |
history/ | Game archive, prediction ledger, props ledger, team-stat snapshots — the long-term memory |
model_state/ | Tuned parameters, Elo snapshot, run log |
tests/ | Test suite |
scripts/check_pages.py | Browser check of the built pages (run by hand) |
scripts/merge_history.py | Folds an overlapping refresh's ledger rows into this one's |
scripts/health_check.py | Is the published site current, correct and honest right now |
docs/RUNBOOK.md | Operating manual: promises, instruments, failure modes |
.claude/agents/site-doctor.md | The agent that diagnoses and fixes breakage |
These are model projections, not betting advice. Prop lines come from the sportsbooks where a market exists; the projection, lean and probability are the site's own read against that line, and the graded record on the Model tab is the only measure of how good that read has been. Check the actual market before acting on anything here.