-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathweather-silver-schema.sql
More file actions
256 lines (222 loc) · 13.1 KB
/
Copy pathweather-silver-schema.sql
File metadata and controls
256 lines (222 loc) · 13.1 KB
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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
-- BHN Strategy 9 — Silver Layer Schema
-- Cleaned, standardized, and deduplicated data.
-- Populated inline by the collector after each bronze write.
-- is_latest_run / is_latest_snapshot flags managed by the collector
-- (reset previous, set new) — never manually set.
--
-- Apply via: sql/migrations/2026-06-11-weather-bsg-tables.sql
-- ─────────────────────────────────────────────────────────────────────────────
-- 1) weather_silver_forecast_conformed
-- One row per (station, source, run_time, target_date).
-- is_latest_run = TRUE on the most recent run per (station, source, target_date).
-- ─────────────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS weather_silver_forecast_conformed (
id BIGSERIAL PRIMARY KEY,
city TEXT NOT NULL,
station_code TEXT NOT NULL,
source_name TEXT NOT NULL, -- 'nws', 'open_meteo_gfs', 'open_meteo_ecmwf'
forecast_run_time TIMESTAMPTZ NOT NULL,
target_date DATE NOT NULL,
lead_hours INTEGER,
tmax_f NUMERIC,
tmin_f NUMERIC,
dewpoint_f NUMERIC,
rh_pct NUMERIC,
wind_speed_mph NUMERIC,
wind_gust_mph NUMERIC,
cloud_cover_pct NUMERIC,
pop_pct NUMERIC,
qpf_in NUMERIC,
snowfall_in NUMERIC,
is_latest_run BOOLEAN NOT NULL DEFAULT FALSE,
is_valid BOOLEAN NOT NULL DEFAULT TRUE,
quality_flag TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT silver_forecast_unique
UNIQUE (station_code, source_name, forecast_run_time, target_date)
);
CREATE INDEX IF NOT EXISTS sfcfc_latest_idx
ON weather_silver_forecast_conformed (station_code, target_date, is_latest_run)
WHERE is_latest_run = TRUE;
CREATE INDEX IF NOT EXISTS sfcfc_source_date_idx
ON weather_silver_forecast_conformed (station_code, source_name, target_date, forecast_run_time DESC);
-- ─────────────────────────────────────────────────────────────────────────────
-- 2) weather_silver_market_conformed
-- One row per (market_ticker, snapshot_time).
-- is_latest_snapshot = TRUE on the most recent snapshot per market_ticker.
-- ─────────────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS weather_silver_market_conformed (
id BIGSERIAL PRIMARY KEY,
market_ticker TEXT NOT NULL,
series_ticker TEXT,
city TEXT NOT NULL,
station_code TEXT NOT NULL,
contract_side TEXT NOT NULL, -- 'high' or 'low'
bucket_floor NUMERIC,
bucket_cap NUMERIC,
bucket_type TEXT,
bucket_label TEXT,
target_date DATE NOT NULL,
snapshot_time TIMESTAMPTZ NOT NULL,
yes_mid NUMERIC,
yes_bid NUMERIC,
yes_ask NUMERIC,
implied_prob NUMERIC, -- same as yes_mid, explicit alias
volume NUMERIC,
open_interest NUMERIC,
market_status TEXT,
market_liquidity_flag TEXT, -- 'liquid' >1000, 'thin' >100, 'illiquid' else
is_latest_snapshot BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT silver_market_unique
UNIQUE (market_ticker, snapshot_time)
);
CREATE INDEX IF NOT EXISTS smktc_latest_idx
ON weather_silver_market_conformed (market_ticker, is_latest_snapshot)
WHERE is_latest_snapshot = TRUE;
CREATE INDEX IF NOT EXISTS smktc_station_date_idx
ON weather_silver_market_conformed (station_code, target_date, is_latest_snapshot);
CREATE INDEX IF NOT EXISTS smktc_series_date_idx
ON weather_silver_market_conformed (series_ticker, target_date)
WHERE series_ticker IS NOT NULL;
-- ─────────────────────────────────────────────────────────────────────────────
-- 3) weather_silver_actuals_conformed
-- Settlement truth from NWS CLI.
-- UNIQUE per (station, date, source) — multiple sources possible in future.
-- ─────────────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS weather_silver_actuals_conformed (
id BIGSERIAL PRIMARY KEY,
city TEXT NOT NULL,
station_code TEXT NOT NULL,
target_date DATE NOT NULL,
final_tmax_f NUMERIC,
final_tmin_f NUMERIC,
actual_source TEXT NOT NULL DEFAULT 'nws_cli',
report_issued_at TIMESTAMPTZ,
settlement_label_high TEXT, -- which Kalshi bucket the actual fell in, e.g. '88-89'
settlement_label_low TEXT,
is_final BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT silver_actuals_unique
UNIQUE (station_code, target_date, actual_source)
);
CREATE INDEX IF NOT EXISTS sact_station_date_idx
ON weather_silver_actuals_conformed (station_code, target_date DESC);
-- ─────────────────────────────────────────────────────────────────────────────
-- 4) weather_silver_forecast_error
-- Forecast vs actual pairs for calibration training.
-- Error convention: forecast_error_f = actual - forecast
-- positive = NWS ran cold (forecast too low)
-- negative = NWS ran hot (forecast too high)
-- ─────────────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS weather_silver_forecast_error (
id BIGSERIAL PRIMARY KEY,
city TEXT NOT NULL,
station_code TEXT NOT NULL,
target_date DATE NOT NULL,
feature_name TEXT NOT NULL, -- 'tmax_f' or 'tmin_f'
source_name TEXT NOT NULL,
forecast_run_time TIMESTAMPTZ,
lead_hours INTEGER,
forecast_value NUMERIC,
actual_value NUMERIC,
forecast_error_f NUMERIC, -- actual - forecast
error_sign TEXT, -- 'cold', 'hot', 'exact'
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT silver_fcerr_unique
UNIQUE (station_code, target_date, feature_name, source_name, forecast_run_time)
);
CREATE INDEX IF NOT EXISTS sfce_station_feature_idx
ON weather_silver_forecast_error (station_code, feature_name, target_date DESC);
CREATE INDEX IF NOT EXISTS sfce_station_source_idx
ON weather_silver_forecast_error (station_code, source_name, target_date DESC);
-- ─────────────────────────────────────────────────────────────────────────────
-- 5) weather_silver_calibration_training_set
-- Per-bucket training rows. Populated after actuals arrive.
-- actual_outcome = 1 if the actual temp fell in the bucket, else 0.
-- ─────────────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS weather_silver_calibration_training_set (
id BIGSERIAL PRIMARY KEY,
city TEXT NOT NULL,
station_code TEXT NOT NULL,
contract_side TEXT NOT NULL, -- 'high' or 'low'
market_ticker TEXT,
target_date DATE NOT NULL,
forecast_run_time TIMESTAMPTZ,
lead_hours INTEGER,
raw_prob NUMERIC, -- model raw probability for this bucket
market_price NUMERIC, -- kalshi yes_mid at time of signal
actual_outcome INTEGER, -- 1 = bucket hit, 0 = miss
actual_tmax_f NUMERIC,
forecast_error_f NUMERIC,
season TEXT, -- 'spring','summer','fall','winter'
calibration_window_flag BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS scal_station_side_idx
ON weather_silver_calibration_training_set (station_code, contract_side, target_date DESC);
-- ─────────────────────────────────────────────────────────────────────────────
-- 6) weather_silver_model_base
-- One clean row per (station, date, contract_side, forecast_run_time).
-- Joins NWS + GFS + Kalshi + actuals in one place.
-- Updated on new forecasts; actual fields populated after settlement.
-- ─────────────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS weather_silver_model_base (
id BIGSERIAL PRIMARY KEY,
city TEXT NOT NULL,
station_code TEXT NOT NULL,
target_date DATE NOT NULL,
contract_side TEXT NOT NULL, -- 'high' or 'low'
forecast_run_time TIMESTAMPTZ NOT NULL,
lead_hours INTEGER,
-- NWS forecast
nws_tmax_f NUMERIC,
nws_tmin_f NUMERIC,
nws_dewpoint_f NUMERIC,
nws_rh_pct NUMERIC,
nws_wind_speed_mph NUMERIC,
nws_cloud_cover_pct NUMERIC,
nws_pop_pct NUMERIC,
-- Open-Meteo GFS forecast
gfs_tmax_f NUMERIC,
gfs_tmin_f NUMERIC,
gfs_dewpoint_f NUMERIC,
gfs_rh_pct NUMERIC,
gfs_wind_speed NUMERIC,
gfs_cloud_cover NUMERIC,
-- NWS vs GFS disagreement
nws_gfs_tmax_delta NUMERIC, -- nws_tmax_f - gfs_tmax_f
nws_gfs_tmin_delta NUMERIC,
-- Kalshi market at time of signal (latest snapshot when row written)
market_ticker TEXT,
kalshi_yes_mid NUMERIC,
kalshi_implied_prob NUMERIC,
kalshi_volume NUMERIC,
kalshi_snapshot_time TIMESTAMPTZ,
-- Actual settlement (NULL until CLI report arrives)
actual_tmax_f NUMERIC,
actual_tmin_f NUMERIC,
is_settled BOOLEAN NOT NULL DEFAULT FALSE,
is_valid BOOLEAN NOT NULL DEFAULT TRUE,
quality_flag TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT silver_model_base_unique
UNIQUE (station_code, target_date, contract_side, forecast_run_time)
);
CREATE INDEX IF NOT EXISTS smbase_station_date_idx
ON weather_silver_model_base (station_code, target_date DESC, contract_side);
CREATE INDEX IF NOT EXISTS smbase_unsettled_idx
ON weather_silver_model_base (target_date, station_code)
WHERE is_settled = FALSE;
-- Permissions
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'horizon_agent_reader') THEN
GRANT SELECT ON weather_silver_forecast_conformed TO horizon_agent_reader;
GRANT SELECT ON weather_silver_market_conformed TO horizon_agent_reader;
GRANT SELECT ON weather_silver_actuals_conformed TO horizon_agent_reader;
GRANT SELECT ON weather_silver_forecast_error TO horizon_agent_reader;
GRANT SELECT ON weather_silver_calibration_training_set TO horizon_agent_reader;
GRANT SELECT ON weather_silver_model_base TO horizon_agent_reader;
END IF;
END $$;