Handle range-based Polymarket temperature buckets

This commit is contained in:
2569718930@qq.com
2026-04-22 00:38:19 +08:00
parent 5a1610d428
commit e5facb041c
4 changed files with 107 additions and 13 deletions
@@ -95,23 +95,42 @@ function findMarketBucketForDisplayTemp(
displayTemp: number | null,
detail: Pick<CityDetail, "temp_symbol">,
) {
const marketTemp = displayTempToMarketCelsius(displayTemp, detail);
if (marketTemp == null) return null;
if (displayTemp == null || !Number.isFinite(displayTemp)) return null;
const tolerance = isFahrenheitSymbol(detail.temp_symbol) ? 0.56 : 0.26;
let best: MarketTopBucket | null = null;
let bestDelta = Number.POSITIVE_INFINITY;
for (const bucket of buckets) {
const bucketUnit = String(bucket.unit || "").toUpperCase();
const compareTemp =
bucketUnit === "F"
? displayTemp
: displayTempToMarketCelsius(displayTemp, detail);
if (compareTemp == null) continue;
const lower = bucket.lower != null ? Number(bucket.lower) : null;
const upper = bucket.upper != null ? Number(bucket.upper) : null;
if (
lower != null &&
upper != null &&
Number.isFinite(lower) &&
Number.isFinite(upper) &&
compareTemp >= lower - 0.01 &&
compareTemp <= upper + 0.01
) {
return bucket;
}
const rawTemp = bucket.temp ?? bucket.value ?? null;
if (rawTemp == null) continue;
const candidateTemp = Number(rawTemp);
if (!Number.isFinite(candidateTemp)) continue;
const delta = Math.abs(candidateTemp - marketTemp);
const delta = Math.abs(candidateTemp - compareTemp);
if (delta < bestDelta) {
best = bucket;
bestDelta = delta;
}
}
const tolerance = isFahrenheitSymbol(detail.temp_symbol) ? 0.56 : 0.26;
return best && bestDelta <= tolerance ? best : null;
}
+3
View File
@@ -310,6 +310,9 @@ export interface MarketTopBucket {
label?: string | null;
value?: number | null;
temp?: number | null;
lower?: number | null;
upper?: number | null;
unit?: string | null;
probability?: number | null;
market_price?: number | null;
yes_buy?: number | null;
+69 -9
View File
@@ -1249,20 +1249,69 @@ class PolymarketReadOnlyLayer:
return None
def _extract_market_bucket_temp(self, market: Dict[str, Any]) -> Optional[float]:
parsed = self._extract_market_bucket_range(market)
if parsed:
lower, upper, _unit = parsed
if upper is not None:
return (lower + upper) / 2.0
return lower
return None
def _extract_market_bucket_range(
self,
market: Dict[str, Any],
) -> Optional[Tuple[float, Optional[float], str]]:
slug = str(market.get("slug") or "").strip().lower()
slug_range = re.search(
r"-(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)([cf])(?:$|-or-higher|-or-lower|orhigher|orlower)",
slug,
re.IGNORECASE,
)
if slug_range:
lower = _safe_float(slug_range.group(1))
upper = _safe_float(slug_range.group(2))
unit = slug_range.group(3).upper()
if lower is not None and upper is not None and abs(upper - lower) <= 20:
return min(lower, upper), max(lower, upper), unit
text = " ".join(
str(part or "")
for part in (
market.get("question"),
market.get("title"),
market.get("slug"),
)
)
if not text:
return None
# Match "... 9°C ..." / "... 9C ..." / "... -2 C ..."
match = re.search(r"(-?\d+(?:\.\d+)?)\s*°?\s*c\b", text, re.IGNORECASE)
# Match range buckets such as "80-81°F" and "80 to 81F".
range_match = re.search(
r"(-?\d+(?:\.\d+)?)\s*(?:-||—|\bto\b)\s*(-?\d+(?:\.\d+)?)\s*°?\s*([cf])\b",
text,
re.IGNORECASE,
)
if range_match:
lower = _safe_float(range_match.group(1))
upper = _safe_float(range_match.group(2))
unit = range_match.group(3).upper()
if lower is not None and upper is not None and abs(upper - lower) <= 20:
return min(lower, upper), max(lower, upper), unit
slug_exact = re.search(
r"-(\d+(?:\.\d+)?)([cf])(?:$|-or-higher|-or-lower|orhigher|orlower)",
slug,
re.IGNORECASE,
)
if slug_exact:
value = _safe_float(slug_exact.group(1))
unit = slug_exact.group(2).upper()
if value is not None:
return value, None, unit
# Match "... 9°C ..." / "... 9F ..." / "... -2 C ..."
match = re.search(r"(-?\d+(?:\.\d+)?)\s*°?\s*([cf])\b", text, re.IGNORECASE)
if match:
return _safe_float(match.group(1))
value = _safe_float(match.group(1))
unit = match.group(2).upper()
if value is not None:
return value, None, unit
return None
def _build_weather_event_slug(self, city_key: str, target_date: str) -> Optional[str]:
@@ -1605,12 +1654,14 @@ class PolymarketReadOnlyLayer:
Dict[str, Any],
Dict[str, Any],
Dict[str, Any],
Optional[Tuple[float, Optional[float], str]],
]
] = []
for market in candidate_markets:
if not self._market_trade_state(market).get("tradable"):
continue
bucket_temp = self._extract_market_bucket_temp(market)
bucket_range = self._extract_market_bucket_range(market)
if bucket_temp is None:
continue
@@ -1658,6 +1709,7 @@ class PolymarketReadOnlyLayer:
no_token,
yes_prices,
no_prices,
bucket_range,
)
)
@@ -1681,6 +1733,7 @@ class PolymarketReadOnlyLayer:
no_token,
yes_prices,
no_prices,
bucket_range,
) in ranked:
row_direction = self._extract_market_bucket_direction(market)
if (
@@ -1713,6 +1766,9 @@ class PolymarketReadOnlyLayer:
"label": self._extract_market_bucket_label(market, bucket_temp),
"value": bucket_temp,
"temp": bucket_temp,
"lower": bucket_range[0] if bucket_range else None,
"upper": bucket_range[1] if bucket_range else None,
"unit": bucket_range[2] if bucket_range else None,
"probability": market_prob,
"market_price": yes_midpoint,
"yes_buy": yes_buy,
@@ -1843,12 +1899,16 @@ class PolymarketReadOnlyLayer:
) -> str:
question = str(market.get("question") or market.get("title") or "").strip()
direction = self._extract_market_bucket_direction(market)
bucket_range = self._extract_market_bucket_range(market)
unit = bucket_range[2] if bucket_range else "C"
if bucket_range and bucket_range[1] is not None:
return f"{bucket_range[0]:g}-{bucket_range[1]:g}{unit}"
if bucket_temp is not None:
if direction == "above":
return f"{bucket_temp:g}C+"
return f"{bucket_temp:g}{unit}+"
if direction == "below":
return f"<={bucket_temp:g}C"
return f"{bucket_temp:g}C"
return f"<={bucket_temp:g}{unit}"
return f"{bucket_temp:g}{unit}"
return question or str(market.get("slug") or "")
def _extract_market_bucket_direction(self, market: Dict[str, Any]) -> str:
+12
View File
@@ -25,6 +25,18 @@ def test_normalize_orderbook_uses_sorted_best_prices():
assert book["ask_levels"][0][0] == 0.39
def test_extract_market_bucket_range_supports_fahrenheit_ranges():
layer = PolymarketReadOnlyLayer()
market = {
"question": "Will the highest temperature in Miami be between 80-81°F on April 21?",
"slug": "highest-temperature-in-miami-on-april-21-2026-80-81f",
}
assert layer._extract_market_bucket_range(market) == (80.0, 81.0, "F")
assert layer._extract_market_bucket_temp(market) == 80.5
assert layer._extract_market_bucket_label(market, 80.5) == "80-81F"
def test_fetch_token_market_data_prefers_orderbook_executable_prices():
class FakeClob:
@staticmethod