Initial commit
This commit is contained in:
139
sql/match_prediction_openai.sql
Normal file
139
sql/match_prediction_openai.sql
Normal file
@@ -0,0 +1,139 @@
|
||||
-- =====================================================================
|
||||
-- OpenAI match predictions + comparison views
|
||||
-- Run against the "bookie" schema (Postgres).
|
||||
-- Safe to run multiple times (IF NOT EXISTS / CREATE OR REPLACE).
|
||||
-- =====================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 1) Table: one row PER TEAM PER EXECUTION of the OpenAI predictor.
|
||||
-- History is kept (no unique constraint) so you can inspect how
|
||||
-- predictions change across runs; the views below always pick the
|
||||
-- most recent row per (match, team).
|
||||
-- ---------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS bookie.match_prediction_openai (
|
||||
prediction_id SERIAL PRIMARY KEY,
|
||||
match_id INT NOT NULL REFERENCES bookie.match(match_id),
|
||||
team_id INT NOT NULL REFERENCES bookie.team(team_id),
|
||||
|
||||
predicted_goals NUMERIC(6,2),
|
||||
goals_low NUMERIC(6,2),
|
||||
goals_high NUMERIC(6,2),
|
||||
|
||||
predicted_shots_on_target NUMERIC(6,2),
|
||||
shots_on_target_low NUMERIC(6,2),
|
||||
shots_on_target_high NUMERIC(6,2),
|
||||
|
||||
predicted_corners NUMERIC(6,2),
|
||||
corners_low NUMERIC(6,2),
|
||||
corners_high NUMERIC(6,2),
|
||||
|
||||
predicted_fouls NUMERIC(6,2),
|
||||
fouls_low NUMERIC(6,2),
|
||||
fouls_high NUMERIC(6,2),
|
||||
|
||||
predicted_yellow_cards NUMERIC(6,2),
|
||||
yellow_cards_low NUMERIC(6,2),
|
||||
yellow_cards_high NUMERIC(6,2),
|
||||
|
||||
predicted_red_cards NUMERIC(6,2),
|
||||
red_cards_low NUMERIC(6,2),
|
||||
red_cards_high NUMERIC(6,2),
|
||||
|
||||
confidence TEXT,
|
||||
reasoning TEXT,
|
||||
model TEXT,
|
||||
predicted_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_match_prediction_openai_match_team
|
||||
ON bookie.match_prediction_openai (match_id, team_id, predicted_at DESC);
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 2) Latest OpenAI prediction per (match, team).
|
||||
-- ---------------------------------------------------------------------
|
||||
CREATE OR REPLACE VIEW bookie.v_match_prediction_openai_latest AS
|
||||
SELECT DISTINCT ON (match_id, team_id) *
|
||||
FROM bookie.match_prediction_openai
|
||||
ORDER BY match_id, team_id, predicted_at DESC;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 3) Side-by-side comparison: LLM (bookie.match_prediction) vs OpenAI
|
||||
-- vs ACTUAL, one row per (match, team). Actual goals come from the
|
||||
-- final score; the other actuals come from bookie.match_team_stats.
|
||||
-- ---------------------------------------------------------------------
|
||||
CREATE OR REPLACE VIEW bookie.v_prediction_comparison AS
|
||||
SELECT
|
||||
m.match_id,
|
||||
t.team_id,
|
||||
t.name AS team_name,
|
||||
m.kickoff_at,
|
||||
m.status,
|
||||
(m.home_team_id = t.team_id) AS is_home,
|
||||
opp.name AS opponent_name,
|
||||
|
||||
-- goals
|
||||
lp.predicted_goals AS llm_goals,
|
||||
op.predicted_goals AS openai_goals,
|
||||
CASE WHEN m.home_team_id = t.team_id THEN m.home_score_ft ELSE m.away_score_ft END AS actual_goals,
|
||||
|
||||
-- shots on target
|
||||
lp.predicted_shots_on_target AS llm_shots_on_target,
|
||||
op.predicted_shots_on_target AS openai_shots_on_target,
|
||||
mts.shots_on_target AS actual_shots_on_target,
|
||||
|
||||
-- corners
|
||||
lp.predicted_corners AS llm_corners,
|
||||
op.predicted_corners AS openai_corners,
|
||||
mts.corners AS actual_corners,
|
||||
|
||||
-- fouls
|
||||
lp.predicted_fouls AS llm_fouls,
|
||||
op.predicted_fouls AS openai_fouls,
|
||||
mts.fouls AS actual_fouls,
|
||||
|
||||
-- yellow cards
|
||||
lp.predicted_yellow_cards AS llm_yellow_cards,
|
||||
op.predicted_yellow_cards AS openai_yellow_cards,
|
||||
mts.yellow_cards AS actual_yellow_cards,
|
||||
|
||||
op.confidence AS openai_confidence,
|
||||
op.model AS openai_model
|
||||
FROM bookie.match m
|
||||
JOIN bookie.team t
|
||||
ON t.team_id IN (m.home_team_id, m.away_team_id)
|
||||
JOIN bookie.team opp
|
||||
ON opp.team_id = CASE WHEN m.home_team_id = t.team_id THEN m.away_team_id ELSE m.home_team_id END
|
||||
LEFT JOIN bookie.match_prediction lp
|
||||
ON lp.match_id = m.match_id AND lp.team_id = t.team_id
|
||||
LEFT JOIN bookie.v_match_prediction_openai_latest op
|
||||
ON op.match_id = m.match_id AND op.team_id = t.team_id
|
||||
LEFT JOIN bookie.match_team_stats mts
|
||||
ON mts.match_id = m.match_id AND mts.team_id = t.team_id
|
||||
WHERE lp.prediction_id IS NOT NULL OR op.prediction_id IS NOT NULL;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 4) Accuracy summary: mean absolute error of each model vs actual,
|
||||
-- per metric, over finished matches only. Lower = better.
|
||||
-- ---------------------------------------------------------------------
|
||||
CREATE OR REPLACE VIEW bookie.v_prediction_accuracy AS
|
||||
WITH c AS (
|
||||
SELECT * FROM bookie.v_prediction_comparison WHERE status = 'finished'
|
||||
)
|
||||
SELECT metric,
|
||||
ROUND(AVG(ABS(llm_pred - actual)), 3) AS llm_mae,
|
||||
ROUND(AVG(ABS(openai_pred - actual)), 3) AS openai_mae,
|
||||
COUNT(*) FILTER (WHERE llm_pred IS NOT NULL AND actual IS NOT NULL) AS llm_samples,
|
||||
COUNT(*) FILTER (WHERE openai_pred IS NOT NULL AND actual IS NOT NULL) AS openai_samples
|
||||
FROM (
|
||||
SELECT 'goals' AS metric, llm_goals AS llm_pred, openai_goals AS openai_pred, actual_goals AS actual FROM c
|
||||
UNION ALL
|
||||
SELECT 'shots_on_target', llm_shots_on_target, openai_shots_on_target, actual_shots_on_target FROM c
|
||||
UNION ALL
|
||||
SELECT 'corners', llm_corners, openai_corners, actual_corners FROM c
|
||||
UNION ALL
|
||||
SELECT 'fouls', llm_fouls, openai_fouls, actual_fouls FROM c
|
||||
UNION ALL
|
||||
SELECT 'yellow_cards', llm_yellow_cards, openai_yellow_cards, actual_yellow_cards FROM c
|
||||
) x
|
||||
GROUP BY metric
|
||||
ORDER BY metric;
|
||||
227
sql/model_and_odds_views.sql
Normal file
227
sql/model_and_odds_views.sql
Normal file
@@ -0,0 +1,227 @@
|
||||
-- =====================================================================
|
||||
-- Inspection views for Dixon-Coles model + betting odds tables (bookie)
|
||||
-- Assumes tables already exist. Safe to re-run (CREATE OR REPLACE).
|
||||
--
|
||||
-- psql -U <user> -d <db> -f sql/model_and_odds_views.sql
|
||||
-- =====================================================================
|
||||
|
||||
-- Predictions with match & team context
|
||||
CREATE OR REPLACE VIEW bookie.v_match_prediction_detail AS
|
||||
SELECT
|
||||
mp.prediction_id,
|
||||
mp.match_id,
|
||||
m.kickoff_at,
|
||||
m.status,
|
||||
ht.name AS home_team,
|
||||
at.name AS away_team,
|
||||
l.name AS league,
|
||||
l.country,
|
||||
t.name AS team_name,
|
||||
(m.home_team_id = t.team_id) AS is_home,
|
||||
mp.predicted_goals,
|
||||
mp.predicted_shots_total,
|
||||
mp.predicted_shots_on_target,
|
||||
mp.predicted_corners,
|
||||
mp.predicted_fouls,
|
||||
mp.predicted_yellow_cards,
|
||||
mp.model_trained_at,
|
||||
mp.half_life_days,
|
||||
mp.predicted_at
|
||||
FROM bookie.match_prediction mp
|
||||
JOIN bookie.match m ON m.match_id = mp.match_id
|
||||
JOIN bookie.team t ON t.team_id = mp.team_id
|
||||
JOIN bookie.team ht ON ht.team_id = m.home_team_id
|
||||
JOIN bookie.team at ON at.team_id = m.away_team_id
|
||||
JOIN bookie.matchday md ON md.matchday_id = m.matchday_id
|
||||
JOIN bookie.season s ON s.season_id = md.season_id
|
||||
JOIN bookie.league l ON l.league_id = s.league_id
|
||||
ORDER BY m.kickoff_at DESC, mp.match_id, is_home DESC;
|
||||
|
||||
-- League Dixon-Coles parameters
|
||||
CREATE OR REPLACE VIEW bookie.v_league_model_param_detail AS
|
||||
SELECT
|
||||
l.league_id,
|
||||
l.name AS league_name,
|
||||
l.country,
|
||||
l.tier_level,
|
||||
p.home_advantage,
|
||||
p.rho,
|
||||
p.avg_home_goals,
|
||||
p.avg_away_goals,
|
||||
p.matches_used,
|
||||
p.half_life_days,
|
||||
p.trained_at,
|
||||
(SELECT COUNT(*) FROM bookie.team_strength ts WHERE ts.league_id = l.league_id) AS team_strength_count
|
||||
FROM bookie.league l
|
||||
LEFT JOIN bookie.league_model_param p ON p.league_id = l.league_id
|
||||
ORDER BY l.country, l.name;
|
||||
|
||||
-- Team attack/defense with readable multipliers
|
||||
CREATE OR REPLACE VIEW bookie.v_team_strength_detail AS
|
||||
SELECT
|
||||
ts.team_strength_id,
|
||||
ts.team_id,
|
||||
t.name AS team_name,
|
||||
ts.league_id,
|
||||
l.name AS league_name,
|
||||
l.country,
|
||||
ts.log_attack,
|
||||
ts.log_defense,
|
||||
ROUND(EXP(ts.log_attack)::numeric, 4) AS attack_factor,
|
||||
ROUND(EXP(ts.log_defense)::numeric, 4) AS defense_factor,
|
||||
ts.matches_used,
|
||||
ts.trained_at
|
||||
FROM bookie.team_strength ts
|
||||
JOIN bookie.team t ON t.team_id = ts.team_id
|
||||
JOIN bookie.league l ON l.league_id = ts.league_id
|
||||
ORDER BY l.country, l.name, attack_factor DESC;
|
||||
|
||||
-- 1X2 odds per bookmaker with match context
|
||||
CREATE OR REPLACE VIEW bookie.v_match_odds_detail AS
|
||||
SELECT
|
||||
mo.match_odds_id,
|
||||
mo.match_id,
|
||||
m.kickoff_at,
|
||||
m.status,
|
||||
ht.name AS home_team,
|
||||
at.name AS away_team,
|
||||
l.name AS league,
|
||||
mo.bookmaker,
|
||||
mo.home_odds,
|
||||
mo.draw_odds,
|
||||
mo.away_odds,
|
||||
ROUND((1 / mo.home_odds)::numeric, 4) AS home_implied,
|
||||
ROUND((1 / mo.draw_odds)::numeric, 4) AS draw_implied,
|
||||
ROUND((1 / mo.away_odds)::numeric, 4) AS away_implied,
|
||||
mo.total_line,
|
||||
mo.over_odds,
|
||||
mo.under_odds,
|
||||
CASE WHEN mo.over_odds IS NOT NULL THEN ROUND((1 / mo.over_odds)::numeric, 4) END AS over_implied,
|
||||
CASE WHEN mo.under_odds IS NOT NULL THEN ROUND((1 / mo.under_odds)::numeric, 4) END AS under_implied,
|
||||
mo.fetched_at
|
||||
FROM bookie.match_odds mo
|
||||
JOIN bookie.match m ON m.match_id = mo.match_id
|
||||
JOIN bookie.team ht ON ht.team_id = m.home_team_id
|
||||
JOIN bookie.team at ON at.team_id = m.away_team_id
|
||||
JOIN bookie.matchday md ON md.matchday_id = m.matchday_id
|
||||
JOIN bookie.season s ON s.season_id = md.season_id
|
||||
JOIN bookie.league l ON l.league_id = s.league_id
|
||||
ORDER BY m.kickoff_at DESC, mo.match_id, mo.bookmaker;
|
||||
|
||||
-- Averaged 1X2 odds per match (what predict uses for market blending)
|
||||
CREATE OR REPLACE VIEW bookie.v_match_odds_consensus AS
|
||||
SELECT
|
||||
mo.match_id,
|
||||
m.kickoff_at,
|
||||
m.status,
|
||||
ht.name AS home_team,
|
||||
at.name AS away_team,
|
||||
l.name AS league,
|
||||
COUNT(*) AS bookmaker_count,
|
||||
ROUND(AVG(mo.home_odds)::numeric, 3) AS avg_home_odds,
|
||||
ROUND(AVG(mo.draw_odds)::numeric, 3) AS avg_draw_odds,
|
||||
ROUND(AVG(mo.away_odds)::numeric, 3) AS avg_away_odds,
|
||||
ROUND((1 / AVG(mo.home_odds))::numeric, 4) AS home_implied,
|
||||
ROUND((1 / AVG(mo.draw_odds))::numeric, 4) AS draw_implied,
|
||||
ROUND((1 / AVG(mo.away_odds))::numeric, 4) AS away_implied,
|
||||
ROUND(AVG(mo.total_line)::numeric, 2) AS avg_total_line,
|
||||
ROUND(AVG(mo.over_odds)::numeric, 3) AS avg_over_odds,
|
||||
ROUND(AVG(mo.under_odds)::numeric, 3) AS avg_under_odds,
|
||||
CASE WHEN AVG(mo.over_odds) IS NOT NULL
|
||||
THEN ROUND((1 / AVG(mo.over_odds))::numeric, 4) END AS over_implied,
|
||||
CASE WHEN AVG(mo.under_odds) IS NOT NULL
|
||||
THEN ROUND((1 / AVG(mo.under_odds))::numeric, 4) END AS under_implied,
|
||||
COUNT(mo.total_line) AS totals_bookmaker_count,
|
||||
MAX(mo.fetched_at) AS latest_fetched_at
|
||||
FROM bookie.match_odds mo
|
||||
JOIN bookie.match m ON m.match_id = mo.match_id
|
||||
JOIN bookie.team ht ON ht.team_id = m.home_team_id
|
||||
JOIN bookie.team at ON at.team_id = m.away_team_id
|
||||
JOIN bookie.matchday md ON md.matchday_id = m.matchday_id
|
||||
JOIN bookie.season s ON s.season_id = md.season_id
|
||||
JOIN bookie.league l ON l.league_id = s.league_id
|
||||
GROUP BY mo.match_id, m.kickoff_at, m.status, ht.name, at.name, l.name
|
||||
ORDER BY m.kickoff_at DESC;
|
||||
|
||||
-- Team name aliases for odds API matching
|
||||
CREATE OR REPLACE VIEW bookie.v_team_odds_alias_detail AS
|
||||
SELECT
|
||||
a.team_id,
|
||||
t.name AS team_name,
|
||||
a.odds_api_name,
|
||||
(a.odds_api_name = t.name) AS exact_name_match
|
||||
FROM bookie.team_odds_alias a
|
||||
JOIN bookie.team t ON t.team_id = a.team_id
|
||||
ORDER BY t.name;
|
||||
|
||||
-- Extra markets (corners/cards O/U)
|
||||
CREATE OR REPLACE VIEW bookie.v_match_extra_odds_detail AS
|
||||
SELECT
|
||||
eo.match_extra_odds_id,
|
||||
eo.match_id,
|
||||
m.kickoff_at,
|
||||
ht.name AS home_team,
|
||||
at.name AS away_team,
|
||||
eo.market,
|
||||
eo.bookmaker,
|
||||
eo.line,
|
||||
eo.over_odds,
|
||||
eo.under_odds,
|
||||
eo.fetched_at
|
||||
FROM bookie.match_extra_odds eo
|
||||
JOIN bookie.match m ON m.match_id = eo.match_id
|
||||
JOIN bookie.team ht ON ht.team_id = m.home_team_id
|
||||
JOIN bookie.team at ON at.team_id = m.away_team_id
|
||||
ORDER BY m.kickoff_at DESC, eo.match_id, eo.market, eo.bookmaker;
|
||||
|
||||
-- One-row-per-match snapshot for pre-match review (model + odds + prediction coverage)
|
||||
CREATE OR REPLACE VIEW bookie.v_prematch_model_snapshot AS
|
||||
SELECT
|
||||
m.match_id,
|
||||
m.kickoff_at,
|
||||
m.status,
|
||||
ht.name AS home_team,
|
||||
at.name AS away_team,
|
||||
l.league_id,
|
||||
l.name AS league,
|
||||
l.country,
|
||||
s.name AS season,
|
||||
lmp.home_advantage,
|
||||
lmp.rho,
|
||||
lmp.avg_home_goals,
|
||||
lmp.avg_away_goals,
|
||||
lmp.matches_used AS model_matches_used,
|
||||
lmp.trained_at AS model_trained_at,
|
||||
hs.log_attack AS home_log_attack,
|
||||
hs.log_defense AS home_log_defense,
|
||||
aws.log_attack AS away_log_attack,
|
||||
aws.log_defense AS away_log_defense,
|
||||
oc.bookmaker_count,
|
||||
oc.avg_home_odds,
|
||||
oc.avg_draw_odds,
|
||||
oc.avg_away_odds,
|
||||
oc.home_implied,
|
||||
oc.draw_implied,
|
||||
oc.away_implied,
|
||||
oc.avg_total_line,
|
||||
oc.avg_over_odds,
|
||||
oc.avg_under_odds,
|
||||
oc.over_implied,
|
||||
oc.under_implied,
|
||||
hp.predicted_goals AS home_pred_goals,
|
||||
ap.predicted_goals AS away_pred_goals,
|
||||
hp.predicted_at AS home_predicted_at,
|
||||
ap.predicted_at AS away_predicted_at
|
||||
FROM bookie.match m
|
||||
JOIN bookie.team ht ON ht.team_id = m.home_team_id
|
||||
JOIN bookie.team at ON at.team_id = m.away_team_id
|
||||
JOIN bookie.matchday md ON md.matchday_id = m.matchday_id
|
||||
JOIN bookie.season s ON s.season_id = md.season_id
|
||||
JOIN bookie.league l ON l.league_id = s.league_id
|
||||
LEFT JOIN bookie.league_model_param lmp ON lmp.league_id = l.league_id
|
||||
LEFT JOIN bookie.team_strength hs ON hs.team_id = m.home_team_id AND hs.league_id = l.league_id
|
||||
LEFT JOIN bookie.team_strength aws ON aws.team_id = m.away_team_id AND aws.league_id = l.league_id
|
||||
LEFT JOIN bookie.v_match_odds_consensus oc ON oc.match_id = m.match_id
|
||||
LEFT JOIN bookie.match_prediction hp ON hp.match_id = m.match_id AND hp.team_id = m.home_team_id
|
||||
LEFT JOIN bookie.match_prediction ap ON ap.match_id = m.match_id AND ap.team_id = m.away_team_id
|
||||
ORDER BY m.kickoff_at DESC;
|
||||
Reference in New Issue
Block a user