From 1ef209ce2fc3fa05c819f1f2f7c9ee0546312b33 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Mon, 25 May 2026 07:37:43 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4=20REST=20=E4=BB=B7=E6=A0=BC?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=EF=BC=8C=E7=BA=AF=20WebSocket=20=E6=8A=A5?= =?UTF-8?q?=E4=BB=B7=EF=BC=9B=E9=BB=98=E8=AE=A4=E5=BC=80=E5=90=AF=20WS=20?= =?UTF-8?q?=E4=BB=B7=E6=A0=BC=EF=BC=9B=E4=BF=AE=E5=A4=8D=E5=9B=BE=E8=A1=A8?= =?UTF-8?q?=20TS=20=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LiveTemperatureThresholdChart.tsx | 144 ++++++++++++++---- src/data_collection/polymarket_readonly.py | 132 +--------------- src/data_collection/polymarket_ws_cache.py | 2 +- 3 files changed, 119 insertions(+), 159 deletions(-) diff --git a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx index 982de188..268cefd6 100644 --- a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx +++ b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx @@ -41,39 +41,80 @@ function validNumber(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; } -function toTimestamp(value?: string | null): number | null { - const raw = String(value || "").trim(); +function getCityLocalUtcTimestamp( + value: string | number | null | undefined, + tzOffsetSeconds: number, + referenceLocalDate?: string | null +): number | null { + if (value == null) return null; + + if (typeof value === "number") { + const d = new Date(value + tzOffsetSeconds * 1000); + return Date.UTC( + d.getUTCFullYear(), + d.getUTCMonth(), + d.getUTCDate(), + d.getUTCHours(), + d.getUTCMinutes() + ); + } + + const raw = String(value).trim(); if (!raw) return null; - const d = new Date(raw); - if (!Number.isNaN(d.getTime())) return d.getTime(); - // HH:MM or HH:MM:SS — treat as today, but handle cross-midnight: - // if parsed time is >2h ahead of now, assume yesterday + + if (raw.includes("T") || raw.includes("Z") || raw.includes("-")) { + const d = new Date(raw); + if (!Number.isNaN(d.getTime())) { + const localMs = d.getTime() + tzOffsetSeconds * 1000; + const localDate = new Date(localMs); + return Date.UTC( + localDate.getUTCFullYear(), + localDate.getUTCMonth(), + localDate.getUTCDate(), + localDate.getUTCHours(), + localDate.getUTCMinutes() + ); + } + } + const m = raw.match(/(\d{1,2}):(\d{2})/); if (m) { - const now = new Date(); - const h = +m[1], min = +m[2]; - const candidate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), h, min); - if (candidate.getTime() - now.getTime() > 2 * 60 * 60 * 1000) { - candidate.setDate(candidate.getDate() - 1); + const h = +m[1]; + const min = +m[2]; + + let year = new Date().getUTCFullYear(); + let month = new Date().getUTCMonth(); + let date = new Date().getUTCDate(); + + if (referenceLocalDate) { + const dateParts = referenceLocalDate.split("-"); + if (dateParts.length === 3) { + year = parseInt(dateParts[0]); + month = parseInt(dateParts[1]) - 1; + date = parseInt(dateParts[2]); + } } - return candidate.getTime(); + + return Date.UTC(year, month, date, h, min); } + return null; } function formatTimestamp(ts: number): string { const d = new Date(ts); - return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; + return `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}`; } -function normObs(points?: ObsPoint[] | null, limit = MAX_OBS_POINTS) { +function normObs(points: ObsPoint[] | null | undefined, tzOffsetSeconds: number, limit = MAX_OBS_POINTS) { return (points || []) - .filter((p) => validNumber(p.temp) !== null && toTimestamp(p.time) !== null) - .slice(-limit) + .filter((p) => validNumber(p.temp) !== null && p.time) .map((p) => ({ - ts: toTimestamp(p.time)!, + ts: getCityLocalUtcTimestamp(p.time, tzOffsetSeconds)!, value: Number(p.temp), - })); + })) + .filter((p) => p.ts !== null) + .slice(-limit); } function seriesStats(values: Array) { @@ -99,8 +140,8 @@ function buildSlidingChartData( row: ScanOpportunityRow | null, hourly: HourlyForecast, ) { - const settlementObs = normObs(row?.settlement_today_obs || row?.metar_context?.settlement_today_obs); - const metarObs = normObs(row?.metar_today_obs || row?.metar_context?.today_obs || row?.metar_recent_obs || row?.metar_context?.recent_obs); + const settlementObs = normObs(row?.settlement_today_obs || row?.metar_context?.settlement_today_obs, 0); + const metarObs = normObs(row?.metar_today_obs || row?.metar_context?.today_obs || row?.metar_recent_obs || row?.metar_context?.recent_obs, 0); // Collect all timestamps from observations + forecasts const allTimes = new Set(); @@ -320,9 +361,11 @@ function buildChartDomain( export function LiveTemperatureThresholdChart({ isEn, row, + allRows = [], }: { isEn: boolean; row: ScanOpportunityRow | null; + allRows?: ScanOpportunityRow[]; }) { const [hourly, setHourly] = useState(null); const city = String(row?.city || "").toLowerCase().trim(); @@ -362,6 +405,36 @@ export function LiveTemperatureThresholdChart({ const { data, series } = useMemo(() => buildSlidingChartData(row, hourly), [row, hourly]); const threshold = validNumber(row?.target_threshold) ?? validNumber(row?.target_value); + + const cityThresholds = useMemo(() => { + if (!row || !allRows || !allRows.length) return []; + const cityKey = String(row.city || "").toLowerCase().trim(); + const sameCityRows = allRows.filter( + (r) => String(r.city || "").toLowerCase().trim() === cityKey + ); + + const seen = new Set(); + const list: { threshold: number; label: string; isBreached: boolean; kind: "gte" | "lte" }[] = []; + sameCityRows.forEach((r) => { + const t = Number(r.target_threshold ?? r.target_value ?? r.target_lower ?? r.target_upper); + if (!Number.isFinite(t) || seen.has(t)) return; + seen.add(t); + + const maxTemp = Number(r.current_max_so_far ?? r.current_temp ?? 0); + const q = String(r.market_question || r.target_label || "").toLowerCase(); + const kind: "gte" | "lte" = q.includes("below") || q.includes("under") || q.includes("lte") ? "lte" : "gte"; + const isBreached = kind === "lte" ? maxTemp > t : maxTemp >= t; + + list.push({ + threshold: t, + label: r.target_label || `${t}°C`, + isBreached, + kind, + }); + }); + + return list.sort((a, b) => a.threshold - b.threshold); + }, [row, allRows]); const modelSummaryCards = useMemo(() => { const cards = buildModelSummaryCards(row); if (!hourly?.modelCurves) return cards; @@ -449,15 +522,28 @@ export function LiveTemperatureThresholdChart({ domain={chartDomain} ticks={marketTicks ?? undefined} /> - {threshold !== null && ( - - )} + {cityThresholds.map((t, idx) => { + const isSelected = row && (Number(row.target_threshold ?? row.target_value) === t.threshold); + const labelText = isEn + ? `${t.kind === "gte" ? "≥" : "≤"} ${t.threshold.toFixed(1)}° [${t.isBreached ? "Excluded" : "Active"}]` + : `${t.kind === "gte" ? "≥" : "≤"} ${t.threshold.toFixed(1)}° [${t.isBreached ? "已排除" : "活跃"}]`; + + return ( + + ); + })} Dict[str, Any]: # REST-only path: CLOB public endpoints. @@ -2723,128 +2718,7 @@ class PolymarketReadOnlyLayer: results[token_id] = ws_data missing.remove(token_id) - if not missing: - return results - - buy_map: Dict[str, float] = {} - sell_map: Dict[str, float] = {} - midpoint_map: Dict[str, float] = {} - spread_map: Dict[str, float] = {} - last_trade_map: Dict[str, float] = {} - book_map: Dict[str, Dict[str, Any]] = {} - - for chunk in self._batch_chunks(missing): - buy_payload = self._clob_post( - "/prices", - [{"token_id": token_id, "side": "BUY"} for token_id in chunk], - ) - sell_payload = self._clob_post( - "/prices", - [{"token_id": token_id, "side": "SELL"} for token_id in chunk], - ) - midpoint_payload = ( - self._clob_post( - "/midpoints", - [{"token_id": token_id} for token_id in chunk], - ) - if not self.fast_price_only - else None - ) - spread_payload = ( - self._clob_post( - "/spreads", - [{"token_id": token_id} for token_id in chunk], - ) - if not self.fast_price_only - else None - ) - last_trade_payload = ( - self._clob_post( - "/last-trade-prices", - [{"token_id": token_id} for token_id in chunk], - ) - if not self.fast_price_only - else None - ) - books_payload = ( - self._clob_post( - "/books", - [{"token_id": token_id} for token_id in chunk], - ) - if include_books and not self.fast_price_only - else None - ) - buy_map.update(self._extract_batch_price_map(buy_payload, "BUY")) - sell_map.update(self._extract_batch_price_map(sell_payload, "SELL")) - midpoint_map.update(self._extract_batch_scalar_map(midpoint_payload)) - spread_map.update(self._extract_batch_scalar_map(spread_payload)) - last_trade_map.update(self._extract_batch_scalar_map(last_trade_payload)) - if include_books: - book_map.update(self._extract_batch_book_map(books_payload)) - - fetched: Dict[str, Dict[str, Any]] = {} - unresolved: List[str] = [] - for token_id in missing: - raw_book = book_map.get(token_id) - book, book_liquidity = self._normalize_orderbook(raw_book) - buy_price = _extract_price(buy_map.get(token_id)) - sell_price = _extract_price(sell_map.get(token_id)) - midpoint = _extract_price(midpoint_map.get(token_id)) - spread = _extract_price(spread_map.get(token_id)) - last_trade = _extract_price(last_trade_map.get(token_id)) - - buy, sell = self._resolve_trade_prices( - buy=buy_price, - sell=sell_price, - book=book, - ) - if midpoint is None and buy is not None and sell is not None: - midpoint = (buy + sell) / 2.0 - if midpoint is None: - midpoint = _extract_price(raw_book.get("last_trade_price") if isinstance(raw_book, dict) else None) - if spread is None and buy is not None and sell is not None: - spread = max(0.0, float(buy) - float(sell)) - if spread is not None and midpoint is not None: - midpoint = _clamp_probability(midpoint) - if buy is None and midpoint is not None and spread is not None: - buy = _clamp_probability(midpoint + spread / 2.0) - if sell is None and midpoint is not None and spread is not None: - sell = _clamp_probability(midpoint - spread / 2.0) - - if ( - buy is None - and sell is None - and midpoint is None - and last_trade is None - and book is None - ): - unresolved.append(token_id) - continue - - fetched[token_id] = { - "buy": buy, - "sell": sell, - "midpoint": midpoint, - "spread": spread, - "last_trade_price": last_trade, - "quote_source": ( - "polymarket_clob_fast_batch" - if self.fast_price_only - else "polymarket_clob_rest_batch" - ), - "quote_age_ms": 0, - "book": book, - "book_liquidity": book_liquidity, - } - - for token_id in unresolved: - fetched[token_id] = self._fetch_token_market_data(token_id) - - with self._lock: - for token_id, data in fetched.items(): - self._price_cache[token_id] = {"data": data, "t": now} - - results.update(fetched) + # No REST fallback — prices exclusively from WebSocket. return results def _normalize_scan_filters(self, scan_filters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: @@ -3796,4 +3670,4 @@ class PolymarketReadOnlyLayer: "distribution_preview": distribution_preview[:6], "distribution_full": distribution_preview, "resolved_market_type": "maxtemp", - } + } \ No newline at end of file diff --git a/src/data_collection/polymarket_ws_cache.py b/src/data_collection/polymarket_ws_cache.py index 1fa92efa..25b66d7d 100644 --- a/src/data_collection/polymarket_ws_cache.py +++ b/src/data_collection/polymarket_ws_cache.py @@ -86,7 +86,7 @@ class PolymarketWsQuoteCache: @classmethod def from_env(cls) -> "PolymarketWsQuoteCache": return cls( - enabled=_env_bool("POLYMARKET_WS_PRICE_ENABLED", False), + enabled=_env_bool("POLYMARKET_WS_PRICE_ENABLED", True), endpoint=os.getenv("POLYMARKET_WS_MARKET_URL"), quote_ttl_sec=int(os.getenv("POLYMARKET_WS_QUOTE_TTL_SEC", "8")), max_assets=int(os.getenv("POLYMARKET_WS_MAX_ASSETS", "256")),