清理最后一批 Polymarket 注释+死代码 WeatherDecisionBand

This commit is contained in:
2569718930@qq.com
2026-05-25 20:53:03 +08:00
parent 11a901d22e
commit 7b8826dfa1
13 changed files with 328 additions and 112 deletions
@@ -46,6 +46,7 @@ import { TrainingDashboard } from "@/components/dashboard/scan-terminal/Training
import { LiveTemperatureThresholdChart } from "@/components/dashboard/scan-terminal/LiveTemperatureThresholdChart";
import { KoyfinRowsTable } from "@/components/dashboard/scan-terminal/KoyfinRowsTable";
import { rowName, pct, money, temp, edgeClass } from "@/components/dashboard/scan-terminal/utils";
import { CitySelectorDropdown } from "@/components/dashboard/scan-terminal/CitySelectorDropdown";
function createEmptyAccess(loading = true): ProAccessState {
return {
@@ -248,20 +249,21 @@ function EmptySlotCard({
slotIndex,
isActive,
isEn,
availableCities,
onSelectSlot,
onSelectCity,
onOpenSearch,
}: {
slotIndex: number;
isActive: boolean;
isEn: boolean;
availableCities: { city: string; name: string }[];
onSelectSlot: () => void;
onSelectCity: (city: string) => void;
onOpenSearch: () => void;
}) {
return (
<div
onClick={onSelectSlot}
onClick={() => {
onSelectSlot();
onOpenSearch();
}}
className={clsx(
"flex flex-col items-center justify-center h-full rounded-[4px] border-2 border-dashed p-6 cursor-pointer bg-slate-50/50 transition-all",
isActive
@@ -277,24 +279,15 @@ function EmptySlotCard({
</div>
<div className="text-[10px] text-slate-400 text-center mb-3 max-w-[180px]">
{isEn
? "Select this card and click a city on the left, or select below:"
: "激活此卡片并从左侧选择城市,或从下方直接选择:"}
? "Click to choose a city weather chart for this slot."
: "点击为该槽位选择一个城市天气图表。"}
</div>
<select
value=""
onClick={(e) => e.stopPropagation()} // prevent triggering onSelectSlot
onChange={(e) => {
if (e.target.value) onSelectCity(e.target.value);
}}
className="text-[11px] font-semibold text-slate-600 px-2 py-1 rounded border border-slate-300 bg-white outline-none max-w-[160px]"
<button
type="button"
className="text-[11px] font-semibold text-white px-3 py-1.5 rounded bg-blue-600 hover:bg-blue-700 transition-colors shadow-sm outline-none"
>
<option value="">{isEn ? "Choose city..." : "选择城市..."}</option>
{availableCities.map((c) => (
<option key={c.city} value={c.city}>
{c.name}
</option>
))}
</select>
{isEn ? "Choose City..." : "选择城市..."}
</button>
</div>
);
}
@@ -376,6 +369,7 @@ function PolyWeatherTerminal({
});
const [activeSlotIndex, setActiveSlotIndex] = useState<number>(0);
const [maximizedSlotIndex, setMaximizedSlotIndex] = useState<number | null>(null);
const [activeSearchSlotIndex, setActiveSearchSlotIndex] = useState<number | null>(null);
const NAV_ITEMS = [
{ key: "contracts", Icon: Table2, labelEn: "Contracts", labelZh: "天气合约" },
@@ -388,6 +382,7 @@ function PolyWeatherTerminal({
}, [selectedRegionKey, setSelectedCity]);
const filteredRegionRows = useMemo(() => {
if (selectedRegionKey === "all") return rows;
return rows.filter(
(row) => getCityRegion(row) === selectedRegionKey,
);
@@ -395,7 +390,12 @@ function PolyWeatherTerminal({
useEffect(() => {
if (filteredRegionRows.length && slots.every((s) => s === null)) {
const next = [filteredRegionRows[0].city, null, null, null];
const next = [
filteredRegionRows[0]?.city || null,
filteredRegionRows[1]?.city || null,
filteredRegionRows[2]?.city || null,
filteredRegionRows[3]?.city || null,
];
setSlots(next);
try {
localStorage.setItem("polyweather_terminal_slots", JSON.stringify(next));
@@ -677,29 +677,19 @@ function PolyWeatherTerminal({
</div>
{/* Desktop layout */}
<div className="hidden h-full min-h-0 lg:grid lg:grid-cols-[0.96fr_1.72fr] gap-2">
<div className="flex min-h-0 flex-col gap-2">
<CityRegionList
isEn={isEn}
rows={filteredRegionRows}
selectedCity={slots[activeSlotIndex]}
onSelectCity={(city) => handleSelectCityForSlot(activeSlotIndex, city)}
slots={slots}
activeSlotIndex={activeSlotIndex}
/>
</div>
<div className="min-h-0">
<div className="hidden h-full min-h-0 lg:block">
<div className="h-full w-full min-h-0">
{maximizedSlotIndex !== null ? (
// Maximized view
<div
onClick={() => setActiveSlotIndex(maximizedSlotIndex)}
className={clsx(
"relative h-full rounded-[4px] border overflow-hidden border-blue-500 ring-2 ring-blue-500/20 shadow-md z-10"
"relative h-full rounded-[4px] border border-blue-500 ring-2 ring-blue-500/20 shadow-md z-10",
activeSearchSlotIndex === maximizedSlotIndex ? "" : "overflow-hidden"
)}
>
{/* Floating actions toolbar */}
<div className="absolute right-[110px] top-[6px] z-20 flex items-center gap-1.5">
<div className="absolute right-2 top-[38px] z-20 flex items-center gap-1.5">
<button
type="button"
onClick={(e) => {
@@ -736,7 +726,21 @@ function PolyWeatherTerminal({
row={filteredRegionRows.find((r) => String(r.city || "").toLowerCase() === slots[maximizedSlotIndex]) || null}
allRows={filteredRegionRows}
compact={false}
onSearchClick={() => setActiveSearchSlotIndex(maximizedSlotIndex)}
/>
{activeSearchSlotIndex === maximizedSlotIndex && (
<CitySelectorDropdown
isEn={isEn}
rows={filteredRegionRows}
onSelectCity={(city) => {
handleSelectCityForSlot(maximizedSlotIndex, city);
setActiveSearchSlotIndex(null);
}}
onClose={() => setActiveSearchSlotIndex(null)}
className="absolute left-3 top-9 z-50 w-[280px] bg-white border border-slate-200 rounded shadow-lg p-2"
/>
)}
</div>
) : (
// 2x2 grid layout
@@ -747,15 +751,28 @@ function PolyWeatherTerminal({
if (!cityInSlot) {
return (
<EmptySlotCard
key={slotIndex}
slotIndex={slotIndex}
isActive={isSlotActive}
isEn={isEn}
availableCities={availableCities}
onSelectSlot={() => setActiveSlotIndex(slotIndex)}
onSelectCity={(city) => handleSelectCityForSlot(slotIndex, city)}
/>
<div key={slotIndex} className="relative h-full">
<EmptySlotCard
slotIndex={slotIndex}
isActive={isSlotActive}
isEn={isEn}
onSelectSlot={() => setActiveSlotIndex(slotIndex)}
onOpenSearch={() => setActiveSearchSlotIndex(slotIndex)}
/>
{activeSearchSlotIndex === slotIndex && (
<CitySelectorDropdown
isEn={isEn}
rows={filteredRegionRows}
onSelectCity={(city) => {
handleSelectCityForSlot(slotIndex, city);
setActiveSearchSlotIndex(null);
}}
onClose={() => setActiveSearchSlotIndex(null)}
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-[280px] bg-white border border-slate-200 rounded shadow-lg p-2"
/>
)}
</div>
);
}
@@ -768,14 +785,15 @@ function PolyWeatherTerminal({
key={slotIndex}
onClick={() => setActiveSlotIndex(slotIndex)}
className={clsx(
"relative h-full rounded-[4px] border overflow-hidden transition-all",
"relative h-full rounded-[4px] border transition-all",
isSlotActive
? "border-blue-500 ring-2 ring-blue-500/20 shadow-md z-10"
: "border-[#d2d9e2] hover:border-slate-400"
: "border-[#d2d9e2] hover:border-slate-400",
activeSearchSlotIndex === slotIndex ? "" : "overflow-hidden"
)}
>
{/* Floating actions toolbar */}
<div className="absolute right-[110px] top-[6px] z-20 flex items-center gap-1.5">
<div className="absolute right-2 top-[38px] z-20 flex items-center gap-1.5">
<button
type="button"
onClick={(e) => {
@@ -812,7 +830,21 @@ function PolyWeatherTerminal({
row={rowForSlot}
allRows={filteredRegionRows}
compact={true}
onSearchClick={() => setActiveSearchSlotIndex(slotIndex)}
/>
{activeSearchSlotIndex === slotIndex && (
<CitySelectorDropdown
isEn={isEn}
rows={filteredRegionRows}
onSelectCity={(city) => {
handleSelectCityForSlot(slotIndex, city);
setActiveSearchSlotIndex(null);
}}
onClose={() => setActiveSearchSlotIndex(null)}
className="absolute left-3 top-9 z-50 w-[280px] bg-white border border-slate-200 rounded shadow-lg p-2"
/>
)}
</div>
);
})}
@@ -845,9 +877,9 @@ function ScanTerminalScreen() {
hydrated && (proAccess.subscriptionActive || canUseLocalFullAccess);
const userLocalTime = useUserLocalClock();
const { themeMode } = useScanTerminalTheme();
const [selectedRegionKey, setSelectedRegionKey] = useState<string>("east_asia");
const [selectedRegionKey, setSelectedRegionKey] = useState<string>("all");
const [localTimezoneOffsetSeconds, setLocalTimezoneOffsetSeconds] = useState<number | null>(null);
const [useLocalTimezoneDefault, setUseLocalTimezoneDefault] = useState(true);
const [useLocalTimezoneDefault, setUseLocalTimezoneDefault] = useState(false);
const [visibleRegions, setVisibleRegions] = useState<Set<string>>(() => {
try {
const stored = localStorage.getItem("polyweather_visible_regions");
@@ -938,7 +970,7 @@ function ScanTerminalScreen() {
}, []);
useEffect(() => {
setSelectedRegionKey(getDefaultRegion());
setSelectedRegionKey("all");
setLocalTimezoneOffsetSeconds(-new Date().getTimezoneOffset() * 60);
}, []);
@@ -4,7 +4,7 @@ import type { CityDetail } from "@/lib/dashboard-types";
import { getDisplayAirportPrimary } from "@/lib/airport-observation-display";
import { formatTemperatureValue } from "@/lib/temperature-utils";
// Settlement runway mapping — matches Polymarket settlement anchors
// Settlement runway mapping — matches settlement anchors
const SETTLEMENT_RUNWAY_PAIRS: Record<string, Array<[string, string]>> = {
shanghai: [["17L", "35R"]],
beijing: [["01", "19"]],
@@ -0,0 +1,201 @@
"use client";
import { useEffect, useMemo, useRef, useState } from "react";
import clsx from "clsx";
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
import { REGIONS, getCityRegion } from "./continent-grouping";
import { rowName, temp } from "./utils";
interface CitySelectorDropdownProps {
isEn: boolean;
rows: ScanOpportunityRow[];
onSelectCity: (city: string) => void;
onClose: () => void;
className?: string;
}
export function CitySelectorDropdown({
isEn,
rows,
onSelectCity,
onClose,
className,
}: CitySelectorDropdownProps) {
const containerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const [searchQuery, setSearchQuery] = useState("");
const [activeTab, setActiveTab] = useState<string>("all");
// Auto-focus input on mount
useEffect(() => {
inputRef.current?.focus();
}, []);
// Handle click outside and Escape key
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
onClose();
}
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
onClose();
}
};
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("keydown", handleKeyDown);
};
}, [onClose]);
// Tab definitions
const tabs = useMemo(() => {
return [
{ key: "all", labelEn: "ALL", labelZh: "全部" },
...REGIONS.map((r) => ({
key: r.key,
labelEn: r.labelEn.replace("Asia", "").replace("America", "").trim() || r.labelEn,
labelZh: r.labelZh,
})),
];
}, []);
// Filter rows
const filteredRows = useMemo(() => {
const q = searchQuery.toLowerCase().trim();
return rows.filter((row) => {
// 1. Region filter
if (activeTab !== "all") {
const region = getCityRegion(row);
if (region !== activeTab) return false;
}
// 2. Query filter
if (!q) return true;
const haystack = [
row.city,
row.city_display_name,
row.display_name,
row.airport,
]
.filter(Boolean)
.map((s) => s!.toLowerCase());
return haystack.some((s) => s.includes(q));
});
}, [rows, searchQuery, activeTab]);
return (
<div
ref={containerRef}
className={clsx(
"flex flex-col bg-white border border-slate-200 rounded-md shadow-2xl overflow-hidden text-xs text-[#202833] animate-in fade-in-50 zoom-in-95 duration-100",
className
)}
onClick={(e) => e.stopPropagation()} // Prevent card activation
>
{/* Search Input Area */}
<div className="flex items-center gap-2 p-2 border-b border-slate-100 bg-[#f8fafc]">
<input
ref={inputRef}
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={isEn ? "Search city or airport..." : "搜索城市、机场..."}
className="w-full px-2.5 py-1.5 border border-slate-300 rounded bg-white text-xs outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20"
/>
<button
type="button"
onClick={onClose}
className="text-slate-400 hover:text-slate-600 px-1 font-mono"
>
</button>
</div>
{/* Region filter tabs */}
<div className="flex items-center gap-1.5 px-2.5 py-1.5 border-b border-slate-100 bg-slate-50/50 overflow-x-auto scrollbar-none">
{tabs.map((tab) => {
const isActive = activeTab === tab.key;
return (
<button
key={tab.key}
type="button"
onClick={() => setActiveTab(tab.key)}
className={clsx(
"px-2 py-0.5 text-[10px] font-bold rounded whitespace-nowrap transition-all",
isActive
? "bg-blue-600 text-white shadow-sm"
: "bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-700"
)}
>
{isEn ? tab.labelEn : tab.labelZh}
</button>
);
})}
</div>
{/* Scrollable list */}
<div className="flex-1 overflow-y-auto max-h-[220px] divide-y divide-slate-50">
{filteredRows.length === 0 ? (
<div className="p-4 text-center text-slate-400 font-medium">
{isEn ? "No matching cities" : "无匹配城市"}
</div>
) : (
filteredRows.map((row) => {
const cityName = rowName(row);
const obsTemp = row.current_temp ?? row.current_max_so_far;
const debPrediction = row.deb_prediction;
const symbol = row.temp_symbol || "°C";
return (
<button
key={row.id}
type="button"
onClick={() => onSelectCity(String(row.city || "").toLowerCase())}
className="flex w-full items-center justify-between px-3 py-2 text-left hover:bg-blue-50/50 transition-colors group"
>
<div className="min-w-0 flex-1 pr-3">
<div className="font-bold text-slate-800 group-hover:text-blue-600 truncate">
{cityName}
</div>
{row.airport && (
<div className="text-[10px] text-slate-400 font-mono truncate">
{row.airport}
</div>
)}
</div>
{/* Weather info */}
<div className="flex items-center gap-3 font-mono text-[11px] shrink-0 text-slate-500">
{obsTemp !== undefined && obsTemp !== null && (
<div className="flex flex-col items-end">
<span className="text-[8px] text-slate-400 font-sans scale-90 uppercase">Obs</span>
<strong className="text-slate-700 font-bold">
{obsTemp}
<span className="text-[9px] font-normal font-sans">{symbol}</span>
</strong>
</div>
)}
{debPrediction !== undefined && debPrediction !== null && (
<div className="flex flex-col items-end">
<span className="text-[8px] text-slate-400 font-sans scale-90 uppercase">DEB</span>
<strong className="text-orange-600 font-bold">
{debPrediction}
<span className="text-[9px] font-normal font-sans">{symbol}</span>
</strong>
</div>
)}
</div>
</button>
);
})
)}
</div>
</div>
);
}
@@ -832,11 +832,13 @@ export function LiveTemperatureThresholdChart({
row,
allRows = [],
compact = false,
onSearchClick,
}: {
isEn: boolean;
row: ScanOpportunityRow | null;
allRows?: ScanOpportunityRow[];
compact?: boolean;
onSearchClick?: () => void;
}) {
const [hourly, setHourly] = useState<HourlyForecast>(null);
const city = String(row?.city || "").toLowerCase().trim();
@@ -1009,19 +1011,37 @@ export function LiveTemperatureThresholdChart({
[series, data],
);
const panelTitle = row
? `${rowName(row)} · ${
isEn
? timeframe === "1D"
? "Live & Forecast"
: `${timeframe} Forecast`
: timeframe === "1D"
? "实测与预测"
: `${timeframe}预报`
}`
: isEn
? "Temperature Chart"
: "气温图表";
const subtitle = row
? isEn
? timeframe === "1D"
? "Live & Forecast"
: `${timeframe} Forecast`
: timeframe === "1D"
? "实测与预测"
: `${timeframe}预报`
: "";
const panelTitle = row ? (
<div className="flex items-center gap-1">
<button
type="button"
onClick={onSearchClick}
className={clsx(
"flex items-center gap-1.5 px-1.5 py-0.5 rounded text-left transition-colors font-bold text-slate-800 outline-none select-none",
onSearchClick ? "hover:bg-slate-200/80 cursor-pointer" : ""
)}
>
<span>{rowName(row)}</span>
{onSearchClick && <span className="text-[8px] text-slate-400"></span>}
</button>
<span className="text-slate-400 font-normal">·</span>
<span className="text-slate-500 font-normal">{subtitle}</span>
</div>
) : isEn ? (
"Temperature Chart"
) : (
"气温图表"
);
const timeframeActions = (
<div className="flex items-center gap-1 rounded bg-[#eef2f6] p-0.5 border border-slate-200">
@@ -10,7 +10,7 @@ export function Panel({
}: {
children: React.ReactNode;
className?: string;
title: string;
title: React.ReactNode;
actions?: React.ReactNode;
}) {
return (
@@ -1,35 +0,0 @@
"use client";
import clsx from "clsx";
import type { WeatherDecisionView } from "@/components/dashboard/scan-terminal/city-card-decision-utils";
export function WeatherDecisionBand({
decisionView,
decisionWhyText,
isEn,
paceDeltaText,
}: {
decisionView: WeatherDecisionView;
decisionWhyText: string;
isEn: boolean;
paceDeltaText: string;
}) {
return (
<section className={clsx("scan-ai-decision-band", decisionView.tone)}>
<div className="scan-ai-decision-main">
<span>{decisionView.kicker}</span>
<strong>{decisionView.action}</strong>
<p className="scan-ai-decision-why">{decisionWhyText}</p>
</div>
<div className="scan-ai-decision-metrics">
<span>
{isEn ? "Weather range" : "天气区间"}
<b>{decisionView.targetRange}</b>
</span>
<span>
{isEn ? "Path delta" : "路径偏差"} <b>{paceDeltaText}</b>
</span>
</div>
</section>
);
}
@@ -27,8 +27,6 @@ const LABELS: Record<string, string> = {
hko: "HKO (香港)",
singapore_mss: "Singapore MSS",
cwa: "CWA (台湾)",
polymarket_gamma: "Polymarket Gamma",
polymarket_clob: "Polymarket CLOB",
amos: "AMOS (韩国跑道)",
amsc_awos: "AMSC AWOS (中国)",
noaa_wrh: "NOAA WRH (美国结算)",
+2 -2
View File
@@ -492,7 +492,7 @@ export const DOCS_PAGES: DocsPage[] = [
{
type: "bullets",
items: [
"自动识别当前 Polymarket 页面中的城市,也支持手动切换。",
"自动识别当前页面中的城市,也支持手动切换。",
"展示城市档案:结算站点、站点距离、观测更新时间、周边站点数量。",
"展示今日日内走势(简版):DEB 走势与机场主站实况 / 官方增强站网对照,可悬停查看时间与温度。",
"展示多日最高温预报(简版),并提供一键刷新与跳转主站入口。",
@@ -566,7 +566,7 @@ export const DOCS_PAGES: DocsPage[] = [
{
type: "bullets",
items: [
"Auto-detects the current Polymarket page city, with manual switching also available.",
"Auto-detects the current page city, with manual switching also available.",
"Shows a city profile with settlement station, station distance, observation timestamp, and nearby station count.",
"Shows a compact intraday chart with DEB versus airport-primary observations and official nearby-network observations, including hoverable time and temperature.",
"Shows a compact multi-day daily-high forecast, plus refresh and jump-to-site actions.",
+1 -1
View File
@@ -151,7 +151,7 @@ CITY_REGISTRY = {
"risk_emoji": "🟡",
"airport_name": "流浮山天文台站",
"distance_km": 0.0,
"warning": "香港天文台流浮山站分钟级观测,Polymarket 深圳温度市场结算源;非机场 METAR,不可与 ZGSZ/VHHH 报文混用。",
"warning": "香港天文台流浮山站分钟级观测,深圳温度市场结算源;非机场 METAR,不可与 ZGSZ/VHHH 报文混用。",
},
"taipei": {
"name": "Taipei",
+1 -1
View File
@@ -1,4 +1,4 @@
# Polymarket 城市温度市场 - 数据偏差风险档案
# 城市温度市场 - 数据偏差风险档案
# 基于 METAR 机场站与市区实际温度的系统性差异
from src.data_collection.city_registry import CITY_REGISTRY
+2 -2
View File
@@ -37,7 +37,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
- NWS (US National Weather Service)
- MGM (Turkish Meteorological Service)
- JMA / HKO / CWA (country official networks)
- Polymarket (weather derivative markets)
- Weather derivative markets
"""
from src.data_collection.city_registry import CITY_REGISTRY
@@ -612,7 +612,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
def extract_city_from_question(self, question: str) -> Optional[str]:
"""
Polymarket 问题描述或 Slug 中提取城市名称
市场问题描述或 Slug 中提取城市名称
"""
q = question.lower()
+1 -1
View File
@@ -595,7 +595,7 @@ HIGH_FREQ_AIRPORT_ICAO = {
"san francisco": "KSFO", "houston": "KHOU", "dallas": "KDAL",
"austin": "KAUS", "seattle": "KSEA",
}
# Settlement runway mapping — matches Polymarket settlement anchor stations.
# Settlement runway mapping — matches settlement anchor stations.
# Format: (low_number, high_number) order-independent; stored sorted for lookup.
SETTLEMENT_RUNWAY_PAIRS: Dict[str, Set[Tuple[str, str]]] = {
"shanghai": {("17L", "35R")},
+1 -1
View File
@@ -148,7 +148,7 @@ def _scan_city_terminal_rows_quick(
*,
force_refresh: bool = False,
) -> Dict[str, Any]:
"""Fast path that skips Polymarket matching — returns a single row per city
"""Fast path that returns cached analysis rows only — returns a single row per city
with cached analysis data (Obs, DEB, probabilities) but no market prices."""
data = _analyze(
city,