Refine future forecast modal trade signals and card styling
This commit is contained in:
@@ -2772,6 +2772,36 @@
|
|||||||
padding: 14px;
|
padding: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-card-head) {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-card-kicker) {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-focus-card) {
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(34, 211, 238, 0.08) 0%,
|
||||||
|
rgba(255, 255, 255, 0.02) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-support-card) {
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(148, 163, 184, 0.08) 0%,
|
||||||
|
rgba(255, 255, 255, 0.02) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.future-v2-hero-card) {
|
.root :global(.future-v2-hero-card) {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
@@ -3075,6 +3105,27 @@
|
|||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-pace-signal-grid) {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-pace-signal-card) {
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
background: rgba(255, 255, 255, 0.025);
|
||||||
|
padding: 10px;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-pace-signal-note) {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.future-v2-pace-meter) {
|
.root :global(.future-v2-pace-meter) {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
@@ -3228,6 +3279,20 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-market-card) {
|
||||||
|
background:
|
||||||
|
radial-gradient(
|
||||||
|
circle at top right,
|
||||||
|
rgba(34, 197, 94, 0.08) 0%,
|
||||||
|
rgba(15, 23, 42, 0) 40%
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(7, 16, 26, 0.96) 0%,
|
||||||
|
rgba(255, 255, 255, 0.02) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.market-layer-loading-overlay) {
|
.root :global(.market-layer-loading-overlay) {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import {
|
|||||||
} from "@/components/dashboard/PanelSections";
|
} from "@/components/dashboard/PanelSections";
|
||||||
import {
|
import {
|
||||||
getFutureModalView,
|
getFutureModalView,
|
||||||
|
getModelView,
|
||||||
|
getProbabilityView,
|
||||||
getTodayPaceView,
|
getTodayPaceView,
|
||||||
parseAiAnalysis,
|
parseAiAnalysis,
|
||||||
getTemperatureChartData,
|
getTemperatureChartData,
|
||||||
@@ -182,6 +184,46 @@ function parseVisibilityText(
|
|||||||
return "--";
|
return "--";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseBucketBoundaries(
|
||||||
|
bucket?: {
|
||||||
|
label?: string | null;
|
||||||
|
bucket?: string | null;
|
||||||
|
range?: string | null;
|
||||||
|
value?: number | null;
|
||||||
|
temp?: number | null;
|
||||||
|
} | null,
|
||||||
|
) {
|
||||||
|
if (!bucket) return null;
|
||||||
|
const raw =
|
||||||
|
String(bucket.label || "").trim() ||
|
||||||
|
String(bucket.bucket || "").trim() ||
|
||||||
|
String(bucket.range || "").trim();
|
||||||
|
if (!raw) return null;
|
||||||
|
const numbers = Array.from(raw.matchAll(/-?\d+(?:\.\d+)?/g)).map((match) =>
|
||||||
|
Number(match[0]),
|
||||||
|
);
|
||||||
|
if (!numbers.length) return null;
|
||||||
|
if (raw.includes("+")) {
|
||||||
|
return {
|
||||||
|
lower: numbers[0] ?? null,
|
||||||
|
upper: null as number | null,
|
||||||
|
boundaryLabel: `${numbers[0]}°C`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (numbers.length >= 2) {
|
||||||
|
return {
|
||||||
|
lower: numbers[0],
|
||||||
|
upper: numbers[1],
|
||||||
|
boundaryLabel: null as string | null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
lower: numbers[0],
|
||||||
|
upper: null as number | null,
|
||||||
|
boundaryLabel: `${numbers[0]}°C`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function clamp(value: number, min: number, max: number) {
|
function clamp(value: number, min: number, max: number) {
|
||||||
return Math.min(Math.max(value, min), max);
|
return Math.min(Math.max(value, min), max);
|
||||||
}
|
}
|
||||||
@@ -707,6 +749,158 @@ export function FutureForecastModal() {
|
|||||||
() => (isToday ? getTodayPaceView(detail, locale) : null),
|
() => (isToday ? getTodayPaceView(detail, locale) : null),
|
||||||
[detail, isToday, locale],
|
[detail, isToday, locale],
|
||||||
);
|
);
|
||||||
|
const probabilityView = useMemo(
|
||||||
|
() => getProbabilityView(detail, dateStr),
|
||||||
|
[dateStr, detail],
|
||||||
|
);
|
||||||
|
const modelView = useMemo(() => getModelView(detail, dateStr), [dateStr, detail]);
|
||||||
|
const topProbabilityBucket = useMemo(() => {
|
||||||
|
const buckets = Array.isArray(probabilityView?.probabilities)
|
||||||
|
? probabilityView.probabilities
|
||||||
|
: [];
|
||||||
|
return [...buckets]
|
||||||
|
.filter((bucket) => Number.isFinite(Number(bucket?.probability)))
|
||||||
|
.sort((a, b) => Number(b?.probability) - Number(a?.probability))[0];
|
||||||
|
}, [probabilityView]);
|
||||||
|
const modelSpreadView = useMemo(() => {
|
||||||
|
const values = Object.values(modelView?.models || {})
|
||||||
|
.map((value) => Number(value))
|
||||||
|
.filter((value) => Number.isFinite(value));
|
||||||
|
if (!values.length) return null;
|
||||||
|
const min = Math.min(...values);
|
||||||
|
const max = Math.max(...values);
|
||||||
|
const spread = max - min;
|
||||||
|
return {
|
||||||
|
max,
|
||||||
|
min,
|
||||||
|
spread,
|
||||||
|
};
|
||||||
|
}, [modelView]);
|
||||||
|
const boundaryRiskView = useMemo(() => {
|
||||||
|
if (!isToday || !paceView) return null;
|
||||||
|
const selectedBucket = marketScan?.temperature_bucket || null;
|
||||||
|
const bounds = parseBucketBoundaries(selectedBucket);
|
||||||
|
if (!bounds) return null;
|
||||||
|
const projected =
|
||||||
|
paceView.paceAdjustedHigh ??
|
||||||
|
(detail.deb?.prediction != null ? Number(detail.deb.prediction) : null);
|
||||||
|
if (projected == null || !Number.isFinite(projected)) return null;
|
||||||
|
|
||||||
|
const distances = [bounds.lower, bounds.upper]
|
||||||
|
.filter((value): value is number => value != null && Number.isFinite(value))
|
||||||
|
.map((value) => ({
|
||||||
|
boundary: value,
|
||||||
|
gap: Math.abs(projected - value),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.gap - b.gap);
|
||||||
|
if (!distances.length) return null;
|
||||||
|
|
||||||
|
const nearest = distances[0];
|
||||||
|
const tone =
|
||||||
|
nearest.gap <= 0.4 ? "amber" : nearest.gap <= 0.8 ? "blue" : "cyan";
|
||||||
|
const status =
|
||||||
|
nearest.gap <= 0.4
|
||||||
|
? locale === "en-US"
|
||||||
|
? "High boundary risk"
|
||||||
|
: "边界风险高"
|
||||||
|
: nearest.gap <= 0.8
|
||||||
|
? locale === "en-US"
|
||||||
|
? "Watch boundary"
|
||||||
|
: "边界需观察"
|
||||||
|
: locale === "en-US"
|
||||||
|
? "Boundary buffer"
|
||||||
|
: "边界缓冲";
|
||||||
|
const note =
|
||||||
|
locale === "en-US"
|
||||||
|
? `${projected.toFixed(1)}${detail.temp_symbol} is ${nearest.gap.toFixed(1)}${detail.temp_symbol} from the nearest boundary ${nearest.boundary.toFixed(1)}°C.`
|
||||||
|
: `${projected.toFixed(1)}${detail.temp_symbol} 距最近边界 ${nearest.boundary.toFixed(1)}°C 还有 ${nearest.gap.toFixed(1)}${detail.temp_symbol}。`;
|
||||||
|
return {
|
||||||
|
label: locale === "en-US" ? "Boundary risk" : "边界风险",
|
||||||
|
note,
|
||||||
|
status,
|
||||||
|
tone,
|
||||||
|
value: `${nearest.gap.toFixed(1)}${detail.temp_symbol}`,
|
||||||
|
};
|
||||||
|
}, [detail.deb?.prediction, detail.temp_symbol, isToday, locale, marketScan?.temperature_bucket, paceView]);
|
||||||
|
const peakWindowStateView = useMemo(() => {
|
||||||
|
if (!isToday || !paceView) return null;
|
||||||
|
const firstHour = Number(detail.peak?.first_h);
|
||||||
|
const lastHour = Number(detail.peak?.last_h);
|
||||||
|
if (
|
||||||
|
!Number.isFinite(firstHour) ||
|
||||||
|
!Number.isFinite(lastHour) ||
|
||||||
|
firstHour < 0 ||
|
||||||
|
lastHour < firstHour
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const currentMinutes = parseClockMinutes(detail.local_time);
|
||||||
|
const startMinutes = firstHour * 60;
|
||||||
|
const endMinutes = (lastHour + 1) * 60;
|
||||||
|
let status = locale === "en-US" ? "Awaiting peak" : "未进入峰值";
|
||||||
|
let tone: "amber" | "blue" | "cyan" = "blue";
|
||||||
|
if (currentMinutes != null && currentMinutes >= endMinutes) {
|
||||||
|
status = locale === "en-US" ? "Past peak" : "已过峰值";
|
||||||
|
tone = "cyan";
|
||||||
|
} else if (currentMinutes != null && currentMinutes >= startMinutes) {
|
||||||
|
status = locale === "en-US" ? "Peak window live" : "峰值窗口进行中";
|
||||||
|
tone = "amber";
|
||||||
|
}
|
||||||
|
const note =
|
||||||
|
locale === "en-US"
|
||||||
|
? `Primary peak window ${paceView.peakWindowText}.`
|
||||||
|
: `核心峰值窗口 ${paceView.peakWindowText}。`;
|
||||||
|
return {
|
||||||
|
label: locale === "en-US" ? "Peak window" : "峰值窗口状态",
|
||||||
|
note,
|
||||||
|
status,
|
||||||
|
tone,
|
||||||
|
value: paceView.peakWindowText,
|
||||||
|
};
|
||||||
|
}, [detail.local_time, detail.peak?.first_h, detail.peak?.last_h, isToday, locale, paceView]);
|
||||||
|
const networkLeadView = useMemo(() => {
|
||||||
|
if (!isToday) return null;
|
||||||
|
const delta = Number(detail.airport_vs_network_delta);
|
||||||
|
const leadSignal = detail.network_lead_signal;
|
||||||
|
if (!Number.isFinite(delta)) return null;
|
||||||
|
const leaderLabel =
|
||||||
|
String(leadSignal?.leader_station_label || "").trim() ||
|
||||||
|
String(leadSignal?.leader_station_code || "").trim();
|
||||||
|
const absDelta = Math.abs(delta);
|
||||||
|
const status =
|
||||||
|
delta <= -0.4
|
||||||
|
? locale === "en-US"
|
||||||
|
? "Airport trailing"
|
||||||
|
: "机场落后"
|
||||||
|
: delta >= 0.4
|
||||||
|
? locale === "en-US"
|
||||||
|
? "Airport leading"
|
||||||
|
: "机场领先"
|
||||||
|
: locale === "en-US"
|
||||||
|
? "Tracking network"
|
||||||
|
: "与站网齐平";
|
||||||
|
const tone =
|
||||||
|
delta <= -0.4 ? "amber" : delta >= 0.4 ? "cyan" : "blue";
|
||||||
|
const note =
|
||||||
|
delta <= -0.4
|
||||||
|
? locale === "en-US"
|
||||||
|
? `Airport anchor is ${absDelta.toFixed(1)}${detail.temp_symbol} cooler than the nearby official network${leaderLabel ? `, led by ${leaderLabel}` : ""}.`
|
||||||
|
: `机场主站当前比周边官方站网低 ${absDelta.toFixed(1)}${detail.temp_symbol}${leaderLabel ? `,领先点位是 ${leaderLabel}` : ""}。`
|
||||||
|
: delta >= 0.4
|
||||||
|
? locale === "en-US"
|
||||||
|
? `Airport anchor is ${absDelta.toFixed(1)}${detail.temp_symbol} hotter than the nearby official network.`
|
||||||
|
: `机场主站当前比周边官方站网高 ${absDelta.toFixed(1)}${detail.temp_symbol}。`
|
||||||
|
: locale === "en-US"
|
||||||
|
? "Airport anchor and nearby official network are broadly aligned."
|
||||||
|
: "机场主站与周边官方站网当前大体齐平。";
|
||||||
|
return {
|
||||||
|
label: locale === "en-US" ? "Airport vs network" : "机场 vs 周边站",
|
||||||
|
note,
|
||||||
|
status,
|
||||||
|
tone,
|
||||||
|
value: `${delta > 0 ? "+" : ""}${delta.toFixed(1)}${detail.temp_symbol}`,
|
||||||
|
};
|
||||||
|
}, [detail.airport_vs_network_delta, detail.network_lead_signal, detail.temp_symbol, isToday, locale]);
|
||||||
const isNoaaSettlement =
|
const isNoaaSettlement =
|
||||||
detail.current?.settlement_source === "noaa" ||
|
detail.current?.settlement_source === "noaa" ||
|
||||||
detail.current?.settlement_source_label === "NOAA";
|
detail.current?.settlement_source_label === "NOAA";
|
||||||
@@ -766,6 +960,48 @@ export function FutureForecastModal() {
|
|||||||
marketScan.confidence ? ` / ${marketScan.confidence}` : ""
|
marketScan.confidence ? ` / ${marketScan.confidence}` : ""
|
||||||
}`
|
}`
|
||||||
: "--";
|
: "--";
|
||||||
|
const marketExecutionSummary = (() => {
|
||||||
|
if (!marketScan?.available) {
|
||||||
|
return locale === "en-US"
|
||||||
|
? "Market scan is unavailable right now; keep the trade read anchored to station pace and bracket risk."
|
||||||
|
: "当前暂时拿不到市场扫描结果,先以站点节奏和边界风险为主。";
|
||||||
|
}
|
||||||
|
if (marketScan?.signal_label?.toUpperCase() === "BUY YES") {
|
||||||
|
return locale === "en-US"
|
||||||
|
? `Model side is hotter than the tape by ${marketEdge}. If pace stays constructive, the Yes side still has room.`
|
||||||
|
: `模型侧当前比盘口更热 ${marketEdge},如果盘中节奏不掉,Yes 侧仍有空间。`;
|
||||||
|
}
|
||||||
|
if (marketScan?.signal_label?.toUpperCase() === "BUY NO") {
|
||||||
|
return locale === "en-US"
|
||||||
|
? `Model side is cooler than the tape by ${marketEdge}. If pace keeps lagging, the No side stays cleaner.`
|
||||||
|
: `模型侧当前比盘口更冷 ${marketEdge},如果节奏继续落后,No 侧会更干净。`;
|
||||||
|
}
|
||||||
|
return locale === "en-US"
|
||||||
|
? "Model and market are broadly balanced; let pace and boundary risk break the tie."
|
||||||
|
: "模型和市场当前大体均衡,接下来主要看节奏和边界风险来决定站边。";
|
||||||
|
})();
|
||||||
|
const probabilitySummary = (() => {
|
||||||
|
if (!topProbabilityBucket) {
|
||||||
|
return locale === "en-US"
|
||||||
|
? "Probability mass is still too dispersed; avoid over-reading a single bracket."
|
||||||
|
: "当前概率还比较分散,不要只盯单一区间。";
|
||||||
|
}
|
||||||
|
const bucketLabel = formatBucketLabel(topProbabilityBucket);
|
||||||
|
const bucketProb = formatMarketPercent(topProbabilityBucket.probability);
|
||||||
|
return locale === "en-US"
|
||||||
|
? `Highest current hit probability is ${bucketLabel} at ${bucketProb}. Treat this as the base case, not the final settlement.`
|
||||||
|
: `当前命中概率最高的是 ${bucketLabel}(${bucketProb}),可把它当作基准情形,但不要直接等同于最终结算。`;
|
||||||
|
})();
|
||||||
|
const modelSummary = (() => {
|
||||||
|
if (!modelSpreadView) {
|
||||||
|
return locale === "en-US"
|
||||||
|
? "Model spread is unavailable right now."
|
||||||
|
: "当前拿不到可用的模型分歧。";
|
||||||
|
}
|
||||||
|
return locale === "en-US"
|
||||||
|
? `Model range runs from ${modelSpreadView.min.toFixed(1)}${detail.temp_symbol} to ${modelSpreadView.max.toFixed(1)}${detail.temp_symbol}; spread ${modelSpreadView.spread.toFixed(1)}${detail.temp_symbol}.`
|
||||||
|
: `当前模型区间在 ${modelSpreadView.min.toFixed(1)}${detail.temp_symbol} 到 ${modelSpreadView.max.toFixed(1)}${detail.temp_symbol},分歧 ${modelSpreadView.spread.toFixed(1)}${detail.temp_symbol}。`;
|
||||||
|
})();
|
||||||
const upperAirSignal = detail.vertical_profile_signal || {};
|
const upperAirSignal = detail.vertical_profile_signal || {};
|
||||||
const tafSignal = detail.taf?.signal || {};
|
const tafSignal = detail.taf?.signal || {};
|
||||||
const topBucketProbability = normalizeMarketValue(topBucket?.probability);
|
const topBucketProbability = normalizeMarketValue(topBucket?.probability);
|
||||||
@@ -1003,6 +1239,33 @@ export function FutureForecastModal() {
|
|||||||
...getCurrentMetricDescriptor("visibility", visibilityText, visibilityValue),
|
...getCurrentMetricDescriptor("visibility", visibilityText, visibilityValue),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
const tradeMetricVisuals = currentMetricVisuals.filter((metric) =>
|
||||||
|
["dewpoint", "wind", "humidity"].includes(metric.key),
|
||||||
|
);
|
||||||
|
const tradeMetricSummary = (() => {
|
||||||
|
const dewSpread =
|
||||||
|
detail.current?.temp != null && fallbackDewpoint != null
|
||||||
|
? Number(detail.current.temp) - Number(fallbackDewpoint)
|
||||||
|
: null;
|
||||||
|
if (dewSpread != null && dewSpread <= 2) {
|
||||||
|
return locale === "en-US"
|
||||||
|
? "Near-saturation air is still in play. If cloud builds into the peak window, upside can get capped quickly."
|
||||||
|
: "露点差已经很窄,空气接近饱和。如果云层在峰值窗口内长起来,上冲会更容易被压住。";
|
||||||
|
}
|
||||||
|
if (windValue != null && windValue >= 18) {
|
||||||
|
return locale === "en-US"
|
||||||
|
? "Surface wind is still strong. Mixing support is better, but it can also make the tape noisier intraday."
|
||||||
|
: "近地面风速仍偏强,混合作用会更充分,但盘中波动也会更大。";
|
||||||
|
}
|
||||||
|
if (humidityValue != null && humidityValue <= 40) {
|
||||||
|
return locale === "en-US"
|
||||||
|
? "Air mass is still relatively dry. Unless cloud/rain arrives, the surface layer is not the main cap right now."
|
||||||
|
: "空气仍偏干,除非后续云雨快速起来,否则近地面层目前不是主要压温来源。";
|
||||||
|
}
|
||||||
|
return locale === "en-US"
|
||||||
|
? "Surface readings are broadly neutral. Keep the main decision anchored to pace, boundary risk, and the peak window."
|
||||||
|
: "近地面指标整体偏中性,主判断仍然要看当前节奏、边界风险和峰值窗口。";
|
||||||
|
})();
|
||||||
const displayedUpperAirSummary =
|
const displayedUpperAirSummary =
|
||||||
marketAwareUpperAirCue?.summary || view.front.upperAirSummary;
|
marketAwareUpperAirCue?.summary || view.front.upperAirSummary;
|
||||||
const displayedUpperAirMetrics = (view.front.upperAirMetrics || []).map(
|
const displayedUpperAirMetrics = (view.front.upperAirMetrics || []).map(
|
||||||
@@ -1018,6 +1281,40 @@ export function FutureForecastModal() {
|
|||||||
}
|
}
|
||||||
: metric,
|
: metric,
|
||||||
);
|
);
|
||||||
|
const todayTradeSummaryLines = useMemo(() => {
|
||||||
|
if (!isToday) return [] as string[];
|
||||||
|
const lines: string[] = [];
|
||||||
|
if (paceView) {
|
||||||
|
const headline =
|
||||||
|
paceView.biasTone === "warm"
|
||||||
|
? locale === "en-US"
|
||||||
|
? `Intraday pace is running hot by ${paceView.deltaText}; the day high still has room to lean above the base curve.`
|
||||||
|
: `日内节奏当前偏热 ${paceView.deltaText},日高仍有机会落在基础曲线之上。`
|
||||||
|
: paceView.biasTone === "cold"
|
||||||
|
? locale === "en-US"
|
||||||
|
? `Intraday pace is trailing by ${paceView.deltaText}; higher buckets need more caution.`
|
||||||
|
: `日内节奏当前落后 ${paceView.deltaText},继续追更高温区间要更谨慎。`
|
||||||
|
: locale === "en-US"
|
||||||
|
? "Intraday pace is still tracking the curve; the next move depends on the peak-window push."
|
||||||
|
: "日内节奏当前基本贴着曲线运行,下一步主要看峰值窗口内还有没有上冲。";
|
||||||
|
lines.push(headline);
|
||||||
|
}
|
||||||
|
if (boundaryRiskView) {
|
||||||
|
lines.push(
|
||||||
|
locale === "en-US"
|
||||||
|
? `${boundaryRiskView.label}: ${boundaryRiskView.status}. ${boundaryRiskView.note}`
|
||||||
|
: `${boundaryRiskView.label}:${boundaryRiskView.status}。${boundaryRiskView.note}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (networkLeadView) {
|
||||||
|
lines.push(
|
||||||
|
locale === "en-US"
|
||||||
|
? `${networkLeadView.label}: ${networkLeadView.status}. ${networkLeadView.note}`
|
||||||
|
: `${networkLeadView.label}:${networkLeadView.status}。${networkLeadView.note}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}, [boundaryRiskView, isToday, locale, networkLeadView, paceView]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -1134,11 +1431,18 @@ export function FutureForecastModal() {
|
|||||||
<div className="future-v2-layout">
|
<div className="future-v2-layout">
|
||||||
<aside className="future-v2-left">
|
<aside className="future-v2-left">
|
||||||
<section className="future-v2-card future-v2-hero-card">
|
<section className="future-v2-card future-v2-hero-card">
|
||||||
<h3 className="future-v2-hero-title">
|
<div className="future-v2-card-head">
|
||||||
{locale === "en-US"
|
<h3 className="future-v2-hero-title">
|
||||||
? "Current Conditions"
|
{locale === "en-US"
|
||||||
: "实况与气象特征"}
|
? "Anchor Status"
|
||||||
</h3>
|
: "锚点状态"}
|
||||||
|
</h3>
|
||||||
|
<div className="future-v2-card-kicker">
|
||||||
|
{locale === "en-US"
|
||||||
|
? "Settlement anchor and current clock"
|
||||||
|
: "结算锚点与当前时钟"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="future-v2-hero-main">
|
<div className="future-v2-hero-main">
|
||||||
<div className="future-v2-hero-temp">
|
<div className="future-v2-hero-temp">
|
||||||
{currentTempText}
|
{currentTempText}
|
||||||
@@ -1188,13 +1492,19 @@ export function FutureForecastModal() {
|
|||||||
<div className="future-v2-mini-grid">
|
<div className="future-v2-mini-grid">
|
||||||
<div className="future-v2-mini-item">
|
<div className="future-v2-mini-item">
|
||||||
<span>
|
<span>
|
||||||
{locale === "en-US" ? "High So Far" : "目前最高温"}
|
{locale === "en-US" ? "High so far" : "日内已见高点"}
|
||||||
</span>
|
</span>
|
||||||
<strong>
|
<strong>
|
||||||
{topObservedTemp ?? "--"}
|
{topObservedTemp ?? "--"}
|
||||||
{detail.temp_symbol}
|
{detail.temp_symbol}
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="future-v2-mini-item">
|
||||||
|
<span>
|
||||||
|
{locale === "en-US" ? "Anchor clock" : "锚点时钟"}
|
||||||
|
</span>
|
||||||
|
<strong>{detail.current?.obs_time || "--"}</strong>
|
||||||
|
</div>
|
||||||
<div className="future-v2-mini-item">
|
<div className="future-v2-mini-item">
|
||||||
<span>
|
<span>
|
||||||
{locale === "en-US" ? "Sunrise" : "日出时间"}
|
{locale === "en-US" ? "Sunrise" : "日出时间"}
|
||||||
@@ -1205,26 +1515,25 @@ export function FutureForecastModal() {
|
|||||||
<span>
|
<span>
|
||||||
{locale === "en-US" ? "Sunset" : "日落时间"}
|
{locale === "en-US" ? "Sunset" : "日落时间"}
|
||||||
</span>
|
</span>
|
||||||
<strong>{detail.forecast?.sunset || "--"}</strong>
|
|
||||||
</div>
|
|
||||||
<div className="future-v2-mini-item">
|
|
||||||
<span>
|
|
||||||
{locale === "en-US" ? "Sunshine" : "日照时长"}
|
|
||||||
</span>
|
|
||||||
<strong>
|
<strong>
|
||||||
{detail.forecast?.sunshine_hours != null
|
{detail.forecast?.sunset || "--"}
|
||||||
? `${detail.forecast.sunshine_hours}h`
|
|
||||||
: "--"}
|
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{paceView ? (
|
{paceView ? (
|
||||||
<section className="future-v2-card future-v2-pace-card">
|
<section className="future-v2-card future-v2-pace-card future-v2-focus-card">
|
||||||
<h4 className="future-v2-card-title">
|
<div className="future-v2-card-head">
|
||||||
{locale === "en-US" ? "Current Pace" : "当前节奏 Pace"}
|
<h4 className="future-v2-card-title">
|
||||||
</h4>
|
{locale === "en-US" ? "Current Pace" : "当前节奏"}
|
||||||
|
</h4>
|
||||||
|
<div className="future-v2-card-kicker">
|
||||||
|
{locale === "en-US"
|
||||||
|
? "Expected now vs airport anchor"
|
||||||
|
: "此刻应到 vs 机场锚点"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="future-v2-pace-head">
|
<div className="future-v2-pace-head">
|
||||||
<span className="future-v2-pace-kicker">
|
<span className="future-v2-pace-kicker">
|
||||||
{paceView.kicker}
|
{paceView.kicker}
|
||||||
@@ -1305,19 +1614,50 @@ export function FutureForecastModal() {
|
|||||||
<strong>{paceView.peakWindowText}</strong>
|
<strong>{paceView.peakWindowText}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="future-v2-pace-signal-grid">
|
||||||
|
{[boundaryRiskView, peakWindowStateView, networkLeadView]
|
||||||
|
.filter((item) => item != null)
|
||||||
|
.map((item) => (
|
||||||
|
<div key={item.label} className="future-v2-pace-signal-card">
|
||||||
|
<div className="future-v2-signal-head">
|
||||||
|
<span>{item.label}</span>
|
||||||
|
<em
|
||||||
|
className={clsx(
|
||||||
|
"future-v2-signal-tag",
|
||||||
|
item.tone === "cyan" && "cyan",
|
||||||
|
item.tone === "blue" && "blue",
|
||||||
|
item.tone === "amber" && "amber",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.status}
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
<strong>{item.value}</strong>
|
||||||
|
<div className="future-v2-pace-signal-note">
|
||||||
|
{item.note}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<section className="future-v2-card">
|
<section className="future-v2-card future-v2-support-card">
|
||||||
<h4 className="future-v2-card-title">
|
<div className="future-v2-card-head">
|
||||||
{locale === "en-US" ? "Current Metrics" : "当前指标"}
|
<h4 className="future-v2-card-title">
|
||||||
</h4>
|
{locale === "en-US" ? "Trade-Sensitive Metrics" : "交易关键指标"}
|
||||||
<IntradaySignalScene
|
</h4>
|
||||||
metrics={currentMetricVisuals}
|
<div className="future-v2-card-kicker">
|
||||||
score={view.front.score}
|
{locale === "en-US"
|
||||||
/>
|
? "Only the surface readings that still matter"
|
||||||
|
: "只保留仍会影响峰值判断的近地面项"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="future-v2-pace-summary" style={{ marginTop: "12px" }}>
|
||||||
|
{tradeMetricSummary}
|
||||||
|
</div>
|
||||||
<div className="future-v2-mini-grid future-v2-mini-grid-tight">
|
<div className="future-v2-mini-grid future-v2-mini-grid-tight">
|
||||||
{currentMetricVisuals.map((metric) => (
|
{tradeMetricVisuals.map((metric) => (
|
||||||
<div key={metric.key} className="future-v2-mini-item future-v2-signal-item">
|
<div key={metric.key} className="future-v2-mini-item future-v2-signal-item">
|
||||||
<div className="future-v2-signal-head">
|
<div className="future-v2-signal-head">
|
||||||
<span>{metric.label}</span>
|
<span>{metric.label}</span>
|
||||||
@@ -1352,10 +1692,17 @@ export function FutureForecastModal() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="future-v2-card">
|
<section className="future-v2-card future-v2-market-card future-v2-focus-card">
|
||||||
<h4 className="future-v2-card-title">
|
<div className="future-v2-card-head">
|
||||||
{locale === "en-US" ? "Market Alignment" : "市场对照"}
|
<h4 className="future-v2-card-title">
|
||||||
</h4>
|
{locale === "en-US" ? "Market Alignment" : "市场对照"}
|
||||||
|
</h4>
|
||||||
|
<div className="future-v2-card-kicker">
|
||||||
|
{locale === "en-US"
|
||||||
|
? "Model, tape, and execution in one view"
|
||||||
|
: "把模型、盘口和执行价放到一张卡里"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="future-v2-market-v3">
|
<div className="future-v2-market-v3">
|
||||||
{/* Loading Overlay */}
|
{/* Loading Overlay */}
|
||||||
{store.loadingState.marketScan && (
|
{store.loadingState.marketScan && (
|
||||||
@@ -1387,6 +1734,69 @@ export function FutureForecastModal() {
|
|||||||
{settlementBucketLabel}
|
{settlementBucketLabel}
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="market-edge-box">
|
||||||
|
<div className="market-edge-header">
|
||||||
|
<span>
|
||||||
|
{locale === "en-US" ? "Current edge" : "当前 edge"}
|
||||||
|
</span>
|
||||||
|
<strong
|
||||||
|
className={clsx(
|
||||||
|
"market-edge-val",
|
||||||
|
Number(marketScan?.edge_percent) > 0
|
||||||
|
? "positive"
|
||||||
|
: "negative",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{marketEdge}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div className="market-edge-compare">
|
||||||
|
<div className="edge-stat">
|
||||||
|
<span className="edge-label">
|
||||||
|
{locale === "en-US" ? "Model prob" : "模型概率"}
|
||||||
|
</span>
|
||||||
|
<span className="edge-value">{modelProbability}</span>
|
||||||
|
</div>
|
||||||
|
<div className="edge-stat">
|
||||||
|
<span className="edge-label">
|
||||||
|
{locale === "en-US" ? "Market mid" : "市场中位"}
|
||||||
|
</span>
|
||||||
|
<span className="edge-value">{marketMidpoint}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="market-layer-book">
|
||||||
|
<div className="market-sub-title">
|
||||||
|
💸 {locale === "en-US" ? "Execution" : "盘口执行"}
|
||||||
|
</div>
|
||||||
|
<div className="market-book-row">
|
||||||
|
<span className="book-label">
|
||||||
|
{locale === "en-US" ? "Yes bid / ask" : "Yes 买 / 卖"}
|
||||||
|
</span>
|
||||||
|
<div className="book-quote">
|
||||||
|
<strong>{marketYesBuy}</strong>
|
||||||
|
<span> / {marketYesSell}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="market-book-row">
|
||||||
|
<span className="book-label">
|
||||||
|
{locale === "en-US" ? "No bid / ask" : "No 买 / 卖"}
|
||||||
|
</span>
|
||||||
|
<div className="book-quote">
|
||||||
|
<strong>{marketNoBuy}</strong>
|
||||||
|
<span> / {marketNoSell}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="market-book-row">
|
||||||
|
<span className="book-label">
|
||||||
|
{locale === "en-US" ? "Spread" : "价差"}
|
||||||
|
</span>
|
||||||
|
<div className="book-quote">
|
||||||
|
<strong>{marketSpread}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Layer 3: Context */}
|
{/* Layer 3: Context */}
|
||||||
@@ -1413,13 +1823,16 @@ export function FutureForecastModal() {
|
|||||||
{locale === "en-US" ? "Signal" : "信号"}:{" "}
|
{locale === "en-US" ? "Signal" : "信号"}:{" "}
|
||||||
<strong>{marketSignal}</strong>
|
<strong>{marketSignal}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="future-v2-pace-summary" style={{ marginTop: "10px" }}>
|
||||||
|
{marketExecutionSummary}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="future-v2-card">
|
<section className="future-v2-card">
|
||||||
<h4 className="future-v2-card-title">
|
<h4 className="future-v2-card-title">
|
||||||
{locale === "en-US"
|
{locale === "en-US"
|
||||||
? "Current Metrics"
|
? "Trade Snapshot"
|
||||||
: "当前指标补充"}
|
: "交易快照"}
|
||||||
</h4>
|
</h4>
|
||||||
<div className="future-v2-mini-grid future-v2-mini-grid-tight">
|
<div className="future-v2-mini-grid future-v2-mini-grid-tight">
|
||||||
<div className="future-v2-mini-item">
|
<div className="future-v2-mini-item">
|
||||||
@@ -1433,6 +1846,14 @@ export function FutureForecastModal() {
|
|||||||
<span>{locale === "en-US" ? "Current obs" : "当前观测"}</span>
|
<span>{locale === "en-US" ? "Current obs" : "当前观测"}</span>
|
||||||
<strong>{currentTempText}</strong>
|
<strong>{currentTempText}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="future-v2-mini-item">
|
||||||
|
<span>{locale === "en-US" ? "Market signal" : "市场信号"}</span>
|
||||||
|
<strong>{marketSignal}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="future-v2-mini-item">
|
||||||
|
<span>{locale === "en-US" ? "Current edge" : "当前 edge"}</span>
|
||||||
|
<strong>{marketEdge}</strong>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -1449,7 +1870,12 @@ export function FutureForecastModal() {
|
|||||||
|
|
||||||
<div className="future-modal-grid">
|
<div className="future-modal-grid">
|
||||||
<section className="future-modal-section">
|
<section className="future-modal-section">
|
||||||
<h3>{t("future.probability")}</h3>
|
<h3>
|
||||||
|
{locale === "en-US" ? "Current Hit Odds" : "当前命中胜率"}
|
||||||
|
</h3>
|
||||||
|
<div className="future-text-block" style={{ marginBottom: "12px" }}>
|
||||||
|
{probabilitySummary}
|
||||||
|
</div>
|
||||||
<div style={{ position: "relative", minHeight: "120px" }}>
|
<div style={{ position: "relative", minHeight: "120px" }}>
|
||||||
{/* Loading Overlay */}
|
{/* Loading Overlay */}
|
||||||
{store.loadingState.marketScan && (
|
{store.loadingState.marketScan && (
|
||||||
@@ -1477,7 +1903,12 @@ export function FutureForecastModal() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="future-modal-section">
|
<section className="future-modal-section">
|
||||||
<h3>{t("future.models")}</h3>
|
<h3>
|
||||||
|
{locale === "en-US" ? "Model Range & Spread" : "模型区间与分歧"}
|
||||||
|
</h3>
|
||||||
|
<div className="future-text-block" style={{ marginBottom: "12px" }}>
|
||||||
|
{modelSummary}
|
||||||
|
</div>
|
||||||
<ModelForecast
|
<ModelForecast
|
||||||
detail={detail}
|
detail={detail}
|
||||||
targetDate={dateStr}
|
targetDate={dateStr}
|
||||||
@@ -1516,14 +1947,18 @@ export function FutureForecastModal() {
|
|||||||
{Math.round(view.front.precipMax)}%
|
{Math.round(view.front.precipMax)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{view.front.summary ? (
|
{todayTradeSummaryLines.length > 0 || view.front.summary ? (
|
||||||
<div className="future-trend-summary">
|
<div className="future-trend-summary">
|
||||||
{Array.isArray(view.front.summaryLines) &&
|
{[
|
||||||
view.front.summaryLines.length > 0
|
...todayTradeSummaryLines,
|
||||||
? view.front.summaryLines.map((line, index) => (
|
...(Array.isArray(view.front.summaryLines)
|
||||||
|
? view.front.summaryLines
|
||||||
|
: view.front.summary
|
||||||
|
? [view.front.summary]
|
||||||
|
: []),
|
||||||
|
].map((line, index) => (
|
||||||
<div key={`${index}-${line}`}>{line}</div>
|
<div key={`${index}-${line}`}>{line}</div>
|
||||||
))
|
))}
|
||||||
: view.front.summary}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user