气象情报终端:移除 Polymarket 市场数据列和 MarketOverviewView

GroupedMarketTable 9列→6列(删Market/Edge/SprLiq)。continent-grouping 删 formatPrice/formatSpreadLiquidity,getGapColor 纯温度语义。KoyfinRowsTable 精简为城市列表。MobileCityCard 市场价格替换为本地时间。侧边栏移除市场概览入口。
This commit is contained in:
2569718930@qq.com
2026-05-25 19:42:06 +08:00
parent 2e2670491f
commit cdd3cd25ae
6 changed files with 53 additions and 182 deletions
@@ -22,8 +22,6 @@ import { ProductAccessRequired } from "@/components/dashboard/scan-terminal/Prod
import {
type ContinentGroup,
buildContinentGroups,
formatPrice,
formatSpreadLiquidity,
GAP_COLOR_MAP,
getDefaultExpanded,
getGapColor,
@@ -46,7 +44,6 @@ import { useRelativeTime } from "@/hooks/useRelativeTime";
import { Panel } from "@/components/dashboard/scan-terminal/Panel";
import { TrainingDashboard } from "@/components/dashboard/scan-terminal/TrainingDashboard";
import { LiveTemperatureThresholdChart } from "@/components/dashboard/scan-terminal/LiveTemperatureThresholdChart";
import { MarketOverviewView } from "@/components/dashboard/scan-terminal/MarketOverviewView";
import { KoyfinRowsTable } from "@/components/dashboard/scan-terminal/KoyfinRowsTable";
import { rowName, pct, money, temp, edgeClass } from "@/components/dashboard/scan-terminal/utils";
@@ -165,10 +162,6 @@ function decisionLabel(row?: ScanOpportunityRow | null) {
return "Monitor";
}
function tablePrice(row: ScanOpportunityRow) {
return formatPrice(row.midpoint, row.ask, row.bid);
}
function CityRegionList({
isEn,
rows,
@@ -512,17 +505,6 @@ function PolyWeatherTerminal({
<main className="min-h-0 flex-1 overflow-hidden flex flex-col p-2 bg-[#eef2f6]">
{activeNavKey === "training" ? (
<TrainingDashboard isEn={isEn} />
) : activeNavKey === "markets" ? (
<MarketOverviewView
isEn={isEn}
rows={rows}
onSelectRow={(row) => {
const regionKey = resolveTradingRegionKey(row);
if (regionKey) setSelectedRegionKey(regionKey);
setSelectedRow(row);
setActiveNavKey("contracts");
}}
/>
) : (
<>
{/* Region tabs */}
@@ -612,7 +594,7 @@ function PolyWeatherTerminal({
["DEB", temp(selectedRow.deb_prediction, selectedRow.temp_symbol)],
["Gap", temp(selectedRow.signed_gap ?? selectedRow.gap_to_target, selectedRow.temp_symbol)],
["Edge", pct(selectedRow.edge_percent)],
["Market", formatPrice(selectedRow.midpoint, selectedRow.ask, selectedRow.bid)],
["Market", "--"],
].map(([label, value]) => (
<div key={label} className="rounded border border-slate-200 bg-slate-50 p-2">
<div className="text-[10px] font-black uppercase text-slate-500">{label}</div>
@@ -2,19 +2,17 @@
import { Fragment, useState } from "react";
import clsx from "clsx";
import { ChevronDown, ChevronRight, ExternalLink } from "lucide-react";
import { ChevronDown, ChevronRight } from "lucide-react";
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
import {
type ContinentGroup,
formatPrice,
formatSpreadLiquidity,
GAP_COLOR_MAP,
getGapColor,
getSignalLabel,
getSignalState,
getDefaultExpanded,
} from "@/components/dashboard/scan-terminal/continent-grouping";
import { rowName, temp, pct, edgeClass } from "./utils";
import { rowName, temp } from "./utils";
export function GroupedMarketTable({
groups,
@@ -51,7 +49,7 @@ export function GroupedMarketTable({
return (
<div className="overflow-auto h-full">
<table className="w-full min-w-[800px] border-collapse text-[13px]">
<table className="w-full min-w-[600px] border-collapse text-[13px]">
<thead>
<tr className="border-b border-slate-200 bg-[#f8f9fa] text-left text-[11px] uppercase font-bold tracking-wider text-slate-500">
<th className="px-3 py-1.5 font-bold">City</th>
@@ -59,9 +57,6 @@ export function GroupedMarketTable({
<th className="px-2 py-1.5 text-right font-bold">High</th>
<th className="px-2 py-1.5 text-right font-bold">DEB</th>
<th className="px-2 py-1.5 text-right font-bold">Gap</th>
<th className="px-2 py-1.5 text-right font-bold">Market</th>
<th className="px-2 py-1.5 text-right font-bold">Edge</th>
<th className="px-2 py-1.5 text-right font-bold">Spr/Liq</th>
<th className="px-3 py-1.5 font-bold">Signal</th>
</tr>
</thead>
@@ -73,7 +68,7 @@ export function GroupedMarketTable({
<Fragment key={group.key}>
{showHeaders && (
<tr className="border-b border-slate-200 bg-[#eef2f6]">
<td colSpan={9} className="p-0">
<td colSpan={6} className="p-0">
<button
type="button"
onClick={() => toggleGroup(group.key)}
@@ -124,29 +119,6 @@ export function GroupedMarketTable({
<td className={clsx("px-2 py-1.5 text-right font-mono font-bold", gapColor)}>
{temp(row.signed_gap ?? row.gap_to_target, row.temp_symbol)}
</td>
<td className="px-2 py-1.5 text-right font-mono">
{row.market_url ? (
<a
href={row.market_url}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="inline-flex items-center gap-0.5 text-blue-600 hover:text-blue-800 hover:underline"
title={row.market_question || "Polymarket"}
>
{formatPrice(row.midpoint, row.ask, row.bid)}
<ExternalLink size={10} />
</a>
) : (
formatPrice(row.midpoint, row.ask, row.bid)
)}
</td>
<td className={clsx("px-2 py-1.5 text-right font-mono font-bold", edgeClass(row.edge_percent))}>
{pct(row.edge_percent)}
</td>
<td className="px-2 py-1.5 text-right font-mono text-[12px]">
{formatSpreadLiquidity(row.spread, row.book_liquidity ?? row.market_liquidity)}
</td>
<td className="px-3 py-1.5">
<span className={clsx(
"text-[12px] font-black",
@@ -2,15 +2,9 @@
import clsx from "clsx";
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
import { formatPrice } from "@/components/dashboard/scan-terminal/continent-grouping";
import { rowName, pct } from "@/components/dashboard/scan-terminal/utils";
function tablePrice(row: ScanOpportunityRow) {
return formatPrice(row.midpoint, row.ask, row.bid);
}
import { rowName } from "@/components/dashboard/scan-terminal/utils";
export function KoyfinRowsTable({
compact = false,
isEn,
onSelect,
rows,
@@ -26,67 +20,31 @@ export function KoyfinRowsTable({
<table className="w-full border-collapse text-[11px]">
<thead>
<tr className="border-b border-slate-200 bg-[#f3f5f7] text-[11px] uppercase tracking-wide text-slate-500">
<th className="w-5 px-2 py-1 text-left font-black">
<span className="block h-3 w-3 rounded-[2px] border border-slate-300 bg-white" />
</th>
<th className="px-1.5 py-1 text-left font-black">
<th className="px-2 py-1 text-left font-bold">
{isEn ? "City" : "城市"}
</th>
<th className="px-1.5 py-1 text-right font-black">
{isEn ? "Price" : "价格"}
</th>
<th className="px-1.5 py-1 text-right font-black">
{isEn ? "Chg" : "变化"}
</th>
<th className="px-2 py-1 text-right font-black">%</th>
</tr>
</thead>
<tbody>
{rows.map((row) => {
const edge = Number(row.edge_percent ?? row.signed_gap ?? row.gap ?? 0);
const positive = edge >= 0;
return (
<tr
key={row.id}
onClick={() => onSelect(row)}
className={clsx(
"cursor-pointer border-b border-slate-100 hover:bg-blue-50/70",
selectedId === row.id && "bg-blue-50",
)}
>
<td className="px-2 py-1">
<span className="block h-3 w-3 rounded-[2px] border border-slate-300 bg-white" />
</td>
<td className="px-1.5 py-1">
<div className="truncate font-bold text-slate-800">
{rowName(row)}
</div>
<div className="truncate text-[10px] font-medium text-slate-400">
{row.target_label || row.market_question || row.airport || "--"}
</div>
</td>
<td className="px-1.5 py-1 text-right font-mono font-bold text-slate-800">
{tablePrice(row)}
</td>
<td
className={clsx(
"px-1.5 py-1 text-right font-mono font-bold",
positive ? "text-emerald-700" : "text-red-600",
)}
>
{Number.isFinite(edge) ? `${positive ? "+" : ""}${edge.toFixed(1)}` : "--"}
</td>
<td
className={clsx(
"px-2 py-1 text-right font-mono font-bold",
positive ? "text-emerald-700" : "text-red-600",
)}
>
{pct(row.market_probability ?? row.market_event_probability ?? row.model_probability)}
</td>
</tr>
);
})}
{rows.map((row) => (
<tr
key={row.id}
onClick={() => onSelect(row)}
className={clsx(
"cursor-pointer border-b border-slate-100 hover:bg-blue-50/70",
selectedId === row.id && "bg-blue-50",
)}
>
<td className="px-2 py-1">
<div className="truncate font-bold text-slate-800">
{rowName(row)}
</div>
<div className="truncate text-[10px] font-medium text-slate-400">
{row.airport || "--"}
</div>
</td>
</tr>
))}
</tbody>
</table>
);
@@ -545,47 +545,30 @@ function buildModelSummaryCards(row: ScanOpportunityRow | null): EvidenceSeries[
}));
}
// ── Market temperature ticks for Y-axis ─────────────────────────────────
// ── Integer-degree ticks for Y-axis ─────────────────────────────────
function parseTemperatureOptionsFromText(value?: string | null) {
const raw = String(value || "");
const matches = raw.match(/-?\d+(?:\.\d+)?/g) || [];
return matches.map(Number).filter((v) => Number.isFinite(v) && v > -80 && v < 80);
}
function buildMarketTemperatureOptions(row: ScanOpportunityRow | null) {
const buckets = row?.distribution_full?.length
? row.distribution_full
: row?.distribution_preview;
const values = new Set<number>();
(buckets || []).forEach((b) => {
const v = validNumber(b.value);
if (v !== null) values.add(v);
parseTemperatureOptionsFromText(b.label).forEach((x) => values.add(x));
});
[row?.target_lower, row?.target_upper, row?.target_value, row?.target_threshold]
.forEach((v) => { if (validNumber(v) !== null) values.add(validNumber(v)!); });
parseTemperatureOptionsFromText(row?.target_label).forEach((x) => values.add(x));
const sorted = [...values].sort((a, b) => a - b);
if (sorted.length) return sorted;
const t = validNumber(row?.target_threshold) ?? validNumber(row?.target_value);
if (t === null) return null;
return [t - 2, t - 1, t, t + 1, t + 2];
function buildIntDegreeTicks(series: EvidenceSeries[], data?: Array<Record<string, string | number | null>>): number[] | null {
const vals = data?.length
? data.flatMap((point) => series.map((s) => point[s.key])).filter((v): v is number => validNumber(v) !== null)
: series.flatMap((s) => s.values).filter((v): v is number => validNumber(v) !== null);
if (!vals.length) return null;
const min = Math.floor(Math.min(...vals));
const max = Math.ceil(Math.max(...vals));
const ticks: number[] = [];
for (let d = min; d <= max; d++) ticks.push(d);
return ticks.length > 0 ? ticks : null;
}
function buildChartDomain(
ticks: number[] | null,
series: EvidenceSeries[],
data?: Array<Record<string, string | number | null>>,
): [number, number] | ["auto", "auto"] {
const vals = data?.length
? data.flatMap((point) => series.map((s) => point[s.key])).filter((v): v is number => validNumber(v) !== null)
: series.flatMap((s) => s.values).filter((v): v is number => validNumber(v) !== null);
const all = [...(ticks || []), ...vals];
if (!all.length) return ["auto", "auto"];
const min = Math.min(...all);
const max = Math.max(...all);
if (!vals.length) return ["auto", "auto"];
const min = Math.min(...vals);
const max = Math.max(...vals);
const span = Math.max(1, max - min);
const pad = Math.max(0.5, span * 0.08);
return [Number((min - pad).toFixed(1)), Number((max + pad).toFixed(1))];
@@ -777,10 +760,10 @@ export function LiveTemperatureThresholdChart({
return list.sort((a, b) => a.threshold - b.threshold);
}, [row, allRows]);
const marketTicks = useMemo(() => buildMarketTemperatureOptions(row), [row]);
const intDegreeTicks = useMemo(() => buildIntDegreeTicks(series, data), [series, data]);
const chartDomain = useMemo(
() => buildChartDomain(marketTicks, series, data),
[marketTicks, series, data],
() => buildChartDomain(series, data),
[series, data],
);
return (
@@ -968,7 +951,7 @@ export function LiveTemperatureThresholdChart({
))}
</div>
<ResponsiveContainer width="100%" height="100%">
<ReLineChart data={data} margin={{ top: 16, right: 28, left: 8, bottom: 8 }}>
<ReLineChart data={data} margin={{ top: 16, right: 44, left: 4, bottom: 8 }}>
<CartesianGrid stroke="#dbe6ef" strokeDasharray="2 2" />
<XAxis
dataKey="label"
@@ -978,12 +961,13 @@ export function LiveTemperatureThresholdChart({
interval={Math.max(1, Math.floor(data.length / 8))}
/>
<YAxis
orientation="right"
tick={{ fontSize: 10, fill: "#64748b" }}
tickFormatter={(v) => `${Number(v).toFixed(1)}°`}
tickFormatter={(v) => `${Number(v).toFixed(0)}°`}
axisLine={{ stroke: "#cbd5e1" }}
tickLine={false}
domain={chartDomain}
ticks={marketTicks ?? undefined}
ticks={intDegreeTicks ?? undefined}
/>
{cityThresholds.map((t, idx) => {
const isSelected = row && (Number(row.target_threshold ?? row.target_value) === t.threshold);
@@ -2,7 +2,6 @@
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
import {
formatPrice,
GAP_COLOR_MAP,
getGapColor,
getSignalLabel,
@@ -58,7 +57,7 @@ export function MobileCityCard({
Gap {tempVal(row.signed_gap ?? row.gap_to_target, row.temp_symbol)}
</span>
<span className="text-slate-600">
{formatPrice(row.midpoint, row.ask, row.bid)}
{row.local_time || "--"}
</span>
</div>
@@ -176,16 +176,10 @@ export type GapColor = "green" | "orange" | "slate" | "gray" | "red";
export function getGapColor(row: ScanOpportunityRow): GapColor {
const gap = Number(row.signed_gap ?? row.gap_to_target);
const edge = Number(row.edge_percent || 0);
const spread = Number(row.spread || 0);
const liq = Number(row.book_liquidity || row.market_liquidity || 0);
if (!Number.isFinite(gap)) return "gray";
if (liq <= 0 || spread > 20) return "red";
if (gap >= 2) return "green";
if (gap >= 0 && edge > 5) return "orange";
if (gap >= 0) return "slate";
if (gap < -5 || edge < -10) return "gray";
if (gap >= 0) return "orange";
if (gap < -5) return "gray";
return "slate";
}
@@ -197,30 +191,12 @@ export const GAP_COLOR_MAP: Record<GapColor, string> = {
red: "text-red-500",
};
export function formatPrice(midpoint?: number | null, ask?: number | null, bid?: number | null): string {
const m = Number(midpoint);
if (Number.isFinite(m) && m > 0) {
const cents = Math.round(m * 100);
return `Y ${cents}¢`;
}
const a = Number(ask);
if (Number.isFinite(a) && a > 0) {
const cents = Math.round(a * 100);
return `Y ${cents}¢`;
}
export function formatPrice(_midpoint?: number | null, _ask?: number | null, _bid?: number | null): string {
return "--";
}
export function formatSpreadLiquidity(spread?: number | null, liquidity?: number | null): string {
const sp = Number(spread);
const liq = Number(liquidity);
const spStr = Number.isFinite(sp) ? `${Math.round(sp)}¢` : "--";
const liqStr = Number.isFinite(liq)
? liq >= 1000
? `$${(liq / 1000).toFixed(1)}K`
: `$${Math.round(liq)}`
: "--";
return `${spStr} / ${liqStr}`;
export function formatSpreadLiquidity(_spread?: number | null, _liquidity?: number | null): string {
return "--";
}
export function buildContinentGroups(rows: ScanOpportunityRow[], isEn: boolean): ContinentGroup[] {