Closes#21
All 14 async test methods in tests/test_automated_data_fetcher.py were
missing @pytest.mark.asyncio. Without it, pytest collects them and
silently passes without executing the coroutine body, providing no
coverage.
Added explicit @pytest.mark.asyncio to each async def test_* method.
This makes the async intent unambiguous and is robust against any
future asyncio_mode configuration changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Allow upload scripts to target a local API server instead of the remote
production server, enabling 32x+ concurrency for dramatically faster
full-cardset uploads (~30-45s vs ~2-3min for 800 cards).
- pd_cards/core/upload.py: add api_url param to upload_cards_to_s3(),
refresh_card_images(), and check_card_images()
- pd_cards/commands/upload.py: add --api-url CLI option to upload s3
- check_cards_and_upload.py: read PD_API_URL env var with prod fallback
- Update CLAUDE.md, CLI reference, and Phase 0 project plan docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
get_event_loop() is deprecated in Python 3.10+ when called inside
a running coroutine. get_running_loop() is the correct replacement.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pickoffs were using min(pick_cap, hold_num) which let high pickoff counts
completely override bad CS%, giving 31% of pitchers a -3 hold rating.
Now pickoffs act as a 1-3 point bonus on top of the CS%-based rating.
Pitchers with no CS data default to +2 (capped at -1 with pickoff bonus)
instead of the old +9.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents current branch state, what's built, what's left,
known bugs, and decision points so the next session can pick
up without re-investigating.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The FullCard migration requires offense_col and player_id on each player's
DataFrame row. The retrosheet pipeline calculates ratings before posting,
so both fields were missing — causing silent card layout builder failures.
Adds a three-tier resolution: CSV cache → API bulk fetch → deterministic
hash fallback. Also includes player_id fallback in both calcs modules.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix duplicate elif branch in HR overflow cascade that prevented
single_one from receiving excess chances, and reorder single_two
secondary dispatch to check flyout full_name before groundout
short_name to prevent false 'B' matches on fly ball results.
Also add missing new_ratings.flyout_cf_b increment.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The extracted batting and pitching models used malformed SLG equations that double-counted and omitted outcomes, skewing slash lines. Align formulas with canonical weighting and add regression tests to prevent recurrence.
Co-Authored-By: Claude GPT-5.3-Codex <noreply@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move each ratings model class (and, for batters, the helper functions it
depends on) into a dedicated models.py so that calcs_*.py can import from
card_builder.py at module level without circular imports.
- batters/models.py: BattingCardRatingsModel + bp_singles, wh_singles,
one_singles, bp_homeruns, triples, two_doubles, hit_by_pitch, strikeouts,
flyout_a, flyout_bq, flyout_b, groundball_a, groundball_c
- pitchers/models.py: PitchingCardRatingsModel (no helper deps needed)
- batters/calcs_batter.py: imports model + build_batter_full_cards at top
- pitchers/calcs_pitcher.py: imports model + build_pitcher_full_cards at top
- batters/card_builder.py: imports from batters.models
- pitchers/card_builder.py: imports from pitchers.models
- tests/test_batter_calcs.py: import bp_singles, wh_singles from batters.models
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- card_layout.py: Port PlayResult, PLAY_RESULTS, EXACT_CHANCES, get_chances(),
CardResult, CardColumn, FullCard, FullBattingCard, FullPitchingCard from
database/app/card_creation.py. card_output() uses col_* key names.
get_chances() always returns Decimal to avoid float/Decimal type errors.
- batters/card_builder.py: Port get_batter_card_data() algorithm as
build_batter_full_cards(ratings_vl, ratings_vr, offense_col, player_id, hand).
assign_bchances() returns float tuples for compatibility with float-based
BattingCardRatingsModel fields.
- pitchers/card_builder.py: Port get_pitcher_card_data() algorithm as
build_pitcher_full_cards(). assign_pchances() returns float tuples.
Includes card.add_fatigue() at end of each card iteration.
- batters/calcs_batter.py: Integrate card builder in get_batter_ratings().
After computing raw ratings, call build_batter_full_cards() and merge
9 col_* rendered column fields into each ratings dict. Lazy import to
avoid circular dependency.
- pitchers/calcs_pitcher.py: Same integration for get_pitcher_ratings().
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
790 players (397 batters, 393 pitchers) processed from Retrosheet data
through 2005-08-15 with 0.728 season percentage. Includes updated scouting
reports, card deltas, and FanGraphs scrape script.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents proposed architecture for moving card-building logic upstream
to Python, including:
- Executive summary with problem statement and migration path
- CardBuilder sketch with contract system for pluggable placement strategies
- Support for different card "personalities" (Standard, Clutch, Power Heavy, etc.)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add `pd-cards scouting upload` command to upload scouting CSVs to database server via SCP
- Update CLAUDE.md with critical warning: scouting must always run for ALL cardsets
- Document full workflow: `pd-cards scouting all && pd-cards scouting upload`
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add FRANCHISE_NORMALIZE dict and helper function
- Update FRANCHISE_LIST to return city-agnostic values
- Update mlbteam_and_franchise() to normalize franchise
Ensures new cards use normalized franchise format for AI roster matching
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Regenerated scouting CSVs for all cardsets (6211 batters, 7070 pitchers)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add --create/-c flag to create new players directly from YAML profiles
- Skip MLBPlayer creation (not needed for custom players)
- Auto-populate required API fields (cost, rarity, mlbclub, etc.)
- Update YAML profile with player_id and card_id after creation
- Add Adm Ball Traits custom player profile
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add --last-week-ratio, --last-twoweeks-ratio, --last-month-ratio flags
- Auto-enable 0.2 recency bias for last 2 weeks on Live series after May 30
- Fix main() call to pass empty args list (legacy parameter required)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Migrated all major card creation workflows to pd-cards CLI:
live-series:
- update: Full FanGraphs/BBRef card generation with CLI options
- status: Show cardset status from database
retrosheet:
- process: Historical Retrosheet data processing
- arms: Generate outfield arm ratings from play-by-play
- validate: Check for position anomalies in cardsets
- defense: Fetch defensive stats from Baseball Reference
scouting:
- batters: Generate batting scouting reports
- pitchers: Generate pitching scouting reports
- all: Generate all reports at once
upload:
- s3: Upload card images to AWS S3
- check: Validate cards without uploading
- refresh: Re-generate and re-upload card images
Updated CLAUDE.md with comprehensive CLI documentation.
Legacy scripts remain available but CLI is now the primary interface.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add pitcher detection via player_type field or ratings schema
- Separate calc_ops and verify_total for batters vs pitchers
- Pitcher template with correct schema (double_cf, xcheck fields, etc.)
- Combined OPS formula: max() for pitchers, min() for batters
- Add --type option to 'pd-cards custom new' command
- Migrate Tony Smehrik to YAML pitcher profile
Pitcher schema differences from batters:
- double_cf instead of double_pull
- flyout_cf_b instead of flyout_a/flyout_bq
- No groundout_c
- xcheck_* fields (29 chances for fielder plays)
- pitching block for starter/relief/closer ratings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Introduces new pd-cards CLI tool for all card creation workflows:
- custom: manage fictional character cards via YAML profiles
- live-series: live season card updates (stub)
- retrosheet: historical data processing (stub)
- scouting: scouting report generation (stub)
- upload: S3 card image upload (stub)
Key features:
- Typer-based CLI with auto-generated help and shell completion
- YAML profiles for custom characters (replaces per-character Python scripts)
- Preview, submit, new, and list commands for custom cards
- First character migrated: Kalin Young
Install with: uv pip install -e .
Run with: pd-cards --help
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Regenerated scouting CSVs with May PotM players (13287-13294)
- Reset retrosheet_data.py from May PotM back to Live series config
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Changed card URL generation to fetch from PD API endpoint
(/v2/players/{id}/battingcard) instead of existing S3 URL
- This ensures database changes (like cardpositions) are reflected
in regenerated card images
- Added fix_cardpositions.py utility for regenerating batter positions
without re-running full retrosheet_data.py script
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Increased target OPS from 0.820 to 0.850 with adjusted stat splits:
- vs RHP: .260/.340/.495 (power profile)
- vs LHP: .260/.375/.420 (patient/OBP profile)
- Cost updated from 85 to 188
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Regenerate scouting CSVs with latest player ratings
- Update archetype calculator with BP-HR whole number rule
- Refresh retrosheet normalized data
- Minor script updates for Kalin Young card creation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
CRITICAL BUG FIX: Removed code that was appending asterisks to left-handed
players' names and hash symbols to switch hitters' names in production.
## Changes
### Core Fix (retrosheet_data.py)
- Removed name_suffix code from new_player_payload() (lines 1103-1108)
- Players names now stored cleanly without visual indicators
- Affected 20 left-handed batters in 2005 Live cardset
### New Utility Scripts
- fix_player_names.py: PATCH player names to remove symbols (uses 'name' param)
- check_player_names.py: Verify all players for asterisks/hashes
- regenerate_lefty_cards.py: Update image URLs with cache-busting dates
- upload_lefty_cards_to_s3.py: Fetch fresh cards and upload to S3
### Documentation (CRITICAL - READ BEFORE WORKING WITH CARDS)
- docs/LESSONS_LEARNED_ASTERISK_REGRESSION.md: Comprehensive guide
* API parameter is 'name' NOT 'p_name'
* Card generation caching requires timestamp cache-busting
* S3 keys must not include query parameters
* Player names only in 'players' table
* Never append visual indicators to stored data
- CLAUDE.md: Added critical warnings section at top
## Key Learnings
1. API param for player name is 'name', not 'p_name'
2. Cards are cached - use timestamp in ?d= parameter
3. S3 keys != S3 URLs (no query params in keys)
4. Fix data BEFORE generating/uploading cards
5. Visual indicators belong in UI, not database
## Impact
- Fixed 20 player records in production
- Regenerated and uploaded 20 clean cards to S3
- Documented to prevent future regressions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Creates migrate_all_cards_to_s3.py to migrate historical card images from
Paper Dynasty API to S3 bucket. Key features:
- Processes all cardsets automatically (12,966 player cards across 29 cardsets)
- Detects and skips URLs already pointing to AWS S3
- Dry-run mode for previewing changes before execution
- Flexible filtering by cardset ID ranges or exclusion lists
- Per-cardset and global statistics tracking
- Updates player records with new S3 URLs after upload
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Changed from range(1, 28) to empty list [] to automatically include
all cardsets without future maintenance. This ensures new cardsets
(like cardset 29) are automatically included in scouting reports.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Two bugs were preventing switch hitters from being correctly identified:
1. Missing handedness indicator in player names
- Player names need special characters appended (* for left, # for switch)
- new_player_payload() now appends '#' for switch hitters
2. Overly strict threshold in get_bat_hand()
- Required 10+ total PAs to classify as switch hitter
- Now correctly identifies ANY player who batted from both sides as 'S'
- Removes arbitrary PA threshold that caused misclassification
Impact: Fixes Jimmie Rollins and Jorge Posada showing as 'R' instead of 'S'
Applies to all switch hitters in retrosheet-based cardsets
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
User requirement: Only 1 player with -6 arm, no more than 3 with -5 arm
2005 tz_runs_total data analysis:
- 23: Jim Edmonds (1 player)
- 21: Carl Crawford (1 player)
- 19: Coco Crisp, Brady Clark, Andruw Jones (3 players)
- 18: Cliff Floyd
- 17: Jason Michaels, Ichiro Suzuki (2 players)
Updated thresholds:
- > 22: -6 arm (Jim Edmonds only)
- > 19: -5 arm (Carl Crawford only, satisfies 'no more than 3')
- > 16: -4 arm (the three 19s plus 18s and 17s)
- Graduated scale for remaining tiers
Result: Elite arm ratings are now truly exceptional and rare
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The arm_outfield function had thresholds designed for bis_runs_outfield,
but retrosheet_data.py uses tz_runs_total (different scale).
Issue: 20 players had -6 arm (top rating) - should be exceptionally rare
Analysis of tz_runs_total distribution:
- Ranges from -8 to +23 (not -10 to +10)
- Old threshold: > 8 gave 20 players with -6 arm
- New threshold: > 18 gives ~2-3 players with -6 arm
Updated thresholds to properly map tz_runs_total values to arm ratings:
- > 18: -6 (exceptional, top 2-3 players like Andruw Jones)
- > 14: -5 (elite arms, ~5-8 players)
- > 10: -4 (very good)
- Graduated scale down to +2 for very poor arms
Result: -6 arms now truly exceptional, proper distribution across ratings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The post_positions function was being called twice (batters then pitchers).
Each call deleted ALL cardpositions, so the second call would delete the
batter positions that were just created.
Solution: Added delete_existing parameter (default False). Only the first
call (batters) sets delete_existing=True to clean up old data. The second
call (pitchers) just appends positions without deletion.
Result: Both batter and pitcher positions now persist correctly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>