气象情报终端:移除 Polymarket 市场数据列和 MarketOverviewView
GroupedMarketTable 9列→6列(删Market/Edge/SprLiq)。continent-grouping 删 formatPrice/formatSpreadLiquidity,getGapColor 纯温度语义。KoyfinRowsTable 精简为城市列表。MobileCityCard 市场价格替换为本地时间。侧边栏移除市场概览入口。
This commit is contained in:
@@ -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[] {
|
||||
|
||||
Reference in New Issue
Block a user