Skip to content

Fix/agent activation pr250#251

Merged
jaayslaughter-cpu merged 4 commits into
mainfrom
fix/agent-activation-pr250
Apr 6, 2026
Merged

Fix/agent activation pr250#251
jaayslaughter-cpu merged 4 commits into
mainfrom
fix/agent-activation-pr250

Conversation

@jaayslaughter-cpu
Copy link
Copy Markdown
Owner

@jaayslaughter-cpu jaayslaughter-cpu commented Apr 6, 2026

Summary by cubic

Adds Action Network MLB public‑betting sentiment and a sharp line‑drift agent to improve signal quality; also blocks pure same‑team stacks so slips aren’t rejected by platforms.

  • New Features

    • New action_network_layer.py: fetches game‑level ticket%/money% from Action Network (no auth), daily‑cached; computes RLM (≥15pp) and direction; exposed as market.an_game_sentiment per team.
    • _SharpFadeAgent: uses AN game‑level RLM as a fallback when no player‑level sharp report exists; batter props only; scales by divergence; requires +1.5pp extra EV.
    • Replace _TimeValueAgent with _LineDriftAgent: fires when sb_implied_prob − platform implied ≥ 4pp; +1.5 EV bonus when sb_line_gap < −0.25; skips excluded props and cases with no sportsbook data.
  • Bug Fixes

    • _are_legs_correlated: blocks slips where all known legs are from the same team (e.g., LAD+LAD or LAD+LAD+LAD); mixed stacks (e.g., LAD+LAD+NYY) still allowed; duplicate player and same player+prop remain blocked.

Written for commit 4a65484. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Reverse Line Movement (RLM) detection for MLB games using Action Network data
    • Introduced same-team parlay stack blocking to prevent correlated bets
    • Launched drift-based bet evaluation replacing time-value analysis
  • Improvements

    • Enhanced market enrichment with game-level sentiment signals
    • Sharpened RLM signal strength computation for batter-prop decisions with stricter EV thresholds

…orrelated()

PrizePicks and Underdog both reject slips where every leg is on the same team.
Previously _are_legs_correlated() only blocked duplicate players — StackSmithAgent
(which groups teammates by pitching matchup) could produce pure LAD+LAD+LAD stacks
that would be voided on platform.

New Rule 3: if len(unique_teams) == 1 and any team appears 2+ times → blocked.
Mixed stacks (LAD+LAD+NYY) are still allowed — the cross-team leg satisfies both
platforms' correlated parlay policy.

7/7 test cases pass:
  LAD+LAD (2-leg)         → blocked ✅
  LAD+LAD+NYY (3-leg)     → allowed ✅
  STL+DET+STL (3-leg)     → allowed ✅
  LAD+LAD+LAD (3-leg)     → blocked ✅
  all-different teams      → allowed ✅
  duplicate player         → blocked ✅
  solo leg                 → allowed ✅

Applies to ALL agents (EVHunter, BullpenAgent, PropCycle, StackSmith, etc.)
…gnal)

- Removes _TimeValueAgent (duplicate of WeatherAgent — dome/temp boosts only, no actual drift)
- Adds _LineDriftAgent: compares sb_implied_prob (DK/FD/BetMGM vig-stripped, already
  stamped by sportsbook_reference_layer) vs platform implied from over_american
- Fires when drift >= 4 percentage points (sharp books ahead of UD/PP)
- Secondary signal: sb_line_gap < -0.25 (DFS line set easier than sportsbook) adds +1.5 EV bonus
- Skips props with no Odds API data (sb_implied_prob=0) — no fabricated signal
- Excludes stolen_bases, home_runs, walks, walks_allowed per prop exclusion directive
- Added to _AGENT_CLASSES (agent 15 of 15 active)
- 8/8 logic tests pass
… SharpFadeAgent game-level RLM fallback

- action_network_layer.py: fetches game-level ticket%/money% from Action Network (no auth required)
  - Endpoint: api.actionnetwork.com/web/v2/scoreboard/publicbetting/mlb
  - Returns per-team: over_ticket_pct, over_money_pct, rlm_signal, rlm_direction, game_total, opposing_team
  - RLM threshold: 15pp divergence between ticket% and money%
  - In-process daily cache — one fetch per PT calendar day
- tasklets.py: hub["market"]["an_game_sentiment"] populated each DataHub cycle
- _SharpFadeAgent: Path 1 = player-level sharp_report (existing); Path 2 = game-level AN RLM fallback
  - Game-level path: batter props only, requires +1.5pp extra EV vs player-level threshold
  - Signal strength scales 15pp (weak) → 35pp+ (strong)
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 6, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 19742137-947f-40ae-9c9f-efcb667b6c7a

📥 Commits

Reviewing files that changed from the base of the PR and between 8611f52 and 4553072.

📒 Files selected for processing (2)
  • action_network_layer.py
  • tasklets.py

📝 Walkthrough

Walkthrough

A new action_network_layer.py module fetches MLB game sentiment from Action Network's betting API, parsing Over/Under ticket and money percentages and computing RLM signals with per-day caching. The tasklets.py file integrates this sentiment data into market enrichment, updates parlay correlation rules to block pure same-team stacks, adds a game-level RLM decision path to the Sharp Fade Agent, and replaces the TimeValueAgent with a new LineDriftAgent for drift-based proposition evaluation.

Changes

Cohort / File(s) Summary
Action Network Integration
action_network_layer.py
New module that fetches MLB game sentiment from Action Network's publicbetting endpoint. Parses Over/Under tickets and money percentages, computes RLM signals (checking if abs(Over ticket% − Over money%) ≥ 15pp), assigns rlm_direction based on ticket/money comparison, and returns cached results keyed by PT date.
DataHub Market Enrichment
tasklets.py
Integrates Action Network sentiment into _is_pre_match() by importing and calling fetch_mlb_game_sentiment(), storing results in market["an_game_sentiment"] with fallback to {} on errors.
Parlay Correlation Rules
tasklets.py
Updates _are_legs_correlated() to block slips when all legs with known team belong to the same team and at least two legs are present (pure same-team stack rule), while retaining existing duplicate player and (player, prop_type) combo blocking.
Agent Logic Overhaul
tasklets.py
Removes _TimeValueAgent; adds _LineDriftAgent that gates props by sb_implied_prob > 0, computes drift from line gap, applies optional LINE_GAP_BONUS (1.5), and requires EV above threshold + 1.5pp. Extends _SharpFadeAgent.evaluate() with a second game-level RLM decision path using market["an_game_sentiment"] for batter props when player-level data is unavailable. Updates _AGENT_CLASSES registry.

Sequence Diagram

sequenceDiagram
    participant Client as Proposition Evaluator
    participant TaskLets as TaskLets Engine
    participant ANLayer as Action Network Layer
    participant ANApi as Action Network API
    participant Cache as Per-Day Cache
    participant SharpAgent as Sharp Fade Agent

    Client->>TaskLets: Request market pre-match enrichment
    TaskLets->>ANLayer: fetch_mlb_game_sentiment(date)
    ANLayer->>Cache: Check cached data for date
    alt Cache Hit
        Cache-->>ANLayer: Return cached sentiment
    else Cache Miss
        ANLayer->>ANApi: GET publicbetting endpoint (MLB scoreboard)
        ANApi-->>ANLayer: Event + market data (Over/Under tickets%, money%)
        ANLayer->>ANLayer: Parse OUs, compute RLM signals, build per-team entries
        ANLayer->>Cache: Store in cache by PT date
        Cache-->>ANLayer: Acknowledged
    end
    ANLayer-->>TaskLets: game_sentiment dict
    TaskLets->>TaskLets: Enrich market["an_game_sentiment"]
    TaskLets->>SharpAgent: Evaluate proposition
    alt Player-level sharp_report exists
        SharpAgent->>SharpAgent: Use player report (existing logic)
    else No player-level report
        SharpAgent->>SharpAgent: Use game-level RLM from an_game_sentiment
        SharpAgent->>SharpAgent: Apply batter-props RLM signal, compute divergence EV
    end
    SharpAgent-->>Client: Bet decision with strength/EV
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • #229: Modifies tasklets.py's DataHub pre-match and market enrichment logic, directly affected by the new Action Network sentiment integration added in this PR.

Poem

🐰 Action Networks bloom, sentiment now clear,
RLM signals hop through cache each day,
Drift agents replace old time-value cheer,
While same-team stacks now safely fade away,
Sharp fades leap forward, richer data in play! 🎰✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/agent-activation-pr250

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@deepsource-io
Copy link
Copy Markdown

deepsource-io Bot commented Apr 6, 2026

DeepSource Code Review

We reviewed changes in 8611f52...4553072 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
Docker Apr 6, 2026 2:12a.m. Review ↗
JavaScript Apr 6, 2026 2:12a.m. Review ↗
Python Apr 6, 2026 2:12a.m. Review ↗
SQL Apr 6, 2026 2:12a.m. Review ↗
Secrets Apr 6, 2026 2:12a.m. Review ↗

@jaayslaughter-cpu jaayslaughter-cpu merged commit 62d4161 into main Apr 6, 2026
2 of 4 checks passed
@codacy-production
Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

TIP This summary will be updated as you push new changes. Give us feedback

Comment thread action_network_layer.py
- Results are cached in-process per PT calendar day.
- Both home and away teams get an entry (same sentiment, different opposing_team).
"""
global _CACHE, _CACHE_DATE
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the global statement


It is recommended not to use global statement unless it is really necessary. Global variables are dangerous because they can be simultaneously accessed from multiple sections of a program. This frequently results in bugs. This also make code difficult to read, because they force you to search through multiple functions or even modules just to understand all the different locations where the global variable is used and modified. Read more about why it should be avoided here.

Comment thread action_network_layer.py
resp.raise_for_status()
data = resp.json()
except Exception as exc:
logger.warning(f"[ActionNetwork] fetch failed: {exc}")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use lazy % formatting in logging functions


Formatting the message manually before passing it to a logging call does unnecessary work if logging is disabled. Consider using the logging module's built-in formatting features to avoid that.

Comment thread action_network_layer.py
_CACHE_DATE = date_str

rlm_count = sum(1 for v in result.values() if v["rlm_signal"])
logger.info(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use lazy % formatting in logging functions


Formatting the message manually before passing it to a logging call does unnecessary work if logging is disabled. Consider using the logging module's built-in formatting features to avoid that.

Comment thread tasklets.py
from action_network_layer import fetch_mlb_game_sentiment
_an_sentiment = fetch_mlb_game_sentiment()
except Exception as _an_exc:
logger.warning(f"[DataHub] ActionNetwork sentiment unavailable: {_an_exc}")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use lazy % formatting in logging functions


Formatting the message manually before passing it to a logging call does unnecessary work if logging is disabled. Consider using the logging module's built-in formatting features to avoid that.

@secure-code-warrior-for-github
Copy link
Copy Markdown

Micro-Learning Topic: Cross-site scripting (Detected by phrase)

Matched on "XSS"

Cross-site scripting vulnerabilities occur when unescaped input is rendered into a page displayed to the user. When HTML or script is included in the input, it will be processed by a user's browser as HTML or script and can alter the appearance of the page or execute malicious scripts in their user context.

Try a challenge in Secure Code Warrior

Helpful references

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the action_network_layer.py module for fetching MLB public betting data and integrates it into the betting agents to provide a game-level Reverse Line Movement (RLM) signal. It also replaces the _TimeValueAgent with a new _LineDriftAgent that leverages sharp sportsbook data and updates parlay correlation logic to prevent pure same-team stacks. The review identified critical scale mismatches in the EV calculations for the new agents, a likely lookup failure due to team name formatting, and a logic bug where doubleheader data would overwrite previous games.

Comment thread tasklets.py
Comment on lines +3106 to +3111
implied = _american_to_implied(odds) * 100
prob_side = model_prob if sharp_side == "OVER" else (100.0 - model_prob)

# Apply a signal-strength discount — game-level signal is weaker than player-level
adjusted_prob = prob_side * (0.85 + 0.15 * signal_strength)
ev_pct = (adjusted_prob / 100 - _american_to_implied(odds)) / _american_to_implied(odds) * 100
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There is a critical scale mismatch in the EV calculation for the game-level RLM signal. _american_to_implied returns a percentage (0-100), but the formula at line 3111 incorrectly treats it as a fraction (0-1) in the numerator while keeping it as a percentage in the denominator. This will result in incorrect EV values and prevent the agent from firing correctly.

Suggested change
implied = _american_to_implied(odds) * 100
prob_side = model_prob if sharp_side == "OVER" else (100.0 - model_prob)
# Apply a signal-strength discount — game-level signal is weaker than player-level
adjusted_prob = prob_side * (0.85 + 0.15 * signal_strength)
ev_pct = (adjusted_prob / 100 - _american_to_implied(odds)) / _american_to_implied(odds) * 100
implied = _american_to_implied(odds)
prob_side = model_prob if sharp_side == "OVER" else (100.0 - model_prob)
# Apply a signal-strength discount — game-level signal is weaker than player-level
adjusted_prob = prob_side * (0.85 + 0.15 * signal_strength)
ev_pct = (adjusted_prob - implied) / implied * 100

Comment thread tasklets.py
Comment on lines +3161 to +3178
platform_implied: float = _american_to_implied(over_odds) # 0-1 fraction

# Core drift signal: how far ahead of the DFS platform are the sharp books?
drift: float = sharp_implied - platform_implied
if drift < self.DRIFT_MIN:
return None

# Line gap bonus: DFS line set easier than sharp consensus line
sb_line_gap: float = float(prop.get("sb_line_gap", 0.0) or 0.0)
line_gap_bonus: float = self.LINE_GAP_BONUS if sb_line_gap < -0.25 else 0.0

# Use sharp implied as the model probability — it IS the signal
model_prob = min(95.0, sharp_implied * 100)
implied_pct = platform_implied * 100
ev_pct = (
(model_prob / 100 - platform_implied) / platform_implied * 100
+ line_gap_bonus
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The _LineDriftAgent contains multiple scale mismatches. sharp_implied is a fraction (0-1), but platform_implied is a percentage (0-100) because it's derived directly from _american_to_implied. This causes the drift calculation at line 3164 to be incorrect (always negative). Additionally, the ev_pct calculation at line 3176 suffers from the same scale inconsistency between model_prob and platform_implied.

Suggested change
platform_implied: float = _american_to_implied(over_odds) # 0-1 fraction
# Core drift signal: how far ahead of the DFS platform are the sharp books?
drift: float = sharp_implied - platform_implied
if drift < self.DRIFT_MIN:
return None
# Line gap bonus: DFS line set easier than sharp consensus line
sb_line_gap: float = float(prop.get("sb_line_gap", 0.0) or 0.0)
line_gap_bonus: float = self.LINE_GAP_BONUS if sb_line_gap < -0.25 else 0.0
# Use sharp implied as the model probability — it IS the signal
model_prob = min(95.0, sharp_implied * 100)
implied_pct = platform_implied * 100
ev_pct = (
(model_prob / 100 - platform_implied) / platform_implied * 100
+ line_gap_bonus
)
platform_implied: float = _american_to_implied(over_odds) / 100 # 0-1 fraction
# Core drift signal: how far ahead of the DFS platform are the sharp books?
drift: float = sharp_implied - platform_implied
if drift < self.DRIFT_MIN:
return None
# Line gap bonus: DFS line set easier than sharp consensus line
sb_line_gap: float = float(prop.get("sb_line_gap", 0.0) or 0.0)
line_gap_bonus: float = self.LINE_GAP_BONUS if sb_line_gap < -0.25 else 0.0
# Use sharp implied as the model probability — it IS the signal
model_prob = min(95.0, sharp_implied * 100)
implied_pct = platform_implied * 100
ev_pct = (
(model_prob - implied_pct) / implied_pct * 100
+ line_gap_bonus
)

Comment thread tasklets.py
Comment on lines +3078 to +3079
team = prop.get("_team", prop.get("team", "")).lower()
game_ctx = an_sentiment.get(team, {})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The team lookup in an_sentiment will likely fail because prop.get("team") typically returns team abbreviations (e.g., "NYY"), whereas an_sentiment is keyed by full team names (e.g., "new york yankees"). You should use the _ABBREV_TO_FULL mapping to resolve the full name before performing the lookup.

Suggested change
team = prop.get("_team", prop.get("team", "")).lower()
game_ctx = an_sentiment.get(team, {})
team_abbr = prop.get("_team", prop.get("team", "")).upper()
team_full = _ABBREV_TO_FULL.get(team_abbr, team_abbr).lower()
game_ctx = an_sentiment.get(team_full, {})

Comment thread action_network_layer.py
Comment on lines +195 to +196
result[home] = {**sentiment, "opposing_team": away}
result[away] = {**sentiment, "opposing_team": home}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation uses team names as keys in the result dictionary. In the event of an MLB doubleheader, the data for the second game will overwrite the first game for both teams involved. Consider using a unique game identifier or storing a list of game sentiments per team if doubleheaders need to be supported.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant