Session 7j: Soccer intelligence - 9 leagues, 11 signals, 6 traps, poller, prefetch, 131 new tests (1173 total)
This commit is contained in:
@@ -30,57 +30,132 @@ function explainErrors(errors) {
|
||||
return errors.map((e) => ERROR_EXPLANATIONS[e] || `Data gap: ${e}.`).join(' ');
|
||||
}
|
||||
|
||||
// Soccer reasoning — different signals than NBA (xG, penalty role,
|
||||
// altitude, referee, minutes). Concrete sentences from real values;
|
||||
// nothing fires unless the underlying feature is non-null.
|
||||
function buildSoccerReasoningLines(features = {}, meta = {}, prop = {}) {
|
||||
const lines = [];
|
||||
const statType = prop.stat_type || '';
|
||||
|
||||
if (Number.isFinite(features.goals_per_90)) {
|
||||
lines.push(`${prop.player || 'Player'} scores ${features.goals_per_90.toFixed(2)} goals per 90 minutes.`);
|
||||
} else if (Number.isFinite(features.l5_avg)) {
|
||||
lines.push(`${prop.player || 'Player'} is averaging ${features.l5_avg.toFixed(2)} ${statType} over his last 5 matches.`);
|
||||
}
|
||||
|
||||
if (Number.isFinite(features.xg_per_90)) {
|
||||
const delta = features.xg_delta;
|
||||
let trend = 'tracking expectations';
|
||||
if (Number.isFinite(delta)) {
|
||||
if (delta > 0.2) trend = 'overperforming — regression risk';
|
||||
else if (delta < -0.2) trend = 'underperforming — breakout candidate';
|
||||
}
|
||||
lines.push(`Expected goals (xG): ${features.xg_per_90.toFixed(2)} per 90 — ${trend}.`);
|
||||
}
|
||||
|
||||
if (features.is_penalty_taker) {
|
||||
lines.push('Designated penalty taker — adds ~0.15 goals per 90 to base rate.');
|
||||
}
|
||||
if (features.takes_free_kicks && (statType === 'goals' || statType === 'shots' || statType === 'shots_on_target')) {
|
||||
lines.push('Direct free-kick specialist — boosts shot/goal probability on fouls drawn.');
|
||||
}
|
||||
if (features.takes_corners && statType === 'assists') {
|
||||
lines.push('Designated corner taker — meaningfully lifts assist probability.');
|
||||
}
|
||||
|
||||
if (features.altitude_impact === 'high') {
|
||||
lines.push(`Match at ${features.venue_altitude_ft || 'high'}ft altitude. ${features.home_continent ? 'Acclimated host team.' : 'Non-acclimatized side — historical goal reduction.'}`);
|
||||
} else if (features.altitude_impact === 'moderate' && !features.home_continent) {
|
||||
lines.push(`Moderate altitude at ${features.venue_altitude_ft || 'venue'}ft — minor stamina impact.`);
|
||||
}
|
||||
|
||||
if (Number.isFinite(features.referee_cards_per_game)) {
|
||||
const refName = features.referee_name || 'Referee';
|
||||
lines.push(`${refName} averages ${features.referee_cards_per_game.toFixed(1)} cards per match.`);
|
||||
}
|
||||
|
||||
if (Number.isFinite(features.minutes_per_game) && features.minutes_per_game < 75) {
|
||||
lines.push(`Averaging only ${features.minutes_per_game.toFixed(0)} minutes per match — line may assume full 90.`);
|
||||
}
|
||||
|
||||
if (Number.isFinite(features.opp_goals_conceded_per_game)) {
|
||||
lines.push(`${meta.opponentAbbr || 'Opponent'} concedes ${features.opp_goals_conceded_per_game.toFixed(2)} goals per game.`);
|
||||
}
|
||||
|
||||
if (features.tournament_player && Number.isFinite(features.wc_goals_career)) {
|
||||
lines.push(`Tournament pedigree: ${features.wc_goals_career} career World Cup goals.`);
|
||||
}
|
||||
|
||||
if (features.home_away === 1.0) lines.push('Playing at home.');
|
||||
else if (features.home_away === 0.0) lines.push('Playing on the road.');
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
// Build a human-readable reasoning summary + steps from the actual
|
||||
// features (which carry real numbers) and engine1's grade.
|
||||
function buildConcreteReasoning(features = {}, engine1Result = {}, meta = {}, prop = {}) {
|
||||
const lines = [];
|
||||
// Soccer (Session 7j) routes to a sport-specific line builder and
|
||||
// returns before the NBA-flavored sentences would fire. The closer
|
||||
// logic (trap, engine1 verdict, error gaps, steps shape) is shared
|
||||
// between sports and lives below this branch.
|
||||
const sportLc = String(meta.sport || '').toLowerCase();
|
||||
const isSoccer = sportLc === 'soccer' || sportLc === 'football';
|
||||
|
||||
// Recent form vs the line — L5 and L20 are the orchestrator's
|
||||
// canonical season-trend signals.
|
||||
if (Number.isFinite(features.l5_avg)) {
|
||||
lines.push(`${prop.player || 'Player'} is averaging ${features.l5_avg.toFixed(1)} ${prop.stat_type || ''} over his last 5 games.`);
|
||||
}
|
||||
if (Number.isFinite(features.l20_avg)) {
|
||||
lines.push(`Last 20 games average: ${features.l20_avg.toFixed(1)}.`);
|
||||
}
|
||||
const lines = isSoccer
|
||||
? buildSoccerReasoningLines(features, meta, prop)
|
||||
: [];
|
||||
|
||||
// Trend direction relative to the line.
|
||||
if (Number.isFinite(features.l5_avg) && Number.isFinite(prop.line)) {
|
||||
const diff = features.l5_avg - prop.line;
|
||||
if (Math.abs(diff) >= 0.5) {
|
||||
const dir = diff > 0 ? 'above' : 'below';
|
||||
lines.push(`That's ${Math.abs(diff).toFixed(1)} ${dir} the line of ${prop.line}.`);
|
||||
if (!isSoccer) {
|
||||
// Recent form vs the line — L5 and L20 are the orchestrator's
|
||||
// canonical season-trend signals.
|
||||
if (Number.isFinite(features.l5_avg)) {
|
||||
lines.push(`${prop.player || 'Player'} is averaging ${features.l5_avg.toFixed(1)} ${prop.stat_type || ''} over his last 5 games.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Home / away.
|
||||
if (features.home_away === 1.0) lines.push('Playing at home tonight.');
|
||||
else if (features.home_away === 0.0) lines.push('Playing on the road tonight.');
|
||||
|
||||
// Opponent matchup. opp_rank_stat is 0..1 normalized
|
||||
// (0 = best D, 1 = worst D) — translate to friendlier language.
|
||||
if (Number.isFinite(features.opp_rank_stat) && meta.opponentAbbr) {
|
||||
if (features.opp_rank_stat >= 0.7) {
|
||||
lines.push(`${meta.opponentAbbr} is a bottom-tier defense vs this stat.`);
|
||||
} else if (features.opp_rank_stat <= 0.3) {
|
||||
lines.push(`${meta.opponentAbbr} is a top-tier defense vs this stat.`);
|
||||
} else {
|
||||
lines.push(`${meta.opponentAbbr} is a middling defense vs this stat.`);
|
||||
if (Number.isFinite(features.l20_avg)) {
|
||||
lines.push(`Last 20 games average: ${features.l20_avg.toFixed(1)}.`);
|
||||
}
|
||||
|
||||
// Trend direction relative to the line.
|
||||
if (Number.isFinite(features.l5_avg) && Number.isFinite(prop.line)) {
|
||||
const diff = features.l5_avg - prop.line;
|
||||
if (Math.abs(diff) >= 0.5) {
|
||||
const dir = diff > 0 ? 'above' : 'below';
|
||||
lines.push(`That's ${Math.abs(diff).toFixed(1)} ${dir} the line of ${prop.line}.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Home / away.
|
||||
if (features.home_away === 1.0) lines.push('Playing at home tonight.');
|
||||
else if (features.home_away === 0.0) lines.push('Playing on the road tonight.');
|
||||
}
|
||||
|
||||
// Rest / fatigue context.
|
||||
if (features.rest_days === 0) lines.push('Back-to-back — fatigue concern.');
|
||||
else if (Number.isFinite(features.rest_days) && features.rest_days >= 2) {
|
||||
lines.push(`${features.rest_days} days of rest.`);
|
||||
}
|
||||
if (Number.isFinite(features.game_count_in_7d) && features.game_count_in_7d >= 4) {
|
||||
lines.push(`Heavy workload — ${features.game_count_in_7d} games in the last week.`);
|
||||
}
|
||||
if (!isSoccer) {
|
||||
// Opponent matchup. opp_rank_stat is 0..1 normalized
|
||||
// (0 = best D, 1 = worst D) — translate to friendlier language.
|
||||
if (Number.isFinite(features.opp_rank_stat) && meta.opponentAbbr) {
|
||||
if (features.opp_rank_stat >= 0.7) {
|
||||
lines.push(`${meta.opponentAbbr} is a bottom-tier defense vs this stat.`);
|
||||
} else if (features.opp_rank_stat <= 0.3) {
|
||||
lines.push(`${meta.opponentAbbr} is a top-tier defense vs this stat.`);
|
||||
} else {
|
||||
lines.push(`${meta.opponentAbbr} is a middling defense vs this stat.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Injury context.
|
||||
if (Number.isFinite(features.injury_severity_score) && features.injury_severity_score > 0) {
|
||||
lines.push(`${features.injury_severity_score} opponent starter(s) on the injury report.`);
|
||||
// Rest / fatigue context.
|
||||
if (features.rest_days === 0) lines.push('Back-to-back — fatigue concern.');
|
||||
else if (Number.isFinite(features.rest_days) && features.rest_days >= 2) {
|
||||
lines.push(`${features.rest_days} days of rest.`);
|
||||
}
|
||||
if (Number.isFinite(features.game_count_in_7d) && features.game_count_in_7d >= 4) {
|
||||
lines.push(`Heavy workload — ${features.game_count_in_7d} games in the last week.`);
|
||||
}
|
||||
|
||||
// Injury context.
|
||||
if (Number.isFinite(features.injury_severity_score) && features.injury_severity_score > 0) {
|
||||
lines.push(`${features.injury_severity_score} opponent starter(s) on the injury report.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Trap composite — surfaced when meaningful.
|
||||
|
||||
Reference in New Issue
Block a user