Changelog¶
All notable changes to this project will be documented in this file. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]¶
Added¶
--deltasrooting guide in the HTML report (#99):--format html --deltaspreviously dropped the per-game deltas silently, because only the Excel workbook had a Deltas sheet.FantasyHtmlExporternow hasexport_deltas(same signature as the Excel exporter), and thefantasyfbCLI feeds the deltas to every active exporter. The report gains a Rooting guide tab, shown only when--deltasran. For the team picked in the Viewing selector it lists this week's games (their own game first) with the change in that team's expected earnings if either side wins and a "Root for ..." call, followed by the full league-wide matrix shaded red to green around zero. Test coverage added.
[0.10.0] — 2026-10-06¶
Interactive HTML report, game-day cache freshness, and live-week projection fixes.
Added¶
- Interactive HTML report as an alternative to the Excel workbook (#93): a new
FantasyHtmlExporter(fantasyfb.io.html_exporter) has the same shape asFantasyExcelExporter(export_rosters,export_available,export_schedule,export_standings,export_analysis,close) but writes one self-contained.htmlfile. The tables are embedded as a JSON blob and rendered client-side by inlined vanilla JS and the "Industry" design-system CSS, so the report works offline and can be emailed (only Google Fonts is fetched, with a system-font fallback). Tabs: My week (matchup card with score distributions, outlook, season path, lineup and bench), Standings (sortable, click a team to scope the whole report to it), Schedule (week-by-week matchup cards), Moves (Adds, Pickups, Drops and Trades, shown only for the analyses that ran) and Free agents (search, position filter, minimum rostered % slider defaulting to 5%, WAR-sorted). The Viewing selector rescopes the report to any team, and the tab and team are kept in the URL hash. For--bestballruns, which have no schedule, the matchup, season path and Schedule views are hidden. --format excel|html|bothflag onfantasyfb(#93, defaultexcel, so existing behavior is unchanged):htmlwritesFantasyFootballProjections_<Weekday>Week<N>.htmlnext to where the workbook would go,bothwrites both.Deltasremains Excel-only.--drop-safe-thresholdand--drop-depth-threshold(defaults -4 and -12): the dollar cutoffs behind the Drops view's "Safe to cut" / "Depth" / "Keep" verdicts in the HTML report.jinja2>=3.1is now a runtime dependency (used to render the report template, which ships as package data).
Changed¶
- Shorter NFL data cache on game days (#95): the
fantasyfbCLI now uses a 30 minute cache TTL instead of nflreadpy's 24 hour default in September through January on every day except Wednesday and Friday (is_live_nfl_window), so rerunning on Sunday or Monday picks up newly finished games and nflverse stat corrections without--refresh-cache. Before this, a team whose players had all finished could still show a nonzero chance of losing because the cached schedule and box scores predated the final whistle. Wednesday, Friday, and the offseason keep the 24 hour default. An explicitcache_durationargument or theNFLREADPY_CACHE_DURATIONenvironment variable still wins, and--refresh-cachestill forces a fresh pull. Newlive_cache_durationargument onNflreadpyProvider. Test coverage added.
Fixed¶
- IR-slotted players counted as extra starters during a live week (#92):
LineupOptimizer._handle_live_week_lineuptreated any rostered player not in aBNslot whose NFL game had finished as "already started", which included players parked onIR. They were locked in withstarter=Trueon top of a full lineup, so their projection was added to the team's live-week total. In week 3 of 2026, Nico Collins and Rico Dowdle both sat on IR and pushed finished teams' projections above their final Yahoo scores. Inactive slots (BN,IR,IR+,NA) are now excluded from both the started set and the lineup candidates. Test coverage added. - Team defenses never matched their stats, so every DEF was projected as a league-average defense (#92):
PlayerDataManager.map_player_idsset a defense'splayer_id_srto the platform's display name (Yahoo's"Chiefs"), whileNflreadpyProviderkeys team-DEF stat rows by team code ("KC"). The join never matched, so every defense hadnum_games=0and fell back to the positional average projection all season, and during a live week no defense ever got its actual points substituted in (the Chiefs kept a 10.2 projection after their game ended). Defenses are now keyed bycurrent_team.League.get_rateskeeps the platform's display name rather than renaming defenses to their team code. Test coverage added. - Kickers in distance-scored leagues got an extra 3 points per made field goal (#92):
apply_default_scoring_categoriesaddedFG 0-19 = 3whenever that key was missing, even for leagues that score kicks only byFG Yds(0.1 per yard). Every made field goal was double-counted, overprojecting kickers all season and overstating live-week actuals by 3 to 9 points per kicker in week 3 of 2026 (e.g. Will Reichard at 26.3 vs Yahoo's 17.3). The flat default now applies only when the league has noFG *scoring category at all. Test coverage added.
[0.9.0] — 2026-09-15¶
Removed¶
- Dead V1 CLI flags
--gamesand--basaloppstringtime(#85): both were parsed and accepted but silently ignored downstream,League.load_parameters()explicitly documented them as "accepted for backward compatibility with the V1 CLI but ignored downstream." A user passing either flag would reasonably expect their projections to change; nothing happened. Removed fromcli.py's argument parsing (including thebasaloppstringtimecomma-split/validation block), the correspondingLeague.__init__/load_parametersparameters and placeholderself.reference_games/self.basaloppstringtimeattributes, and their rows in the CLI reference docs. Breaking change: passing either flag now raises optparse's "no such option" error instead of silently doing nothing. Fixes #83.
Fixed¶
MoveAnalyzer.possible_pickupscrashed with aTypeErrorfor any healthy drop candidate (#86):"'>=' not supported between instances of 'NoneType' and 'int'".untilisNaN/Noneby default for a player with no active injury designation (set only whenPlayerDataManager.add_injuriesfinds one), but the "is this player out through the current week" check compared it directly against an int with no null guard, unlike the free-agent-availability filter two lines earlier in the same method, which already handled this correctly viauntil.isnull() | (until < 17). A nulluntilnow takes the same branch a healthy player should: only free agents within 0.5 WAR of the player being dropped are considered, rather than "any free agent is an upgrade" (reserved for a player who's actually out and contributing nothing this week). First test coverage added forMoveAnalyzer, which is how this went unnoticed.- 2025+ depth-chart schema labeled kickers "PK" instead of "K" (follow-up to #78): nflreadpy's newer depth-chart feed uses
pos_abbvalue"PK"for kickers, while everywhere else in fantasyfb (theplayersDataFrame, the legacy pre-2025 depth-chart schema,MatchupModel's_STARTER_SLOTS/_DEFAULT_WEIGHTS) keys on"K".get_depth_charts()didn't normalize this, so the live feed returned zero"K"rows andPlayerDataManager.add_depth_charts's name+position fallback could never match a kicker, silently defaulting every one of them tostring=2.0. Currently harmless in practice sinceK'sgammaweight is0.0(the string penalty never fires for kickers regardless), but would misfire the moment that weight is tuned away from 0.get_depth_charts()now normalizes"PK"to"K"right after the schema-specific rename. - Already-started players in a completed game showed as benched during a live week (follow-up to #18):
LineupOptimizer._handle_live_week_lineupcorrectly identifies players whose game has finished and who were active (selected_position != "BN") in the real platform lineup, but only ever used that set to compute remaining roster capacity -- nothing setplayers["starter"] = Truefor them, so they fell back to thestarter=Falsedefault despite being legitimately active. That silently broke every downstream consumer of the starter flag:League._apply_live_week_actuals's real-score blending never substituted in their actual points, andseason_simsnever counted their points toward the week's team total at all. First test coverage added forLineupOptimizer(previously untested), which is how this went unnoticed. - Depth-chart rows fanned out into duplicates for any player with a secondary role (follow-up to #78): nflreadpy's depth chart has one row per (player, role) -- a WR who also returns punts, or an RB who also returns kicks, has multiple rows sharing the same
player_id_sr.get_depth_charts()joined these onto the players DataFrame with no dedup, so 200+ players league-wide were duplicated downstream. That corruptedLineupOptimizer's per-position slot-filling: two rows for one real player could each independently pass the "not yet a starter" filter, silently consuming a roster slot meant for a different, lower-scoring teammate. A new_dedupe_depth_chart()collapses to one row perplayer_id_sr, preferring the fantasy-relevant offensive/kicking role over a special-teams-only one (a naive "lowest string wins" rule doesn't work -- a KR/PR string is often numerically lower than a player's real offensive string). MatchupModel's depth-chart string penalty crushed real WR2/WR3/RB2/TE2 starters (#78): the flatmax(0, string - 1)penalty treated nflreadpy'spos_rank(a full ordinal depth ranking, 1-9+) as a starter/backup flag for every position, when that assumption only holds for QB -- real NFL offenses routinely start 2-3 WRs and use multiple RBs. Legitimate starting WR2s/WR3s/RB2s/TE2s were penalized as if they were backups, corruptingpoints_avg, lineup selection, andseason_simsfor every week of the season. Replaced with a per-position "real starter slots" cutoff (QB: 1, RB: 2, WR: 3, TE: 2) below which no penalty applies, tapering in beyond that. Also fixedsim/backtest.py's hardcodedstring=1.0, which meant the walk-forward fit never actually exercised the string penalty:get_depth_charts()now accepts an optional(season, week)to look up real historical depth-chart data (available back to at least 2015, contrary to a since-removed comment claiming otherwise).- Current-week matchup projections ignored points already on the board mid-slate (#18): during a live week, the season simulator summed every starter's pre-game
points_avgfor the current week even after their NFL game had finished, so a matchup's projected score and win probability stayed frozen at their pre-kickoff values as Sunday unfolded. AddedLeague._apply_live_week_actuals: for the active season's current week, any starter whose NFL game has finished gets their actual fantasy points substituted in aspoints_avgwithpoints_stdevset to 0, so that part of the matchup becomes deterministic and only pending games still carry variance.
[0.8.2] — 2026-08-26¶
Fixed¶
- ADP merge silently dropped every suffixed player name (#68):
merge_adpjoined projections onto ADP with an exact string match on[name, position]. FantasyPros-style ADP exports append generational suffixes (James Cook III,Travis Etienne Jr.) that the projections' nflreadpy-sourcednamecolumn omits, so ~two dozen players a season, including early-round starters like James Cook (RB5), Kenneth Walker III (RB12), and Travis Etienne Jr. (RB20), silently gotNaNADP and were treated as undraftable depth. A new_normalize_name_keyhelper builds a suffix-stripped, whitespace-collapsed, lowercased join key used for the merge only; both display names are preserved untouched. The ADP side is deduped on that key (earliest pick wins) so a collapsed namesake pair can't fan out a projection row, and the suffix regex is anchored so an ordinary trailing name token is never stripped.
[0.8.1] — 2026-08-24¶
Fixed¶
current_teamstuck on a player's previous team during the offseason/preseason (#65):PlayerDataManager.add_depth_chartsalready fetched nflreadpy's live depth chart, which carries the correctcurrent_team, but only pulled itsstring(depth-chart rank) column out, leavingcurrent_teamto come from the season-locked roster snapshot everywhere. That snapshot doesn't reflect a trade/signing until nflverse publishes the new season's roster file, well after free agency, so a player who changed teams (e.g. Stefon Diggs to WAS in 2026) still showed their old team (NE) on the draft board with no current-season games played yet to correct it. The name-based fallback join made this worse by requiringcurrent_teamto already match, which can never be true for exactly the players it exists to catch.current_teamfrom the depth chart now overrides the stale value on both the id-based and name-based join paths; the name join is deduped on(name, position)first to avoid fanning out rows on rare same-name collisions.
[0.8.0] — 2026-08-23¶
Custom roster shapes for generic mock drafts, plus a fix for mock-opponent picks drifting from real-world ADP.
Added¶
--roster-spotsonsnake-draftandsalary-cap-draft(#59): lets a--platform genericmock draft use a custom roster shape (extra flex, superflex viaQ/W/R/T, custom bench size, etc.) instead of the fixed default, via comma-separatedPOSITION=COUNTpairs (e.g.QB=1,RB=2,W/R/T=2,Q/W/R/T=1,BN=6) or an interactive prompt when omitted.GenericClientandLeague(platform="generic")now accept an optionalroster_spotsDataFrame directly; an unrecognized position code raisesValueErrorat construction time instead of failing downstream incompute_vorp/Roster.
Fixed¶
random/random til medrafting far ahead of real-world ADP (#58): the candidate pool for simulated opponents was ranked top-VORP-first, with ADP only reweighting sampling within that already VORP-biased shortlist.random_picknow windows the candidate pool to players withinwindow_roundsof the current overall pick (mirroring thenearestview) before ranking by need-adjusted VORP, falling back to the old top-VORP pool whenpick_overall/num_teamsaren't supplied or the window is empty.
[0.7.0] — 2026-08-18¶
Multi-platform league support: SleeperClient and a fully synthetic generic mock mode join Yahoo behind a shared FantasyPlatformClient interface, with --platform now available across all three draft CLIs. Also includes dual (position + flex) VORP, simadd win/earnings-delta evaluation during live drafts, and a rebranded navy/green logo.
Added¶
SleeperClient, parallel toYahooFantasyClient(#37):data/sleeper_client.pygainsget_current_week,get_fantasy_teams,get_all_players,get_team_rosters, andget_scheduleagainst Sleeper's public read API (no OAuth).player_id_sris populated straight from Sleeper'sgsis_idwhere available, matching the IDNflreadpyProvideralready keys rosters/stats/depth-charts on, so it can join against nflreadpy output without a name-matching fallback. The full/players/nflmap is cached locally (~/.cache/fantasyfb) per Sleeper's integrator guidance against re-fetching it every run. Live smoke tests (tests/test_sleeper_client_smoke.py) hit the real public SFB16 league end-to-end; they self-skip rather than fail when Sleeper is unreachable, so sandboxed/offline environments and the defaultpytest -qstay green.FantasyPlatformClientabstraction wiresSleeperClientintoLeague(#37): a shared read-only interface bothYahooFantasyClientandSleeperClientimplement, soLeague(platform="yahoo"|"sleeper")can pick either backend at construction time instead of hardcoding Yahoo.--platform/--sleeper-league-idonsnake-draftandsalary-cap-draft: threadsLeague's platform selection through both cockpits so a live draft can be tracked against a Sleeper league, not just Yahoo. Manual pick-by-pick entry is unchanged for either platform since it isn't polling either API for picks — onlyLeagueconstruction differs.--fresh-draftonsnake-draftandsalary-cap-draft: clearsfantasy_teambefore the board snapshot, for mock-drafting against a league's settings/player-pool without the keeper-preservation logic (intended for resuming an in-progress Yahoo draft) treating that league's current real rosters as keepers. Needed for mock-drafting against a Sleeper league that has already completed its actual draft.- Generic (no Yahoo/Sleeper) mock draft mode (#47):
League(platform="generic"), backed by a newGenericClientimplementingFantasyPlatformClient, synthesizes teams/rosters/a round-robin regular-season schedule and pulls the real player pool (plus synthetic DEF rows) fromNflreadpyProvider, reusing Sleeper's public/state/nflendpoint for the current week.configs.pygainsSTANDARD_CONFIG/HALF_PPR_CONFIG/PPR_CONFIGpresets (identical exceptRecpoints) with a fixed roster (1 QB, 2 RB, 2 WR, 1 TE, 1 FLEX, 1 K, 1 DEF, 7 BN). Useful for sanity-checking VORP rankings against a familiar scoring baseline instead of an unusual one like SFB's, or for running any of the three draft CLIs with no live league credentials at all. Wired intosnake-draft,salary-cap-draft, anddraft-prepvia--platform generic --num-teams --mock-scoring;genericis now the default platform when--platformis omitted (previously Yahoo was implied). simaddcommand onsnake-draftandsalary-cap-draft(#39, #43): sims the top-N available candidates per position and ranks them by win/playoff/earnings delta from a cached baseline, wiring the existingMoveAnalyzerseason-sim engine into the live draft loop so users can evaluate a specific pick's impact during slow drafts without committing to it. Sim count is dropped to 1000 during per-pick evaluation and restored after (matching the original V1 script's approach); K/DEF are excluded from candidate pools since their sim delta is noise. Onsnake-draft,--bestball(bare defaults tounderdog;--bestball dkuses DraftKings settings) switchessim/simadd/the end-of-draft summary fromseason_simstobestball_sims. Onsalary-cap-draft, since adding a candidate costs cap space rather than being free, each candidate is priced at itsmax_my_bid(inflated market value clipped to the user's remaining budget) before the hypothetical roster add is simulated.best/simaddprompt for pool and scoring instead of separate commands (#40): replaces the earliernearest/bestball/nearestbestball/nearestsimadd/bestballsimaddcommands onsnake-draftwith interactive prompts — pool (best available vs. nearest ADP) and, outside--bestballdrafts, scoring (normal vs. bestball upside). Bestball drafts skip the scoring prompt since bestball scoring is mandatory in that mode. Prompts now spell out the literal choice words (e.g.(best/nearest) [best]) rather than relying on prose alone.simaddalso gains a per-run "players per position to simulate?" prompt (defaulting to--simadd-limit, still 3) so slow drafts can simulate more candidates and fast ones fewer without restarting with a different flag.- ADP blended into
random/random til me's auto-pick sampling weights: previously sampled purely by need-adjusted VORP, ignoring market ADP entirely, so a model-favorite with poor market consensus was weighted the same as a widely-agreed top prospect. Sampling weight is nowvorp_adjusted * (1/adp), so a player needs to look good by both signals to be picked often; the candidate pool itself (top-N by need-adjusted VORP) is unchanged. Players missing from the ADP source fall back to double the worst real ADP in the available pool, so they read as deep depth rather than tying for a top-ADP player. - Sample ADP CSV (
examples/) as a reference fordraft-prep's--adpflag, trimmed to the columnsload_adp_csvactually reads (Player,POS,Team,AVG) rather than a full FantasyPros export, to avoid redistributing proprietary per-source data. - Persistent nflreadpy caching for offline draft prep (#50):
NflreadpyProvidernow defaults nflreadpy's cache to filesystem mode (24h TTL) instead of its own in-memory default, which was wiped at the end of every process and therefore provided no benefit across the many separate CLI invocations a draft-prep session runs. Once a pull succeeds, subsequent pulls within the cache window reuse it without a network connection.draft-prep,snake-draft, andsalary-cap-draftgain a--refresh-cacheflag to force a fresh download for a single run; an explicitNFLREADPY_CACHEenv var (or a caller's ownnflreadpy.update_config()) still takes precedence over the new default. YahooFantasyClient.list_team_names(season): lists the user's fantasy team name(s) for a season without connecting to a league or picking one. Lets a caller (e.g. a UI) offer a picker beforeconnect_to_league/League(name=...)needs one — previously, an account with multiple teams for a season could only be resolved viaconnect_to_league's interactiveinput()prompt, which hangs in any non-interactive context (e.g. a Streamlit app).connect_to_leaguenow shares its league-lookup logic with the new method via a private_find_nfl_league_teamshelper instead of duplicating it, and now raises a clearValueErrorwhen no NFL league is found for the season instead of silently falling through with a stale/Noneleague id.
Changed¶
--platformflag added todraft-prep, matchingsnake-draft/salary-cap-draft(#53):_build_leaguepreviously calledfb.League(...)with noplatformkwarg, so it always fell through to the Yahoo default —draft-prephung indefinitely without live Yahoo credentials and had no way to run against Sleeper or a generic mock league. Ports the same--platform/--sleeper-league-id/--num-teams/--mock-scoringwiring already used bysnake-draftandsalary-cap-draft.League()'s single-source version now comes fromimportlib.metadatainstead of a hardcoded__version__string, sopyproject.tomlis the only place the version needs updating.- Rebranded logo: replaced the navy/gold snake-and-shield mark with a simpler navy/green "FFB51" football outline, cropped from its source photo and set as the new
assets/fantasyfb_logo.pngmaster (regenerateddocs/assets/fantasyfb_logo.pnganddocs/assets/favicon.pngviascripts/build-assets.py).docs/stylesheets/extra.css's Material palette now pulls navy#18304a/ green#79ac57from the new mark instead of the old navy#06152b/ gold#b29d76.
Removed¶
--sfbCLI flag (#48) fromsnake-draftanddraft-prep, along with theLeague(sfb=...)kwarg,configs.SFB_CONFIG, andconfigs.get_sfb_config_from_sleeper(). Now that--platform sleeper --sleeper-league-id <id>(added above) covers pulling a live Sleeper league's scoring/roster settings — including SFB leagues — throughSleeperClient.get_league_config(), the separate--sfboverlay mechanism was redundant. Breaking change toLeague()'s signature and both CLIs. Historical SFB13–16 configs were preserved outside the repo (a gitignorednotes/dir) for posterity, not in the codebase.PlayerDataManager.apply_name_corrections: fetched a legacyname_corrections.csvstraight fromraw.githubusercontent.comon everyLeagueconstruction, entirely outsideNflreadpyProvider's persistent caching, purely to patch stale player-name spellings from the pre-nflreadpy, Pro-Football-Reference-based name-matching era.map_player_idshas linked players byyahoo_id/gsis_idinstead of name for a while now, andsnake-draft/salary-cap-draft's V2 rewrites had already dropped the equivalent inline fetch on that same assumption — this was the one remaining caller still carrying the dead weight. Surfaced as an offline-draft-prep crash (see Fixed below) before being removed outright;process_players/League._process_playersno longer need thestatsargument that only this step consumed.
Fixed¶
PlayerDataManager.map_player_idsignoring Sleeper-resolved IDs: was entirely Yahoo-ID-shaped and ignored theplayer_id_srthat Sleeper already resolves viagsis_id, surfaced while live-testing the newSleeperClientintegration against a real Sleeper league.ScheduleManager._clean_scheduleteam/score swap could corruptscore_2: a pandas block-aliasing bug in the tuple-assignment swap, pre-existing and unrelated to the Sleeper work but caught along the way during the same live-testing pass.NflreadpyProvider.get_depth_chartsbreaking offline draft prep on a cold cache: unlikeget_schedule/get_rosters/get_player_stats, it always requested the current calendar year's depth-chart file directly instead of going through the season-clamping the others use, and had no fallback for a download failure — only for a successful-but-empty response. With no network and no cached depth-chart file yet, this raised a rawConnectionError/ValueErrorstraight out ofLeagueconstruction, defeating the persistent filesystem caching added above. It now degrades to an empty depth-chart frame in that case, same as the other providers' missing-season handling, so a second offline run completes rather than crashing.PlayerDataManager.add_injuriesbreaking offline draft prep: fetches a small CSV directly fromraw.githubusercontent.com, entirely outsideNflreadpyProvider's persistent caching, with no fallback for a dead network. A--refresh-cache-free, fully offline second run (real-world repro: nflreadpy's own cache warm, wifi off) crashed withurllib.error.URLErrorfrom inside this step. Now catchesOSError(covers bareurllib's andrequests' connection failures alike), warns, and skips the injury-timespan enrichment rather than raising — it's optional signal, not required for a draft-prep session to complete.connect_to_leagueconnecting to the wrong league when a season's teams span multiple leagues: Yahoo groups a user's teams for a season under one response entry even when they belong to entirely different leagues (e.g. two teams in the same season, two differentteam_keyleague-id segments). The multi-team branch derived the league id from an arbitrary team'steam_keyrather than the selected team's own, so picking the second team by name could silently connect you to the first team's league instead._find_nfl_league_teamsnow maps each team name to its own league id.- Dual VORP (position-specific + flex):
compute_vorpnow emits three additional columns —flex_replacement_rate,vorp_flex_per_game, andvorp_flex_season— alongside the existing position-only columns. The newcompute_flex_replacement_levelsfunction sets each position's replacement baseline against the combined pool of all players eligible for the deepest flex slot covering that position. In a superflex (Q/W/R/T) league this correctly compresses QB VORP to reflect that the superflex slot would otherwise be filled by a WR/RB/TE rather than another QB. All draft decision surfaces (vorp_adjustedin cockpit views, salary cap dollar values, mock draft pick strategies) now usevorp_flex_per_game/vorp_flex_seasonas their primary signal. For positions with no flex slot (K, DEF) and for QB in a non-superflex league, flex VORP equals position VORP so there is no behavioral change in those cases.
[0.6.0] — 2026-07-09¶
Sleeper-native scoring for SFB-style leagues (Third-Round Reversal, stacking yardage bonuses), V2 projection engine now the sole engine after winning the V1 bake-off (#24), and a new MkDocs documentation site (#21).
Added¶
- Docs site (#21): MkDocs Material site at https://tefirman.github.io/fantasyfb/. Covers install, Yahoo OAuth setup, an end-to-end first-weekly-report walkthrough, full CLI reference for all four entry points, an architecture overview, and auto-generated API reference via
mkdocstrings. Built and--strict-validated on every PR; deployed to GitHub Pages on push tomain. scripts/build-assets.py: regenerates the docs-site logo variants (web hero + favicon) from the high-res master atassets/fantasyfb_logo.png.- Sleeper scoring/roster settings:
data/sleeper_client.pypulls a league's scoring, roster spots, and Third-Round Reversal setting directly from Sleeper's public read-only API (no OAuth) instead of hand-transcribing them.configs.get_sfb_config_from_sleeper(league_id)andLeague(sfb=<sleeper_league_id>)both use it to pull current settings live. - Third-Round Reversal support:
drafts/tools.round_direction/MockDraft(reversal_round=...)anddrafts/snake.snake_pick_slot(..., reversal_round=...)can express TRR draft order (matching Sleeper's ownreversal_rounddraft setting) instead of assuming plain alternating snake. Exposed as--reversal-roundon bothsnake-draftanddraft-prep mock. - New yardage-bonus scoring categories:
FantasyScorernow supports stackingPass 300+/Pass 400+andRush 100+/200+/Rec 100+/200+tiers, plus a combinedRush+Rec 100+/200+bonus -- needed to accurately score SFB16's Sleeper-based ruleset. Per-play "long play" bonuses (40+ yard completions/rushes, 20-29/30-39/40+ yard receptions) are intentionally not modeled -- they require play-by-play data, not per-game box-score aggregates. --sfbflag onsnake-draftanddraft-prep(previously only reachable via aLeague()kwarg, not exposed on any CLI script).
Changed¶
League(fit_matchup=True)is now the default (#24): matchup weights are ridge-fitted via walk-forward least squares on the prior season instead of using the hand-tuned defaults. A 2024 full-season walk-forward backtest showedV2_fittedbeats the hand-tunedV2_defaulton QB/WR/K and overall RMSE.
Removed¶
- V1 projection engine (#24):
projections/engine.pydeleted after a walk-forward bake-off confirmed V2 beats V1 by 5.7% overall MAE on the 2024 season (4.317 vs 4.576), consistent across 2023 as well. Bake-off script preserved at commit894b6b4.
[0.5.0] — 2026-05-24¶
Best ball support across the draft and simulation stack (#31), and cost-plus-N keeper support across the salary cap V2 stack (#29).
Added¶
- Best ball season simulation (#32):
SeasonSimulator.simulate_season(best_ball=True)auto-fills optimal weekly lineups;select_optimal_lineupgreedily fills fixed slots then flex;compute_best_ball_team_projectionsMonte Carlos per-player projections into team-level(avg, stdev). - Best ball cockpit views (#32):
view_bestballandview_nearestbestballrank available players by upside-weighted VORP (points_rate + 0.5 × points_stdev − replacement_rate);bestball/nearestbestballwired into the snake draft pick loop. - Cost-plus-N keeper pricing (#33): new
--keeper-surchargeflag onsalary-cap-draft(default5); keeper price = last year's salary + surcharge. - Keepers in salary cap V2 stack (#33):
build_boardaccepts akeepersDataFrame and excludes keepers from the dollar pool (VORP still computed on full pool);MockSalaryCapDraftaccepts akeepersDataFrame to pre-apply keepers in mock drafts;backtest_salary_valuesgains akeeper_namesparameter to strip pre-negotiated picks from surplus / overpay calculations. - Per-team keeper budget validation (#33): teams whose keeper commitments exceed the cap have their keepers dropped with a warning before the draft starts. Resume-safe: keepers already present in an
--inprogressfile are skipped when--keepersis also passed.
Changed¶
- Draft cockpits default to real Yahoo team names instead of generic placeholders (#33).
- Keepers CSV
fantasy_teamcolumn accepts the user's actual Yahoo team name (#33).
Fixed¶
_simulate_playoffsreturnsNoneearly for unsupported bracket sizes (anything other than 4 or 6 playoff teams), covering DraftKings / Underdog best-ball formats with no traditional playoff bracket (#32).build_boardno longer raisesKeyError: 'fantasy_team'when called on a bare projection pool (#33).
[0.4.0] — 2026-05-21¶
Salary cap draft V2 (#11). Rebuilds salary-cap-draft on top of a tested valuation layer and cockpit views, with snake-parity ergonomics and a mock salary cap draft simulator.
Added¶
- Valuation primitives in
drafts.tools(#25):compute_salary_values(VORP-proportional, money-conserving) andmax_bid(budget-constraint helper). - Salary cap cockpit views (#26):
build_board,compute_inflation,view_best,view_nominate,view_what_if,view_lookup,view_roster,view_budget_status. - Salary cap CLI rewrite (#27): argparse-based
salary-cap-draftwith snake-parity commands (best,nominate,whatif,lookup,roster,budgets,exclude,sim,go back,help,exit), tab completion + readline history, and inflated-value /max_my_bidsurfaced in the player lookup output. - Mock salary cap draft simulator and backtest harness (#28):
MockSalaryCapDraft(Vickrey-style bidding withvalue/aggressive/conservativestrategies),backtest_salary_valuesfor V1-vs-V2 surplus comparison, andsimulate_nominationfor in-cockpit use. random/random til fullcommands insalary-cap-draftfor auto-piloting nominations and full-draft fills.- Resume support for legacy V1 draft progress CSVs —
--inprogressaccepts the oldsalarycolumn and renames it transparently.
Changed¶
- Standings sorted by earnings, then
wins_avg, thenpoints_avg(#23). move_analyzerrefactored to list-accumulate + single concat (#22).
Fixed¶
- pyarrow fallback for nflverse parquet files that polars rejects as invalid UTF-8; bad-byte string columns are sanitized before the Arrow → pandas conversion (#27, #30).
player_idcast tostrwhen filling theplayer_id_srfallback, fixing an arrow-string dtype crash inmap_player_ids(#30)._clean_scheduleteam/score swap uses tuple assignment to avoid dtype-coupled arrow-string crashes (#30).
Removed¶
- V1
best_comboscartesian-product optimizer (replaced byview_best). - V1
--starterpct/--limitknobs (replaced by need scaling inview_best). - V1
possible_addsMonte Carlo bench loop. - V1 inline
name_correctionsHTTP fetch. optparseusage insalary_cap.py(replaced byargparse).
[0.3.0] — 2026-05-11¶
First PyPI release.
Added¶
- Projection engine V2 with Vegas-backed matchup factors and walk-forward weight fitting.
draft-prepCLI for pre-draft tiers, VORP, ADP value, and mock-draft sims;trapssubcommand for overdrafted-player avoid lists.- Snake-draft cockpit V2 with VORP/tier/ADP board, tab completion, input history, and auto-pilot (
random,random til me) commands. --seasonflag for pre-draft runs targeting upcoming seasons.- Backtest harness comparing V1 vs V2 projections.
Changed¶
- Repository restructured as
src/fantasyfb/for PyPI packaging. - CLIs standardized on
--teamflag for team identifier. - Projection engine wired to V2 by default; V2 diagnostic columns preserved through
League.get_rates(). MatchupModel.apply_factorsis now idempotent across repeated calls.
Fixed¶
- Duplicate player rows in
League.get_rates()(#7). - Season sim no longer double-counts
runners_upfrom RangeIndex collision; completed-week scores are now locked. - Yahoo API calls clamped when
--weekis pastend_week. as_ofweek always treated as start-of-week in schedule.- Negative projections from MatchupModel factors clipped at zero.
- Stale references to ESPN and Pro Football Reference removed.
Removed¶
--emailoption fromsend-spreadsheet.- Many Mile feature from season simulator.