Files
vyndr/specs/feature-1-3-prop-analysis.md

12 KiB

Feature 1.3 — Prop Analysis Engine

Overview

The core intelligence of VYNDR. 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_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.