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.
- max_pf-1.0.0/.claude/settings.local.json +53 -0
- max_pf-1.0.0/.gitignore +26 -0
- max_pf-1.0.0/CHANGELOG.md +39 -0
- max_pf-1.0.0/LICENSE +21 -0
- max_pf-1.0.0/PKG-INFO +226 -0
- max_pf-1.0.0/README.md +174 -0
- max_pf-1.0.0/TODO.md +35 -0
- max_pf-1.0.0/docs/PROMPT_LOG.md +422 -0
- max_pf-1.0.0/pyproject.toml +55 -0
- max_pf-1.0.0/src/max_pf/__init__.py +41 -0
- max_pf-1.0.0/src/max_pf/__main__.py +57 -0
- max_pf-1.0.0/src/max_pf/_vendor/NOTICE.md +18 -0
- max_pf-1.0.0/src/max_pf/_vendor/__init__.py +7 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/LICENSE +21 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/__init__.py +26 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/api.py +135 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/exceptions.py +39 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/__init__.py +40 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/_parse.py +85 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/base.py +13 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/game.py +93 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/league.py +408 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/player.py +134 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/position.py +64 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/roster.py +221 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/scoring_period.py +377 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/standings.py +85 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/status.py +41 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/team.py +127 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/trade.py +127 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/trade_block.py +49 -0
- max_pf-1.0.0/src/max_pf/_vendor/fantraxapi/objs/transaction.py +77 -0
- max_pf-1.0.0/src/max_pf/app.py +137 -0
- max_pf-1.0.0/src/max_pf/box_bref.py +290 -0
- max_pf-1.0.0/src/max_pf/categories.py +49 -0
- max_pf-1.0.0/src/max_pf/engine.py +198 -0
- max_pf-1.0.0/src/max_pf/estimators.py +94 -0
- max_pf-1.0.0/src/max_pf/metric.py +79 -0
- max_pf-1.0.0/src/max_pf/models.py +90 -0
- max_pf-1.0.0/src/max_pf/optimize.py +228 -0
- max_pf-1.0.0/src/max_pf/platforms/__init__.py +6 -0
- max_pf-1.0.0/src/max_pf/platforms/base.py +53 -0
- max_pf-1.0.0/src/max_pf/platforms/fantrax.py +445 -0
- max_pf-1.0.0/src/max_pf/projections.py +45 -0
- max_pf-1.0.0/src/max_pf/report.py +109 -0
- max_pf-1.0.0/src/max_pf/sources.py +39 -0
- max_pf-1.0.0/src/max_pf/zscores.py +104 -0
- max_pf-1.0.0/tests/test_app.py +105 -0
- max_pf-1.0.0/tests/test_box_bref.py +65 -0
- max_pf-1.0.0/tests/test_box_source.py +122 -0
- max_pf-1.0.0/tests/test_engine.py +140 -0
- max_pf-1.0.0/tests/test_estimators.py +42 -0
- max_pf-1.0.0/tests/test_metric.py +42 -0
- max_pf-1.0.0/tests/test_optimize.py +66 -0
- max_pf-1.0.0/tests/test_projections.py +102 -0
- max_pf-1.0.0/tests/test_report.py +79 -0
- max_pf-1.0.0/tests/test_sources.py +25 -0
- 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
|
+
}
|
max_pf-1.0.0/.gitignore
ADDED
|
@@ -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.
|