气象情报终端:移除 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 {
|
import {
|
||||||
type ContinentGroup,
|
type ContinentGroup,
|
||||||
buildContinentGroups,
|
buildContinentGroups,
|
||||||
formatPrice,
|
|
||||||
formatSpreadLiquidity,
|
|
||||||
GAP_COLOR_MAP,
|
GAP_COLOR_MAP,
|
||||||
getDefaultExpanded,
|
getDefaultExpanded,
|
||||||
getGapColor,
|
getGapColor,
|
||||||
@@ -46,7 +44,6 @@ import { useRelativeTime } from "@/hooks/useRelativeTime";
|
|||||||
import { Panel } from "@/components/dashboard/scan-terminal/Panel";
|
import { Panel } from "@/components/dashboard/scan-terminal/Panel";
|
||||||
import { TrainingDashboard } from "@/components/dashboard/scan-terminal/TrainingDashboard";
|
import { TrainingDashboard } from "@/components/dashboard/scan-terminal/TrainingDashboard";
|
||||||
import { LiveTemperatureThresholdChart } from "@/components/dashboard/scan-terminal/LiveTemperatureThresholdChart";
|
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 { KoyfinRowsTable } from "@/components/dashboard/scan-terminal/KoyfinRowsTable";
|
||||||
import { rowName, pct, money, temp, edgeClass } from "@/components/dashboard/scan-terminal/utils";
|
import { rowName, pct, money, temp, edgeClass } from "@/components/dashboard/scan-terminal/utils";
|
||||||
|
|
||||||
@@ -165,10 +162,6 @@ function decisionLabel(row?: ScanOpportunityRow | null) {
|
|||||||
return "Monitor";
|
return "Monitor";
|
||||||
}
|
}
|
||||||
|
|
||||||
function tablePrice(row: ScanOpportunityRow) {
|
|
||||||
return formatPrice(row.midpoint, row.ask, row.bid);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CityRegionList({
|
function CityRegionList({
|
||||||
isEn,
|
isEn,
|
||||||
rows,
|
rows,
|
||||||
@@ -512,17 +505,6 @@ function PolyWeatherTerminal({
|
|||||||
<main className="min-h-0 flex-1 overflow-hidden flex flex-col p-2 bg-[#eef2f6]">
|
<main className="min-h-0 flex-1 overflow-hidden flex flex-col p-2 bg-[#eef2f6]">
|
||||||
{activeNavKey === "training" ? (
|
{activeNavKey === "training" ? (
|
||||||
<TrainingDashboard isEn={isEn} />
|
<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 */}
|
{/* Region tabs */}
|
||||||
@@ -612,7 +594,7 @@ function PolyWeatherTerminal({
|
|||||||
["DEB", temp(selectedRow.deb_prediction, selectedRow.temp_symbol)],
|
["DEB", temp(selectedRow.deb_prediction, selectedRow.temp_symbol)],
|
||||||
["Gap", temp(selectedRow.signed_gap ?? selectedRow.gap_to_target, selectedRow.temp_symbol)],
|
["Gap", temp(selectedRow.signed_gap ?? selectedRow.gap_to_target, selectedRow.temp_symbol)],
|
||||||
["Edge", pct(selectedRow.edge_percent)],
|
["Edge", pct(selectedRow.edge_percent)],
|
||||||
["Market", formatPrice(selectedRow.midpoint, selectedRow.ask, selectedRow.bid)],
|
["Market", "--"],
|
||||||
].map(([label, value]) => (
|
].map(([label, value]) => (
|
||||||
<div key={label} className="rounded border border-slate-200 bg-slate-50 p-2">
|
<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>
|
<div className="text-[10px] font-black uppercase text-slate-500">{label}</div>
|
||||||
|
|||||||
@@ -2,19 +2,17 @@
|
|||||||
|
|
||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import clsx from "clsx";
|
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 { ScanOpportunityRow } from "@/lib/dashboard-types";
|
||||||
import {
|
import {
|
||||||
type ContinentGroup,
|
type ContinentGroup,
|
||||||
formatPrice,
|
|
||||||
formatSpreadLiquidity,
|
|
||||||
GAP_COLOR_MAP,
|
GAP_COLOR_MAP,
|
||||||
getGapColor,
|
getGapColor,
|
||||||
getSignalLabel,
|
getSignalLabel,
|
||||||
getSignalState,
|
getSignalState,
|
||||||
getDefaultExpanded,
|
getDefaultExpanded,
|
||||||
} from "@/components/dashboard/scan-terminal/continent-grouping";
|
} from "@/components/dashboard/scan-terminal/continent-grouping";
|
||||||
import { rowName, temp, pct, edgeClass } from "./utils";
|
import { rowName, temp } from "./utils";
|
||||||
|
|
||||||
export function GroupedMarketTable({
|
export function GroupedMarketTable({
|
||||||
groups,
|
groups,
|
||||||
@@ -51,7 +49,7 @@ export function GroupedMarketTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-auto h-full">
|
<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>
|
<thead>
|
||||||
<tr className="border-b border-slate-200 bg-[#f8f9fa] text-left text-[11px] uppercase font-bold tracking-wider text-slate-500">
|
<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>
|
<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">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">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">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>
|
<th className="px-3 py-1.5 font-bold">Signal</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -73,7 +68,7 @@ export function GroupedMarketTable({
|
|||||||
<Fragment key={group.key}>
|
<Fragment key={group.key}>
|
||||||
{showHeaders && (
|
{showHeaders && (
|
||||||
<tr className="border-b border-slate-200 bg-[#eef2f6]">
|
<tr className="border-b border-slate-200 bg-[#eef2f6]">
|
||||||
<td colSpan={9} className="p-0">
|
<td colSpan={6} className="p-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleGroup(group.key)}
|
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)}>
|
<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)}
|
{temp(row.signed_gap ?? row.gap_to_target, row.temp_symbol)}
|
||||||
</td>
|
</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">
|
<td className="px-3 py-1.5">
|
||||||
<span className={clsx(
|
<span className={clsx(
|
||||||
"text-[12px] font-black",
|
"text-[12px] font-black",
|
||||||
|
|||||||
@@ -2,15 +2,9 @@
|
|||||||
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
|
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
|
||||||
import { formatPrice } from "@/components/dashboard/scan-terminal/continent-grouping";
|
import { rowName } from "@/components/dashboard/scan-terminal/utils";
|
||||||
import { rowName, pct } from "@/components/dashboard/scan-terminal/utils";
|
|
||||||
|
|
||||||
function tablePrice(row: ScanOpportunityRow) {
|
|
||||||
return formatPrice(row.midpoint, row.ask, row.bid);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function KoyfinRowsTable({
|
export function KoyfinRowsTable({
|
||||||
compact = false,
|
|
||||||
isEn,
|
isEn,
|
||||||
onSelect,
|
onSelect,
|
||||||
rows,
|
rows,
|
||||||
@@ -26,67 +20,31 @@ export function KoyfinRowsTable({
|
|||||||
<table className="w-full border-collapse text-[11px]">
|
<table className="w-full border-collapse text-[11px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-slate-200 bg-[#f3f5f7] text-[11px] uppercase tracking-wide text-slate-500">
|
<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">
|
<th className="px-2 py-1 text-left font-bold">
|
||||||
<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">
|
|
||||||
{isEn ? "City" : "城市"}
|
{isEn ? "City" : "城市"}
|
||||||
</th>
|
</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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{rows.map((row) => {
|
{rows.map((row) => (
|
||||||
const edge = Number(row.edge_percent ?? row.signed_gap ?? row.gap ?? 0);
|
<tr
|
||||||
const positive = edge >= 0;
|
key={row.id}
|
||||||
return (
|
onClick={() => onSelect(row)}
|
||||||
<tr
|
className={clsx(
|
||||||
key={row.id}
|
"cursor-pointer border-b border-slate-100 hover:bg-blue-50/70",
|
||||||
onClick={() => onSelect(row)}
|
selectedId === row.id && "bg-blue-50",
|
||||||
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)}
|
||||||
<td className="px-2 py-1">
|
</div>
|
||||||
<span className="block h-3 w-3 rounded-[2px] border border-slate-300 bg-white" />
|
<div className="truncate text-[10px] font-medium text-slate-400">
|
||||||
</td>
|
{row.airport || "--"}
|
||||||
<td className="px-1.5 py-1">
|
</div>
|
||||||
<div className="truncate font-bold text-slate-800">
|
</td>
|
||||||
{rowName(row)}
|
</tr>
|
||||||
</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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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) {
|
function buildIntDegreeTicks(series: EvidenceSeries[], data?: Array<Record<string, string | number | null>>): number[] | null {
|
||||||
const raw = String(value || "");
|
const vals = data?.length
|
||||||
const matches = raw.match(/-?\d+(?:\.\d+)?/g) || [];
|
? data.flatMap((point) => series.map((s) => point[s.key])).filter((v): v is number => validNumber(v) !== null)
|
||||||
return matches.map(Number).filter((v) => Number.isFinite(v) && v > -80 && v < 80);
|
: 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));
|
||||||
function buildMarketTemperatureOptions(row: ScanOpportunityRow | null) {
|
const max = Math.ceil(Math.max(...vals));
|
||||||
const buckets = row?.distribution_full?.length
|
const ticks: number[] = [];
|
||||||
? row.distribution_full
|
for (let d = min; d <= max; d++) ticks.push(d);
|
||||||
: row?.distribution_preview;
|
return ticks.length > 0 ? ticks : null;
|
||||||
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 buildChartDomain(
|
function buildChartDomain(
|
||||||
ticks: number[] | null,
|
|
||||||
series: EvidenceSeries[],
|
series: EvidenceSeries[],
|
||||||
data?: Array<Record<string, string | number | null>>,
|
data?: Array<Record<string, string | number | null>>,
|
||||||
): [number, number] | ["auto", "auto"] {
|
): [number, number] | ["auto", "auto"] {
|
||||||
const vals = data?.length
|
const vals = data?.length
|
||||||
? data.flatMap((point) => series.map((s) => point[s.key])).filter((v): v is number => validNumber(v) !== null)
|
? 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);
|
: series.flatMap((s) => s.values).filter((v): v is number => validNumber(v) !== null);
|
||||||
const all = [...(ticks || []), ...vals];
|
if (!vals.length) return ["auto", "auto"];
|
||||||
if (!all.length) return ["auto", "auto"];
|
const min = Math.min(...vals);
|
||||||
const min = Math.min(...all);
|
const max = Math.max(...vals);
|
||||||
const max = Math.max(...all);
|
|
||||||
const span = Math.max(1, max - min);
|
const span = Math.max(1, max - min);
|
||||||
const pad = Math.max(0.5, span * 0.08);
|
const pad = Math.max(0.5, span * 0.08);
|
||||||
return [Number((min - pad).toFixed(1)), Number((max + pad).toFixed(1))];
|
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);
|
return list.sort((a, b) => a.threshold - b.threshold);
|
||||||
}, [row, allRows]);
|
}, [row, allRows]);
|
||||||
|
|
||||||
const marketTicks = useMemo(() => buildMarketTemperatureOptions(row), [row]);
|
const intDegreeTicks = useMemo(() => buildIntDegreeTicks(series, data), [series, data]);
|
||||||
const chartDomain = useMemo(
|
const chartDomain = useMemo(
|
||||||
() => buildChartDomain(marketTicks, series, data),
|
() => buildChartDomain(series, data),
|
||||||
[marketTicks, series, data],
|
[series, data],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -968,7 +951,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<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" />
|
<CartesianGrid stroke="#dbe6ef" strokeDasharray="2 2" />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="label"
|
dataKey="label"
|
||||||
@@ -978,12 +961,13 @@ export function LiveTemperatureThresholdChart({
|
|||||||
interval={Math.max(1, Math.floor(data.length / 8))}
|
interval={Math.max(1, Math.floor(data.length / 8))}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
|
orientation="right"
|
||||||
tick={{ fontSize: 10, fill: "#64748b" }}
|
tick={{ fontSize: 10, fill: "#64748b" }}
|
||||||
tickFormatter={(v) => `${Number(v).toFixed(1)}°`}
|
tickFormatter={(v) => `${Number(v).toFixed(0)}°`}
|
||||||
axisLine={{ stroke: "#cbd5e1" }}
|
axisLine={{ stroke: "#cbd5e1" }}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
domain={chartDomain}
|
domain={chartDomain}
|
||||||
ticks={marketTicks ?? undefined}
|
ticks={intDegreeTicks ?? undefined}
|
||||||
/>
|
/>
|
||||||
{cityThresholds.map((t, idx) => {
|
{cityThresholds.map((t, idx) => {
|
||||||
const isSelected = row && (Number(row.target_threshold ?? row.target_value) === t.threshold);
|
const isSelected = row && (Number(row.target_threshold ?? row.target_value) === t.threshold);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
|
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
|
||||||
import {
|
import {
|
||||||
formatPrice,
|
|
||||||
GAP_COLOR_MAP,
|
GAP_COLOR_MAP,
|
||||||
getGapColor,
|
getGapColor,
|
||||||
getSignalLabel,
|
getSignalLabel,
|
||||||
@@ -58,7 +57,7 @@ export function MobileCityCard({
|
|||||||
Gap {tempVal(row.signed_gap ?? row.gap_to_target, row.temp_symbol)}
|
Gap {tempVal(row.signed_gap ?? row.gap_to_target, row.temp_symbol)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-slate-600">
|
<span className="text-slate-600">
|
||||||
{formatPrice(row.midpoint, row.ask, row.bid)}
|
{row.local_time || "--"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -176,16 +176,10 @@ export type GapColor = "green" | "orange" | "slate" | "gray" | "red";
|
|||||||
|
|
||||||
export function getGapColor(row: ScanOpportunityRow): GapColor {
|
export function getGapColor(row: ScanOpportunityRow): GapColor {
|
||||||
const gap = Number(row.signed_gap ?? row.gap_to_target);
|
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 (!Number.isFinite(gap)) return "gray";
|
||||||
if (liq <= 0 || spread > 20) return "red";
|
|
||||||
if (gap >= 2) return "green";
|
if (gap >= 2) return "green";
|
||||||
if (gap >= 0 && edge > 5) return "orange";
|
if (gap >= 0) return "orange";
|
||||||
if (gap >= 0) return "slate";
|
if (gap < -5) return "gray";
|
||||||
if (gap < -5 || edge < -10) return "gray";
|
|
||||||
return "slate";
|
return "slate";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,30 +191,12 @@ export const GAP_COLOR_MAP: Record<GapColor, string> = {
|
|||||||
red: "text-red-500",
|
red: "text-red-500",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function formatPrice(midpoint?: number | null, ask?: number | null, bid?: number | null): string {
|
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}¢`;
|
|
||||||
}
|
|
||||||
return "--";
|
return "--";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatSpreadLiquidity(spread?: number | null, liquidity?: number | null): string {
|
export function formatSpreadLiquidity(_spread?: number | null, _liquidity?: number | null): string {
|
||||||
const sp = Number(spread);
|
return "--";
|
||||||
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 buildContinentGroups(rows: ScanOpportunityRow[], isEn: boolean): ContinentGroup[] {
|
export function buildContinentGroups(rows: ScanOpportunityRow[], isEn: boolean): ContinentGroup[] {
|
||||||
|
|||||||
Reference in New Issue
Block a user