feat: Implement PolyWeather web application with map and market alert engine, including related utilities and tests.

This commit is contained in:
2569718930@qq.com
2026-03-06 10:31:06 +08:00
parent cac2110b16
commit 68cb056907
5 changed files with 81 additions and 18 deletions
+1
View File
@@ -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/
+73 -13
View File
@@ -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}")
+1 -1
View File
@@ -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,
+3 -1
View File
@@ -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
+3 -3
View File
@@ -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,