feat: Implement initial PolyWeather web map API with comprehensive weather data collection, analysis, and Polymarket integration.
This commit is contained in:
+30
@@ -26,6 +26,11 @@ 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
|
||||
@@ -453,6 +458,31 @@ 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)
|
||||
city_mkts = get_city_markets(city, target_date=local_date_str)
|
||||
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
|
||||
|
||||
|
||||
@@ -326,6 +326,8 @@ function renderPanel(data) {
|
||||
renderForecast(data);
|
||||
// AI
|
||||
renderAI(data);
|
||||
// Market Odds
|
||||
renderMarket(data);
|
||||
// Risk
|
||||
renderRisk(data);
|
||||
}
|
||||
@@ -824,6 +826,75 @@ function renderAI(data) {
|
||||
container.innerHTML = text;
|
||||
}
|
||||
|
||||
function renderMarket(data) {
|
||||
const container = document.getElementById("marketOdds");
|
||||
if (
|
||||
!data.polymarket ||
|
||||
!data.polymarket.markets ||
|
||||
data.polymarket.markets.length === 0
|
||||
) {
|
||||
container.innerHTML = '<div class="market-no-data">暂无相关天气合约</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
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 += `
|
||||
<div class="market-card">
|
||||
<div class="market-question">
|
||||
<a href="${mkt.url}" target="_blank" style="color:var(--text-primary);text-decoration:none;">${mkt.question} 🔗</a>
|
||||
</div>
|
||||
<div class="market-prices">
|
||||
<span class="market-price yes">${probText}</span><span class="market-price-label">Yes</span>
|
||||
<span style="margin: 0 4px; color:var(--text-muted)">|</span>
|
||||
<span class="market-price no">${noText}</span><span class="market-price-label">No</span>
|
||||
</div>
|
||||
<div class="market-volume">
|
||||
交易量: $${mkt.volume ? Math.round(mkt.volume).toLocaleString() : 0}
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 += `
|
||||
<div class="market-divergence ${divClass.replace("slight_", "")}">
|
||||
<div><b>${divIcon} 你的模型:</b> ${Math.round(divSignal.our_prob * 100)}%</div>
|
||||
<div style="margin-top:2px;"><b>🆚 市场定价:</b> ${Math.round(divSignal.market_prob * 100)}%</div>
|
||||
<div style="margin-top:6px;font-weight:600;">💡 信号: ${divText}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderRisk(data) {
|
||||
const container = document.getElementById("riskInfo");
|
||||
const risk = data.risk || {};
|
||||
|
||||
@@ -131,6 +131,14 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Polymarket Odds ── -->
|
||||
<section class="market-section">
|
||||
<h3>📈 市场赔率 <span style="font-size:11px;color:var(--text-muted);font-weight:400;">Polymarket</span></h3>
|
||||
<div id="marketOdds" class="market-odds">
|
||||
<div style="color:var(--text-muted);font-size:13px;">点击城市后加载...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Risk Profile ── -->
|
||||
<section class="risk-section">
|
||||
<h3>⚠️ 数据偏差风险</h3>
|
||||
|
||||
@@ -804,6 +804,95 @@ body {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ── Market Odds Section ── */
|
||||
.market-odds {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
Reference in New Issue
Block a user