diff --git a/src/analysis/market_alert_engine.py b/src/analysis/market_alert_engine.py index b0a8db29..2852ac42 100644 --- a/src/analysis/market_alert_engine.py +++ b/src/analysis/market_alert_engine.py @@ -811,6 +811,57 @@ def _build_advice_cn( return ",".join(parts) + "。" +def _classify_low_yes_signal( + rules: Dict[str, Dict[str, Any]], + market_snapshot: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + snapshot = market_snapshot or {} + forecast_bucket = snapshot.get("forecast_bucket") or {} + yes_buy = _norm_probability(forecast_bucket.get("yes_buy")) + center_deb = rules.get("ankara_center_deb_hit", {}) or {} + momentum = rules.get("momentum_spike", {}) or {} + advection = rules.get("advection", {}) or {} + breakthrough = rules.get("forecast_breakthrough", {}) or {} + + cheap_yes = yes_buy is not None and yes_buy < 0.10 + center_hit = bool(center_deb.get("triggered")) + momentum_direction = str(momentum.get("direction") or "neutral").lower() + momentum_up = bool(momentum.get("triggered")) and momentum_direction == "up" + momentum_down = bool(momentum.get("triggered")) and momentum_direction == "down" + warm_flow = bool(advection.get("triggered")) + model_break = bool(breakthrough.get("triggered")) + + bullish_confirmation = momentum_up or warm_flow or model_break + bearish_conflict = momentum_down and not warm_flow and not model_break + + if cheap_yes and center_hit and bullish_confirmation: + return { + "label": "undervalued", + "should_push": True, + "reason_cn": "该桶 Yes 价格 < 10c,且天气信号与上行方向一致,疑似低估", + } + + if cheap_yes and center_hit and bearish_conflict: + return { + "label": "conflicted", + "should_push": False, + "reason_cn": "该桶 Yes 价格虽 < 10c,但短时动量转弱,暂不判定低估", + } + + if cheap_yes: + return { + "label": "watch", + "should_push": False, + "reason_cn": "该桶 Yes 价格 < 10c,但方向确认不足,先观察", + } + + return { + "label": "none", + "should_push": False, + "reason_cn": "当前未出现明确的低价错配信号", + } + + def _build_telegram_messages( city_weather: Dict[str, Any], rules: Dict[str, Dict[str, Any]], @@ -1022,6 +1073,7 @@ def _build_telegram_messages_mispricing( snapshot = market_snapshot or _extract_market_snapshot(city_weather) momentum = rules.get("momentum_spike", {}) center_deb = rules.get("ankara_center_deb_hit", {}) + signal_state = _classify_low_yes_signal(rules, snapshot) local_time = str(city_weather.get("local_time") or "").strip() obs_time = str(current.get("obs_time") or "").strip() suppressed = bool((suppression or {}).get("suppressed")) @@ -1081,8 +1133,10 @@ def _build_telegram_messages_mispricing( if lead_gap is not None: center_line += f" | 领先 {lead_gap:+.1f}{temp_symbol}" - if snapshot.get("available"): + if snapshot.get("available") and signal_state.get("should_push"): title_zh = "🚨 PolyWeather 错价雷达" + elif snapshot.get("available"): + title_zh = "📍 PolyWeather 错价观察" else: title_zh = "🚨 PolyWeather 异动预警" if (has_active_trigger or suppressed) else "📍 PolyWeather 状态快照" @@ -1101,7 +1155,7 @@ def _build_telegram_messages_mispricing( else: lines_zh.append("基准:多模型最高温 --(结算参考 --)") lines_zh.append(f"命中桶:{match_bucket_label} | Yes: {match_bucket_yes}") - lines_zh.append("触发:该桶 Yes 价格 < 10c,疑似低估") + lines_zh.append(f"触发:{signal_state.get('reason_cn')}") if center_line: lines_zh.append(center_line) lines_zh.append("") @@ -1119,8 +1173,15 @@ def _build_telegram_messages_mispricing( lines_zh.append(f"市场链接:{market_url}") lines_zh.append(f"地图:{final_map}") + title_en = ( + "🚨 PolyWeather Mispricing Radar" + if snapshot.get("available") and signal_state.get("should_push") + else "📍 PolyWeather Mispricing Watch" + if snapshot.get("available") + else "🚨 PolyWeather Alert" + ) lines_en = [ - f"🚨 PolyWeather Mispricing Radar [{city_name}]", + f"{title_en} [{city_name}]", "", f"Type: {types_cn}", f"Now: {dynamic_text}", @@ -1172,6 +1233,7 @@ def _build_alert_evidence( trigger_types = [row.get("type") for row in triggered if row.get("type")] forecast_bucket = market_snapshot.get("forecast_bucket") or {} + low_yes_signal = _classify_low_yes_signal(rules, market_snapshot) return { "version": 1, @@ -1252,6 +1314,7 @@ def _build_alert_evidence( }, "market": { "available": bool(market_snapshot.get("available")), + "low_yes_signal": low_yes_signal, "market_prob": market_snapshot.get("market_prob"), "model_prob": market_snapshot.get("model_prob"), "edge_percent": market_snapshot.get("edge_percent"), @@ -1294,6 +1357,7 @@ def build_trading_alerts( city = city_weather.get("name", "") now = datetime.now(timezone.utc).isoformat() market_snapshot = _extract_market_snapshot(city_weather) + low_yes_signal = _classify_low_yes_signal({}, {}) rules: Dict[str, Dict[str, Any]] = { "ankara_center_deb_hit": _calc_ankara_center_deb_alert(city_weather, temp_symbol), @@ -1301,6 +1365,7 @@ def build_trading_alerts( "forecast_breakthrough": _calc_forecast_breakthrough_alert(city_weather, temp_symbol), "advection": _calc_advection_alert(city_weather, temp_symbol), } + low_yes_signal = _classify_low_yes_signal(rules, market_snapshot) triggered = [ { @@ -1330,13 +1395,22 @@ def build_trading_alerts( if force_push and severity == "none": severity = "medium" - telegram = _build_telegram_messages_mispricing( - city_weather=city_weather, - rules=rules, - market_snapshot=market_snapshot, - suppression=suppression, - map_url=map_url, - ) + if market_snapshot.get("available") and low_yes_signal.get("should_push"): + telegram = _build_telegram_messages_mispricing( + city_weather=city_weather, + rules=rules, + market_snapshot=market_snapshot, + suppression=suppression, + map_url=map_url, + ) + else: + telegram = _build_telegram_messages( + city_weather=city_weather, + rules=rules, + map_url=map_url, + market_snapshot=market_snapshot, + suppression=suppression, + ) evidence = _build_alert_evidence( city_weather=city_weather, rules=rules, @@ -1359,4 +1433,3 @@ def build_trading_alerts( "evidence": evidence, "telegram": telegram, } -