Handle range-based Polymarket temperature buckets
This commit is contained in:
@@ -95,23 +95,42 @@ function findMarketBucketForDisplayTemp(
|
|||||||
displayTemp: number | null,
|
displayTemp: number | null,
|
||||||
detail: Pick<CityDetail, "temp_symbol">,
|
detail: Pick<CityDetail, "temp_symbol">,
|
||||||
) {
|
) {
|
||||||
const marketTemp = displayTempToMarketCelsius(displayTemp, detail);
|
if (displayTemp == null || !Number.isFinite(displayTemp)) return null;
|
||||||
if (marketTemp == null) return null;
|
|
||||||
|
|
||||||
const tolerance = isFahrenheitSymbol(detail.temp_symbol) ? 0.56 : 0.26;
|
|
||||||
let best: MarketTopBucket | null = null;
|
let best: MarketTopBucket | null = null;
|
||||||
let bestDelta = Number.POSITIVE_INFINITY;
|
let bestDelta = Number.POSITIVE_INFINITY;
|
||||||
for (const bucket of buckets) {
|
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;
|
const rawTemp = bucket.temp ?? bucket.value ?? null;
|
||||||
if (rawTemp == null) continue;
|
if (rawTemp == null) continue;
|
||||||
const candidateTemp = Number(rawTemp);
|
const candidateTemp = Number(rawTemp);
|
||||||
if (!Number.isFinite(candidateTemp)) continue;
|
if (!Number.isFinite(candidateTemp)) continue;
|
||||||
const delta = Math.abs(candidateTemp - marketTemp);
|
const delta = Math.abs(candidateTemp - compareTemp);
|
||||||
if (delta < bestDelta) {
|
if (delta < bestDelta) {
|
||||||
best = bucket;
|
best = bucket;
|
||||||
bestDelta = delta;
|
bestDelta = delta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const tolerance = isFahrenheitSymbol(detail.temp_symbol) ? 0.56 : 0.26;
|
||||||
return best && bestDelta <= tolerance ? best : null;
|
return best && bestDelta <= tolerance ? best : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -310,6 +310,9 @@ export interface MarketTopBucket {
|
|||||||
label?: string | null;
|
label?: string | null;
|
||||||
value?: number | null;
|
value?: number | null;
|
||||||
temp?: number | null;
|
temp?: number | null;
|
||||||
|
lower?: number | null;
|
||||||
|
upper?: number | null;
|
||||||
|
unit?: string | null;
|
||||||
probability?: number | null;
|
probability?: number | null;
|
||||||
market_price?: number | null;
|
market_price?: number | null;
|
||||||
yes_buy?: number | null;
|
yes_buy?: number | null;
|
||||||
|
|||||||
@@ -1249,20 +1249,69 @@ class PolymarketReadOnlyLayer:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _extract_market_bucket_temp(self, market: Dict[str, Any]) -> Optional[float]:
|
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(
|
text = " ".join(
|
||||||
str(part or "")
|
str(part or "")
|
||||||
for part in (
|
for part in (
|
||||||
market.get("question"),
|
market.get("question"),
|
||||||
market.get("title"),
|
market.get("title"),
|
||||||
market.get("slug"),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if not text:
|
# Match range buckets such as "80-81°F" and "80 to 81F".
|
||||||
return None
|
range_match = re.search(
|
||||||
# Match "... 9°C ..." / "... 9C ..." / "... -2 C ..."
|
r"(-?\d+(?:\.\d+)?)\s*(?:-|–|—|\bto\b)\s*(-?\d+(?:\.\d+)?)\s*°?\s*([cf])\b",
|
||||||
match = re.search(r"(-?\d+(?:\.\d+)?)\s*°?\s*c\b", text, re.IGNORECASE)
|
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:
|
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
|
return None
|
||||||
|
|
||||||
def _build_weather_event_slug(self, city_key: str, target_date: str) -> Optional[str]:
|
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],
|
Dict[str, Any],
|
||||||
Dict[str, Any],
|
Dict[str, Any],
|
||||||
|
Optional[Tuple[float, Optional[float], str]],
|
||||||
]
|
]
|
||||||
] = []
|
] = []
|
||||||
for market in candidate_markets:
|
for market in candidate_markets:
|
||||||
if not self._market_trade_state(market).get("tradable"):
|
if not self._market_trade_state(market).get("tradable"):
|
||||||
continue
|
continue
|
||||||
bucket_temp = self._extract_market_bucket_temp(market)
|
bucket_temp = self._extract_market_bucket_temp(market)
|
||||||
|
bucket_range = self._extract_market_bucket_range(market)
|
||||||
if bucket_temp is None:
|
if bucket_temp is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -1658,6 +1709,7 @@ class PolymarketReadOnlyLayer:
|
|||||||
no_token,
|
no_token,
|
||||||
yes_prices,
|
yes_prices,
|
||||||
no_prices,
|
no_prices,
|
||||||
|
bucket_range,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1681,6 +1733,7 @@ class PolymarketReadOnlyLayer:
|
|||||||
no_token,
|
no_token,
|
||||||
yes_prices,
|
yes_prices,
|
||||||
no_prices,
|
no_prices,
|
||||||
|
bucket_range,
|
||||||
) in ranked:
|
) in ranked:
|
||||||
row_direction = self._extract_market_bucket_direction(market)
|
row_direction = self._extract_market_bucket_direction(market)
|
||||||
if (
|
if (
|
||||||
@@ -1713,6 +1766,9 @@ class PolymarketReadOnlyLayer:
|
|||||||
"label": self._extract_market_bucket_label(market, bucket_temp),
|
"label": self._extract_market_bucket_label(market, bucket_temp),
|
||||||
"value": bucket_temp,
|
"value": bucket_temp,
|
||||||
"temp": 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,
|
"probability": market_prob,
|
||||||
"market_price": yes_midpoint,
|
"market_price": yes_midpoint,
|
||||||
"yes_buy": yes_buy,
|
"yes_buy": yes_buy,
|
||||||
@@ -1843,12 +1899,16 @@ class PolymarketReadOnlyLayer:
|
|||||||
) -> str:
|
) -> str:
|
||||||
question = str(market.get("question") or market.get("title") or "").strip()
|
question = str(market.get("question") or market.get("title") or "").strip()
|
||||||
direction = self._extract_market_bucket_direction(market)
|
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 bucket_temp is not None:
|
||||||
if direction == "above":
|
if direction == "above":
|
||||||
return f"{bucket_temp:g}C+"
|
return f"{bucket_temp:g}{unit}+"
|
||||||
if direction == "below":
|
if direction == "below":
|
||||||
return f"<={bucket_temp:g}C"
|
return f"<={bucket_temp:g}{unit}"
|
||||||
return f"{bucket_temp:g}C"
|
return f"{bucket_temp:g}{unit}"
|
||||||
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:
|
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
|
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():
|
def test_fetch_token_market_data_prefers_orderbook_executable_prices():
|
||||||
class FakeClob:
|
class FakeClob:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
Reference in New Issue
Block a user