feat: Integrate Polymarket read-only data collection and display in dashboard panels.

This commit is contained in:
2569718930@qq.com
2026-03-11 11:04:24 +08:00
parent 1958b2764b
commit d8cc193618
3 changed files with 216 additions and 49 deletions
@@ -119,6 +119,19 @@ function getMarketTopBuckets(scan?: MarketScan | null) {
); );
} }
function getMarketTopBucketKey(bucket: MarketTopBucket) {
const valueNum = Number(bucket?.value);
if (Number.isFinite(valueNum)) return `v:${valueNum.toFixed(2)}`;
const tempNum = Number(bucket?.temp);
if (Number.isFinite(tempNum)) return `t:${tempNum.toFixed(2)}`;
const parsed = parseTempFromText(bucket?.label);
if (parsed != null) return `l:${parsed.toFixed(2)}`;
return `s:${String(bucket?.slug || bucket?.question || bucket?.label || "")}`;
}
export function HeroSummary() { export function HeroSummary() {
const { data } = useCityData(); const { data } = useCityData();
const { locale } = useI18n(); const { locale } = useI18n();
@@ -367,11 +380,23 @@ export function ProbabilityDistribution({
const marketNoText = toPercent(marketNoPrice); const marketNoText = toPercent(marketNoPrice);
const isToday = !targetDate || targetDate === detail.local_date; const isToday = !targetDate || targetDate === detail.local_date;
const marketTopBuckets = isToday ? getMarketTopBuckets(marketScan) : []; const marketTopBuckets = isToday ? getMarketTopBuckets(marketScan) : [];
const sortedMarketTopBuckets = [...marketTopBuckets] const sortedMarketTopBuckets = (() => {
.sort((a, b) => Number(b.probability || 0) - Number(a.probability || 0)) const sorted = [...marketTopBuckets].sort(
.slice(0, 4); (a, b) => Number(b.probability || 0) - Number(a.probability || 0),
);
const deduped: Array<MarketTopBucket & { probability: number }> = [];
const seenKeys = new Set<string>();
for (const row of sorted) {
const key = getMarketTopBucketKey(row);
if (seenKeys.has(key)) continue;
seenKeys.add(key);
deduped.push(row);
if (deduped.length >= 4) break;
}
return deduped;
})();
const useMarketTopBuckets = const useMarketTopBuckets =
marketScan?.available && sortedMarketTopBuckets.length > 0; marketScan?.available && sortedMarketTopBuckets.length >= 2;
const topMarketBucketText = toPercent(sortedMarketTopBuckets[0]?.probability); const topMarketBucketText = toPercent(sortedMarketTopBuckets[0]?.probability);
return ( return (
+105 -45
View File
@@ -1332,52 +1332,72 @@ class PolymarketReadOnlyLayer:
top_rows: List[Dict[str, Any]] = [] top_rows: List[Dict[str, Any]] = []
max_items = max(1, int(limit or 4)) max_items = max(1, int(limit or 4))
primary_slug = str(primary_market.get("slug") or "").strip().lower() primary_slug = str(primary_market.get("slug") or "").strip().lower()
primary_direction = self._extract_market_bucket_direction(primary_market)
seen_temp_keys: set = set()
for ( def _append_rows(enforce_primary_direction: bool) -> None:
market_prob, for (
_volume, market_prob,
bucket_temp, _volume,
market, bucket_temp,
yes_token, market,
no_token, yes_token,
yes_prices, no_token,
no_prices, yes_prices,
) in ranked[ no_prices,
:max_items ) in ranked:
]: row_direction = self._extract_market_bucket_direction(market)
yes_buy = _extract_price(yes_prices.get("buy")) if (
yes_sell = _extract_price(yes_prices.get("sell")) enforce_primary_direction
yes_midpoint = _extract_price(yes_prices.get("midpoint")) or market_prob and primary_direction in {"above", "below"}
no_buy = _extract_price(no_prices.get("buy")) and row_direction != primary_direction
no_sell = _extract_price(no_prices.get("sell")) ):
continue
if no_buy is None and yes_buy is not None: temp_key = f"{round(float(bucket_temp), 2):.2f}"
no_buy = max(0.0, min(1.0, 1.0 - yes_buy)) if temp_key in seen_temp_keys:
if no_sell is None and yes_sell is not None: continue
no_sell = max(0.0, min(1.0, 1.0 - yes_sell))
market_slug = str(market.get("slug") or "").strip() yes_buy = _extract_price(yes_prices.get("buy"))
yes_sell = _extract_price(yes_prices.get("sell"))
yes_midpoint = _extract_price(yes_prices.get("midpoint")) or market_prob
no_buy = _extract_price(no_prices.get("buy"))
no_sell = _extract_price(no_prices.get("sell"))
top_rows.append( if no_buy is None and yes_buy is not None:
{ no_buy = max(0.0, min(1.0, 1.0 - yes_buy))
"label": self._extract_market_bucket_label(market, bucket_temp), if no_sell is None and yes_sell is not None:
"value": bucket_temp, no_sell = max(0.0, min(1.0, 1.0 - yes_sell))
"temp": bucket_temp,
"probability": market_prob, market_slug = str(market.get("slug") or "").strip()
"market_price": yes_midpoint, top_rows.append(
"yes_buy": yes_buy, {
"yes_sell": yes_sell, "label": self._extract_market_bucket_label(market, bucket_temp),
"no_buy": no_buy, "value": bucket_temp,
"no_sell": no_sell, "temp": bucket_temp,
"slug": market_slug or None, "probability": market_prob,
"question": market.get("question") or market.get("title"), "market_price": yes_midpoint,
"is_primary": bool( "yes_buy": yes_buy,
primary_slug "yes_sell": yes_sell,
and market_slug "no_buy": no_buy,
and primary_slug == market_slug.strip().lower() "no_sell": no_sell,
), "slug": market_slug or None,
} "question": market.get("question") or market.get("title"),
) "is_primary": bool(
primary_slug
and market_slug
and primary_slug == market_slug.strip().lower()
),
}
)
seen_temp_keys.add(temp_key)
if len(top_rows) >= max_items:
break
if primary_direction in {"above", "below"}:
_append_rows(enforce_primary_direction=True)
if len(top_rows) < max_items:
_append_rows(enforce_primary_direction=False)
return top_rows return top_rows
@@ -1480,11 +1500,51 @@ class PolymarketReadOnlyLayer:
bucket_temp: Optional[float], bucket_temp: Optional[float],
) -> str: ) -> str:
question = str(market.get("question") or market.get("title") or "").strip() question = str(market.get("question") or market.get("title") or "").strip()
text = question.lower() direction = self._extract_market_bucket_direction(market)
if bucket_temp is not None: if bucket_temp is not None:
if "or higher" in text or "or above" in text or "and above" in text: if direction == "above":
return f"{bucket_temp:g}C+" return f"{bucket_temp:g}C+"
if "or lower" in text or "or below" in text or "and below" in text: if direction == "below":
return f"<={bucket_temp:g}C" return f"<={bucket_temp:g}C"
return f"{bucket_temp:g}C" return f"{bucket_temp:g}C"
return question or str(market.get("slug") or "") return question or str(market.get("slug") or "")
def _extract_market_bucket_direction(self, market: Dict[str, Any]) -> str:
text = " ".join(
str(part or "")
for part in (
market.get("question"),
market.get("title"),
market.get("slug"),
)
).lower()
if not text:
return "exact"
if any(
token in text
for token in (
"or higher",
"or above",
"and above",
"forhigher",
"forabove",
"or-higher",
"or-above",
)
):
return "above"
if any(
token in text
for token in (
"or lower",
"or below",
"and below",
"forlower",
"forbelow",
"or-lower",
"or-below",
)
):
return "below"
return "exact"
+82
View File
@@ -60,3 +60,85 @@ def test_fetch_token_market_data_prefers_orderbook_executable_prices():
assert data["midpoint"] == 0.5 assert data["midpoint"] == 0.5
assert data["last_trade_price"] == 0.49 assert data["last_trade_price"] == 0.49
def test_build_top_temperature_buckets_dedupes_same_temperature():
layer = PolymarketReadOnlyLayer()
primary_market = {
"slug": "highest-temperature-in-ankara-on-march-12-2026-14c-or-higher",
"question": "Will the highest temperature in Ankara be 14C or higher on March 12?",
"volumeNum": 1000,
}
markets = [
primary_market,
{
"slug": "highest-temperature-in-ankara-on-march-12-2026-14c-or-higher-v2",
"question": "Will the highest temperature in Ankara be 14C or higher on March 12? (v2)",
"volumeNum": 900,
},
{
"slug": "highest-temperature-in-ankara-on-march-12-2026-13c-or-higher",
"question": "Will the highest temperature in Ankara be 13C or higher on March 12?",
"volumeNum": 1100,
},
{
"slug": "highest-temperature-in-ankara-on-march-12-2026-12c-or-higher",
"question": "Will the highest temperature in Ankara be 12C or higher on March 12?",
"volumeNum": 1200,
},
{
"slug": "highest-temperature-in-ankara-on-march-12-2026-14c-or-lower",
"question": "Will the highest temperature in Ankara be 14C or lower on March 12?",
"volumeNum": 1300,
},
]
layer._collect_related_temperature_markets = (
lambda city_key, target_date, primary_market: markets
)
def _fake_extract_market_tokens(market):
slug = str(market.get("slug") or "")
return [
{"outcome": "Yes", "token_id": f"{slug}|yes"},
{"outcome": "No", "token_id": f"{slug}|no"},
]
layer._extract_market_tokens = _fake_extract_market_tokens
midpoint_map = {
"highest-temperature-in-ankara-on-march-12-2026-14c-or-higher": 0.79,
"highest-temperature-in-ankara-on-march-12-2026-14c-or-higher-v2": 0.16,
"highest-temperature-in-ankara-on-march-12-2026-13c-or-higher": 0.06,
"highest-temperature-in-ankara-on-march-12-2026-12c-or-higher": 0.01,
"highest-temperature-in-ankara-on-march-12-2026-14c-or-lower": 0.92,
}
def _fake_get_token_market_data(token_id):
slug, side = str(token_id).split("|", 1)
if side == "yes":
midpoint = midpoint_map.get(slug, 0.5)
return {
"midpoint": midpoint,
"buy": max(0.0, min(1.0, midpoint + 0.01)),
"sell": max(0.0, min(1.0, midpoint - 0.01)),
}
midpoint = 1.0 - midpoint_map.get(slug, 0.5)
return {
"midpoint": midpoint,
"buy": max(0.0, min(1.0, midpoint + 0.01)),
"sell": max(0.0, min(1.0, midpoint - 0.01)),
}
layer._get_token_market_data = _fake_get_token_market_data
rows = layer._build_top_temperature_buckets(
city_key="ankara",
target_date="2026-03-12",
primary_market=primary_market,
limit=4,
)
values = [row.get("value") for row in rows]
assert len(values) == len(set(values))
assert rows[0]["value"] == 14.0
assert all(not str(row.get("label") or "").startswith("<=") for row in rows)