术语统一:合约/市场/交易 → 阈值/信号/实况路径/多模型预测
This commit is contained in:
@@ -20,7 +20,7 @@ const jetbrainsMono = JetBrains_Mono({
|
|||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "PolyWeather | Weather Intelligence",
|
title: "PolyWeather | Weather Intelligence",
|
||||||
description:
|
description:
|
||||||
"PolyWeather paid professional terminal for weather-market intelligence, city decision cards, and subscription-only analytics.",
|
"PolyWeather paid professional terminal for weather-signal intelligence, city decision cards, and subscription-only analytics.",
|
||||||
manifest: "/site.webmanifest",
|
manifest: "/site.webmanifest",
|
||||||
icons: {
|
icons: {
|
||||||
icon: [
|
icon: [
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { redirect } from "next/navigation";
|
|||||||
import { InstitutionalLandingPage } from "@/components/landing/InstitutionalLandingPage";
|
import { InstitutionalLandingPage } from "@/components/landing/InstitutionalLandingPage";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "PolyWeather | Institutional Weather Market Intelligence",
|
title: "PolyWeather | Institutional Weather Signal Intelligence",
|
||||||
description:
|
description:
|
||||||
"PolyWeather is a paid professional weather-market intelligence terminal with METAR evidence, DEB forecast blending, and AI decision cards.",
|
"PolyWeather is a paid professional weather-signal intelligence terminal with METAR evidence, DEB forecast blending, and AI decision cards.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function HomePage({
|
export default async function HomePage({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ScanTerminalDashboard } from "@/components/dashboard/ScanTerminalDashbo
|
|||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "PolyWeather Terminal | Paid Product",
|
title: "PolyWeather Terminal | Paid Product",
|
||||||
description:
|
description:
|
||||||
"Paid PolyWeather decision terminal for weather-market analysis and city decision cards.",
|
"Paid PolyWeather decision terminal for weather-signal analysis and city decision cards.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TerminalPage() {
|
export default function TerminalPage() {
|
||||||
|
|||||||
@@ -105,8 +105,8 @@ export function LoginClient({ nextPath, initialMode }: LoginClientProps) {
|
|||||||
? "By proceeding, you agree to the Privacy Policy and Terms & Conditions."
|
? "By proceeding, you agree to the Privacy Policy and Terms & Conditions."
|
||||||
: "继续操作即代表您同意隐私政策与服务条款。",
|
: "继续操作即代表您同意隐私政策与服务条款。",
|
||||||
desc: isEn
|
desc: isEn
|
||||||
? "Access robust METAR observations, advanced DEB forecast blends, and real-time AI decision cards that bring clarity to your weather-market portfolios."
|
? "Access robust METAR observations, advanced DEB forecast blends, and real-time AI decision cards that bring clarity to your weather-signal portfolios."
|
||||||
: "提供精准的机场 METAR 实况、先进的 DEB 智能融合预测和实时 AI 决策卡片,助您看清天气市场脉络。",
|
: "提供精准的机场 METAR 实况、先进的 DEB 智能融合预测和实时 AI 决策卡片,助您看清天气信号脉络。",
|
||||||
trusted: isEn ? "Trusted by institutional traders" : "深受机构交易员信赖",
|
trusted: isEn ? "Trusted by institutional traders" : "深受机构交易员信赖",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -82,18 +82,18 @@ function createLocalAccess(): ProAccessState {
|
|||||||
|
|
||||||
|
|
||||||
const TERM = {
|
const TERM = {
|
||||||
cityContract: { en: "City / Contract", zh: "城市 / 合约" },
|
cityThreshold: { en: "City / Threshold", zh: "城市 / 阈值" },
|
||||||
live: { en: "Live", zh: "实测" },
|
live: { en: "Live", zh: "实测" },
|
||||||
deb: { en: "DEB", zh: "DEB" },
|
deb: { en: "DEB", zh: "DEB" },
|
||||||
mkt: { en: "Mkt", zh: "市场" },
|
mkt: { en: "Mkt", zh: "信号" },
|
||||||
edge: { en: "Edge", zh: "优势" },
|
edge: { en: "Edge", zh: "优势" },
|
||||||
liq: { en: "Liq", zh: "流动性" },
|
liq: { en: "Liq", zh: "流动性" },
|
||||||
signal: { en: "Signal", zh: "信号" },
|
signal: { en: "Signal", zh: "信号" },
|
||||||
searchPlaceholder: { en: "Search city, contract, station, or signal", zh: "搜索城市、合约、站点或信号" },
|
searchPlaceholder: { en: "Search city, threshold, station, or signal", zh: "搜索城市、阈值、站点或信号" },
|
||||||
weatherContracts: { en: "Weather Contracts", zh: "天气合约" },
|
weatherThresholds: { en: "Weather Thresholds", zh: "天气阈值" },
|
||||||
selectedContractMonitor: { en: "Selected Contract Monitor", zh: "选中合约监控" },
|
selectedThresholdMonitor: { en: "Selected Threshold Monitor", zh: "选中阈值监控" },
|
||||||
probabilityDistribution: { en: "Probability Distribution", zh: "概率分布" },
|
probabilityDistribution: { en: "Probability Distribution", zh: "概率分布" },
|
||||||
marketList: { en: "Market List", zh: "市场列表" },
|
signalList: { en: "Signal List", zh: "信号列表" },
|
||||||
watchlist: { en: "Watchlist", zh: "观察列表" },
|
watchlist: { en: "Watchlist", zh: "观察列表" },
|
||||||
rows: { en: "Rows", zh: "行数" },
|
rows: { en: "Rows", zh: "行数" },
|
||||||
avgEdge: { en: "Avg Edge", zh: "平均优势" },
|
avgEdge: { en: "Avg Edge", zh: "平均优势" },
|
||||||
@@ -103,9 +103,9 @@ const TERM = {
|
|||||||
model: { en: "Model", zh: "模型" },
|
model: { en: "Model", zh: "模型" },
|
||||||
noData: { en: "No data", zh: "无数据" },
|
noData: { en: "No data", zh: "无数据" },
|
||||||
noDistributionData: { en: "No distribution data", zh: "无分布数据" },
|
noDistributionData: { en: "No distribution data", zh: "无分布数据" },
|
||||||
selectContract: {
|
selectThreshold: {
|
||||||
en: "Select a weather contract to inspect model edge, market price, and live evidence.",
|
en: "Select a weather threshold to inspect model edge, signal price, and live evidence.",
|
||||||
zh: "选择天气合约以查看模型优势、市场价格和实况证据。",
|
zh: "选择天气阈值以查看模型优势、信号价格和实况证据。",
|
||||||
},
|
},
|
||||||
signInToContinue: { en: "Sign in to continue", zh: "请先登录" },
|
signInToContinue: { en: "Sign in to continue", zh: "请先登录" },
|
||||||
signInHint: {
|
signInHint: {
|
||||||
@@ -117,8 +117,8 @@ const TERM = {
|
|||||||
learnAbout: { en: "Learn about PolyWeather", zh: "了解 PolyWeather" },
|
learnAbout: { en: "Learn about PolyWeather", zh: "了解 PolyWeather" },
|
||||||
proAccessRequired: { en: "Pro Access Required", zh: "需要付费订阅" },
|
proAccessRequired: { en: "Pro Access Required", zh: "需要付费订阅" },
|
||||||
proDesc: {
|
proDesc: {
|
||||||
en: "The PolyWeather terminal is a paid product. Subscribe to unlock real-time weather-market intelligence.",
|
en: "The PolyWeather terminal is a paid product. Subscribe to unlock real-time weather-signal intelligence.",
|
||||||
zh: "PolyWeather 决策台为付费产品。订阅以解锁实时天气市场情报。",
|
zh: "PolyWeather 决策台为付费产品。订阅以解锁实时天气信号情报。",
|
||||||
},
|
},
|
||||||
subscriptionTerms: {
|
subscriptionTerms: {
|
||||||
en: "Billed monthly. Cancel anytime. Payment via USDC on Polygon.",
|
en: "Billed monthly. Cancel anytime. Payment via USDC on Polygon.",
|
||||||
@@ -131,7 +131,7 @@ const TERM = {
|
|||||||
zh: "你需要开通有效订阅才能访问决策台。",
|
zh: "你需要开通有效订阅才能访问决策台。",
|
||||||
},
|
},
|
||||||
backToProduct: { en: "Back to product overview", zh: "返回产品介绍页" },
|
backToProduct: { en: "Back to product overview", zh: "返回产品介绍页" },
|
||||||
dashboard: { en: "PolyWeather Terminal", zh: "PolyWeather 交易决策台" },
|
dashboard: { en: "PolyWeather Terminal", zh: "PolyWeather 天气决策台" },
|
||||||
refresh: { en: "Refresh", zh: "刷新" },
|
refresh: { en: "Refresh", zh: "刷新" },
|
||||||
switchLang: { en: "Switch to Chinese", zh: "切换到英文" },
|
switchLang: { en: "Switch to Chinese", zh: "切换到英文" },
|
||||||
globalWeatherFactors: { en: "Global Weather Factors", zh: "全球天气因子" },
|
globalWeatherFactors: { en: "Global Weather Factors", zh: "全球天气因子" },
|
||||||
@@ -356,7 +356,7 @@ function PolyWeatherTerminal({
|
|||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [searchInputRef, setSearchQuery]);
|
}, [searchInputRef, setSearchQuery]);
|
||||||
const [navExpanded, setNavExpanded] = useState(false);
|
const [navExpanded, setNavExpanded] = useState(false);
|
||||||
const [activeNavKey, setActiveNavKey] = useState<string>("contracts");
|
const [activeNavKey, setActiveNavKey] = useState<string>("thresholds");
|
||||||
|
|
||||||
const [gridCols, setGridCols] = useState<number>(() => {
|
const [gridCols, setGridCols] = useState<number>(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
@@ -473,7 +473,7 @@ function PolyWeatherTerminal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NAV_ITEMS = [
|
const NAV_ITEMS = [
|
||||||
{ key: "contracts", Icon: Table2, labelEn: "Contracts", labelZh: "天气合约" },
|
{ key: "thresholds", Icon: Table2, labelEn: "Thresholds", labelZh: "天气阈值" },
|
||||||
{ key: "training", Icon: GraduationCap, labelEn: "Training", labelZh: "训练数据" },
|
{ key: "training", Icon: GraduationCap, labelEn: "Training", labelZh: "训练数据" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,21 @@ function pairKey(pair: [string, string]) {
|
|||||||
return pair.map(normalizeRunwayLabel).sort().join("/");
|
return pair.map(normalizeRunwayLabel).sort().join("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runwaySeriesKey(rwy: string) {
|
||||||
|
return `runway_${String(rwy || "unknown")
|
||||||
|
.split("/")
|
||||||
|
.map(normalizeRunwayLabel)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("_")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTemperatureSeriesVisibleByDefault(city: string, seriesKey: string) {
|
||||||
|
if (seriesKey.startsWith("model_curve_")) {
|
||||||
|
return normalizeCityKey(city) === "paris" && seriesKey === "model_curve_AROME HD";
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function buildRunwayPlates(
|
function buildRunwayPlates(
|
||||||
amos: AmosData | null | undefined,
|
amos: AmosData | null | undefined,
|
||||||
row: ScanOpportunityRow | null,
|
row: ScanOpportunityRow | null,
|
||||||
@@ -289,6 +304,8 @@ type HourlyForecast = {
|
|||||||
airportPrimary?: AirportCurrentConditions | null;
|
airportPrimary?: AirportCurrentConditions | null;
|
||||||
forecastDaily?: ForecastDay[];
|
forecastDaily?: ForecastDay[];
|
||||||
multiModelDaily?: Record<string, DailyModelForecast>;
|
multiModelDaily?: Record<string, DailyModelForecast>;
|
||||||
|
settlementTodayObs?: ObsPoint[];
|
||||||
|
metarTodayObs?: ObsPoint[];
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
function parseRunwayHistoryValue(point: Record<string, unknown>) {
|
function parseRunwayHistoryValue(point: Record<string, unknown>) {
|
||||||
@@ -335,7 +352,7 @@ function buildRunwayHistorySeries(
|
|||||||
.slice(-MAX_OBS_POINTS);
|
.slice(-MAX_OBS_POINTS);
|
||||||
const isSettlement = isSettlementRunway(row, normalizedRwy);
|
const isSettlement = isSettlementRunway(row, normalizedRwy);
|
||||||
return {
|
return {
|
||||||
key: `runway_${index}`,
|
key: runwaySeriesKey(normalizedRwy),
|
||||||
label: `${normalizedRwy}${isSettlement ? (row ? " 结算跑道" : " Settlement") : ""}`,
|
label: `${normalizedRwy}${isSettlement ? (row ? " 结算跑道" : " Settlement") : ""}`,
|
||||||
rwy: normalizedRwy,
|
rwy: normalizedRwy,
|
||||||
isSettlement,
|
isSettlement,
|
||||||
@@ -374,7 +391,7 @@ function buildRunwayHistorySeries(
|
|||||||
.filter((point): point is { ts: number; value: number } => point !== null);
|
.filter((point): point is { ts: number; value: number } => point !== null);
|
||||||
if (values.length <= 1) return null;
|
if (values.length <= 1) return null;
|
||||||
return {
|
return {
|
||||||
key: `runway_${index}`,
|
key: runwaySeriesKey(rwy),
|
||||||
label: `${rwy}${isSettlement ? " 结算跑道" : ""}`,
|
label: `${rwy}${isSettlement ? " 结算跑道" : ""}`,
|
||||||
rwy,
|
rwy,
|
||||||
isSettlement,
|
isSettlement,
|
||||||
@@ -605,8 +622,8 @@ function buildFullDayChartData(
|
|||||||
const tzOffset = row?.tz_offset_seconds ?? 0;
|
const tzOffset = row?.tz_offset_seconds ?? 0;
|
||||||
const localDateStr = row?.local_date || new Date().toISOString().slice(0, 10);
|
const localDateStr = row?.local_date || new Date().toISOString().slice(0, 10);
|
||||||
|
|
||||||
const settlementObs = normObs(row?.settlement_today_obs || row?.metar_context?.settlement_today_obs, tzOffset);
|
const settlementObs = normObs(hourly?.settlementTodayObs || row?.settlement_today_obs || row?.metar_context?.settlement_today_obs, tzOffset);
|
||||||
const metarObs = normObs(row?.metar_today_obs || row?.metar_context?.today_obs || row?.metar_recent_obs || row?.metar_context?.recent_obs, tzOffset);
|
const metarObs = normObs(hourly?.metarTodayObs || row?.metar_today_obs || row?.metar_context?.today_obs || row?.metar_recent_obs || row?.metar_context?.recent_obs, tzOffset);
|
||||||
const runwayHistorySeries = buildRunwayHistorySeries(row, hourly, tzOffset, localDateStr);
|
const runwayHistorySeries = buildRunwayHistorySeries(row, hourly, tzOffset, localDateStr);
|
||||||
|
|
||||||
const slots = generateFullDaySlots(localDateStr);
|
const slots = generateFullDaySlots(localDateStr);
|
||||||
@@ -633,7 +650,7 @@ function buildFullDayChartData(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── Settlement observations ──
|
// ── Settlement observations ──
|
||||||
if (!runwayHistorySeries.length && settlementObs.length) {
|
if (settlementObs.length) {
|
||||||
const svals = binObservationsToSlots(slots, settlementObs);
|
const svals = binObservationsToSlots(slots, settlementObs);
|
||||||
if (svals.some((v) => v !== null)) {
|
if (svals.some((v) => v !== null)) {
|
||||||
series.push({
|
series.push({
|
||||||
@@ -653,7 +670,7 @@ function buildFullDayChartData(
|
|||||||
if (mvals.some((v) => v !== null)) {
|
if (mvals.some((v) => v !== null)) {
|
||||||
series.push({
|
series.push({
|
||||||
key: "metar",
|
key: "metar",
|
||||||
label: "METAR",
|
label: row?.metar_context?.station_label || "METAR",
|
||||||
source: row?.airport || "METAR",
|
source: row?.airport || "METAR",
|
||||||
color: "#0ea5e9",
|
color: "#0ea5e9",
|
||||||
dashed: true,
|
dashed: true,
|
||||||
@@ -888,6 +905,8 @@ export function LiveTemperatureThresholdChart({
|
|||||||
airportPrimary: json.airport_primary || null,
|
airportPrimary: json.airport_primary || null,
|
||||||
forecastDaily: json.forecast?.daily || [],
|
forecastDaily: json.forecast?.daily || [],
|
||||||
multiModelDaily: json.multi_model_daily || {},
|
multiModelDaily: json.multi_model_daily || {},
|
||||||
|
settlementTodayObs: (json as any).timeseries?.settlement_today_obs || (json as any)?.settlement_today_obs || undefined,
|
||||||
|
metarTodayObs: (json as any).timeseries?.metar_today_obs || (json as any)?.metar_today_obs || undefined,
|
||||||
};
|
};
|
||||||
_hourlyCache.set(city, { ts: Date.now(), data });
|
_hourlyCache.set(city, { ts: Date.now(), data });
|
||||||
setHourly(data);
|
setHourly(data);
|
||||||
@@ -905,8 +924,15 @@ export function LiveTemperatureThresholdChart({
|
|||||||
|
|
||||||
const tzOffset = row?.tz_offset_seconds ?? 0;
|
const tzOffset = row?.tz_offset_seconds ?? 0;
|
||||||
const settlementObs = useMemo(() => {
|
const settlementObs = useMemo(() => {
|
||||||
return normObs(row?.settlement_today_obs || row?.metar_context?.settlement_today_obs, tzOffset);
|
let obs = normObs(hourly?.settlementTodayObs || row?.settlement_today_obs || row?.metar_context?.settlement_today_obs, tzOffset);
|
||||||
}, [row, tzOffset]);
|
if (!obs.length && !hourly?.runwayPlateHistory) {
|
||||||
|
const mObs = normObs(hourly?.metarTodayObs || row?.metar_today_obs || row?.metar_context?.today_obs, tzOffset);
|
||||||
|
if (mObs.length > 0) {
|
||||||
|
obs = mObs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obs;
|
||||||
|
}, [row, hourly, tzOffset]);
|
||||||
|
|
||||||
const runwayPlates = useMemo(() => buildRunwayPlates(hourly?.amos, row, settlementObs), [hourly?.amos, row, settlementObs]);
|
const runwayPlates = useMemo(() => buildRunwayPlates(hourly?.amos, row, settlementObs), [hourly?.amos, row, settlementObs]);
|
||||||
const hasRunwayData = runwayPlates.length > 0;
|
const hasRunwayData = runwayPlates.length > 0;
|
||||||
@@ -920,13 +946,7 @@ export function LiveTemperatureThresholdChart({
|
|||||||
if (userToggledKeys[sKey] !== undefined) {
|
if (userToggledKeys[sKey] !== undefined) {
|
||||||
return userToggledKeys[sKey];
|
return userToggledKeys[sKey];
|
||||||
}
|
}
|
||||||
if (sKey.startsWith("model_curve_")) {
|
return isTemperatureSeriesVisibleByDefault(city, sKey);
|
||||||
if (city === "paris" && sKey === "model_curve_AROME HD") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeSeries = useMemo(() => {
|
const activeSeries = useMemo(() => {
|
||||||
@@ -1436,3 +1456,13 @@ export function LiveTemperatureThresholdChart({
|
|||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function __buildTemperatureChartDataForTest(
|
||||||
|
row: ScanOpportunityRow | null,
|
||||||
|
hourly: HourlyForecast,
|
||||||
|
timeframe: "1D" | "3D" = "1D",
|
||||||
|
) {
|
||||||
|
return timeframe === "3D" ? build3DayChartData(row, hourly) : buildFullDayChartData(row, hourly);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const __isTemperatureSeriesVisibleByDefaultForTest = isTemperatureSeriesVisibleByDefault;
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ function SubscriptionGate({ isEn }: { isEn: boolean }) {
|
|||||||
"Real-time METAR observations across 500+ stations",
|
"Real-time METAR observations across 500+ stations",
|
||||||
"DEB forecast blends with 0–240h horizon",
|
"DEB forecast blends with 0–240h horizon",
|
||||||
"AI decision cards with Poly-score ranking",
|
"AI decision cards with Poly-score ranking",
|
||||||
"Historical backtesting & weather market signals",
|
"Historical backtesting & weather signals",
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
"500+ 气象站实时 METAR 实况",
|
"500+ 气象站实时 METAR 实况",
|
||||||
"DEB 智能融合预测(0–240 小时)",
|
"DEB 智能融合预测(0–240 小时)",
|
||||||
"AI 决策卡片 + Poly-score 排名",
|
"AI 决策卡片 + Poly-score 排名",
|
||||||
"历史回测与天气市场交易信号",
|
"历史回测与天气信号",
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,7 +46,7 @@ function SubscriptionGate({ isEn }: { isEn: boolean }) {
|
|||||||
<h1 className="text-xl font-black tracking-tight">
|
<h1 className="text-xl font-black tracking-tight">
|
||||||
{isEn
|
{isEn
|
||||||
? "Unlock the Weather Terminal"
|
? "Unlock the Weather Terminal"
|
||||||
: "解锁天气交易决策台"}
|
: "解锁天气决策台"}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 text-sm text-blue-100">
|
<p className="mt-1 text-sm text-blue-100">
|
||||||
{isEn
|
{isEn
|
||||||
|
|||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
import {
|
||||||
|
__buildTemperatureChartDataForTest,
|
||||||
|
__isTemperatureSeriesVisibleByDefaultForTest,
|
||||||
|
} from "@/components/dashboard/scan-terminal/LiveTemperatureThresholdChart";
|
||||||
|
|
||||||
|
function assert(condition: unknown, message: string) {
|
||||||
|
if (!condition) throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function seriesByKey(series: Array<{ key: string }>, key: string) {
|
||||||
|
return series.find((item) => item.key === key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runTests() {
|
||||||
|
const guangzhou = {
|
||||||
|
city: "guangzhou",
|
||||||
|
local_date: "2026-05-25",
|
||||||
|
local_time: "10:00",
|
||||||
|
tz_offset_seconds: 8 * 60 * 60,
|
||||||
|
airport: "ZGGG",
|
||||||
|
deb_prediction: 31,
|
||||||
|
runway_plate_history: {
|
||||||
|
"02L/20R": [
|
||||||
|
{ time: "00:05", temp: 29.1 },
|
||||||
|
{ time: "00:35", temp: 29.3 },
|
||||||
|
],
|
||||||
|
"01L/19R": [
|
||||||
|
{ time: "00:05", temp: 28.7 },
|
||||||
|
{ time: "00:35", temp: 28.9 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
settlement_today_obs: [
|
||||||
|
{ time: "00:05", temp: 29.0 },
|
||||||
|
{ time: "00:35", temp: 29.2 },
|
||||||
|
],
|
||||||
|
metar_today_obs: [
|
||||||
|
{ time: "00:05", temp: 28.0 },
|
||||||
|
{ time: "00:35", temp: 28.5 },
|
||||||
|
],
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const hourly = {
|
||||||
|
localTime: "10:00",
|
||||||
|
times: ["00:00", "00:30"],
|
||||||
|
temps: [29, 30],
|
||||||
|
modelCurves: {
|
||||||
|
ECMWF: [30.1, 30.2],
|
||||||
|
GFS: [29.7, 29.9],
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const { series } = __buildTemperatureChartDataForTest(guangzhou, hourly, "1D");
|
||||||
|
|
||||||
|
const settlementRunway = seriesByKey(series, "runway_02L_20R") as any;
|
||||||
|
assert(settlementRunway, "settlement runway should use a stable runway-pair key");
|
||||||
|
assert(settlementRunway.label.includes("结算跑道"), "settlement runway should be labeled as settlement runway");
|
||||||
|
assert(settlementRunway.color === "#009688", "settlement runway should use the highlight cyan color");
|
||||||
|
assert(settlementRunway.featured === true, "settlement runway should be featured");
|
||||||
|
assert(!settlementRunway.dashed, "settlement runway should be solid");
|
||||||
|
|
||||||
|
const auxiliaryRunway = seriesByKey(series, "runway_01L_19R") as any;
|
||||||
|
assert(auxiliaryRunway, "auxiliary runway should be displayed by default in the chart data");
|
||||||
|
assert(auxiliaryRunway.dashed === true, "auxiliary runway should be dashed");
|
||||||
|
assert(auxiliaryRunway.featured !== true, "auxiliary runway should not be featured");
|
||||||
|
|
||||||
|
assert(seriesByKey(series, "settlement"), "settlement/HKO observation series should still be present when runway data exists");
|
||||||
|
assert(seriesByKey(series, "metar"), "METAR observation series should be present by its own key");
|
||||||
|
|
||||||
|
assert(
|
||||||
|
__isTemperatureSeriesVisibleByDefaultForTest("guangzhou", "runway_02L_20R"),
|
||||||
|
"runway series should be visible by default",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
__isTemperatureSeriesVisibleByDefaultForTest("guangzhou", "settlement"),
|
||||||
|
"settlement/HKO observations should be visible by default",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
__isTemperatureSeriesVisibleByDefaultForTest("guangzhou", "metar"),
|
||||||
|
"METAR observations should be visible by default",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
!__isTemperatureSeriesVisibleByDefaultForTest("guangzhou", "model_curve_ECMWF"),
|
||||||
|
"multi-model curves should be hidden by default",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
__isTemperatureSeriesVisibleByDefaultForTest("paris", "model_curve_AROME HD"),
|
||||||
|
"Paris AROME HD should be the only default-visible model curve exception",
|
||||||
|
);
|
||||||
|
|
||||||
|
const shenzhen = __buildTemperatureChartDataForTest(
|
||||||
|
{
|
||||||
|
city: "shenzhen",
|
||||||
|
local_date: "2026-05-25",
|
||||||
|
local_time: "10:00",
|
||||||
|
tz_offset_seconds: 8 * 60 * 60,
|
||||||
|
metar_context: {
|
||||||
|
station: "Lau Fau Shan",
|
||||||
|
station_label: "HKO Lau Fau Shan",
|
||||||
|
today_obs: [
|
||||||
|
{ time: "00:05", temp: 28.4 },
|
||||||
|
{ time: "00:35", temp: 28.5 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as any,
|
||||||
|
null,
|
||||||
|
"1D",
|
||||||
|
);
|
||||||
|
assert(seriesByKey(shenzhen.series, "metar"), "Shenzhen/Lau Fau Shan observations should stay as METAR/HKO observations, not runway data");
|
||||||
|
assert(!shenzhen.series.some((item) => item.key.startsWith("runway_")), "Shenzhen should not be treated as an AMSC runway city");
|
||||||
|
}
|
||||||
@@ -466,8 +466,8 @@ export function buildMarketDecisionView({
|
|||||||
? "Quote is available, but model probability or YES price is incomplete."
|
? "Quote is available, but model probability or YES price is incomplete."
|
||||||
: "已获取报价,但模型概率或 YES 价格不完整。"
|
: "已获取报价,但模型概率或 YES 价格不完整。"
|
||||||
: isEn
|
: isEn
|
||||||
? `Model probability is ${formatMarketPercent(modelProbability)} versus market-implied ${formatMarketPercent(implied)}.${bucketMappingExplanation ? ` ${bucketMappingExplanation}` : ""}`
|
? `Model probability is ${formatMarketPercent(modelProbability)} versus signal-implied ${formatMarketPercent(implied)}.${bucketMappingExplanation ? ` ${bucketMappingExplanation}` : ""}`
|
||||||
: `模型概率 ${formatMarketPercent(modelProbability)},市场隐含约 ${formatMarketPercent(implied)}。${bucketMappingExplanation ? ` ${bucketMappingExplanation}` : ""}`,
|
: `模型概率 ${formatMarketPercent(modelProbability)},信号隐含约 ${formatMarketPercent(implied)}。${bucketMappingExplanation ? ` ${bucketMappingExplanation}` : ""}`,
|
||||||
status: "ready",
|
status: "ready",
|
||||||
title,
|
title,
|
||||||
tone,
|
tone,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const RAW_MARKET_ROWS_ZH = [
|
|||||||
const COVERAGE_EN = [
|
const COVERAGE_EN = [
|
||||||
"Live airport observations",
|
"Live airport observations",
|
||||||
"DEB blend forecast",
|
"DEB blend forecast",
|
||||||
"Market-implied temperature",
|
"Signal-implied temperature",
|
||||||
"Intraday settlement windows",
|
"Intraday settlement windows",
|
||||||
"AI weather evidence",
|
"AI weather evidence",
|
||||||
"Paid Telegram alerts",
|
"Paid Telegram alerts",
|
||||||
@@ -50,7 +50,7 @@ const COVERAGE_EN = [
|
|||||||
const COVERAGE_ZH = [
|
const COVERAGE_ZH = [
|
||||||
"机场实况观测数据",
|
"机场实况观测数据",
|
||||||
"DEB 智能融合预报",
|
"DEB 智能融合预报",
|
||||||
"市场隐含温度定价",
|
"信号隐含温度定价",
|
||||||
"日内分段结算窗口",
|
"日内分段结算窗口",
|
||||||
"AI 气象证据链解读",
|
"AI 气象证据链解读",
|
||||||
"付费电报实时通知",
|
"付费电报实时通知",
|
||||||
@@ -60,7 +60,7 @@ const PRO_FEATURES_EN = [
|
|||||||
"Real-time METAR observations & runway sensor data",
|
"Real-time METAR observations & runway sensor data",
|
||||||
"Real-time METAR observations & alerts",
|
"Real-time METAR observations & alerts",
|
||||||
"DEB blend forecast model",
|
"DEB blend forecast model",
|
||||||
"Market-implied temperature pricing",
|
"Signal-implied temperature pricing",
|
||||||
"Intraday settlement windows & risk metrics",
|
"Intraday settlement windows & risk metrics",
|
||||||
"Paid Telegram alerts & Webhook API",
|
"Paid Telegram alerts & Webhook API",
|
||||||
"24/7 priority professional support",
|
"24/7 priority professional support",
|
||||||
@@ -70,7 +70,7 @@ const PRO_FEATURES_ZH = [
|
|||||||
"实时 METAR 机场实测与跑道传感器数据",
|
"实时 METAR 机场实测与跑道传感器数据",
|
||||||
"实时 METAR 机场实测与预警",
|
"实时 METAR 机场实测与预警",
|
||||||
"DEB 智能融合预测模型",
|
"DEB 智能融合预测模型",
|
||||||
"市场隐含温度定价与估值",
|
"信号隐含温度定价与估值",
|
||||||
"日内结算窗口与风险度量指标",
|
"日内结算窗口与风险度量指标",
|
||||||
"付费电报群通知与 API 接口推送",
|
"付费电报群通知与 API 接口推送",
|
||||||
"7×24小时专业技术与客服支持",
|
"7×24小时专业技术与客服支持",
|
||||||
@@ -106,7 +106,7 @@ function InstitutionalLandingScreen() {
|
|||||||
{
|
{
|
||||||
icon: Gauge,
|
icon: Gauge,
|
||||||
title: "Decision Workflow",
|
title: "Decision Workflow",
|
||||||
body: "City cards combine model forecast, current deviation, risk, and target contract context.",
|
body: "City cards combine model forecast, current deviation, risk, and target threshold context.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
@@ -123,18 +123,18 @@ function InstitutionalLandingScreen() {
|
|||||||
{
|
{
|
||||||
icon: Gauge,
|
icon: Gauge,
|
||||||
title: "决策工作流",
|
title: "决策工作流",
|
||||||
body: "城市决策卡片融合了气象预报、实况偏差、历史风险系数及目标合约盘口。",
|
body: "城市决策卡片融合了气象预报、实况偏差、历史风险系数及目标阈值盘口。",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
title: "付费准入",
|
title: "付费准入",
|
||||||
body: "除公开介绍和账户管理外,气象交易决策台仅向付费活跃订阅用户开放。",
|
body: "除公开介绍和账户管理外,气象决策台仅向付费活跃订阅用户开放。",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const modelLabels = isEn
|
const modelLabels = isEn
|
||||||
? ["DEB Blend", "Live METAR", "Market Implied"]
|
? ["DEB Blend", "Live METAR", "Signal Implied"]
|
||||||
: ["DEB 融合预测", "METAR 机场实测", "市场隐含价格"];
|
: ["DEB 融合预测", "METAR 机场实测", "信号隐含价格"];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#f4f7fb] text-slate-950">
|
<div className="min-h-screen bg-[#f4f7fb] text-slate-950">
|
||||||
@@ -220,12 +220,12 @@ function InstitutionalLandingScreen() {
|
|||||||
{isEn ? "Paid professional terminal" : "付费专业交易终端"}
|
{isEn ? "Paid professional terminal" : "付费专业交易终端"}
|
||||||
</div>
|
</div>
|
||||||
<h1 className="max-w-2xl text-4xl font-black leading-[1.05] tracking-normal text-slate-950 sm:text-5xl lg:text-6xl">
|
<h1 className="max-w-2xl text-4xl font-black leading-[1.05] tracking-normal text-slate-950 sm:text-5xl lg:text-6xl">
|
||||||
{isEn ? "Institutional weather market intelligence for paid users." : "面向付费用户的机构级天气交易决策台"}
|
{isEn ? "Institutional weather signal intelligence for paid users." : "面向付费用户的机构级天气决策台"}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-5 max-w-xl text-base leading-8 text-slate-600 sm:text-lg">
|
<p className="mt-5 max-w-xl text-base leading-8 text-slate-600 sm:text-lg">
|
||||||
{isEn
|
{isEn
|
||||||
? "PolyWeather turns live METAR observations, DEB forecast blends, model probabilities, and market settlement logic into one professional decision workspace."
|
? "PolyWeather turns live METAR observations, DEB forecast blends, model probabilities, and signal settlement logic into one professional decision workspace."
|
||||||
: "PolyWeather 将 METAR 机场实测、DEB 智能融合预报、模型概率及市场结算逻辑整合于一体,打造气象风险管理专业决策环境。"}
|
: "PolyWeather 将 METAR 机场实测、DEB 智能融合预报、模型概率及信号结算逻辑整合于一体,打造气象风险管理专业决策环境。"}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
||||||
<Link
|
<Link
|
||||||
@@ -253,7 +253,7 @@ function InstitutionalLandingScreen() {
|
|||||||
<div className="flex items-center justify-between border-b border-slate-200 px-4 py-3">
|
<div className="flex items-center justify-between border-b border-slate-200 px-4 py-3">
|
||||||
<div className="flex items-center gap-2 text-sm font-bold">
|
<div className="flex items-center gap-2 text-sm font-bold">
|
||||||
<BarChart3 size={16} className="text-blue-700" />
|
<BarChart3 size={16} className="text-blue-700" />
|
||||||
{isEn ? "Weather Markets Dashboard" : "天气市场交易面板"}
|
{isEn ? "Weather Signals Dashboard" : "天气信号交易面板"}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-xs font-semibold text-slate-500">
|
<div className="flex items-center gap-2 text-xs font-semibold text-slate-500">
|
||||||
<span className="h-2 w-2 rounded-full bg-emerald-500" />
|
<span className="h-2 w-2 rounded-full bg-emerald-500" />
|
||||||
@@ -263,7 +263,7 @@ function InstitutionalLandingScreen() {
|
|||||||
<div className="grid gap-3 p-3 lg:grid-cols-[1fr_0.85fr]">
|
<div className="grid gap-3 p-3 lg:grid-cols-[1fr_0.85fr]">
|
||||||
<div className="rounded-xl border border-slate-200">
|
<div className="rounded-xl border border-slate-200">
|
||||||
<div className="flex items-center justify-between border-b border-slate-200 bg-slate-50 px-3 py-2">
|
<div className="flex items-center justify-between border-b border-slate-200 bg-slate-50 px-3 py-2">
|
||||||
<strong className="text-sm">{isEn ? "Temperature Contracts" : "温度天气合约"}</strong>
|
<strong className="text-sm">{isEn ? "Temperature Thresholds" : "温度天气阈值"}</strong>
|
||||||
<span className="text-xs font-semibold text-slate-500">
|
<span className="text-xs font-semibold text-slate-500">
|
||||||
{isEn ? "Price / Edge / Signal" : "价格 / 偏差 / 信号"}
|
{isEn ? "Price / Edge / Signal" : "价格 / 偏差 / 信号"}
|
||||||
</span>
|
</span>
|
||||||
@@ -325,8 +325,8 @@ function InstitutionalLandingScreen() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-sm leading-6 text-emerald-900">
|
<p className="text-sm leading-6 text-emerald-900">
|
||||||
{isEn
|
{isEn
|
||||||
? "New York high-temperature market shows a positive observation deviation with confirmed airport evidence."
|
? "New York high-temperature signal shows a positive observation deviation with confirmed airport evidence."
|
||||||
: "纽约高温合约市场出现显著的正向观测偏差,机场天气实况已验证确认。"}
|
: "纽约高温阈值信号出现显著的正向观测偏差,机场天气实况已验证确认。"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -356,7 +356,7 @@ function InstitutionalLandingScreen() {
|
|||||||
{isEn ? "Data Coverage" : "数据覆盖范围"}
|
{isEn ? "Data Coverage" : "数据覆盖范围"}
|
||||||
</p>
|
</p>
|
||||||
<h2 className="mt-2 text-3xl font-black">
|
<h2 className="mt-2 text-3xl font-black">
|
||||||
{isEn ? "Everything weather-market users need in one place." : "天气市场交易者所需的一切,在此集结。"}
|
{isEn ? "Everything weather-signal users need in one place." : "天气信号交易者所需的一切,在此集结。"}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<p className="max-w-xl text-sm leading-6 text-slate-600">
|
<p className="max-w-xl text-sm leading-6 text-slate-600">
|
||||||
@@ -404,8 +404,8 @@ function InstitutionalLandingScreen() {
|
|||||||
</h3>
|
</h3>
|
||||||
<p className="mt-2 text-sm text-slate-500 leading-relaxed">
|
<p className="mt-2 text-sm text-slate-500 leading-relaxed">
|
||||||
{isEn
|
{isEn
|
||||||
? "Full access to the institutional weather-market terminal. Live METAR, DEB forecasts, probability distribution, AI decision cards, and real-time alerts."
|
? "Full access to the institutional weather-signal terminal. Live METAR, DEB forecasts, probability distribution, AI decision cards, and real-time alerts."
|
||||||
: "完整访问机构级天气市场终端。实时 METAR、DEB 预报、概率分布、AI 决策卡片、实时通知。"}
|
: "完整访问机构级天气信号终端。实时 METAR、DEB 预报、概率分布、AI 决策卡片、实时通知。"}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-6 flex items-baseline">
|
<div className="mt-6 flex items-baseline">
|
||||||
<span className="text-5xl font-black tracking-tight text-slate-900">
|
<span className="text-5xl font-black tracking-tight text-slate-900">
|
||||||
|
|||||||
@@ -202,11 +202,11 @@ export const DOCS_PAGES: DocsPage[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "market-layer",
|
id: "market-layer",
|
||||||
title: "市场价格层怎么读",
|
title: "信号价格层怎么读",
|
||||||
blocks: [
|
blocks: [
|
||||||
{ type: "paragraph", text: "温度桶标签来自完整市场桶列表,并会按 label / slug / question 识别 exact、or higher、or lower、range 等方向,避免把 30.5°C 错配到不合理的 16°C 或反向尾部桶。" },
|
{ type: "paragraph", text: "温度桶标签来自完整信号桶列表,并会按 label / slug / question 识别 exact、or higher、or lower、range 等方向,避免把 30.5°C 错配到不合理的 16°C 或反向尾部桶。" },
|
||||||
{ type: "callout", tone: "info", title: "模型-市场差", text: "模型-市场差 = 模型概率 − 市场隐含概率。正数表示天气概率高于市场报价;负数表示市场已经把该 YES 计价得更充分。它不是温度变化,也不是收益率。" },
|
{ type: "callout", tone: "info", title: "模型-信号差", text: "模型-信号差 = 模型概率 − 信号隐含概率。正数表示天气概率高于信号报价;负数表示信号已经把该 YES 计价得更充分。它不是温度变化,也不是收益率。" },
|
||||||
{ type: "paragraph", text: "YES 买入价以可执行报价为主;没有可靠模型概率或 YES 价格时,决策卡会显示报价已匹配但暂不计算模型-市场差。" },
|
{ type: "paragraph", text: "YES 买入价以可执行报价为主;没有可靠模型概率或 YES 价格时,决策卡会显示报价已匹配但暂不计算模型-信号差。" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -240,11 +240,11 @@ export const DOCS_PAGES: DocsPage[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "market-layer",
|
id: "market-layer",
|
||||||
title: "How to read the market layer",
|
title: "How to read the signal layer",
|
||||||
blocks: [
|
blocks: [
|
||||||
{ type: "paragraph", text: "Bucket labels come from the full market bucket list. The card reads label / slug / question text to distinguish exact, or-higher, or-lower, and range buckets, so a 30.5°C weather center is not matched to an unreasonable 16°C or reverse-tail bucket." },
|
{ type: "paragraph", text: "Bucket labels come from the full signal bucket list. The card reads label / slug / question text to distinguish exact, or-higher, or-lower, and range buckets, so a 30.5°C weather center is not matched to an unreasonable 16°C or reverse-tail bucket." },
|
||||||
{ type: "callout", tone: "info", title: "Model-market difference", text: "Model-market difference = model probability minus market-implied probability. A positive value means the weather probability is above market pricing; a negative value means the YES is already priced more fully by the market. It is not a temperature delta or return." },
|
{ type: "callout", tone: "info", title: "Model-signal difference", text: "Model-signal difference = model probability minus signal-implied probability. A positive value means the weather probability is above signal pricing; a negative value means the YES is already priced more fully by the signal. It is not a temperature delta or return." },
|
||||||
{ type: "paragraph", text: "YES buy uses executable quote data when available. If either model probability or YES price is incomplete, the card shows the quote match but withholds the model-market difference." },
|
{ type: "paragraph", text: "YES buy uses executable quote data when available. If either model probability or YES price is incomplete, the card shows the quote match but withholds the model-signal difference." },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user