-
Notifications
You must be signed in to change notification settings - Fork 0
PR #586: Emergency — restore crash files, fix INSERT tuple, XGB scale, _get_props #457
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1 +1,214 @@ | ||||||||||||||||||||
| placeholder — will be overwritten by localPath | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| action_network_layer.py | ||||||||||||||||||||
| ======================= | ||||||||||||||||||||
| PropIQ — Action Network data layer. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Fetches MLB prop projections, game sentiment, and sharp money signals | ||||||||||||||||||||
| from Action Network's internal API (cookie-authenticated). | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Required env var: ACTION_NETWORK_COOKIE — raw token (no "Bearer" prefix). | ||||||||||||||||||||
|
|
||||||||||||||||||||
| PR #585: pitcher_walks key corrected to walks_allowed; | ||||||||||||||||||||
| implied-prob clamp widened to max(0.30, min(0.70, ...)) to allow | ||||||||||||||||||||
| real sharp flow beyond the old ±10pp cap. | ||||||||||||||||||||
| """ | ||||||||||||||||||||
|
|
||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import logging | ||||||||||||||||||||
| import os | ||||||||||||||||||||
| from typing import Optional | ||||||||||||||||||||
|
|
||||||||||||||||||||
| logger = logging.getLogger("propiq.action_network") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| _AN_COOKIE = os.environ.get("ACTION_NETWORK_COOKIE", "") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Market name → canonical prop_type | ||||||||||||||||||||
| _AN_MARKET: dict[str, str] = { | ||||||||||||||||||||
| "pitcher strikeouts": "strikeouts", | ||||||||||||||||||||
| "strikeouts": "strikeouts", | ||||||||||||||||||||
| "hits": "hits", | ||||||||||||||||||||
| "total bases": "total_bases", | ||||||||||||||||||||
| "total_bases": "total_bases", | ||||||||||||||||||||
| "hits runs rbis": "hits_runs_rbis", | ||||||||||||||||||||
| "hits + runs + rbis": "hits_runs_rbis", | ||||||||||||||||||||
| "h+r+rbi": "hits_runs_rbis", | ||||||||||||||||||||
| "batter strikeouts": "hitter_strikeouts", | ||||||||||||||||||||
| "hitter strikeouts": "hitter_strikeouts", | ||||||||||||||||||||
| "pitching outs": "pitching_outs", | ||||||||||||||||||||
| "outs": "pitching_outs", | ||||||||||||||||||||
| "hits allowed": "hits_allowed", | ||||||||||||||||||||
| "earned runs": "earned_runs", | ||||||||||||||||||||
| "walks allowed": "walks_allowed", | ||||||||||||||||||||
| "pitcher walks": "walks_allowed", # PR #585: was pitcher_walks (wrong) | ||||||||||||||||||||
| "walks": "walks_allowed", | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| _HEADERS: dict = { | ||||||||||||||||||||
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", | ||||||||||||||||||||
| "Accept": "application/json", | ||||||||||||||||||||
| "Origin": "https://www.actionnetwork.com", | ||||||||||||||||||||
| "Referer": "https://www.actionnetwork.com/", | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| def _auth_headers() -> dict: | ||||||||||||||||||||
| h = dict(_HEADERS) | ||||||||||||||||||||
| if _AN_COOKIE: | ||||||||||||||||||||
| h["x-auth-token"] = _AN_COOKIE | ||||||||||||||||||||
| return h | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| def fetch_mlb_prop_projections() -> list[dict]: | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| Fetch MLB player prop projections with over/under ticket % and money %. | ||||||||||||||||||||
| Returns list of dicts with player, prop_type, line, over_ticket_pct, etc. | ||||||||||||||||||||
| Returns [] on any failure. | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| if not _AN_COOKIE: | ||||||||||||||||||||
| logger.debug("[AN] No ACTION_NETWORK_COOKIE — prop projections skipped.") | ||||||||||||||||||||
| return [] | ||||||||||||||||||||
| try: | ||||||||||||||||||||
| return _fetch_prop_proj_rest() | ||||||||||||||||||||
| except Exception as exc: | ||||||||||||||||||||
| logger.warning("[AN] fetch_mlb_prop_projections failed: %s", exc) | ||||||||||||||||||||
| return [] | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| def _fetch_prop_proj_rest() -> list[dict]: | ||||||||||||||||||||
| """REST fallback via Action Network internal API.""" | ||||||||||||||||||||
| import requests # noqa: PLC0415 | ||||||||||||||||||||
| url = "https://api.actionnetwork.com/web/v2/games?league=mlb&market=player_props&bookIds=15,30" | ||||||||||||||||||||
| try: | ||||||||||||||||||||
| resp = requests.get(url, headers=_auth_headers(), timeout=12) | ||||||||||||||||||||
| if resp.status_code != 200: | ||||||||||||||||||||
| logger.debug("[AN] prop proj REST: HTTP %d", resp.status_code) | ||||||||||||||||||||
| return [] | ||||||||||||||||||||
| data = resp.json() | ||||||||||||||||||||
| except Exception as exc: | ||||||||||||||||||||
| logger.debug("[AN] prop proj REST request failed: %s", exc) | ||||||||||||||||||||
| return [] | ||||||||||||||||||||
|
|
||||||||||||||||||||
| results = [] | ||||||||||||||||||||
| for game in data.get("games", []) or []: | ||||||||||||||||||||
| for prop in game.get("player_props", []) or []: | ||||||||||||||||||||
| player = (prop.get("player_name") or prop.get("player") or "").strip() | ||||||||||||||||||||
| market = (prop.get("market_name") or prop.get("type") or "").lower().strip() | ||||||||||||||||||||
| prop_type = _AN_MARKET.get(market, market) | ||||||||||||||||||||
| line = prop.get("line") or prop.get("value") | ||||||||||||||||||||
| if not player or not prop_type: | ||||||||||||||||||||
| continue | ||||||||||||||||||||
| over_t = prop.get("over_ticket_pct", prop.get("over_bets_pct", 50)) | ||||||||||||||||||||
| under_t = prop.get("under_ticket_pct", prop.get("under_bets_pct", 50)) | ||||||||||||||||||||
| over_m = prop.get("over_money_pct", 50) | ||||||||||||||||||||
| under_m = prop.get("under_money_pct", 50) | ||||||||||||||||||||
| # PR #585: widened clamp from ±10pp to ±20pp around 50% | ||||||||||||||||||||
| _raw = float(over_m or over_t or 50) | ||||||||||||||||||||
| implied = max(0.30, min(0.70, _raw / 100.0)) | ||||||||||||||||||||
| results.append({ | ||||||||||||||||||||
| "player": player, | ||||||||||||||||||||
| "prop_type": prop_type, | ||||||||||||||||||||
| "line": float(line) if line is not None else None, | ||||||||||||||||||||
| "over_ticket_pct": float(over_t or 50), | ||||||||||||||||||||
| "under_ticket_pct": float(under_t or 50), | ||||||||||||||||||||
| "over_money_pct": float(over_m or 50), | ||||||||||||||||||||
| "under_money_pct": float(under_m or 50), | ||||||||||||||||||||
| "implied_prob": round(implied, 4), | ||||||||||||||||||||
| "rlm_signal": False, | ||||||||||||||||||||
| "rlm_direction": None, | ||||||||||||||||||||
| "source": "action_network", | ||||||||||||||||||||
| }) | ||||||||||||||||||||
| logger.debug("[AN] prop proj REST: %d props", len(results)) | ||||||||||||||||||||
| return results | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| def fetch_mlb_game_sentiment() -> dict: | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| Fetch per-team game sentiment (over/under ticket % and money %). | ||||||||||||||||||||
| Returns dict keyed by team_name → {over_ticket_pct, over_money_pct, ...}. | ||||||||||||||||||||
| Returns {} on any failure. | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| if not _AN_COOKIE: | ||||||||||||||||||||
| return {} | ||||||||||||||||||||
| try: | ||||||||||||||||||||
| import requests # noqa: PLC0415 | ||||||||||||||||||||
| resp = requests.get( | ||||||||||||||||||||
| "https://api.actionnetwork.com/web/v2/games?league=mlb", | ||||||||||||||||||||
| headers=_auth_headers(), timeout=10, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| if resp.status_code != 200: | ||||||||||||||||||||
| return {} | ||||||||||||||||||||
| data = resp.json() | ||||||||||||||||||||
| result: dict = {} | ||||||||||||||||||||
| for game in data.get("games", []) or []: | ||||||||||||||||||||
| for side in ("away", "home"): | ||||||||||||||||||||
| team = game.get(f"{side}_team", {}) | ||||||||||||||||||||
| name = team.get("abbr") or team.get("name") or "" | ||||||||||||||||||||
| if not name: | ||||||||||||||||||||
| continue | ||||||||||||||||||||
| result[name] = { | ||||||||||||||||||||
| "over_ticket_pct": game.get("over_bets_pct", 50), | ||||||||||||||||||||
| "over_money_pct": game.get("over_money_pct", 50), | ||||||||||||||||||||
| "under_ticket_pct": game.get("under_bets_pct", 50), | ||||||||||||||||||||
| "under_money_pct": game.get("under_money_pct",50), | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return result | ||||||||||||||||||||
| except Exception as exc: | ||||||||||||||||||||
| logger.warning("[AN] fetch_mlb_game_sentiment failed: %s", exc) | ||||||||||||||||||||
| return {} | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| def build_sharp_report() -> dict: | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| Build a sharp-money report dict: player → {sharp_side, rlm, steam}. | ||||||||||||||||||||
| Returns {} on failure. | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| try: | ||||||||||||||||||||
| props = fetch_mlb_prop_projections() | ||||||||||||||||||||
| report: dict = {} | ||||||||||||||||||||
| for p in props: | ||||||||||||||||||||
| player = p.get("player", "") | ||||||||||||||||||||
| pt = p.get("prop_type", "") | ||||||||||||||||||||
| if not player or not pt: | ||||||||||||||||||||
| continue | ||||||||||||||||||||
| over_t = float(p.get("over_ticket_pct", 50)) | ||||||||||||||||||||
| over_m = float(p.get("over_money_pct", 50)) | ||||||||||||||||||||
| # Sharp on Over when money% > ticket% by 10pp+ | ||||||||||||||||||||
| _sharp_over = (over_m - over_t) >= 10 | ||||||||||||||||||||
| _sharp_under = (over_t - over_m) >= 10 | ||||||||||||||||||||
| report[f"{player.lower()}:{pt}"] = { | ||||||||||||||||||||
| "sharp_side": "OVER" if _sharp_over else ("UNDER" if _sharp_under else None), | ||||||||||||||||||||
| "rlm": p.get("rlm_signal", False), | ||||||||||||||||||||
| "over_ticket_pct": over_t, | ||||||||||||||||||||
| "over_money_pct": over_m, | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return report | ||||||||||||||||||||
| except Exception as exc: | ||||||||||||||||||||
| logger.warning("[AN] build_sharp_report failed: %s", exc) | ||||||||||||||||||||
| return {} | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| def fetch_live_projections() -> list[dict]: | ||||||||||||||||||||
| """Alias for fetch_mlb_prop_projections — live projections from AN.""" | ||||||||||||||||||||
| return fetch_mlb_prop_projections() | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| def fetch_all_projections() -> dict: | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| Return projections split into batters/pitchers DataFrames-like structure. | ||||||||||||||||||||
| Used by DraftEdge iteration path. | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| try: | ||||||||||||||||||||
| import pandas as _pd # noqa: PLC0415 | ||||||||||||||||||||
| props = fetch_mlb_prop_projections() | ||||||||||||||||||||
| batters = [p for p in props if p.get("prop_type") in {"hits", "total_bases", "hits_runs_rbis", "hitter_strikeouts"}] | ||||||||||||||||||||
| pitchers = [p for p in props if p.get("prop_type") in {"strikeouts", "pitching_outs", "hits_allowed", "earned_runs", "walks_allowed"}] | ||||||||||||||||||||
| return { | ||||||||||||||||||||
| "batters": _pd.DataFrame(batters) if batters else _pd.DataFrame(), | ||||||||||||||||||||
| "pitchers": _pd.DataFrame(pitchers) if pitchers else _pd.DataFrame(), | ||||||||||||||||||||
| } | ||||||||||||||||||||
| except ImportError: | ||||||||||||||||||||
| return {"batters": [], "pitchers": []} | ||||||||||||||||||||
| except Exception as exc: | ||||||||||||||||||||
| logger.warning("[AN] fetch_all_projections failed: %s", exc) | ||||||||||||||||||||
| return {"batters": [], "pitchers": []} | ||||||||||||||||||||
|
Comment on lines
+211
to
+214
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error handling paths return empty lists
Suggested change
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1 +1,173 @@ | ||||||
| placeholder | ||||||
| """ | ||||||
| sportsbook_reference_layer.py | ||||||
| ============================= | ||||||
| PropIQ — Sportsbook sharp-line reference layer. | ||||||
|
|
||||||
| Fallback chain: | ||||||
| Tier 0 — DraftKings Internal API (6 prop types, no auth) | ||||||
| Tier 0.5 — FanDuel Internal API (5 pitcher types) | ||||||
| Tier 1 — The Odds API (Pinnacle+DK+FD+MGM) | ||||||
| Tier 1.5b — odds-api.net (bet365+betr) | ||||||
| Tier 4 — DraftEdge projections | ||||||
| Tier 5 — ActionNetwork money% | ||||||
| Tier 6 — TheRundown (all 9 MLB prop markets) | ||||||
| Tier 7 — VegasInsider scrape | ||||||
| Tier 8 — RotoWire supplement | ||||||
|
|
||||||
| PR #585: ActionNetwork always-run supplement + TheRundown all 9 markets. | ||||||
| """ | ||||||
|
|
||||||
| from __future__ import annotations | ||||||
|
|
||||||
| import logging | ||||||
| import os | ||||||
| from typing import Optional | ||||||
|
|
||||||
| logger = logging.getLogger("propiq.sportsbook_reference") | ||||||
|
|
||||||
|
|
||||||
| def build_sportsbook_reference() -> dict: | ||||||
| """ | ||||||
| Build sharp-line reference dict keyed by (player_name_lower, prop_type). | ||||||
| Returns dict with implied-prob values from sharp books. | ||||||
| Returns empty dict on any failure so callers degrade gracefully. | ||||||
| """ | ||||||
| try: | ||||||
| return _build() | ||||||
| except Exception as exc: | ||||||
| logger.warning("[SBRef] build_sportsbook_reference failed: %s", exc) | ||||||
| return {} | ||||||
|
|
||||||
|
|
||||||
| def _build() -> dict: | ||||||
| """Internal — assemble reference data from all available tiers.""" | ||||||
| _mem_ref: dict = {} | ||||||
|
|
||||||
| # ── Tier 0: DraftKings Internal API ───────────────────────────────────── | ||||||
| try: | ||||||
| from draftkings_layer import get_dk_sharp_prob as _dk_prob # noqa: PLC0415 | ||||||
| _dk_result = _dk_prob() | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
| if isinstance(_dk_result, dict): | ||||||
| for k, v in _dk_result.items(): | ||||||
| _mem_ref.setdefault(k, v) | ||||||
| logger.debug("[SBRef] Tier 0 DK: %d entries", len(_dk_result) if isinstance(_dk_result, dict) else 0) | ||||||
| except Exception as _e0: | ||||||
| logger.debug("[SBRef] Tier 0 DK failed: %s", _e0) | ||||||
|
|
||||||
| # ── Tier 0.5: FanDuel Internal API ────────────────────────────────────── | ||||||
| try: | ||||||
| from fanduel_layer import get_fd_sharp_prob as _fd_prob # noqa: PLC0415 | ||||||
| _fd_result = _fd_prob() | ||||||
| if isinstance(_fd_result, dict): | ||||||
| for k, v in _fd_result.items(): | ||||||
| _mem_ref.setdefault(k, v) | ||||||
| logger.debug("[SBRef] Tier 0.5 FD: %d entries", len(_fd_result) if isinstance(_fd_result, dict) else 0) | ||||||
| except Exception as _e05: | ||||||
| logger.debug("[SBRef] Tier 0.5 FD failed: %s", _e05) | ||||||
|
|
||||||
| # ── Tier 1: The Odds API ───────────────────────────────────────────────── | ||||||
| try: | ||||||
| import redis as _redis_mod # noqa: PLC0415 | ||||||
| import json as _json # noqa: PLC0415 | ||||||
| from datetime import datetime # noqa: PLC0415 | ||||||
| import pytz # noqa: PLC0415 | ||||||
| _r_url = os.environ.get("REDIS_URL") or os.environ.get("REDIS_PUBLIC_URL") | ||||||
| if _r_url: | ||||||
| _r = _redis_mod.from_url(_r_url, decode_responses=True) | ||||||
| _date = datetime.now(pytz.timezone("America/Los_Angeles")).strftime("%Y%m%d") | ||||||
| _cached = _r.get(f"sb_ref_{_date}") | ||||||
| if _cached: | ||||||
| _cached_dict = _json.loads(_cached) | ||||||
| for k, v in _cached_dict.items(): | ||||||
| _mem_ref.setdefault(k, v) | ||||||
| logger.debug("[SBRef] Tier 1 Redis cache: %d entries", len(_cached_dict)) | ||||||
| except Exception as _e1: | ||||||
| logger.debug("[SBRef] Tier 1 Redis read failed: %s", _e1) | ||||||
|
|
||||||
| # ── Tier 5 (always-run supplement): ActionNetwork ──────────────────────── | ||||||
| try: | ||||||
| from action_network_layer import ( # noqa: PLC0415 | ||||||
| fetch_mlb_prop_projections as _an_fetch, | ||||||
| ) | ||||||
| _an_props = _an_fetch() | ||||||
| _an_count = 0 | ||||||
| for p in (_an_props or []): | ||||||
| if not isinstance(p, dict): | ||||||
| continue | ||||||
| _player = (p.get("player") or "").strip().lower() | ||||||
| _pt = (p.get("prop_type") or "").strip().lower() | ||||||
| if not _player or not _pt: | ||||||
| continue | ||||||
| _over_pct = p.get("over_ticket_pct") or p.get("over_pct") | ||||||
| _money_pct = p.get("over_money_pct") | ||||||
| if _over_pct is None and _money_pct is None: | ||||||
| continue | ||||||
| _ref_pct = float(_over_pct or _money_pct or 50) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the issue in
Suggested change
|
||||||
| _implied = max(0.30, min(0.70, _ref_pct / 100.0)) | ||||||
| key = f"{_player}:{_pt}" | ||||||
| _mem_ref.setdefault(key, {"implied_prob": _implied, "source": "action_network"}) | ||||||
| _an_count += 1 | ||||||
| if _an_count: | ||||||
| logger.debug("[SBRef] Tier 5 AN supplement: %d entries", _an_count) | ||||||
| except Exception as _e5: | ||||||
| logger.debug("[SBRef] Tier 5 AN supplement failed: %s", _e5) | ||||||
|
|
||||||
| # ── Tier 6 (always-run supplement): TheRundown ─────────────────────────── | ||||||
| _RUNDOWN_MARKETS: dict[str, int] = { | ||||||
| "strikeouts": 19, | ||||||
| "hits": 53, | ||||||
| "total_bases": 54, | ||||||
| "hits_runs_rbis": 55, | ||||||
| "hitter_strikeouts": 56, | ||||||
| "pitching_outs": 57, | ||||||
| "hits_allowed": 58, | ||||||
| "earned_runs": 59, | ||||||
| "walks_allowed": 60, | ||||||
| } | ||||||
| _rundown_key = os.environ.get("RUNDOWN_API_KEY", "") | ||||||
| if _rundown_key: | ||||||
| try: | ||||||
| import requests as _req # noqa: PLC0415 | ||||||
| _added = 0 | ||||||
| for _prop_type, _market_id in _RUNDOWN_MARKETS.items(): | ||||||
| try: | ||||||
| _resp = _req.get( | ||||||
| f"https://therundown-therundown-v1.p.rapidapi.com/sports/3/odds/market/{_market_id}", | ||||||
| headers={ | ||||||
| "X-RapidAPI-Key": _rundown_key, | ||||||
| "X-RapidAPI-Host": "therundown-therundown-v1.p.rapidapi.com", | ||||||
| }, | ||||||
| timeout=8, | ||||||
| ) | ||||||
| if _resp.status_code != 200: | ||||||
| continue | ||||||
| data = _resp.json() | ||||||
| for event in data.get("events", []) or []: | ||||||
| for line in event.get("lines", {}).values(): | ||||||
| _pname = (line.get("player_name") or "").strip().lower() | ||||||
| if not _pname: | ||||||
| continue | ||||||
| _over_ml = line.get("over") | ||||||
| _under_ml = line.get("under") | ||||||
| if _over_ml is None: | ||||||
| continue | ||||||
| try: | ||||||
| _op = abs(_over_ml) | ||||||
| _over_dec = (100 / _op + 1) if _over_ml < 0 else (_op / 100 + 1) | ||||||
| _up = abs(_under_ml) if _under_ml else _op | ||||||
| _under_dec = (100 / _up + 1) if (_under_ml or 0) < 0 else (_up / 100 + 1) | ||||||
| _fair = (1 / _over_dec) / (1 / _over_dec + 1 / _under_dec) | ||||||
| except Exception: | ||||||
| _fair = 0.5 | ||||||
| _key = f"{_pname}:{_prop_type}" | ||||||
| if _key not in _mem_ref: | ||||||
| _mem_ref[_key] = {"implied_prob": round(_fair, 4), "source": "therundown"} | ||||||
| _added += 1 | ||||||
| except Exception: | ||||||
| continue | ||||||
| if _added: | ||||||
| logger.debug("[SBRef] Tier 6 TheRundown supplement: %d entries", _added) | ||||||
| except Exception as _e6: | ||||||
| logger.debug("[SBRef] Tier 6 TheRundown failed: %s", _e6) | ||||||
|
|
||||||
| return _mem_ref | ||||||
|
Comment on lines
+42
to
+173
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
This function must be memoized or the result should be cached in Redis/memory with a reasonable TTL (e.g., 5-10 minutes). |
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of the
oroperator for default values with numeric percentages (where0is a valid and significant value) will cause0to be incorrectly replaced by the fallback value (e.g.,50). In betting data, 0% money or tickets is a very strong signal that should not be normalized to 50%. This pattern is repeated forover_t,under_t,over_m, andunder_m.