尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Live Fantasy Premier League dashboard: xG, bookmaker-implied fixture strength and an expected-points model, rebuilt every three hours
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗A live dashboard for a Fantasy Premier League squad and mini-league. It shows the numbers behind the points (expected goals, expected assists, defensive contributions, bookmaker-implied fixture strength) and turns them into decisions: who to captain, who to sell, when to play a chip.
Live: https://fpl-insights-jm.vercel.app (scheduled to rebuild every three hours; actual timing can vary)
| Planning | Mini-league |
|---|---|
FPL's own site tells you what happened (points) but not why, and not whether it will happen again. A defender with 15 points from one lucky goal and a midfielder quietly racking up chances look the same in the points column. I wanted the underlying numbers per match, in one place, next to the decisions they should inform, and I wanted my mini-league's results explained rather than just ranked.
It also became a real exercise in working with messy public data: four sources with no shared keys, an expected-points model built because FPL's own projection is unusable (26 distinct values across 515 players, capped at 4.0), and a pipeline that has to keep running unattended.
Built with Claude Code as a pair programmer.
python cli.py dashboard --open the whole thing as a web page
python cli.py team your 15, with per-match xG/xA
python cli.py league how your rivals are doing, and why
python cli.py player szoboszlai one player, in detail
python -m pytest the tests
config.json holds the FPL ids to report on. The committed one points at my
own team and mini-league, so everything above runs straight after a clone.
To point it at a different team:
{
"entry_id": 3385667,
"league_id": 1453141
}
Your entry_id is the number in the URL when you view your own team
(/entry/<id>/event/1/). The league_id is the number in a mini-league URL.
Both can also be passed as --entry / --league.
Responses are cached in .cache/ for 15 minutes. --ttl 0 forces a refresh.
python cli.py dashboard --open writes dashboard.html and opens it. Re-run it
whenever you want current numbers. --artifact also writes a complete HTML copy,
dashboard-artifact.html, which is what the live site serves.
Four tabs:
The design system is in docs/design.md: FPL purple #37003c,
night #16001a, green #00ff87, white and a paper ground, Noto Sans with
tabular figures, and a generated icon set packed into one embedded sprite
(scripts/pack_icons.py). Nothing on the page means something by hue alone:
gains carry a sign or arrow, states change fill, weight or shape, and fixture
pills print the opponent and the number on a dark-to-green ramp that stays
readable in greyscale.
The dashboard is something you go and look at. The deadline is the moment
that matters, so a companion bot, in a separate private repo
(fpl-lineup-alert), pushes to my phone through ntfy
instead:
sub spence for hall or captain haaland runs a workflow that
makes the change on the real FPL team. Only the repo owner's comments count,
and transfers are deliberately left out because they cost points and cannot
be undone.The two repos share data, not code. Each build here publishes
projections.json (next gameweek's expected points for every player) beside
the page, and the bot fetches it from the live site, falling back to points
per game if it is missing or for the wrong gameweek. The bot is kept private
because it holds an FPL login and can change the team.
fplapi.py FPL API client: retries, backoff, 15-minute disk cache
analysis.py per-player reports, findings, the expected-points model
odds.py Pinnacle odds -> expected goals and clean-sheet chances
pulse.py Premier League Opta stats, joined on Opta id
ffs.py Fantasy Football Scout predicted line-ups
elite.py ownership among the top of the global league
transfers.py transfer and pair suggestions
chips.py chip planner
captaincy.py captaincy model
scout.py the Scout tab's player pools
ticker.py fixture ticker and runs
dashboard.py page assembly: build() gathers data and cards, render() wraps them
cards_*.py one module per tab (squad, planning, league, scout) plus shared helpers
components.py chart-like building blocks shared across cards
dashboard.css/.js the page's styles and script; the other .js files are per-feature
assets/ the packed icon sprite (scripts/pack_icons.py rebuilds it)
cli.py the command-line entry point
Bookmaker odds: Pinnacle's public guest API, no key. Pinnacle runs on low
margins and high limits, so its line is close to a market consensus. The total
line nearest even money gives the match's expected goals, the handicap line
nearest even gives the supremacy, and those two solve for each side's expected
goals. A clean sheet is then the opponent failing to score under a Poisson
model, exp(-m). That last step is an assumption (Poisson slightly understates
0-0 draws), so it is labelled as derived, not quoted, wherever it appears.
Predicted line-ups: Fantasy Football Scout's team-news page. FPL's API says
nothing about who will start. FFS renders each predicted starter with a photo
whose filename is the same id FPL exposes in element["photo"], so the two
join on a number, not a name. If the page fails to parse, the feature reports
nothing rather than marking everyone benched.
Premier League Opta stats: footballapi.pulselive.com, the open API behind
premierleague.com. Big chances created and missed, shots, shots on target,
dribbles and touches, none of which FPL shows. The join is exact: Pulselive
returns altIds: {"opta": "p223094"} and FPL carries the identical string in
element["opta_code"]. Two quirks: ids come back as floats and must be cast to
int for URLs, and the ranked endpoints only include Opta ids when asked with
altIds=true.
Elite ownership: FPL's global league (id 314) is public and ranked, so the
top managers' squads can be read like a mini-league rival's. Ownership among
that group separates "popular" from "popular with people who are winning".
Early in a season, though, the current top of that table is mostly a
survivorship sample: two or three lucky captaincy picks, not evidence of
judgement. So the reference group is "proven" managers: of the current top
250 (--elite N to change it, 0 to skip), only those who finished inside the
top 100,000 last season count. That buys a season-plus of track record at the
cost of a smaller sample, so every ownership figure carries its 95% margin of
error, and a gap smaller than twice that margin is treated as no difference.
Price-change forecasts: FPL's own price_change_percent and
price_change_projections, which the site does not surface. Positive rises,
negative falls, and the change fires at 100.
Sources checked and rejected: FBref (403), FotMob (404), Sofascore (403), and Fantasy Football Scout's projected points and DefCon tables, which show names to anonymous visitors but strip the numbers (that is their paid product).
GitHub Actions builds it (.github/workflows/build.yml) on code changes and
on a three-hour schedule. It runs the regression tests first, then commits
dashboard-artifact.html and projections.json if they changed. Optional source
failures appear in the run log and on the page. Every run ends with a source-health
block, so a broken scrape shows up as a FAIL line rather than quietly vanishing.
Three-hourly rather than twice a day because GitHub's scheduler is best-effort: the very first scheduled run here was skipped outright, with no error, leaving an eight-hour-old page live. Eight chances a day makes a dropped run a non-event, and runs where nothing changed exit without committing. Prices change around 01:30 UK, so early builds catch them; predicted line-ups sharpen through the day, so later builds land before a typical 18:30 deadline.
Vercel deploys it. The Vercel project is linked to this repo, so every build that changes the page triggers a fresh deploy with no extra step. This replaced a second scheduled job that republished the page twice a day and quietly stopped running with no error. Deploying off the git push removes that failure mode rather than monitoring it better. If the GitHub Action stops firing, the page warns once its snapshot is six hours old. Scheduled runs can be delayed or skipped, so three hours is a requested cadence, not a freshness guarantee. Fatal build errors and failed regression tests prevent publication, preserving the last valid page.
The built HTML is committed. A 2.2 MB page eight times a day sounds expensive, but the embedded images are byte-identical between builds, so git deltas them away. Measured across two consecutive builds: zero growth.
Per-match history is current season only. element-summary gives one row
per fixture for this season, but only totals for earlier seasons. Those totals
are used as a baseline while the current season is short. For older per-match
data, github.com/vaastav/Fantasy-Premier-League archives it as CSV per
gameweek from 2016/17.
Defensive contribution. FPL awards 2 points for hitting a threshold in a
match: 10 for defenders (clearances, blocks, interceptions, tackles), 12 for
midfielders and forwards (the same plus recoveries). The API's
defensive_contribution field is already composed correctly per position
(verified against the raw components rather than assumed), so only the
threshold differs.
Early season. Per-90 rates over one or two matches are noise. The report says so when players are under 270 minutes and falls back to last-season baselines.
FPL's xG is Opta's. Understat runs a different model and will not agree: for 2025/26 Haaland was 25.50 xG by FPL and 28.80 by Understat. The two are never mixed in one number.
curl cannot reach the FPL API from my machine: the TLS handshake fails
(curl exit 35) while Python's urllib succeeds against the same host, so
everything uses urllib.Squad, bank and free-transfer estimates describe the last public picks. Moves already made before the next deadline are not visible without signing in. Transfer affordability uses selling values recovered from public purchase history; advice is withheld when those values cannot be recovered. Bench Boost rebuilds show at most one affordable upgrade, checked against the full squad.
The model currently supports single-fixture gameweeks. Chip and transfer advice is withheld if its eight-week planning window contains a double gameweek, and missing projections are reported rather than replaced by repeated market odds. Blank weeks are distinguished from unavailable fixture data in the player profile. The budget-based Free Hit and Wildcard search is approximate; owned-XI selection is exact for a legal fifteen. Projections are estimates, not backtested forecasts.
© 2026 Jonno Millar, all rights reserved. Public to read, not to reuse (see LICENSE).