Reorganize intraday signal cards for clearer trade guidance
This commit is contained in:
@@ -2056,6 +2056,27 @@
|
|||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-stack) {
|
||||||
|
margin-top: 14px;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-subpanel) {
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.025);
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-subpanel-title) {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.future-v2-market-v3) {
|
.root :global(.future-v2-market-v3) {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2308,6 +2329,13 @@
|
|||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.future-subsection-title) {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.future-trend-value.warm) {
|
.root :global(.future-trend-value.warm) {
|
||||||
color: #34d399;
|
color: #34d399;
|
||||||
}
|
}
|
||||||
@@ -2444,6 +2472,10 @@
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.future-v2-subpanel) {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.future-trend-value) {
|
.root :global(.future-trend-value) {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
@@ -2454,6 +2486,11 @@
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.future-subsection-title) {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.future-front-meta) {
|
.root :global(.future-front-meta) {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -828,6 +828,76 @@ export function FutureForecastModal() {
|
|||||||
<strong>{marketSignal}</strong>
|
<strong>{marketSignal}</strong>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="future-v2-card">
|
||||||
|
<h4 className="future-v2-card-title">
|
||||||
|
{locale === "en-US"
|
||||||
|
? "City Risk Profile & Airport Narrative"
|
||||||
|
: "城市风险档案与机场报文解读"}
|
||||||
|
</h4>
|
||||||
|
<div className="future-v2-stack">
|
||||||
|
<div className="future-v2-subpanel">
|
||||||
|
<h5 className="future-v2-subpanel-title">
|
||||||
|
{locale === "en-US" ? "City Risk Profile" : "城市风险档案"}
|
||||||
|
</h5>
|
||||||
|
<div className="risk-info" style={{ marginTop: "10px" }}>
|
||||||
|
{!risk.airport ? (
|
||||||
|
<span style={{ color: "var(--text-muted)" }}>
|
||||||
|
{t("section.noRiskProfile")}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="risk-row">
|
||||||
|
<span className="risk-label">
|
||||||
|
{settlementProfileLabel}
|
||||||
|
</span>
|
||||||
|
<span>{settlementProfileValue}</span>
|
||||||
|
</div>
|
||||||
|
<div className="risk-row">
|
||||||
|
<span className="risk-label">
|
||||||
|
{t("section.distance")}
|
||||||
|
</span>
|
||||||
|
<span>{risk.distance_km ?? "--"}km</span>
|
||||||
|
</div>
|
||||||
|
{risk.warning ? (
|
||||||
|
<div className="risk-row">
|
||||||
|
<span className="risk-label">
|
||||||
|
{t("section.note")}
|
||||||
|
</span>
|
||||||
|
<span>{risk.warning}</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="future-v2-subpanel">
|
||||||
|
<h5 className="future-v2-subpanel-title">
|
||||||
|
{locale === "en-US" ? "Airport Narrative" : "机场报文解读"}
|
||||||
|
</h5>
|
||||||
|
<div className="ai-box" style={{ marginTop: "10px" }}>
|
||||||
|
{!ai.summary && ai.bullets.length === 0 ? (
|
||||||
|
<span className="ai-placeholder">
|
||||||
|
{t("future.noAi")}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{ai.summary ? (
|
||||||
|
<div className="ai-summary">{ai.summary}</div>
|
||||||
|
) : null}
|
||||||
|
{ai.bullets.length > 0 ? (
|
||||||
|
<ul className="ai-list">
|
||||||
|
{ai.bullets.map((item) => (
|
||||||
|
<li key={item}>{item}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main className="future-v2-right">
|
<main className="future-v2-right">
|
||||||
@@ -915,6 +985,9 @@ export function FutureForecastModal() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="future-subsection-title">
|
||||||
|
{locale === "en-US" ? "Surface Structure" : "近地面信号"}
|
||||||
|
</div>
|
||||||
<div className="future-trend-grid">
|
<div className="future-trend-grid">
|
||||||
{view.front.metrics.slice(0, 6).map((metric) => (
|
{view.front.metrics.slice(0, 6).map((metric) => (
|
||||||
<div key={metric.label} className="future-trend-card">
|
<div key={metric.label} className="future-trend-card">
|
||||||
@@ -935,14 +1008,7 @@ export function FutureForecastModal() {
|
|||||||
{view.front.upperAirSummary ||
|
{view.front.upperAirSummary ||
|
||||||
(view.front.upperAirMetrics?.length || 0) > 0 ? (
|
(view.front.upperAirMetrics?.length || 0) > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div
|
<div className="future-subsection-title">
|
||||||
style={{
|
|
||||||
color: "var(--text-primary)",
|
|
||||||
fontSize: "0.95rem",
|
|
||||||
fontWeight: 700,
|
|
||||||
marginTop: "18px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{locale === "en-US" ? "Upper-Air Structure" : "高空结构信号"}
|
{locale === "en-US" ? "Upper-Air Structure" : "高空结构信号"}
|
||||||
</div>
|
</div>
|
||||||
{view.front.upperAirSummary ? (
|
{view.front.upperAirSummary ? (
|
||||||
@@ -970,74 +1036,6 @@ export function FutureForecastModal() {
|
|||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="future-modal-section">
|
|
||||||
<h3>
|
|
||||||
{locale === "en-US"
|
|
||||||
? "City Risk Profile & Airport Narrative"
|
|
||||||
: "城市风险档案与机场报文解读"}
|
|
||||||
</h3>
|
|
||||||
<div className="future-modal-grid">
|
|
||||||
<div>
|
|
||||||
<h4 className="future-v2-card-title">
|
|
||||||
{locale === "en-US" ? "City Risk Profile" : "城市风险档案"}
|
|
||||||
</h4>
|
|
||||||
<div className="risk-info" style={{ marginTop: "10px" }}>
|
|
||||||
{!risk.airport ? (
|
|
||||||
<span style={{ color: "var(--text-muted)" }}>
|
|
||||||
{t("section.noRiskProfile")}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="risk-row">
|
|
||||||
<span className="risk-label">
|
|
||||||
{settlementProfileLabel}
|
|
||||||
</span>
|
|
||||||
<span>{settlementProfileValue}</span>
|
|
||||||
</div>
|
|
||||||
<div className="risk-row">
|
|
||||||
<span className="risk-label">
|
|
||||||
{t("section.distance")}
|
|
||||||
</span>
|
|
||||||
<span>{risk.distance_km ?? "--"}km</span>
|
|
||||||
</div>
|
|
||||||
{risk.warning ? (
|
|
||||||
<div className="risk-row">
|
|
||||||
<span className="risk-label">
|
|
||||||
{t("section.note")}
|
|
||||||
</span>
|
|
||||||
<span>{risk.warning}</span>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="future-v2-card-title">{t("future.ai")}</h4>
|
|
||||||
<div className="ai-box" style={{ marginTop: "10px" }}>
|
|
||||||
{!ai.summary && ai.bullets.length === 0 ? (
|
|
||||||
<span className="ai-placeholder">
|
|
||||||
{t("future.noAi")}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{ai.summary ? (
|
|
||||||
<div className="ai-summary">{ai.summary}</div>
|
|
||||||
) : null}
|
|
||||||
{ai.bullets.length > 0 ? (
|
|
||||||
<ul className="ai-list">
|
|
||||||
{ai.bullets.map((item) => (
|
|
||||||
<li key={item}>{item}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -317,6 +317,8 @@ export interface CityDetail {
|
|||||||
trigger_risk?: string | null;
|
trigger_risk?: string | null;
|
||||||
mixing_strength?: string | null;
|
mixing_strength?: string | null;
|
||||||
shear_risk?: string | null;
|
shear_risk?: string | null;
|
||||||
|
heating_setup?: string | null;
|
||||||
|
heating_score?: number | null;
|
||||||
summary_zh?: string | null;
|
summary_zh?: string | null;
|
||||||
summary_en?: string | null;
|
summary_en?: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -580,17 +580,77 @@ export function computeFrontTrendSignal(
|
|||||||
locale: Locale = "zh-CN",
|
locale: Locale = "zh-CN",
|
||||||
) {
|
) {
|
||||||
const upperAirSignal = detail.vertical_profile_signal || {};
|
const upperAirSignal = detail.vertical_profile_signal || {};
|
||||||
|
const upperAirSummary = upperAirSignal.source
|
||||||
|
? (() => {
|
||||||
|
const hasMetrics =
|
||||||
|
upperAirSignal.cape_max != null ||
|
||||||
|
upperAirSignal.cin_min != null ||
|
||||||
|
upperAirSignal.boundary_layer_height_max != null ||
|
||||||
|
upperAirSignal.shear_10m_180m_max != null;
|
||||||
|
if (!hasMetrics) {
|
||||||
|
return isEnglish(locale)
|
||||||
|
? "Upper-air inputs are incomplete. For now, trade direction should rely more on surface structure."
|
||||||
|
: "高空输入还不完整,当前交易方向先更多参考近地面结构信号。";
|
||||||
|
}
|
||||||
|
if (upperAirSignal.heating_setup === "supportive") {
|
||||||
|
return isEnglish(locale)
|
||||||
|
? "Upper-air structure still favors further warming. Leaning too early against higher buckets is risky."
|
||||||
|
: "高空结构仍偏向继续增温,过早去押更低温区间风险较高。";
|
||||||
|
}
|
||||||
|
if (upperAirSignal.heating_setup === "suppressed") {
|
||||||
|
return isEnglish(locale)
|
||||||
|
? "Upper-air structure leans toward capping the afternoon high. Chasing higher buckets needs caution."
|
||||||
|
: "高空结构更偏向压住午后峰值,追更高温区间要更谨慎。";
|
||||||
|
}
|
||||||
|
return isEnglish(locale)
|
||||||
|
? "Upper-air structure is fairly neutral. It does not give a clean edge by itself, so surface trends still decide the trade."
|
||||||
|
: "高空结构整体偏中性,单看这层不给明确边,交易仍要让近地面走势来定。";
|
||||||
|
})()
|
||||||
|
: "";
|
||||||
const upperAirMetrics = upperAirSignal.source
|
const upperAirMetrics = upperAirSignal.source
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: isEnglish(locale) ? "Convective suppression" : "对流压温风险",
|
label: isEnglish(locale) ? "Peak setup" : "冲高环境",
|
||||||
|
note:
|
||||||
|
upperAirSignal.heating_setup === "supportive"
|
||||||
|
? isEnglish(locale)
|
||||||
|
? "Still supportive of more daytime heating. Fading stronger buckets too early is risky."
|
||||||
|
: "仍偏向白天继续冲高,过早去反着做更高温区间比较危险。"
|
||||||
|
: upperAirSignal.heating_setup === "suppressed"
|
||||||
|
? isEnglish(locale)
|
||||||
|
? "Leans toward capping the afternoon peak. Be careful chasing stronger buckets."
|
||||||
|
: "更偏向压住午后峰值,追更高温区间要谨慎。"
|
||||||
|
: isEnglish(locale)
|
||||||
|
? "Neutral on its own. Surface structure still decides the side."
|
||||||
|
: "单看这层偏中性,最终还是要看近地面信号站哪边。",
|
||||||
|
tone:
|
||||||
|
upperAirSignal.heating_setup === "supportive"
|
||||||
|
? "warm"
|
||||||
|
: upperAirSignal.heating_setup === "suppressed"
|
||||||
|
? "cold"
|
||||||
|
: "",
|
||||||
|
value:
|
||||||
|
upperAirSignal.heating_setup === "supportive"
|
||||||
|
? isEnglish(locale)
|
||||||
|
? "Supportive"
|
||||||
|
: "偏支持"
|
||||||
|
: upperAirSignal.heating_setup === "suppressed"
|
||||||
|
? isEnglish(locale)
|
||||||
|
? "Suppressed"
|
||||||
|
: "偏压制"
|
||||||
|
: isEnglish(locale)
|
||||||
|
? "Neutral"
|
||||||
|
: "中性",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: isEnglish(locale) ? "Peak suppression risk" : "压温风险",
|
||||||
note:
|
note:
|
||||||
upperAirSignal.cape_max != null || upperAirSignal.cin_min != null
|
upperAirSignal.cape_max != null || upperAirSignal.cin_min != null
|
||||||
? isEnglish(locale)
|
? isEnglish(locale)
|
||||||
? `CAPE max ${Math.round(Number(upperAirSignal.cape_max ?? 0))}, CIN min ${Number(upperAirSignal.cin_min ?? 0).toFixed(0)}.`
|
? `How likely clouds or showers are to cap the high. CAPE ${Math.round(Number(upperAirSignal.cape_max ?? 0))}, CIN ${Number(upperAirSignal.cin_min ?? 0).toFixed(0)}.`
|
||||||
: `CAPE 峰值 ${Math.round(Number(upperAirSignal.cape_max ?? 0))},CIN 最低 ${Number(upperAirSignal.cin_min ?? 0).toFixed(0)}。`
|
: `看云和阵雨有多大概率把峰值压住。CAPE ${Math.round(Number(upperAirSignal.cape_max ?? 0))},CIN ${Number(upperAirSignal.cin_min ?? 0).toFixed(0)}。`
|
||||||
: isEnglish(locale)
|
: isEnglish(locale)
|
||||||
? "Derived from the next 48h upper-air profile."
|
? "Estimated from the next 48h upper-air profile."
|
||||||
: "根据未来 48 小时高空剖面估算。",
|
: "根据未来 48 小时高空剖面估算。",
|
||||||
tone:
|
tone:
|
||||||
upperAirSignal.suppression_risk === "high"
|
upperAirSignal.suppression_risk === "high"
|
||||||
@@ -612,12 +672,12 @@ export function computeFrontTrendSignal(
|
|||||||
: "低",
|
: "低",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: isEnglish(locale) ? "Trigger setup" : "午后触发性",
|
label: isEnglish(locale) ? "Afternoon disruption" : "午后扰动",
|
||||||
note:
|
note:
|
||||||
upperAirSignal.lifted_index_min != null
|
upperAirSignal.lifted_index_min != null
|
||||||
? isEnglish(locale)
|
? isEnglish(locale)
|
||||||
? `Lifted index min ${Number(upperAirSignal.lifted_index_min).toFixed(1)}.`
|
? `How easily the afternoon can turn noisy. Lifted Index ${Number(upperAirSignal.lifted_index_min).toFixed(1)}.`
|
||||||
: `Lifted Index 最低 ${Number(upperAirSignal.lifted_index_min).toFixed(1)}。`
|
: `看午后是否容易突然起云、起对流,把走势搅乱。Lifted Index ${Number(upperAirSignal.lifted_index_min).toFixed(1)}。`
|
||||||
: isEnglish(locale)
|
: isEnglish(locale)
|
||||||
? "Uses instability and lifted-index structure."
|
? "Uses instability and lifted-index structure."
|
||||||
: "结合不稳定能量与抬升指数判断。",
|
: "结合不稳定能量与抬升指数判断。",
|
||||||
@@ -641,15 +701,15 @@ export function computeFrontTrendSignal(
|
|||||||
: "低",
|
: "低",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: isEnglish(locale) ? "Deep mixing" : "深层混合",
|
label: isEnglish(locale) ? "Heating efficiency" : "冲高效率",
|
||||||
note:
|
note:
|
||||||
upperAirSignal.boundary_layer_height_max != null
|
upperAirSignal.boundary_layer_height_max != null
|
||||||
? isEnglish(locale)
|
? isEnglish(locale)
|
||||||
? `Boundary-layer height peaks near ${Math.round(Number(upperAirSignal.boundary_layer_height_max))} m.`
|
? `How efficiently surface warmth can keep translating upward. Mixing depth peaks near ${Math.round(Number(upperAirSignal.boundary_layer_height_max))} m.`
|
||||||
: `边界层高度峰值约 ${Math.round(Number(upperAirSignal.boundary_layer_height_max))} 米。`
|
: `看地面热量能不能持续往上送,决定冲高效率。混合层高度峰值约 ${Math.round(Number(upperAirSignal.boundary_layer_height_max))} 米。`
|
||||||
: isEnglish(locale)
|
: isEnglish(locale)
|
||||||
? "Tracks daytime boundary-layer depth."
|
? "Tracks daytime mixing depth."
|
||||||
: "跟踪白天边界层混合深度。",
|
: "跟踪白天混合层深度。",
|
||||||
tone:
|
tone:
|
||||||
upperAirSignal.mixing_strength === "strong"
|
upperAirSignal.mixing_strength === "strong"
|
||||||
? "warm"
|
? "warm"
|
||||||
@@ -666,32 +726,8 @@ export function computeFrontTrendSignal(
|
|||||||
? "Medium"
|
? "Medium"
|
||||||
: "中"
|
: "中"
|
||||||
: isEnglish(locale)
|
: isEnglish(locale)
|
||||||
? "Weak"
|
? "Weak"
|
||||||
: "弱",
|
: "弱",
|
||||||
},
|
|
||||||
{
|
|
||||||
label: isEnglish(locale) ? "Shear proxy" : "高空风切变",
|
|
||||||
note:
|
|
||||||
upperAirSignal.shear_10m_180m_max != null
|
|
||||||
? isEnglish(locale)
|
|
||||||
? `10m-180m shear proxy peaks near ${Number(upperAirSignal.shear_10m_180m_max).toFixed(1)}.`
|
|
||||||
: `10m-180m 风切变代理峰值约 ${Number(upperAirSignal.shear_10m_180m_max).toFixed(1)}。`
|
|
||||||
: isEnglish(locale)
|
|
||||||
? "Uses 10m vs 180m wind-vector spread as a simple proxy."
|
|
||||||
: "使用 10m 与 180m 风矢量差做简化代理。",
|
|
||||||
tone: upperAirSignal.shear_risk === "high" ? "cold" : "",
|
|
||||||
value:
|
|
||||||
upperAirSignal.shear_risk === "high"
|
|
||||||
? isEnglish(locale)
|
|
||||||
? "High"
|
|
||||||
: "高"
|
|
||||||
: upperAirSignal.shear_risk === "medium"
|
|
||||||
? isEnglish(locale)
|
|
||||||
? "Medium"
|
|
||||||
: "中"
|
|
||||||
: isEnglish(locale)
|
|
||||||
? "Low"
|
|
||||||
: "低",
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
@@ -719,9 +755,7 @@ export function computeFrontTrendSignal(
|
|||||||
value: string;
|
value: string;
|
||||||
}>,
|
}>,
|
||||||
upperAirMetrics,
|
upperAirMetrics,
|
||||||
upperAirSummary: isEnglish(locale)
|
upperAirSummary,
|
||||||
? String(upperAirSignal.summary_en || "").trim()
|
|
||||||
: String(upperAirSignal.summary_zh || "").trim(),
|
|
||||||
precipMax: 0,
|
precipMax: 0,
|
||||||
score: 0,
|
score: 0,
|
||||||
summary:
|
summary:
|
||||||
@@ -1204,9 +1238,7 @@ export function computeFrontTrendSignal(
|
|||||||
label,
|
label,
|
||||||
metrics,
|
metrics,
|
||||||
upperAirMetrics,
|
upperAirMetrics,
|
||||||
upperAirSummary: isEnglish(locale)
|
upperAirSummary,
|
||||||
? String(upperAirSignal.summary_en || "").trim()
|
|
||||||
: String(upperAirSignal.summary_zh || "").trim(),
|
|
||||||
precipMax,
|
precipMax,
|
||||||
score,
|
score,
|
||||||
summary: backendSummary || summary,
|
summary: backendSummary || summary,
|
||||||
|
|||||||
+117
-13
@@ -77,6 +77,16 @@ def _build_vertical_profile_signal(
|
|||||||
valid = [_sf(value) for value in values if _sf(value) is not None]
|
valid = [_sf(value) for value in values if _sf(value) is not None]
|
||||||
return min(valid) if valid else None
|
return min(valid) if valid else None
|
||||||
|
|
||||||
|
def _level_label(level: str, locale: str) -> str:
|
||||||
|
mapping = {
|
||||||
|
"high": {"zh": "高", "en": "high"},
|
||||||
|
"medium": {"zh": "中", "en": "medium"},
|
||||||
|
"low": {"zh": "低", "en": "low"},
|
||||||
|
"strong": {"zh": "强", "en": "strong"},
|
||||||
|
"weak": {"zh": "弱", "en": "weak"},
|
||||||
|
}
|
||||||
|
return mapping.get(level, {}).get(locale, level)
|
||||||
|
|
||||||
cape_max = _max_numeric(_series("cape"))
|
cape_max = _max_numeric(_series("cape"))
|
||||||
cin_min = _min_numeric(_series("convective_inhibition"))
|
cin_min = _min_numeric(_series("convective_inhibition"))
|
||||||
lifted_index_min = _min_numeric(_series("lifted_index"))
|
lifted_index_min = _min_numeric(_series("lifted_index"))
|
||||||
@@ -102,43 +112,78 @@ def _build_vertical_profile_signal(
|
|||||||
shear_10m_180m_max = max(shear_values) if shear_values else None
|
shear_10m_180m_max = max(shear_values) if shear_values else None
|
||||||
|
|
||||||
suppression_risk = "low"
|
suppression_risk = "low"
|
||||||
if (cape_max is not None and cape_max >= 900) or (
|
if (cape_max is not None and cape_max >= 700) or (
|
||||||
cin_min is not None and cin_min <= -60
|
cin_min is not None and cin_min <= -50
|
||||||
):
|
):
|
||||||
suppression_risk = "high"
|
suppression_risk = "high"
|
||||||
elif (cape_max is not None and cape_max >= 300) or (
|
elif (cape_max is not None and cape_max >= 150) or (
|
||||||
cin_min is not None and cin_min <= -20
|
cin_min is not None and cin_min <= -15
|
||||||
):
|
):
|
||||||
suppression_risk = "medium"
|
suppression_risk = "medium"
|
||||||
|
|
||||||
trigger_risk = "low"
|
trigger_risk = "low"
|
||||||
if (
|
if (
|
||||||
cape_max is not None
|
cape_max is not None
|
||||||
and cape_max >= 800
|
and cape_max >= 550
|
||||||
and lifted_index_min is not None
|
and lifted_index_min is not None
|
||||||
and lifted_index_min <= -2
|
and lifted_index_min <= -1.5
|
||||||
):
|
):
|
||||||
trigger_risk = "high"
|
trigger_risk = "high"
|
||||||
elif (
|
elif (
|
||||||
cape_max is not None
|
cape_max is not None
|
||||||
and cape_max >= 250
|
and cape_max >= 120
|
||||||
and lifted_index_min is not None
|
and lifted_index_min is not None
|
||||||
and lifted_index_min <= 0
|
and lifted_index_min <= 0.5
|
||||||
):
|
):
|
||||||
trigger_risk = "medium"
|
trigger_risk = "medium"
|
||||||
|
|
||||||
mixing_strength = "weak"
|
mixing_strength = "weak"
|
||||||
if boundary_layer_height_max is not None and boundary_layer_height_max >= 1800:
|
if boundary_layer_height_max is not None and boundary_layer_height_max >= 1400:
|
||||||
mixing_strength = "strong"
|
mixing_strength = "strong"
|
||||||
elif boundary_layer_height_max is not None and boundary_layer_height_max >= 1000:
|
elif boundary_layer_height_max is not None and boundary_layer_height_max >= 700:
|
||||||
mixing_strength = "medium"
|
mixing_strength = "medium"
|
||||||
|
|
||||||
shear_risk = "low"
|
shear_risk = "low"
|
||||||
if shear_10m_180m_max is not None and shear_10m_180m_max >= 12:
|
if shear_10m_180m_max is not None and shear_10m_180m_max >= 8:
|
||||||
shear_risk = "high"
|
shear_risk = "high"
|
||||||
elif shear_10m_180m_max is not None and shear_10m_180m_max >= 6:
|
elif shear_10m_180m_max is not None and shear_10m_180m_max >= 4:
|
||||||
shear_risk = "medium"
|
shear_risk = "medium"
|
||||||
|
|
||||||
|
heating_setup = "neutral"
|
||||||
|
heating_score = 0
|
||||||
|
if suppression_risk == "high":
|
||||||
|
heating_score -= 2
|
||||||
|
elif suppression_risk == "medium":
|
||||||
|
heating_score -= 1
|
||||||
|
if trigger_risk == "high":
|
||||||
|
heating_score -= 2
|
||||||
|
elif trigger_risk == "medium":
|
||||||
|
heating_score -= 1
|
||||||
|
if mixing_strength == "strong":
|
||||||
|
heating_score += 2
|
||||||
|
elif mixing_strength == "medium":
|
||||||
|
heating_score += 1
|
||||||
|
else:
|
||||||
|
heating_score -= 1
|
||||||
|
if shear_risk == "high":
|
||||||
|
heating_score -= 1
|
||||||
|
|
||||||
|
if heating_score >= 2:
|
||||||
|
heating_setup = "supportive"
|
||||||
|
elif heating_score <= -2:
|
||||||
|
heating_setup = "suppressed"
|
||||||
|
|
||||||
|
has_profile_data = any(
|
||||||
|
value is not None
|
||||||
|
for value in (
|
||||||
|
cape_max,
|
||||||
|
cin_min,
|
||||||
|
lifted_index_min,
|
||||||
|
boundary_layer_height_max,
|
||||||
|
shear_10m_180m_max,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
zh_parts = []
|
zh_parts = []
|
||||||
en_parts = []
|
en_parts = []
|
||||||
if suppression_risk == "high":
|
if suppression_risk == "high":
|
||||||
@@ -147,26 +192,83 @@ def _build_vertical_profile_signal(
|
|||||||
elif suppression_risk == "medium":
|
elif suppression_risk == "medium":
|
||||||
zh_parts.append("存在一定云雨压温风险。")
|
zh_parts.append("存在一定云雨压温风险。")
|
||||||
en_parts.append("There is some cloud and shower suppression risk.")
|
en_parts.append("There is some cloud and shower suppression risk.")
|
||||||
|
elif has_profile_data:
|
||||||
|
zh_parts.append("高空对流压温风险暂时不高。")
|
||||||
|
en_parts.append("Upper-air suppression risk remains limited for now.")
|
||||||
if mixing_strength == "strong":
|
if mixing_strength == "strong":
|
||||||
zh_parts.append("边界层混合较深,若无云雨打断仍有冲高空间。")
|
zh_parts.append("边界层混合较深,若无云雨打断仍有冲高空间。")
|
||||||
en_parts.append("Deep boundary-layer mixing still supports additional warming if convection stays limited.")
|
en_parts.append("Deep boundary-layer mixing still supports additional warming if convection stays limited.")
|
||||||
elif mixing_strength == "medium":
|
elif mixing_strength == "medium":
|
||||||
zh_parts.append("白天混合条件中等。")
|
zh_parts.append("白天混合条件中等。")
|
||||||
en_parts.append("Daytime mixing potential is moderate.")
|
en_parts.append("Daytime mixing potential is moderate.")
|
||||||
|
elif has_profile_data:
|
||||||
|
zh_parts.append("边界层混合偏浅。")
|
||||||
|
en_parts.append("Boundary-layer mixing remains shallow.")
|
||||||
if shear_risk == "high":
|
if shear_risk == "high":
|
||||||
zh_parts.append("高空风切变较强,午后结构波动可能加大。")
|
zh_parts.append("高空风切变较强,午后结构波动可能加大。")
|
||||||
en_parts.append("Upper-level shear is relatively strong and may increase afternoon volatility.")
|
en_parts.append("Upper-level shear is relatively strong and may increase afternoon volatility.")
|
||||||
|
elif shear_risk == "medium":
|
||||||
|
zh_parts.append("高空风切变有一定存在感。")
|
||||||
|
en_parts.append("Upper-level shear is noticeable.")
|
||||||
|
elif has_profile_data:
|
||||||
|
zh_parts.append("高空风切变扰动有限。")
|
||||||
|
en_parts.append("Upper-level shear disruption remains limited.")
|
||||||
if trigger_risk == "high":
|
if trigger_risk == "high":
|
||||||
zh_parts.append("抬升触发条件较好,需警惕午后云团发展。")
|
zh_parts.append("抬升触发条件较好,需警惕午后云团发展。")
|
||||||
en_parts.append("Trigger conditions are favorable enough to watch for afternoon convective development.")
|
en_parts.append("Trigger conditions are favorable enough to watch for afternoon convective development.")
|
||||||
elif trigger_risk == "medium":
|
elif trigger_risk == "medium":
|
||||||
zh_parts.append("午后具备一定触发条件。")
|
zh_parts.append("午后具备一定触发条件。")
|
||||||
en_parts.append("There is some afternoon trigger potential.")
|
en_parts.append("There is some afternoon trigger potential.")
|
||||||
if not zh_parts:
|
elif has_profile_data:
|
||||||
|
zh_parts.append("午后触发条件偏弱。")
|
||||||
|
en_parts.append("Afternoon trigger potential remains weak.")
|
||||||
|
if not has_profile_data:
|
||||||
|
zh_parts.append("高空剖面字段暂缺,当前仅保留基础默认信号。")
|
||||||
|
en_parts.append("Upper-air profile fields are currently unavailable, so only a fallback signal is shown.")
|
||||||
|
elif not zh_parts:
|
||||||
zh_parts.append("高空结构整体平稳,暂未看到明显压温信号。")
|
zh_parts.append("高空结构整体平稳,暂未看到明显压温信号。")
|
||||||
if not en_parts:
|
if not en_parts:
|
||||||
en_parts.append("The upper-air structure looks fairly stable, without a strong suppression signal yet.")
|
en_parts.append("The upper-air structure looks fairly stable, without a strong suppression signal yet.")
|
||||||
|
|
||||||
|
if has_profile_data:
|
||||||
|
summary_tokens_zh = []
|
||||||
|
summary_tokens_en = []
|
||||||
|
window_start = str(times[candidate_indexes[0]]).split("T")[1][:5]
|
||||||
|
window_end = str(times[candidate_indexes[-1]]).split("T")[1][:5]
|
||||||
|
zh_parts.append(f"判断窗口:{window_start}-{window_end}。")
|
||||||
|
en_parts.append(f"Signal window: {window_start}-{window_end}.")
|
||||||
|
if cape_max is not None:
|
||||||
|
summary_tokens_zh.append(f"CAPE≈{round(cape_max)}")
|
||||||
|
summary_tokens_en.append(f"CAPE≈{round(cape_max)}")
|
||||||
|
if cin_min is not None:
|
||||||
|
summary_tokens_zh.append(f"CIN≈{round(cin_min)}")
|
||||||
|
summary_tokens_en.append(f"CIN≈{round(cin_min)}")
|
||||||
|
if boundary_layer_height_max is not None:
|
||||||
|
summary_tokens_zh.append(f"混合层≈{round(boundary_layer_height_max)}m")
|
||||||
|
summary_tokens_en.append(f"mixing≈{round(boundary_layer_height_max)}m")
|
||||||
|
if shear_10m_180m_max is not None:
|
||||||
|
summary_tokens_zh.append(f"切变≈{shear_10m_180m_max:.1f}")
|
||||||
|
summary_tokens_en.append(f"shear≈{shear_10m_180m_max:.1f}")
|
||||||
|
zh_parts.append(
|
||||||
|
f"压温{_level_label(suppression_risk, 'zh')}、触发{_level_label(trigger_risk, 'zh')}、混合{_level_label(mixing_strength, 'zh')}、切变{_level_label(shear_risk, 'zh')}。"
|
||||||
|
)
|
||||||
|
en_parts.append(
|
||||||
|
f"Suppression { _level_label(suppression_risk, 'en') }, trigger { _level_label(trigger_risk, 'en') }, mixing { _level_label(mixing_strength, 'en') }, shear { _level_label(shear_risk, 'en') }."
|
||||||
|
)
|
||||||
|
if heating_setup == "supportive":
|
||||||
|
zh_parts.append("整体更偏向支持白天冲高。")
|
||||||
|
en_parts.append("Overall, the profile is more supportive of daytime heating.")
|
||||||
|
elif heating_setup == "suppressed":
|
||||||
|
zh_parts.append("整体更偏向抑制午后冲高。")
|
||||||
|
en_parts.append("Overall, the profile leans more toward suppressing the afternoon peak.")
|
||||||
|
else:
|
||||||
|
zh_parts.append("整体更像中性环境,仍需结合地面信号。")
|
||||||
|
en_parts.append("Overall, the profile looks fairly neutral and still needs surface confirmation.")
|
||||||
|
if summary_tokens_zh:
|
||||||
|
zh_parts.append(" / ".join(summary_tokens_zh) + "。")
|
||||||
|
if summary_tokens_en:
|
||||||
|
en_parts.append(" / ".join(summary_tokens_en) + ".")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"source": "open-meteo-gfs",
|
"source": "open-meteo-gfs",
|
||||||
"window_start": times[candidate_indexes[0]] if candidate_indexes else None,
|
"window_start": times[candidate_indexes[0]] if candidate_indexes else None,
|
||||||
@@ -180,6 +282,8 @@ def _build_vertical_profile_signal(
|
|||||||
"trigger_risk": trigger_risk,
|
"trigger_risk": trigger_risk,
|
||||||
"mixing_strength": mixing_strength,
|
"mixing_strength": mixing_strength,
|
||||||
"shear_risk": shear_risk,
|
"shear_risk": shear_risk,
|
||||||
|
"heating_setup": heating_setup,
|
||||||
|
"heating_score": heating_score,
|
||||||
"summary_zh": "".join(zh_parts),
|
"summary_zh": "".join(zh_parts),
|
||||||
"summary_en": " ".join(en_parts),
|
"summary_en": " ".join(en_parts),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user