diff --git a/frontend/components/dashboard/PanelSections.tsx b/frontend/components/dashboard/PanelSections.tsx index 9ae7a446..37376b28 100644 --- a/frontend/components/dashboard/PanelSections.tsx +++ b/frontend/components/dashboard/PanelSections.tsx @@ -63,31 +63,6 @@ function getBucketTemp(bucket: ProbabilityBucket) { return parseTempFromText(bucket.label || bucket.bucket || bucket.range); } -function getMarketBucketTemp(scan?: MarketScan | null) { - if (!scan) return null; - - if (scan.temperature_bucket?.value != null) { - const byBucketValue = Number(scan.temperature_bucket.value); - if (Number.isFinite(byBucketValue)) return byBucketValue; - } - - const byBucketLabel = parseTempFromText( - scan.temperature_bucket?.label || - scan.temperature_bucket?.bucket || - scan.temperature_bucket?.range, - ); - if (byBucketLabel != null) return byBucketLabel; - - const slug = String(scan.selected_slug || scan.primary_market?.slug || ""); - const slugMatch = slug.match(/-(-?\d+(?:\.\d+)?)c(?:$|[^a-z0-9])/i); - if (slugMatch) { - const numeric = Number(slugMatch[1]); - if (Number.isFinite(numeric)) return numeric; - } - - return parseTempFromText(scan.primary_market?.question); -} - function getMarketYesPrice(scan?: MarketScan | null) { if (scan?.market_price != null) { const preferred = Number(scan.market_price); @@ -100,6 +75,46 @@ function getMarketYesPrice(scan?: MarketScan | null) { return null; } +function isFahrenheitSymbol(symbol?: string | null) { + return String(symbol || "").toUpperCase().includes("F"); +} + +function displayTempToMarketCelsius( + value: number | null, + detail: Pick, +) { + if (value == null || !Number.isFinite(value)) return null; + if (isFahrenheitSymbol(detail.temp_symbol)) { + return ((value - 32) * 5) / 9; + } + return value; +} + +function findMarketBucketForDisplayTemp( + buckets: MarketTopBucket[], + displayTemp: number | null, + detail: Pick, +) { + const marketTemp = displayTempToMarketCelsius(displayTemp, detail); + if (marketTemp == null) 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 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); + if (delta < bestDelta) { + best = bucket; + bestDelta = delta; + } + } + return best && bestDelta <= tolerance ? best : null; +} + type ModelMetadata = NonNullable< NonNullable["open_meteo_multi_model"] >["model_metadata"]; @@ -633,7 +648,6 @@ export function ProbabilityDistribution({ }) { const { locale, t } = useI18n(); const view = getProbabilityView(detail, targetDate); - const marketBucketTemp = getMarketBucketTemp(marketScan); const marketYesPrice = getMarketYesPrice(marketScan); const marketYesText = toPercent(marketYesPrice); const isToday = !targetDate || targetDate === detail.local_date; @@ -684,15 +698,12 @@ export function ProbabilityDistribution({ const topProbabilityTemp = topProbability ? getBucketTemp(topProbability) : null; const linkedMarketBucket = useMemo(() => { if (topProbabilityTemp == null) return null; - return ( - marketAllBuckets.find((bucket) => { - const bucketTemp = bucket.temp ?? bucket.value ?? null; - if (bucketTemp == null) return false; - const numeric = Number(bucketTemp); - return Number.isFinite(numeric) && Math.abs(numeric - topProbabilityTemp) < 0.26; - }) || null + return findMarketBucketForDisplayTemp( + marketAllBuckets, + topProbabilityTemp, + detail, ); - }, [marketAllBuckets, topProbabilityTemp]); + }, [detail, marketAllBuckets, topProbabilityTemp]); const priceAnalysis = marketScan?.price_analysis; const yesPriceView = priceAnalysis?.yes; const noPriceView = priceAnalysis?.no; @@ -936,13 +947,15 @@ export function ProbabilityDistribution({ Number(bucket.probability || 0) * 100, ); const bucketTemp = getBucketTemp(bucket); - const isMarketBucket = - marketYesText != null && - marketBucketTemp != null && - bucketTemp != null && - Math.abs(bucketTemp - marketBucketTemp) < 0.26; - const yesPriceText = toPriceCents(marketYesPrice); - const marketTagFinal = isMarketBucket + const rowMarketBucket = findMarketBucketForDisplayTemp( + marketAllBuckets, + bucketTemp, + detail, + ); + const rowMarketPrice = + rowMarketBucket?.market_price ?? rowMarketBucket?.yes_buy ?? null; + const yesPriceText = toPriceCents(rowMarketPrice); + const marketTagFinal = rowMarketBucket ? locale === "en-US" ? `Market ref: ${yesPriceText || "--"}` : `市场参考: ${yesPriceText || "--"}` @@ -951,9 +964,11 @@ export function ProbabilityDistribution({ bucket.label || `${bucket.value}${detail.temp_symbol}`; if (bucketLabel) { let str = String(bucketLabel).toUpperCase().replace(/\s+/g, ""); - str = str.replace(/°?C($|\+|-)/g, "℃$1"); - if (!str.includes("℃") && /[0-9]/.test(str)) { - str += "℃"; + const symbol = detail.temp_symbol || "°C"; + str = str.replace(/°?C($|\+|-)/g, "°C$1"); + str = str.replace(/°?F($|\+|-)/g, "°F$1"); + if (!/[°℃][CF]/.test(str) && /[0-9]/.test(str)) { + str += symbol; } bucketLabel = str; } @@ -976,7 +991,7 @@ export function ProbabilityDistribution({
{marketTagFinal} diff --git a/src/data_collection/polymarket_ws_cache.py b/src/data_collection/polymarket_ws_cache.py index fcfb287c..aba3bebb 100644 --- a/src/data_collection/polymarket_ws_cache.py +++ b/src/data_collection/polymarket_ws_cache.py @@ -35,6 +35,14 @@ def _safe_float(value: Any) -> Optional[float]: return None +def _first_float(*values: Any) -> Optional[float]: + for value in values: + parsed = _safe_float(value) + if parsed is not None: + return parsed + return None + + def _env_bool(name: str, default: bool = False) -> bool: raw = os.getenv(name) if raw is None: @@ -327,15 +335,15 @@ class PolymarketWsQuoteCache: if not asset_id: return - best_bid = ( - _safe_float(item.get("best_bid")) - or _safe_float(item.get("bid")) - or _safe_float(item.get("bestBid")) + best_bid = _first_float( + item.get("best_bid"), + item.get("bid"), + item.get("bestBid"), ) - best_ask = ( - _safe_float(item.get("best_ask")) - or _safe_float(item.get("ask")) - or _safe_float(item.get("bestAsk")) + best_ask = _first_float( + item.get("best_ask"), + item.get("ask"), + item.get("bestAsk"), ) if event_type == "book": parsed_bid, parsed_ask = self._extract_book_top(item) @@ -345,9 +353,9 @@ class PolymarketWsQuoteCache: side = str(item.get("side") or "").strip().upper() if event_type == "price_change" and price is not None: if side == "BUY": - best_ask = price - elif side == "SELL": best_bid = price + elif side == "SELL": + best_ask = price last_trade = ( _safe_float(item.get("last_trade_price")) diff --git a/tests/test_polymarket_ws_cache.py b/tests/test_polymarket_ws_cache.py index f232b842..bacddcb1 100644 --- a/tests/test_polymarket_ws_cache.py +++ b/tests/test_polymarket_ws_cache.py @@ -63,8 +63,8 @@ def test_ws_cache_parses_price_change_side_updates(): data = cache.get_market_data("asset-1") assert data is not None - assert data["buy"] == 0.48 - assert data["sell"] == 0.45 + assert data["buy"] == 0.45 + assert data["sell"] == 0.48 def test_ws_cache_parses_price_changes_key_and_book_event(): @@ -94,8 +94,8 @@ def test_ws_cache_parses_price_changes_key_and_book_event(): data = cache.get_market_data("asset-1") assert data is not None - assert data["sell"] == 0.44 - assert data["buy"] == 0.48 + assert data["sell"] == 0.48 + assert data["buy"] == 0.49 def test_ws_cache_subscription_payloads_match_market_channel_shape():