From 05a034959cca450e900fc973532797e40d88aad5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 16:08:05 +0000 Subject: [PATCH] feat(sports): add Wimbledon tennis coverage (ATP + WTA match-winner) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires KXATPMATCH (ATP) and KXWTAMATCH (WTA) through the edge-detection pipeline as h2h game markets. No spread or total — tennis is match-winner only on Kalshi. New `wimbledon` and `tennis` filter shortcuts route to tennis_atp_wimbledon / tennis_wta_wimbledon Odds API keys. Extends extract_event_teams() with a tennis-specific "wins this match against" regex and adds sport-display wiring (KXATP/KXWTA → "Tennis") in ticker_display.py. CAVEAT: Kalshi API was egress-blocked in the cloud environment so ticker prefixes (KXATPMATCH/KXWTAMATCH) were not directly verified. Must confirm locally: python scripts/scan.py sports --filter wimbledon --top 30 +10 tests → 503 passing. --- CLAUDE.md | 2 +- docs/CHANGELOG.md | 51 +++++++++++++++++++ .../kalshi-sports-betting/SPORTS_GUIDE.md | 15 ++++++ scripts/kalshi/edge_detector.py | 22 ++++++++ scripts/shared/ticker_display.py | 4 +- tests/test_edge_detection.py | 50 ++++++++++++++++++ tests/test_ticker_display.py | 22 ++++++++ 7 files changed, 164 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bf4cd60..387cfa0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ Read relevant memory files before starting work to avoid re-learning prior conte | Domain | Coverage | Data Sources | |:-------|:---------|:-------------| -| **Sports Betting** | NBA, NHL, MLB, NFL, NCAA, MLS, World Cup, soccer, UFC, boxing, F1, NASCAR, PGA, IPL, esports (28 filters) | The Odds API, ESPN, NHL/MLB Stats, NWS | +| **Sports Betting** | NBA, NHL, MLB, NFL, NCAA, MLS, World Cup, soccer, UFC, boxing, F1, NASCAR, PGA, IPL, Wimbledon tennis, esports (30 filters) | The Odds API, ESPN, NHL/MLB Stats, NWS | | **Prediction Markets** | Crypto (BTC, ETH, XRP, DOGE, SOL), weather (13 cities), S&P 500 | CoinGecko, Yahoo Finance, NWS | | **Championship Futures** | NFL, NBA, NHL, MLB, PGA | Sportsbook futures odds | | **Execution Pipeline** | Unified scan → risk-check → size → execute | Kalshi API (RSA-signed) | diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 47b341a..98d43c7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,57 @@ --- +## 2026-06-28 -- Wimbledon Tennis Sport Coverage Added + +### Why + +Wimbledon 2026 starts June 29. The scanner had no tennis mapping, so Kalshi's +Wimbledon match-winner markets were invisible. Tennis was deferred from the +2026-06-20 World Cup release because markets weren't open yet (Kalshi API +confirmed 0 open markets on June 20 for all tested prefixes). + +### What landed + +- **Tennis wired as a new h2h-only sport** — match-winner (`game` category) only; + no spread or total markets on Kalshi for tennis. `KXATPMATCH` → `tennis_atp_wimbledon` + and `KXWTAMATCH` → `tennis_wta_wimbledon` added to `CATEGORY_MAP` and + `KALSHI_TO_ODDS_SPORT`. New `wimbledon` and `tennis` filter shortcuts. +- **Player-name extraction** — extended `extract_event_teams()` with a tennis-specific + regex for the "If [Player A] wins this match against [Player B]" rules_primary + pattern used by Kalshi tennis markets. All existing team-sport patterns unchanged. +- **Display wiring** — `KXATP`/`KXWTA` prefixes → sport label "Tennis" in + `ticker_display.py`. Player abbreviations in ticker suffixes pass through raw + (not in the US-sport team alias table, which is correct). +- **No edge-math changes** — tennis uses the existing de-vigged h2h moneyline + path (`detect_edge_game`). No spread/total stdev entries needed. + +### Ticker prefix caveat + +The Kalshi API was egress-blocked in the cloud environment during implementation, +so `KXATPMATCH`/`KXWTAMATCH` are best-guess. **Must be verified locally:** + +```bash +python scripts/scan.py sports --filter wimbledon --top 30 +``` + +If the real prefix differs (e.g. `KXATPGAME`, `KXWTAGAME`, `KXWIMMEN`), update +`CATEGORY_MAP`, `KALSHI_TO_ODDS_SPORT`, and `FILTER_SHORTCUTS` in +`scripts/kalshi/edge_detector.py`, plus `_SPORT_PREFIXES` in +`scripts/shared/ticker_display.py`. + +### Verification + ++10 tests (`TestTennisMappings` in `tests/test_edge_detection.py`; `TestTennisDisplay` +in `tests/test_ticker_display.py`) → **503 passing** (was 493). + +### Files + +`scripts/kalshi/edge_detector.py`, `scripts/shared/ticker_display.py`, +`tests/test_edge_detection.py`, `tests/test_ticker_display.py`, +`CLAUDE.md`, `docs/kalshi/kalshi-sports-betting/SPORTS_GUIDE.md`, `docs/CHANGELOG.md`. + +--- + ## 2026-06-24 -- C4: retire the base "high" confidence tier's composite-score premium ### Why diff --git a/docs/kalshi/kalshi-sports-betting/SPORTS_GUIDE.md b/docs/kalshi/kalshi-sports-betting/SPORTS_GUIDE.md index 1e12d28..352d93f 100644 --- a/docs/kalshi/kalshi-sports-betting/SPORTS_GUIDE.md +++ b/docs/kalshi/kalshi-sports-betting/SPORTS_GUIDE.md @@ -113,6 +113,17 @@ Use `--filter` to target a specific sport. Supports comma-separated values for m | `f1` | Formula 1 | Drivers + constructors championship | No (not on Odds API) | | `nascar` | NASCAR | Race winners | No (not on Odds API) | +### Tennis + +| Filter | Sport | Key Markets | Edge Detection | +|--------|-------|-------------|----------------| +| `wimbledon` | Wimbledon (ATP + WTA) | Match winner (h2h only) | Yes -- game | +| `tennis` | Wimbledon (ATP + WTA) | Match winner (h2h only) | Yes -- game | + +> **Tennis specifics:** Match-winner only (no spread or total Kalshi markets). ATP men's singles routes to `tennis_atp_wimbledon`; WTA women's singles routes to `tennis_wta_wimbledon` via the Odds API. Player names are extracted from `rules_primary` via the "wins this match against" pattern. Player abbreviations in the ticker suffix pass through as-is (they aren't in the US-sport team alias table). +> +> **Ticker prefix caveat:** The wiring uses `KXATPMATCH` (ATP) and `KXWTAMATCH` (WTA). Kalshi may instead use `KXATPGAME`/`KXWTAGAME`, `KXWIMMEN`, or other variants — verify locally by running `python scripts/scan.py sports --filter wimbledon --top 30`. If the prefix differs, update `CATEGORY_MAP`, `KALSHI_TO_ODDS_SPORT`, and `FILTER_SHORTCUTS` in `scripts/kalshi/edge_detector.py` and the `_SPORT_PREFIXES` map in `scripts/shared/ticker_display.py`. + ### Other Sports | Filter | Sport | Key Markets | Edge Detection | @@ -209,6 +220,10 @@ The system cross-references Kalshi prices against these Odds API sport keys: | KXBOXING | `boxing_boxing` | Moneyline (h2h) | | KXPGATOUR | `golf_{us_open,pga_championship,masters_tournament,the_open_championship}_winner` (outrights; resolved per-major by `futures_edge.py`) | Major tournament winner (4 majors only; weekly stops have no odds) | | KXIPL | `cricket_ipl` | Moneyline (h2h) | +| KXATPMATCH | `tennis_atp_wimbledon` | Moneyline (h2h) | +| KXWTAMATCH | `tennis_wta_wimbledon` | Moneyline (h2h) | + +> **Tennis prefix caveat:** Kalshi ticker prefixes for tennis were not directly verifiable (API egress-blocked in cloud). `KXATPMATCH`/`KXWTAMATCH` are best-guess — verify locally and update if needed. > **Soccer is 3-way.** Soccer h2h returns home/draw/away. The Kalshi "team to win?" market is binary, so fair value is the team's devigged **win** share and a draw resolves to the NO side. (Before the 2026-06-03 fix, 3-outcome markets were silently skipped, so soccer produced no edges.) diff --git a/scripts/kalshi/edge_detector.py b/scripts/kalshi/edge_detector.py index ee13d38..e7d25ca 100644 --- a/scripts/kalshi/edge_detector.py +++ b/scripts/kalshi/edge_detector.py @@ -87,6 +87,9 @@ "KXUFCFIGHT": "game", "KXBOXING": "game", "KXIPL": "game", + # --- Tennis (Wimbledon) --- + "KXATPMATCH": "game", + "KXWTAMATCH": "game", # --- Spread --- "KXNBASPREAD": "spread", "KXNHLSPREAD": "spread", @@ -173,6 +176,12 @@ # futures_edge.py using the `outrights` market type. # --- Cricket --- "KXIPL": "cricket_ipl", + # --- Tennis (Wimbledon) --- + # h2h match-winner only; no spread/total on Kalshi. + # NOTE: verify these prefixes locally — Kalshi API was egress-blocked during + # the initial implementation (2026-06-28). Also try: KXATPGAME, KXWTAGAME. + "KXATPMATCH": "tennis_atp_wimbledon", + "KXWTAMATCH": "tennis_wta_wimbledon", } @@ -1112,6 +1121,13 @@ def extract_event_teams(market: dict) -> tuple[str, str] | None: return _clean_team(match.group(1)), _clean_team(match.group(2)) # Fallback: simpler pattern match = re.search(r"in the (.+?) (?:vs\.?|at) (.+?) (?:game|match)", rules, re.IGNORECASE) + if match: + return _clean_team(match.group(1)), _clean_team(match.group(2)) + # Tennis h2h: "If [Player A] wins this match against [Player B]" + match = re.search( + r"If (.+?) wins this (?:[\w\s]*?)?match against (.+?)(?:\s+(?:at|in)\b|[,.]|$)", + rules, re.IGNORECASE, + ) if match: return _clean_team(match.group(1)), _clean_team(match.group(2)) return None @@ -1976,6 +1992,12 @@ def detect_edge_spread_analysis(market: dict) -> Opportunity | None: "pga": ["__FUTURES__golf-futures"], # --- Cricket --- "ipl": ["KXIPL"], + # --- Tennis (Wimbledon) --- + # Match-winner only (no spread/total). Both ATP and WTA singles. + # NOTE: verify KXATPMATCH/KXWTAMATCH against live Kalshi markets. + # Alternate prefixes to try: KXATPGAME, KXWTAGAME, KXWIMMEN, KXWMENSINGLES. + "wimbledon": ["KXATPMATCH", "KXWTAMATCH"], + "tennis": ["KXATPMATCH", "KXWTAMATCH"], # --- Esports --- "cs2": ["KXCS2MAP", "KXCS2GAME"], "lol": ["KXLOLMAP", "KXLOLGAME"], diff --git a/scripts/shared/ticker_display.py b/scripts/shared/ticker_display.py index a04d16c..7ab9c5d 100644 --- a/scripts/shared/ticker_display.py +++ b/scripts/shared/ticker_display.py @@ -89,6 +89,8 @@ def _resolve_team_abbr(abbr: str, ticker: str = "") -> str: "KXGOLF": "golf", "KXPGA": "golf", "KXNASCAR": "nascar", "KXIPL": "ipl", "KXESPORT": "esports", + "KXATPMATCH": "tennis", "KXATP": "tennis", + "KXWTAMATCH": "tennis", "KXWTA": "tennis", } @@ -105,7 +107,7 @@ def _detect_sport(ticker: str) -> str | None: "ncaab": "NCAAB", "ncaaf": "NCAAF", "soccer": "Soccer", "mls": "MLS", "worldcup": "World Cup", "ufc": "UFC", "boxing": "Boxing", "golf": "Golf", "nascar": "NASCAR", - "ipl": "IPL", "esports": "Esports", + "ipl": "IPL", "esports": "Esports", "tennis": "Tennis", } diff --git a/tests/test_edge_detection.py b/tests/test_edge_detection.py index fdcff9c..bf26cbe 100644 --- a/tests/test_edge_detection.py +++ b/tests/test_edge_detection.py @@ -1297,3 +1297,53 @@ def test_missing_last_update_excluded_on_live_game(self): assert _is_bookmaker_stale(bad_lu, live, 1200) is True assert _is_bookmaker_stale(no_lu, pregame, 1200) is False + + +class TestTennisMappings: + """Wimbledon tennis (KXATPMATCH / KXWTAMATCH) wiring — added 2026-06-28. + + Tennis is h2h match-winner only (no spread/total). ATP and WTA singles + route to their respective Odds API keys. These tests assert only the + prefix/category wiring; live odds-matching requires local verification + (Kalshi API was egress-blocked in the cloud environment at implementation + time — run: python scripts/scan.py sports --filter wimbledon --top 30). + + NOTE: if the live ticker prefix turns out to be KXATPGAME or KXWTAGAME + (not KXATPMATCH / KXWTAMATCH), update these constants and re-run the suite. + """ + + def test_atp_match_categorized_as_game(self): + assert CATEGORY_MAP["KXATPMATCH"] == "game" + + def test_wta_match_categorized_as_game(self): + assert CATEGORY_MAP["KXWTAMATCH"] == "game" + + def test_atp_odds_sport_key(self): + assert KALSHI_TO_ODDS_SPORT["KXATPMATCH"] == "tennis_atp_wimbledon" + + def test_wta_odds_sport_key(self): + assert KALSHI_TO_ODDS_SPORT["KXWTAMATCH"] == "tennis_wta_wimbledon" + + def test_tennis_extract_event_teams_against_pattern(self): + market = { + "ticker": "KXATPMATCH-26JUL01DJOKMED-DJOK", + "rules_primary": ( + "If Novak Djokovic wins this Wimbledon match against Carlos Alcaraz " + "in the men's singles tournament, this market resolves Yes." + ), + } + result = extract_event_teams(market) + assert result is not None + player_a, player_b = result + assert "Djokovic" in player_a + assert "Alcaraz" in player_b + + def test_tennis_extract_event_teams_simple_against(self): + market = { + "ticker": "KXWTAMATCH-26JUL01SWISWIA-SWI", + "rules_primary": "If Iga Swiatek wins this match against Elena Rybakina.", + } + result = extract_event_teams(market) + assert result is not None + assert "Swiatek" in result[0] + assert "Rybakina" in result[1] diff --git a/tests/test_ticker_display.py b/tests/test_ticker_display.py index 782336f..4bcdd66 100644 --- a/tests/test_ticker_display.py +++ b/tests/test_ticker_display.py @@ -294,3 +294,25 @@ def test_date_only_ticker_never_started(self): def test_unparseable_ticker_not_started(self): assert is_game_started("RANDOM-TICKER") is False + + +# ── Tennis (Wimbledon) ─────────────────────────────────────────────────────── + +class TestTennisDisplay: + """Sport detection and label for Wimbledon ATP/WTA tickers.""" + + def test_atp_ticker_sport_is_tennis(self): + from ticker_display import sport_from_ticker + assert sport_from_ticker("KXATPMATCH-26JUL01DJOKMED-DJOK") == "Tennis" + + def test_wta_ticker_sport_is_tennis(self): + from ticker_display import sport_from_ticker + assert sport_from_ticker("KXWTAMATCH-26JUL01SWIRYB-SWI") == "Tennis" + + def test_atp_player_abbr_returned_raw(self): + # Player abbreviations aren't in TEAM_NAMES, so parse_pick_team returns + # the raw suffix rather than mis-resolving it as a US team. + assert parse_pick_team("KXATPMATCH-26JUL01DJOKMED-DJOK") == "DJOK" + + def test_wta_player_abbr_returned_raw(self): + assert parse_pick_team("KXWTAMATCH-26JUL01SWIRYB-SWI") == "SWI"