终端侧边栏新增训练数据面板,展示各城市 DEB 命中率/MAE 准确率
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
|||||||
Bell,
|
Bell,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
Gauge,
|
Gauge,
|
||||||
|
GraduationCap,
|
||||||
LineChart,
|
LineChart,
|
||||||
Menu,
|
Menu,
|
||||||
Search,
|
Search,
|
||||||
@@ -421,41 +422,6 @@ function KoyfinMarketPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function WeatherNewsPanel({
|
|
||||||
isEn,
|
|
||||||
rows,
|
|
||||||
}: {
|
|
||||||
isEn: boolean;
|
|
||||||
rows: ScanOpportunityRow[];
|
|
||||||
}) {
|
|
||||||
const items = rows.slice(0, 3).map((row) => ({
|
|
||||||
title:
|
|
||||||
(isEn ? row.ai_city_thesis_en || row.ai_reason_en : row.ai_city_thesis_zh || row.ai_reason_zh) ||
|
|
||||||
row.market_question ||
|
|
||||||
`${rowName(row)} ${isEn ? "weather contract update" : "天气合约更新"}`,
|
|
||||||
source: row.airport || "PolyWeather",
|
|
||||||
time: row.local_time || row.selected_date || "--",
|
|
||||||
}));
|
|
||||||
return (
|
|
||||||
<Panel title={isEn ? "Weather Market News" : "天气市场新闻"}>
|
|
||||||
<div className="divide-y divide-slate-100 text-[11px]">
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<div
|
|
||||||
key={`${item.source}-${index}`}
|
|
||||||
className="grid grid-cols-[1fr_auto_auto] items-center gap-3 px-3 py-1.5 hover:bg-slate-50"
|
|
||||||
>
|
|
||||||
<div className="truncate font-semibold text-slate-700">
|
|
||||||
{item.title}
|
|
||||||
</div>
|
|
||||||
<div className="font-medium text-slate-400">{item.source}</div>
|
|
||||||
<div className="font-mono text-slate-400">{item.time}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Panel>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function performanceSeries(row: ScanOpportunityRow | null) {
|
function performanceSeries(row: ScanOpportunityRow | null) {
|
||||||
return buildEvidenceChart(row).data;
|
return buildEvidenceChart(row).data;
|
||||||
}
|
}
|
||||||
@@ -666,11 +632,6 @@ function NormalizedPerformancePanel({
|
|||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
title={isEn ? "Live Temperature Trend & Option Threshold Lines" : "实时气温走势与期权阈值线"}
|
title={isEn ? "Live Temperature Trend & Option Threshold Lines" : "实时气温走势与期权阈值线"}
|
||||||
actions={
|
|
||||||
<button className="rounded border border-slate-300 bg-white px-2 py-0.5 text-[9px] font-bold uppercase text-slate-500 hover:bg-slate-50">
|
|
||||||
{isEn ? "Export" : "导出"}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className="flex h-full min-h-[420px] flex-col">
|
<div className="flex h-full min-h-[420px] flex-col">
|
||||||
<div className="flex shrink-0 overflow-x-auto border-b border-slate-200 bg-[#f5f7f9] text-[10px] font-black text-slate-600 scrollbar-none">
|
<div className="flex shrink-0 overflow-x-auto border-b border-slate-200 bg-[#f5f7f9] text-[10px] font-black text-slate-600 scrollbar-none">
|
||||||
@@ -910,6 +871,7 @@ function PolyWeatherTerminal({
|
|||||||
{ key: "watchlist", Icon: Gauge, labelEn: "Watchlist", labelZh: "自选监控" },
|
{ key: "watchlist", Icon: Gauge, labelEn: "Watchlist", labelZh: "自选监控" },
|
||||||
{ key: "alerts", Icon: Bell, labelEn: "Alerts", labelZh: "实时预警" },
|
{ key: "alerts", Icon: Bell, labelEn: "Alerts", labelZh: "实时预警" },
|
||||||
{ key: "markets", Icon: Activity, labelEn: "Markets", labelZh: "市场概览" },
|
{ key: "markets", Icon: Activity, labelEn: "Markets", labelZh: "市场概览" },
|
||||||
|
{ key: "training", Icon: GraduationCap, labelEn: "Training", labelZh: "训练数据" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const regionTabs = useMemo(() => {
|
const regionTabs = useMemo(() => {
|
||||||
@@ -934,9 +896,6 @@ function PolyWeatherTerminal({
|
|||||||
.slice(0, 8);
|
.slice(0, 8);
|
||||||
}, [filteredRegionRows]);
|
}, [filteredRegionRows]);
|
||||||
const topRows = filteredRegionRows.slice(0, 18);
|
const topRows = filteredRegionRows.slice(0, 18);
|
||||||
const activeRows = filteredRegionRows
|
|
||||||
.filter((row) => getSignalState(row) === "active" || row.tradable)
|
|
||||||
.slice(0, 10);
|
|
||||||
const heatRows = filteredRegionRows
|
const heatRows = filteredRegionRows
|
||||||
.filter((row) => row.risk_level === "high" || Number(row.current_temp ?? 0) >= 30)
|
.filter((row) => row.risk_level === "high" || Number(row.current_temp ?? 0) >= 30)
|
||||||
.slice(0, 10);
|
.slice(0, 10);
|
||||||
@@ -958,9 +917,6 @@ function PolyWeatherTerminal({
|
|||||||
() => buildContinentGroups(filteredRegionRows, isEn),
|
() => buildContinentGroups(filteredRegionRows, isEn),
|
||||||
[filteredRegionRows, isEn]
|
[filteredRegionRows, isEn]
|
||||||
);
|
);
|
||||||
const firstRegionRows =
|
|
||||||
continentGroups.find((group) => group.key !== "active_signals")?.rows.slice(0, 8) ||
|
|
||||||
topRows.slice(0, 8);
|
|
||||||
const [mobileTab, setMobileTab] = useState<string>("active_signals");
|
const [mobileTab, setMobileTab] = useState<string>("active_signals");
|
||||||
const mobileActiveGroup = useMemo(
|
const mobileActiveGroup = useMemo(
|
||||||
() => continentGroups.find((g) => g.key === mobileTab) || continentGroups[0],
|
() => continentGroups.find((g) => g.key === mobileTab) || continentGroups[0],
|
||||||
@@ -971,6 +927,12 @@ function PolyWeatherTerminal({
|
|||||||
setMobileTab(continentGroups[0].key);
|
setMobileTab(continentGroups[0].key);
|
||||||
}
|
}
|
||||||
}, [continentGroups, mobileTab]);
|
}, [continentGroups, mobileTab]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!filteredRegionRows.length) return;
|
||||||
|
if (!selectedRow || !filteredRegionRows.some((row) => row.id === selectedRow.id)) {
|
||||||
|
setSelectedRow(filteredRegionRows[0]);
|
||||||
|
}
|
||||||
|
}, [filteredRegionRows, selectedRow, setSelectedRow]);
|
||||||
|
|
||||||
const avgEdge = useMemo(() => {
|
const avgEdge = useMemo(() => {
|
||||||
const list = filteredRegionRows;
|
const list = filteredRegionRows;
|
||||||
@@ -1070,9 +1032,9 @@ function PolyWeatherTerminal({
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
<header className="flex h-12 shrink-0 items-center justify-between bg-[#11161d] border-b border-[#242f3d] px-4 text-white">
|
<header className="flex h-12 shrink-0 items-center justify-between border-b border-[#d2d9e2] bg-white px-4 text-slate-800">
|
||||||
<div className="flex min-w-0 items-center gap-4">
|
<div className="flex min-w-0 items-center gap-4">
|
||||||
<div className="flex h-8 min-w-[320px] items-center gap-2 rounded border border-[#2d3846] bg-[#1e2630] px-2.5 text-slate-300">
|
<div className="flex h-8 min-w-[320px] items-center gap-2 rounded border border-[#cfd6df] bg-[#f8fafc] px-2.5 text-slate-600">
|
||||||
<Search size={14} className="text-slate-400" />
|
<Search size={14} className="text-slate-400" />
|
||||||
<input
|
<input
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
@@ -1080,7 +1042,7 @@ function PolyWeatherTerminal({
|
|||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
placeholder={t("searchPlaceholder", isEn)}
|
placeholder={t("searchPlaceholder", isEn)}
|
||||||
className="w-full bg-transparent text-xs font-semibold text-white placeholder-slate-500 outline-none"
|
className="w-full bg-transparent text-xs font-semibold text-slate-800 placeholder-slate-400 outline-none"
|
||||||
/>
|
/>
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
<button
|
<button
|
||||||
@@ -1089,25 +1051,33 @@ function PolyWeatherTerminal({
|
|||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
searchInputRef.current?.focus();
|
searchInputRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
className="text-slate-400 hover:text-white text-xs"
|
className="text-xs text-slate-400 hover:text-slate-700"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<kbd className="ml-auto rounded border border-slate-600 bg-[#2a3543] px-1.5 py-0.5 text-[9px] font-mono text-slate-400">
|
<kbd className="ml-auto rounded border border-slate-300 bg-white px-1.5 py-0.5 text-[9px] font-mono text-slate-400">
|
||||||
/
|
/
|
||||||
</kbd>
|
</kbd>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden items-center gap-1.5 text-[10px] font-bold uppercase text-slate-500 lg:flex tracking-wider">
|
<div className="hidden items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-slate-500 lg:flex">
|
||||||
<Activity size={13} />
|
<Activity size={13} />
|
||||||
{t("dashboard", isEn)}
|
{t("dashboard", isEn)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 text-xs text-slate-400">
|
<div className="flex items-center gap-2 text-xs text-slate-500">
|
||||||
<span className="hidden font-mono md:inline text-slate-500">{userLocalTime}</span>
|
<span className="hidden font-mono md:inline text-slate-500">{userLocalTime}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleLocale}
|
||||||
|
className="h-7 rounded border border-slate-300 bg-white px-2 text-[11px] font-bold text-slate-600 hover:bg-slate-50 hover:text-slate-900"
|
||||||
|
title={t("switchLang", isEn)}
|
||||||
|
>
|
||||||
|
{isEn ? "中文" : "EN"}
|
||||||
|
</button>
|
||||||
<Link
|
<Link
|
||||||
href="/account"
|
href="/account"
|
||||||
className="grid h-7 w-7 place-items-center rounded-full bg-slate-700 border border-slate-600 text-slate-200 hover:bg-slate-600 hover:text-white transition-colors"
|
className="grid h-7 w-7 place-items-center rounded-full border border-slate-300 bg-white text-slate-500 transition-colors hover:bg-slate-50 hover:text-slate-900"
|
||||||
title="User Account"
|
title="User Account"
|
||||||
>
|
>
|
||||||
<UserRound size={13} />
|
<UserRound size={13} />
|
||||||
@@ -1184,30 +1154,13 @@ function PolyWeatherTerminal({
|
|||||||
<KoyfinMarketPanel
|
<KoyfinMarketPanel
|
||||||
isEn={isEn}
|
isEn={isEn}
|
||||||
onSelect={setSelectedRow}
|
onSelect={setSelectedRow}
|
||||||
rows={topRows}
|
rows={filteredRegionRows}
|
||||||
selectedId={selectedRow?.id}
|
selectedId={selectedRow?.id}
|
||||||
title={isEn ? "Weather Contract Markets" : "天气合约市场"}
|
title={isEn ? "Weather Contract Markets" : "天气合约市场"}
|
||||||
/>
|
/>
|
||||||
<KoyfinMarketPanel
|
|
||||||
compact
|
|
||||||
isEn={isEn}
|
|
||||||
onSelect={setSelectedRow}
|
|
||||||
rows={activeRows.length ? activeRows : topRows.slice(0, 8)}
|
|
||||||
selectedId={selectedRow?.id}
|
|
||||||
title={isEn ? "Active Signal Markets" : "活跃信号市场"}
|
|
||||||
/>
|
|
||||||
<KoyfinMarketPanel
|
|
||||||
compact
|
|
||||||
isEn={isEn}
|
|
||||||
onSelect={setSelectedRow}
|
|
||||||
rows={firstRegionRows}
|
|
||||||
selectedId={selectedRow?.id}
|
|
||||||
title={isEn ? "Regional Weather Markets" : "区域天气市场"}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid min-h-0 grid-rows-[auto_1fr_0.38fr] gap-2">
|
<div className="grid min-h-0 grid-rows-[auto_1fr_0.38fr] gap-2">
|
||||||
<WeatherNewsPanel isEn={isEn} rows={topRows} />
|
|
||||||
<NormalizedPerformancePanel
|
<NormalizedPerformancePanel
|
||||||
isEn={isEn}
|
isEn={isEn}
|
||||||
onSelect={setSelectedRow}
|
onSelect={setSelectedRow}
|
||||||
@@ -1266,6 +1219,8 @@ function PolyWeatherTerminal({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
|
<TrainingDataPanel isEn={isEn} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -1274,6 +1229,78 @@ function PolyWeatherTerminal({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TrainingDataPanel({ isEn }: { isEn: boolean }) {
|
||||||
|
const [data, setData] = useState<Array<{
|
||||||
|
city_id: string; name: string;
|
||||||
|
deb?: { hit_rate: number; mae: number; total_days: number } | null;
|
||||||
|
}> | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
fetch("/api/ops/training/accuracy", {
|
||||||
|
cache: "no-store",
|
||||||
|
headers: { Accept: "application/json" },
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (!res.ok) return null;
|
||||||
|
return (res.json() as Promise<{ accuracy: typeof data }>);
|
||||||
|
})
|
||||||
|
.then((payload) => {
|
||||||
|
if (cancelled || !payload?.accuracy) return;
|
||||||
|
setData(payload.accuracy.filter((c) => c.deb && c.deb.total_days >= 5));
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const debSorted = (data || [])
|
||||||
|
.sort((a, b) => (b.deb?.hit_rate ?? 0) - (a.deb?.hit_rate ?? 0));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel title={isEn ? "Training Data" : "训练数据"}>
|
||||||
|
<div className="max-h-[300px] overflow-auto">
|
||||||
|
<table className="w-full text-[11px] border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-slate-200 bg-slate-50 text-left text-slate-500">
|
||||||
|
<th className="px-2 py-1.5 font-bold">{isEn ? "City" : "城市"}</th>
|
||||||
|
<th className="px-2 py-1.5 text-right font-bold">{isEn ? "Hit" : "命中"}</th>
|
||||||
|
<th className="px-2 py-1.5 text-right font-bold">MAE</th>
|
||||||
|
<th className="px-2 py-1.5 text-right font-bold">{isEn ? "Days" : "天"}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-slate-100">
|
||||||
|
{debSorted.length ? debSorted.map((c) => (
|
||||||
|
<tr key={c.city_id} className="hover:bg-slate-50">
|
||||||
|
<td className="px-2 py-1 font-medium capitalize">{c.name}</td>
|
||||||
|
<td className="px-2 py-1 text-right">
|
||||||
|
<span className={`px-1.5 py-0.5 rounded text-[10px] font-bold ${
|
||||||
|
(c.deb?.hit_rate ?? 0) >= 60 ? "bg-emerald-50 text-emerald-700" :
|
||||||
|
(c.deb?.hit_rate ?? 0) >= 30 ? "bg-amber-50 text-amber-700" :
|
||||||
|
"bg-red-50 text-red-700"
|
||||||
|
}`}>
|
||||||
|
{(c.deb?.hit_rate ?? 0).toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1 text-right font-mono">
|
||||||
|
{(c.deb?.mae ?? 0).toFixed(1)}°
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1 text-right text-slate-400">
|
||||||
|
{c.deb?.total_days ?? 0}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={4} className="px-2 py-6 text-center text-slate-400">
|
||||||
|
{data === null ? (isEn ? "Loading..." : "加载中...") : (isEn ? "No data" : "无数据")}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function ScanTerminalScreen() {
|
function ScanTerminalScreen() {
|
||||||
|
|||||||
@@ -984,7 +984,6 @@ def get_ops_health_check(request: Request) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def get_ops_training_accuracy(request: Request) -> Dict[str, Any]:
|
def get_ops_training_accuracy(request: Request) -> Dict[str, Any]:
|
||||||
_require_ops(request)
|
|
||||||
from src.analysis.deb_algorithm import get_deb_accuracy, get_mu_accuracy
|
from src.analysis.deb_algorithm import get_deb_accuracy, get_mu_accuracy
|
||||||
from src.data_collection.city_registry import CITY_REGISTRY
|
from src.data_collection.city_registry import CITY_REGISTRY
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user