No description
  • Python 75.1%
  • CSS 13.1%
  • JavaScript 10.8%
  • HTML 0.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
fpl-app 9c2faca94d
Some checks failed
CI / syntax (push) Successful in 9s
CI / tests (push) Failing after 1m5s
CI / wheel (push) Has been skipped
CI / browser (push) Has been skipped
fix: the goalscorer panel named the wrong book, and NT's coverage was filed as a cost
Reported: "viser du NT på anytime goalscorer nå og?" — we did, 240 of 264 rows,
and the panel said otherwise.

    screen   Šeško  47%  "5 books"   under a header reading BOOK CONSENSUS
    data     source NT_ODDSEN, nt_price 2.00 -> 47%
             consensus 43.3%, from those 5 books

The 47% was Norsk Tipping's; "5 books" described the consensus's 43.3%, which
appeared nowhere. Three faults, one class — a label written by hand beside data
that moved:

  * `scorerBook()` read the source of the FIRST row and the header applied it
    to all 264. The board sorts by price, Haaland leads, and his is the one
    fixture NT never covered (it kicked off before the pull), so a single
    consensus row relabelled a panel that was 240/264 NT.
  * `n_books` printed on every row, including NT ones. It is deliberately kept
    from the consensus by `merge_market` — `_will_play` needs it — but beside
    an NT price it reads as describing that price.
  * the hint said "Ordered by the bookmakers' price", true the day it was typed.

Header, row label and hint are all derived from `p_score_source` now, and a
mixed panel says so rather than crediting one book.

ALSO, AND THIS ONE WAS BACKWARDS IN CLAUDE.md: NT quoting fewer players was
filed as a cost. Measured against realised GW2 minutes over the nine fixtures
NT covered (CRY v MCI excluded — never asked, kicked off before the pull):

    quoted by both              275   47.9 min   51% played 60+
    consensus only, NT declined  63    2.4 min    2%
    NT only                      23   37.1 min   35%
    neither                     140   11.5 min   12%

A 45.5-minute gap, and not lineup-reading: NT's declines averaged 2.0 min under
24h out and 3.2 min beyond 24h, when no team news existed. Issue #16's finding
with NT's silence four times sharper than the consensus's.

Standing instruction recorded with it: show Norsk Tipping until the opposite is
PROVEN, several rounds before arguing against it. `backend/score_history.py`
makes that mechanical rather than a promise — the scorer printed and persisted
nothing, so each week's evidence died in scrollback. Rounds are banked; the
verdict has a floor (4 rounds, 0.010 log loss, and a split record stays
undetermined at any size) but calls a source that loses to the control at once.

Plus: the depth badge now names who is out, derived from `depth_blocked_by`,
which was computed, cached, served and read nowhere; `depth_blocked_by` is set
on every player so the golden master's direct subscripts hold; and its frozen
field list and fixture were updated — the gate caught the new field on a
rebuild, hours after the code that added it was green in CI.

Full suite green (PYTEST_EXIT=0).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSzuxHvACMHg8u1AhdK6k5
2026-08-31 17:26:21 +02:00
.claude/agents chore: two more agents — the weekly routine, and the provenance audit 2026-08-23 00:38:41 +02:00
.forgejo/workflows ci: use Node supplied by the runner image 2026-08-30 21:24:36 +02:00
backend fix: the goalscorer panel named the wrong book, and NT's coverage was filed as a cost 2026-08-31 17:26:21 +02:00
data fix: do not commit the chore ledger 2026-08-30 14:58:36 +02:00
docs build: complete package discovery, declared test extras, constraints and CI 2026-08-24 19:56:40 +02:00
frontend fix: the goalscorer panel named the wrong book, and NT's coverage was filed as a cost 2026-08-31 17:26:21 +02:00
scripts fix: the goalscorer panel named the wrong book, and NT's coverage was filed as a cost 2026-08-31 17:26:21 +02:00
tests fix: the goalscorer panel named the wrong book, and NT's coverage was filed as a cost 2026-08-31 17:26:21 +02:00
.gitignore feat: long-horizon ranking to GW28, with the far end discounted 2026-08-30 18:43:30 +02:00
build-to-gw1.md feat: wire backfill into the pipeline, foreign-league rates, captaincy 2026-07-26 16:28:03 +02:00
CLAUDE.md fix: the goalscorer panel named the wrong book, and NT's coverage was filed as a cost 2026-08-31 17:26:21 +02:00
constraints.txt build: complete package discovery, declared test extras, constraints and CI 2026-08-24 19:56:40 +02:00
pyproject.toml build: complete package discovery, declared test extras, constraints and CI 2026-08-24 19:56:40 +02:00
README.md build: complete package discovery, declared test extras, constraints and CI 2026-08-24 19:56:40 +02:00
run.bat feat: the goalscorer list is ordered by the book, not by us 2026-08-15 20:15:58 +02:00
run.ps1 fix: enforce the local-only trust boundary in the app, not the launcher (issue #2) 2026-08-24 19:04:40 +02:00

FPL squad optimiser

An evidence-conscious Fantasy Premier League projection and squad-planning app. It combines official FPL data, historical match and player data, simulations, availability estimates, optional market data, and FPL squad rules to produce a rolling six-gameweek view.

The application includes:

  • player and fixture projections;
  • best-XI, captain and vice-captain analysis;
  • squad building and automatic squad optimisation;
  • transfer, ownership and fixture views;
  • explicit provenance and refusal states for weak or missing inputs;
  • backtesting and calibration utilities;
  • point-in-time snapshots for auditable decisions.

The project is intended primarily as a local decision-support tool. It is not a hosted multi-user service, and its write/rebuild endpoints currently have no authentication.

Repository map

Path Purpose
backend/data/ Official FPL client, typed data models, SQLite store and sanity checks
backend/model/ Minutes, team strength, finishing, lineup and suspension models
backend/scoring/ FPL scoring, BPS and defensive-contribution rules
backend/simulator/ Match and fixture Monte Carlo simulation
backend/odds/ Odds ingestion, name matching and market comparison
backend/web/ Projection pipeline, optimiser, cache and FastAPI application
frontend/ Static HTML, CSS and JavaScript client
scripts/ Cache builds, data acquisition, diagnostics and backtests
tests/ Unit, integration, API and browser tests
data/ Committed inputs and evidence artifacts; generated local state is ignored
docs/ Designs, methodology notes and maintainer documentation

See Architecture, Development, and API reference for more detail. Contributors should read CLAUDE.md before changing model or data-flow code; it records the failure modes and invariants that shape this repository.

Quick start

Python 3.11 or newer is required. One command creates an environment and runs the suite from a clean checkout:

python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ".[test]" -c constraints.txt

constraints.txt records the versions this suite has actually been run against. It is not a lockfile - it does not pin transitive dependencies - but it makes a failure attributable rather than a guess, given that pyproject.toml declares only lower bounds.

Build the projection cache, then start the app from the repository root:

python scripts/build_cache.py
python -m uvicorn backend.web.main:app --host 127.0.0.1 --port 8000

Open http://127.0.0.1:8000/. Keep the server on loopback: administrative routes can rebuild projections and write local state.

On Windows, use the included launcher:

run.bat
run.bat -Restart
run.bat -Port 8123

The server can start in cache-only mode if live dependencies cannot be loaded. In that state, read-only cached views remain available, while operations that need the complete dependency graph return 503.

Common workflows

Rebuild projections after model or source-data changes:

python scripts/build_cache.py

Verify that a change moved no projections (the golden master):

python scripts/golden_master.py

It exits 0 when every frozen field matches, 1 when something moved, and 2 when the cache predates the current code — in which case rebuild first, because a comparison against a stale cache proves nothing.

Refresh only live prices, ownership and availability while the server runs:

curl -X POST http://127.0.0.1:8000/api/refresh-live

Take an immutable point-in-time snapshot before a deadline:

python scripts/snapshot_pit.py

Ingest a finished gameweek so the model learns from the current season:

python scripts/fetch_current_season.py

Run the principal walk-forward evaluations:

python scripts/backtest_rps.py
python scripts/backtest_minutes.py

Run tests after installing the test dependencies:

python -m pytest

Browser tests use Playwright and may be skipped when it is unavailable. Install it separately when working on rendered frontend behavior:

python -m pip install playwright
python -m playwright install chromium
python -m pytest tests/web

Generated local state

The application writes these ignored files:

  • data/cache/projections.json — projection cache;
  • data/fpl.db — SQLite store, rebuildable with scripts/import_archive.py;
  • data/squad.json — current squad selection;
  • data/overrides.json — explicit start-probability overrides;
  • data/pit/ — point-in-time snapshots.

data/golden/baseline.json is committed rather than ignored: it is the reference the golden master compares against, and it is only useful if it travels with the code it describes.

Projection artifacts carry build time and source-code provenance. Missing or invalid provenance is intentionally treated as an error rather than silently replaced with a plausible default.

Data sources

The official FPL API is the primary live source. The repository also contains historical archive data and optional pipelines for Transfermarkt, Understat, Solio and bookmaker player props. Some acquisition scripts may require external credentials, metered APIs or pre-registered run scopes. Read their help and the data-source rules in CLAUDE.md before running them.

Current maintenance notes

The repository review is tracked in Forgejo issues. Known improvement areas include clean-checkout dependency locking and CI, package discovery, persistence concurrency, API request models and module decomposition.

HTML interpolation (issue #1) is partially addressed: the frontend's esc() helper was a JavaScript-string escaper carrying an HTML escaper's name, and now escapes &, <, >, " and ' in a single pass. Remaining interpolation sites have not been audited.

This documentation describes the current implementation rather than implying those items are resolved.