From dbf10253a86417dd8ed3eb77778120a214bde01c Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Fri, 20 Mar 2026 13:35:34 +0800 Subject: [PATCH] feat: Implement multi-source weather data collection with caching, rate limiting, and disk persistence. --- config/config.yaml | 165 ++++++++++++++----------- src/analysis/deb_algorithm.py | 10 +- src/analysis/market_alert_engine.py | 4 +- src/analysis/settlement_rounding.py | 21 ++++ src/analysis/trend_engine.py | 34 +++-- src/data_collection/city_registry.py | 81 ++++++++++++ src/data_collection/weather_sources.py | 15 +++ 7 files changed, 239 insertions(+), 91 deletions(-) diff --git a/config/config.yaml b/config/config.yaml index 6e3df6ef..68ffe921 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,76 +1,97 @@ -# Weather API Configuration weather: timeout: 30 - -# Target Cities cities: - - id: "london" - city: "London" - country: "UK" - latitude: 51.5074 - longitude: -0.1278 - - id: "paris" - city: "Paris" - country: "France" - latitude: 48.8566 - longitude: 2.3522 - - id: "ankara" - city: "Ankara" - country: "Turkey" - latitude: 39.9334 - longitude: 32.8597 - - id: "new_york" - city: "New York" - country: "USA" - latitude: 40.7128 - longitude: -74.0060 - - id: "chicago" - city: "Chicago" - country: "USA" - latitude: 41.8781 - longitude: -87.6298 - - id: "lucknow" - city: "Lucknow" - country: "India" - latitude: 26.7606 - longitude: 80.8893 - - id: "sao paulo" - city: "São Paulo" - country: "Brazil" - latitude: -23.4356 - longitude: -46.4731 - - id: "munich" - city: "Munich" - country: "Germany" - latitude: 48.3538 - longitude: 11.7861 - - id: "hong_kong" - city: "Hong Kong" - country: "China" - latitude: 22.3080 - longitude: 113.9185 - - id: "shanghai" - city: "Shanghai" - country: "China" - latitude: 31.1434 - longitude: 121.8052 - - id: "singapore" - city: "Singapore" - country: "Singapore" - latitude: 1.3644 - longitude: 103.9915 - - id: "tokyo" - city: "Tokyo" - country: "Japan" - latitude: 35.5523 - longitude: 139.7798 - - id: "tel_aviv" - city: "Tel Aviv" - country: "Israel" - latitude: 32.0114 - longitude: 34.8867 -# Logging +- id: london + city: London + country: UK + latitude: 51.5074 + longitude: -0.1278 +- id: paris + city: Paris + country: France + latitude: 48.8566 + longitude: 2.3522 +- id: ankara + city: Ankara + country: Turkey + latitude: 39.9334 + longitude: 32.8597 +- id: new_york + city: New York + country: USA + latitude: 40.7128 + longitude: -74.006 +- id: chicago + city: Chicago + country: USA + latitude: 41.8781 + longitude: -87.6298 +- id: lucknow + city: Lucknow + country: India + latitude: 26.7606 + longitude: 80.8893 +- id: sao paulo + city: São Paulo + country: Brazil + latitude: -23.4356 + longitude: -46.4731 +- id: munich + city: Munich + country: Germany + latitude: 48.3538 + longitude: 11.7861 +- id: hong_kong + city: Hong Kong + country: China + latitude: 22.308 + longitude: 113.9185 +- id: shanghai + city: Shanghai + country: China + latitude: 31.1434 + longitude: 121.8052 +- id: singapore + city: Singapore + country: Singapore + latitude: 1.3644 + longitude: 103.9915 +- id: tokyo + city: Tokyo + country: Japan + latitude: 35.5523 + longitude: 139.7798 +- id: tel_aviv + city: Tel Aviv + country: Israel + latitude: 32.0114 + longitude: 34.8867 +- id: chengdu + city: Chengdu + country: China + latitude: 30.5785 + longitude: 103.9471 +- id: chongqing + city: Chongqing + country: China + latitude: 29.7196 + longitude: 106.6416 +- id: shenzhen + city: Shenzhen + country: China + latitude: 22.6393 + longitude: 113.8107 +- id: beijing + city: Beijing + country: China + latitude: 40.0801 + longitude: 116.5846 +- id: wuhan + city: Wuhan + country: China + latitude: 30.7838 + longitude: 114.2081 logging: - level: "INFO" - rotation: "10 MB" - retention: "10 days" + level: INFO + rotation: 10 MB + retention: 10 days diff --git a/src/analysis/deb_algorithm.py b/src/analysis/deb_algorithm.py index 4107acfa..35430f10 100644 --- a/src/analysis/deb_algorithm.py +++ b/src/analysis/deb_algorithm.py @@ -2,7 +2,7 @@ import os import json from datetime import datetime, timedelta import requests -from src.analysis.settlement_rounding import wu_round +from src.analysis.settlement_rounding import wu_round, apply_city_settlement, is_exact_settlement_city # Cross-platform file locking import sys @@ -442,8 +442,8 @@ def get_deb_accuracy(city_name): continue total += 1 - deb_wu = wu_round(deb_pred) - actual_wu = wu_round(actual) + deb_wu = apply_city_settlement(city_name, deb_pred) + actual_wu = apply_city_settlement(city_name, actual) if deb_wu == actual_wu: hits += 1 errors.append(abs(deb_pred - actual)) @@ -507,13 +507,13 @@ def get_mu_accuracy(city_name): total += 1 mu_errors.append(abs(mu_val - actual)) - if wu_round(mu_val) == wu_round(actual): + if apply_city_settlement(city_name, mu_val) == apply_city_settlement(city_name, actual): mu_hits += 1 # Brier Score from probability snapshot prob_snap = record.get("prob_snapshot", []) if prob_snap: - actual_wu = wu_round(actual) + actual_wu = apply_city_settlement(city_name, actual) bs = 0.0 for entry in prob_snap: predicted_p = entry.get("p", 0) diff --git a/src/analysis/market_alert_engine.py b/src/analysis/market_alert_engine.py index 2852ac42..f95e6af6 100644 --- a/src/analysis/market_alert_engine.py +++ b/src/analysis/market_alert_engine.py @@ -9,7 +9,7 @@ import re from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Tuple -from src.analysis.settlement_rounding import wu_round +from src.analysis.settlement_rounding import wu_round, apply_city_settlement, is_exact_settlement_city def _sf(v: Any) -> Optional[float]: @@ -716,7 +716,7 @@ def _extract_market_snapshot(city_weather: Dict[str, Any]) -> Dict[str, Any]: market_url = f"https://polymarket.com/market/{slug}" anchor_today_high_c, anchor_model = _extract_multi_model_anchor_high_c(city_weather) - anchor_settlement = wu_round(anchor_today_high_c) + anchor_settlement = apply_city_settlement(city, anchor_today_high_c) forecast_bucket = _pick_bucket_for_forecast( rows=all_bucket_rows, forecast_settlement=anchor_settlement, diff --git a/src/analysis/settlement_rounding.py b/src/analysis/settlement_rounding.py index 9e5e94bb..745819d9 100644 --- a/src/analysis/settlement_rounding.py +++ b/src/analysis/settlement_rounding.py @@ -18,3 +18,24 @@ def wu_round(value: Optional[Number]) -> Optional[int]: return int(math.floor(x + 0.5)) return int(math.ceil(x - 0.5)) + +def is_exact_settlement_city(city: str) -> bool: + """是否为不四舍五入的精确结算城市""" + if not city: + return False + c = str(city).lower().strip() + return c in ["hong kong", "hk", "taipei", "tpe", "臺北", "台北", "香港"] + + +def apply_city_settlement(city: str, value: Optional[Number]) -> Optional[int]: + """ + 根据城市返回最终的结算值: + - 香港/台北: 向下取整 (e.g. 28.9 -> 28) + - 其他: WU 规则四舍五入 + """ + if value is None: + return None + if is_exact_settlement_city(city): + return int(math.floor(float(value))) + return wu_round(value) + diff --git a/src/analysis/trend_engine.py b/src/analysis/trend_engine.py index ba1f610e..17d7a9fb 100644 --- a/src/analysis/trend_engine.py +++ b/src/analysis/trend_engine.py @@ -15,7 +15,7 @@ from src.analysis.deb_algorithm import ( update_daily_record, _is_excluded_model_name, ) -from src.analysis.settlement_rounding import wu_round +from src.analysis.settlement_rounding import wu_round, apply_city_settlement, is_exact_settlement_city from src.data_collection.city_registry import CITY_REGISTRY from src.data_collection.city_risk_profiles import get_city_risk_profile @@ -429,7 +429,7 @@ def analyze_weather_trend( forecast_miss_deg = 0.0 if is_dead_market: - settled_wu = wu_round(max_so_far) if max_so_far is not None else 0 + settled_wu = apply_city_settlement(city_name, max_so_far) if max_so_far is not None else 0 dead_msg = ( f"🎲 结算预测:已锁定 {settled_wu}{temp_symbol} " f"({settlement_source_label} 死盘确认)" @@ -481,7 +481,7 @@ def analyze_weather_trend( # Probability Engine probs_result = calculate_prob_distribution( - mu, sigma, max_so_far, temp_symbol + mu, sigma, max_so_far, temp_symbol, city_name ) mu = probs_result.get("mu", mu) probabilities = probs_result.get("probabilities", []) @@ -514,7 +514,7 @@ def analyze_weather_trend( # === Settlement boundary === if max_so_far is not None: - settled = wu_round(max_so_far) + settled = apply_city_settlement(city_name, max_so_far) fractional = max_so_far - int(max_so_far) dist_to_boundary = abs(fractional - 0.5) if dist_to_boundary <= 0.3: @@ -585,7 +585,7 @@ def analyze_weather_trend( if max_so_far is not None: ai_features.append( f"🏔️ 今日实测最高温: {max_so_far}{temp_symbol} " - f"({settlement_source_label}结算={wu_round(max_so_far)}{temp_symbol})。" + f"({settlement_source_label}结算={apply_city_settlement(city_name, max_so_far)}{temp_symbol})。" ) if city_name: _profile = get_city_risk_profile(city_name) @@ -634,7 +634,7 @@ def analyze_weather_trend( for t, p in sorted_probs[:4] ] elif is_dead_market and max_so_far is not None: - _prob_list = [{"value": wu_round(max_so_far), "probability": 1.0}] + _prob_list = [{"value": apply_city_settlement(city_name, max_so_far), "probability": 1.0}] update_daily_record( city_name, @@ -672,14 +672,14 @@ def analyze_weather_trend( "forecast_miss_deg": forecast_miss_deg, "max_so_far": max_so_far, "cur_temp": cur_temp, - "wu_settle": wu_round(max_so_far) if max_so_far is not None else None, + "wu_settle": apply_city_settlement(city_name, max_so_far) if max_so_far is not None else None, } display_str = "\n".join(insights) if insights else "" return display_str, "\n".join(ai_features), structured def calculate_prob_distribution( - mu: float, sigma: float, max_so_far: Optional[float], temp_symbol: str + mu: float, sigma: float, max_so_far: Optional[float], temp_symbol: str, city_name: str = "" ) -> Dict[str, Any]: """ Generalized Gaussian probability distribution calculation. @@ -691,17 +691,26 @@ def calculate_prob_distribution( # 0.5 * (1 + erf( (x-m)/(s*sqrt(2)) )) return 0.5 * (1 + math.erf((x - m) / (sigma * math.sqrt(2)))) - min_possible_wu = wu_round(max_so_far) if max_so_far is not None else -999 + min_possible_wu = apply_city_settlement(city_name, max_so_far) if max_so_far is not None else -999 probs = {} # Range: mu +/- 3 sigma or at least +/- 2 degrees search_range = max(2, int(sigma * 2.5)) - target_mu = wu_round(mu) + is_exact = is_exact_settlement_city(city_name) + target_mu = apply_city_settlement(city_name, mu) + if is_exact: + target_mu = int(math.floor(mu)) for n in range(target_mu - search_range, target_mu + search_range + 1): if n < min_possible_wu: continue - p = _norm_cdf(n + 0.5, mu, sigma) - _norm_cdf(n - 0.5, mu, sigma) + if is_exact: + # 向下取整的概率区间为 [n, n + 1) + p = _norm_cdf(n + 1.0, mu, sigma) - _norm_cdf(n, mu, sigma) + else: + # 常规四舍五入的概率区间为 [n - 0.5, n + 0.5) + p = _norm_cdf(n + 0.5, mu, sigma) - _norm_cdf(n - 0.5, mu, sigma) + if p > 0.01: probs[n] = p @@ -713,9 +722,10 @@ def calculate_prob_distribution( norm_probs = {k: v / total_p for k, v in probs.items()} sorted_probs = sorted(norm_probs.items(), key=lambda x: x[1], reverse=True) for t, p in sorted_probs[:4]: + rng_str = f"[{t}.0~{t+1}.0)" if is_exact else f"[{t-0.5}~{t+0.5})" probabilities.append({ "value": int(t), - "range": f"[{t-0.5}~{t+0.5})", + "range": rng_str, "probability": round(p, 3) }) diff --git a/src/data_collection/city_registry.py b/src/data_collection/city_registry.py index ffb74761..483e0c56 100644 --- a/src/data_collection/city_registry.py +++ b/src/data_collection/city_registry.py @@ -357,6 +357,77 @@ CITY_REGISTRY = { "distance_km": 13.0, "warning": "机场位于东北侧开阔区,午后混合层增强时与核心城区体感温度可能出现偏差。", }, + + "chengdu": { + "name": "Chengdu", + "lat": 30.5785, + "lon": 103.9471, + "icao": "ZUUU", + "tz_offset": 28800, + "use_fahrenheit": False, + "is_major": True, + "risk_level": "medium", + "risk_emoji": "🟡", + "airport_name": "成都双流国际机场", + "distance_km": 16.0, + "warning": "盆地地形,夜间降温较慢,多云雾影响日照升温。", + }, + "chongqing": { + "name": "Chongqing", + "lat": 29.7196, + "lon": 106.6416, + "icao": "ZUCK", + "tz_offset": 28800, + "use_fahrenheit": False, + "is_major": True, + "risk_level": "high", + "risk_emoji": "🔴", + "airport_name": "重庆江北国际机场", + "distance_km": 19.0, + "warning": "四大火炉之一,夏季持续高温,夜间温度偏高,湿度大。", + }, + "shenzhen": { + "name": "Shenzhen", + "lat": 22.6393, + "lon": 113.8107, + "icao": "ZGSZ", + "tz_offset": 28800, + "use_fahrenheit": False, + "is_major": True, + "risk_level": "medium", + "risk_emoji": "🟡", + "airport_name": "深圳宝安国际机场", + "distance_km": 32.0, + "warning": "近海受海洋调节,热岛效应强,日夜温差较小。", + }, + "beijing": { + "name": "Beijing", + "lat": 40.0801, + "lon": 116.5846, + "icao": "ZBAA", + "tz_offset": 28800, + "use_fahrenheit": False, + "is_major": True, + "risk_level": "medium", + "risk_emoji": "🟡", + "airport_name": "北京首都国际机场", + "distance_km": 25.0, + "warning": "北方内陆,夏季干热,冬季寒冷,春秋易受风沙影响。", + }, + "wuhan": { + "name": "Wuhan", + "lat": 30.7838, + "lon": 114.2081, + "icao": "ZHHH", + "tz_offset": 28800, + "use_fahrenheit": False, + "is_major": True, + "risk_level": "high", + "risk_emoji": "🔴", + "airport_name": "武汉天河国际机场", + "distance_km": 26.0, + "warning": "江汉平原,水汽充足,夏季常现长时闷热高温。", + }, } ALIASES = { @@ -406,4 +477,14 @@ ALIASES = { "華沙": "warsaw", "马德里": "madrid", "馬德里": "madrid", + "ctu": "chengdu", "cd": "chengdu", + "ckg": "chongqing", "cq": "chongqing", + "szx": "shenzhen", "sz": "shenzhen", + "pek": "beijing", "bjs": "beijing", "bj": "beijing", + "wuh": "wuhan", "wh": "wuhan", + "成都": "chengdu", + "重庆": "chongqing", + "深圳": "shenzhen", + "北京": "beijing", + "武汉": "wuhan", } diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py index c4538433..a5bf3c55 100644 --- a/src/data_collection/weather_sources.py +++ b/src/data_collection/weather_sources.py @@ -34,6 +34,11 @@ class WeatherDataCollector: "seoul": ["RKSI", "RKSS"], "hong kong": ["VHHH", "VMMC", "ZGSZ"], "taipei": ["RCSS", "RCTP"], + "chengdu": ["ZUUU", "ZUTF"], + "chongqing": ["ZUCK", "ZUPS"], + "shenzhen": ["ZGSZ", "ZGGG"], + "beijing": ["ZBAA", "ZBAD"], + "wuhan": ["ZHHH", "ZHES"], "shanghai": ["ZSPD", "ZSSS", "ZSNB", "ZSHC"], "singapore": ["WSSS", "WSAP", "WMKK"], "tokyo": ["RJTT", "RJAA", "RJAH", "RJTJ"], @@ -1960,6 +1965,16 @@ class WeatherDataCollector: "taipei": "Taipei", "台北": "Taipei", "臺北": "Taipei", + "chengdu": "Chengdu", + "成都": "Chengdu", + "chongqing": "Chongqing", + "重庆": "Chongqing", + "shenzhen": "Shenzhen", + "深圳": "Shenzhen", + "beijing": "Beijing", + "北京": "Beijing", + "wuhan": "Wuhan", + "武汉": "Wuhan", "shanghai": "Shanghai", "上海": "Shanghai", "singapore": "Singapore",