From 68cb0569074ff8e2f6a5f25d162a8f0c802bafe7 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Fri, 6 Mar 2026 10:31:06 +0800 Subject: [PATCH] feat: Implement PolyWeather web application with map and market alert engine, including related utilities and tests. --- .env.example | 1 + src/analysis/market_alert_engine.py | 86 ++++++++++++++++++++++++----- src/utils/telegram_push.py | 2 +- tests/test_market_alert_engine.py | 4 +- web/app.py | 6 +- 5 files changed, 81 insertions(+), 18 deletions(-) diff --git a/.env.example b/.env.example index eb527743..7a77c9b7 100644 --- a/.env.example +++ b/.env.example @@ -21,3 +21,4 @@ HTTP_PROXY=http://127.0.0.1:7890 # Other Settings LOG_LEVEL=INFO ENV=production +POLYWEATHER_MAP_URL=https://polyweather-pro.vercel.app/ diff --git a/src/analysis/market_alert_engine.py b/src/analysis/market_alert_engine.py index 4d2b9182..4bc5bdf5 100644 --- a/src/analysis/market_alert_engine.py +++ b/src/analysis/market_alert_engine.py @@ -256,6 +256,48 @@ def _find_market_by_id(markets: List[Dict[str, Any]], market_id: Any) -> Optiona return None +def _extract_market_prices(target_market: Optional[Dict[str, Any]]) -> Dict[str, Any]: + prices: Dict[str, Any] = { + "question": None, + "yes_buy": None, + "yes_sell": None, + "yes_last": None, + "no_buy": None, + "no_sell": None, + "no_last": None, + } + if not target_market: + return prices + + prices["question"] = target_market.get("question") + for oc in target_market.get("outcomes", []) or []: + name = str(oc.get("name") or "").strip().lower() + if name not in ("yes", "no"): + continue + prefix = "yes" if name == "yes" else "no" + prices[f"{prefix}_buy"] = _sf(oc.get("buy_price")) + prices[f"{prefix}_sell"] = _sf(oc.get("sell_price")) + prices[f"{prefix}_last"] = _sf(oc.get("last_trade_price")) + if prices[f"{prefix}_last"] is None: + prices[f"{prefix}_last"] = _sf(oc.get("last_price")) + return prices + + +def _format_market_price(value: Optional[float]) -> str: + if value is None: + return "-" + return f"{value * 100:.0f}c" + + +def _format_temp_display(value: Optional[float], temp_symbol: str) -> str: + if value is None: + return f"-{temp_symbol}" + rounded = round(float(value), 1) + if abs(rounded - round(rounded)) < 1e-9: + return f"{int(round(rounded))}{temp_symbol}" + return f"{rounded:.1f}{temp_symbol}" + + def _calc_kill_zone_alert( city_weather: Dict[str, Any], market_snapshot: Dict[str, Any], @@ -290,21 +332,14 @@ def _calc_kill_zone_alert( distance = abs(current_temp - strike) triggered = distance < threshold - no_probability = None markets = market_snapshot.get("markets", []) or [] target_market = _find_market_by_id(markets, nearest.get("market_id")) + market_prices = _extract_market_prices(target_market) + + no_probability = None if target_market: - yes_price = None - no_price = None - for oc in target_market.get("outcomes", []) or []: - oc_name = str(oc.get("name") or "").strip().lower() - candidate_price = _sf(oc.get("buy_price")) - if candidate_price is None: - candidate_price = _sf(oc.get("last_price")) - if oc_name == "yes": - yes_price = candidate_price - elif oc_name == "no": - no_price = candidate_price + yes_price = market_prices.get("yes_buy") + no_price = market_prices.get("no_buy") if no_price is None and yes_price is not None: no_price = max(0.0, min(1.0, 1.0 - yes_price)) no_probability = no_price @@ -314,12 +349,14 @@ def _calc_kill_zone_alert( "triggered": triggered, "current_temp": round(current_temp, 2), "strike_price": round(strike, 2), + "market_label": f"{_format_temp_display(strike, temp_symbol)} 档位", "distance": round(distance, 2), "threshold": round(threshold, 2), "market_id": nearest.get("market_id"), "question": nearest.get("question"), "strike_source": nearest.get("source"), "no_probability": round(no_probability, 4) if no_probability is not None else None, + "market_prices": market_prices, } @@ -495,6 +532,8 @@ def _build_telegram_messages( delta_min = momentum.get("delta_minutes") strike = _sf(kill_zone.get("strike_price")) distance = _sf(kill_zone.get("distance")) + market_label = str(kill_zone.get("market_label") or "").strip() + market_prices = kill_zone.get("market_prices") or {} dyn = f"实测 {current_temp:.1f}{temp_symbol}" if delta_temp is not None and delta_min is not None: @@ -515,8 +554,20 @@ def _build_telegram_messages( if lead_delta is not None: lead_line = f"联动:{st_name} 已领先 {lead_delta:+.1f}{temp_symbol}" + price_line = "" + if any( + market_prices.get(key) is not None + for key in ("yes_buy", "yes_sell", "no_buy", "no_sell") + ): + price_prefix = f"盘口({market_label}):" if market_label else "盘口:" + price_line = ( + price_prefix + f"Yes 买 {_format_market_price(market_prices.get('yes_buy'))} / 卖 {_format_market_price(market_prices.get('yes_sell'))} | " + f"No 买 {_format_market_price(market_prices.get('no_buy'))} / 卖 {_format_market_price(market_prices.get('no_sell'))}" + ) + advice = _build_advice_cn(rules, temp_symbol) - final_map = map_url or "https://polyweather.vercel.app" + final_map = map_url or "https://polyweather-pro.vercel.app/" lines_zh = [ f"🚨 PolyWeather 异动预警 [{city_name}]", @@ -526,6 +577,8 @@ def _build_telegram_messages( ] if strike_line: lines_zh.append(strike_line) + if price_line: + lines_zh.append(price_line) if lead_line: lines_zh.append(lead_line) lines_zh.append(f"AI 建议:{advice}") @@ -550,6 +603,13 @@ def _build_telegram_messages( ] if strike is not None and distance is not None: lines_en.append(f"Distance to {strike:.1f}{temp_symbol} strike: {distance:.1f}{temp_symbol}") + if price_line: + price_label_en = f"Quotes ({market_label}): " if market_label else "Quotes: " + lines_en.append( + price_label_en + f"Yes buy {_format_market_price(market_prices.get('yes_buy'))} / sell {_format_market_price(market_prices.get('yes_sell'))} | " + f"No buy {_format_market_price(market_prices.get('no_buy'))} / sell {_format_market_price(market_prices.get('no_sell'))}" + ) lines_en.append(f"Action: {advice}") lines_en.append(f"Map: {final_map}") diff --git a/src/utils/telegram_push.py b/src/utils/telegram_push.py index b971005c..f8852f36 100644 --- a/src/utils/telegram_push.py +++ b/src/utils/telegram_push.py @@ -156,7 +156,7 @@ def build_trade_alert_for_city( proxy=proxy, force_refresh=force_refresh, ) - map_url = os.getenv("POLYWEATHER_MAP_URL") or "https://polyweather.vercel.app" + map_url = os.getenv("POLYWEATHER_MAP_URL") or "https://polyweather-pro.vercel.app/" alert_payload = build_trading_alerts( city_weather=city_weather, market_snapshot=market_snapshot, diff --git a/tests/test_market_alert_engine.py b/tests/test_market_alert_engine.py index 963d3c70..8221838f 100644 --- a/tests/test_market_alert_engine.py +++ b/tests/test_market_alert_engine.py @@ -84,6 +84,9 @@ def test_trading_alerts_all_core_rules_trigger(): msg = out["telegram"]["zh"] assert "PolyWeather 异动预警" in msg assert "动量突变" in msg + assert "盘口:" in msg + assert "Yes 买 73c / 卖 -" in msg + assert "No 买 27c / 卖 -" in msg assert "No\" 单需谨慎" in msg assert "https://example.com/map" in msg @@ -97,4 +100,3 @@ def test_forecast_breakthrough_not_triggered_when_current_not_above_margin(): market_snapshot=_sample_market_snapshot(), ) assert out["rules"]["forecast_breakthrough"]["triggered"] is False - diff --git a/web/app.py b/web/app.py index 2b0dd1ed..3bb2e674 100644 --- a/web/app.py +++ b/web/app.py @@ -35,7 +35,7 @@ app = FastAPI(title="PolyWeather Map", version="1.0") _cors_origins = os.getenv( "WEB_CORS_ORIGINS", - "http://localhost:3000,http://127.0.0.1:3000,https://polyweather.vercel.app", + "http://localhost:3000,http://127.0.0.1:3000,https://polyweather-pro.vercel.app", ) app.add_middleware( CORSMiddleware, @@ -658,7 +658,7 @@ async def city_polymarket_alerts( force_refresh=force_refresh, ) city_weather = _analyze(city, force_refresh=force_refresh) - map_url = os.getenv("POLYWEATHER_MAP_URL") or "https://polyweather.vercel.app" + map_url = os.getenv("POLYWEATHER_MAP_URL") or "https://polyweather-pro.vercel.app/" trade_alerts = build_trading_alerts( city_weather=city_weather, market_snapshot=snapshot, @@ -701,7 +701,7 @@ async def city_trade_alerts( force_refresh=force_refresh, ) city_weather = _analyze(city, force_refresh=force_refresh) - map_url = os.getenv("POLYWEATHER_MAP_URL") or "https://polyweather.vercel.app" + map_url = os.getenv("POLYWEATHER_MAP_URL") or "https://polyweather-pro.vercel.app/" return build_trading_alerts( city_weather=city_weather, market_snapshot=snapshot,