Core intelligence for BetonBLK prop analysis: - POST /api/analyze/prop — single prop analysis - POST /api/analyze/batch — multi-prop analysis for parlay scanner - 6-step pipeline: season avg → recent form → situational splits → cross-book lines → kill conditions → grade (A/B/C/D) - 6 kill conditions: low_minutes, small_sample, b2b_high_usage, blowout_risk, split_conflict, no_opponent_data - Composite scoring with confidence (30-95), bonuses, penalties - Added spreads market to Odds API fetch (zero extra credits) - Full reasoning output with step-by-step breakdown 36 new tests (unit + integration), 128 total across all features Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
12 KiB
Feature 1.3 — Prop Analysis Engine
Overview
The core intelligence of BetonBLK. Takes a player prop (player, stat, line, book) and runs a 6-step grading pipeline to produce a grade (A/B/C/D), edge percentage, confidence score, kill condition flags, and natural-language reasoning. This is the engine that powers the parlay scanner (Feature 2.1) and drives all user-facing analysis.
Dependencies
- Feature 1.1 — Odds API Integration (provides lines from multiple books)
- Feature 1.2 — NBA_API Stats Wrapper (provides season averages, last-N, splits)
Endpoint
POST /api/analyze/prop
Analyzes a single player prop.
Request body:
{
"player": "Nikola Jokic",
"stat_type": "points",
"line": 26.5,
"direction": "over",
"book": "draftkings"
}
Response (200):
{
"player": "Nikola Jokic",
"stat_type": "points",
"line": 26.5,
"direction": "over",
"book": "draftkings",
"grade": "A",
"edge_pct": 8.3,
"confidence": 82,
"kill_conditions_triggered": [],
"reasoning": {
"summary": "Jokic averages 26.3 on the season but 28.1 over his last 10. At home vs LAL where he averages 30.5, this line is soft. No kill conditions. Strong play.",
"steps": {
"season_avg": { "value": 26.3, "vs_line": -0.2, "signal": "neutral" },
"recent_form": { "value": 28.1, "vs_line": 1.6, "signal": "bullish" },
"situational": {
"home_away": { "value": 27.8, "context": "home", "signal": "bullish" },
"rest_days": { "value": 26.5, "context": "1_day_rest", "signal": "neutral" },
"vs_opponent": { "value": 30.5, "games": 3, "signal": "strong_bullish" }
},
"line_comparison": {
"best_line": { "book": "fanduel", "line": 27.0 },
"worst_line": { "book": "draftkings", "line": 26.5 },
"edge_from_best": 0.5,
"signal": "bullish"
},
"kill_conditions": [],
"final_grade": "A"
}
}
}
POST /api/analyze/batch
Analyzes multiple props at once (used by parlay scanner in Feature 2.1).
Request body:
{
"props": [
{ "player": "Nikola Jokic", "stat_type": "points", "line": 26.5, "direction": "over", "book": "draftkings" },
{ "player": "LeBron James", "stat_type": "rebounds", "line": 8.5, "direction": "over", "book": "fanduel" }
]
}
Response (200):
{
"results": [
{ "...same shape as single prop response..." },
{ "...same shape as single prop response..." }
]
}
Error Responses
| Status | When | Body |
|---|---|---|
| 400 | Missing required fields or invalid values | { "error": "player is required" } |
| 404 | Player not found in nba_api | { "error": "Player not found: Xyz" } |
| 503 | NBA stats service or Odds API unavailable | { "error": "Analysis service temporarily unavailable" } |
6-Step Grading Pipeline
Step 1: Season Average Compare
- Fetch season average for
player + stat_typefrom NBA stats service - Compare to the line:
delta = season_avg - line - For "over": positive delta = bullish. For "under": negative delta = bullish.
- Signal:
|delta| < 0.5= neutral,0.5-2.0= lean,2.0-4.0= bullish,>4.0= strong_bullish
Step 2: Recent Form (Last 10 Games)
- Fetch last 10 game average from NBA stats service
- Compare to line same as Step 1
- Weight recent form higher than season average (recent form = 1.5x weight)
- Signal: same thresholds as Step 1
Step 3: Situational Factors
Fetch splits from NBA stats service and compare each to the line:
3a. Home/Away
- Determine if today's game is home or away (from odds data: home_team/away_team)
- Fetch the relevant split average
- Signal based on delta from line
3b. Rest Days (Back-to-Back Detection)
- Fetch rest_days split from NBA stats service
- Determine current rest status (requires game schedule — use last game date from last-n data)
- B2B is a common kill condition for overs on high-minute players
3c. Vs Opponent
- Fetch vs_team split for today's opponent
- Small sample warning if < 3 games
- Signal based on delta from line
Step 4: Cross-Book Line Comparison
- Fetch lines from all 3 books for this player + stat_type (from Feature 1.1 data)
- Identify best and worst line
- If the requested book has a line 0.5+ points off the consensus, flag as edge or trap
- Signal: line is lower than consensus = bullish for over, bearish for under
Step 5: Kill Conditions
Hard flags that override or cap the grade. If any trigger, grade cannot exceed C.
| Kill Condition | Trigger | Description |
|---|---|---|
low_minutes |
Season avg minutes < 24 | Player doesn't play enough for volume stats |
small_sample |
Games played < 15 | Not enough data for reliable analysis |
b2b_high_usage |
B2B game + minutes > 32 | High-minute players often rest or play less on B2B |
blowout_risk |
Spread > 10 points | Garbage time / early bench risk for star players |
split_conflict |
Home/away split differs > 5 from recent form | Conflicting signals reduce confidence |
no_opponent_data |
vs_team games < 2 | Can't assess matchup-specific performance |
Step 6: Grade Assignment
Composite Score Calculation:
composite = (
season_delta * 1.0 +
recent_delta * 1.5 +
situational_avg_delta * 1.2 +
line_edge * 0.8
) / 4.5
Where each delta is: (avg - line) for overs, (line - avg) for unders.
situational_avg_delta = weighted average of home/away, rest, vs_opponent deltas.
line_edge = difference between requested line and best available line across books.
Grade thresholds:
| Grade | Composite Score | Confidence Range | Description |
|---|---|---|---|
| A | >= 3.0 | 80-95 | Strong edge. Clear play. |
| B | >= 1.5 | 65-79 | Solid lean. Playable. |
| C | >= 0.5 | 50-64 | Marginal. Proceed with caution. |
| D | < 0.5 | 30-49 | No edge or negative edge. Avoid. |
If any kill condition is triggered, grade is capped at C and confidence reduced by 15.
Confidence calculation:
confidence = base_confidence_from_grade + sample_bonus + consistency_bonus
sample_bonus: +5 if games_played > 50, +3 if > 30consistency_bonus: +5 if recent form and season avg agree in direction, -5 if they conflict
Service Architecture
src/
├── services/
│ ├── propAnalyzer.js # Main 6-step pipeline orchestrator
│ ├── grader.js # Composite score + grade assignment
│ ├── killConditions.js # Kill condition evaluation
│ └── nbaStatsClient.js # HTTP client for Feature 1.2 (FastAPI)
├── routes/
│ └── analyze.js # POST /api/analyze/prop, POST /api/analyze/batch
└── utils/
└── signals.js # Delta-to-signal conversion helpers
- propAnalyzer.js — Orchestrator. Calls odds service + NBA stats client, runs all 6 steps, returns analysis result.
- grader.js — Pure function. Takes step results, computes composite score, assigns grade + confidence.
- killConditions.js — Pure function. Takes stats + context, returns list of triggered kill conditions.
- nbaStatsClient.js — HTTP client wrapper for the Python FastAPI service (localhost:8000). Handles timeouts and errors.
- signals.js — Converts numeric deltas to signal strings (neutral/lean/bullish/strong_bullish/bearish).
- routes/analyze.js — Thin route layer. Validates input, calls propAnalyzer, formats response.
Data Flow
Request: { player, stat_type, line, direction, book }
│
├─→ Feature 1.1 (Odds API): Get lines from all books for this player+stat
│ Returns: lines[] with book, line, over_odds, under_odds
│
├─→ Feature 1.2 (NBA Stats): Get season average
│ Returns: { stats: { points: 26.3, ... } }
│
├─→ Feature 1.2 (NBA Stats): Get last 10 average
│ Returns: { stats: { points: 28.1, ... } }
│
├─→ Feature 1.2 (NBA Stats): Get home/away split
│ Returns: { splits: { home: { avg: 27.8 }, away: { avg: 24.9 } } }
│
├─→ Feature 1.2 (NBA Stats): Get rest days split
│ Returns: { splits: { b2b: { avg: 23.1 }, ... } }
│
└─→ Feature 1.2 (NBA Stats): Get vs opponent split
Returns: { splits: { vs_opponent: { avg: 30.5, games: 3 } } }
│
▼
propAnalyzer.js: Run 6-step pipeline
│
├─ Step 1: season_avg vs line → delta + signal
├─ Step 2: last_10_avg vs line → delta + signal (1.5x weight)
├─ Step 3: situational deltas → signals
├─ Step 4: cross-book line comparison → edge signal
├─ Step 5: kill conditions check → flags[]
└─ Step 6: composite score → grade + confidence
│
▼
Response: { grade, edge_pct, confidence, kill_conditions, reasoning }
Acceptance Criteria
POST /api/analyze/propaccepts a valid prop and returns a complete analysis with grade, edge_pct, confidence, kill_conditions, and reasoning- Grade is one of A, B, C, D based on composite score thresholds
- Edge percentage is calculated as
((relevant_avg - line) / line) * 100for overs (inverse for unders) - All 6 kill conditions are checked and correctly trigger when applicable
- If any kill condition triggers, grade is capped at C and confidence reduced
- Reasoning includes step-by-step breakdown with each factor's value and signal
- Cross-book line comparison identifies best/worst lines and flags edges
POST /api/analyze/batchprocesses multiple props and returns an array of results- Returns 400 for missing required fields (player, stat_type, line, direction)
- Returns 404 when player is not found in the NBA stats service
- Returns 503 when NBA stats service or Odds API is unreachable
- All data fetches use UTC timestamps
Test Plan
Unit Tests (grader.js)
- Composite score >= 3.0 returns grade A with confidence 80-95
- Composite score 1.5-2.99 returns grade B with confidence 65-79
- Composite score 0.5-1.49 returns grade C with confidence 50-64
- Composite score < 0.5 returns grade D with confidence 30-49
- Kill condition caps grade at C and reduces confidence by 15
- Sample bonus adds correctly (+5 for >50 games, +3 for >30)
- Consistency bonus: +5 when season + recent agree, -5 when they conflict
- Direction handling: "under" inverts delta signs
Unit Tests (killConditions.js)
low_minutes: triggers when avg minutes < 24small_sample: triggers when games_played < 15b2b_high_usage: triggers when B2B and minutes > 32blowout_risk: triggers when point spread > 10split_conflict: triggers when home/away vs recent differs > 5no_opponent_data: triggers when vs_team games < 2- Returns empty array when no conditions trigger
- Multiple conditions can trigger simultaneously
Unit Tests (signals.js)
- Delta 0.0-0.49 maps to "neutral"
- Delta 0.5-1.99 maps to "lean"
- Delta 2.0-3.99 maps to "bullish"
- Delta >= 4.0 maps to "strong_bullish"
- Negative deltas map to bearish equivalents
Unit Tests (propAnalyzer.js)
- Orchestrates all 6 steps in correct order
- Returns complete analysis shape with all required fields
- Handles missing splits gracefully (still produces a grade)
- Handles NBA stats service timeout (returns 503-appropriate error)
Integration Tests (routes/analyze.js)
- Full analysis: POST with valid prop → verify response shape and all fields present
- Grade A scenario: player averaging well above line, favorable splits
- Grade D scenario: player averaging below line, kill conditions triggered
- Kill condition scenario: B2B + high minutes → grade capped at C
- Batch analysis: POST with 3 props → array of 3 results
- Error: missing player field → 400
- Error: invalid stat_type → 400
- Error: NBA stats service down → 503
- Error: unknown player → 404
Open Questions
- Blowout risk data: Need game spread data. The Odds API provides h2h odds which can derive the spread. May need to add a spread fetch to Feature 1.1 or calculate from moneyline. Decide during implementation.
- Game schedule for B2B detection: The last-n response includes game dates. Can calculate days since last game from there. No additional API call needed.