Session 7j: Soccer intelligence - 9 leagues, 11 signals, 6 traps, poller, prefetch, 131 new tests (1173 total)

This commit is contained in:
Kev
2026-06-10 14:50:13 -04:00
parent b9084408bf
commit ad5ea8d5a8
28 changed files with 3175 additions and 49 deletions
+28 -2
View File
@@ -29,6 +29,9 @@ const featureCache = require('./featureCache');
const trapDetection = require('./trapDetection');
const consistencyScore = require('./consistencyScore');
const gameLogService = require('./gameLogService');
// Session 7j — soccer branch. The extractor reads from prefetched
// Redis cache; no external HTTP on the user request path.
const { extractSoccerFeatures, isSoccerSport } = require('./soccerFeatureExtractor');
const HTTP_TIMEOUT_MS = 8_000;
@@ -121,14 +124,37 @@ async function safeGetConsistency({ playerName, sport, statType }) {
}
async function computeFeaturesForProp(rawProp = {}) {
// Default to NBA when caller omits — matches what legacy analyzeProp does.
const sport = String(rawProp.sport || 'nba').toLowerCase();
// Soccer routes to a different extractor — different data sources
// (football-data.org + cache vs ESPN scoreboard), different feature
// set (xG, altitude, referee, set-piece role). The extractor returns
// the same {features, trap, consistency, prop, meta} shape engine1
// consumes, so analyzeViaEngine1 is sport-agnostic downstream.
if (isSoccerSport(sport)) {
const soccerResult = await extractSoccerFeatures(rawProp);
// Soccer extractor returns a placeholder trap object. Run the real
// soccer-branch trap detection here using the freshly computed
// features so analyzeViaEngine1 sees a populated trap composite.
const soccerTrap = await safeGetTrap({
sport: 'soccer',
playerName: rawProp.player,
statType: soccerResult.meta?.statType,
gameId: null,
gameContext: { home_away: soccerResult.features?.home_away === 1.0 ? 'home' : (soccerResult.features?.home_away === 0.0 ? 'away' : null) },
features: soccerResult.features,
odds: { playerLine: soccerResult.prop?.line, consensus: null },
});
return { ...soccerResult, trap: soccerTrap };
}
const errors = [];
const player = rawProp.player;
const statType = rawProp.stat_type || rawProp.statType;
const line = Number(rawProp.line);
const direction = rawProp.direction || 'over';
const book = rawProp.book || 'unknown';
// Default to NBA when caller omits — matches what legacy analyzeProp does.
const sport = (rawProp.sport || 'nba').toLowerCase();
if (!player || !statType || !Number.isFinite(line)) {
errors.push('missing required fields (player, stat_type, or line)');