diff --git a/bot_listener.py b/bot_listener.py index a52e9440..77a2e7eb 100644 --- a/bot_listener.py +++ b/bot_listener.py @@ -217,53 +217,48 @@ def analyze_weather_trend(weather_data, temp_symbol, city_name=None): sigma *= 0.3 # 峰值已过,结果基本锁定 elif first_peak_h <= local_hour_frac <= last_peak_h: sigma *= 0.7 # 正在峰值窗口 - - # 分布中心:以 DEB/多模型中位数为主锚(权重 70%),集合中位数为辅(30%) - # 因为集合中位数经常偏保守,不如确定性模型和 DEB 融合值可靠 - if forecast_median is not None: - mu = forecast_median * 0.7 + ens_median * 0.3 - else: - mu = ens_median - - # 实时修正:如果实测最高温已经超过了预报的 μ,则向上修正 + # === 判定是否为“死盘” (Dead Market) === + # 逻辑:深夜且气温大幅回落,或者已过峰值时段且明显降温 + is_dead_market = False + current_temp = _sf(metar.get("current", {}).get("temp")) + if max_so_far is not None and current_temp is not None: + # 深夜死盘:21:00 后,回落超过 3°C + if local_hour >= 21 and max_so_far - current_temp >= 3.0: + is_dead_market = True + # 峰值后死盘:已过最热窗口,回落超过 1.5°C + elif local_hour > last_peak_h and max_so_far - current_temp >= 1.5: + is_dead_market = True + + if ens_p10 is not None and ens_p90 is not None and not is_dead_market: + # (现有概率计算逻辑保留,但增加 is_dead_market 排除) + mu = forecast_median * 0.7 + ens_median * 0.3 if forecast_median is not None else ens_median if max_so_far is not None and max_so_far > mu: - if not is_cooling: - # 还在升温,预期最终比当前再高一点 - mu = max_so_far + 0.3 - else: - # 已降温,以实测峰值为锚 - mu = max_so_far + mu = max_so_far + (0.3 if not is_cooling else 0.0) - # 简化的正态 CDF (不依赖 scipy) def _norm_cdf(x, m, s): return 0.5 * (1 + _math.erf((x - m) / (s * _math.sqrt(2)))) - # 计算每个 WU 整数区间 [N-0.5, N+0.5) 的概率 - center = round(mu) - candidates = range(center - 2, center + 3) # 5 个候选整数 - # 如果已有实测最高温,低于该值的 WU 结算整数不可能出现 min_possible_wu = round(max_so_far) if max_so_far is not None else -999 - probs = {} - for n in candidates: - if n < min_possible_wu: - continue # 已实测超过此温度,不可能结算在这里 + for n in range(round(mu) - 2, round(mu) + 3): + if n < min_possible_wu: continue p = _norm_cdf(n + 0.5, mu, sigma) - _norm_cdf(n - 0.5, mu, sigma) - if p > 0.01: - probs[n] = p + if p > 0.01: probs[n] = p - # 归一化 total_p = sum(probs.values()) if total_p > 0: probs = {k: v / total_p for k, v in probs.items()} - - # 格式化输出(按概率从高到低排列,显示区间) - sorted_probs = sorted(probs.items(), key=lambda x: x[1], reverse=True) - prob_parts = [f"{int(t)}{temp_symbol} [{t-0.5}~{t+0.5}) {p*100:.0f}%" for t, p in sorted_probs[:4]] - if prob_parts: - prob_str = " | ".join(prob_parts) - insights.append(f"🎲 结算概率 (μ={mu:.1f}):{prob_str}") - ai_features.append(f"🎲 数学概率分布:{prob_str}") + sorted_probs = sorted(probs.items(), key=lambda x: x[1], reverse=True) + prob_parts = [f"{int(t)}{temp_symbol} [{t-0.5}~{t+0.5}) {p*100:.0f}%" for t, p in sorted_probs[:4]] + if prob_parts: + prob_str = " | ".join(prob_parts) + insights.append(f"🎲 结算概率 (μ={mu:.1f}):{prob_str}") + ai_features.append(f"🎲 数学概率分布:{prob_str}") + elif is_dead_market: + settled_wu = round(max_so_far) if max_so_far is not None else "N/A" + dead_msg = f"🎲 结算预测:已锁定 {settled_wu}{temp_symbol} (死盘确认)" + insights.append(dead_msg) + ai_features.append(f"🎲 状态: 确认死盘,结算已无悬念。") # === 实测已超预报 & 趋势输出 === if max_so_far is not None and forecast_high is not None: