Finalize SQLite defaults and refresh calibration artifacts
This commit is contained in:
+1
-1
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
## 运维验收
|
## 运维验收
|
||||||
|
|||||||
@@ -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
@@ -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 升级。
|
|
||||||
|
|||||||
@@ -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 升级。
|
|
||||||
|
|||||||
@@ -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 运行态存储(当前线上主路径);(b)legacy JSON/JSONL 文件(主要保留给迁移回滚与部分离线脚本默认输入)。当前设计仍支持 `POLYWEATHER_STATE_STORAGE_MODE=file|dual|sqlite`,但对线上部署而言,推荐目标状态已经是 `sqlite`。
|
(a)SQLite 运行态存储(当前线上与核心离线链路主路径);(b)legacy 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 文件路径主要是显式回退入口,而不再是默认主输入。
|
||||||
**第三方服务合规与稳定性风险**:
|
**第三方服务合规与稳定性风险**:
|
||||||
项目强依赖外部 API(Open-Meteo、AviationWeather、NWS、HKO、CWA、Polymarket、Supabase)。其中 AviationWeather Data API 有明确速率限制;Polymarket 官方说明 Gamma/Data/CLOB 三套 API 分属不同域,CLOB 交易端点需鉴权且策略可能变化;Supabase 明确强调 `service_role`/secret keys 绝不可暴露。若缺乏集中治理(重试/退避/熔断/降级/配额监控/密钥轮换),稳定性与合规不可控。
|
项目强依赖外部 API(Open-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,减少解析失败 |
|
||||||
|
|||||||
@@ -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()}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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]] = []
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user