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,86 @@
|
||||
const { evaluateKillConditions } = require('../../src/services/killConditions');
|
||||
|
||||
function makeContext(overrides = {}) {
|
||||
return {
|
||||
seasonStats: { minutes: 34, games_played: 65, points: 26 },
|
||||
recentStats: { value: 28 },
|
||||
homeAwaySplit: { avg: 27 },
|
||||
restSplit: { isB2B: false },
|
||||
vsOpponentSplit: { games: 3 },
|
||||
spread: -3,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('killConditions', () => {
|
||||
test('returns empty array when no conditions trigger', () => {
|
||||
const result = evaluateKillConditions(makeContext());
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test('low_minutes: triggers when avg minutes < 24', () => {
|
||||
const result = evaluateKillConditions(makeContext({
|
||||
seasonStats: { minutes: 22, games_played: 65 },
|
||||
}));
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].code).toBe('low_minutes');
|
||||
});
|
||||
|
||||
test('small_sample: triggers when games_played < 15', () => {
|
||||
const result = evaluateKillConditions(makeContext({
|
||||
seasonStats: { minutes: 34, games_played: 10 },
|
||||
}));
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].code).toBe('small_sample');
|
||||
});
|
||||
|
||||
test('b2b_high_usage: triggers when B2B and minutes > 32', () => {
|
||||
const result = evaluateKillConditions(makeContext({
|
||||
restSplit: { isB2B: true },
|
||||
seasonStats: { minutes: 35, games_played: 65 },
|
||||
}));
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].code).toBe('b2b_high_usage');
|
||||
});
|
||||
|
||||
test('blowout_risk: triggers when spread > 10', () => {
|
||||
const result = evaluateKillConditions(makeContext({ spread: -12 }));
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].code).toBe('blowout_risk');
|
||||
});
|
||||
|
||||
test('blowout_risk: triggers when spread > +10 too', () => {
|
||||
const result = evaluateKillConditions(makeContext({ spread: 11 }));
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].code).toBe('blowout_risk');
|
||||
});
|
||||
|
||||
test('split_conflict: triggers when home/away vs recent differs > 5', () => {
|
||||
const result = evaluateKillConditions(makeContext({
|
||||
homeAwaySplit: { avg: 20 },
|
||||
recentStats: { value: 28 },
|
||||
}));
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].code).toBe('split_conflict');
|
||||
});
|
||||
|
||||
test('no_opponent_data: triggers when vs_team games < 2', () => {
|
||||
const result = evaluateKillConditions(makeContext({
|
||||
vsOpponentSplit: { games: 1 },
|
||||
}));
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].code).toBe('no_opponent_data');
|
||||
});
|
||||
|
||||
test('multiple conditions can trigger simultaneously', () => {
|
||||
const result = evaluateKillConditions(makeContext({
|
||||
seasonStats: { minutes: 20, games_played: 10 },
|
||||
spread: -15,
|
||||
}));
|
||||
const codes = result.map((r) => r.code);
|
||||
expect(codes).toContain('low_minutes');
|
||||
expect(codes).toContain('small_sample');
|
||||
expect(codes).toContain('blowout_risk');
|
||||
expect(result.length).toBe(3);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user