移除温度急变检测,改为 per-city 定时推送温度+DEB

去掉 0.5°C/10min 阈值触发、最高温锁定、冷却期等机制。
四座机场城市各自 10 分钟间隔独立推送当前温度和 DEB 预测,
仅当地 08:00-20:00 时段发送,无触发条件、无警报标题。

Constraint: 首尔/釜山展示跑道对温度,东京/安卡拉展示单站温度+本地时间
Scope-risk: 高,核心逻辑变更
Tested: ruff check 通过
This commit is contained in:
2569718930@qq.com
2026-05-12 19:02:36 +08:00
parent 4006e82ded
commit e062fedb3a
2 changed files with 37 additions and 70 deletions
+3 -3
View File
@@ -188,11 +188,11 @@ class StartupCoordinator:
chat_ids = get_telegram_chat_ids_from_env()
interval = max(60, _env_int("TELEGRAM_AIRPORT_PUSH_INTERVAL_SEC", 600))
details = {
"mode": "airport-high-freq",
"mode": "airport-periodic",
"interval_sec": interval,
"cities": ["seoul", "busan", "tokyo"],
"cities": ["seoul", "busan", "tokyo", "ankara"],
"chat_targets": len(chat_ids),
"cooldown_sec": _env_int("TELEGRAM_AIRPORT_COOLDOWN_SEC", 7200),
"daytime_only": "08:00-20:00 local",
}
validation_error = None if chat_ids else "missing_TELEGRAM_CHAT_IDS"
return self._start_with_validation(
+34 -67
View File
@@ -692,9 +692,6 @@ def start_trade_alert_push_loop(bot: Any, config: Dict[str, Any]) -> Optional[th
HIGH_FREQ_AIRPORT_CITIES = {"seoul", "busan", "tokyo", "ankara"}
HIGH_FREQ_AIRPORT_ICAO = {"seoul": "RKSI", "busan": "RKPK", "tokyo": "RJTT", "ankara": "17128"}
HIGH_FREQ_PUSH_INTERVAL_SEC = 600
HIGH_FREQ_MOMENTUM_THRESHOLD_C = 0.5
HIGH_FREQ_COOLDOWN_SEC = 7200
_AIRPORT_PUSH_STATE_PATH = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
@@ -726,77 +723,58 @@ def _save_airport_state(state: Dict[str, Any]) -> None:
os.replace(tmp, path)
def _check_high_locked(city: str) -> bool:
icao = HIGH_FREQ_AIRPORT_ICAO.get(city)
if not icao:
return False
try:
from src.database.db_manager import DBManager
db = DBManager()
obs = db.get_airport_obs_recent(icao, minutes=120)
except Exception:
return False
if len(obs) < 6:
return False
temps = [r.get("temp_c") for r in obs if r.get("temp_c") is not None]
if len(temps) < 6:
return False
peak_idx = temps.index(max(temps))
if peak_idx >= len(temps) - 2:
return False
post_peak = temps[peak_idx:]
for i in range(1, len(post_peak)):
if post_peak[i] > post_peak[i - 1] + 0.05:
return False
return True
_AIRPORT_CITY_LABEL = {"seoul": "首尔/仁川", "busan": "釜山/金海", "tokyo": "东京/羽田", "ankara": "安卡拉/Esenboğa"}
def _build_airport_rapid_change_message(
def _build_airport_status_message(
city: str,
rule: Dict[str, Any],
city_weather: Dict[str, Any],
deb_pred: Optional[float],
local_time: str = "",
) -> str:
airport_label = {"seoul": "首尔/仁川", "busan": "釜山/金海", "tokyo": "东京/羽田", "ankara": "安卡拉/Esenboğa"}.get(city, city.title())
runway_pairs = rule.get("runway_pairs") or []
runway_temps = rule.get("runway_temps") or []
last = rule.get("last_temp", 0)
label = _AIRPORT_CITY_LABEL.get(city, city.title())
amos = city_weather.get("amos") or {}
runway_data = (amos.get("runway_obs") or {}) if amos else {}
runway_pairs = runway_data.get("runway_pairs") or []
runway_temps = runway_data.get("temperatures") or []
current = city_weather.get("current") or {}
temp = current.get("temp")
lines = [f"🚨 {airport_label} 温度急变", ""]
lines = [label, ""]
if runway_pairs and runway_temps and len(runway_pairs) == len(runway_temps):
for (r1, r2), (t, _d) in zip(runway_pairs, runway_temps):
lines.append(f"{r1}/{r2} {t:.1f}°C")
else:
elif temp is not None:
time_suffix = f" ({local_time})" if local_time else ""
lines.append(f"当前 {last:.1f}°C{time_suffix}")
lines.append(f"{temp:.1f}°C{time_suffix}")
if deb_pred is not None:
lines.append(f"DEB 预测最高 {deb_pred:.1f}°C")
lines.append(f"DEB 最高 {deb_pred:.1f}°C")
return "\n".join(lines)
_AIRPORT_PUSH_INTERVAL_SEC = 600
_AIRPORT_PUSH_HOUR_START = 8
_AIRPORT_PUSH_HOUR_END = 20
def _run_high_freq_airport_cycle(
bot: Any,
config: Dict[str, Any],
chat_ids: List[str],
state: Dict[str, Any],
) -> bool:
from datetime import timedelta
state_dirty = False
now_ts = int(time.time())
last_by_city = state.setdefault("last_by_city", {})
for city in sorted(HIGH_FREQ_AIRPORT_CITIES):
try:
icao = HIGH_FREQ_AIRPORT_ICAO.get(city, "")
last_city = last_by_city.get(city) or {}
last_city_ts = int(last_city.get("ts") or 0)
try:
from src.database.db_manager import DBManager
db = DBManager()
recent_obs = db.get_airport_obs_recent(icao, minutes=20)
latest_obs = recent_obs[-1] if recent_obs else {}
except Exception:
latest_obs = {}
if now_ts - last_city_ts < _AIRPORT_PUSH_INTERVAL_SEC:
continue
city_weather: Dict[str, Any] = {}
deb_pred: Optional[float] = None
@@ -809,27 +787,15 @@ def _run_high_freq_airport_cycle(
except Exception:
pass
# Check high-locked
if _check_high_locked(city):
# Time gate: skip outside local daytime
tz_offsets = {"seoul": 9, "busan": 9, "tokyo": 9, "ankara": 3}
offset = tz_offsets.get(city, 0)
local_hour = (datetime.utcnow() + timedelta(hours=offset)).hour
if not (_AIRPORT_PUSH_HOUR_START <= local_hour < _AIRPORT_PUSH_HOUR_END):
continue
from src.analysis.market_alert_engine import _calc_airport_rapid_temp_change
rule = _calc_airport_rapid_temp_change(city_weather, "°C")
if not rule.get("triggered"):
continue
if last_city_ts and now_ts - last_city_ts < HIGH_FREQ_COOLDOWN_SEC:
continue
# Extract runway temps for AMOS cities
amos = city_weather.get("amos") or {}
runway_data = (amos.get("runway_obs") or {}) if amos else {}
rule["runway_pairs"] = runway_data.get("runway_pairs") or []
rule["runway_temps"] = runway_data.get("temperatures") or []
rule["wind_kt"] = latest_obs.get("wind_kt")
local_time = city_weather.get("local_time") or ""
message = _build_airport_rapid_change_message(city, rule, deb_pred, local_time)
message = _build_airport_status_message(city, city_weather, deb_pred, local_time)
sent = False
for chat_id in chat_ids:
@@ -840,12 +806,13 @@ def _run_high_freq_airport_cycle(
logger.warning("airport push failed city={} chat_id={}: {}", city, chat_id, exc)
if sent:
last_by_city[city] = {"ts": now_ts, "trigger": "rapid_temp_change"}
last_by_city[city] = {"ts": now_ts}
state_dirty = True
logger.info("airport rapid change pushed city={} delta_temp={:.1f}", city, rule.get("delta_temp", 0))
logger.info("airport status pushed city={} temp={}", city,
(city_weather.get("current") or {}).get("temp"))
except Exception:
logger.exception("high freq airport cycle failed for city={}", city)
logger.exception("airport cycle failed for city={}", city)
return state_dirty
@@ -860,7 +827,7 @@ def start_high_freq_airport_push_loop(bot: Any, config: Dict[str, Any]) -> Optio
logger.warning("airport high-freq push loop skipped: TELEGRAM_CHAT_IDS is not set")
return None
interval_sec = max(60, _env_int("TELEGRAM_AIRPORT_PUSH_INTERVAL_SEC", HIGH_FREQ_PUSH_INTERVAL_SEC))
interval_sec = max(60, _env_int("TELEGRAM_AIRPORT_PUSH_INTERVAL_SEC", _AIRPORT_PUSH_INTERVAL_SEC))
def _runner() -> None:
state = _load_airport_state()