Handle range-based Polymarket temperature buckets
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user