Finalize SQLite defaults and refresh calibration artifacts

This commit is contained in:
2569718930@qq.com
2026-04-02 23:24:38 +08:00
parent 603546f2f9
commit 95c0c452f1
19 changed files with 1055 additions and 2442 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ OPEN_METEO_DISK_CACHE_PATH=/var/lib/polyweather/open_meteo_cache.json
# Windows / macOS can usually keep the defaults. # Windows / macOS can usually keep the defaults.
UID=1000 UID=1000
GID=1000 GID=1000
POLYWEATHER_STATE_STORAGE_MODE=dual POLYWEATHER_STATE_STORAGE_MODE=sqlite
POLYWEATHER_PROMETHEUS_PORT=9090 POLYWEATHER_PROMETHEUS_PORT=9090
POLYWEATHER_ALERTMANAGER_PORT=9093 POLYWEATHER_ALERTMANAGER_PORT=9093
POLYWEATHER_ALERT_RELAY_PORT=9099 POLYWEATHER_ALERT_RELAY_PORT=9099
+4 -2
View File
@@ -25,7 +25,7 @@ Public docs center: `/docs/intro` on the main site (bilingual product documentat
- Auto-reconciliation live: event listener + periodic confirm loop. - Auto-reconciliation live: event listener + periodic confirm loop.
- Ops dashboard live: `/ops` for memberships, leaderboard, manual point grants, and payment incident triage. - Ops dashboard live: `/ops` for memberships, leaderboard, manual point grants, and payment incident triage.
- Lightweight observability live: `/healthz`, `/api/system/status`, `/metrics`. - Lightweight observability live: `/healthz`, `/api/system/status`, `/metrics`.
- Runtime state supports gradual SQLite migration (`file / dual / sqlite`). - Runtime state, cache, and core offline training/backfill flows now use SQLite as the primary path; legacy JSON/JSONL files remain only for migration, export, and explicit fallback input.
- EMOS/CRPS pipeline is integrated in `shadow` mode with rollout gating. - EMOS/CRPS pipeline is integrated in `shadow` mode with rollout gating.
- Intraday structural signal is now peak-window aware and bilingual (`zh-CN` / `en-US`). - Intraday structural signal is now peak-window aware and bilingual (`zh-CN` / `en-US`).
- Non-Hong Kong airport cities now ingest `TAF` and parse `FM / TEMPO / BECMG / PROB30/40`. - Non-Hong Kong airport cities now ingest `TAF` and parse `FM / TEMPO / BECMG / PROB30/40`.
@@ -101,7 +101,8 @@ npm run dev
## Recent Highlights ## Recent Highlights
- Taipei settlement is aligned to `NOAA RCTP` and rounded whole-degree Celsius logic. - Taipei settlement is aligned to `Wunderground RCSS` with whole-degree Celsius resolution logic.
- Shenzhen settlement is aligned to `Wunderground ZGSZ`.
- Hong Kong keeps `HKO` official readings in dashboard and history, without falling back to airport METAR lines. - Hong Kong keeps `HKO` official readings in dashboard and history, without falling back to airport METAR lines.
- Intraday analysis now separates: - Intraday analysis now separates:
- `Surface Structure` - `Surface Structure`
@@ -117,6 +118,7 @@ Use external runtime storage to avoid SQLite/git conflicts:
```env ```env
POLYWEATHER_RUNTIME_DATA_DIR=/var/lib/polyweather POLYWEATHER_RUNTIME_DATA_DIR=/var/lib/polyweather
POLYWEATHER_DB_PATH=/var/lib/polyweather/polyweather.db POLYWEATHER_DB_PATH=/var/lib/polyweather/polyweather.db
POLYWEATHER_STATE_STORAGE_MODE=sqlite
``` ```
## Ops Verification ## Ops Verification
+3 -3
View File
@@ -24,7 +24,7 @@
- 已上线轻量运营后台:`/ops`(会员、周榜、补分、支付异常单)。 - 已上线轻量运营后台:`/ops`(会员、周榜、补分、支付异常单)。
- 已上线轻量可观测性:`/healthz``/api/system/status``/metrics` - 已上线轻量可观测性:`/healthz``/api/system/status``/metrics`
- 已补最小外部监控栈:Prometheus + Alertmanager + Grafana + Telegram 告警 relay。 - 已补最小外部监控栈:Prometheus + Alertmanager + Grafana + Telegram 告警 relay。
- 运行态状态缓存已支持 SQLite 渐进迁移:`file / dual / sqlite` - 运行态状态缓存与核心离线训练/回填链路已完成 SQLite 主路径收口;legacy JSON/JSONL 仅保留给迁移、导出与显式回退输入
- 已接入 EMOS/CRPS 校准链路,但当前仍保持 `emos_shadow` - 已接入 EMOS/CRPS 校准链路,但当前仍保持 `emos_shadow`
## 许可证与商用边界(重要) ## 许可证与商用边界(重要)
@@ -64,7 +64,7 @@ flowchart LR
ANA --> PAY["支付状态(Intent + Event + Confirm Loop"] ANA --> PAY["支付状态(Intent + Event + Confirm Loop"]
ANA --> PM["Polymarket 只读层"] ANA --> PM["Polymarket 只读层"]
API --> OBS["healthz / system status / metrics"] API --> OBS["healthz / system status / metrics"]
ANA --> STATE["SQLite runtime state + dual fallback"] ANA --> STATE["SQLite runtime state<br/>legacy files only for migration/export fallback"]
``` ```
## 监控城市(30 ## 监控城市(30
@@ -98,7 +98,7 @@ npm run dev
```env ```env
POLYWEATHER_RUNTIME_DATA_DIR=/var/lib/polyweather POLYWEATHER_RUNTIME_DATA_DIR=/var/lib/polyweather
POLYWEATHER_DB_PATH=/var/lib/polyweather/polyweather.db POLYWEATHER_DB_PATH=/var/lib/polyweather/polyweather.db
POLYWEATHER_STATE_STORAGE_MODE=dual POLYWEATHER_STATE_STORAGE_MODE=sqlite
``` ```
## 运维验收 ## 运维验收
+45 -129
View File
@@ -1,20 +1,20 @@
{ {
"version": "emos-20260320132525", "version": "emos-20260402151301",
"trained_at": "2026-03-20T13:25:25.836021+00:00", "trained_at": "2026-04-02T15:13:01.073253+00:00",
"global": { "global": {
"mu": { "mu": {
"intercept": -1.57406048, "intercept": 1.52725527,
"raw_mu_coef": 2.80583627, "raw_mu_coef": 2.96318139,
"deb_coef": -0.06819634, "deb_coef": -1.54490032,
"ens_median_coef": -1.81560215, "ens_median_coef": -0.71820793,
"max_so_far_gap_coef": 0.0 "max_so_far_gap_coef": 10.22802132
}, },
"sigma": { "sigma": {
"intercept": 0.67509915, "intercept": 0.67227661,
"raw_sigma_coef": 0.14431833, "raw_sigma_coef": 0.69444674,
"spread_coef": 0.14431833, "spread_coef": 0.09134583,
"peak_flag_coef": 0.0, "peak_flag_coef": -0.58201761,
"max_so_far_gap_coef": 0.0 "max_so_far_gap_coef": -0.81482466
} }
}, },
"sigma_constraints": { "sigma_constraints": {
@@ -30,143 +30,59 @@
}, },
"blending": { "blending": {
"alpha_mu": 0.0, "alpha_mu": 0.0,
"alpha_sigma": 0.0 "alpha_sigma": 0.45
}, },
"cities": { "cities": {
"ankara": { "ankara": {
"samples": 7, "samples": 3,
"mu_bias": 0.566273, "mu_bias": 1.283918,
"sigma_scale": 2.0, "sigma_scale": 1.484056,
"confidence": 0.875 "confidence": 0.375
},
"london": {
"samples": 6,
"mu_bias": 0.489961,
"sigma_scale": 2.0,
"confidence": 0.75
},
"new york": {
"samples": 4,
"mu_bias": 1.852451,
"sigma_scale": 0.830444,
"confidence": 0.5
},
"paris": {
"samples": 7,
"mu_bias": 0.308599,
"sigma_scale": 2.0,
"confidence": 0.875
},
"seoul": {
"samples": 6,
"mu_bias": -1.486024,
"sigma_scale": 1.569585,
"confidence": 0.75
},
"toronto": {
"samples": 5,
"mu_bias": -0.734395,
"sigma_scale": 1.656751,
"confidence": 0.625
},
"buenos aires": {
"samples": 5,
"mu_bias": -1.753334,
"sigma_scale": 1.831739,
"confidence": 0.625
},
"wellington": {
"samples": 6,
"mu_bias": 0.350757,
"sigma_scale": 1.377974,
"confidence": 0.75
},
"chicago": {
"samples": 4,
"mu_bias": 3.01062,
"sigma_scale": 0.825575,
"confidence": 0.5
},
"sao paulo": {
"samples": 5,
"mu_bias": 1.632457,
"sigma_scale": 2.0,
"confidence": 0.625
},
"dallas": {
"samples": 4,
"mu_bias": 3.77714,
"sigma_scale": 0.796874,
"confidence": 0.5
},
"miami": {
"samples": 5,
"mu_bias": -4.868741,
"sigma_scale": 2.0,
"confidence": 0.625
},
"atlanta": {
"samples": 5,
"mu_bias": -7.648823,
"sigma_scale": 2.0,
"confidence": 0.625
},
"seattle": {
"samples": 4,
"mu_bias": 4.058619,
"sigma_scale": 2.0,
"confidence": 0.5
},
"lucknow": {
"samples": 4,
"mu_bias": 3.257609,
"sigma_scale": 2.0,
"confidence": 0.5
},
"munich": {
"samples": 6,
"mu_bias": -0.780811,
"sigma_scale": 2.0,
"confidence": 0.75
}, },
"hong kong": { "hong kong": {
"samples": 4,
"mu_bias": 1.329042,
"sigma_scale": 1.045379,
"confidence": 0.5
},
"milan": {
"samples": 3, "samples": 3,
"mu_bias": -0.492675, "mu_bias": -3.932299,
"sigma_scale": 2.0,
"confidence": 0.375
},
"shanghai": {
"samples": 3,
"mu_bias": 1.827618,
"sigma_scale": 2.0, "sigma_scale": 2.0,
"confidence": 0.375 "confidence": 0.375
}, },
"taipei": { "taipei": {
"samples": 3, "samples": 3,
"mu_bias": 1.462462, "mu_bias": 3.387896,
"sigma_scale": 1.132834,
"confidence": 0.375
},
"milan": {
"samples": 3,
"mu_bias": -2.729203,
"sigma_scale": 2.0, "sigma_scale": 2.0,
"confidence": 0.375 "confidence": 0.375
}, },
"warsaw": { "warsaw": {
"samples": 3, "samples": 3,
"mu_bias": 0.349319, "mu_bias": -0.614031,
"sigma_scale": 1.337949, "sigma_scale": 1.252706,
"confidence": 0.375 "confidence": 0.375
} }
}, },
"metrics": { "metrics": {
"sample_count": 105, "sample_count": 54,
"mean_crps": 2.923823, "mean_crps": 3.78558,
"legacy_mean_crps": 2.793938, "legacy_mean_crps": 4.326547,
"legacy_mean_mae": 2.721143, "legacy_mean_mae": 4.528889,
"legacy_bucket_hit_rate": 0.695238, "legacy_bucket_hit_rate": 0.518519,
"legacy_bucket_brier": 0.775463, "legacy_bucket_brier": 0.83496,
"selected_mean_crps": 2.700275, "selected_mean_crps": 4.239815,
"selected_mean_mae": 2.721143, "selected_mean_mae": 4.528889,
"selected_bucket_hit_rate": 0.695238, "selected_bucket_hit_rate": 0.537037,
"selected_bucket_brier": 0.765459, "selected_bucket_brier": 0.853485,
"selected_score": 4.003626, "selected_score": 6.027262,
"legacy_score": 4.104792, "legacy_score": 6.127879,
"filled_actual_from_history": 2, "filled_actual_from_history": 2,
"settlement_history_city_count": 30 "settlement_history_city_count": 30
}, },
@@ -1,190 +1,199 @@
{ {
"summary": { "summary": {
"sample_count": 105, "sample_count": 54,
"filled_actual_from_history": 2, "filled_actual_from_history": 2,
"legacy": { "legacy": {
"mean_crps": 2.793938, "mean_crps": 4.287017,
"mean_mae": 2.721143, "mean_mae": 4.488148,
"bucket_hit_rate": 0.695238 "bucket_hit_rate": 0.537037
}, },
"emos": { "emos": {
"mean_crps": 2.700275, "mean_crps": 4.201116,
"mean_mae": 2.721143, "mean_mae": 4.488148,
"bucket_hit_rate": 0.695238 "bucket_hit_rate": 0.537037
}, },
"delta": { "delta": {
"crps": -0.093663, "crps": -0.085901,
"mae": 0.0, "mae": 0.0,
"bucket_hit_rate": 0.0 "bucket_hit_rate": 0.0
} }
}, },
"by_city": { "by_city": {
"ankara": { "ankara": {
"samples": 7, "samples": 3,
"legacy_mean_crps": 2.023242, "legacy_mean_crps": 0.327701,
"emos_mean_crps": 2.023242, "emos_mean_crps": 0.439705,
"legacy_mean_mae": 1.984286, "legacy_mean_mae": 0.066667,
"emos_mean_mae": 1.984286, "emos_mean_mae": 0.066667,
"legacy_bucket_hit_rate": 0.714286, "legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 0.714286 "emos_bucket_hit_rate": 1.0
}, },
"atlanta": { "atlanta": {
"samples": 5, "samples": 2,
"legacy_mean_crps": 12.792034, "legacy_mean_crps": 30.449382,
"emos_mean_crps": 12.694543, "emos_mean_crps": 30.578432,
"legacy_mean_mae": 12.806, "legacy_mean_mae": 32.015,
"emos_mean_mae": 12.806, "emos_mean_mae": 32.015,
"legacy_bucket_hit_rate": 0.6, "legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.6 "emos_bucket_hit_rate": 0.0
}, },
"buenos aires": { "buenos aires": {
"samples": 5, "samples": 2,
"legacy_mean_crps": 3.846144, "legacy_mean_crps": 9.113412,
"emos_mean_crps": 3.846144, "emos_mean_crps": 8.759954,
"legacy_mean_mae": 4.168, "legacy_mean_mae": 10.27,
"emos_mean_mae": 4.168, "emos_mean_mae": 10.27,
"legacy_bucket_hit_rate": 0.6, "legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.6 "emos_bucket_hit_rate": 0.0
}, },
"chicago": { "chicago": {
"samples": 4, "samples": 1,
"legacy_mean_crps": 1.346667, "legacy_mean_crps": 1.250268,
"emos_mean_crps": 0.601765, "emos_mean_crps": 0.701085,
"legacy_mean_mae": 0.0, "legacy_mean_mae": 0.0,
"emos_mean_mae": 0.0, "emos_mean_mae": 0.0,
"legacy_bucket_hit_rate": 1.0, "legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 1.0 "emos_bucket_hit_rate": 1.0
}, },
"dallas": { "dallas": {
"samples": 4, "samples": 1,
"legacy_mean_crps": 1.256111, "legacy_mean_crps": 2.173363,
"emos_mean_crps": 0.651425, "emos_mean_crps": 0.701085,
"legacy_mean_mae": 0.0, "legacy_mean_mae": 0.0,
"emos_mean_mae": 0.0, "emos_mean_mae": 0.0,
"legacy_bucket_hit_rate": 1.0, "legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 1.0 "emos_bucket_hit_rate": 1.0
}, },
"hong kong": { "hong kong": {
"samples": 3, "samples": 4,
"legacy_mean_crps": 0.261027, "legacy_mean_crps": 0.29509,
"emos_mean_crps": 0.261027, "emos_mean_crps": 0.387946,
"legacy_mean_mae": 0.1, "legacy_mean_mae": 0.075,
"emos_mean_mae": 0.1, "emos_mean_mae": 0.075,
"legacy_bucket_hit_rate": 1.0, "legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 0.666667 "emos_bucket_hit_rate": 0.75
}, },
"london": { "london": {
"samples": 6, "samples": 2,
"legacy_mean_crps": 2.079624, "legacy_mean_crps": 3.885033,
"emos_mean_crps": 2.079624, "emos_mean_crps": 3.866915,
"legacy_mean_mae": 2.451667, "legacy_mean_mae": 4.135,
"emos_mean_mae": 2.451667, "emos_mean_mae": 4.135,
"legacy_bucket_hit_rate": 0.166667, "legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.166667 "emos_bucket_hit_rate": 0.0
}, },
"lucknow": { "lucknow": {
"samples": 4, "samples": 2,
"legacy_mean_crps": 1.468528, "legacy_mean_crps": 2.487193,
"emos_mean_crps": 1.468528, "emos_mean_crps": 2.342342,
"legacy_mean_mae": 1.6025, "legacy_mean_mae": 3.205,
"emos_mean_mae": 1.6025, "emos_mean_mae": 3.205,
"legacy_bucket_hit_rate": 0.5, "legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.5 "emos_bucket_hit_rate": 0.0
}, },
"madrid": { "madrid": {
"samples": 2, "samples": 2,
"legacy_mean_crps": 6.27726, "legacy_mean_crps": 6.27726,
"emos_mean_crps": 6.27726, "emos_mean_crps": 5.967277,
"legacy_mean_mae": 7.33, "legacy_mean_mae": 7.33,
"emos_mean_mae": 7.33, "emos_mean_mae": 7.33,
"legacy_bucket_hit_rate": 0.0, "legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.0 "emos_bucket_hit_rate": 0.0
}, },
"miami": { "miami": {
"samples": 5, "samples": 2,
"legacy_mean_crps": 11.665378, "legacy_mean_crps": 28.637631,
"emos_mean_crps": 11.665378, "emos_mean_crps": 28.482516,
"legacy_mean_mae": 12.07, "legacy_mean_mae": 30.175,
"emos_mean_mae": 12.07, "emos_mean_mae": 30.175,
"legacy_bucket_hit_rate": 0.6, "legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.6 "emos_bucket_hit_rate": 0.0
}, },
"milan": { "milan": {
"samples": 3, "samples": 3,
"legacy_mean_crps": 4.401392, "legacy_mean_crps": 4.401392,
"emos_mean_crps": 3.928883, "emos_mean_crps": 3.858031,
"legacy_mean_mae": 4.06, "legacy_mean_mae": 4.06,
"emos_mean_mae": 4.06, "emos_mean_mae": 4.06,
"legacy_bucket_hit_rate": 0.666667, "legacy_bucket_hit_rate": 0.666667,
"emos_bucket_hit_rate": 0.666667 "emos_bucket_hit_rate": 0.666667
}, },
"munich": { "munich": {
"samples": 6, "samples": 2,
"legacy_mean_crps": 2.988583, "legacy_mean_crps": 3.145192,
"emos_mean_crps": 2.988583, "emos_mean_crps": 3.011312,
"legacy_mean_mae": 3.143333, "legacy_mean_mae": 3.64,
"emos_mean_mae": 3.143333, "emos_mean_mae": 3.64,
"legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.0
},
"new york": {
"samples": 1,
"legacy_mean_crps": 3.692845,
"emos_mean_crps": 3.407357,
"legacy_mean_mae": 4.94,
"emos_mean_mae": 4.94,
"legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.0
},
"paris": {
"samples": 2,
"legacy_mean_crps": 4.013782,
"emos_mean_crps": 3.979293,
"legacy_mean_mae": 4.265,
"emos_mean_mae": 4.265,
"legacy_bucket_hit_rate": 0.5, "legacy_bucket_hit_rate": 0.5,
"emos_bucket_hit_rate": 0.5 "emos_bucket_hit_rate": 0.5
}, },
"new york": {
"samples": 4,
"legacy_mean_crps": 1.861101,
"emos_mean_crps": 1.409393,
"legacy_mean_mae": 1.3725,
"emos_mean_mae": 1.3725,
"legacy_bucket_hit_rate": 0.75,
"emos_bucket_hit_rate": 0.75
},
"paris": {
"samples": 7,
"legacy_mean_crps": 2.430082,
"emos_mean_crps": 2.430082,
"legacy_mean_mae": 2.518571,
"emos_mean_mae": 2.518571,
"legacy_bucket_hit_rate": 0.571429,
"emos_bucket_hit_rate": 0.571429
},
"sao paulo": { "sao paulo": {
"samples": 5, "samples": 2,
"legacy_mean_crps": 2.454756, "legacy_mean_crps": 5.540967,
"emos_mean_crps": 2.454756, "emos_mean_crps": 5.272063,
"legacy_mean_mae": 2.628, "legacy_mean_mae": 6.57,
"emos_mean_mae": 2.628, "emos_mean_mae": 6.57,
"legacy_bucket_hit_rate": 0.6, "legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.6 "emos_bucket_hit_rate": 0.0
}, },
"seattle": { "seattle": {
"samples": 4, "samples": 1,
"legacy_mean_crps": 0.531656, "legacy_mean_crps": 0.315488,
"emos_mean_crps": 0.452784, "emos_mean_crps": 0.425909,
"legacy_mean_mae": 0.0, "legacy_mean_mae": 0.0,
"emos_mean_mae": 0.0, "emos_mean_mae": 0.0,
"legacy_bucket_hit_rate": 1.0, "legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 1.0 "emos_bucket_hit_rate": 1.0
}, },
"seoul": { "seoul": {
"samples": 6, "samples": 2,
"legacy_mean_crps": 0.328088, "legacy_mean_crps": 0.313754,
"emos_mean_crps": 0.328088, "emos_mean_crps": 0.412831,
"legacy_mean_mae": 0.2, "legacy_mean_mae": 0.15,
"emos_mean_mae": 0.2, "emos_mean_mae": 0.15,
"legacy_bucket_hit_rate": 1.0, "legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 1.0 "emos_bucket_hit_rate": 1.0
}, },
"shanghai": { "shanghai": {
"samples": 2, "samples": 3,
"legacy_mean_crps": 0.250034, "legacy_mean_crps": 0.299116,
"emos_mean_crps": 0.250034, "emos_mean_crps": 0.394855,
"legacy_mean_mae": 0.15, "legacy_mean_mae": 0.1,
"emos_mean_mae": 0.15, "emos_mean_mae": 0.1,
"legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 1.0
},
"shenzhen": {
"samples": 1,
"legacy_mean_crps": 0.063696,
"emos_mean_crps": 0.073016,
"legacy_mean_mae": 0.1,
"emos_mean_mae": 0.1,
"legacy_bucket_hit_rate": 1.0, "legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 1.0 "emos_bucket_hit_rate": 1.0
}, },
"singapore": { "singapore": {
"samples": 2, "samples": 2,
"legacy_mean_crps": 0.281993, "legacy_mean_crps": 0.281993,
"emos_mean_crps": 0.281993, "emos_mean_crps": 0.37264,
"legacy_mean_mae": 0.15, "legacy_mean_mae": 0.15,
"emos_mean_mae": 0.15, "emos_mean_mae": 0.15,
"legacy_bucket_hit_rate": 1.0, "legacy_bucket_hit_rate": 1.0,
@@ -193,16 +202,16 @@
"taipei": { "taipei": {
"samples": 3, "samples": 3,
"legacy_mean_crps": 0.356996, "legacy_mean_crps": 0.356996,
"emos_mean_crps": 0.356996, "emos_mean_crps": 0.472738,
"legacy_mean_mae": 0.1, "legacy_mean_mae": 0.1,
"emos_mean_mae": 0.1, "emos_mean_mae": 0.1,
"legacy_bucket_hit_rate": 1.0, "legacy_bucket_hit_rate": 0.666667,
"emos_bucket_hit_rate": 1.0 "emos_bucket_hit_rate": 0.666667
}, },
"tel aviv": { "tel aviv": {
"samples": 2, "samples": 2,
"legacy_mean_crps": 0.446758, "legacy_mean_crps": 0.446758,
"emos_mean_crps": 0.446758, "emos_mean_crps": 0.578006,
"legacy_mean_mae": 0.3, "legacy_mean_mae": 0.3,
"emos_mean_mae": 0.3, "emos_mean_mae": 0.3,
"legacy_bucket_hit_rate": 1.0, "legacy_bucket_hit_rate": 1.0,
@@ -211,36 +220,36 @@
"tokyo": { "tokyo": {
"samples": 2, "samples": 2,
"legacy_mean_crps": 0.450128, "legacy_mean_crps": 0.450128,
"emos_mean_crps": 0.450128, "emos_mean_crps": 0.582151,
"legacy_mean_mae": 0.25, "legacy_mean_mae": 0.25,
"emos_mean_mae": 0.25, "emos_mean_mae": 0.25,
"legacy_bucket_hit_rate": 0.5, "legacy_bucket_hit_rate": 0.5,
"emos_bucket_hit_rate": 1.0 "emos_bucket_hit_rate": 1.0
}, },
"toronto": { "toronto": {
"samples": 5, "samples": 2,
"legacy_mean_crps": 2.647861, "legacy_mean_crps": 5.497916,
"emos_mean_crps": 2.566068, "emos_mean_crps": 5.240552,
"legacy_mean_mae": 2.532, "legacy_mean_mae": 6.33,
"emos_mean_mae": 2.532, "emos_mean_mae": 6.33,
"legacy_bucket_hit_rate": 0.6, "legacy_bucket_hit_rate": 0.0,
"emos_bucket_hit_rate": 0.6 "emos_bucket_hit_rate": 0.0
}, },
"warsaw": { "warsaw": {
"samples": 3, "samples": 3,
"legacy_mean_crps": 1.618875, "legacy_mean_crps": 1.618875,
"emos_mean_crps": 1.618875, "emos_mean_crps": 1.553232,
"legacy_mean_mae": 2.056667, "legacy_mean_mae": 2.056667,
"emos_mean_mae": 2.056667, "emos_mean_mae": 2.056667,
"legacy_bucket_hit_rate": 0.333333, "legacy_bucket_hit_rate": 0.333333,
"emos_bucket_hit_rate": 0.333333 "emos_bucket_hit_rate": 0.333333
}, },
"wellington": { "wellington": {
"samples": 6, "samples": 2,
"legacy_mean_crps": 0.266349, "legacy_mean_crps": 0.364919,
"emos_mean_crps": 0.266349, "emos_mean_crps": 0.475875,
"legacy_mean_mae": 0.2, "legacy_mean_mae": 0.15,
"emos_mean_mae": 0.2, "emos_mean_mae": 0.15,
"legacy_bucket_hit_rate": 1.0, "legacy_bucket_hit_rate": 1.0,
"emos_bucket_hit_rate": 1.0 "emos_bucket_hit_rate": 1.0
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+5 -8
View File
@@ -29,8 +29,7 @@ flowchart TD
end end
subgraph S["状态与概率"] subgraph S["状态与概率"]
S1["SQLite 第二阶段收口(离线脚本统一数据源)"] S1["EMOS shadow -> primary 门禁稳定化"]
S2["EMOS shadow -> primary 门禁稳定化"]
end end
A --> P A --> P
@@ -48,7 +47,7 @@ flowchart TD
- 钱包绑定支持浏览器钱包 + WalletConnect。 - 钱包绑定支持浏览器钱包 + WalletConnect。
- 账户中心与 Pro 权限展示链路打通。 - 账户中心与 Pro 权限展示链路打通。
- 钱包异动支持独立频道路由。 - 钱包异动支持独立频道路由。
- 运行态状态/缓存已完成 SQLite 主读切换 - 运行态状态/缓存与核心离线训练、评估、回填链路已完成 SQLite 主路径收口
- 轻量可观测性已上线(`/healthz``/api/system/status``/metrics`)。 - 轻量可观测性已上线(`/healthz``/api/system/status``/metrics`)。
- EMOS/CRPS 校准链路已上线 shadow 模式。 - EMOS/CRPS 校准链路已上线 shadow 模式。
@@ -56,7 +55,6 @@ flowchart TD
| 项目 | 影响 | 建议动作 | | 项目 | 影响 | 建议动作 |
| :-- | :-- | :-- | | :-- | :-- | :-- |
| SQLite 第二阶段收口 | 线上主链路已切到 `sqlite`,但离线脚本仍有文件默认输入 | 训练、回填、报表脚本统一改成 SQLite 优先,文件参数仅保留显式回退 |
| EMOS 上线门禁 | 当前 `hold`,不能切 primary | 继续积累样本,重点压 `bucket_brier` | | EMOS 上线门禁 | 当前 `hold`,不能切 primary | 继续积累样本,重点压 `bucket_brier` |
| 外部监控与告警 | 只有轻量指标,无外部抓取 | 接 Prometheus/Grafana 或最小巡检 | | 外部监控与告警 | 只有轻量指标,无外部抓取 | 接 Prometheus/Grafana 或最小巡检 |
| 退款与售后链路 | 商业闭环不完整 | 增加退款状态机与工单系统 | | 退款与售后链路 | 商业闭环不完整 | 增加退款状态机与工单系统 |
@@ -78,7 +76,6 @@ flowchart TD
## 6. 下阶段里程碑 ## 6. 下阶段里程碑
1. 完成 SQLite 第二阶段收口,统一离线脚本与训练输入到 SQLite 优先 1. 稳定 EMOS shadow,达到 rollout `observe/promote` 条件
2. 稳定 EMOS shadow,达到 rollout `observe/promote` 条件 2. 补外部监控抓取与告警阈值
3. 补外部监控抓取与告警阈值 3. 评估并推进支付合约 V2 升级
4. 评估并推进支付合约 V2 升级。
+5 -8
View File
@@ -29,8 +29,7 @@ flowchart TD
end end
subgraph S["状态与概率"] subgraph S["状态与概率"]
S1["SQLite 第二阶段收口(离线脚本统一数据源)"] S1["EMOS shadow -> primary 门禁稳定化"]
S2["EMOS shadow -> primary 门禁稳定化"]
end end
A --> P A --> P
@@ -48,7 +47,7 @@ flowchart TD
- 钱包绑定支持浏览器钱包 + WalletConnect。 - 钱包绑定支持浏览器钱包 + WalletConnect。
- 账户中心与 Pro 权限展示链路打通。 - 账户中心与 Pro 权限展示链路打通。
- 钱包异动支持独立频道路由。 - 钱包异动支持独立频道路由。
- 运行态状态/缓存已完成 SQLite 主读切换 - 运行态状态/缓存与核心离线训练、评估、回填链路已完成 SQLite 主路径收口
- 轻量可观测性已上线(`/healthz``/api/system/status``/metrics`)。 - 轻量可观测性已上线(`/healthz``/api/system/status``/metrics`)。
- EMOS/CRPS 校准链路已上线 shadow 模式。 - EMOS/CRPS 校准链路已上线 shadow 模式。
@@ -56,7 +55,6 @@ flowchart TD
| 项目 | 影响 | 建议动作 | | 项目 | 影响 | 建议动作 |
| :-- | :-- | :-- | | :-- | :-- | :-- |
| SQLite 第二阶段收口 | 线上主链路已切到 `sqlite`,但离线脚本仍有文件默认输入 | 训练、回填、报表脚本统一改成 SQLite 优先,文件参数仅保留显式回退 |
| EMOS 上线门禁 | 当前 `hold`,不能切 primary | 继续积累样本,重点压 `bucket_brier` | | EMOS 上线门禁 | 当前 `hold`,不能切 primary | 继续积累样本,重点压 `bucket_brier` |
| 外部监控与告警 | 只有轻量指标,无外部抓取 | 接 Prometheus/Grafana 或最小巡检 | | 外部监控与告警 | 只有轻量指标,无外部抓取 | 接 Prometheus/Grafana 或最小巡检 |
| 退款与售后链路 | 商业闭环不完整 | 增加退款状态机与工单系统 | | 退款与售后链路 | 商业闭环不完整 | 增加退款状态机与工单系统 |
@@ -78,7 +76,6 @@ flowchart TD
## 6. 下阶段里程碑 ## 6. 下阶段里程碑
1. 完成 SQLite 第二阶段收口,统一离线脚本与训练输入到 SQLite 优先 1. 稳定 EMOS shadow,达到 rollout `observe/promote` 条件
2. 稳定 EMOS shadow,达到 rollout `observe/promote` 条件 2. 补外部监控抓取与告警阈值
3. 补外部监控抓取与告警阈值 3. 评估并推进支付合约 V2 升级
4. 评估并推进支付合约 V2 升级。
+5 -6
View File
@@ -3,9 +3,9 @@
## 执行摘要 ## 执行摘要
PolyWeather(仓库:`yangyuan-zhen/PolyWeather`)定位为**面向温度类结算预测市场(如 Polymarket 的温度结算合约)**的“生产级气象情报系统”,核心在于把多源天气观测/预报转化为**结算导向的概率桶(μ + bucket distribution**,并进一步映射到市场报价完成**错价扫描**;同时提供 Web 仪表盘与 Telegram Bot 两套交互入口,并包含 Polygon 链上 USDC/USDC.e 支付、自动补单与订阅/积分体系。项目 README 现明确仓库代码采用 `AGPL-3.0-only`,同时将品牌、商标、生产私有数据与运营阈值保留在代码许可证之外。 PolyWeather(仓库:`yangyuan-zhen/PolyWeather`)定位为**面向温度类结算预测市场(如 Polymarket 的温度结算合约)**的“生产级气象情报系统”,核心在于把多源天气观测/预报转化为**结算导向的概率桶(μ + bucket distribution**,并进一步映射到市场报价完成**错价扫描**;同时提供 Web 仪表盘与 Telegram Bot 两套交互入口,并包含 Polygon 链上 USDC/USDC.e 支付、自动补单与订阅/积分体系。项目 README 现明确仓库代码采用 `AGPL-3.0-only`,同时将品牌、商标、生产私有数据与运营阈值保留在代码许可证之外。
从工程实现看,截至 `2026-03-21`,项目已经完成一轮明确的工程化收口:多源天气采集仍保持现有业务能力,同时已完成采集层与 Web API 大文件拆分、CI 质量门禁、配置分级(`.env.example` / `.env.secrets.example` / 中文部署文档)、EMOS/CRPS 校准链路、运行态状态与缓存 SQLite 的渐进迁移,以及基础可观测性接口(`/healthz``/api/system/status``/metrics`)。 从工程实现看,截至 `2026-04-02`,项目已经完成一轮明确的工程化收口:多源天气采集仍保持现有业务能力,同时已完成采集层与 Web API 大文件拆分、CI 质量门禁、配置分级(`.env.example` / `.env.secrets.example` / 中文部署文档)、EMOS/CRPS 校准链路、运行态状态与缓存迁移到 SQLite 主路径,以及基础可观测性接口(`/healthz``/api/system/status``/metrics`)。
这意味着报告里最初最突出的“工程地基缺失”问题,已经有一部分被关闭:`src/data_collection/weather_sources.py``web/app.py` 不再是原来的超大单文件;GitHub Actions 已覆盖 Python、前端和 Docker build;配置与密钥治理已成体系;运行态状态不再只能依赖 JSON/JSONL 文件;EMOS 也不再只是概念,而是进入了可训练、可评估、可 shadow、可门禁判断的阶段。 这意味着报告里最初最突出的“工程地基缺失”问题,已经有一部分被关闭:`src/data_collection/weather_sources.py``web/app.py` 不再是原来的超大单文件;GitHub Actions 已覆盖 Python、前端和 Docker build;配置与密钥治理已成体系;运行态状态不再只能依赖 JSON/JSONL 文件;EMOS 也不再只是概念,而是进入了可训练、可评估、可 shadow、可门禁判断的阶段。
但项目仍处在“从可用走向稳态”的中段,而不是终局。当前真正的高优先级问题已收敛为类:第一,**运行态 SQLite 已经完成主读切换,但离线训练/回填脚本仍存在 legacy JSON/JSONL 默认输入**,需要继续把数据科学与运维脚本统一到单一数据源;第二,**可观测性只完成了轻量级指标层**,还没有形成完整的外部监控、阈值告警与趋势面板;第,**EMOS 仍未达到生产切换标准**,当前门禁结论明确为 `hold`,阻塞原因是 shadow bucket brier 明显退化。支付链路方面,链下审计与容灾已明显增强:事件重放、SQLite 审计事件、RPC 多节点容灾、合约静态检查、`/ops` 支付异常单、按邮箱恢复脚本都已补齐;当前剩余风险主要集中在**链上合约本身仍是最小实现**,尚未升级到 SafeERC20、Pausable、链上套餐绑定等更强防护版本。 但项目仍处在“从可用走向稳态”的中段,而不是终局。当前真正的高优先级问题已收敛为类:第一,**可观测性只完成了轻量级指标层**,还没有形成完整的外部监控、阈值告警与趋势面板;第,**EMOS 仍未达到生产切换标准**,当前门禁结论明确为 `hold`,阻塞原因是 shadow bucket brier 明显退化。SQLite 迁移方面,运行态主读切换和核心离线训练/回填链路已经完成验收:在移除 `data/*.json` / `data/*.jsonl` 后,训练、评估、shadow report 与关键 backfill 脚本仍可仅依赖运行时数据库正常执行;当前保留的 legacy 文件路径主要用于迁移、导出、校验和显式回退输入。支付链路方面,链下审计与容灾已明显增强:事件重放、SQLite 审计事件、RPC 多节点容灾、合约静态检查、`/ops` 支付异常单、按邮箱恢复脚本都已补齐;当前剩余风险主要集中在**链上合约本身仍是最小实现**,尚未升级到 SafeERC20、Pausable、链上套餐绑定等更强防护版本。
因此,当前阶段最正确的策略已经不是继续做“大范围基础重构”,而是围绕**迁移验收、可观测性补全、EMOS 上线门禁稳定化**这三条线持续收口。短中期内更高 ROI 的方向依然不是引入新的大模型,而是把现有“采集→后处理→市场映射→支付/订阅”的链路做成**状态一致、指标可见、发布可控、回退明确**的生产平台。 因此,当前阶段最正确的策略已经不是继续做“大范围基础重构”,而是围绕**迁移验收、可观测性补全、EMOS 上线门禁稳定化**这三条线持续收口。短中期内更高 ROI 的方向依然不是引入新的大模型,而是把现有“采集→后处理→市场映射→支付/订阅”的链路做成**状态一致、指标可见、发布可控、回退明确**的生产平台。
## 项目概览 ## 项目概览
@@ -58,7 +58,7 @@ flowchart TB
WX[WeatherDataCollector] WX[WeatherDataCollector]
CITY[CITY_REGISTRY] CITY[CITY_REGISTRY]
HIST[(SQLite runtime state<br/>daily_records / cache / snapshots)] HIST[(SQLite runtime state<br/>daily_records / cache / snapshots)]
JSON[Legacy JSON files<br/>dual-mode fallback] JSON[Legacy JSON files<br/>migration/export/explicit fallback only]
end end
subgraph ExternalAPIs subgraph ExternalAPIs
@@ -122,7 +122,7 @@ flowchart TB
**推理流水线(在线)** **推理流水线(在线)**
Web/Telegram 请求 → FastAPI 调用采集器抓取/复用缓存 → 分析引擎输出结构化结果(μ、概率桶、趋势、死盘/窗口判定、DEB 预测、市场扫描)→ 前端渲染或 bot 消息格式化。 Web/Telegram 请求 → FastAPI 调用采集器抓取/复用缓存 → 分析引擎输出结构化结果(μ、概率桶、趋势、死盘/窗口判定、DEB 预测、市场扫描)→ 前端渲染或 bot 消息格式化。
**检查点(checkpoints**:传统 ML checkpoint 不适用;但项目现已形成两类“业务状态 checkpoint”: **检查点(checkpoints**:传统 ML checkpoint 不适用;但项目现已形成两类“业务状态 checkpoint”:
(a)SQLite 运行态存储(当前线上主路径);(blegacy JSON/JSONL 文件(主要保留给迁移回滚与部分离线脚本默认输入)。当前设计仍支持 `POLYWEATHER_STATE_STORAGE_MODE=file|dual|sqlite`,但对线上部署而言,推荐目标状态已经是 `sqlite` (a)SQLite 运行态存储(当前线上与核心离线链路主路径);(blegacy JSON/JSONL 文件(主要保留给迁移回滚、导出比对与显式回退输入)。当前设计仍支持 `POLYWEATHER_STATE_STORAGE_MODE=file|dual|sqlite`,但对线上部署与离线训练/回填而言,推荐目标状态已经是 `sqlite`
### 测试、CI/CD 与运维验证 ### 测试、CI/CD 与运维验证
**测试**:仓库存在 `tests/test_trend_engine.py`,覆盖 μ 计算、死盘判定、预报崩盘提示、趋势方向等核心逻辑(通过 patch 隔离外部依赖)。 **测试**:仓库存在 `tests/test_trend_engine.py`,覆盖 μ 计算、死盘判定、预报崩盘提示、趋势方向等核心逻辑(通过 patch 隔离外部依赖)。
@@ -142,7 +142,7 @@ Web/Telegram 请求 → FastAPI 调用采集器抓取/复用缓存 → 分析引
**核心文件过大问题已明显缓解,但边界仍需继续稳定**`WeatherDataCollector``web/app.py` 的超大文件问题已完成第一阶段拆分;当前风险已从“文件过大”转为“跨模块兼容与边界稳定性”,例如旧调用路径、兼容导出、跨层 helper 仍需持续清理。 **核心文件过大问题已明显缓解,但边界仍需继续稳定**`WeatherDataCollector``web/app.py` 的超大文件问题已完成第一阶段拆分;当前风险已从“文件过大”转为“跨模块兼容与边界稳定性”,例如旧调用路径、兼容导出、跨层 helper 仍需持续清理。
**可复现性已从“缺模板”进入“模板与生产对齐”的阶段**`.env.example``.env.secrets.example`、中文配置文档、前端部署文档、运行时配置校验器都已存在;当前风险主要在于线上历史 `.env` 与新模板并存、旧变量命名残留、以及密钥轮换与分层是否真正落实。 **可复现性已从“缺模板”进入“模板与生产对齐”的阶段**`.env.example``.env.secrets.example`、中文配置文档、前端部署文档、运行时配置校验器都已存在;当前风险主要在于线上历史 `.env` 与新模板并存、旧变量命名残留、以及密钥轮换与分层是否真正落实。
**CI 已建立,但组织级质量门禁未必完全收口**:CI 现已覆盖 Python、前端与 Docker build。当前问题不再是“缺 CI”,而是是否把这些 status check 绑定到 `main` 保护策略,以及是否逐步引入更严格的 pre-merge 审查。 **CI 已建立,但组织级质量门禁未必完全收口**:CI 现已覆盖 Python、前端与 Docker build。当前问题不再是“缺 CI”,而是是否把这些 status check 绑定到 `main` 保护策略,以及是否逐步引入更严格的 pre-merge 审查。
**运行态状态/缓存迁移已完成主切换,但离线脚本仍待收口**`daily_records``telegram_alert_state``probability_training_snapshots``open_meteo` 缓存已经支持并在生产中主读 SQLite,迁移/校验脚本也已验证可用;当前残留问题不再是线上是否能切,而是部分训练、回填、报表脚本仍默认读取 `data/*.json` / `data/*.jsonl`,容易形成“线上一套数据、离线一套输入”的维护成本 **运行态状态/缓存与核心离线链路的 SQLite 收口已完成**`daily_records``telegram_alert_state``probability_training_snapshots``open_meteo` 缓存已经支持并在生产中主读 SQLite,迁移/校验脚本可用;进一步地,在临时移除 `data/*.json` / `data/*.jsonl` 后,训练集导出、概率拟合、评估报告、shadow report 和关键 backfill 脚本已验证仍可运行。当前 legacy 文件路径主要是显式回退入口,而不再是默认主输入
**第三方服务合规与稳定性风险** **第三方服务合规与稳定性风险**
项目强依赖外部 APIOpen-Meteo、AviationWeather、NWS、HKO、CWA、Polymarket、Supabase)。其中 AviationWeather Data API 有明确速率限制;Polymarket 官方说明 Gamma/Data/CLOB 三套 API 分属不同域,CLOB 交易端点需鉴权且策略可能变化;Supabase 明确强调 `service_role`/secret keys 绝不可暴露。若缺乏集中治理(重试/退避/熔断/降级/配额监控/密钥轮换),稳定性与合规不可控。 项目强依赖外部 APIOpen-Meteo、AviationWeather、NWS、HKO、CWA、Polymarket、Supabase)。其中 AviationWeather Data API 有明确速率限制;Polymarket 官方说明 Gamma/Data/CLOB 三套 API 分属不同域,CLOB 交易端点需鉴权且策略可能变化;Supabase 明确强调 `service_role`/secret keys 绝不可暴露。若缺乏集中治理(重试/退避/熔断/降级/配额监控/密钥轮换),稳定性与合规不可控。
**可观测性已起步,但仍不构成完整监控体系**:项目现在已有 `/healthz``/api/system/status``/metrics`,并为 HTTP 与关键第三方源增加了轻量指标;但仍缺少 Prometheus/Grafana 级别的外部抓取、告警阈值、趋势面板和运行日报。这部分现在属于“已开始,不算完成”。 **可观测性已起步,但仍不构成完整监控体系**:项目现在已有 `/healthz``/api/system/status``/metrics`,并为 HTTP 与关键第三方源增加了轻量指标;但仍缺少 Prometheus/Grafana 级别的外部抓取、告警阈值、趋势面板和运行日报。这部分现在属于“已开始,不算完成”。
@@ -172,7 +172,6 @@ Web/Telegram 请求 → FastAPI 调用采集器抓取/复用缓存 → 分析引
下表按截至 `2026-03-21` 的真实状态重排优先级。已完成项不再继续列为“待做”,只保留当前仍需推进的事项。 下表按截至 `2026-03-21` 的真实状态重排优先级。已完成项不再继续列为“待做”,只保留当前仍需推进的事项。
| 优先级 | 改进项 | 预估工作量 | 主要收益 | 主要风险 | 可执行步骤(建议顺序) | | 优先级 | 改进项 | 预估工作量 | 主要收益 | 主要风险 | 可执行步骤(建议顺序) |
| ------ | --------------------------------------------------------------------------------------------------------------------------------- | -------------------: | ------------------------------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------ | --------------------------------------------------------------------------------------------------------------------------------- | -------------------: | ------------------------------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 高 | **完成 SQLite 迁移的“第二阶段收口”**:把离线脚本、训练与回填链路统一成 SQLite 优先 | 2–5 天 | 彻底形成单一数据源,避免线上 SQLite 与离线 JSON/JSONL 双轨漂移 | 训练脚本行为变化可能影响既有报表或回填输出 | 1) 盘点所有默认读取 `data/*.json` / `data/*.jsonl` 的脚本 → 2) 改为按 `state_storage_mode` 自动优先读 SQLite → 3) 保留显式 `--history-file/--snapshot-file` 作为回退输入 → 4) 用同一批数据对比训练与报表结果 |
| 高 | **把轻量可观测性接入外部监控与告警**:围绕 `/metrics` 建立抓取、阈值与巡检 | 3–7 天 | 不再只靠日志定位问题;可以监控第三方源错误率、缓存命中与 HTTP 延迟 | 指标不分层会导致噪音高、告警无用 | 1) 抓取 `/metrics` → 2) 先围绕 HTTP、Open-Meteo、MGM、METAR 建立最小仪表板 → 3) 为 429/403/error/stale_cache 设阈值 → 4) 增加巡检脚本或告警通道 | | 高 | **把轻量可观测性接入外部监控与告警**:围绕 `/metrics` 建立抓取、阈值与巡检 | 3–7 天 | 不再只靠日志定位问题;可以监控第三方源错误率、缓存命中与 HTTP 延迟 | 指标不分层会导致噪音高、告警无用 | 1) 抓取 `/metrics` → 2) 先围绕 HTTP、Open-Meteo、MGM、METAR 建立最小仪表板 → 3) 为 429/403/error/stale_cache 设阈值 → 4) 增加巡检脚本或告警通道 |
| 高 | **稳定 EMOS shadow 并收紧上线门禁** | 1–2 周 | 让概率引擎升级具备明确发布条件,避免拍脑袋切换 | 当前 shadow bucket brier 退化明显,存在误上线风险 | 1) 持续积累 snapshot 样本 → 2) 定期重训与生成 `evaluation_report` / `shadow_report` / `rollout_report` → 3) 重点压 `bucket_brier` 退化 → 4) 只有门禁从 `hold` 进入 `observe/promote` 后才考虑上线 | | 高 | **稳定 EMOS shadow 并收紧上线门禁** | 1–2 周 | 让概率引擎升级具备明确发布条件,避免拍脑袋切换 | 当前 shadow bucket brier 退化明显,存在误上线风险 | 1) 持续积累 snapshot 样本 → 2) 定期重训与生成 `evaluation_report` / `shadow_report` / `rollout_report` → 3) 重点压 `bucket_brier` 退化 → 4) 只有门禁从 `hold` 进入 `observe/promote` 后才考虑上线 |
| 中 | **市场层升级为 async + 类型安全**:引入 `aiopolymarket` 或在现有层加重试/backoff/连接池 | 4–7 天 | 行情层更稳,减少短时网络抖动;更易扩展更多市场/分页 | 依赖升级带来的行为差异 | 1) 把 requests.Session 替换为 aiohttp/httpx → 2) 在 Gamma/CLOB 调用侧实现指数退避 → 3) 引入 typed models,减少解析失败 | | 中 | **市场层升级为 async + 类型安全**:引入 `aiopolymarket` 或在现有层加重试/backoff/连接池 | 4–7 天 | 行情层更稳,减少短时网络抖动;更易扩展更多市场/分页 | 依赖升级带来的行为差异 | 1) 把 requests.Session 替换为 aiohttp/httpx → 2) 在 Gamma/CLOB 调用侧实现指数退避 → 3) 引入 typed models,减少解析失败 |
+7 -6
View File
@@ -16,6 +16,7 @@ from src.analysis.probability_snapshot_archive import ( # noqa: E402
load_snapshot_rows_for_day, load_snapshot_rows_for_day,
) )
from src.database.runtime_state import STATE_STORAGE_FILE, get_state_storage_mode # noqa: E402 from src.database.runtime_state import STATE_STORAGE_FILE, get_state_storage_mode # noqa: E402
from scripts.fit_probability_calibration import _default_history_arg # noqa: E402
def _load_daily_records(path: Path) -> Dict[str, Dict[str, Dict[str, Any]]]: def _load_daily_records(path: Path) -> Dict[str, Dict[str, Dict[str, Any]]]:
@@ -53,8 +54,8 @@ def main() -> int:
) )
parser.add_argument( parser.add_argument(
"--history-file", "--history-file",
default=str(Path("data") / "daily_records.json"), default=_default_history_arg(),
help="Path to daily_records.json", help="Optional legacy daily_records.json path. In sqlite mode this defaults to the runtime database.",
) )
parser.add_argument("--city", help="Optional city filter, e.g. ankara") parser.add_argument("--city", help="Optional city filter, e.g. ankara")
parser.add_argument("--date", help="Optional YYYY-MM-DD filter") parser.add_argument("--date", help="Optional YYYY-MM-DD filter")
@@ -70,8 +71,8 @@ def main() -> int:
) )
args = parser.parse_args() args = parser.parse_args()
history_path = Path(args.history_file) history_path = Path(args.history_file) if args.history_file else None
data = _load_daily_records(history_path) data = _load_daily_records(history_path or Path())
model_name = str(args.model or "").strip() model_name = str(args.model or "").strip()
city_filter = str(args.city or "").strip().lower() or None city_filter = str(args.city or "").strip().lower() or None
date_filter = str(args.date or "").strip() or None date_filter = str(args.date or "").strip() or None
@@ -133,8 +134,8 @@ def main() -> int:
if changed: if changed:
previous_mode = get_state_storage_mode() previous_mode = get_state_storage_mode()
# Reuse existing save path semantics. In sqlite-only mode, save_history would skip file write. # Reuse existing save path semantics. In sqlite-only mode, save_history would skip file write.
save_history(str(history_path), data) save_history(str(history_path or ""), data)
if previous_mode == STATE_STORAGE_FILE and not history_path.exists(): if previous_mode == STATE_STORAGE_FILE and (history_path is None or not history_path.exists()):
raise FileNotFoundError(history_path) raise FileNotFoundError(history_path)
return 0 return 0
@@ -13,6 +13,7 @@ from src.analysis.probability_calibration import ( # noqa: E402
apply_probability_calibration, apply_probability_calibration,
build_probability_features, build_probability_features,
) )
from scripts.fit_probability_calibration import _default_history_arg # noqa: E402
def _sample_to_features(sample): def _sample_to_features(sample):
@@ -44,7 +45,7 @@ def main():
parser = argparse.ArgumentParser(description="Backfill shadow probability snapshots into daily records.") parser = argparse.ArgumentParser(description="Backfill shadow probability snapshots into daily records.")
parser.add_argument( parser.add_argument(
"--history-file", "--history-file",
default=os.path.join(PROJECT_ROOT, "data", "daily_records.json"), default=_default_history_arg(),
) )
parser.add_argument( parser.add_argument(
"--training-samples", "--training-samples",
@@ -9,6 +9,7 @@ if PROJECT_ROOT not in sys.path:
from src.analysis.deb_algorithm import load_history, reconcile_recent_actual_highs, save_history # noqa: E402 from src.analysis.deb_algorithm import load_history, reconcile_recent_actual_highs, save_history # noqa: E402
from src.data_collection.city_registry import CITY_REGISTRY # noqa: E402 from src.data_collection.city_registry import CITY_REGISTRY # noqa: E402
from scripts.fit_probability_calibration import _default_history_arg # noqa: E402
def _target_dates(city_info: dict, lookback_days: int) -> list[str]: def _target_dates(city_info: dict, lookback_days: int) -> list[str]:
@@ -50,7 +51,7 @@ def main() -> None:
) )
args = parser.parse_args() args = parser.parse_args()
history_file = os.path.join(PROJECT_ROOT, "data", "daily_records.json") history_file = _default_history_arg() or ""
data = load_history(history_file) data = load_history(history_file)
selected = {str(item).strip().lower() for item in args.cities if str(item).strip()} selected = {str(item).strip().lower() for item in args.cities if str(item).strip()}
+2 -1
View File
@@ -11,6 +11,7 @@ if PROJECT_ROOT not in sys.path:
from src.analysis.deb_algorithm import load_history # noqa: E402 from src.analysis.deb_algorithm import load_history # noqa: E402
from src.analysis.settlement_rounding import apply_city_settlement # noqa: E402 from src.analysis.settlement_rounding import apply_city_settlement # noqa: E402
from scripts.fit_probability_calibration import _default_history_arg # noqa: E402
def _sf(value): def _sf(value):
@@ -112,7 +113,7 @@ def main():
parser = argparse.ArgumentParser(description="Build live shadow probability report from daily records.") parser = argparse.ArgumentParser(description="Build live shadow probability report from daily records.")
parser.add_argument( parser.add_argument(
"--history-file", "--history-file",
default=os.path.join(PROJECT_ROOT, "data", "daily_records.json"), default=_default_history_arg(),
) )
parser.add_argument( parser.add_argument(
"--output", "--output",
+2 -1
View File
@@ -16,6 +16,7 @@ from src.analysis.probability_calibration import ( # noqa: E402
) )
from src.analysis.settlement_rounding import apply_city_settlement # noqa: E402 from src.analysis.settlement_rounding import apply_city_settlement # noqa: E402
from scripts.fit_probability_calibration import ( # noqa: E402 from scripts.fit_probability_calibration import ( # noqa: E402
_default_history_arg,
_extract_samples, _extract_samples,
_load_json_if_exists, _load_json_if_exists,
) )
@@ -61,7 +62,7 @@ def main():
parser = argparse.ArgumentParser(description="Evaluate legacy vs EMOS probability calibration.") parser = argparse.ArgumentParser(description="Evaluate legacy vs EMOS probability calibration.")
parser.add_argument( parser.add_argument(
"--history-file", "--history-file",
default=os.path.join(PROJECT_ROOT, "data", "daily_records.json"), default=_default_history_arg(),
) )
parser.add_argument( parser.add_argument(
"--settlement-history", "--settlement-history",
@@ -8,6 +8,8 @@ if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT) sys.path.insert(0, PROJECT_ROOT)
from scripts.fit_probability_calibration import ( # noqa: E402 from scripts.fit_probability_calibration import ( # noqa: E402
_default_history_arg,
_default_snapshot_arg,
_extract_samples, _extract_samples,
_load_history_with_fallback, _load_history_with_fallback,
_load_json_if_exists, _load_json_if_exists,
@@ -19,7 +21,7 @@ def main():
parser = argparse.ArgumentParser(description="Export normalized probability calibration training samples.") parser = argparse.ArgumentParser(description="Export normalized probability calibration training samples.")
parser.add_argument( parser.add_argument(
"--history-file", "--history-file",
default=os.path.join(PROJECT_ROOT, "data", "daily_records.json"), default=_default_history_arg(),
) )
parser.add_argument( parser.add_argument(
"--settlement-history", "--settlement-history",
@@ -41,7 +43,7 @@ def main():
) )
parser.add_argument( parser.add_argument(
"--snapshot-file", "--snapshot-file",
default=os.path.join(PROJECT_ROOT, "data", "probability_training_snapshots.jsonl"), default=_default_snapshot_arg(),
) )
args = parser.parse_args() args = parser.parse_args()
+21 -4
View File
@@ -16,6 +16,7 @@ from src.analysis.probability_calibration import ( # noqa: E402
from src.analysis.deb_algorithm import load_history # noqa: E402 from src.analysis.deb_algorithm import load_history # noqa: E402
from src.database.runtime_state import ( # noqa: E402 from src.database.runtime_state import ( # noqa: E402
ProbabilitySnapshotRepository, ProbabilitySnapshotRepository,
STATE_STORAGE_FILE,
STATE_STORAGE_SQLITE, STATE_STORAGE_SQLITE,
get_state_storage_mode, get_state_storage_mode,
) )
@@ -38,6 +39,22 @@ def _load_json_if_exists(path):
return data if isinstance(data, dict) else {} return data if isinstance(data, dict) else {}
def _legacy_history_path():
return os.path.join(PROJECT_ROOT, "data", "daily_records.json")
def _legacy_snapshot_path():
return os.path.join(PROJECT_ROOT, "data", "probability_training_snapshots.jsonl")
def _default_history_arg():
return _legacy_history_path() if get_state_storage_mode() == STATE_STORAGE_FILE else None
def _default_snapshot_arg():
return _legacy_snapshot_path() if get_state_storage_mode() == STATE_STORAGE_FILE else None
def _load_history_with_fallback(path): def _load_history_with_fallback(path):
data = load_history(path) data = load_history(path)
if data: if data:
@@ -253,8 +270,8 @@ def main():
parser = argparse.ArgumentParser(description="Fit PolyWeather probability calibration parameters.") parser = argparse.ArgumentParser(description="Fit PolyWeather probability calibration parameters.")
parser.add_argument( parser.add_argument(
"--history-file", "--history-file",
default=os.path.join(PROJECT_ROOT, "data", "daily_records.json"), default=_default_history_arg(),
help="Path to the historical daily_records.json file.", help="Optional legacy daily_records.json path. In sqlite mode this defaults to the runtime database.",
) )
parser.add_argument( parser.add_argument(
"--output", "--output",
@@ -273,8 +290,8 @@ def main():
) )
parser.add_argument( parser.add_argument(
"--snapshot-file", "--snapshot-file",
default=os.path.join(PROJECT_ROOT, "data", "probability_training_snapshots.jsonl"), default=_default_snapshot_arg(),
help="Optional JSONL file with archived probability snapshots.", help="Optional legacy JSONL snapshot archive path. In sqlite mode this defaults to the runtime database.",
) )
parser.add_argument( parser.add_argument(
"--version", "--version",
+6 -1
View File
@@ -4,13 +4,18 @@ import os
from datetime import datetime as _dt from datetime import datetime as _dt
from datetime import timedelta as _td from datetime import timedelta as _td
from src.database.runtime_state import STATE_STORAGE_FILE, get_state_storage_mode
class DebAnalysisService: class DebAnalysisService:
"""DEB analytics adapter with lazy imports to trim cold startup.""" """DEB analytics adapter with lazy imports to trim cold startup."""
def __init__(self, project_root: str): def __init__(self, project_root: str):
self.project_root = project_root self.project_root = project_root
self.history_file = os.path.join(project_root, "data", "daily_records.json") if get_state_storage_mode() == STATE_STORAGE_FILE:
self.history_file = os.path.join(project_root, "data", "daily_records.json")
else:
self.history_file = ""
@staticmethod @staticmethod
def _load_aliases() -> dict[str, str]: def _load_aliases() -> dict[str, str]:
+38 -4
View File
@@ -8,6 +8,13 @@ from typing import Any, Dict, List, Optional, Tuple
from src.analysis.deb_algorithm import load_history from src.analysis.deb_algorithm import load_history
from src.data_collection.city_registry import ALIASES from src.data_collection.city_registry import ALIASES
from src.database.runtime_state import (
DailyRecordRepository,
ProbabilitySnapshotRepository,
STATE_STORAGE_FILE,
STATE_STORAGE_SQLITE,
get_state_storage_mode,
)
BASE_MODEL_COLUMNS: List[Tuple[str, str]] = [ BASE_MODEL_COLUMNS: List[Tuple[str, str]] = [
@@ -82,12 +89,16 @@ def _parse_timestamp(value: Any) -> Optional[datetime]:
def _history_file_path() -> str: def _history_file_path() -> str:
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
return os.path.join(root, "data", "daily_records.json") if get_state_storage_mode() == STATE_STORAGE_FILE:
return os.path.join(root, "data", "daily_records.json")
return ""
def _snapshot_archive_path() -> str: def _snapshot_archive_path() -> str:
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
return os.path.join(root, "data", "probability_training_snapshots.jsonl") if get_state_storage_mode() == STATE_STORAGE_FILE:
return os.path.join(root, "data", "probability_training_snapshots.jsonl")
return ""
def _normalized_city_key(city_name: str) -> str: def _normalized_city_key(city_name: str) -> str:
@@ -125,8 +136,26 @@ def _compute_model_summary(features: Dict[str, Optional[float]]) -> Tuple[Option
def load_snapshot_index(archive_path: Optional[str] = None) -> Dict[Tuple[str, str], Dict[str, Any]]: def load_snapshot_index(archive_path: Optional[str] = None) -> Dict[Tuple[str, str], Dict[str, Any]]:
mode = get_state_storage_mode()
if mode == STATE_STORAGE_SQLITE:
latest_rows: Dict[Tuple[str, str], Dict[str, Any]] = {}
for row in ProbabilitySnapshotRepository().load_all_rows():
if not isinstance(row, dict):
continue
city = _normalized_city_key(str(row.get("city") or ""))
date_str = str(row.get("date") or "").strip()
if not city or not date_str:
continue
key = (city, date_str)
current_best = latest_rows.get(key)
if current_best is None or str(row.get("timestamp") or "") >= str(
current_best.get("timestamp") or ""
):
latest_rows[key] = row
return latest_rows
path = archive_path or _snapshot_archive_path() path = archive_path or _snapshot_archive_path()
if not os.path.exists(path): if not path or not os.path.exists(path):
return {} return {}
latest_rows: Dict[Tuple[str, str], Dict[str, Any]] = {} latest_rows: Dict[Tuple[str, str], Dict[str, Any]] = {}
@@ -258,7 +287,12 @@ def build_training_samples(
history_data: Optional[Dict[str, Any]] = None, history_data: Optional[Dict[str, Any]] = None,
snapshot_index: Optional[Dict[Tuple[str, str], Dict[str, Any]]] = None, snapshot_index: Optional[Dict[Tuple[str, str], Dict[str, Any]]] = None,
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
data = history_data if isinstance(history_data, dict) else load_history(_history_file_path()) if isinstance(history_data, dict):
data = history_data
elif get_state_storage_mode() == STATE_STORAGE_SQLITE:
data = DailyRecordRepository().load_all()
else:
data = load_history(_history_file_path())
snapshots = snapshot_index if isinstance(snapshot_index, dict) else load_snapshot_index() snapshots = snapshot_index if isinstance(snapshot_index, dict) else load_snapshot_index()
samples: List[Dict[str, Any]] = [] samples: List[Dict[str, Any]] = []