Files
builtbykev c8c0962e56 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>
2026-03-21 11:41:18 -04:00

112 lines
4.3 KiB
JavaScript

const { computeGrade } = require('../../src/services/grader');
function makeStepResults(overrides = {}) {
return {
seasonDelta: 0,
recentDelta: 0,
situationalDelta: 0,
lineEdge: 0,
killConditions: [],
gamesPlayed: 65,
seasonAndRecentAgree: null,
...overrides,
};
}
describe('grader', () => {
describe('grade assignment', () => {
test('composite >= 3.0 returns grade A with confidence 80-95', () => {
// composite = (4*1 + 4*1.5 + 4*1.2 + 4*0.8) / 4.5 = 18/4.5 = 4.0
const result = computeGrade(makeStepResults({
seasonDelta: 4, recentDelta: 4, situationalDelta: 4, lineEdge: 4,
}));
expect(result.grade).toBe('A');
expect(result.confidence).toBeGreaterThanOrEqual(80);
expect(result.confidence).toBeLessThanOrEqual(95);
});
test('composite 1.5-2.99 returns grade B with confidence 65-79', () => {
// composite = (2*1 + 2*1.5 + 2*1.2 + 2*0.8) / 4.5 = 9/4.5 = 2.0
const result = computeGrade(makeStepResults({
seasonDelta: 2, recentDelta: 2, situationalDelta: 2, lineEdge: 2,
}));
expect(result.grade).toBe('B');
expect(result.confidence).toBeGreaterThanOrEqual(65);
expect(result.confidence).toBeLessThanOrEqual(79);
});
test('composite 0.5-1.49 returns grade C with confidence 50-64', () => {
// composite = (1*1 + 1*1.5 + 1*1.2 + 0*0.8) / 4.5 = 3.7/4.5 ≈ 0.82
const result = computeGrade(makeStepResults({
seasonDelta: 1, recentDelta: 1, situationalDelta: 1, lineEdge: 0,
}));
expect(result.grade).toBe('C');
expect(result.confidence).toBeGreaterThanOrEqual(50);
expect(result.confidence).toBeLessThanOrEqual(64);
});
test('composite < 0.5 returns grade D with confidence 30-49', () => {
const result = computeGrade(makeStepResults({
seasonDelta: -1, recentDelta: -1, situationalDelta: -1, lineEdge: 0,
}));
expect(result.grade).toBe('D');
expect(result.confidence).toBeGreaterThanOrEqual(30);
expect(result.confidence).toBeLessThanOrEqual(49);
});
});
describe('kill condition penalty', () => {
test('caps grade at C and reduces confidence by 15', () => {
const result = computeGrade(makeStepResults({
seasonDelta: 4, recentDelta: 4, situationalDelta: 4, lineEdge: 4,
killConditions: [{ code: 'blowout_risk' }],
}));
expect(result.grade).toBe('C');
// Original would be A (80+), minus 15 = 65+
expect(result.confidence).toBeLessThan(85);
});
test('grade B with kill condition becomes C', () => {
const result = computeGrade(makeStepResults({
seasonDelta: 2, recentDelta: 2, situationalDelta: 2, lineEdge: 2,
killConditions: [{ code: 'low_minutes' }],
}));
expect(result.grade).toBe('C');
});
});
describe('bonuses', () => {
test('sample bonus +5 for > 50 games', () => {
const with50 = computeGrade(makeStepResults({ seasonDelta: 1, recentDelta: 1, gamesPlayed: 55 }));
const without = computeGrade(makeStepResults({ seasonDelta: 1, recentDelta: 1, gamesPlayed: 20 }));
expect(with50.confidence).toBe(without.confidence + 5);
});
test('sample bonus +3 for > 30 games', () => {
const with30 = computeGrade(makeStepResults({ seasonDelta: 1, recentDelta: 1, gamesPlayed: 35 }));
const without = computeGrade(makeStepResults({ seasonDelta: 1, recentDelta: 1, gamesPlayed: 20 }));
expect(with30.confidence).toBe(without.confidence + 3);
});
test('consistency bonus +5 when season and recent agree', () => {
const agree = computeGrade(makeStepResults({
seasonDelta: 2, recentDelta: 2, seasonAndRecentAgree: true,
}));
const noInfo = computeGrade(makeStepResults({
seasonDelta: 2, recentDelta: 2, seasonAndRecentAgree: null,
}));
expect(agree.confidence).toBe(noInfo.confidence + 5);
});
test('consistency penalty -5 when season and recent conflict', () => {
const conflict = computeGrade(makeStepResults({
seasonDelta: 2, recentDelta: 2, seasonAndRecentAgree: false,
}));
const noInfo = computeGrade(makeStepResults({
seasonDelta: 2, recentDelta: 2, seasonAndRecentAgree: null,
}));
expect(conflict.confidence).toBe(noInfo.confidence - 5);
});
});
});