max-pf 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. max_pf-1.0.0/.claude/settings.local.json +53 -0
  2. max_pf-1.0.0/.gitignore +26 -0
  3. max_pf-1.0.0/CHANGELOG.md +39 -0
  4. max_pf-1.0.0/LICENSE +21 -0
  5. max_pf-1.0.0/PKG-INFO +226 -0
  6. max_pf-1.0.0/README.md +174 -0
  7. max_pf-1.0.0/TODO.md +35 -0
  8. max_pf-1.0.0/docs/PROMPT_LOG.md +422 -0
  9. max_pf-1.0.0/pyproject.toml +55 -0
  10. max_pf-1.0.0/src/max_pf/__init__.py +41 -0
  11. max_pf-1.0.0/src/max_pf/__main__.py +57 -0
  12. max_pf-1.0.0/src/max_pf/_vendor/NOTICE.md +18 -0
  13. max_pf-1.0.0/src/max_pf/_vendor/__init__.py +7 -0
  14. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/LICENSE +21 -0
  15. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/__init__.py +26 -0
  16. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/api.py +135 -0
  17. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/exceptions.py +39 -0
  18. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/__init__.py +40 -0
  19. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/_parse.py +85 -0
  20. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/base.py +13 -0
  21. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/game.py +93 -0
  22. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/league.py +408 -0
  23. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/player.py +134 -0
  24. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/position.py +64 -0
  25. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/roster.py +221 -0
  26. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/scoring_period.py +377 -0
  27. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/standings.py +85 -0
  28. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/status.py +41 -0
  29. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/team.py +127 -0
  30. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/trade.py +127 -0
  31. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/trade_block.py +49 -0
  32. max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/transaction.py +77 -0
  33. max_pf-1.0.0/src/max_pf/app.py +137 -0
  34. max_pf-1.0.0/src/max_pf/box_bref.py +290 -0
  35. max_pf-1.0.0/src/max_pf/categories.py +49 -0
  36. max_pf-1.0.0/src/max_pf/engine.py +198 -0
  37. max_pf-1.0.0/src/max_pf/estimators.py +94 -0
  38. max_pf-1.0.0/src/max_pf/metric.py +79 -0
  39. max_pf-1.0.0/src/max_pf/models.py +90 -0
  40. max_pf-1.0.0/src/max_pf/optimize.py +228 -0
  41. max_pf-1.0.0/src/max_pf/platforms/__init__.py +6 -0
  42. max_pf-1.0.0/src/max_pf/platforms/base.py +53 -0
  43. max_pf-1.0.0/src/max_pf/platforms/fantrax.py +445 -0
  44. max_pf-1.0.0/src/max_pf/projections.py +45 -0
  45. max_pf-1.0.0/src/max_pf/report.py +109 -0
  46. max_pf-1.0.0/src/max_pf/sources.py +39 -0
  47. max_pf-1.0.0/src/max_pf/zscores.py +104 -0
  48. max_pf-1.0.0/tests/test_app.py +105 -0
  49. max_pf-1.0.0/tests/test_box_bref.py +65 -0
  50. max_pf-1.0.0/tests/test_box_source.py +122 -0
  51. max_pf-1.0.0/tests/test_engine.py +140 -0
  52. max_pf-1.0.0/tests/test_estimators.py +42 -0
  53. max_pf-1.0.0/tests/test_metric.py +42 -0
  54. max_pf-1.0.0/tests/test_optimize.py +66 -0
  55. max_pf-1.0.0/tests/test_projections.py +102 -0
  56. max_pf-1.0.0/tests/test_report.py +79 -0
  57. max_pf-1.0.0/tests/test_sources.py +25 -0
  58. max_pf-1.0.0/tests/test_zscores.py +69 -0
@@ -0,0 +1,53 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(git -C /home/weezy/activity/basketball_max_pf log --oneline -5)",
5
+ "Bash(python3 -c \"import requests; print\\('requests ok', requests.__version__\\)\")",
6
+ "Bash(timeout 120 python3 /tmp/probe_fantrax.py)",
7
+ "Bash(python3 -m pip install -q pytest)",
8
+ "Bash(python3 -m pytest -q)",
9
+ "Bash(python3 -m venv /tmp/mpf_venv)",
10
+ "Bash(/tmp/mpf_venv/bin/pip install *)",
11
+ "Bash(/tmp/mpf_venv/bin/python -m pytest -q)",
12
+ "Bash(git add *)",
13
+ "Bash(git commit *)",
14
+ "Bash(timeout 120 /tmp/mpf_venv/bin/python /tmp/probe_fantrax3.py)",
15
+ "Bash(timeout 120 /tmp/mpf_venv/bin/python /tmp/validate_proj.py)",
16
+ "Bash(timeout 120 /tmp/mpf_venv/bin/python /tmp/probe_fantrax4.py)",
17
+ "Bash(timeout 180 /tmp/mpf_venv/bin/python /tmp/validate_period.py)",
18
+ "Bash(timeout 150 /tmp/mpf_venv/bin/python /tmp/probe_fantrax5.py)",
19
+ "Bash(/tmp/mpf_venv/bin/python -c \"import ast,sys; src=open\\('src/max_pf/platforms/fantrax.py'\\).read\\(\\); print\\('project_period_line' in src and 'still referenced' or 'clean: no stale project_period_line ref'\\)\")",
20
+ "Bash(timeout 200 /tmp/mpf_venv/bin/python /tmp/validate_opt.py)",
21
+ "Bash(timeout 400 /tmp/mpf_venv/bin/python /tmp/validate_delta.py)",
22
+ "Bash(timeout 150 /tmp/mpf_venv/bin/python /tmp/probe_fantrax6.py)",
23
+ "Bash(PYTHONPATH=src:/tmp/FantraxAPI_ref timeout 550 /tmp/mpf_venv/bin/python -m max_pf wserh14rmbbpqtcg --periods 1-3 --out /tmp/season_test)",
24
+ "Bash(timeout 120 /tmp/mpf_venv/bin/python /tmp/probe_fantrax7.py)",
25
+ "Bash(timeout 120 /tmp/mpf_venv/bin/python /tmp/probe8.py)",
26
+ "Bash(curl -s -m 12 -o /dev/null -w \"%{http_code}\" -H \"User-Agent: Mozilla/5.0\" -H \"Referer: https://www.nba.com/\" \"https://stats.nba.com/stats/scoreboardv2?GameDate=2025-12-01&LeagueID=00&DayOffset=0\")",
27
+ "Bash(curl -s -m 12 -o /dev/null -w \"%{http_code}\" -H \"User-Agent: Mozilla/5.0\" \"https://www.basketball-reference.com/robots.txt\")",
28
+ "Bash(/tmp/mpf_venv/bin/pip download *)",
29
+ "Bash(timeout 200 /tmp/mpf_venv/bin/python /tmp/validate_stage1.py)",
30
+ "Bash(/tmp/mpf_venv/bin/python -c \"import bs4; print\\('bs4', bs4.__version__\\)\")",
31
+ "Bash(timeout 550 /tmp/mpf_venv/bin/python /tmp/validate_box.py)",
32
+ "Bash(/tmp/mpf_venv/bin/python /tmp/analyze_unmatched.py)",
33
+ "Bash(timeout 550 /tmp/mpf_venv/bin/python /tmp/validate_hindsight.py)",
34
+ "Bash(PYTHONPATH=src:/tmp/FantraxAPI_ref nohup /tmp/mpf_venv/bin/python -m max_pf wserh14rmbbpqtcg --out season_report)",
35
+ "Bash(echo \"launched PID $!\")",
36
+ "Bash(kill 127435)",
37
+ "Bash(PYTHONPATH=src:/tmp/FantraxAPI_ref /tmp/mpf_venv/bin/python *)",
38
+ "Bash(python -c \"import max_pf; print\\('max_pf OK', max_pf.__file__\\)\")",
39
+ "Bash(python -c \"import fantraxapi; print\\('fantraxapi OK', fantraxapi.__version__ if hasattr\\(fantraxapi,'__version__'\\) else 'installed'\\)\")",
40
+ "Bash(pip show *)",
41
+ "Bash(python -c \"import urllib.request; r=urllib.request.urlopen\\('https://www.fantrax.com', timeout=10\\); print\\('fantrax', r.status\\)\")",
42
+ "Bash(python3 -c \"import urllib.request as u; print\\(u.urlopen\\('https://www.fantrax.com', timeout=12\\).status\\)\")",
43
+ "Bash(python3 -c \"import urllib.request as u; print\\(u.urlopen\\('https://www.fantrax.com/fxea/general/getLeagueInfo?leagueId=wserh14rmbbpqtcg', timeout=15\\).status\\)\")",
44
+ "Bash(gh --version)",
45
+ "Bash(git push *)",
46
+ "Bash(env)",
47
+ "Bash(git rm *)",
48
+ "Bash(curl -s --max-time 20 \"https://api.github.com/repos/riders994/basketball_max_pf/pulls?state=open&head=riders994:initial_dev\")",
49
+ "Bash(curl -s --max-time 20 -o /tmp/prs.json -w \"HTTP %{http_code}\\\\n\" \"https://api.github.com/repos/riders994/basketball_max_pf/pulls?state=open\")",
50
+ "Read(//tmp/**)"
51
+ ]
52
+ }
53
+ }
@@ -0,0 +1,26 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .eggs/
8
+
9
+ # Virtualenvs
10
+ .venv/
11
+ venv/
12
+
13
+ # Test / tooling caches
14
+ .pytest_cache/
15
+ .mypy_cache/
16
+ .ruff_cache/
17
+ .coverage
18
+ htmlcov/
19
+
20
+ # Cached external data (basketball-reference HTML, etc.)
21
+ .cache/
22
+
23
+ # Secrets / platform credentials
24
+ *.cookie
25
+ fantraxloggedin.cookie
26
+ .env
@@ -0,0 +1,39 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres
5
+ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ Planned work is tracked in [TODO.md](TODO.md).
8
+
9
+ ## [1.0.0] - 2026-06-18
10
+
11
+ First public release. Computes the "max points for" metric for 9-category
12
+ head-to-head fantasy basketball.
13
+
14
+ ### Added
15
+
16
+ - **Metric & engine.** Opponent-relative category wins (ties = draws worth 0.5,
17
+ turnovers inverted); one-round best response yielding **M1 / M2 / Δ** (the
18
+ opponent-mismanagement dividend). FG%/FT% aggregated as Σmakes/Σattempts.
19
+ - **Two methodologies** behind a pluggable `StatSource`: **A-expected**
20
+ (season-to-date projections) and **A-hindsight** (realized box scores).
21
+ - **Data sources.** Fantrax for league structure (the FantraxAPI fork, vendored
22
+ under `max_pf._vendor` so installs need no VCS/URL dependency) and
23
+ basketball-reference for exact per-player makes/attempts (disk-cached). Exact
24
+ A-expected on box scores replaces the FG/FT attempt estimator (kept as a
25
+ no-extra-dependency fallback).
26
+ - **Three lineup objectives.** A (`catwins`, opponent-aware category wins),
27
+ B (`zscore`, scarcity-weighted z-value — benches below-replacement players to
28
+ protect ratios/TO), C (`raw`, raw output / volume). All pluggable.
29
+ - **Nash mutual ceiling, stage 1.** `engine.nash_ceiling` runs iterated best
30
+ response to a pure-strategy equilibrium (**M3**), with cycle detection for
31
+ matchups with no pure equilibrium.
32
+ - **Entry point & CLI.** `max_pf.run(login, weeks=..., methodology=...,
33
+ objective=...)` takes a platform login dict and returns the season report;
34
+ `max-pf <login.json|yaml>` (also `python -m max_pf ...`) renders a table and
35
+ writes CSV/Markdown. `weeks` scopes to one week or a selection.
36
+ - Installable package (`src/` layout, hatchling) with `boxscores` and `yaml`
37
+ extras; 52 unit tests; live-validated against a finished public league.
38
+
39
+ [1.0.0]: https://github.com/riders994/basketball_max_pf/releases/tag/v1.0.0
max_pf-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 riders994
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
max_pf-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.4
2
+ Name: max-pf
3
+ Version: 1.0.0
4
+ Summary: Methodology + tooling to compute the 'max points for' metric for 9-cat fantasy basketball leagues.
5
+ Project-URL: Homepage, https://github.com/riders994/basketball_max_pf
6
+ Project-URL: Repository, https://github.com/riders994/basketball_max_pf
7
+ Project-URL: Issues, https://github.com/riders994/basketball_max_pf/issues
8
+ Project-URL: Changelog, https://github.com/riders994/basketball_max_pf/blob/primary/CHANGELOG.md
9
+ Author-email: Rohan V <r.vahalia@gmail.com>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 riders994
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: 9-cat,basketball,fantasy,fantrax,optimization
33
+ Classifier: Development Status :: 5 - Production/Stable
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: OS Independent
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Topic :: Games/Entertainment
41
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
42
+ Requires-Python: >=3.11
43
+ Requires-Dist: requests>=2.28
44
+ Provides-Extra: boxscores
45
+ Requires-Dist: beautifulsoup4>=4.12; extra == 'boxscores'
46
+ Provides-Extra: dev
47
+ Requires-Dist: pytest>=7; extra == 'dev'
48
+ Requires-Dist: pyyaml>=6; extra == 'dev'
49
+ Provides-Extra: yaml
50
+ Requires-Dist: pyyaml>=6; extra == 'yaml'
51
+ Description-Content-Type: text/markdown
52
+
53
+ # basketball_max_pf
54
+
55
+ A Python package for computing a **"max points for"** metric for a 9-category
56
+ head-to-head fantasy basketball league. Give it your league platform and a
57
+ league id and it reconstructs, for every team and every matchup period, the
58
+ best lineup you *could* have set — and turns the gap between that and reality
59
+ into a single interpretable number.
60
+
61
+ The first supported platform is **Fantrax**; the design keeps the platform,
62
+ the player-stat source, and the lineup objective behind seams so other sports
63
+ or platforms can be added later.
64
+
65
+ > Status: Objective A (opponent-aware category-win maximization) is implemented
66
+ > end-to-end and validated against a finished public league. See
67
+ > [Roadmap](#roadmap) for what's next, and
68
+ > [`docs/PROMPT_LOG.md`](docs/PROMPT_LOG.md) for the full design journal.
69
+
70
+ ## The metric
71
+
72
+ "Points for" in this league *are* category wins — they only exist relative to
73
+ an opponent (a tie is a **draw**, worth 0.5; Fantrax awards ties, it does not
74
+ wash or split them). So the metric and the benchmark are one decision, not two.
75
+
76
+ For each matchup period we compute a one-round **best response** (each side
77
+ optimizes against the *other side's actual* lineup — a well-posed fixed target,
78
+ avoiding the Nash infinite regress):
79
+
80
+ | Quantity | Definition | Reads as |
81
+ | --- | --- | --- |
82
+ | **M1** | catwins(`mine_opt`, `opp_actual`) | your exploitation ceiling vs how they actually played |
83
+ | **M2** | catwins(`mine_opt`, `their_opt`) | the same lineup vs their best counter |
84
+ | **Δ** | M1 − M2 | the **opponent-mismanagement dividend** — points banked purely because the opponent didn't optimize |
85
+ | **Luck** | Actual − M1 | points left on the table (negative ⇒ you fell short of your own ceiling) |
86
+
87
+ FG% and FT% are aggregated correctly across a lineup via Σmakes / Σattempts
88
+ (never by averaging percentages); TO is treated as lower-is-better.
89
+
90
+ The best-response above is **Objective A** (the default, `--objective catwins`):
91
+ each side optimizes category wins against the other's *actual* lineup. The
92
+ optimizer's objective is pluggable, with two opponent-independent alternatives
93
+ that report catwins as a readout: **Objective B** (`--objective zscore`) maximizes
94
+ total **z-score value** (scarcity-weighted), and **Objective C** (`--objective raw`)
95
+ maximizes total **raw output** (scarcity-blind, volume-chasing). Both *bench
96
+ below-replacement (negative-value) players* — since value prices in FG%/FT%
97
+ volume-impact and turnovers, dropping them protects the ratio cats and TO that
98
+ swing a week (C, whose raw value is almost always positive, benches far less).
99
+ See [Roadmap](#roadmap).
100
+
101
+ ## Two methodologies
102
+
103
+ The optimizer needs a target statline for each player. Where that comes from is
104
+ the **ex-ante vs ex-post** fork, and the package implements both:
105
+
106
+ - **A-expected** (`--methodology expected`) — season-to-date *per-game rates*
107
+ placed on the days each player's team plays in the period. No clairvoyance;
108
+ the ceiling is a projection. Rates come from exact basketball-reference
109
+ box scores (real makes/attempts, no estimator); the Fantrax season-to-date
110
+ view + position-based FG/FT attempt estimator is the no-extra-dependency
111
+ fallback when box scores aren't attached. Its `Luck` mixes forecast variance
112
+ with real mismanagement.
113
+ - **A-hindsight** (`--methodology hindsight`) — *realized* per-day box scores
114
+ from basketball-reference, joined to the full historical roster. This is the
115
+ true "best lineup you could have set" ceiling: **M1 ≥ Actual always**, so
116
+ `Luck` is genuine lineup mismanagement with projection noise removed.
117
+
118
+ ## Data sources
119
+
120
+ - **Fantrax** (via the [stable fork of FantraxAPI](https://github.com/riders994/FantraxAPI),
121
+ **vendored** under `max_pf._vendor` — see `src/max_pf/_vendor/NOTICE.md`) is the
122
+ source of truth for league structure: rosters (incl. historical daily
123
+ membership), active-slot config, matchup schedule, and **actual results**.
124
+ Public leagues read with only the league id (no credentials).
125
+ - **basketball-reference** supplies exact per-player per-day 9-cat lines with
126
+ real makes/attempts (FG%/FT% denominators Fantrax never exposes). Disk-cached
127
+ under `.cache/bref/` at a polite request rate; `nba_api` is a drop-in for
128
+ local runs where stats.nba.com is reachable.
129
+
130
+ These sit behind a pluggable `StatSource` seam, so the optimizer, engine, and
131
+ report are identical across methodologies.
132
+
133
+ ## Install
134
+
135
+ ```bash
136
+ pip install -e ".[boxscores]" # add 'dev' for the test suite
137
+ ```
138
+
139
+ The Fantrax adapter works out of the box — its API client is vendored, so there
140
+ is no VCS/URL dependency (and the package installs cleanly from PyPI). `boxscores`
141
+ pulls beautifulsoup4 for the basketball-reference source; `yaml` adds YAML
142
+ login-file support on the CLI (JSON works without it). (Per packaging convention, all
143
+ version specifiers are `>=` for compatibility.)
144
+
145
+ ## Usage
146
+
147
+ **Programmatic** — pass a *login dict* naming the platform and its details; you
148
+ get back the season-to-date report rows (render with the `render_*` helpers):
149
+
150
+ ```python
151
+ import max_pf
152
+
153
+ rows = max_pf.run({"platform": "fantrax", "league_id": "wserh14rmbbpqtcg"})
154
+ print(max_pf.render_table(rows))
155
+
156
+ # Scope to one week or a selection, and pick methodology / objective:
157
+ rows = max_pf.run(login, weeks="1-6", methodology="hindsight", objective="zscore")
158
+ rows = max_pf.run(login, weeks=6) # a single week
159
+ ```
160
+
161
+ `weeks` accepts a single int, a list, or a spec string (`"5"`, `"1-6"`,
162
+ `"1,2,5"`); `None` (the default) is the whole season to date. Box scores back
163
+ both methodologies by default; pass `boxscores=False` for the no-extra-dependency
164
+ Fantrax estimator (A-expected only).
165
+
166
+ **Command line** — point it at a JSON or YAML file holding the same login dict:
167
+
168
+ ```bash
169
+ # league.json: {"platform": "fantrax", "league_id": "wserh14rmbbpqtcg"}
170
+ python -m max_pf league.json
171
+ python -m max_pf league.yaml --weeks 1-6 --methodology hindsight --out reports/half1
172
+ ```
173
+
174
+ It prints the table and writes `<out>.csv` and `<out>.md`.
175
+
176
+ ## Findings
177
+
178
+ Full-season run on the finished public league `wserh14rmbbpqtcg` ("Mao's Macho
179
+ Mandarins", 2025-26 NBA, 16 teams × 24 periods), A-expected vs A-hindsight.
180
+ (The committed sample reports were removed in 1.0.0 pending regeneration on the
181
+ box-score default — see [`TODO.md`](TODO.md); reproduce with
182
+ `python -m max_pf league.json [--methodology hindsight]`.)
183
+
184
+ - **The ceiling invariant holds under hindsight.** M1 ≥ Actual for every team
185
+ in both reports; A-hindsight makes this a *guarantee* (realized data),
186
+ whereas A-expected can't because a team that ran hot beats its projection.
187
+ - **Projections are optimistic.** A-expected M1 > A-hindsight M1 for 15 of 16
188
+ teams (smooth per-game rates, no DNPs/variance). The lone exception is
189
+ *Trusting the Process*, whose players out-ran their season rates in the games
190
+ actually played.
191
+ - **Hindsight isolates real mismanagement.** With projection variance stripped,
192
+ `Luck` magnitudes shrink (band −6.5…−60 vs expected −9.5…−66) — the Actual−M1
193
+ gap becomes genuine points left on the table, not forecast noise.
194
+ - **The dividend Δ tightens to a credible band.** The expected spread (7–73)
195
+ compresses to 11–35 under hindsight. The dbyun89 expected outlier (Δ=73,
196
+ driven by an implausible expected M2=38) corrects to Δ=35 (M2=71), confirming
197
+ it was a projection artifact rather than a real opponent giveaway.
198
+
199
+ **Takeaway:** A-hindsight is the trustworthy read for both the efficiency
200
+ ceiling (`Luck`) and the dividend (`Δ`); A-expected is the no-clairvoyance
201
+ projection useful before results are in.
202
+
203
+ ## Roadmap
204
+
205
+ - Live re-validation against the updated `@stable` fork — **done** (results
206
+ bit-identical to the pre-update fork; see the prompt log).
207
+ - Exact A-expected on box scores (estimator replaced as the metric's data
208
+ source) — **done**; reproduces the estimator's category-win metric while using
209
+ real makes/attempts and the real schedule.
210
+ - Objective B (z-score / punt-aware weighting) — **done**. `--objective zscore`
211
+ plays each side's opponent-independent max-total-z-value lineup (league-wide
212
+ per-game population; volume-weighted ratio impact, TO inverted); catwins is a
213
+ readout. Objective A's category-win M1 is >= Objective B's by construction.
214
+ - Objective C (raw statistical output) — **done**. `--objective raw` plays each
215
+ side's max-raw-output lineup (same league-wide population as B, but values
216
+ players without scarcity weighting, so it chases volume); opponent-independent,
217
+ catwins as a readout.
218
+ - Nash "mutual ceiling" (two-player equilibrium): stage 1 done —
219
+ `engine.nash_ceiling` runs iterated best response to the fixed point where each
220
+ lineup best-responds to the other (pure-strategy Nash), yielding **M3** = the
221
+ both-sides-optimal category split; it flags cycles (no pure equilibrium). On
222
+ the test league it converges in ~80% of periods.
223
+
224
+ Open items (Nash stage 2, M3 in the report, artifact regeneration, CI, more
225
+ platforms) are tracked in [`TODO.md`](TODO.md). See
226
+ [`docs/PROMPT_LOG.md`](docs/PROMPT_LOG.md) for the complete design history.
max_pf-1.0.0/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # basketball_max_pf
2
+
3
+ A Python package for computing a **"max points for"** metric for a 9-category
4
+ head-to-head fantasy basketball league. Give it your league platform and a
5
+ league id and it reconstructs, for every team and every matchup period, the
6
+ best lineup you *could* have set — and turns the gap between that and reality
7
+ into a single interpretable number.
8
+
9
+ The first supported platform is **Fantrax**; the design keeps the platform,
10
+ the player-stat source, and the lineup objective behind seams so other sports
11
+ or platforms can be added later.
12
+
13
+ > Status: Objective A (opponent-aware category-win maximization) is implemented
14
+ > end-to-end and validated against a finished public league. See
15
+ > [Roadmap](#roadmap) for what's next, and
16
+ > [`docs/PROMPT_LOG.md`](docs/PROMPT_LOG.md) for the full design journal.
17
+
18
+ ## The metric
19
+
20
+ "Points for" in this league *are* category wins — they only exist relative to
21
+ an opponent (a tie is a **draw**, worth 0.5; Fantrax awards ties, it does not
22
+ wash or split them). So the metric and the benchmark are one decision, not two.
23
+
24
+ For each matchup period we compute a one-round **best response** (each side
25
+ optimizes against the *other side's actual* lineup — a well-posed fixed target,
26
+ avoiding the Nash infinite regress):
27
+
28
+ | Quantity | Definition | Reads as |
29
+ | --- | --- | --- |
30
+ | **M1** | catwins(`mine_opt`, `opp_actual`) | your exploitation ceiling vs how they actually played |
31
+ | **M2** | catwins(`mine_opt`, `their_opt`) | the same lineup vs their best counter |
32
+ | **Δ** | M1 − M2 | the **opponent-mismanagement dividend** — points banked purely because the opponent didn't optimize |
33
+ | **Luck** | Actual − M1 | points left on the table (negative ⇒ you fell short of your own ceiling) |
34
+
35
+ FG% and FT% are aggregated correctly across a lineup via Σmakes / Σattempts
36
+ (never by averaging percentages); TO is treated as lower-is-better.
37
+
38
+ The best-response above is **Objective A** (the default, `--objective catwins`):
39
+ each side optimizes category wins against the other's *actual* lineup. The
40
+ optimizer's objective is pluggable, with two opponent-independent alternatives
41
+ that report catwins as a readout: **Objective B** (`--objective zscore`) maximizes
42
+ total **z-score value** (scarcity-weighted), and **Objective C** (`--objective raw`)
43
+ maximizes total **raw output** (scarcity-blind, volume-chasing). Both *bench
44
+ below-replacement (negative-value) players* — since value prices in FG%/FT%
45
+ volume-impact and turnovers, dropping them protects the ratio cats and TO that
46
+ swing a week (C, whose raw value is almost always positive, benches far less).
47
+ See [Roadmap](#roadmap).
48
+
49
+ ## Two methodologies
50
+
51
+ The optimizer needs a target statline for each player. Where that comes from is
52
+ the **ex-ante vs ex-post** fork, and the package implements both:
53
+
54
+ - **A-expected** (`--methodology expected`) — season-to-date *per-game rates*
55
+ placed on the days each player's team plays in the period. No clairvoyance;
56
+ the ceiling is a projection. Rates come from exact basketball-reference
57
+ box scores (real makes/attempts, no estimator); the Fantrax season-to-date
58
+ view + position-based FG/FT attempt estimator is the no-extra-dependency
59
+ fallback when box scores aren't attached. Its `Luck` mixes forecast variance
60
+ with real mismanagement.
61
+ - **A-hindsight** (`--methodology hindsight`) — *realized* per-day box scores
62
+ from basketball-reference, joined to the full historical roster. This is the
63
+ true "best lineup you could have set" ceiling: **M1 ≥ Actual always**, so
64
+ `Luck` is genuine lineup mismanagement with projection noise removed.
65
+
66
+ ## Data sources
67
+
68
+ - **Fantrax** (via the [stable fork of FantraxAPI](https://github.com/riders994/FantraxAPI),
69
+ **vendored** under `max_pf._vendor` — see `src/max_pf/_vendor/NOTICE.md`) is the
70
+ source of truth for league structure: rosters (incl. historical daily
71
+ membership), active-slot config, matchup schedule, and **actual results**.
72
+ Public leagues read with only the league id (no credentials).
73
+ - **basketball-reference** supplies exact per-player per-day 9-cat lines with
74
+ real makes/attempts (FG%/FT% denominators Fantrax never exposes). Disk-cached
75
+ under `.cache/bref/` at a polite request rate; `nba_api` is a drop-in for
76
+ local runs where stats.nba.com is reachable.
77
+
78
+ These sit behind a pluggable `StatSource` seam, so the optimizer, engine, and
79
+ report are identical across methodologies.
80
+
81
+ ## Install
82
+
83
+ ```bash
84
+ pip install -e ".[boxscores]" # add 'dev' for the test suite
85
+ ```
86
+
87
+ The Fantrax adapter works out of the box — its API client is vendored, so there
88
+ is no VCS/URL dependency (and the package installs cleanly from PyPI). `boxscores`
89
+ pulls beautifulsoup4 for the basketball-reference source; `yaml` adds YAML
90
+ login-file support on the CLI (JSON works without it). (Per packaging convention, all
91
+ version specifiers are `>=` for compatibility.)
92
+
93
+ ## Usage
94
+
95
+ **Programmatic** — pass a *login dict* naming the platform and its details; you
96
+ get back the season-to-date report rows (render with the `render_*` helpers):
97
+
98
+ ```python
99
+ import max_pf
100
+
101
+ rows = max_pf.run({"platform": "fantrax", "league_id": "wserh14rmbbpqtcg"})
102
+ print(max_pf.render_table(rows))
103
+
104
+ # Scope to one week or a selection, and pick methodology / objective:
105
+ rows = max_pf.run(login, weeks="1-6", methodology="hindsight", objective="zscore")
106
+ rows = max_pf.run(login, weeks=6) # a single week
107
+ ```
108
+
109
+ `weeks` accepts a single int, a list, or a spec string (`"5"`, `"1-6"`,
110
+ `"1,2,5"`); `None` (the default) is the whole season to date. Box scores back
111
+ both methodologies by default; pass `boxscores=False` for the no-extra-dependency
112
+ Fantrax estimator (A-expected only).
113
+
114
+ **Command line** — point it at a JSON or YAML file holding the same login dict:
115
+
116
+ ```bash
117
+ # league.json: {"platform": "fantrax", "league_id": "wserh14rmbbpqtcg"}
118
+ python -m max_pf league.json
119
+ python -m max_pf league.yaml --weeks 1-6 --methodology hindsight --out reports/half1
120
+ ```
121
+
122
+ It prints the table and writes `<out>.csv` and `<out>.md`.
123
+
124
+ ## Findings
125
+
126
+ Full-season run on the finished public league `wserh14rmbbpqtcg` ("Mao's Macho
127
+ Mandarins", 2025-26 NBA, 16 teams × 24 periods), A-expected vs A-hindsight.
128
+ (The committed sample reports were removed in 1.0.0 pending regeneration on the
129
+ box-score default — see [`TODO.md`](TODO.md); reproduce with
130
+ `python -m max_pf league.json [--methodology hindsight]`.)
131
+
132
+ - **The ceiling invariant holds under hindsight.** M1 ≥ Actual for every team
133
+ in both reports; A-hindsight makes this a *guarantee* (realized data),
134
+ whereas A-expected can't because a team that ran hot beats its projection.
135
+ - **Projections are optimistic.** A-expected M1 > A-hindsight M1 for 15 of 16
136
+ teams (smooth per-game rates, no DNPs/variance). The lone exception is
137
+ *Trusting the Process*, whose players out-ran their season rates in the games
138
+ actually played.
139
+ - **Hindsight isolates real mismanagement.** With projection variance stripped,
140
+ `Luck` magnitudes shrink (band −6.5…−60 vs expected −9.5…−66) — the Actual−M1
141
+ gap becomes genuine points left on the table, not forecast noise.
142
+ - **The dividend Δ tightens to a credible band.** The expected spread (7–73)
143
+ compresses to 11–35 under hindsight. The dbyun89 expected outlier (Δ=73,
144
+ driven by an implausible expected M2=38) corrects to Δ=35 (M2=71), confirming
145
+ it was a projection artifact rather than a real opponent giveaway.
146
+
147
+ **Takeaway:** A-hindsight is the trustworthy read for both the efficiency
148
+ ceiling (`Luck`) and the dividend (`Δ`); A-expected is the no-clairvoyance
149
+ projection useful before results are in.
150
+
151
+ ## Roadmap
152
+
153
+ - Live re-validation against the updated `@stable` fork — **done** (results
154
+ bit-identical to the pre-update fork; see the prompt log).
155
+ - Exact A-expected on box scores (estimator replaced as the metric's data
156
+ source) — **done**; reproduces the estimator's category-win metric while using
157
+ real makes/attempts and the real schedule.
158
+ - Objective B (z-score / punt-aware weighting) — **done**. `--objective zscore`
159
+ plays each side's opponent-independent max-total-z-value lineup (league-wide
160
+ per-game population; volume-weighted ratio impact, TO inverted); catwins is a
161
+ readout. Objective A's category-win M1 is >= Objective B's by construction.
162
+ - Objective C (raw statistical output) — **done**. `--objective raw` plays each
163
+ side's max-raw-output lineup (same league-wide population as B, but values
164
+ players without scarcity weighting, so it chases volume); opponent-independent,
165
+ catwins as a readout.
166
+ - Nash "mutual ceiling" (two-player equilibrium): stage 1 done —
167
+ `engine.nash_ceiling` runs iterated best response to the fixed point where each
168
+ lineup best-responds to the other (pure-strategy Nash), yielding **M3** = the
169
+ both-sides-optimal category split; it flags cycles (no pure equilibrium). On
170
+ the test league it converges in ~80% of periods.
171
+
172
+ Open items (Nash stage 2, M3 in the report, artifact regeneration, CI, more
173
+ platforms) are tracked in [`TODO.md`](TODO.md). See
174
+ [`docs/PROMPT_LOG.md`](docs/PROMPT_LOG.md) for the complete design history.
max_pf-1.0.0/TODO.md ADDED
@@ -0,0 +1,35 @@
1
+ # TODO — open roadmap items
2
+
3
+ Tracked work beyond the 1.0.0 release. See `docs/PROMPT_LOG.md` for the full
4
+ design history and `CHANGELOG.md` for what shipped.
5
+
6
+ ## Next release round
7
+
8
+ - [ ] **Regenerate the season report artifacts** on the box-score default and
9
+ re-commit them. The 1.0.0 release removed the previous `season_report.*`
10
+ files because they were generated on the old Fantrax-estimator path
11
+ (pre-dating exact A-expected on box scores) and were stale.
12
+ - [ ] **Nash mutual ceiling, stage 2.** Mixed-strategy / minimax *value* via a
13
+ double-oracle LP for the ~20% of matchup periods where iterated best
14
+ response cycles (no pure equilibrium). Adds an LP dependency + a
15
+ payoff-matrix builder.
16
+ - [ ] **Wire Nash M3 into the report.** Surface the mutual-ceiling split (and the
17
+ `M1 − M3` decomposition) as columns in the CSV/Markdown season report.
18
+
19
+ ## Packaging / infra
20
+
21
+ - [ ] **Replace the vendored fantraxapi with a real dependency** once a published
22
+ `fantraxapi` (PyPI, with maintainer access) is available. Swap
23
+ `src/max_pf/_vendor/fantraxapi` for a versioned `>=` dep; refresh steps are
24
+ in `src/max_pf/_vendor/NOTICE.md`.
25
+ - [ ] **CI** (GitHub Actions): run `pytest` and a vendored-import
26
+ self-containment check (import with no top-level `fantraxapi` installed) on
27
+ pushes/PRs.
28
+ - [ ] *(Optional)* Prune the vendored fantraxapi to just the modules the adapter
29
+ uses (trades / trade blocks / standings are unused), trading a slightly
30
+ smaller footprint for more effort at each fork refresh.
31
+
32
+ ## Methodology / scope
33
+
34
+ - [ ] **Additional platforms** behind the `LeaguePlatform` interface (beyond
35
+ Fantrax) via the `register_platform` hook.