feat: Integrate Polymarket read-only data collection and display in dashboard panels.
This commit is contained in:
@@ -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 (
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user