diff --git a/web/app.py b/web/app.py index c563ac7f..60d1e0a7 100644 --- a/web/app.py +++ b/web/app.py @@ -26,11 +26,6 @@ from src.utils.config_loader import load_config from src.data_collection.weather_sources import WeatherDataCollector from src.data_collection.city_risk_profiles import CITY_RISK_PROFILES from src.analysis.deb_algorithm import calculate_dynamic_weights, get_deb_accuracy -from src.data_collection.polymarket_client import ( - fetch_weather_markets, - get_city_markets, - compute_divergence, -) # ────────────────────────────────────────────────────────── # Setup @@ -76,6 +71,7 @@ ALIASES = { # ────────────────────────────────────────────────────────── _cache: Dict[str, Dict] = {} CACHE_TTL = 300 +CACHE_TTL_ANKARA = 60 # Ankara measurement updates frequent, narrower cache def _sf(v) -> Optional[float]: @@ -91,12 +87,15 @@ def _sf(v) -> Optional[float]: # ────────────────────────────────────────────────────────── # Core Analysis (replicates bot_listener logic → JSON) # ────────────────────────────────────────────────────────── -def _analyze(city: str) -> Dict[str, Any]: +def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]: """Fetch, analyse, and return structured weather data for one city.""" # Check cache - cached = _cache.get(city) - if cached and _time.time() - cached["t"] < CACHE_TTL: - return cached["d"] + ttl = CACHE_TTL_ANKARA if city.lower() == "ankara" else CACHE_TTL + + if not force_refresh: + cached = _cache.get(city) + if cached and _time.time() - cached["t"] < ttl: + return cached["d"] info = CITIES[city] lat, lon, is_f = info["lat"], info["lon"], info["f"] @@ -495,32 +494,6 @@ def _analyze(city: str) -> Dict[str, Any]: "updated_at": datetime.now(timezone.utc).isoformat(), } - # ── 16. Polymarket odds (web-only, non-blocking) ── - try: - proxy = _config.get("proxy") - fetch_weather_markets(proxy=proxy, timeout=10) - # We don't filter by target_date here, we want ALL active contracts for the city - city_mkts = get_city_markets(city) - if city_mkts: - result["polymarket"] = { - "markets": [ - { - "question": m["question"], - "yes_price": m["yes_price"], - "volume": m.get("volume"), - "threshold": m.get("threshold"), - "threshold_unit": m.get("threshold_unit"), - "url": m.get("url", ""), - } - for m in city_mkts[:5] - ], - "divergence": compute_divergence( - city_mkts, probabilities, sym, use_fahrenheit=info.get("f", False) - ), - } - except Exception as e: - logger.debug(f"Polymarket data skipped for {city}: {e}") - _cache[city] = {"t": _time.time(), "d": result} return result @@ -556,13 +529,13 @@ async def list_cities(): @app.get("/api/city/{name}") -async def city_detail(name: str): +async def city_detail(name: str, force_refresh: bool = False): """Return full weather analysis for a single city.""" name = name.lower().strip().replace("-", " ") name = ALIASES.get(name, name) if name not in CITIES: raise HTTPException(404, detail=f"Unknown city: {name}") - return _analyze(name) + return _analyze(name, force_refresh=force_refresh) @app.get("/api/history/{name}") diff --git a/web/static/app.js b/web/static/app.js index a65d1519..a8d0126f 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -220,9 +220,11 @@ async function fetchCities() { } } -async function fetchCityDetail(cityName) { +async function fetchCityDetail(cityName, force = false) { const urlName = cityName.replace(/\s/g, "-"); - const res = await fetch(`/api/city/${encodeURIComponent(urlName)}`); + const res = await fetch( + `/api/city/${encodeURIComponent(urlName)}?force_refresh=${force}`, + ); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } @@ -230,13 +232,13 @@ async function fetchCityDetail(cityName) { // ────────────────────────────────────────────────────────── // Load & Render City Detail // ────────────────────────────────────────────────────────── -async function loadCityDetail(cityName) { +async function loadCityDetail(cityName, force = false) { selectedCity = cityName; selectedForecastDate = null; // Reset selection for new city setActiveCityItem(cityName); setSelectedMarker(cityName); - if (cityDataCache[cityName]) { + if (!force && cityDataCache[cityName]) { renderPanel(cityDataCache[cityName]); const cData = cityDataCache[cityName]; if (cData.lat != null && cData.lon != null) { @@ -252,7 +254,7 @@ async function loadCityDetail(cityName) { showLoading(true); try { - const data = await fetchCityDetail(cityName); + const data = await fetchCityDetail(cityName, force); cityDataCache[cityName] = data; saveCache(); renderPanel(data); @@ -326,8 +328,6 @@ function renderPanel(data) { renderForecast(data); // AI renderAI(data); - // Market Odds - renderMarket(data); // Risk renderRisk(data); } @@ -925,77 +925,6 @@ function renderAI(data) { container.innerHTML = text; } -function renderMarket(data) { - const widget = document.getElementById("marketFloatingWidget"); - const container = document.getElementById("marketOdds"); - if ( - !data.polymarket || - !data.polymarket.markets || - data.polymarket.markets.length === 0 - ) { - widget.classList.add("hidden"); - return; - } - - widget.classList.remove("hidden"); - const { markets, divergence } = data.polymarket; - let html = ""; - - markets.forEach((mkt) => { - // Find matching divergence signal - const divSignal = divergence.find((d) => d.question === mkt.question); - - let probText = - mkt.yes_price != null ? `${Math.round(mkt.yes_price * 100)}¢` : "N/A"; - let noText = - mkt.yes_price != null - ? `${Math.round((1 - mkt.yes_price) * 100)}¢` - : "N/A"; - - html += ` -
-
- ${mkt.question} 🔗 -
-
- ${probText}Yes - | - ${noText}No -
-
- 交易量: $${mkt.volume ? Math.round(mkt.volume).toLocaleString() : 0} -
- `; - - if (divSignal) { - let divClass = divSignal.signal; - let divIcon = "⚪"; - let divText = "市场定价合理"; - let diffPct = (Math.abs(divSignal.divergence) * 100).toFixed(1); - - if (divClass === "underpriced" || divClass === "slight_under") { - divIcon = "🟢"; - divText = `低估 (偏离 +${diffPct}%)`; - } else if (divClass === "overpriced" || divClass === "slight_over") { - divIcon = "🔴"; - divText = `高估 (偏离 -${diffPct}%)`; - } - - html += ` -
-
${divIcon} 你的模型: ${Math.round(divSignal.our_prob * 100)}%
-
🆚 市场定价: ${Math.round(divSignal.market_prob * 100)}%
-
💡 信号: ${divText}
-
- `; - } - - html += `
`; - }); - - container.innerHTML = html; -} - function renderRisk(data) { const container = document.getElementById("riskInfo"); const risk = data.risk || {}; @@ -1021,12 +950,6 @@ function closePanel() { panel.classList.remove("visible"); setTimeout(() => panel.classList.add("hidden"), 400); - // Hide Polymarket widget - const marketWidget = document.getElementById("marketFloatingWidget"); - if (marketWidget) { - marketWidget.classList.add("hidden"); - } - selectedCity = null; setSelectedMarker(null); document @@ -1272,20 +1195,12 @@ function closeHistoryModal() { document.getElementById("historyModal").classList.add("hidden"); } -// ────────────────────────────────────────────────────────── -// Init -// ────────────────────────────────────────────────────────── document.addEventListener("DOMContentLoaded", async () => { initMap(); // Panel close document.getElementById("panelClose").addEventListener("click", closePanel); - // Market Widget close - document.getElementById("marketClose").addEventListener("click", () => { - document.getElementById("marketFloatingWidget").classList.add("hidden"); - }); - // Escape key document.addEventListener("keydown", (e) => { if (e.key === "Escape") closePanel(); @@ -1311,7 +1226,7 @@ document.addEventListener("DOMContentLoaded", async () => { cityDataCache = {}; saveCache(); if (selectedCity) { - await loadCityDetail(selectedCity); + await loadCityDetail(selectedCity, true); } btn.classList.remove("spinning"); }); diff --git a/web/static/index.html b/web/static/index.html index 44648b9c..ce220497 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -5,7 +5,7 @@ PolyWeather — 天气衍生品智能地图 - + @@ -140,13 +140,6 @@ - - diff --git a/web/static/style.css b/web/static/style.css index 0237c8b9..a79470ac 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -804,153 +804,6 @@ body { font-style: italic; } -/* ── Market Odds Section (Floating Widget) ── */ -.market-floating { - position: absolute; - bottom: 30px; - left: 20px; - width: 320px; - background: var(--bg-panel); - border: 1px solid var(--border-glass); - border-radius: 12px; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); - backdrop-filter: blur(12px); - z-index: 1000; - overflow: hidden; - transition: - opacity 0.3s ease, - transform 0.3s ease; - max-height: 80vh; - display: flex; - flex-direction: column; -} - -.market-floating.hidden { - opacity: 0; - transform: translateY(20px); - pointer-events: none; -} - -.market-widget-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 16px; - background: rgba(255, 255, 255, 0.03); - border-bottom: 1px solid var(--border-subtle); -} - -.market-widget-header h3 { - margin: 0; - font-size: 14px; - color: var(--text-primary); - display: flex; - align-items: center; - gap: 6px; -} - -.market-close { - background: none; - border: none; - color: var(--text-muted); - font-size: 16px; - cursor: pointer; - padding: 4px; -} -.market-close:hover { - color: var(--text-primary); -} - -.market-odds { - display: flex; - flex-direction: column; - gap: 10px; - padding: 12px; - overflow-y: auto; -} - -.market-card { - background: rgba(255, 255, 255, 0.03); - border: 1px solid var(--border-subtle); - border-radius: 10px; - padding: 12px 14px; - transition: var(--transition); -} - -.market-card:hover { - border-color: var(--border-glass); - background: rgba(255, 255, 255, 0.05); -} - -.market-question { - font-size: 12px; - color: var(--text-secondary); - margin-bottom: 8px; - line-height: 1.4; -} - -.market-prices { - display: flex; - gap: 8px; - align-items: center; - margin-bottom: 6px; -} - -.market-price { - font-size: 20px; - font-weight: 700; - font-family: "Inter", sans-serif; -} - -.market-price.yes { - color: var(--accent-green); -} -.market-price.no { - color: #f87171; -} -.market-price-label { - font-size: 11px; - color: var(--text-muted); - margin-left: 2px; -} - -.market-volume { - font-size: 11px; - color: var(--text-muted); -} - -.market-divergence { - margin-top: 10px; - padding: 10px 12px; - border-radius: 8px; - font-size: 12px; - line-height: 1.6; -} - -.market-divergence.underpriced { - background: rgba(52, 211, 153, 0.08); - border: 1px solid rgba(52, 211, 153, 0.2); - color: var(--accent-green); -} - -.market-divergence.overpriced { - background: rgba(248, 113, 113, 0.08); - border: 1px solid rgba(248, 113, 113, 0.2); - color: #f87171; -} - -.market-divergence.neutral { - background: rgba(255, 255, 255, 0.03); - border: 1px solid var(--border-subtle); - color: var(--text-muted); -} - -.market-no-data { - font-size: 13px; - color: var(--text-muted); - padding: 8px 0; -} - /* ── Risk Section ── */ .risk-info { font-size: 12px;