From 0027a5a318ae1abff668213ac47fa538c690152d Mon Sep 17 00:00:00 2001 From: cjharriskc-ai Date: Sun, 26 Jul 2026 14:19:09 -0500 Subject: [PATCH] Launch Waterboy production sports engine --- .dockerignore | 15 + .gitignore | 3 + AGENTS.md | 2 +- ARCHITECTURE.md | 36 +- AUTONOMOUS_IMPROVEMENT.md | 6 +- DATA_PIPELINE_LINEAGE.md | 59 ++ DATA_RIGHTS_AND_PROVENANCE.md | 38 +- Dockerfile | 25 + KNOWN_LIMITATIONS.md | 17 +- PRODUCTION_RUNBOOK.md | 122 ++++ README.md | 286 ++++---- STATUS.md | 99 +-- docs/assets/waterboy-dashboard.png | Bin 0 -> 226997 bytes pyproject.toml | 25 +- reports/RELEASE_EVIDENCE_V4.md | 69 ++ scripts/build_release_evidence.py | 10 +- src/universal_sports_engine/__init__.py | 4 +- src/universal_sports_engine/autotune.py | 2 +- src/universal_sports_engine/cli.py | 121 +++- src/universal_sports_engine/data/__init__.py | 13 + src/universal_sports_engine/data/espn.py | 312 +++++++++ src/universal_sports_engine/data/models.py | 54 ++ src/universal_sports_engine/data/pipeline.py | 255 +++++++ src/universal_sports_engine/data/store.py | 312 +++++++++ .../league_packs/common.py | 2 +- .../league_packs/registry.py | 16 +- src/universal_sports_engine/projection.py | 493 +++++++++++++ src/universal_sports_engine/service.py | 214 ++++++ src/universal_sports_engine/settings.py | 6 + src/universal_sports_engine/simulation.py | 2 +- src/universal_sports_engine/web/static/app.js | 439 ++++++++++++ .../web/static/favicon.svg | 4 + .../web/static/index.html | 230 ++++++ .../web/static/styles.css | 660 ++++++++++++++++++ src/universal_sports_engine/web_app.py | 172 +++++ src/waterboy/__init__.py | 27 + src/waterboy/__main__.py | 6 + tests/test_core.py | 3 +- tests/test_projection_service_web.py | 171 +++++ tests/test_sports_data.py | 149 ++++ 40 files changed, 4273 insertions(+), 206 deletions(-) create mode 100644 .dockerignore create mode 100644 DATA_PIPELINE_LINEAGE.md create mode 100644 Dockerfile create mode 100644 PRODUCTION_RUNBOOK.md create mode 100644 docs/assets/waterboy-dashboard.png create mode 100644 reports/RELEASE_EVIDENCE_V4.md create mode 100644 src/universal_sports_engine/data/__init__.py create mode 100644 src/universal_sports_engine/data/espn.py create mode 100644 src/universal_sports_engine/data/models.py create mode 100644 src/universal_sports_engine/data/pipeline.py create mode 100644 src/universal_sports_engine/data/store.py create mode 100644 src/universal_sports_engine/projection.py create mode 100644 src/universal_sports_engine/service.py create mode 100644 src/universal_sports_engine/settings.py create mode 100644 src/universal_sports_engine/web/static/app.js create mode 100644 src/universal_sports_engine/web/static/favicon.svg create mode 100644 src/universal_sports_engine/web/static/index.html create mode 100644 src/universal_sports_engine/web/static/styles.css create mode 100644 src/universal_sports_engine/web_app.py create mode 100644 src/waterboy/__init__.py create mode 100644 src/waterboy/__main__.py create mode 100644 tests/test_projection_service_web.py create mode 100644 tests/test_sports_data.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d022d7c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +.git +.github +.mypy_cache +.pytest_cache +.ruff_cache +.venv +.repro-venv +.repro-v3-venv +__pycache__ +*.egg-info +build +dist +output +reports +runtime diff --git a/.gitignore b/.gitignore index 077d4a5..e188b6b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ __pycache__/ .pytest_cache/ .ruff_cache/ .mypy_cache/ +runtime/ +output/ +.playwright-cli/ *.egg-info/ build/ dist/ diff --git a/AGENTS.md b/AGENTS.md index d813013..c0659e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# Universal Sports Engine Rules +# Waterboy Engine Rules - Keep the simulation truth independent from rendering. - Preserve deterministic replay: identical versions, inputs, and seed must produce identical events. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ac83dce..c62dc1e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,4 +1,4 @@ -# Architecture +# Waterboy Architecture ## Decision @@ -17,7 +17,11 @@ Kimi build: ## Layers ```text -CLI / SDK / domain-neutral bridge +Web UI / versioned HTTP API / CLI / SDK / domain-neutral bridge + | +Projection service, weekly board, hypothetical-matchup orchestration + | +Normalized point-in-time games, SQLite provenance store, bounded feed clients | Champion governance, recursive calibration, adaptive weights, drift quarantine | @@ -32,6 +36,34 @@ League packs own scoring, timing, possession/inning/drive/shift semantics, and reference anchors. The kernel contains no player, ball, puck, inning, possession, or scoreboard type. +## Sports-data and projection boundary + +The production application adds a domain adapter outside the neutral kernel: + +1. Read-only source adapters normalize public scoreboards or a protected local + historical database into `GameRecord`. +2. `SportsDataStore` records every normalized observation together with source URL, + observation time, ingest run, and payload SHA-256. +3. `ProjectionEngine` estimates recency-weighted, opponent-adjusted attack and + defense ratings, shrinks them toward league priors, and derives score parameters. +4. The existing analytical F0 engine produces deterministic, reproducible score + distributions from those parameters. +5. `SportsEngineService` exposes scheduled weekly games and hypothetical pairings + without giving the web layer direct database or simulation-kernel access. + +Final games are observations, not projections. In-progress games show their +observed score and intentionally withhold a pregame projection. Sparse or stale +history remains visible in the projection evidence status instead of being hidden. + +## Application boundary + +FastAPI validates and bounds all external inputs, emits a versioned `/api/v1` +surface, and serves a static no-build dashboard. The browser does not evaluate +server-provided HTML. Security headers are applied globally, API responses are +non-cacheable, and remote binding requires an explicit operator flag. A remote +deployment must add TLS, authentication, rate limiting, and network policy at the +reverse proxy as described in `PRODUCTION_RUNBOOK.md`. + ## Fidelity - F0: analytical/batch score distribution (implemented; no event path). diff --git a/AUTONOMOUS_IMPROVEMENT.md b/AUTONOMOUS_IMPROVEMENT.md index 5254d8b..3d48625 100644 --- a/AUTONOMOUS_IMPROVEMENT.md +++ b/AUTONOMOUS_IMPROVEMENT.md @@ -60,16 +60,16 @@ and an untouched test audit after the champion is frozen. ## Commands ```powershell -.\.venv\Scripts\sports-engine.exe auto-improve ` +.\.venv\Scripts\waterboy.exe auto-improve ` --csv data\point-in-time\historical.csv ` --config AUTONOMY_CONFIG.json ` --output reports\improvement-run-001 -.\.venv\Scripts\sports-engine.exe simulate-adaptive ` +.\.venv\Scripts\waterboy.exe simulate-adaptive ` --bundle reports\improvement-run-001\champion_bundle.json ` --league nba --seed 42 --home-strength 1.0 --away-strength 1.0 -.\.venv\Scripts\sports-engine.exe adapt-online ` +.\.venv\Scripts\waterboy.exe adapt-online ` --csv data\point-in-time\shadow.csv ` --config AUTONOMY_CONFIG.json ` --bundle reports\improvement-run-001\champion_bundle.json ` diff --git a/DATA_PIPELINE_LINEAGE.md b/DATA_PIPELINE_LINEAGE.md new file mode 100644 index 0000000..65d5f29 --- /dev/null +++ b/DATA_PIPELINE_LINEAGE.md @@ -0,0 +1,59 @@ +# Data Pipeline Lineage + +## Boundary + +Dummy is a protected source. Waterboy never imports Dummy modules, +writes to Dummy's checkout, migrates its database, or controls its processes. The +only reuse is a clean-room implementation of the required sports-history flow and +a query-only copy of eligible game rows into a target-owned database. + +## Component map + +| Capability observed in Dummy | Waterboy implementation | Boundary | +|---|---|---| +| Normalized sports history | `data.models.GameRecord` | New target-owned contract | +| SQLite sports history | `data.store.SportsDataStore` | New schema and provenance columns | +| Historical league rows | `data.pipeline.DummyHistoryImporter` | Source opened `mode=ro` plus `query_only` | +| Public score refresh | `data.espn.EspnScoreboardClient` | Bounded GET, timeout, retry, cache, payload hash | +| Sports ingestion orchestration | `data.pipeline.SportsDataPipeline` | Target-owned league aliases and ingest runs | +| Board/model consumption | `projection.ProjectionEngine` and `service.SportsEngineService` | No Dummy runtime dependency | + +## Copied records + +The importer selects only sports-history fields needed to reconstruct completed +games: source record identifier, league, UTC start time, home/away team IDs and +names, scores, and final status. It normalizes league aliases and rejects malformed +or same-team records that violate the target contract. + +Each copied observation receives an immutable provenance reference to the protected +source database and source row. The target database can be discarded and rebuilt +without altering the source. + +## Explicit exclusions + +The implementation does not copy or activate: + +- prediction-market, portfolio, brokerage, order, or execution paths; +- credentials, tokens, user data, environment files, logs, or runtime settings; +- model claims, profitability artifacts, settled-market evidence, or readiness + labels; +- Dummy code imports, database migrations, background jobs, or process controls; +- odds feeds, betting lines, injury data, private providers, or restricted data. + +## Current public refresh + +Current-week schedules and scores are normalized from a public, read-only +scoreboard endpoint. The client records the exact request URL, retrieval timestamp, +and SHA-256 of the raw payload. Requests are bounded by timeout, retry count, +response size, date span, and a local cache. College endpoints that reject a date +range fall back to bounded daily requests. + +Provider availability is not guaranteed, and provider terms govern use. A fetch +failure stays visible as an ingest error and never becomes synthetic current data. + +## Verification + +Automated tests create temporary source and destination databases, verify that the +source bytes remain unchanged, exercise normalization and aliases, and inspect the +HTTP/API projection contracts. Operationally, recheck `git status` in Dummy after +an import and confirm it is clean. diff --git a/DATA_RIGHTS_AND_PROVENANCE.md b/DATA_RIGHTS_AND_PROVENANCE.md index 7e45ca7..ac17c5c 100644 --- a/DATA_RIGHTS_AND_PROVENANCE.md +++ b/DATA_RIGHTS_AND_PROVENANCE.md @@ -1,22 +1,42 @@ # Data Rights and Provenance -## Local build inputs +## Waterboy runtime inputs -No licensed event feed, tracking feed, private injury feed, sportsbook feed, or -proprietary roster database is present in this repository. +No licensed tracking feed, private injury feed, sportsbook feed, brokerage feed, +or proprietary roster database is present in this repository. -The shipped engine uses: +The application can use: - deterministic synthetic events generated by the versioned league packs; - transparent league-level reference anchors for broad output sanity checks; -- official rule-source links as provenance pointers for the minimal rule manifests. +- official rule-source links as provenance pointers for the minimal rule manifests; +- keyless, unauthenticated, read-only ESPN public scoreboard responses for schedules, + team identities, game state, and final scores; +- an optional local copy of sports history from Dummy, opened with SQLite `mode=ro` + and `PRAGMA query_only=ON`. The reference anchors are approximate research fixtures. They are not an ingested, point-in-time dataset and are labeled `provisional_reference_anchor` everywhere. +## Runtime provenance contract + +Every normalized game stores its source, source URL, receipt time, and a SHA-256 +hash of the observed payload. Scheduled rows do not receive synthetic observed +scores. In-progress scores remain labeled as observations; the pregame-only model +is withheld on those cards. + +The public HTTP cache and local SQLite database live under `runtime/`, which is +gitignored. The repository does not redistribute provider payloads or the local +Dummy history database. + +The adapter sends bounded GET requests with a descriptive user agent, response-size +limit, timeout, retries, and a short local cache. It has no login, credential, +write, odds, wagering, or automation-bypass behavior. Operators remain responsible +for evaluating provider terms and data rights for their deployment. + ## Prohibited promotion -These inputs cannot support a claim of held-out player, team, game, season, spatial, -or counterfactual accuracy. Such claims require separately licensed data manifests, -earliest-available timestamps, revisions, train/calibration/test boundaries, and -independent reproduction. +These inputs cannot by themselves support a claim of held-out player, game, spatial, +or counterfactual accuracy. Such claims require frozen point-in-time manifests, +earliest-available timestamps, revisions, chronological train/calibration/test +boundaries, prospective scoring, and independent reproduction. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fa21e25 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + WATERBOY_DATA_PATH=/data/sports.db \ + WATERBOY_CACHE_DIR=/data/http_cache + +WORKDIR /app + +RUN groupadd --system sports && useradd --system --gid sports --home-dir /app sports + +COPY pyproject.toml README.md LICENSE ./ +COPY src ./src + +RUN python -m pip install --no-cache-dir . && \ + mkdir -p /data && chown -R sports:sports /app /data + +USER sports +VOLUME ["/data"] +EXPOSE 8765 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD python -c "import json,urllib.request; data=json.load(urllib.request.urlopen('http://127.0.0.1:8765/api/v1/health',timeout=3)); raise SystemExit(0 if data['status']=='healthy' else 1)" + +CMD ["waterboy", "serve", "--host", "0.0.0.0", "--port", "8765", "--allow-remote", "--data", "/data/sports.db", "--cache", "/data/http_cache"] diff --git a/KNOWN_LIMITATIONS.md b/KNOWN_LIMITATIONS.md index 9a25c40..9192c9b 100644 --- a/KNOWN_LIMITATIONS.md +++ b/KNOWN_LIMITATIONS.md @@ -1,8 +1,18 @@ # Known Limitations -- The stalled remote Kimi repository was not exported; this is an independent local reconstruction. +- Waterboy's current score projections use team scoring history, recency, opponent adjustment, + and league priors; they do not yet ingest rosters, injuries, starting lineups, + probable pitchers, coaching changes, travel, officials, or forecast weather. +- The public scoreboard is an operational schedule/final-score source, not a + licensed tracking feed or a predictive-accuracy certification dataset. +- Some imported historical leagues are stale. Those ratings decay toward league + priors and surface `sparse_or_stale_team_history` rather than silently presenting + old form as current. +- In-progress scores are displayed as observations. The current projection model is + pregame-only and is withheld rather than mislabeled as a live win-probability model. - F1 event paths are calibrated only to broad reference anchors, not licensed held-out data. -- Player identities, rosters, coaching policies, officials, injuries, fatigue, venues, and weather are contract surfaces rather than calibrated models. +- Player identities, rosters, coaching policies, officials, injuries, fatigue, and + weather are contract surfaces rather than calibrated models. - F2 spatial trajectories and F3 physics/rendering are intentionally not fabricated without tracking evidence. - Rule manifests cover complete-game flow but are not a complete executable transcription of every official rulebook edge case. - Generic F1 games resolve a winner; competition-specific regular-season tie and @@ -19,4 +29,5 @@ - Windows multi-process calls require an importable script or CLI entrypoint; an interactive/stdin caller receives an explicit error and should use F0 or one worker. - Counterfactual pairing reduces simulation noise but does not by itself establish causality. -- No live feed, wagering, sportsbook, trading, or capital interface exists. +- A read-only public scoreboard adapter exists. No wagering, sportsbook, trading, + brokerage, or capital interface exists. diff --git a/PRODUCTION_RUNBOOK.md b/PRODUCTION_RUNBOOK.md new file mode 100644 index 0000000..9b63a92 --- /dev/null +++ b/PRODUCTION_RUNBOOK.md @@ -0,0 +1,122 @@ +# Production Runbook + +## Scope + +This runbook operates Waterboy 0.4 as a read-only projection and +simulation service. It does not provide wagering, trading, or order execution. + +## Bootstrap + +Python 3.12 is the supported runtime. + +```powershell +py -3.12 -m venv .venv +.\.venv\Scripts\python.exe -m pip install --upgrade pip +.\.venv\Scripts\python.exe -m pip install -e ".[dev]" +``` + +Create the target-owned history store without writing to Dummy: + +```powershell +.\.venv\Scripts\waterboy.exe import-dummy-history +.\.venv\Scripts\waterboy.exe refresh-data --days 7 +``` + +The importer opens Dummy with SQLite URI `mode=ro` and enables +`PRAGMA query_only=ON`. The destination defaults to +`runtime/universal_sports_engine/sports.db`. + +## Local operation + +```powershell +.\.venv\Scripts\waterboy.exe serve --host 127.0.0.1 --port 8765 +``` + +Health check: + +```powershell +Invoke-RestMethod http://127.0.0.1:8765/api/v1/health +``` + +Refresh and weekly projection checks: + +```powershell +.\.venv\Scripts\waterboy.exe refresh-data --days 7 +.\.venv\Scripts\waterboy.exe project-week --simulations 500 +.\.venv\Scripts\waterboy.exe simulate-matchup --league nfl --away KC --home PHI +``` + +## Container + +```powershell +docker build -t waterboy:0.4.0 . +docker run --read-only --tmpfs /tmp -p 127.0.0.1:8765:8765 ` + -v waterboy-data:/data waterboy:0.4.0 +``` + +The image runs as a non-root user and stores mutable state only below `/data`. + +## Remote deployment controls + +The application refuses a non-loopback bind unless `--allow-remote` is present. +That flag is an acknowledgement, not a security layer. Before remote exposure: + +- terminate TLS at an authenticated reverse proxy; +- restrict refresh endpoints to an operator identity; +- apply request-size, request-rate, and concurrency limits; +- restrict egress to approved data-source hosts; +- persist `/data` on encrypted storage; +- collect access, application, refresh, and health logs; +- alert on failed refreshes, stale league data, elevated latency, and disk growth; +- keep at least one tested database backup outside the serving host. + +Do not expose the Uvicorn process directly to the public internet. + +## Data refresh + +Refresh is idempotent at the normalized game identifier and records an ingest run +for auditability. A partial source failure is explicit in the command/API result; +previously stored observations remain available. + +Recommended cadence: + +- in season: every 15 minutes while games are active, hourly otherwise; +- out of season: daily; +- after deploy: one forced refresh followed by `/api/v1/health`. + +Respect provider terms, cache controls, rate limits, and permitted-use boundaries. +This repository does not grant data redistribution rights. + +## Backup and restore + +Stop the serving process or use SQLite's online backup API before copying the +database. Preserve the database, its checksum, UTC timestamp, application version, +and configuration together. After restoration: + +1. start on a non-production port; +2. call `/api/v1/health`; +3. run `PRAGMA integrity_check` through an approved SQLite operator tool; +4. compare row counts and latest observation times; +5. run one deterministic hypothetical matchup twice and compare results. + +## Incident behavior + +If a feed fails, retain the last valid observations, surface stale evidence, and +investigate; never manufacture a current score. If integrity checks fail, remove +the instance from service and restore a verified backup. If forecasts behave +unexpectedly, preserve the database and seed, reproduce locally, and keep the +software/evidence boundary explicit. + +## Release gate + +Before deployment, require: + +```powershell +.\.venv\Scripts\ruff.exe check . +.\.venv\Scripts\mypy.exe src scripts +.\.venv\Scripts\pytest.exe -q +git diff --check +``` + +Also build and inspect a wheel, exercise the browser at desktop and mobile widths, +and validate the container when Docker is available. diff --git a/README.md b/README.md index a48d8f7..530e8e0 100644 --- a/README.md +++ b/README.md @@ -1,176 +1,214 @@
-# Universal Sports Engine +# Waterboy -### Deterministic simulation. Recursive calibration. Auditable evolution. +### Every league. Any matchup. One auditable simulation engine. -A high-throughput, multi-league sports research engine with exact replay, -self-tuning analytical ensembles, drift-aware online calibration, and -content-addressed champion governance. +Waterboy is a production-capable sports intelligence command center for +current-week score projections and hypothetical games. It combines source-backed +history, deterministic Monte Carlo, visible uncertainty, exact replay, and seven +isolated league packs behind one fast interface. -[![Version](https://img.shields.io/badge/version-0.3.0-7c3aed?style=for-the-badge)](./STATUS.md) +[![Version](https://img.shields.io/badge/version-0.4.0-b9ff5a?style=for-the-badge&labelColor=0b1215)](./STATUS.md) +[![Tests](https://img.shields.io/badge/tests-74%20passing-74e8ff?style=for-the-badge&labelColor=0b1215)](./reports/RELEASE_EVIDENCE_V4.md) +[![Leagues](https://img.shields.io/badge/leagues-7-f4f3ed?style=for-the-badge&labelColor=0b1215)](#seven-leagues-one-engine) [![Python](https://img.shields.io/badge/python-3.12%2B-3776ab?style=for-the-badge&logo=python&logoColor=white)](./pyproject.toml) -[![Leagues](https://img.shields.io/badge/leagues-7-0f766e?style=for-the-badge)](#seven-leagues-one-kernel) -[![Tests](https://img.shields.io/badge/tests-67%20passing-16a34a?style=for-the-badge)](./reports/RELEASE_EVIDENCE.md) -[![License](https://img.shields.io/badge/license-MIT-111827?style=for-the-badge)](./LICENSE) +[![License](https://img.shields.io/badge/license-MIT-f4f3ed?style=for-the-badge&labelColor=0b1215)](./LICENSE) -**Research release:** mechanics, integrity, autonomy, and performance are verified. -Real-world predictive supremacy is not claimed without licensed point-in-time data. +**Built to make simulation powerful, legible, and falsifiable.**
---- +![Waterboy dashboard](./docs/assets/waterboy-dashboard.png) -## Why it is different +## What Waterboy does -Universal Sports Engine treats every simulation and model improvement as evidence, -not theater. - -- **Exact by construction** — immutable event streams, deterministic random namespaces, - SHA-256 event chains, and complete result-envelope verification. -- **Fast at two fidelities** — analytical F0 distributions for massive Monte Carlo and - event-level F1 paths for interpretable game flow. -- **Autonomously adaptive** — bounded population search, multiplicative expert weighting, - self-calibrating intervals, champion/challenger gates, and prospective shadow updates. -- **Fail-closed** — drift alerts quarantine adaptive simulation; failed challengers remain - in the ledger; existing checkpoints are never silently overwritten. -- **Portable** — dependency-free runtime, clean Python SDK and CLI, deterministic JSON - replay, and a domain-neutral stochastic-control boundary. - -## Verified release snapshot +| Surface | Capability | +|---|---| +| Current-week command center | Organizes scheduled, live, and final games across seven leagues | +| Score projections | Decimal team scores, totals, margins, win/tie probabilities, and 80% intervals | +| Winner signal | Places an accessible `✓` immediately beside the higher-probability team | +| Matchup Lab | Simulates any two teams at home or a neutral site with 5K–50K draws | +| Team intelligence | Recency weighting, opponent adjustment, prior shrinkage, and freshness penalties | +| Evidence | Source URL, observation time, payload hash, sample size, data-through time, and limitations | +| Replay | Same model, state, inputs, and seed reproduce the same simulated world | +| Operations | Versioned API, CLI, SQLite provenance store, security headers, and non-root container | -| Dimension | Version 0.3 evidence | -|---|---:| -| Supported leagues | 7 | -| Tests | 67 passed | -| Negative and mechanism controls | 49 passed, 0 failed | -| Randomized stress | 14,000 games / 2,003,304 events / 0 failures | -| F0 throughput | 111K–127K games/second | -| F1 single-worker throughput | 347–1,011 games/second | -| F1 four-worker scaling | 1.42×–1.82× | -| Recursive autonomy arena | 4 promotions / 3 guarded retentions | -| Calibration and shadow weight changes | 7 / 4 | -| Shadow drift alerts | 0 | - -Machine-readable results live in -[release evidence](./reports/RELEASE_EVIDENCE.md), -[performance evidence](./reports/PERFORMANCE_AUDIT_V3_FINAL.json), -[stress evidence](./reports/STRESS_AUDIT_V3.json), and the -[autonomy audit](./reports/AUTONOMY_AUDIT_V3.json). - -## Architecture +## The Waterboy pipeline ```mermaid -flowchart TD - CLI["CLI / Python SDK"] --> GOV["Champion governance"] - GOV --> CAL["Recursive calibration + online weights"] - CAL --> F0["F0 analytical distributions"] - GOV --> F1["F1 event simulation"] - F0 --> PACKS["Seven isolated league packs"] - F1 --> PACKS - PACKS --> CORE["Deterministic event / rule / RNG kernel"] - CORE --> PROOF["Replay + hashes + evidence bundles"] - PROOF --> RED["Stress, controls, drift, rollback"] +flowchart LR + A["Read-only schedules
and historical scores"] --> B["Normalized game contract
timestamp + source + SHA-256"] + B --> C["Provenance SQLite
isolated target-owned store"] + C --> D["Team-state intelligence
recency + opponent adjustment"] + D --> E["Posterior parameter worlds
shrinkage + uncertainty"] + E --> F{"Fidelity router"} + F -->|"F0"| G["Mass analytical simulation"] + F -->|"F1"| H["Event-level game paths"] + G --> I["Decimal projections
probabilities + intervals"] + H --> J["Replayable event chain
result-envelope hash"] + I --> K["Current Week + Matchup Lab"] + J --> K ``` -The neutral kernel contains no player, inning, possession, ball, puck, or scoreboard -type. Sport semantics remain inside league packs; model governance remains outside -simulation truth. +Waterboy does not hide source quality behind a confidence color. Sparse or stale +history is explicitly labeled and shrunk toward transparent league priors. +In-progress scores remain observations; the pregame model is withheld instead of +being mislabeled as a live forecast. -## Seven leagues, one kernel +## Showcase snapshot -| Family | League packs | Primary F1 unit | +| Dimension | Verified local evidence | +|---|---:| +| Historical bootstrap | 179,383 valid games copied query-only from Dummy | +| Current seven-day board | 107 games: 92 MLB and 15 WNBA on 2026-07-26 | +| Scheduled pregame projections | 94 | +| League packs | 7 | +| Hypothetical simulation depth | Up to 50,000 draws | +| Current-board latency | Approximately 1.1 seconds at 500 draws/game | +| Automated tests | 74 passed, zero warnings | +| Negative/mechanism controls | 49 passed, zero failures | +| Reference validation | 7/7 provisional league anchors | +| Browser validation | Desktop + mobile, zero console errors/warnings | + +This snapshot demonstrates working software and operational data flow. It does +not establish predictive supremacy or profitability; those require independently +governed prospective, point-in-time evaluation. + +## Seven leagues, one engine + +| UI category | Internal pack | Family | |---|---|---| -| Bat and ball | MLB | Pitch / plate appearance / inning | -| Court invasion | NBA, WNBA, NCAA MBB | Possession / period / game | -| Gridiron | NFL, NCAA Football | Play / drive / game | -| Ice invasion | NHL | Event / shift / period | +| MLB | `mlb` | Bat and ball | +| NFL | `nfl` | Gridiron | +| NCAAF | `ncaaf` | Gridiron | +| NBA | `nba` | Court invasion | +| WNBA / WBA | `wnba` | Court invasion | +| NCAAM | `ncaambb` | Court invasion | +| NHL | `nhl` | Ice invasion | + +Each pack owns its rules, clocks, phases, scoring, stochastic transitions, and +reference anchors. Sport semantics never leak into the domain-neutral event core. -Each pack owns its rules, scoring, clocks, stochastic transitions, and provisional -reference anchors. One sport is never treated as a rescaled copy of another. +## Quick start -## Recursive improvement engine +```powershell +py -3.12 -m venv .venv +.\.venv\Scripts\python.exe -m pip install -e ".[dev]" -```mermaid -flowchart LR - T["Point-in-time train"] --> S["Bounded population search"] - S --> C["Chronological calibration"] - C --> W["Expert reweighting + interval adaptation"] - W --> G{"All gates pass?"} - G -- Yes --> P["Promote + freeze checkpoint"] - G -- No --> R["Retain incumbent + record failure"] - P --> H["Prospective shadow outcomes"] - R --> H - H --> D{"Drift?"} - D -- Healthy --> W - D -- Alert --> Q["Quarantine / recalibrate / rollback"] +# Query-only historical bootstrap; Dummy is never modified. +.\.venv\Scripts\waterboy.exe import-dummy-history + +# Refresh the rolling seven-day board. +.\.venv\Scripts\waterboy.exe refresh-data --days 7 + +# Start Waterboy. +.\.venv\Scripts\waterboy.exe serve ``` -Profiles, ensembles, adaptive states, champion bundles, and promotion ledgers all -carry independent content hashes. Promoted bundles embed the complete incumbent -model and state, so rollback is self-contained rather than merely documented. +Open [http://127.0.0.1:8765](http://127.0.0.1:8765). -Read the full [autonomous improvement protocol](./AUTONOMOUS_IMPROVEMENT.md). +The legacy `sports-engine` command and `universal_sports_engine` Python namespace +remain supported for replay and integration compatibility. New integrations should +use the `waterboy` command or `python -m waterboy`. -## Quick start +## Build any matchup ```powershell -python -m venv .venv -.\.venv\Scripts\python.exe -m pip install -e ".[dev]" +.\.venv\Scripts\waterboy.exe simulate-matchup ` + --league nfl ` + --home KC ` + --away PHI ` + --simulations 50000 ` + --seed 20260726 +``` -.\.venv\Scripts\sports-engine.exe list-leagues -.\.venv\Scripts\sports-engine.exe simulate --league nba --seed 42 -.\.venv\Scripts\sports-engine.exe simulate-fast ` - --league nfl --seed 42 --home-strength 1.0 --away-strength 1.0 +```json +{ + "projected_home_score": 24.8, + "projected_away_score": 22.1, + "home_win_probability": 0.557, + "away_win_probability": 0.431, + "tie_probability": 0.012, + "confidence_label": "HIGH_DATA_COVERAGE" +} ``` -### Run recursive calibration +The example shape illustrates the API contract; actual values depend on the +selected teams, point-in-time history, model version, and seed. -```powershell -.\.venv\Scripts\sports-engine.exe auto-improve ` - --csv data\point-in-time\historical.csv ` - --config AUTONOMY_CONFIG.json ` - --output reports\improvement-run-001 - -.\.venv\Scripts\sports-engine.exe verify-autonomy ` - --bundle reports\improvement-run-001\champion_bundle.json ` - --ledger reports\improvement-run-001\improvement_ledger.jsonl +## API + +| Method | Path | Purpose | +|---|---|---| +| `GET` | `/api/v1/health` | Engine and database integrity | +| `GET` | `/api/v1/leagues` | League catalog and aliases | +| `GET` | `/api/v1/leagues/{league}/teams` | Current team picker | +| `GET` | `/api/v1/week` | Rolling current-week projection board | +| `POST` | `/api/v1/simulations/hypothetical` | Reproducible matchup simulation | +| `POST` | `/api/v1/data/refresh` | Bounded public schedule refresh | + +OpenAPI is available at `/docs`. + +## Determinism and governance + +```mermaid +flowchart TD + T["Point-in-time training"] --> S["Bounded population search"] + S --> C["Chronological calibration"] + C --> W["Expert weights + interval adaptation"] + W --> G{"All promotion gates pass?"} + G -->|"Yes"| P["Freeze content-addressed champion"] + G -->|"No"| R["Retain incumbent + record failure"] + P --> O["Prospective shadow outcomes"] + R --> O + O --> D{"Drift detected?"} + D -->|"No"| W + D -->|"Yes"| Q["Quarantine, recalibrate, or roll back"] ``` -### Simulate an adaptive champion +Event streams are immutable and hash-chained. Result envelopes commit to league, +score, seed, event chain, and evidence metadata. Search cannot mutate rules, +evaluators, split boundaries, tests, or quality gates. + +## Container ```powershell -.\.venv\Scripts\sports-engine.exe simulate-adaptive ` - --bundle reports\improvement-run-001\champion_bundle.json ` - --league nba --seed 42 --home-strength 1.0 --away-strength 1.0 +docker build -t waterboy:0.4.0 . +docker run --read-only --tmpfs /tmp ` + -p 127.0.0.1:8765:8765 ` + -v waterboy-data:/data ` + waterboy:0.4.0 ``` -## Evidence and documentation +The image runs as a non-root user. Remote binding is explicit and must sit behind +an authenticated TLS reverse proxy with rate limits and network policy. + +## Documentation | Document | Purpose | |---|---| -| [Architecture](./ARCHITECTURE.md) | Kernel, fidelity, and governance boundaries | +| [Architecture](./ARCHITECTURE.md) | Kernel, application, data, and fidelity boundaries | +| [Production runbook](./PRODUCTION_RUNBOOK.md) | Bootstrap, deployment, refresh, backup, and incident response | +| [Pipeline lineage](./DATA_PIPELINE_LINEAGE.md) | Exact Dummy-to-Waterboy reuse and exclusion boundary | +| [Data rights and provenance](./DATA_RIGHTS_AND_PROVENANCE.md) | Source, caching, and permitted-use responsibilities | +| [Known limitations](./KNOWN_LIMITATIONS.md) | What Waterboy does not currently model or prove | +| [Version 0.4 evidence](./reports/RELEASE_EVIDENCE_V4.md) | Current release and browser verification | | [Autonomous improvement](./AUTONOMOUS_IMPROVEMENT.md) | Search, weighting, drift, promotion, and rollback | -| [Quality gates](./QUALITY_GATES.yaml) | Machine-readable non-negotiable release rules | -| [Release evidence](./reports/RELEASE_EVIDENCE.md) | Current validation and reproducibility snapshot | -| [Upgrade audit](./reports/AUTONOMOUS_RECURSIVE_UPGRADE_V3.md) | Version 0.3 implementation and results | -| [Known limitations](./KNOWN_LIMITATIONS.md) | Exact boundaries of demonstrated capability | -| [Prior art](./PRIOR_ART_REGISTRY.md) | Primary references and defensible distinctions | ## Evidence boundary -The bundled autonomy arena is deliberately synthetic. It proves that the engine can -discover, reject, promote, reweight, checkpoint, reload, monitor, and roll back models; -it does **not** turn synthetic outcomes into a claim of real-world accuracy. +Waterboy is a sophisticated, working simulation platform—not a magic oracle. +Its current projections omit live roster, injury, starting-lineup, probable-pitcher, +coaching, travel, official, and forecast-weather inputs. F2 spatial calibration +and F3 physics remain blocked pending licensed evidence rather than being fabricated. -Licensed point-in-time game, roster, injury, event, and tracking data are required -before held-out predictive certification. F2 spatial agents and F3 physics remain -blocked rather than fabricated. - ---- +No wagering, sportsbook, brokerage, order, or capital-execution interface exists.
-**Build measurable worlds. Preserve every decision. Believe only what survives.** +### Waterboy + +**Carry the data. Run the worlds. Show the evidence.**
diff --git a/STATUS.md b/STATUS.md index 7799431..be78262 100644 --- a/STATUS.md +++ b/STATUS.md @@ -2,52 +2,53 @@ ## Executive determination -**VERSION 0.3 ADAPTIVE AUDITED PROVISIONAL MULTI-LEAGUE ENGINE COMPLETE** - -Engineering scope is complete for deterministic F0/F1 simulation, portable replay, -counterfactual branching, batch execution, seasons, tournaments, seven league -packs, validation, negative controls, fidelity gating, CLI, and the domain-neutral -future-adaptation bridge. - -The audit added strict typing, full result-envelope integrity, temporal and phase -invariants, confidence-aware reference gates, distributional historical evaluation, -current rule-source manifests, randomized stress testing, and a distinct analytical -F0 path. - -Version 0.3 adds bounded recursive analytical calibration, deterministic population -search, online expert weighting, adaptive interval coverage, drift quarantine, -content-addressed champion checkpoints, rollback lineage, and chained promotion -ledgers. The seven-league synthetic autonomy arena was exactly reproducible, adjusted -all seven expert pools, promoted four challengers, and correctly retained three -incumbents under preregistered guardrails. - -Certification is intentionally blocked at held-out real-world calibration because -the Kimi remote workspace was not exported and no licensed point-in-time event or -tracking dataset exists in the local handoff. - -## Recovery note - -The prior remote run stopped during Stage 5 expansion. Its recovery agents remained -pending for over an hour, and the Kimi memory quota expired before the repository -was exported. This local build reconstructs the captured architecture and completes -the executable scope without claiming to possess the lost remote artifacts. - -## Verified release evidence - -- Static analysis: strict `mypy` and `ruff` passed. -- Integration/replay suite: 62 tests passed. -- Reference-anchor run: 500 games per league, 3,500 total; all seven remained - `provisional_reference_anchor`. -- Negative and mechanism controls: 49 executed, zero failures. -- Randomized stress: 14,000 games and 2,003,304 verified events, zero failures. -- Performance: F0 exceeded 137,000 games/second in every league; single-worker F1 - ranged from 372 to 1,204 games/second with full result-envelope hashing. -- At two workers, F1 throughput improved 2.18x to 4.99x versus the version 0.1 - evidence benchmark, depending on league. -- Clean wheel reproduction: package installed in a new virtual environment and - independently ran league discovery and an NHL simulation. -- Kimi desktop process: stopped. The separate Kimi CLI process was preserved. -- Recursive autonomy audit: seven leagues, eight generations each, 224 candidates - per generation wave, four promotions, three guarded retentions, seven weight - adjustments, exact bundle reload reproduction, four further prospective-shadow - weight changes, zero drift alerts, and exact shadow-checkpoint reproduction. +**VERSION 0.4 PRODUCTION APPLICATION IMPLEMENTED; REAL-WORLD PREDICTIVE +CERTIFICATION REMAINS PROVISIONAL** + +Waterboy now includes a provenance-backed sports-data layer, read-only +historical import, bounded current-scoreboard ingestion, a deterministic +recency/opponent-adjusted projection model, current-week boards, hypothetical +matchups, a versioned HTTP API, and a responsive seven-league dashboard. + +Supported user-facing categories are MLB, NFL, NCAAF, NBA, WNBA (`wba` alias), +NCAAM (`ncaam`/`ncaamb` aliases), and NHL. Projected scores are displayed to one +decimal place and the higher-probability team is marked with a check. + +## Local operational snapshot + +On 2026-07-26, the protected Dummy sports-history database was read through a +query-only SQLite connection and copied into the target repository's independent +runtime store: + +- 179,383 valid historical game rows across seven league packs. +- 107 unique games on the live seven-day board after a bounded public refresh. +- 94 scheduled games received pregame projections; live games displayed observed + scores and withheld pregame projections. +- The weekly board completed in approximately 1.1 seconds at the smoke-test + simulation tier. + +This snapshot is time-sensitive. Operators must run `refresh-data` to obtain a +new current-week view. + +## Evidence boundary + +The deterministic kernel, replay envelope, league packs, API contracts, ingestion +controls, and UI flows are testable software claims. Forecast quality is a +different claim. The application reports intervals, sample counts, data-through +times, sparse/stale evidence states, and limitations, but it is not certified as +best-in-class or profitable. + +No licensed point-in-time event/tracking dataset or independent prospective +held-out campaign is included. F2 tracking calibration, F3 physics/rendering, live +injury/roster/weather context, betting, and order execution remain outside scope. +See `KNOWN_LIMITATIONS.md`. + +## Operations + +The server binds to localhost by default. Remote binding is an explicit action and +requires an authenticated TLS reverse proxy, rate limits, monitoring, backups, and +network policy. See `PRODUCTION_RUNBOOK.md` and `DATA_PIPELINE_LINEAGE.md`. + +Historic version 0.3 evidence remains preserved in `reports/RELEASE_EVIDENCE.md`. +Version 0.4 verification is recorded separately in +`reports/RELEASE_EVIDENCE_V4.md`. diff --git a/docs/assets/waterboy-dashboard.png b/docs/assets/waterboy-dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..d6ca8708360b2f88249853f7925871ed5591b175 GIT binary patch literal 226997 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"] build-backend = "setuptools.build_meta" [project] -name = "universal-sports-engine" -version = "0.3.0" -description = "Deterministic multi-league sports simulation and counterfactual research engine" +name = "waterboy-sports-engine" +version = "0.4.0" +description = "Waterboy: deterministic multi-league sports simulation and projection engine" readme = "README.md" requires-python = ">=3.12" license = "MIT" -authors = [{ name = "Universal Sports Engine Research Program" }] -dependencies = [] +authors = [{ name = "Waterboy Research Program" }] +dependencies = [ + "fastapi>=0.116,<1", + "pydantic>=2.9,<3", + "uvicorn>=0.35,<1", +] [project.optional-dependencies] -dev = ["mypy>=1.16", "pytest>=9.0", "ruff>=0.12"] +dev = [ + "httpx>=0.28,<1", + "mypy>=1.16", + "pytest>=9.0", + "ruff>=0.12", +] [project.scripts] +waterboy = "universal_sports_engine.cli:main" sports-engine = "universal_sports_engine.cli:main" [tool.setuptools.packages.find] where = ["src"] +[tool.setuptools.package-data] +universal_sports_engine = ["web/static/*"] + [tool.pytest.ini_options] addopts = "-ra --strict-markers" testpaths = ["tests"] diff --git a/reports/RELEASE_EVIDENCE_V4.md b/reports/RELEASE_EVIDENCE_V4.md new file mode 100644 index 0000000..07df2b2 --- /dev/null +++ b/reports/RELEASE_EVIDENCE_V4.md @@ -0,0 +1,69 @@ +# Waterboy Version 0.4 Release Evidence + +## Scope + +This record covers the production application added around the existing +deterministic simulation kernel: data ingestion and provenance, current-week +projections, hypothetical matchups, HTTP API, dashboard, packaging, and deployment. + +## Verification result + +The final local release gate completed on 2026-07-26: + +| Gate | Result | +|---|---| +| Ruff | PASS | +| Strict mypy (`src`, `scripts`) | PASS, 43 source files | +| Pytest | PASS, 74 tests, zero warnings | +| Patch whitespace (`git diff --check`) | PASS | +| Browser, 1600x1000 and 1440x900 | PASS, showcase, weekly board, and matchup lab | +| Browser, 390x844 | PASS, responsive weekly board | +| Browser console | PASS, zero errors and zero warnings | +| Wheel build | PASS, `waterboy_sports_engine-0.4.0-py3-none-any.whl` | +| Wheel isolated install | PASS, CLI import and packaged dashboard assets | +| Container build | NOT RUN, Docker is not installed on the validation host | + +The wheel SHA-256 was +`470e3344ddc1a457f883a85784f96ecaa492ded78e0a79ee4d38270d821dcfe5`. +Four required static assets and both Waterboy compatibility modules were inspected +inside the wheel before an independent Python 3.12 installation. The new +`waterboy` command, legacy `sports-engine` command, `waterboy` package, and original +`universal_sports_engine` package all passed isolated smoke checks. + +Historic version 0.3 kernel evidence remains in `RELEASE_EVIDENCE.md`; it is not +rewritten here. + +## Operational smoke snapshot + +On 2026-07-26, a query-only import copied 179,383 valid historical game rows from +the protected local Dummy database into the target-owned runtime store. A bounded +seven-day public refresh produced 107 unique games, of which 94 scheduled games +received pregame projections. In-progress games showed observed scores and +withheld pregame projections. + +This is a time-sensitive operational snapshot, not held-out forecast validation. + +## Deterministic release bundle + +`scripts/build_release_evidence.py` ran with 500 reference games per league, a +100-game benchmark tier, seed `20260726`, and two workers: + +- seven of seven league reports returned `provisional_reference_anchor`; +- 49 negative/mechanism controls ran with zero failures; +- 14 F0/F1 benchmark rows were emitted; +- release reports and a content-addressed source manifest were written to the local + ignored `output/release-evidence-v4` directory. + +## UI contract + +Scheduled projection cards and hypothetical results display decimal scores with +one fractional digit. The team with the higher simulated win probability has an +accessible `✓` immediately beside its name. Final and in-progress observations +remain integer scores and are labeled as observed rather than projected. + +## Required evidence boundary + +Passing code, replay, API, browser, and package tests supports software readiness. +It does not establish forecast superiority, profitability, or real-world +certification. The model remains provisional until independently governed, +prospective, point-in-time calibration evidence exists. diff --git a/scripts/build_release_evidence.py b/scripts/build_release_evidence.py index 1a4381b..327ab1d 100644 --- a/scripts/build_release_evidence.py +++ b/scripts/build_release_evidence.py @@ -37,6 +37,7 @@ def _source_manifest(root: Path) -> tuple[tuple[str, str], ...]: root / "tests", root / "scripts", root / "data", + root / "docs", root / "dummy_bridge", ) files = tuple( @@ -55,7 +56,14 @@ def _source_manifest(root: Path) -> tuple[tuple[str, str], ...]: for path in root.iterdir() if path.is_file() and ( - path.name in {".gitattributes", ".gitignore", "LICENSE"} + path.name + in { + ".dockerignore", + ".gitattributes", + ".gitignore", + "Dockerfile", + "LICENSE", + } or path.suffix in {".json", ".md", ".toml", ".yaml"} ) ) diff --git a/src/universal_sports_engine/__init__.py b/src/universal_sports_engine/__init__.py index e1c433a..4efe776 100644 --- a/src/universal_sports_engine/__init__.py +++ b/src/universal_sports_engine/__init__.py @@ -1,4 +1,4 @@ -"""Universal Sports Engine public API.""" +"""Waterboy public API under the original import-compatible namespace.""" from universal_sports_engine.analytical import ( AdaptiveAnalyticalResult, @@ -14,7 +14,7 @@ simulate_many, ) -__version__ = "0.3.0" +__version__ = "0.4.0" __all__ = [ "AdaptiveAnalyticalResult", diff --git a/src/universal_sports_engine/autotune.py b/src/universal_sports_engine/autotune.py index 44ae19b..bfd1113 100644 --- a/src/universal_sports_engine/autotune.py +++ b/src/universal_sports_engine/autotune.py @@ -31,7 +31,7 @@ from universal_sports_engine.core.rng import normal from universal_sports_engine.reference import ANCHORS -type JsonValue = None | bool | int | float | str | list[JsonValue] | dict[str, JsonValue] +type JsonValue = bool | int | float | str | list[JsonValue] | dict[str, JsonValue] | None @dataclass(frozen=True, slots=True) diff --git a/src/universal_sports_engine/cli.py b/src/universal_sports_engine/cli.py index 636e3b5..22b8258 100644 --- a/src/universal_sports_engine/cli.py +++ b/src/universal_sports_engine/cli.py @@ -5,6 +5,7 @@ import argparse import json from dataclasses import asdict +from datetime import date from pathlib import Path from universal_sports_engine.accuracy import ( @@ -32,12 +33,16 @@ from universal_sports_engine.core.types import GameResult from universal_sports_engine.league_packs.registry import league_identities, league_ids from universal_sports_engine.season import round_robin_schedule, simulate_season +from universal_sports_engine.settings import DEFAULT_CACHE_DIR, DEFAULT_DATA_PATH from universal_sports_engine.simulation import counterfactual_game, simulate_game from universal_sports_engine.validation import validate_all, validate_league def _parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(prog="sports-engine") + parser = argparse.ArgumentParser( + prog="waterboy", + description="Waterboy deterministic multi-league sports simulation engine", + ) subparsers = parser.add_subparsers(dest="command", required=True) subparsers.add_parser("list-leagues") @@ -106,6 +111,51 @@ def _parser() -> argparse.ArgumentParser: counterfactual.add_argument("--seed", required=True, type=int) counterfactual.add_argument("--home-strength", required=True, type=float) counterfactual.add_argument("--away-strength", required=True, type=float) + + serve = subparsers.add_parser("serve") + serve.add_argument("--host", default="127.0.0.1") + serve.add_argument("--port", default=8765, type=int) + serve.add_argument("--data", default=DEFAULT_DATA_PATH, type=Path) + serve.add_argument("--cache", default=DEFAULT_CACHE_DIR, type=Path) + serve.add_argument("--allow-remote", action="store_true") + + refresh = subparsers.add_parser("refresh-data") + refresh.add_argument("--data", default=DEFAULT_DATA_PATH, type=Path) + refresh.add_argument("--cache", default=DEFAULT_CACHE_DIR, type=Path) + refresh.add_argument("--start", type=date.fromisoformat) + refresh.add_argument("--days", default=7, type=int) + refresh.add_argument("--history-days", default=0, type=int) + refresh.add_argument("--force", action="store_true") + + import_dummy = subparsers.add_parser("import-dummy-history") + import_dummy.add_argument("--data", default=DEFAULT_DATA_PATH, type=Path) + import_dummy.add_argument("--cache", default=DEFAULT_CACHE_DIR, type=Path) + import_dummy.add_argument( + "--source", + default=Path(r"C:\src\engine\dummy\runtime\autonomy\sports_history.db"), + type=Path, + ) + + week = subparsers.add_parser("project-week") + week.add_argument("--data", default=DEFAULT_DATA_PATH, type=Path) + week.add_argument("--cache", default=DEFAULT_CACHE_DIR, type=Path) + week.add_argument("--start", type=date.fromisoformat) + week.add_argument("--days", default=7, type=int) + week.add_argument("--league") + week.add_argument("--simulations", default=2_000, type=int) + week.add_argument("--seed", default=20_260_726, type=int) + + matchup = subparsers.add_parser("simulate-matchup") + matchup.add_argument("--data", default=DEFAULT_DATA_PATH, type=Path) + matchup.add_argument("--cache", default=DEFAULT_CACHE_DIR, type=Path) + matchup.add_argument("--league", required=True) + matchup.add_argument("--home", required=True) + matchup.add_argument("--away", required=True) + matchup.add_argument("--home-name") + matchup.add_argument("--away-name") + matchup.add_argument("--neutral-site", action="store_true") + matchup.add_argument("--simulations", default=10_000, type=int) + matchup.add_argument("--seed", default=20_260_726, type=int) return parser @@ -288,6 +338,75 @@ def main() -> None: "away_score_delta": paired.away_score_delta, "shared_seed": paired.shared_seed, } + elif args.command == "serve": + from universal_sports_engine.web_app import run_server + + run_server( + host=args.host, + port=args.port, + data_path=args.data, + cache_dir=args.cache, + allow_remote=args.allow_remote, + ) + return + elif args.command in { + "refresh-data", + "import-dummy-history", + "project-week", + "simulate-matchup", + }: + from universal_sports_engine.service import SportsEngineService + + service = SportsEngineService.from_paths(args.data, args.cache) + if args.command == "refresh-data": + if args.history_days: + ingest_results = service.pipeline.refresh_history( + lookback_days=args.history_days, + force=args.force, + ) + output = { + "status": ( + "ok" + if all(result.status == "ok" for result in ingest_results) + else "partial" + ), + "results": [asdict(result) for result in ingest_results], + "remote_writes": 0, + } + else: + output = service.refresh_current_week( + start=args.start, + days=args.days, + force=args.force, + ) + elif args.command == "import-dummy-history": + output = { + "status": "ok", + "source_mode": "sqlite_query_only", + "results": [ + asdict(result) + for result in service.pipeline.import_dummy_history(args.source) + ], + } + elif args.command == "project-week": + output = service.week( + start=args.start, + days=args.days, + league=args.league, + simulations=args.simulations, + seed=args.seed, + ) + else: + output = service.hypothetical( + league=args.league, + home_team_id=args.home, + away_team_id=args.away, + home_team_name=args.home_name, + away_team_name=args.away_name, + neutral_site=args.neutral_site, + simulations=args.simulations, + seed=args.seed, + ) else: raise AssertionError(f"Unhandled command: {args.command}") print(json.dumps(output, indent=2, sort_keys=True)) diff --git a/src/universal_sports_engine/data/__init__.py b/src/universal_sports_engine/data/__init__.py new file mode 100644 index 0000000..85dc39b --- /dev/null +++ b/src/universal_sports_engine/data/__init__.py @@ -0,0 +1,13 @@ +"""Read-only public sports data ingestion and local persistence.""" + +from universal_sports_engine.data.models import GameRecord, IngestResult, TeamRecord +from universal_sports_engine.data.pipeline import SportsDataPipeline +from universal_sports_engine.data.store import SportsDataStore + +__all__ = [ + "GameRecord", + "IngestResult", + "SportsDataPipeline", + "SportsDataStore", + "TeamRecord", +] diff --git a/src/universal_sports_engine/data/espn.py b/src/universal_sports_engine/data/espn.py new file mode 100644 index 0000000..537bc62 --- /dev/null +++ b/src/universal_sports_engine/data/espn.py @@ -0,0 +1,312 @@ +"""Keyless, read-only ESPN scoreboard adapter. + +The normalized contract is derived from Dummy's proven sports intake boundary, +but this module is standalone and contains no market, sportsbook, execution, or +credential behavior. +""" + +from __future__ import annotations + +import hashlib +import json +import time +from collections.abc import Callable +from datetime import UTC, date, datetime +from pathlib import Path +from typing import Any, cast +from urllib.error import HTTPError, URLError +from urllib.parse import urlencode +from urllib.request import Request, urlopen + +from universal_sports_engine.data.models import GameRecord +from universal_sports_engine.league_packs.registry import normalize_league_id + +LEAGUE_TO_ESPN: dict[str, tuple[str, str]] = { + "mlb": ("baseball", "mlb"), + "nba": ("basketball", "nba"), + "wnba": ("basketball", "wnba"), + "ncaambb": ("basketball", "mens-college-basketball"), + "nfl": ("football", "nfl"), + "ncaaf": ("football", "college-football"), + "nhl": ("hockey", "nhl"), +} + +USER_AGENT = ( + "WaterboySportsEngine/0.4 " + "(read-only public schedule research; https://github.com/ObtuseAI/waterboy)" +) +MAX_RESPONSE_BYTES = 32 * 1024 * 1024 +Transport = Callable[[str, float], bytes] + + +class ScoreboardFeedError(RuntimeError): + """A public feed failed or returned an invalid payload.""" + + +def _utc_now() -> datetime: + return datetime.now(UTC) + + +def _as_dict(value: object) -> dict[str, Any]: + return cast(dict[str, Any], value) if isinstance(value, dict) else {} + + +def _as_list(value: object) -> list[Any]: + return value if isinstance(value, list) else [] + + +def _integer_score(value: object) -> int | None: + if value is None or isinstance(value, bool): + return None + try: + numeric = float(str(value)) + except ValueError: + return None + return int(numeric) if numeric >= 0 and numeric.is_integer() else None + + +def _season_from(event: dict[str, Any], start_time: str) -> int | None: + season = _as_dict(event.get("season")).get("year") + try: + return int(str(season)) if season is not None else None + except (TypeError, ValueError): + try: + return datetime.fromisoformat(start_time.replace("Z", "+00:00")).year + except ValueError: + return None + + +def scoreboard_url(league: str) -> str: + normalized = normalize_league_id(league) + try: + sport, espn_league = LEAGUE_TO_ESPN[normalized] + except KeyError as exc: + raise ValueError(f"ESPN scoreboard is not configured for league: {league!r}") from exc + return f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{espn_league}/scoreboard" + + +def parse_scoreboard( + league: str, + payload: dict[str, Any], + *, + observed_at: str, + source_url: str, +) -> tuple[GameRecord, ...]: + """Normalize a scoreboard payload while rejecting incomplete competitions.""" + + normalized = normalize_league_id(league) + games: list[GameRecord] = [] + for raw_event in _as_list(payload.get("events")): + event = _as_dict(raw_event) + event_id = str(event.get("id") or "").strip() + start_time = str(event.get("date") or "").strip() + competitions = _as_list(event.get("competitions")) + if not event_id or not start_time or not competitions: + continue + competition = _as_dict(competitions[0]) + home: dict[str, Any] | None = None + away: dict[str, Any] | None = None + for raw_competitor in _as_list(competition.get("competitors")): + competitor = _as_dict(raw_competitor) + if competitor.get("homeAway") == "home": + home = competitor + elif competitor.get("homeAway") == "away": + away = competitor + if home is None or away is None: + continue + home_team = _as_dict(home.get("team")) + away_team = _as_dict(away.get("team")) + home_id = str(home_team.get("abbreviation") or home_team.get("id") or "").upper() + away_id = str(away_team.get("abbreviation") or away_team.get("id") or "").upper() + if not home_id or not away_id or home_id == away_id: + continue + state = str( + _as_dict(_as_dict(competition.get("status")).get("type")).get("state") or "pre" + ).lower() + status = {"pre": "scheduled", "in": "in_progress", "post": "final"}.get( + state, "scheduled" + ) + home_score = _integer_score(home.get("score")) if status != "scheduled" else None + away_score = _integer_score(away.get("score")) if status != "scheduled" else None + if status == "final" and (home_score is None or away_score is None): + status = "unresolved" + venue_data = _as_dict(competition.get("venue")) + canonical_event = json.dumps(event, sort_keys=True, separators=(",", ":")).encode() + games.append( + GameRecord( + game_id=f"espn:{normalized}:{event_id}", + league=normalized, + season=_season_from(event, start_time), + start_time=start_time, + status=status, + home_team_id=home_id, + away_team_id=away_id, + home_name=str( + home_team.get("displayName") + or home_team.get("shortDisplayName") + or home_id + ), + away_name=str( + away_team.get("displayName") + or away_team.get("shortDisplayName") + or away_id + ), + home_score=home_score, + away_score=away_score, + neutral_site=bool(competition.get("neutralSite", False)), + venue=str(venue_data.get("fullName")) if venue_data.get("fullName") else None, + source="espn_public_scoreboard", + source_url=source_url, + observed_at=observed_at, + payload_hash=hashlib.sha256(canonical_event).hexdigest(), + ) + ) + return tuple(games) + + +def _default_transport(url: str, timeout_seconds: float) -> bytes: + request = Request( + url, + headers={"Accept": "application/json", "User-Agent": USER_AGENT}, + method="GET", + ) + with urlopen(request, timeout=timeout_seconds) as response: + content_length = response.headers.get("Content-Length") + if content_length is not None and int(content_length) > MAX_RESPONSE_BYTES: + raise ScoreboardFeedError("Scoreboard response exceeds the configured size limit") + body = cast(bytes, response.read(MAX_RESPONSE_BYTES + 1)) + if len(body) > MAX_RESPONSE_BYTES: + raise ScoreboardFeedError("Scoreboard response exceeds the configured size limit") + return body + + +class EspnScoreboardClient: + """Bounded public GET client with deterministic on-disk response caching.""" + + def __init__( + self, + cache_dir: Path, + *, + cache_ttl_seconds: int = 900, + timeout_seconds: float = 20.0, + max_retries: int = 2, + transport: Transport | None = None, + ) -> None: + self.cache_dir = cache_dir + self.cache_dir.mkdir(parents=True, exist_ok=True) + self.cache_ttl_seconds = cache_ttl_seconds + self.timeout_seconds = timeout_seconds + self.max_retries = max_retries + self.transport = transport or _default_transport + + def _cache_path(self, url: str) -> Path: + return self.cache_dir / f"{hashlib.sha256(url.encode()).hexdigest()}.json" + + def _cached(self, path: Path) -> tuple[dict[str, Any], str] | None: + try: + modified_at = path.stat().st_mtime + age = time.time() - modified_at + if age > self.cache_ttl_seconds: + return None + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError, json.JSONDecodeError): + return None + document = _as_dict(value) + if ( + document.get("cache_version") == 1 + and isinstance(document.get("observed_at"), str) + and isinstance(document.get("payload"), dict) + ): + return _as_dict(document["payload"]), str(document["observed_at"]) + if document: + legacy_observed_at = datetime.fromtimestamp(modified_at, UTC).isoformat() + return document, legacy_observed_at + return None + + def _write_cache( + self, + path: Path, + payload: dict[str, Any], + observed_at: str, + ) -> None: + temporary = path.with_suffix(".tmp") + temporary.write_text( + json.dumps( + { + "cache_version": 1, + "observed_at": observed_at, + "payload": payload, + }, + separators=(",", ":"), + sort_keys=True, + ), + encoding="utf-8", + ) + temporary.replace(path) + + def games( + self, + league: str, + start: date, + end: date, + *, + force: bool = False, + ) -> tuple[GameRecord, ...]: + if end < start: + raise ValueError(f"Scoreboard end precedes start: {start} > {end}") + if (end - start).days > 31: + raise ValueError("One scoreboard request may cover at most 32 days") + base_url = scoreboard_url(league) + parameters = { + "dates": ( + f"{start:%Y%m%d}" + if start == end + else f"{start:%Y%m%d}-{end:%Y%m%d}" + ), + "limit": "1000", + } + request_url = f"{base_url}?{urlencode(parameters)}" + cache_path = self._cache_path(request_url) + cached = None if force else self._cached(cache_path) + if cached is None: + body: bytes | None = None + last_error: Exception | None = None + for attempt in range(self.max_retries + 1): + try: + body = self.transport(request_url, self.timeout_seconds) + break + except (HTTPError, URLError, TimeoutError, OSError) as exc: + last_error = exc + if attempt < self.max_retries: + time.sleep(0.5 * (2**attempt)) + if body is None: + if start != end: + merged: dict[str, GameRecord] = {} + cursor = start + while cursor <= end: + for game in self.games(league, cursor, cursor, force=force): + merged[game.game_id] = game + cursor = date.fromordinal(cursor.toordinal() + 1) + return tuple( + sorted(merged.values(), key=lambda game: (game.start_time, game.game_id)) + ) + raise ScoreboardFeedError( + f"ESPN scoreboard request failed after retries: {type(last_error).__name__}" + ) from last_error + try: + parsed = json.loads(body) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise ScoreboardFeedError("ESPN scoreboard returned invalid JSON") from exc + payload = _as_dict(parsed) + if not payload: + raise ScoreboardFeedError("ESPN scoreboard returned an empty object") + observed_at = _utc_now().isoformat() + self._write_cache(cache_path, payload, observed_at) + else: + payload, observed_at = cached + return parse_scoreboard( + league, + payload, + observed_at=observed_at, + source_url=request_url, + ) diff --git a/src/universal_sports_engine/data/models.py b/src/universal_sports_engine/data/models.py new file mode 100644 index 0000000..dffca2d --- /dev/null +++ b/src/universal_sports_engine/data/models.py @@ -0,0 +1,54 @@ +"""Typed records shared by sports data sources, storage, and projection.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class GameRecord: + """One source-backed game observation. + + Scores are populated only for completed or in-progress games. Scheduled + records never receive synthetic scores in the data layer. + """ + + game_id: str + league: str + season: int | None + start_time: str + status: str + home_team_id: str + away_team_id: str + home_name: str + away_name: str + home_score: int | None + away_score: int | None + neutral_site: bool + venue: str | None + source: str + source_url: str + observed_at: str + payload_hash: str + + +@dataclass(frozen=True, slots=True) +class TeamRecord: + league: str + team_id: str + display_name: str + games: int + last_game_at: str | None + + +@dataclass(frozen=True, slots=True) +class IngestResult: + source: str + league: str + window_start: str + window_end: str + fetched: int + stored: int + status: str + observed_at: str + error: str | None = None diff --git a/src/universal_sports_engine/data/pipeline.py b/src/universal_sports_engine/data/pipeline.py new file mode 100644 index 0000000..7c54e2d --- /dev/null +++ b/src/universal_sports_engine/data/pipeline.py @@ -0,0 +1,255 @@ +"""Standalone sports data pipeline with an optional read-only Dummy bootstrap.""" + +from __future__ import annotations + +import hashlib +import json +import sqlite3 +from collections.abc import Iterable +from contextlib import closing +from datetime import UTC, date, datetime, timedelta +from pathlib import Path + +from universal_sports_engine.data.espn import EspnScoreboardClient +from universal_sports_engine.data.models import GameRecord, IngestResult +from universal_sports_engine.data.store import SportsDataStore +from universal_sports_engine.league_packs.registry import league_ids, normalize_league_id + +DUMMY_LEAGUE_ALIASES = {"ncaamb": "ncaambb"} + + +def _utc_now_iso() -> str: + return datetime.now(UTC).isoformat() + + +def _chunks(start: date, end: date, days: int) -> Iterable[tuple[date, date]]: + cursor = start + while cursor <= end: + chunk_end = min(end, cursor + timedelta(days=days - 1)) + yield cursor, chunk_end + cursor = chunk_end + timedelta(days=1) + + +class SportsDataPipeline: + """Orchestrate isolated, fail-soft league ingestion runs.""" + + def __init__(self, store: SportsDataStore, client: EspnScoreboardClient) -> None: + self.store = store + self.client = client + + def refresh_window( + self, + start: date, + end: date, + *, + leagues: tuple[str, ...] | None = None, + force: bool = False, + ) -> tuple[IngestResult, ...]: + selected = leagues or league_ids() + results: list[IngestResult] = [] + for requested in selected: + league = normalize_league_id(requested) + observed_at = _utc_now_iso() + try: + games = self.client.games(league, start, end, force=force) + stored = self.store.upsert_games(games) + result = IngestResult( + source="espn_public_scoreboard", + league=league, + window_start=start.isoformat(), + window_end=end.isoformat(), + fetched=len(games), + stored=stored, + status="ok", + observed_at=observed_at, + ) + except Exception as exc: + result = IngestResult( + source="espn_public_scoreboard", + league=league, + window_start=start.isoformat(), + window_end=end.isoformat(), + fetched=0, + stored=0, + status="error", + observed_at=observed_at, + error=f"{type(exc).__name__}: {str(exc)[:240]}", + ) + self.store.record_ingest(result) + results.append(result) + return tuple(results) + + def refresh_current_week( + self, + *, + start: date | None = None, + days: int = 7, + force: bool = False, + ) -> tuple[IngestResult, ...]: + if not 1 <= days <= 14: + raise ValueError(f"Current-week days must be within [1, 14]: {days}") + window_start = start or datetime.now(UTC).date() + return self.refresh_window( + window_start, + window_start + timedelta(days=days - 1), + force=force, + ) + + def refresh_history( + self, + *, + lookback_days: int = 180, + chunk_days: int = 28, + leagues: tuple[str, ...] | None = None, + force: bool = False, + ) -> tuple[IngestResult, ...]: + if not 1 <= lookback_days <= 1_095: + raise ValueError(f"History lookback must be within [1, 1095]: {lookback_days}") + if not 1 <= chunk_days <= 32: + raise ValueError(f"History chunk size must be within [1, 32]: {chunk_days}") + end = datetime.now(UTC).date() + start = end - timedelta(days=lookback_days) + results: list[IngestResult] = [] + for chunk_start, chunk_end in _chunks(start, end, chunk_days): + results.extend( + self.refresh_window( + chunk_start, + chunk_end, + leagues=leagues, + force=force, + ) + ) + return tuple(results) + + def import_dummy_history( + self, + source_database: Path, + *, + batch_size: int = 2_000, + ) -> tuple[IngestResult, ...]: + """Copy sports rows through a query-only connection. + + The Dummy database is never migrated, vacuumed, checkpointed, or + modified. All normalized writes go only to this engine's store. + """ + + if batch_size <= 0: + raise ValueError(f"batch_size must be positive: {batch_size}") + if not source_database.is_file(): + raise FileNotFoundError(f"Dummy sports history database is missing: {source_database}") + observed_at = _utc_now_iso() + counts: dict[str, int] = {} + with closing( + sqlite3.connect( + f"file:{source_database.as_posix()}?mode=ro", + uri=True, + timeout=5, + ) + ) as connection: + connection.row_factory = sqlite3.Row + connection.execute("PRAGMA query_only=ON") + cursor = connection.execute( + """ + SELECT game_id, league, season, start_time, status, home, away, + home_score, away_score, venue, neutral, source, + provenance_url, result_available_at, received_at, as_of, extra + FROM games ORDER BY league, start_time, game_id + """ + ) + while True: + rows = cursor.fetchmany(batch_size) + if not rows: + break + normalized_rows: list[GameRecord] = [] + for row in rows: + if not row["home"] or not row["away"] or not row["start_time"]: + continue + home_team_id = str(row["home"]).upper() + away_team_id = str(row["away"]).upper() + if home_team_id == away_team_id: + continue + league = DUMMY_LEAGUE_ALIASES.get( + str(row["league"]), normalize_league_id(str(row["league"])) + ) + if league not in league_ids(): + continue + raw_status = str(row["status"]).lower() + status = { + "post": "final", + "final": "final", + "in": "in_progress", + "in_progress": "in_progress", + "scheduled": "scheduled", + "pre": "scheduled", + }.get(raw_status, "unresolved") + source = str(row["source"] or "dummy_history") + external_id = str(row["game_id"]) + identity = f"{source}:{league}:{external_id}" + extra = str(row["extra"] or "") + content = json.dumps( + { + "game_id": external_id, + "league": league, + "start_time": row["start_time"], + "status": status, + "home": row["home"], + "away": row["away"], + "home_score": row["home_score"], + "away_score": row["away_score"], + "extra": extra, + }, + sort_keys=True, + separators=(",", ":"), + ).encode() + normalized_rows.append( + GameRecord( + game_id=( + f"espn:{league}:{external_id}" + if source == "espn" + else hashlib.sha256(identity.encode()).hexdigest() + ), + league=league, + season=int(row["season"]) if row["season"] is not None else None, + start_time=str(row["start_time"]), + status=status, + home_team_id=home_team_id, + away_team_id=away_team_id, + home_name=str(row["home"]), + away_name=str(row["away"]), + home_score=( + int(row["home_score"]) if row["home_score"] is not None else None + ), + away_score=( + int(row["away_score"]) if row["away_score"] is not None else None + ), + neutral_site=bool(row["neutral"]), + venue=str(row["venue"]) if row["venue"] else None, + source=f"dummy_read_only:{source}", + source_url=str(row["provenance_url"] or "local-read-only-import"), + observed_at=str( + row["received_at"] + or row["result_available_at"] + or row["as_of"] + or observed_at + ), + payload_hash=hashlib.sha256(content).hexdigest(), + ) + ) + counts[league] = counts.get(league, 0) + 1 + self.store.upsert_games(normalized_rows) + results: list[IngestResult] = [] + for league in league_ids(): + count = counts.get(league, 0) + result = IngestResult( + source="dummy_sports_history_read_only_import", + league=league, + window_start="historical", + window_end="historical", + fetched=count, + stored=count, + status="ok", + observed_at=observed_at, + ) + self.store.record_ingest(result) + results.append(result) + return tuple(results) diff --git a/src/universal_sports_engine/data/store.py b/src/universal_sports_engine/data/store.py new file mode 100644 index 0000000..a73313d --- /dev/null +++ b/src/universal_sports_engine/data/store.py @@ -0,0 +1,312 @@ +"""SQLite-backed local sports history with explicit source provenance.""" + +from __future__ import annotations + +import sqlite3 +from collections.abc import Iterable +from contextlib import closing +from pathlib import Path +from typing import Any, cast + +from universal_sports_engine.data.models import GameRecord, IngestResult, TeamRecord +from universal_sports_engine.league_packs.registry import normalize_league_id + +SCHEMA_VERSION = 1 + + +class SportsDataStore: + """Small, concurrency-safe local history database. + + Connections are short lived so FastAPI workers and refresh commands do not + share mutable sqlite connection state. + """ + + def __init__(self, path: Path) -> None: + self.path = path + self.path.parent.mkdir(parents=True, exist_ok=True) + self.initialize() + + def _connect(self, *, read_only: bool = False) -> sqlite3.Connection: + if read_only: + connection = sqlite3.connect( + f"file:{self.path.as_posix()}?mode=ro", + uri=True, + timeout=5, + ) + else: + connection = sqlite3.connect(self.path, timeout=10) + connection.row_factory = sqlite3.Row + connection.execute("PRAGMA busy_timeout=5000") + connection.execute("PRAGMA foreign_keys=ON") + return connection + + def initialize(self) -> None: + with closing(self._connect()) as connection: + connection.execute("PRAGMA journal_mode=WAL") + connection.executescript( + """ + CREATE TABLE IF NOT EXISTS metadata( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS games( + game_id TEXT PRIMARY KEY, + league TEXT NOT NULL, + season INTEGER, + start_time TEXT NOT NULL, + status TEXT NOT NULL, + home_team_id TEXT NOT NULL, + away_team_id TEXT NOT NULL, + home_name TEXT NOT NULL, + away_name TEXT NOT NULL, + home_score INTEGER, + away_score INTEGER, + neutral_site INTEGER NOT NULL DEFAULT 0, + venue TEXT, + source TEXT NOT NULL, + source_url TEXT NOT NULL, + observed_at TEXT NOT NULL, + payload_hash TEXT NOT NULL, + CHECK(home_team_id <> away_team_id), + CHECK(home_score IS NULL OR home_score >= 0), + CHECK(away_score IS NULL OR away_score >= 0) + ); + CREATE INDEX IF NOT EXISTS ix_games_league_start + ON games(league, start_time); + CREATE INDEX IF NOT EXISTS ix_games_league_status + ON games(league, status, start_time); + CREATE TABLE IF NOT EXISTS ingest_runs( + id INTEGER PRIMARY KEY AUTOINCREMENT, + source TEXT NOT NULL, + league TEXT NOT NULL, + window_start TEXT NOT NULL, + window_end TEXT NOT NULL, + fetched INTEGER NOT NULL, + stored INTEGER NOT NULL, + status TEXT NOT NULL, + observed_at TEXT NOT NULL, + error TEXT + ); + CREATE INDEX IF NOT EXISTS ix_ingest_source_league + ON ingest_runs(source, league, id DESC); + """ + ) + connection.execute( + """ + INSERT INTO metadata(key, value) VALUES('schema_version', ?) + ON CONFLICT(key) DO UPDATE SET value=excluded.value + """, + (str(SCHEMA_VERSION),), + ) + connection.commit() + + def upsert_games(self, games: Iterable[GameRecord]) -> int: + rows = tuple(games) + if not rows: + return 0 + values = [ + ( + game.game_id, + game.league, + game.season, + game.start_time, + game.status, + game.home_team_id, + game.away_team_id, + game.home_name, + game.away_name, + game.home_score, + game.away_score, + int(game.neutral_site), + game.venue, + game.source, + game.source_url, + game.observed_at, + game.payload_hash, + ) + for game in rows + ] + with closing(self._connect()) as connection: + connection.executemany( + """ + INSERT INTO games( + game_id, league, season, start_time, status, + home_team_id, away_team_id, home_name, away_name, + home_score, away_score, neutral_site, venue, + source, source_url, observed_at, payload_hash + ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ON CONFLICT(game_id) DO UPDATE SET + season=excluded.season, + start_time=excluded.start_time, + status=excluded.status, + home_team_id=excluded.home_team_id, + away_team_id=excluded.away_team_id, + home_name=excluded.home_name, + away_name=excluded.away_name, + home_score=excluded.home_score, + away_score=excluded.away_score, + neutral_site=excluded.neutral_site, + venue=excluded.venue, + source=excluded.source, + source_url=excluded.source_url, + observed_at=excluded.observed_at, + payload_hash=excluded.payload_hash + """, + values, + ) + connection.commit() + return len(rows) + + def record_ingest(self, result: IngestResult) -> None: + with closing(self._connect()) as connection: + connection.execute( + """ + INSERT INTO ingest_runs( + source, league, window_start, window_end, fetched, stored, + status, observed_at, error + ) VALUES(?,?,?,?,?,?,?,?,?) + """, + ( + result.source, + result.league, + result.window_start, + result.window_end, + result.fetched, + result.stored, + result.status, + result.observed_at, + result.error, + ), + ) + connection.commit() + + @staticmethod + def _game(row: sqlite3.Row) -> GameRecord: + return GameRecord( + game_id=str(row["game_id"]), + league=str(row["league"]), + season=int(row["season"]) if row["season"] is not None else None, + start_time=str(row["start_time"]), + status=str(row["status"]), + home_team_id=str(row["home_team_id"]), + away_team_id=str(row["away_team_id"]), + home_name=str(row["home_name"]), + away_name=str(row["away_name"]), + home_score=int(row["home_score"]) if row["home_score"] is not None else None, + away_score=int(row["away_score"]) if row["away_score"] is not None else None, + neutral_site=bool(row["neutral_site"]), + venue=str(row["venue"]) if row["venue"] is not None else None, + source=str(row["source"]), + source_url=str(row["source_url"]), + observed_at=str(row["observed_at"]), + payload_hash=str(row["payload_hash"]), + ) + + def games_between( + self, + start_time: str, + end_time: str, + *, + league: str | None = None, + ) -> tuple[GameRecord, ...]: + query = "SELECT * FROM games WHERE start_time >= ? AND start_time < ?" + parameters: list[object] = [start_time, end_time] + if league is not None: + query += " AND league = ?" + parameters.append(normalize_league_id(league)) + query += " ORDER BY start_time, league, game_id" + with closing(self._connect(read_only=True)) as connection: + rows = connection.execute(query, parameters).fetchall() + return tuple(self._game(row) for row in rows) + + def completed_games( + self, + league: str, + *, + before: str | None = None, + limit: int = 10_000, + ) -> tuple[GameRecord, ...]: + normalized = normalize_league_id(league) + query = ( + "SELECT * FROM games WHERE league=? AND status='final' " + "AND home_score IS NOT NULL AND away_score IS NOT NULL" + ) + parameters: list[object] = [normalized] + if before is not None: + query += " AND start_time < ?" + parameters.append(before) + query += " ORDER BY start_time DESC LIMIT ?" + parameters.append(max(1, limit)) + with closing(self._connect(read_only=True)) as connection: + rows = connection.execute(query, parameters).fetchall() + return tuple(reversed(tuple(self._game(row) for row in rows))) + + def teams(self, league: str) -> tuple[TeamRecord, ...]: + normalized = normalize_league_id(league) + with closing(self._connect(read_only=True)) as connection: + rows = connection.execute( + """ + WITH appearances AS ( + SELECT league, home_team_id AS team_id, home_name AS display_name, + start_time + FROM games WHERE league=? + UNION ALL + SELECT league, away_team_id AS team_id, away_name AS display_name, + start_time + FROM games WHERE league=? + ), + latest AS ( + SELECT league, team_id, MAX(start_time) AS last_game_at, + COUNT(*) AS games + FROM appearances GROUP BY league, team_id + ) + SELECT latest.league, latest.team_id, appearances.display_name, + latest.games, latest.last_game_at + FROM latest + JOIN appearances + ON appearances.league=latest.league + AND appearances.team_id=latest.team_id + AND appearances.start_time=latest.last_game_at + GROUP BY latest.league, latest.team_id + ORDER BY appearances.display_name, latest.team_id + """, + (normalized, normalized), + ).fetchall() + return tuple( + TeamRecord( + league=str(row["league"]), + team_id=str(row["team_id"]), + display_name=str(row["display_name"]), + games=int(row["games"]), + last_game_at=str(row["last_game_at"]) if row["last_game_at"] else None, + ) + for row in rows + ) + + def health(self) -> dict[str, Any]: + with closing(self._connect(read_only=True)) as connection: + integrity = str(connection.execute("PRAGMA quick_check").fetchone()[0]) + game_rows = connection.execute( + """ + SELECT league, COUNT(*) AS games, + SUM(CASE WHEN status='final' THEN 1 ELSE 0 END) AS finals, + MAX(observed_at) AS last_observed_at + FROM games GROUP BY league ORDER BY league + """ + ).fetchall() + latest_ingest = connection.execute( + """ + SELECT source, league, status, observed_at, error + FROM ingest_runs ORDER BY id DESC LIMIT 1 + """ + ).fetchone() + return { + "database": self.path.name, + "integrity": integrity, + "leagues": [dict(cast(Iterable[tuple[str, Any]], row)) for row in game_rows], + "latest_ingest": ( + dict(cast(Iterable[tuple[str, Any]], latest_ingest)) + if latest_ingest is not None + else None + ), + } diff --git a/src/universal_sports_engine/league_packs/common.py b/src/universal_sports_engine/league_packs/common.py index 62aaa5f..6ff30d1 100644 --- a/src/universal_sports_engine/league_packs/common.py +++ b/src/universal_sports_engine/league_packs/common.py @@ -17,7 +17,7 @@ payload, ) -ENGINE_VERSION = "0.3.0" +ENGINE_VERSION = "0.4.0" @dataclass(frozen=True, slots=True) diff --git a/src/universal_sports_engine/league_packs/registry.py b/src/universal_sports_engine/league_packs/registry.py index 2bbc96e..14c3c7f 100644 --- a/src/universal_sports_engine/league_packs/registry.py +++ b/src/universal_sports_engine/league_packs/registry.py @@ -15,6 +15,20 @@ Simulator = Callable[[int, float, float], GameResult] +LEAGUE_ALIASES = { + "wba": "wnba", + "ncaam": "ncaambb", + "ncaamb": "ncaambb", + "ncaa-m": "ncaambb", +} + + +def normalize_league_id(league: str) -> str: + """Normalize supported public aliases to the stable internal league ID.""" + + normalized = league.strip().lower() + return LEAGUE_ALIASES.get(normalized, normalized) + def league_identities() -> tuple[LeagueIdentity, ...]: """Return every supported league in stable order.""" @@ -31,7 +45,7 @@ def league_ids() -> tuple[str, ...]: def simulator_for(league: str) -> Simulator: """Resolve a simulator or fail explicitly with the supported set.""" - normalized = league.strip().lower() + normalized = normalize_league_id(league) if normalized == "mlb": return simulate_mlb if normalized in BASKETBALL_CONFIGS: diff --git a/src/universal_sports_engine/projection.py b/src/universal_sports_engine/projection.py new file mode 100644 index 0000000..771f2f8 --- /dev/null +++ b/src/universal_sports_engine/projection.py @@ -0,0 +1,493 @@ +"""Team-aware probabilistic score projections and hypothetical matchups.""" + +from __future__ import annotations + +import hashlib +import json +import math +import statistics +from dataclasses import asdict, dataclass +from datetime import UTC, datetime + +from universal_sports_engine.analytical import simulate_analytical +from universal_sports_engine.core.rng import normal +from universal_sports_engine.data.models import GameRecord +from universal_sports_engine.data.store import SportsDataStore +from universal_sports_engine.league_packs.registry import normalize_league_id +from universal_sports_engine.reference import ANCHORS, ReferenceAnchor + +PROJECTION_MODEL_VERSION = "hierarchical-recency-opponent-adjusted-v1" + + +@dataclass(frozen=True, slots=True) +class ProjectionConfig: + half_life_days: float + prior_games: float + posterior_strength_sd: float + minimum_effective_games: float + + +PROJECTION_CONFIGS = { + "mlb": ProjectionConfig(35.0, 12.0, 0.10, 8.0), + "nba": ProjectionConfig(55.0, 10.0, 0.08, 8.0), + "wnba": ProjectionConfig(45.0, 8.0, 0.09, 6.0), + "ncaambb": ProjectionConfig(65.0, 10.0, 0.11, 8.0), + "nfl": ProjectionConfig(120.0, 5.0, 0.12, 4.0), + "ncaaf": ProjectionConfig(110.0, 5.0, 0.14, 4.0), + "nhl": ProjectionConfig(55.0, 10.0, 0.10, 8.0), +} + + +@dataclass(frozen=True, slots=True) +class TeamRating: + team_id: str + offense: float + defense: float + effective_games: float + games: int + uncertainty: float + last_game_at: str | None + data_age_days: float | None + status: str + + +@dataclass(frozen=True, slots=True) +class ScoreInterval: + low: int + high: int + + +@dataclass(frozen=True, slots=True) +class MatchupProjection: + projection_id: str + model_version: str + league: str + home_team_id: str + away_team_id: str + home_team_name: str + away_team_name: str + neutral_site: bool + simulations: int + seed: int + projected_home_score: float + projected_away_score: float + projected_total: float + projected_margin: float + home_win_probability: float + away_win_probability: float + tie_probability: float + home_score_interval_80: ScoreInterval + away_score_interval_80: ScoreInterval + total_interval_80: ScoreInterval + home_rating: TeamRating + away_rating: TeamRating + data_games: int + data_through: str | None + source_counts: tuple[tuple[str, int], ...] + confidence_label: str + evidence_status: str + generated_at: str + limitations: tuple[str, ...] + + +@dataclass(frozen=True, slots=True) +class ProjectionContext: + league: str + as_of: datetime + history: tuple[GameRecord, ...] + ratings: dict[str, TeamRating] + + +@dataclass(slots=True) +class _Accumulator: + weighted_for: float = 0.0 + weighted_against: float = 0.0 + total_weight: float = 0.0 + games: int = 0 + last_game_at: str | None = None + + +def _parse_time(value: str) -> datetime | None: + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError: + return None + if parsed.tzinfo is None: + return None + return parsed.astimezone(UTC) + + +def _clamp(value: float, low: float, high: float) -> float: + return max(low, min(high, value)) + + +def _quantile(values: tuple[int, ...], fraction: float) -> int: + if not values: + raise ValueError("Quantile input cannot be empty") + ordered = sorted(values) + index = round((len(ordered) - 1) * fraction) + return ordered[index] + + +def _rating_weight(game_time: datetime, as_of: datetime, half_life_days: float) -> float: + age_days = max(0.0, (as_of - game_time).total_seconds() / 86_400.0) + return math.exp(-math.log(2.0) * age_days / half_life_days) + + +def _neutral_rating(team_id: str) -> TeamRating: + return TeamRating( + team_id=team_id, + offense=1.0, + defense=1.0, + effective_games=0.0, + games=0, + uncertainty=1.0, + last_game_at=None, + data_age_days=None, + status="cold_start_reference_only", + ) + + +class TeamStrengthEstimator: + """Recency weighted, opponent-adjusted attack and defense estimator. + + Ratios shrink to the league prior before four fixed-point opponent + adjustment passes. This prevents tiny or stale samples from appearing + more certain than the underlying observations support. + """ + + def estimate( + self, + league: str, + games: tuple[GameRecord, ...], + *, + as_of: datetime, + ) -> dict[str, TeamRating]: + normalized = normalize_league_id(league) + config = PROJECTION_CONFIGS[normalized] + anchor = ANCHORS[normalized] + usable: list[tuple[GameRecord, datetime, float]] = [] + for game in games: + home_score = game.home_score + away_score = game.away_score + if home_score is None or away_score is None: + continue + game_time = _parse_time(game.start_time) + if game_time is None or game_time >= as_of: + continue + weight = _rating_weight(game_time, as_of, config.half_life_days) + if weight < 0.0001: + continue + usable.append((game, game_time, weight)) + team_ids = { + team_id + for game, _game_time, _weight in usable + for team_id in (game.home_team_id, game.away_team_id) + } + if not team_ids: + return {} + offense = dict.fromkeys(team_ids, 1.0) + defense = dict.fromkeys(team_ids, 1.0) + final_accumulators: dict[str, _Accumulator] = {} + for _iteration in range(4): + accumulators = {team_id: _Accumulator() for team_id in team_ids} + for game, _game_time, weight in usable: + home_score = game.home_score + away_score = game.away_score + if home_score is None or away_score is None: + continue + home = accumulators[game.home_team_id] + away = accumulators[game.away_team_id] + home_expected_base = anchor.mean_home_score + away_expected_base = anchor.mean_away_score + home_for_ratio = float(home_score) / max( + 0.1, home_expected_base * defense[game.away_team_id] + ) + away_for_ratio = float(away_score) / max( + 0.1, away_expected_base * defense[game.home_team_id] + ) + home_against_ratio = float(away_score) / max( + 0.1, away_expected_base * offense[game.away_team_id] + ) + away_against_ratio = float(home_score) / max( + 0.1, home_expected_base * offense[game.home_team_id] + ) + home.weighted_for += weight * home_for_ratio + home.weighted_against += weight * home_against_ratio + home.total_weight += weight + home.games += 1 + home.last_game_at = max(home.last_game_at or "", game.start_time) + away.weighted_for += weight * away_for_ratio + away.weighted_against += weight * away_against_ratio + away.total_weight += weight + away.games += 1 + away.last_game_at = max(away.last_game_at or "", game.start_time) + for team_id, accumulator in accumulators.items(): + denominator = config.prior_games + accumulator.total_weight + offense[team_id] = _clamp( + (config.prior_games + accumulator.weighted_for) / denominator, + 0.55, + 1.65, + ) + defense[team_id] = _clamp( + (config.prior_games + accumulator.weighted_against) / denominator, + 0.55, + 1.65, + ) + final_accumulators = accumulators + ratings: dict[str, TeamRating] = {} + for team_id in sorted(team_ids): + accumulator = final_accumulators[team_id] + last_time = _parse_time(accumulator.last_game_at or "") + age_days = ( + max(0.0, (as_of - last_time).total_seconds() / 86_400.0) + if last_time is not None + else None + ) + sample_uncertainty = math.sqrt( + config.prior_games / (config.prior_games + accumulator.total_weight) + ) + stale_penalty = ( + min(0.35, max(0.0, age_days - config.half_life_days) / 365.0) + if age_days is not None + else 0.35 + ) + uncertainty = _clamp(sample_uncertainty + stale_penalty, 0.08, 1.0) + status = ( + "historical_team_form_provisional" + if accumulator.total_weight >= config.minimum_effective_games + and (age_days is None or age_days <= config.half_life_days * 2.0) + else "sparse_or_stale_team_history" + ) + ratings[team_id] = TeamRating( + team_id=team_id, + offense=offense[team_id], + defense=defense[team_id], + effective_games=round(accumulator.total_weight, 3), + games=accumulator.games, + uncertainty=round(uncertainty, 4), + last_game_at=accumulator.last_game_at, + data_age_days=round(age_days, 1) if age_days is not None else None, + status=status, + ) + return ratings + + +class ProjectionEngine: + """Generate deterministic posterior-predictive score distributions.""" + + def __init__(self, store: SportsDataStore) -> None: + self.store = store + self.estimator = TeamStrengthEstimator() + + def _history( + self, + league: str, + as_of: datetime, + ) -> tuple[tuple[GameRecord, ...], dict[str, TeamRating]]: + history = self.store.completed_games(league, before=as_of.isoformat(), limit=10_000) + return history, self.estimator.estimate(league, history, as_of=as_of) + + def prepare_context( + self, + league: str, + *, + as_of: datetime | None = None, + ) -> ProjectionContext: + normalized = normalize_league_id(league) + decision_time = (as_of or datetime.now(UTC)).astimezone(UTC) + history, ratings = self._history(normalized, decision_time) + return ProjectionContext(normalized, decision_time, history, ratings) + + def project( + self, + league: str, + home_team_id: str, + away_team_id: str, + *, + home_team_name: str | None = None, + away_team_name: str | None = None, + neutral_site: bool = False, + simulations: int = 5_000, + seed: int = 20_260_726, + as_of: datetime | None = None, + context: ProjectionContext | None = None, + ) -> MatchupProjection: + normalized = normalize_league_id(league) + if normalized not in ANCHORS: + raise ValueError(f"Unsupported projection league: {league!r}") + home_id = home_team_id.strip().upper() + away_id = away_team_id.strip().upper() + if not home_id or not away_id or home_id == away_id: + raise ValueError("Hypothetical matchups require two distinct team IDs") + if not 100 <= simulations <= 50_000: + raise ValueError(f"simulations must be within [100, 50000]: {simulations}") + if seed < 0: + raise ValueError(f"seed must be non-negative: {seed}") + decision_time = (as_of or datetime.now(UTC)).astimezone(UTC) + if context is None: + history, ratings = self._history(normalized, decision_time) + else: + if context.league != normalized: + raise ValueError( + f"Projection context league mismatch: {context.league} != {normalized}" + ) + decision_time = context.as_of + history, ratings = context.history, context.ratings + home_rating = ratings.get(home_id, _neutral_rating(home_id)) + away_rating = ratings.get(away_id, _neutral_rating(away_id)) + anchor = ANCHORS[normalized] + home_base, away_base = _site_bases(anchor, neutral_site) + expected_home = home_base * home_rating.offense * away_rating.defense + expected_away = away_base * away_rating.offense * home_rating.defense + home_strength = _clamp(expected_home / anchor.mean_home_score, 0.25, 2.0) + away_strength = _clamp(expected_away / anchor.mean_away_score, 0.25, 2.0) + config = PROJECTION_CONFIGS[normalized] + home_scores: list[int] = [] + away_scores: list[int] = [] + for index in range(simulations): + draw_seed = seed + index + home_parameter = normal( + draw_seed, + math.log(home_strength), + config.posterior_strength_sd * home_rating.uncertainty, + normalized, + home_id, + "home_strength_posterior", + ) + away_parameter = normal( + draw_seed, + math.log(away_strength), + config.posterior_strength_sd * away_rating.uncertainty, + normalized, + away_id, + "away_strength_posterior", + ) + result = simulate_analytical( + normalized, + draw_seed, + _clamp(math.exp(home_parameter), 0.25, 2.0), + _clamp(math.exp(away_parameter), 0.25, 2.0), + ) + home_scores.append(result.home_score) + away_scores.append(result.away_score) + home_tuple = tuple(home_scores) + away_tuple = tuple(away_scores) + totals = tuple( + home + away for home, away in zip(home_tuple, away_tuple, strict=True) + ) + home_wins = sum(home > away for home, away in zip(home_tuple, away_tuple, strict=True)) + away_wins = sum(away > home for home, away in zip(home_tuple, away_tuple, strict=True)) + ties = simulations - home_wins - away_wins + source_counts: dict[str, int] = {} + for game in history: + source_counts[game.source] = source_counts.get(game.source, 0) + 1 + data_through = max((game.start_time for game in history), default=None) + confidence = _confidence_label(home_rating, away_rating) + evidence_status = ( + "historical_form_provisional_not_calibrated" + if confidence != "LOW" + else "reference_anchor_or_sparse_history_only" + ) + identity_payload = { + "model_version": PROJECTION_MODEL_VERSION, + "league": normalized, + "home": home_id, + "away": away_id, + "neutral_site": neutral_site, + "simulations": simulations, + "seed": seed, + "as_of": decision_time.isoformat(), + "home_rating": asdict(home_rating), + "away_rating": asdict(away_rating), + } + projection_id = hashlib.sha256( + json.dumps(identity_payload, sort_keys=True, separators=(",", ":")).encode() + ).hexdigest() + return MatchupProjection( + projection_id=projection_id, + model_version=PROJECTION_MODEL_VERSION, + league=normalized, + home_team_id=home_id, + away_team_id=away_id, + home_team_name=home_team_name or home_id, + away_team_name=away_team_name or away_id, + neutral_site=neutral_site, + simulations=simulations, + seed=seed, + projected_home_score=round(statistics.fmean(home_tuple), 1), + projected_away_score=round(statistics.fmean(away_tuple), 1), + projected_total=round(statistics.fmean(totals), 1), + projected_margin=round( + statistics.fmean( + home - away + for home, away in zip(home_tuple, away_tuple, strict=True) + ), + 1, + ), + home_win_probability=round(home_wins / simulations, 4), + away_win_probability=round(away_wins / simulations, 4), + tie_probability=round(ties / simulations, 4), + home_score_interval_80=ScoreInterval( + _quantile(home_tuple, 0.10), _quantile(home_tuple, 0.90) + ), + away_score_interval_80=ScoreInterval( + _quantile(away_tuple, 0.10), _quantile(away_tuple, 0.90) + ), + total_interval_80=ScoreInterval( + _quantile(totals, 0.10), _quantile(totals, 0.90) + ), + home_rating=home_rating, + away_rating=away_rating, + data_games=len(history), + data_through=data_through, + source_counts=tuple(sorted(source_counts.items())), + confidence_label=confidence, + evidence_status=evidence_status, + generated_at=decision_time.isoformat(), + limitations=( + "Projection is a simulation, not a guarantee or certified real-world forecast.", + "Roster, injury, player, coaching, weather, and travel inputs are not yet modeled.", + "Confidence describes data coverage and freshness, not demonstrated accuracy.", + ), + ) + + def project_game( + self, + game: GameRecord, + *, + simulations: int = 5_000, + seed: int = 20_260_726, + as_of: datetime | None = None, + context: ProjectionContext | None = None, + ) -> MatchupProjection: + return self.project( + game.league, + game.home_team_id, + game.away_team_id, + home_team_name=game.home_name, + away_team_name=game.away_name, + neutral_site=game.neutral_site, + simulations=simulations, + seed=seed, + as_of=as_of, + context=context, + ) + + +def _site_bases(anchor: ReferenceAnchor, neutral_site: bool) -> tuple[float, float]: + if not neutral_site: + return anchor.mean_home_score, anchor.mean_away_score + neutral = (anchor.mean_home_score + anchor.mean_away_score) / 2.0 + return neutral, neutral + + +def _confidence_label(home: TeamRating, away: TeamRating) -> str: + maximum_uncertainty = max(home.uncertainty, away.uncertainty) + if maximum_uncertainty <= 0.55 and all( + rating.status == "historical_team_form_provisional" for rating in (home, away) + ): + return "HIGH_DATA_COVERAGE" + if maximum_uncertainty <= 0.8 and all(rating.games > 0 for rating in (home, away)): + return "MEDIUM_DATA_COVERAGE" + return "LOW" diff --git a/src/universal_sports_engine/service.py b/src/universal_sports_engine/service.py new file mode 100644 index 0000000..5131433 --- /dev/null +++ b/src/universal_sports_engine/service.py @@ -0,0 +1,214 @@ +"""Application service for schedules, projections, and hypothetical games.""" + +from __future__ import annotations + +import hashlib +from dataclasses import asdict +from datetime import UTC, date, datetime, time, timedelta +from pathlib import Path +from typing import Any + +from universal_sports_engine.data.espn import EspnScoreboardClient +from universal_sports_engine.data.models import GameRecord +from universal_sports_engine.data.pipeline import SportsDataPipeline +from universal_sports_engine.data.store import SportsDataStore +from universal_sports_engine.league_packs.registry import ( + league_identities, + normalize_league_id, +) +from universal_sports_engine.projection import ProjectionEngine + +LEAGUE_PRESENTATION = { + "mlb": {"short_name": "MLB", "accent": "coral"}, + "nfl": {"short_name": "NFL", "accent": "amber"}, + "ncaaf": {"short_name": "NCAAF", "accent": "gold"}, + "nba": {"short_name": "NBA", "accent": "violet"}, + "wnba": {"short_name": "WNBA", "accent": "rose"}, + "ncaambb": {"short_name": "NCAAM", "accent": "cyan"}, + "nhl": {"short_name": "NHL", "accent": "ice"}, +} +LEAGUE_ORDER = ("mlb", "nfl", "ncaaf", "nba", "wnba", "ncaambb", "nhl") +NON_CLUB_TEAM_IDS = {"mlb": {"AL", "NL"}} + + +def _stable_game_seed(base_seed: int, game_id: str) -> int: + digest = hashlib.blake2b(game_id.encode(), digest_size=8, person=b"USE-WEEK").digest() + return base_seed + int.from_bytes(digest, "big") % 1_000_000_000 + + +def _logical_key(game: GameRecord) -> tuple[str, str, str, str]: + return game.league, game.start_time, game.home_team_id, game.away_team_id + + +def _record_priority(game: GameRecord) -> tuple[int, int, str]: + return ( + int(game.source == "espn_public_scoreboard"), + {"final": 3, "in_progress": 2, "scheduled": 1}.get(game.status, 0), + game.observed_at, + ) + + +def _deduplicate(games: tuple[GameRecord, ...]) -> tuple[GameRecord, ...]: + selected: dict[tuple[str, str, str, str], GameRecord] = {} + for game in games: + key = _logical_key(game) + incumbent = selected.get(key) + if incumbent is None or _record_priority(game) > _record_priority(incumbent): + selected[key] = game + return tuple(sorted(selected.values(), key=lambda game: (game.start_time, game.game_id))) + + +class SportsEngineService: + """Cohesive, framework-neutral facade used by the CLI and web API.""" + + def __init__( + self, + store: SportsDataStore, + pipeline: SportsDataPipeline, + projections: ProjectionEngine, + ) -> None: + self.store = store + self.pipeline = pipeline + self.projections = projections + + @classmethod + def from_paths(cls, data_path: Path, cache_dir: Path) -> SportsEngineService: + store = SportsDataStore(data_path) + client = EspnScoreboardClient(cache_dir) + return cls(store, SportsDataPipeline(store, client), ProjectionEngine(store)) + + def leagues(self) -> list[dict[str, Any]]: + identities = {identity.league_id: identity for identity in league_identities()} + output: list[dict[str, Any]] = [] + for league in LEAGUE_ORDER: + identity = identities[league] + output.append( + { + **asdict(identity), + **LEAGUE_PRESENTATION[league], + "aliases": { + "wnba": ["wba"], + "ncaambb": ["ncaam", "ncaamb"], + }.get(league, []), + } + ) + return output + + def teams(self, league: str) -> list[dict[str, Any]]: + normalized = normalize_league_id(league) + excluded = NON_CLUB_TEAM_IDS.get(normalized, set()) + return [ + asdict(team) + for team in self.store.teams(normalized) + if team.team_id not in excluded + ] + + def refresh_current_week( + self, + *, + start: date | None = None, + days: int = 7, + force: bool = False, + ) -> dict[str, Any]: + results = self.pipeline.refresh_current_week(start=start, days=days, force=force) + return { + "status": "ok" if all(result.status == "ok" for result in results) else "partial", + "results": [asdict(result) for result in results], + "remote_writes": 0, + "data_contract": "read_only_public_scoreboard", + } + + def week( + self, + *, + start: date | None = None, + days: int = 7, + league: str | None = None, + simulations: int = 2_000, + seed: int = 20_260_726, + ) -> dict[str, Any]: + if not 1 <= days <= 14: + raise ValueError(f"days must be within [1, 14]: {days}") + if not 100 <= simulations <= 20_000: + raise ValueError(f"simulations must be within [100, 20000]: {simulations}") + window_start = start or datetime.now(UTC).date() + window_end = window_start + timedelta(days=days) + start_iso = datetime.combine(window_start, time.min, UTC).isoformat() + end_iso = datetime.combine(window_end, time.min, UTC).isoformat() + normalized_league = normalize_league_id(league) if league else None + games = _deduplicate( + self.store.games_between(start_iso, end_iso, league=normalized_league) + ) + contexts = { + league_id: self.projections.prepare_context(league_id) + for league_id in sorted({game.league for game in games if game.status == "scheduled"}) + } + rows: list[dict[str, Any]] = [] + for game in games: + projection = None + if game.status == "scheduled": + projection = self.projections.project_game( + game, + simulations=simulations, + seed=_stable_game_seed(seed, game.game_id), + context=contexts[game.league], + ) + rows.append( + { + "game": asdict(game), + "projection": asdict(projection) if projection is not None else None, + } + ) + by_league = { + league_id: sum(row["game"]["league"] == league_id for row in rows) + for league_id in LEAGUE_ORDER + } + return { + "window": { + "start": window_start.isoformat(), + "end_exclusive": window_end.isoformat(), + "days": days, + "label": "rolling_current_week", + }, + "generated_at": datetime.now(UTC).isoformat(), + "games": rows, + "counts_by_league": by_league, + "simulations_per_projection": simulations, + "evidence_status": "simulation_outputs_not_certified_forecasts", + } + + def hypothetical( + self, + *, + league: str, + home_team_id: str, + away_team_id: str, + home_team_name: str | None = None, + away_team_name: str | None = None, + neutral_site: bool = False, + simulations: int = 10_000, + seed: int = 20_260_726, + ) -> dict[str, Any]: + projection = self.projections.project( + league, + home_team_id, + away_team_id, + home_team_name=home_team_name, + away_team_name=away_team_name, + neutral_site=neutral_site, + simulations=simulations, + seed=seed, + ) + return asdict(projection) + + def health(self) -> dict[str, Any]: + database = self.store.health() + return { + "status": "healthy" if database["integrity"] == "ok" else "degraded", + "engine_name": "Waterboy", + "engine_version": "0.4.0", + "database": database, + "execution_authority": False, + "remote_writes": False, + "supported_leagues": list(LEAGUE_ORDER), + } diff --git a/src/universal_sports_engine/settings.py b/src/universal_sports_engine/settings.py new file mode 100644 index 0000000..fcbfddb --- /dev/null +++ b/src/universal_sports_engine/settings.py @@ -0,0 +1,6 @@ +"""Runtime path defaults shared by the CLI and web application.""" + +from pathlib import Path + +DEFAULT_DATA_PATH = Path("runtime/universal_sports_engine/sports.db") +DEFAULT_CACHE_DIR = Path("runtime/universal_sports_engine/http_cache") diff --git a/src/universal_sports_engine/simulation.py b/src/universal_sports_engine/simulation.py index 80a60ee..e6d035d 100644 --- a/src/universal_sports_engine/simulation.py +++ b/src/universal_sports_engine/simulation.py @@ -65,7 +65,7 @@ def simulate_many(league: str, games: int, seed: int, workers: int) -> tuple[Gam if os.name == "nt" and (not main_file or main_file.startswith("<")): raise ParallelExecutionUnavailableError( "Windows process workers require an importable __main__ file; " - "run through sports-engine, pytest, or a script guarded by " + "run through waterboy, pytest, or a script guarded by " "if __name__ == '__main__'" ) tasks = ((league, seed + index) for index in range(games)) diff --git a/src/universal_sports_engine/web/static/app.js b/src/universal_sports_engine/web/static/app.js new file mode 100644 index 0000000..540e673 --- /dev/null +++ b/src/universal_sports_engine/web/static/app.js @@ -0,0 +1,439 @@ +"use strict"; + +const state = { + leagues: [], + week: null, + activeLeague: "all", +}; + +const byId = (id) => document.getElementById(id); + +function element(tag, className, text) { + const node = document.createElement(tag); + if (className) node.className = className; + if (text !== undefined) node.textContent = text; + return node; +} + +function formatPercent(value) { + return `${Math.round(Number(value || 0) * 100)}%`; +} + +function formatScore(value, projected) { + if (value === undefined || value === null) return "—"; + return projected ? Number(value).toFixed(1) : String(value); +} + +function shortDate(value) { + return new Intl.DateTimeFormat(undefined, { + weekday: "short", + month: "short", + day: "numeric", + }).format(new Date(value)); +} + +function calendarDate(value) { + return new Intl.DateTimeFormat(undefined, { + weekday: "short", + month: "short", + day: "numeric", + }).format(new Date(`${value}T12:00:00`)); +} + +function gameTime(value) { + return new Intl.DateTimeFormat(undefined, { + hour: "numeric", + minute: "2-digit", + }).format(new Date(value)); +} + +async function api(path, options = {}) { + const response = await fetch(path, { + headers: { "Content-Type": "application/json", ...(options.headers || {}) }, + ...options, + }); + if (!response.ok) { + let message = `${response.status} ${response.statusText}`; + try { + const payload = await response.json(); + message = typeof payload.detail === "string" ? payload.detail : JSON.stringify(payload.detail); + } catch (_error) { + // Keep the HTTP status when the error body is not JSON. + } + throw new Error(message); + } + return response.json(); +} + +async function loadHealth() { + const pill = byId("health-pill"); + try { + const health = await api("/api/v1/health"); + pill.classList.add(health.status === "healthy" ? "is-healthy" : "is-error"); + byId("health-text").textContent = health.status === "healthy" ? "System healthy" : "System degraded"; + } catch (_error) { + pill.classList.add("is-error"); + byId("health-text").textContent = "System unavailable"; + } +} + +async function loadLeagues() { + state.leagues = await api("/api/v1/leagues"); + const strip = byId("league-strip"); + for (const league of state.leagues) { + const button = element("button", "league-chip"); + button.type = "button"; + button.dataset.league = league.league_id; + button.append(element("span", "", league.short_name)); + const count = element("b", "", "0"); + count.id = `count-${league.league_id}`; + button.append(count); + strip.append(button); + } + const simLeague = byId("sim-league"); + for (const league of state.leagues) { + const option = element("option", "", `${league.short_name} · ${league.display_name}`); + option.value = league.league_id; + simLeague.append(option); + } + simLeague.value = "mlb"; + await loadTeams("mlb"); +} + +async function loadWeek() { + byId("schedule-loading").hidden = false; + byId("schedule-empty").hidden = true; + byId("schedule-board").replaceChildren(); + try { + state.week = await api("/api/v1/week?days=7&simulations=2000"); + const projected = state.week.games.filter((row) => row.projection).length; + byId("games-count").textContent = String(state.week.games.length); + byId("projection-count").textContent = String(projected); + byId("week-title").textContent = + `${calendarDate(state.week.window.start)} — ` + + calendarDate(state.week.window.end_exclusive); + byId("updated-text").textContent = + `Generated ${new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" }).format(new Date(state.week.generated_at))}`; + byId("count-all").textContent = String(state.week.games.length); + for (const [league, count] of Object.entries(state.week.counts_by_league)) { + const target = byId(`count-${league}`); + if (target) target.textContent = String(count); + } + renderSchedule(); + } catch (error) { + renderScheduleError(error); + } finally { + byId("schedule-loading").hidden = true; + } +} + +function renderScheduleError(error) { + const empty = byId("schedule-empty"); + empty.hidden = false; + empty.querySelector("h3").textContent = "Schedule unavailable"; + empty.querySelector("p").textContent = error.message; +} + +function renderSchedule() { + const board = byId("schedule-board"); + board.replaceChildren(); + if (!state.week) return; + const rows = state.week.games.filter( + (row) => state.activeLeague === "all" || row.game.league === state.activeLeague, + ); + byId("schedule-empty").hidden = rows.length > 0; + if (!rows.length) return; + const grouped = new Map(); + for (const row of rows) { + const key = row.game.start_time.slice(0, 10); + if (!grouped.has(key)) grouped.set(key, []); + grouped.get(key).push(row); + } + for (const [day, games] of grouped.entries()) { + const section = element("section", "day-group"); + section.append(element("h3", "day-heading", shortDate(`${day}T12:00:00Z`))); + const grid = element("div", "game-grid"); + for (const row of games) grid.append(buildGameCard(row)); + section.append(grid); + board.append(section); + } +} + +function buildGameCard(row) { + const game = row.game; + const projection = row.projection; + const card = element("article", "game-card"); + const top = element("div", "card-topline"); + const league = state.leagues.find((item) => item.league_id === game.league); + top.append(element("span", "league-tag", league ? league.short_name : game.league.toUpperCase())); + top.append(element("span", "", `${gameTime(game.start_time)}${game.neutral_site ? " · Neutral" : ""}`)); + card.append(top); + + const observed = game.status === "final" || game.status === "in_progress"; + const homeScore = observed ? game.home_score : projection?.projected_home_score; + const awayScore = observed ? game.away_score : projection?.projected_away_score; + const homeWin = game.status === "final" + ? Number(game.home_score > game.away_score) + : projection?.home_win_probability; + const awayWin = game.status === "final" + ? Number(game.away_score > game.home_score) + : projection?.away_win_probability; + const homeProjectedWinner = Boolean( + projection && projection.home_win_probability > projection.away_win_probability, + ); + const awayProjectedWinner = Boolean( + projection && projection.away_win_probability > projection.home_win_probability, + ); + card.append(teamRow( + game.home_name, + game.home_team_id, + formatScore(homeScore, Boolean(projection)), + homeWin, + "Home", + homeProjectedWinner, + )); + card.append(teamRow( + game.away_name, + game.away_team_id, + formatScore(awayScore, Boolean(projection)), + awayWin, + "Away", + awayProjectedWinner, + )); + + const meta = element("div", "projection-meta"); + if (game.status === "final") { + meta.append(element("span", "final-label", "FINAL")); + meta.append(element("span", "", game.venue || "Venue unavailable")); + } else if (game.status === "in_progress") { + meta.append(element("span", "final-label", "LIVE · OBSERVED SCORE")); + meta.append(element("span", "", "Pregame-only model withheld")); + } else if (projection) { + meta.append(element( + "span", + "", + `Total ${formatScore(projection.projected_total, true)} · 80% ${projection.total_interval_80.low}–${projection.total_interval_80.high}`, + )); + const badge = element("span", "coverage-badge", projection.confidence_label.replaceAll("_", " ")); + if (projection.confidence_label === "LOW") badge.classList.add("is-low"); + meta.append(badge); + } else { + meta.append(element("span", "", "Projection unavailable")); + } + card.append(meta); + return card; +} + +function teamRow(name, id, score, probability, side, projectedWinner) { + const row = element("div", "team-row"); + const team = element("div", "team-name"); + const nameLine = element("strong", "", name); + if (projectedWinner) { + const check = element("span", "winner-check", "✓"); + check.title = "Projected winner"; + check.setAttribute("aria-label", "projected winner"); + nameLine.append(" ", check); + } + team.append(nameLine); + team.append(element("small", "", `${side} · ${id}`)); + row.append(team); + row.append(element("span", "team-win", probability === undefined ? "—" : formatPercent(probability))); + row.append(element("span", "team-score", score === undefined || score === null ? "—" : String(score))); + return row; +} + +async function loadTeams(league) { + const home = byId("home-team"); + const away = byId("away-team"); + home.disabled = true; + away.disabled = true; + home.replaceChildren(element("option", "", "Loading teams…")); + away.replaceChildren(element("option", "", "Loading teams…")); + try { + const teams = await api(`/api/v1/leagues/${encodeURIComponent(league)}/teams`); + home.replaceChildren(); + away.replaceChildren(); + if (teams.length < 2) { + home.append(element("option", "", "Refresh history to load teams")); + away.append(element("option", "", "Refresh history to load teams")); + return; + } + for (const [index, team] of teams.entries()) { + const homeOption = element("option", "", `${team.display_name} (${team.team_id})`); + homeOption.value = team.team_id; + homeOption.dataset.name = team.display_name; + home.append(homeOption); + const awayOption = homeOption.cloneNode(true); + away.append(awayOption); + if (index === 1) away.value = team.team_id; + } + home.disabled = false; + away.disabled = false; + } catch (error) { + home.replaceChildren(element("option", "", error.message)); + away.replaceChildren(element("option", "", error.message)); + } +} + +function renderLabResult(result) { + const root = byId("lab-result"); + root.replaceChildren(); + const shell = element("div", "result-shell"); + shell.append(element("span", "result-kicker", `${result.league.toUpperCase()} · ${result.simulations.toLocaleString()} simulations`)); + const matchup = element("div", "result-matchup"); + const homeProjectedWinner = result.home_win_probability > result.away_win_probability; + const awayProjectedWinner = result.away_win_probability > result.home_win_probability; + matchup.append(resultTeam( + result.home_team_name, + result.projected_home_score, + result.home_score_interval_80, + homeProjectedWinner, + )); + matchup.append(element("span", "result-vs", result.neutral_site ? "NEUTRAL" : "VS")); + matchup.append(resultTeam( + result.away_team_name, + result.projected_away_score, + result.away_score_interval_80, + awayProjectedWinner, + )); + shell.append(matchup); + + const bar = element("div", "probability-bar"); + const homeBar = element("div", "probability-home"); + const awayBar = element("div", "probability-away"); + const decisive = Math.max(0.0001, result.home_win_probability + result.away_win_probability); + homeBar.style.width = `${(result.home_win_probability / decisive) * 100}%`; + awayBar.style.width = `${(result.away_win_probability / decisive) * 100}%`; + bar.append(homeBar, awayBar); + shell.append(bar); + const legend = element("div", "probability-legend"); + legend.append( + element("span", "", `${formatPercent(result.home_win_probability)} home win`), + element("span", "", `${formatPercent(result.away_win_probability)} away win`), + ); + shell.append(legend); + + const stats = element("div", "result-stats"); + stats.append( + resultStat(formatScore(result.projected_total, true), "Projected total"), + resultStat( + `${result.projected_margin > 0 ? "+" : ""}${formatScore(result.projected_margin, true)}`, + "Home margin", + ), + resultStat(result.confidence_label.replaceAll("_", " "), "Data coverage"), + ); + shell.append(stats); + shell.append(element( + "p", + "result-evidence", + `${result.evidence_status.replaceAll("_", " ")} · Data through ${result.data_through ? shortDate(result.data_through) : "not available"}. ${result.limitations[0]}`, + )); + root.append(shell); +} + +function resultTeam(name, score, interval, projectedWinner) { + const team = element("div", "result-team"); + team.append(element("strong", "", formatScore(score, true))); + const nameLine = element("span", "", name); + if (projectedWinner) { + const check = element("span", "winner-check", "✓"); + check.title = "Projected winner"; + check.setAttribute("aria-label", "projected winner"); + nameLine.append(" ", check); + } + team.append(nameLine); + team.append(element("small", "", `80% range ${interval.low}–${interval.high}`)); + return team; +} + +function resultStat(value, label) { + const node = element("div"); + node.append(element("strong", "", value), element("small", "", label)); + return node; +} + +function wireEvents() { + document.querySelectorAll(".mode-tab").forEach((button) => { + button.addEventListener("click", () => { + document.querySelectorAll(".mode-tab").forEach((item) => item.classList.remove("is-active")); + button.classList.add("is-active"); + document.querySelectorAll(".view-panel").forEach((panel) => { panel.hidden = true; }); + byId(button.dataset.view).hidden = false; + }); + }); + + byId("league-strip").addEventListener("click", (event) => { + const button = event.target.closest(".league-chip"); + if (!button) return; + document.querySelectorAll(".league-chip").forEach((item) => item.classList.remove("is-active")); + button.classList.add("is-active"); + state.activeLeague = button.dataset.league; + renderSchedule(); + }); + + byId("sim-league").addEventListener("change", (event) => loadTeams(event.target.value)); + + byId("refresh-button").addEventListener("click", async () => { + const button = byId("refresh-button"); + button.disabled = true; + button.textContent = "Refreshing…"; + try { + const result = await api("/api/v1/data/refresh", { + method: "POST", + body: JSON.stringify({ days: 7, force: true }), + }); + button.textContent = result.status === "ok" ? "Data refreshed" : "Partial refresh"; + await loadWeek(); + await loadTeams(byId("sim-league").value); + } catch (error) { + button.textContent = `Refresh failed: ${error.message}`; + } finally { + setTimeout(() => { + button.disabled = false; + button.textContent = "↻ Refresh data"; + }, 1800); + } + }); + + byId("simulator-form").addEventListener("submit", async (event) => { + event.preventDefault(); + const button = byId("simulate-button"); + const home = byId("home-team"); + const away = byId("away-team"); + if (home.value === away.value) { + byId("lab-result").replaceChildren(element("div", "lab-placeholder", "Choose two different teams.")); + return; + } + button.disabled = true; + button.textContent = "Running simulations…"; + try { + const result = await api("/api/v1/simulations/hypothetical", { + method: "POST", + body: JSON.stringify({ + league: byId("sim-league").value, + home_team_id: home.value, + away_team_id: away.value, + home_team_name: home.selectedOptions[0]?.dataset.name || home.value, + away_team_name: away.selectedOptions[0]?.dataset.name || away.value, + neutral_site: byId("neutral-site").checked, + simulations: Number(byId("simulations").value), + seed: Number(byId("seed").value), + }), + }); + renderLabResult(result); + } catch (error) { + byId("lab-result").replaceChildren(element("div", "lab-placeholder", error.message)); + } finally { + button.disabled = false; + button.textContent = "Run matchup simulation"; + } + }); +} + +async function start() { + wireEvents(); + await Promise.all([loadHealth(), loadLeagues()]); + await loadWeek(); +} + +start().catch((error) => renderScheduleError(error)); diff --git a/src/universal_sports_engine/web/static/favicon.svg b/src/universal_sports_engine/web/static/favicon.svg new file mode 100644 index 0000000..91174c0 --- /dev/null +++ b/src/universal_sports_engine/web/static/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/universal_sports_engine/web/static/index.html b/src/universal_sports_engine/web/static/index.html new file mode 100644 index 0000000..fd8fd20 --- /dev/null +++ b/src/universal_sports_engine/web/static/index.html @@ -0,0 +1,230 @@ + + + + + + + + Waterboy · Sports Simulation Intelligence + + + + + + +
+ + + + Waterboy + Sports Intelligence + + +
+ + + Checking system + + +
+
+ +
+
+
+

Waterboy · deterministic sports intelligence

+

Every league.
Any matchup.

+

+ One elite command center for current-week score distributions and + hypothetical games—powered by provenance-backed history, exact replay, + visible uncertainty, and up to 50,000 simulations per matchup. +

+
+
+
+ + Games loaded +
+
+ + Projected +
+
+ 7 + League packs +
+
+
+ +
+
+
+

Inside the engine

+

From schedule to simulation—without the black box.

+
+

+ Waterboy turns source-backed game history into transparent team state, + then routes it through a deterministic posterior simulation stack. +

+
+ +
+
+ 01 + Read-only data + Schedules, scores, timestamps, and payload hashes +
+ +
+ 02 + Team intelligence + Recency, opponent adjustment, shrinkage, and freshness +
+ +
+ 03 + Posterior worlds + Deterministic score distributions with replayable seeds +
+ +
+ 04 + Decision clarity + Decimal scores, winner marks, intervals, and evidence labels +
+
+ +
+
+ 179K+ + historical-game bootstrap snapshot +
+
+ 7 + isolated league rule packs +
+
+ 50K + simulations per hypothetical matchup +
+
+ Exact + same-input, same-seed replay +
+
+
+ + + +
+
+
+

Rolling seven-day board

+

Current week

+
+

Loading schedule…

+
+ +
+ +
+ +
+ + Building score distributions… +
+ +
+
+ + +
+ + + + diff --git a/src/universal_sports_engine/web/static/styles.css b/src/universal_sports_engine/web/static/styles.css new file mode 100644 index 0000000..1922845 --- /dev/null +++ b/src/universal_sports_engine/web/static/styles.css @@ -0,0 +1,660 @@ +:root { + --ink: #f4f3ed; + --muted: #9ca7ad; + --dim: #66727a; + --surface: #11191d; + --surface-raised: #172227; + --surface-soft: #1d2a30; + --line: rgba(255, 255, 255, 0.10); + --accent: #b9ff5a; + --accent-soft: rgba(185, 255, 90, 0.12); + --cyan: #74e8ff; + --danger: #ff7b69; + --radius: 18px; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + color: var(--ink); + background: #0b1215; + font-synthesis: none; +} + +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + background: + radial-gradient(circle at 72% -10%, rgba(116, 232, 255, 0.12), transparent 31rem), + radial-gradient(circle at 12% 24%, rgba(185, 255, 90, 0.06), transparent 25rem), + #0b1215; +} + +button, select, input { font: inherit; } +button, a { -webkit-tap-highlight-color: transparent; } +a { color: inherit; } + +.skip-link { + position: fixed; + left: 1rem; + top: -4rem; + z-index: 50; + padding: .7rem 1rem; + border-radius: .5rem; + color: #091014; + background: var(--accent); +} +.skip-link:focus { top: 1rem; } + +.topbar { + position: sticky; + top: 0; + z-index: 30; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + min-height: 76px; + padding: .75rem max(1.25rem, calc((100vw - 1220px) / 2)); + border-bottom: 1px solid var(--line); + background: rgba(11, 18, 21, .88); + backdrop-filter: blur(18px); +} + +.brand { + display: flex; + align-items: center; + gap: .75rem; + color: var(--ink); + text-decoration: none; +} +.brand-mark { + display: grid; + place-items: center; + width: 42px; + height: 42px; + border-radius: 12px; + color: #0b1215; + background: var(--accent); + font-weight: 900; + font-size: 1.3rem; + box-shadow: 0 0 28px rgba(185, 255, 90, .18); +} +.brand strong, .brand small { display: block; } +.brand strong { font-size: .98rem; letter-spacing: .01em; } +.brand small { + margin-top: .08rem; + color: var(--muted); + font-size: .7rem; + letter-spacing: .14em; + text-transform: uppercase; +} +.topbar-actions { display: flex; align-items: center; gap: .75rem; } +.health-pill { + display: inline-flex; + align-items: center; + gap: .55rem; + color: var(--muted); + font-size: .78rem; +} +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--dim); + box-shadow: 0 0 0 4px rgba(102, 114, 122, .12); +} +.health-pill.is-healthy .status-dot { + background: var(--accent); + box-shadow: 0 0 0 4px rgba(185, 255, 90, .12); +} +.health-pill.is-error .status-dot { + background: var(--danger); + box-shadow: 0 0 0 4px rgba(255, 123, 105, .12); +} + +.button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: .45rem; + min-height: 42px; + padding: .7rem 1rem; + border: 1px solid var(--line); + border-radius: 11px; + color: var(--ink); + background: transparent; + cursor: pointer; + font-weight: 700; + font-size: .82rem; + transition: transform .15s ease, border-color .15s ease, background .15s ease; +} +.button:hover { transform: translateY(-1px); border-color: rgba(255,255,255,.25); } +.button:disabled { cursor: wait; opacity: .55; transform: none; } +.button-quiet { background: rgba(255,255,255,.035); } +.button-primary { + min-height: 50px; + border-color: var(--accent); + color: #0a120c; + background: var(--accent); + box-shadow: 0 12px 34px rgba(185, 255, 90, .14); +} + +main { + width: min(1220px, calc(100% - 2.5rem)); + margin: 0 auto; +} +.hero { + display: grid; + grid-template-columns: minmax(0, 1.5fr) minmax(330px, .9fr); + align-items: end; + gap: 3rem; + min-height: 390px; + padding: 5rem 0 3.3rem; +} +.eyebrow { + margin: 0 0 .8rem; + color: var(--accent); + font-size: .68rem; + font-weight: 800; + letter-spacing: .16em; + text-transform: uppercase; +} +h1, h2, h3, p { margin-top: 0; } +h1 { + max-width: 760px; + margin-bottom: 1.15rem; + font-size: clamp(3.1rem, 7vw, 6.4rem); + line-height: .88; + letter-spacing: -.065em; +} +h1 span { color: var(--cyan); } +.hero-copy { + max-width: 650px; + margin-bottom: 0; + color: var(--muted); + font-size: clamp(.98rem, 1.4vw, 1.12rem); + line-height: 1.65; +} +.hero-metrics { + display: grid; + grid-template-columns: repeat(3, 1fr); + overflow: hidden; + border: 1px solid var(--line); + border-radius: var(--radius); + background: rgba(17, 25, 29, .8); +} +.hero-metrics article { + min-height: 120px; + padding: 1.3rem; + border-right: 1px solid var(--line); +} +.hero-metrics article:last-child { border-right: 0; } +.hero-metrics span, .hero-metrics small { display: block; } +.hero-metrics span { + margin-bottom: .9rem; + font-size: clamp(1.8rem, 3vw, 2.7rem); + font-weight: 850; + letter-spacing: -.04em; +} +.hero-metrics small { color: var(--muted); font-size: .72rem; line-height: 1.35; } + +.showcase { + margin-bottom: 4.5rem; + padding: 2rem; + border: 1px solid var(--line); + border-radius: 24px; + background: + linear-gradient(135deg, rgba(185, 255, 90, .055), transparent 42%), + linear-gradient(315deg, rgba(116, 232, 255, .05), transparent 38%), + rgba(17, 25, 29, .76); + box-shadow: 0 28px 90px rgba(0, 0, 0, .2); +} +.showcase-heading { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(260px, .65fr); + align-items: end; + gap: 2rem; + margin-bottom: 2rem; +} +.showcase-heading h2 { max-width: 760px; } +.showcase-heading > p { + margin-bottom: .2rem; + color: var(--muted); + line-height: 1.6; +} +.engine-flow { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr) auto) minmax(0, 1fr); + align-items: stretch; + gap: .65rem; +} +.engine-flow article { + min-height: 155px; + padding: 1rem; + border: 1px solid var(--line); + border-radius: 15px; + background: rgba(7, 13, 16, .5); +} +.engine-flow article span, +.engine-flow article strong, +.engine-flow article small { display: block; } +.engine-flow article span { + margin-bottom: 1.15rem; + color: var(--accent); + font-size: .62rem; + font-weight: 850; + letter-spacing: .14em; +} +.engine-flow article strong { margin-bottom: .55rem; font-size: .92rem; } +.engine-flow article small { color: var(--muted); font-size: .68rem; line-height: 1.5; } +.engine-flow i { + align-self: center; + color: var(--dim); + font-style: normal; +} +.proof-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: .65rem; + margin-top: .65rem; +} +.proof-grid article { + padding: 1rem; + border-radius: 14px; + background: rgba(255,255,255,.025); +} +.proof-grid strong, .proof-grid span { display: block; } +.proof-grid strong { + margin-bottom: .25rem; + color: var(--cyan); + font-size: 1.45rem; + letter-spacing: -.04em; +} +.proof-grid span { color: var(--muted); font-size: .65rem; line-height: 1.35; } + +.mode-tabs { + display: flex; + align-items: center; + gap: .2rem; + border-bottom: 1px solid var(--line); +} +.mode-tab { + position: relative; + padding: 1rem .9rem; + border: 0; + color: var(--muted); + background: transparent; + cursor: pointer; + font-weight: 750; + font-size: .85rem; +} +.mode-tab::after { + position: absolute; + left: .9rem; + right: .9rem; + bottom: -1px; + height: 2px; + content: ""; + background: transparent; +} +.mode-tab.is-active { color: var(--ink); } +.mode-tab.is-active::after { background: var(--accent); } +.api-link { + margin-left: auto; + color: var(--muted); + font-size: .78rem; + text-decoration: none; +} +.api-link:hover { color: var(--ink); } + +.view-panel { padding: 3rem 0 5rem; } +.section-heading { + display: flex; + align-items: end; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.8rem; +} +h2 { + margin-bottom: 0; + font-size: clamp(1.9rem, 3.4vw, 3rem); + letter-spacing: -.045em; +} +.updated-text { margin-bottom: .2rem; color: var(--muted); font-size: .78rem; } + +.league-strip { + display: flex; + gap: .55rem; + overflow-x: auto; + padding-bottom: .8rem; + scrollbar-width: thin; +} +.league-chip { + display: flex; + align-items: center; + gap: .65rem; + flex: 0 0 auto; + padding: .65rem .7rem .65rem .9rem; + border: 1px solid var(--line); + border-radius: 999px; + color: var(--muted); + background: rgba(255,255,255,.025); + cursor: pointer; + font-size: .76rem; + font-weight: 700; +} +.league-chip b { + display: grid; + place-items: center; + min-width: 23px; + height: 23px; + padding: 0 .35rem; + border-radius: 999px; + color: var(--muted); + background: rgba(255,255,255,.07); + font-size: .66rem; +} +.league-chip:hover, .league-chip.is-active { + border-color: rgba(185,255,90,.42); + color: var(--ink); + background: var(--accent-soft); +} +.league-chip.is-active b { color: #10180f; background: var(--accent); } + +.loading-state, .empty-state { + display: flex; + align-items: center; + justify-content: center; + gap: .8rem; + min-height: 270px; + color: var(--muted); +} +.loader { + width: 18px; + height: 18px; + border: 2px solid var(--line); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin .8s linear infinite; +} +@keyframes spin { to { transform: rotate(360deg); } } +.empty-state { flex-direction: column; text-align: center; } +.empty-state h3 { margin-bottom: .3rem; } +.empty-state p { max-width: 460px; color: var(--muted); line-height: 1.5; } +.empty-icon { color: var(--accent); font-size: 2.5rem; } + +.day-group { margin-top: 2rem; } +.day-heading { + display: flex; + align-items: center; + gap: .8rem; + margin-bottom: .85rem; + color: var(--muted); + font-size: .68rem; + font-weight: 800; + letter-spacing: .14em; + text-transform: uppercase; +} +.day-heading::after { flex: 1; height: 1px; content: ""; background: var(--line); } +.game-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: .75rem; +} +.game-card { + position: relative; + overflow: hidden; + min-height: 245px; + padding: 1rem; + border: 1px solid var(--line); + border-radius: 15px; + background: linear-gradient(145deg, rgba(29,42,48,.85), rgba(17,25,29,.92)); + transition: transform .16s ease, border-color .16s ease; +} +.game-card:hover { transform: translateY(-2px); border-color: rgba(255,255,255,.2); } +.card-topline { + display: flex; + justify-content: space-between; + gap: .75rem; + color: var(--muted); + font-size: .67rem; + font-weight: 750; + letter-spacing: .05em; + text-transform: uppercase; +} +.league-tag { color: var(--accent); } +.team-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: center; + gap: .65rem; + padding: .8rem 0; + border-bottom: 1px solid var(--line); +} +.team-row:first-of-type { margin-top: .55rem; } +.team-name { min-width: 0; } +.team-name strong, .team-name small { display: block; } +.team-name strong { overflow: hidden; font-size: .88rem; text-overflow: ellipsis; white-space: nowrap; } +.team-name small { margin-top: .2rem; color: var(--dim); font-size: .62rem; text-transform: uppercase; } +.winner-check { + color: var(--accent); + font-size: .88em; + font-weight: 950; + text-shadow: 0 0 14px rgba(185, 255, 90, .35); +} +.team-win { color: var(--muted); font-size: .72rem; font-variant-numeric: tabular-nums; } +.team-score { min-width: 2.5rem; text-align: right; font-size: 1.65rem; font-weight: 850; letter-spacing: -.05em; } +.projection-meta { + display: flex; + justify-content: space-between; + gap: .7rem; + padding-top: .9rem; + color: var(--muted); + font-size: .64rem; +} +.coverage-badge { + padding: .18rem .45rem; + border-radius: 999px; + color: var(--cyan); + background: rgba(116,232,255,.08); + white-space: nowrap; +} +.coverage-badge.is-low { color: #ffc06b; background: rgba(255,192,107,.08); } +.final-label { color: var(--accent); } + +.lab-layout { + display: grid; + grid-template-columns: minmax(380px, .8fr) minmax(0, 1.2fr); + gap: 1rem; +} +.simulator-form, .lab-result { + min-height: 470px; + padding: 1.35rem; + border: 1px solid var(--line); + border-radius: var(--radius); + background: rgba(17,25,29,.78); +} +.field label { + display: block; + margin: 0 0 .42rem; + color: var(--muted); + font-size: .69rem; + font-weight: 750; + letter-spacing: .04em; +} +select, input { + width: 100%; + min-height: 47px; + padding: .7rem .8rem; + border: 1px solid var(--line); + border-radius: 10px; + outline: none; + color: var(--ink); + background: var(--surface-soft); +} +select:focus, input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } +.matchup-fields { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: end; + gap: .6rem; + margin: 1rem 0; +} +.versus-mark { + display: grid; + place-items: center; + width: 38px; + height: 47px; + color: var(--dim); + font-size: .66rem; + font-weight: 900; +} +.sim-options { + display: grid; + grid-template-columns: 1fr 1fr; + gap: .8rem; + margin-bottom: 1rem; +} +.toggle { + display: flex; + align-items: center; + gap: .6rem; + grid-column: 1 / -1; + color: var(--muted); + cursor: pointer; + font-size: .75rem; +} +.toggle input { position: absolute; opacity: 0; width: 1px; height: 1px; } +.toggle span { + position: relative; + width: 38px; + height: 22px; + border-radius: 999px; + background: var(--surface-soft); + border: 1px solid var(--line); +} +.toggle span::after { + position: absolute; + top: 3px; + left: 3px; + width: 14px; + height: 14px; + border-radius: 50%; + content: ""; + background: var(--muted); + transition: transform .15s ease, background .15s ease; +} +.toggle input:checked + span::after { transform: translateX(16px); background: var(--accent); } +.simulator-form .button-primary { width: 100%; margin-top: .2rem; } +.form-note { margin: 1rem 0 0; color: var(--dim); font-size: .68rem; line-height: 1.5; } +.lab-result { display: grid; place-items: center; } +.lab-placeholder { max-width: 390px; color: var(--muted); text-align: center; } +.lab-placeholder > span { color: var(--accent); font-size: 3rem; } +.lab-placeholder h3 { margin: .8rem 0 .4rem; color: var(--ink); } +.lab-placeholder p { line-height: 1.55; } +.result-shell { width: 100%; align-self: stretch; display: flex; flex-direction: column; } +.result-kicker { color: var(--accent); font-size: .66rem; font-weight: 800; letter-spacing: .13em; text-transform: uppercase; } +.result-matchup { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 1rem; + margin: 1rem 0 1.4rem; + text-align: center; +} +.result-team strong, .result-team span, .result-team small { display: block; } +.result-team strong { font-size: 2.8rem; letter-spacing: -.06em; } +.result-team span { margin-top: .3rem; font-weight: 750; } +.result-team .winner-check { display: inline; margin: 0 0 0 .2rem; } +.result-team small { margin-top: .25rem; color: var(--muted); font-size: .68rem; } +.result-vs { color: var(--dim); font-size: .72rem; font-weight: 800; } +.probability-bar { + display: flex; + height: 10px; + overflow: hidden; + border-radius: 999px; + background: var(--surface-soft); +} +.probability-home { background: var(--accent); } +.probability-away { background: var(--cyan); } +.probability-legend { + display: flex; + justify-content: space-between; + margin-top: .55rem; + color: var(--muted); + font-size: .68rem; +} +.result-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: .55rem; + margin-top: 1.3rem; +} +.result-stats div { + padding: .8rem; + border: 1px solid var(--line); + border-radius: 10px; + background: rgba(255,255,255,.02); +} +.result-stats strong, .result-stats small { display: block; } +.result-stats strong { margin-bottom: .25rem; font-size: 1rem; } +.result-stats small { color: var(--muted); font-size: .61rem; } +.result-evidence { + margin: auto 0 0; + padding-top: 1rem; + color: var(--dim); + font-size: .64rem; + line-height: 1.45; +} + +footer { + display: flex; + justify-content: space-between; + gap: 1rem; + width: min(1220px, calc(100% - 2.5rem)); + margin: 0 auto; + padding: 1.5rem 0 2.3rem; + border-top: 1px solid var(--line); + color: var(--dim); + font-size: .68rem; +} +footer p { margin: 0; } +footer a { color: var(--muted); } + +[hidden] { display: none !important; } + +@media (max-width: 960px) { + .hero { grid-template-columns: 1fr; min-height: auto; padding-top: 4rem; } + .hero-metrics { max-width: 620px; } + .showcase-heading { grid-template-columns: 1fr; } + .engine-flow { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .engine-flow i { display: none; } + .game-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .lab-layout { grid-template-columns: 1fr; } +} + +@media (max-width: 640px) { + .topbar { min-height: 66px; } + .health-pill { display: none; } + .button-quiet { padding: .6rem .7rem; } + main, footer { width: min(100% - 1.5rem, 1220px); } + .hero { padding: 3.2rem 0 2.5rem; gap: 2rem; } + h1 { font-size: clamp(3rem, 15vw, 4.6rem); } + .hero-metrics article { min-height: 95px; padding: 1rem .7rem; } + .hero-metrics small { font-size: .6rem; } + .showcase { margin-bottom: 2.8rem; padding: 1rem; border-radius: 18px; } + .engine-flow, .proof-grid { grid-template-columns: 1fr; } + .engine-flow article { min-height: auto; } + .section-heading { align-items: start; flex-direction: column; } + .game-grid { grid-template-columns: 1fr; } + .matchup-fields { grid-template-columns: 1fr; } + .versus-mark { width: auto; height: 20px; } + .result-matchup { gap: .5rem; } + .result-team strong { font-size: 2.15rem; } + .result-stats { grid-template-columns: 1fr; } + footer { flex-direction: column; } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { scroll-behavior: auto !important; animation-duration: .001ms !important; } +} diff --git a/src/universal_sports_engine/web_app.py b/src/universal_sports_engine/web_app.py new file mode 100644 index 0000000..4984000 --- /dev/null +++ b/src/universal_sports_engine/web_app.py @@ -0,0 +1,172 @@ +"""FastAPI application and local production server.""" + +from __future__ import annotations + +import os +from collections.abc import Awaitable, Callable +from datetime import date +from pathlib import Path +from typing import Any + +from fastapi import FastAPI, Query, Request +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse, Response +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel, ConfigDict, Field + +from universal_sports_engine.service import SportsEngineService +from universal_sports_engine.settings import DEFAULT_CACHE_DIR, DEFAULT_DATA_PATH + + +class HypotheticalRequest(BaseModel): + model_config = ConfigDict(extra="forbid", str_strip_whitespace=True) + + league: str = Field(min_length=2, max_length=16) + home_team_id: str = Field(min_length=1, max_length=64) + away_team_id: str = Field(min_length=1, max_length=64) + home_team_name: str | None = Field(default=None, max_length=120) + away_team_name: str | None = Field(default=None, max_length=120) + neutral_site: bool = False + simulations: int = Field(default=10_000, ge=100, le=50_000) + seed: int = Field(default=20_260_726, ge=0, le=2_147_483_647) + + +class RefreshRequest(BaseModel): + model_config = ConfigDict(extra="forbid") + + start: date | None = None + days: int = Field(default=7, ge=1, le=14) + force: bool = False + + +def _runtime_path(primary_name: str, legacy_name: str, fallback: Path) -> Path: + value = os.environ.get(primary_name) or os.environ.get(legacy_name) + return Path(value) if value else fallback + + +def create_app( + *, + service: SportsEngineService | None = None, + data_path: Path | None = None, + cache_dir: Path | None = None, +) -> FastAPI: + application_service = service or SportsEngineService.from_paths( + data_path + or _runtime_path("WATERBOY_DATA_PATH", "USE_DATA_PATH", DEFAULT_DATA_PATH), + cache_dir + or _runtime_path("WATERBOY_CACHE_DIR", "USE_CACHE_DIR", DEFAULT_CACHE_DIR), + ) + app = FastAPI( + title="Waterboy API", + version="0.4.0", + description=( + "Read-only public schedule ingestion, deterministic score distributions, " + "and hypothetical matchup simulation." + ), + ) + app.state.service = application_service + + @app.middleware("http") + async def security_headers( + request: Request, + call_next: Callable[[Request], Awaitable[Response]], + ) -> Response: + response = await call_next(request) + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["X-Frame-Options"] = "DENY" + response.headers["Referrer-Policy"] = "no-referrer" + response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()" + response.headers["Content-Security-Policy"] = ( + "default-src 'self'; script-src 'self'; style-src 'self'; " + "img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; " + "base-uri 'self'; form-action 'self'" + ) + if request.url.path.startswith("/api/"): + response.headers["Cache-Control"] = "no-store" + return response + + @app.exception_handler(ValueError) + async def value_error_handler(_request: Request, exc: ValueError) -> JSONResponse: + return JSONResponse(status_code=422, content={"detail": str(exc)}) + + @app.exception_handler(RequestValidationError) + async def validation_error_handler( + _request: Request, exc: RequestValidationError + ) -> JSONResponse: + return JSONResponse(status_code=422, content={"detail": exc.errors()}) + + @app.get("/api/v1/health") + def health() -> dict[str, Any]: + return application_service.health() + + @app.get("/api/v1/leagues") + def leagues() -> list[dict[str, Any]]: + return application_service.leagues() + + @app.get("/api/v1/leagues/{league}/teams") + def teams(league: str) -> list[dict[str, Any]]: + return application_service.teams(league) + + @app.get("/api/v1/week") + def week( + start: date | None = None, + days: int = Query(default=7, ge=1, le=14), + league: str | None = None, + simulations: int = Query(default=2_000, ge=100, le=20_000), + seed: int = Query(default=20_260_726, ge=0, le=2_147_483_647), + ) -> dict[str, Any]: + return application_service.week( + start=start, + days=days, + league=league, + simulations=simulations, + seed=seed, + ) + + @app.post("/api/v1/data/refresh") + def refresh(request: RefreshRequest) -> dict[str, Any]: + return application_service.refresh_current_week( + start=request.start, + days=request.days, + force=request.force, + ) + + @app.post("/api/v1/simulations/hypothetical") + def hypothetical(request: HypotheticalRequest) -> dict[str, Any]: + return application_service.hypothetical(**request.model_dump()) + + static_dir = Path(__file__).with_name("web") / "static" + app.mount("/", StaticFiles(directory=static_dir, html=True), name="dashboard") + return app + + +def run_server( + *, + host: str = "127.0.0.1", + port: int = 8765, + data_path: Path | None = None, + cache_dir: Path | None = None, + allow_remote: bool = False, +) -> None: + """Run the local dashboard with a production ASGI server.""" + + if not 1 <= port <= 65_535: + raise ValueError(f"port must be within [1, 65535]: {port}") + if host not in {"127.0.0.1", "localhost", "::1"} and not allow_remote: + raise ValueError( + "The built-in server is localhost-only; deploy behind an authenticated " + "reverse proxy and pass --allow-remote for remote access" + ) + import uvicorn + + application = create_app(data_path=data_path, cache_dir=cache_dir) + uvicorn.run( + application, + host=host, + port=port, + access_log=True, + server_header=False, + proxy_headers=False, + timeout_keep_alive=5, + limit_concurrency=100, + ) diff --git a/src/waterboy/__init__.py b/src/waterboy/__init__.py new file mode 100644 index 0000000..0428fc5 --- /dev/null +++ b/src/waterboy/__init__.py @@ -0,0 +1,27 @@ +"""Public Waterboy package with compatibility exports from the proven kernel.""" + +from universal_sports_engine import ( + AdaptiveAnalyticalResult, + AnalyticalResult, + __version__, + counterfactual_game, + simulate_analytical, + simulate_ensemble_analytical, + simulate_game, + simulate_many, + simulate_many_analytical, + simulate_profile_analytical, +) + +__all__ = [ + "AdaptiveAnalyticalResult", + "AnalyticalResult", + "__version__", + "counterfactual_game", + "simulate_analytical", + "simulate_ensemble_analytical", + "simulate_game", + "simulate_many", + "simulate_many_analytical", + "simulate_profile_analytical", +] diff --git a/src/waterboy/__main__.py b/src/waterboy/__main__.py new file mode 100644 index 0000000..6f095b8 --- /dev/null +++ b/src/waterboy/__main__.py @@ -0,0 +1,6 @@ +"""Run the Waterboy command-line interface.""" + +from universal_sports_engine.cli import main + +if __name__ == "__main__": + main() diff --git a/tests/test_core.py b/tests/test_core.py index b9f8d24..557af8b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -25,6 +25,7 @@ ) from universal_sports_engine.league_packs.common import ENGINE_VERSION from universal_sports_engine.simulation import simulate_game +from waterboy import __version__ as waterboy_version def test_random_draws_are_label_addressed_and_repeatable() -> None: @@ -36,7 +37,7 @@ def test_random_draws_are_label_addressed_and_repeatable() -> None: def test_package_and_evidence_versions_match() -> None: - assert __version__ == ENGINE_VERSION == "0.3.0" + assert waterboy_version == __version__ == ENGINE_VERSION == "0.4.0" def test_rng_rejects_invalid_inputs() -> None: diff --git a/tests/test_projection_service_web.py b/tests/test_projection_service_web.py new file mode 100644 index 0000000..5eb9149 --- /dev/null +++ b/tests/test_projection_service_web.py @@ -0,0 +1,171 @@ +from __future__ import annotations + +import asyncio +import hashlib +from datetime import UTC, date, datetime, timedelta +from pathlib import Path + +import httpx + +from universal_sports_engine.data.espn import EspnScoreboardClient +from universal_sports_engine.data.models import GameRecord +from universal_sports_engine.data.pipeline import SportsDataPipeline +from universal_sports_engine.data.store import SportsDataStore +from universal_sports_engine.projection import ProjectionEngine +from universal_sports_engine.service import SportsEngineService +from universal_sports_engine.web_app import create_app + + +def _game( + game_id: str, + start_time: str, + *, + home: str = "ALP", + away: str = "BET", + home_score: int | None = None, + away_score: int | None = None, +) -> GameRecord: + final = home_score is not None and away_score is not None + return GameRecord( + game_id=game_id, + league="nba", + season=2026, + start_time=start_time, + status="final" if final else "scheduled", + home_team_id=home, + away_team_id=away, + home_name="Alphas" if home == "ALP" else "Betas", + away_name="Betas" if away == "BET" else "Alphas", + home_score=home_score, + away_score=away_score, + neutral_site=False, + venue="Test Arena", + source="fixture", + source_url="https://example.test", + observed_at="2026-07-26T00:00:00+00:00", + payload_hash=hashlib.sha256(game_id.encode()).hexdigest(), + ) + + +def _service(tmp_path: Path) -> SportsEngineService: + store = SportsDataStore(tmp_path / "sports.db") + history = [] + start = datetime(2026, 6, 1, tzinfo=UTC) + for index in range(24): + game_time = (start + timedelta(days=index)).isoformat() + if index % 2: + history.append( + _game( + f"h{index}", + game_time, + home="BET", + away="ALP", + home_score=101, + away_score=119, + ) + ) + else: + history.append( + _game(f"h{index}", game_time, home_score=121, away_score=99) + ) + history.append(_game("upcoming", "2026-07-27T00:00:00+00:00")) + store.upsert_games(history) + client = EspnScoreboardClient( + tmp_path / "cache", + transport=lambda _url, _timeout: b'{"events":[]}', + ) + return SportsEngineService( + store, + SportsDataPipeline(store, client), + ProjectionEngine(store), + ) + + +def test_projection_is_reproducible_and_team_aware(tmp_path: Path) -> None: + service = _service(tmp_path) + as_of = datetime(2026, 7, 26, tzinfo=UTC) + first = service.projections.project( + "nba", + "ALP", + "BET", + simulations=1_000, + seed=42, + as_of=as_of, + ) + second = service.projections.project( + "nba", + "ALP", + "BET", + simulations=1_000, + seed=42, + as_of=as_of, + ) + assert first == second + assert first.projected_home_score > first.projected_away_score + assert first.home_win_probability > first.away_win_probability + assert first.data_games == 24 + assert first.evidence_status == "historical_form_provisional_not_calibrated" + + +def test_service_week_and_hypothetical_alias(tmp_path: Path) -> None: + service = _service(tmp_path) + week = service.week(start=date(2026, 7, 26), simulations=100) + assert len(week["games"]) == 1 + assert week["games"][0]["projection"]["home_team_id"] == "ALP" + cold = service.hypothetical( + league="wba", + home_team_id="ONE", + away_team_id="TWO", + simulations=100, + seed=7, + ) + assert cold["league"] == "wnba" + assert cold["confidence_label"] == "LOW" + + +def test_web_api_and_dashboard_security_headers(tmp_path: Path) -> None: + async def exercise_api() -> None: + transport = httpx.ASGITransport(app=create_app(service=_service(tmp_path))) + async with httpx.AsyncClient( + transport=transport, + base_url="http://testserver", + ) as client: + dashboard = await client.get("/") + assert dashboard.status_code == 200 + assert "Every league" in dashboard.text + assert dashboard.headers["x-frame-options"] == "DENY" + assert "default-src 'self'" in dashboard.headers["content-security-policy"] + + health = await client.get("/api/v1/health") + assert health.status_code == 200 + assert health.json()["engine_name"] == "Waterboy" + assert health.json()["execution_authority"] is False + assert health.json()["database"]["database"] == "sports.db" + assert str(tmp_path) not in health.text + week = await client.get("/api/v1/week?start=2026-07-26&simulations=100") + assert week.status_code == 200 + assert len(week.json()["games"]) == 1 + matchup = await client.post( + "/api/v1/simulations/hypothetical", + json={ + "league": "nba", + "home_team_id": "ALP", + "away_team_id": "BET", + "simulations": 100, + "seed": 12, + }, + ) + assert matchup.status_code == 200 + assert matchup.json()["model_version"].startswith("hierarchical") + rejected = await client.post( + "/api/v1/simulations/hypothetical", + json={ + "league": "nba", + "home_team_id": "ALP", + "away_team_id": "ALP", + "simulations": 100, + }, + ) + assert rejected.status_code == 422 + + asyncio.run(exercise_api()) diff --git a/tests/test_sports_data.py b/tests/test_sports_data.py new file mode 100644 index 0000000..cdf0707 --- /dev/null +++ b/tests/test_sports_data.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import json +import sqlite3 +from datetime import date +from pathlib import Path +from urllib.error import HTTPError + +from universal_sports_engine.data.espn import EspnScoreboardClient, parse_scoreboard +from universal_sports_engine.data.pipeline import SportsDataPipeline +from universal_sports_engine.data.store import SportsDataStore + + +def _payload() -> dict[str, object]: + return { + "events": [ + { + "id": "401", + "date": "2026-07-26T19:10:00Z", + "season": {"year": 2026}, + "competitions": [ + { + "neutralSite": False, + "venue": {"fullName": "Research Field"}, + "status": {"type": {"state": "pre"}}, + "competitors": [ + { + "homeAway": "home", + "team": { + "id": "1", + "abbreviation": "HOM", + "displayName": "Home Club", + }, + }, + { + "homeAway": "away", + "team": { + "id": "2", + "abbreviation": "AWY", + "displayName": "Away Club", + }, + }, + ], + } + ], + } + ] + } + + +def test_espn_parser_preserves_schedule_truth_and_provenance() -> None: + games = parse_scoreboard( + "mlb", + _payload(), + observed_at="2026-07-26T12:00:00+00:00", + source_url="https://example.test/scoreboard", + ) + assert len(games) == 1 + game = games[0] + assert game.game_id == "espn:mlb:401" + assert game.status == "scheduled" + assert game.home_score is None and game.away_score is None + assert game.home_name == "Home Club" + assert game.source == "espn_public_scoreboard" + assert len(game.payload_hash) == 64 + + +def test_espn_client_cache_avoids_repeat_network_reads(tmp_path: Path) -> None: + calls: list[str] = [] + + def transport(url: str, _timeout: float) -> bytes: + calls.append(url) + return json.dumps(_payload()).encode() + + client = EspnScoreboardClient(tmp_path / "cache", transport=transport) + first = client.games("mlb", date(2026, 7, 26), date(2026, 7, 27)) + second = client.games("mlb", date(2026, 7, 26), date(2026, 7, 27)) + assert first == second + assert len(calls) == 1 + + +def test_espn_client_falls_back_to_bounded_daily_reads(tmp_path: Path) -> None: + calls: list[str] = [] + + def transport(url: str, _timeout: float) -> bytes: + calls.append(url) + if "dates=20260726-20260727" in url: + raise HTTPError(url, 400, "date range rejected", None, None) + return json.dumps(_payload()).encode() + + client = EspnScoreboardClient( + tmp_path / "cache", + max_retries=0, + transport=transport, + ) + games = client.games("ncaambb", date(2026, 7, 26), date(2026, 7, 27)) + assert len(games) == 1 + assert len(calls) == 3 + assert all("dates=20260726-20260727" not in url for url in calls[1:]) + + +def test_store_and_read_only_dummy_import(tmp_path: Path) -> None: + source = tmp_path / "dummy.db" + connection = sqlite3.connect(source) + connection.execute( + """ + CREATE TABLE games( + game_id TEXT, league TEXT, season INTEGER, start_time TEXT, status TEXT, + home TEXT, away TEXT, home_score INTEGER, away_score INTEGER, + venue TEXT, neutral INTEGER, source TEXT, provenance_url TEXT, + result_available_at TEXT, received_at TEXT, as_of TEXT, extra TEXT + ) + """ + ) + connection.execute( + """ + INSERT INTO games VALUES( + 'g1','ncaamb',2026,'2026-03-01T18:00:00Z','final', + 'AAA','BBB',81,72,'Arena',0,'espn','https://example.test/g1', + '2026-03-01T21:00:00Z','2026-03-01T21:01:00Z',NULL,'{}' + ) + """ + ) + connection.execute( + """ + INSERT INTO games VALUES( + 'g2','ncaamb',2026,'2026-03-02T18:00:00Z','final', + 'SAME','SAME',70,68,'Arena',0,'espn','https://example.test/g2', + '2026-03-02T21:00:00Z','2026-03-02T21:01:00Z',NULL,'{}' + ) + """ + ) + connection.commit() + connection.close() + before = source.read_bytes() + + store = SportsDataStore(tmp_path / "target.db") + pipeline = SportsDataPipeline( + store, + EspnScoreboardClient(tmp_path / "cache", transport=lambda _url, _timeout: b"{}"), + ) + results = pipeline.import_dummy_history(source) + assert source.read_bytes() == before + assert sum(result.stored for result in results) == 1 + games = store.completed_games("ncaam") + assert len(games) == 1 + assert games[0].league == "ncaambb" + assert games[0].home_score == 81 + assert {team.team_id for team in store.teams("ncaambb")} == {"AAA", "BBB"}