File size: 11,082 Bytes
367f592
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
-- =============================================================================
-- frontal-lobe2 : THE MATCH PAGE -- the lead page for this lake and every ramp
-- to come. A strict, hardcoded query chain, deterministic, no model logic:
--
--   1. what is today                         -> v_today
--   2. are there NFL games today (yes/no)    -> v_games_today_flag
--   3. if yes, list the matchups             -> v_matchups_today
--   4. for each matchup: inactives, weather  -> v_matchup_inactives,
--      signal, and odds                         v_matchup_weather_signal,
--                                                v_matchup_odds
--
-- This chain is deliberately simple SQL, not application code, per the user's
-- explicit request: "these are static queries the loop or jobs" and "there
-- are things i want hard coded." Impact SCORING of weather/inactives is
-- explicitly deferred to a later "logic engine" phase -- this layer only
-- surfaces the raw signal, it does not judge it.
--
-- Engine: DuckDB. "Today" is CURRENT_DATE, i.e. the database engine's own
-- clock at query time -- never a value baked into a table, so this chain is
-- correct on any day it's run, not just the day it was built.
-- =============================================================================

CREATE SCHEMA IF NOT EXISTS gold;

-- -----------------------------------------------------------------------------
-- STEP 1: what is today. The single anchor everything else depends on.
--
-- Anchored explicitly to America/Chicago (US Central) -- the user's own
-- timezone (New Orleans) -- NOT the host machine's local timezone. Confirmed
-- during this build: the host container is CDT, and naive CURRENT_DATE gave
-- a different answer than an explicit-timezone version depending on time of
-- day. This bug would silently vary by which machine runs the query.
--
-- NOTE: NFL scheduling itself (gametime strings in silver.nfl_schedule) is
-- published in US/Eastern by the league; game_time_local_cst below converts
-- that to the user's Central time for DISPLAY. The "is there a game today"
-- date comparison still correctly uses the calendar date, which does not
-- shift between Eastern and Central on the same day for any NFL kickoff time
-- (earliest kickoffs are ~11am CT / 12pm ET, never near a date boundary).
-- -----------------------------------------------------------------------------
CREATE OR REPLACE VIEW gold.v_today AS
SELECT CAST(CURRENT_TIMESTAMP AT TIME ZONE 'America/Chicago' AS DATE) AS today_date,
       CURRENT_TIMESTAMP AS as_of_utc,
       dayname(CAST(CURRENT_TIMESTAMP AT TIME ZONE 'America/Chicago' AS DATE)) AS weekday;

-- -----------------------------------------------------------------------------
-- STEP 2: are there any NFL games today. A single-row yes/no flag with a count.
-- -----------------------------------------------------------------------------
CREATE OR REPLACE VIEW gold.v_games_today_flag AS
SELECT t.today_date,
       count(s.game_id) > 0 AS has_games_today,
       count(s.game_id) AS game_count
FROM gold.v_today t
LEFT JOIN silver.nfl_schedule s ON s.game_date = t.today_date
GROUP BY t.today_date;

-- -----------------------------------------------------------------------------
-- STEP 3: the matchup list for today (empty result set is a valid, correct
-- answer on a non-game day -- the chain must not error or fake a matchup).
-- -----------------------------------------------------------------------------
-- game_time_local in silver.nfl_schedule is published by the league in
-- US/Eastern (confirmed: a game at Melbourne Cricket Ground shows 20:35,
-- the same convention as a normal US Eastern-time evening kickoff -- this is
-- NOT literal stadium-local time). game_time_cst below converts to the
-- user's own timezone (New Orleans, US/Central) for display. ET is always
-- exactly 1 hour ahead of CT (both observe DST on the same schedule), so this
-- is a flat, safe offset -- no timezone-library edge cases.
CREATE OR REPLACE VIEW gold.v_matchups_today AS
SELECT s.game_id, s.season, s.week, s.game_date, s.weekday,
       s.game_time_local AS game_time_et,
       strftime(s.game_date + CAST(s.game_time_local AS TIME)
                - INTERVAL 1 HOUR, '%H:%M') AS game_time_cst,
       s.away_team, s.home_team, s.location, s.roof, s.surface, s.div_game
FROM silver.nfl_schedule s
WHERE s.game_date = CURRENT_DATE
ORDER BY s.game_time_local;

-- A "next games" view is the practical companion to v_matchups_today --
-- most days have zero games today, so this answers "what's coming up" without
-- changing the hardcoded yes/no chain above.
CREATE OR REPLACE VIEW gold.v_matchups_upcoming AS
SELECT s.game_id, s.season, s.week, s.game_date, s.weekday,
       s.game_time_local AS game_time_et,
       strftime(s.game_date + CAST(s.game_time_local AS TIME)
                - INTERVAL 1 HOUR, '%H:%M') AS game_time_cst,
       s.away_team, s.home_team, s.location, s.roof, s.surface, s.div_game,
       s.game_date - CURRENT_DATE AS days_until
FROM silver.nfl_schedule s
WHERE s.game_date > CURRENT_DATE
ORDER BY s.game_date, s.game_time_local;

-- -----------------------------------------------------------------------------
-- STEP 4a: inactives/injury status per matchup. Two distinct signals kept
-- separate on purpose -- do not blur them:
--   - official Wed/Thu/Fri practice-report status (the closest proxy we have)
--   - the TRUE final inactives list is NOT available from any source in this
--     ramp (released ~90 min pre-kickoff; see docs/MATCH_PAGE.md). This view
--     is the proxy, clearly labeled as such, not the real thing.
-- -----------------------------------------------------------------------------
CREATE OR REPLACE VIEW gold.v_matchup_inactives AS
SELECT s.game_id, s.game_date, s.away_team, s.home_team,
       r.team, r.full_name, r.position, r.report_status, r.report_primary_injury,
       r.practice_status,
       'practice_report_proxy -- NOT the final inactives list' AS signal_type
FROM silver.nfl_schedule s
JOIN silver.nfl_injury_report r
  ON r.team IN (s.away_team, s.home_team)
 AND r.season = s.season
 AND r.week = s.week
WHERE r.report_status IS NOT NULL
ORDER BY s.game_id,
         CASE r.report_status WHEN 'Out' THEN 0 WHEN 'Doubtful' THEN 1
                               WHEN 'Questionable' THEN 2 ELSE 3 END;

-- -----------------------------------------------------------------------------
-- STEP 4b: weather signal per matchup. Raw signal only -- no impact score
-- (that's explicitly the logic engine's job later). Two honest distinctions:
--   - roof/surface: KNOWN ahead of time (fixed stadium property) for every
--     game, past or future.
--   - temp_actual/wind_actual: ONLY populated for games already played
--     (recorded conditions). For an upcoming game these will be NULL --
--     that's correct, not a bug; a real pre-game FORECAST needs a different,
--     forward-looking source not built in this ramp yet.
-- -----------------------------------------------------------------------------
CREATE OR REPLACE VIEW gold.v_matchup_weather_signal AS
SELECT game_id, game_date, away_team, home_team,
       roof, surface,
       temp_actual, wind_actual,
       CASE WHEN roof = 'outdoors' THEN true ELSE false END AS is_outdoor_venue,
       CASE WHEN temp_actual IS NULL THEN 'no_recorded_conditions_yet_or_forecast_not_sourced'
            WHEN roof != 'outdoors' THEN 'indoor_venue_weather_not_a_factor'
            WHEN temp_actual <= 32 THEN 'freezing'
            WHEN temp_actual >= 90 THEN 'extreme_heat'
            WHEN wind_actual >= 20 THEN 'high_wind'
            ELSE 'no_extreme_flag' END AS weather_flag_hardcoded_threshold
FROM silver.nfl_schedule;

-- -----------------------------------------------------------------------------
-- STEP 4c: odds per matchup -- BOTH sources, clearly separated by grain.
--   - game-level (spread/total/moneyline): from the schedule feed itself.
--   - player-prop lines: from RotoWire, already snapshotted for line
--     movement (gold.nfl_prop_line_rotowire / gold.v_nfl_prop_best_price).
-- Kept as two views, not merged, since they are different grains (team vs.
-- player) and merging them would hide that distinction from whatever reads
-- this next (the Lake Logic phase the user referenced).
-- -----------------------------------------------------------------------------
CREATE OR REPLACE VIEW gold.v_matchup_game_odds AS
SELECT game_id, game_date, away_team, home_team,
       spread_line, away_spread_odds, home_spread_odds,
       total_line, over_odds, under_odds,
       away_moneyline, home_moneyline,
       'nflverse_schedule -- reference line, not a live-updating snapshot' AS source_note
FROM silver.nfl_schedule
WHERE spread_line IS NOT NULL OR total_line IS NOT NULL;

-- NOTE on the join: RotoWire's game_id (a RotoWire-internal numeric id, e.g.
-- "2978668") and the nflverse schedule's game_id (e.g. "2026_03_ATL_GB") are
-- two entirely different ID schemes -- confirmed while building this view;
-- joining on game_id directly returns zero rows even when real prop data
-- exists for both teams. Joining on team membership instead (a prop's player
-- team is one of the two teams in the matchup) is the correct link until a
-- real cross-source game_id bridge is built.
CREATE OR REPLACE VIEW gold.v_matchup_prop_odds AS
SELECT s.game_id, s.game_date, s.away_team, s.home_team,
       pl.player_name, pl.market_slug, pl.line,
       pl.best_over_book, pl.best_over_price, pl.best_under_book, pl.best_under_price,
       pl.best_moneyline_book, pl.best_moneyline_price
FROM silver.nfl_schedule s
JOIN gold.nfl_prop_line_rotowire raw
  ON raw.team IN (s.away_team, s.home_team)
JOIN gold.v_nfl_prop_best_price pl
  ON pl.player_id = raw.player_id AND pl.market_slug = raw.market_slug
GROUP BY s.game_id, s.game_date, s.away_team, s.home_team, pl.player_name,
         pl.market_slug, pl.line, pl.best_over_book, pl.best_over_price,
         pl.best_under_book, pl.best_under_price, pl.best_moneyline_book,
         pl.best_moneyline_price;

-- -----------------------------------------------------------------------------
-- The full bundle, one row set per matchup for TODAY only. If v_games_today_flag
-- says false, this returns zero rows -- correct behavior, not an error.
-- -----------------------------------------------------------------------------
CREATE OR REPLACE VIEW gold.v_match_page_today AS
SELECT m.game_id, m.season, m.week, m.game_date, m.weekday, m.game_time_et, m.game_time_cst,
       m.away_team, m.home_team, m.roof, m.surface, m.div_game,
       (SELECT count(*) FROM gold.v_matchup_inactives i WHERE i.game_id = m.game_id) AS inactive_signal_count,
       (SELECT weather_flag_hardcoded_threshold FROM gold.v_matchup_weather_signal w
        WHERE w.game_id = m.game_id) AS weather_flag,
       (SELECT count(*) FROM gold.v_matchup_game_odds o WHERE o.game_id = m.game_id) AS has_game_odds,
       (SELECT count(*) FROM gold.v_matchup_prop_odds p WHERE p.game_id = m.game_id) AS prop_line_count
FROM gold.v_matchups_today m;