feat: Feature 1.3 — Prop Analysis Engine with 6-step grading pipeline
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>
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
# 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:**
|
||||
```json
|
||||
{
|
||||
"player": "Nikola Jokic",
|
||||
"stat_type": "points",
|
||||
"line": 26.5,
|
||||
"direction": "over",
|
||||
"book": "draftkings"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"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:**
|
||||
```json
|
||||
{
|
||||
"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):**
|
||||
```json
|
||||
{
|
||||
"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_type` from 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 > 30
|
||||
- `consistency_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
|
||||
|
||||
1. `POST /api/analyze/prop` accepts a valid prop and returns a complete analysis with grade, edge_pct, confidence, kill_conditions, and reasoning
|
||||
2. Grade is one of A, B, C, D based on composite score thresholds
|
||||
3. Edge percentage is calculated as `((relevant_avg - line) / line) * 100` for overs (inverse for unders)
|
||||
4. All 6 kill conditions are checked and correctly trigger when applicable
|
||||
5. If any kill condition triggers, grade is capped at C and confidence reduced
|
||||
6. Reasoning includes step-by-step breakdown with each factor's value and signal
|
||||
7. Cross-book line comparison identifies best/worst lines and flags edges
|
||||
8. `POST /api/analyze/batch` processes multiple props and returns an array of results
|
||||
9. Returns 400 for missing required fields (player, stat_type, line, direction)
|
||||
10. Returns 404 when player is not found in the NBA stats service
|
||||
11. Returns 503 when NBA stats service or Odds API is unreachable
|
||||
12. 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 < 24
|
||||
- `small_sample`: triggers when games_played < 15
|
||||
- `b2b_high_usage`: triggers when B2B and minutes > 32
|
||||
- `blowout_risk`: triggers when point spread > 10
|
||||
- `split_conflict`: triggers when home/away vs recent differs > 5
|
||||
- `no_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.
|
||||
Reference in New Issue
Block a user