Skip to content

Commit a11000d

Browse files
committed
Polish public advisory page
1 parent e5f25f1 commit a11000d

5 files changed

Lines changed: 82 additions & 66 deletions

File tree

src/quant_advisor_research/advisory_report.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,21 @@
139139
},
140140
}
141141

142+
COMPANY_RISKS_ZH = {
143+
"AMD": "主要风险是 AI 加速器竞争强、客户订单兑现不及预期、毛利率波动,以及股价快速上涨后的回撤风险。",
144+
"AVGO": "主要风险是云厂商资本开支节奏放缓、定制芯片订单集中、软件整合不及预期,以及估值对 AI 需求预期较敏感。",
145+
"COIN": "主要风险是加密资产价格和交易量波动大、监管规则变化、手续费率下行,以及平台合规成本上升。",
146+
"CVX": "主要风险是油气价格回落、项目资本开支超预期、地缘政治和政策变化,以及能源股周期性回撤。",
147+
"DELL": "主要风险是 AI 服务器利润率偏低、订单转收入节奏不稳定、企业硬件需求周期波动,以及股价已反映较高增长预期。",
148+
"INTC": "主要风险是晶圆代工转型执行难度高、资本开支和现金流压力大、先进制程追赶不及预期,以及竞争格局仍然激烈。",
149+
"MSTR": "主要风险是比特币价格大幅波动、融资环境变化、资产净值溢价收缩,以及监管规则变化。",
150+
"MU": "主要风险是存储周期反转、HBM 供需和价格不及预期、客户集中度较高,以及强动量后的估值和回撤压力。",
151+
"NEE": "主要风险是利率上行压制公用事业估值、电网和新能源项目审批延迟、资本开支压力,以及电力需求兑现慢于预期。",
152+
"TSM": "主要风险是先进制程资本开支高、客户需求周期波动、地缘政治风险,以及 AI 芯片需求预期过高。",
153+
"VRT": "主要风险是数据中心建设节奏放缓、订单转收入和交付能力不及预期、液冷竞争加剧,以及估值已反映较高成长预期。",
154+
"XOM": "主要风险是油气价格回落、炼化利润收缩、地缘政治和政策变化,以及能源股周期性回撤。",
155+
}
156+
142157
EXCLUDED_THEME_PICK_SYMBOLS = {
143158
"DIA",
144159
"IWM",
@@ -337,6 +352,14 @@ def company_profile(symbol: Any, name: Any = "") -> dict[str, str]:
337352
}
338353

339354

355+
def company_risk_summary(symbol: Any) -> str:
356+
symbol_text = str(symbol or "").upper()
357+
return COMPANY_RISKS_ZH.get(
358+
symbol_text,
359+
"主要风险是行业需求不及预期、估值对增长假设较敏感、业绩兑现延后,以及股价波动带来的回撤。",
360+
)
361+
362+
340363
def normalize_ai_mapping(mapping: Any) -> dict[str, Any]:
341364
if not isinstance(mapping, dict):
342365
return {}
@@ -925,7 +948,7 @@ def build_final_decisions(
925948
"business_summary": profile["business"],
926949
"prospect_summary": profile["prospect"],
927950
"why_selected": dedupe(reasons),
928-
"risk_summary": rec.get("risk_notes", ["需复核估值、财报、回撤和流动性。"])[0] if rec else "需复核估值、财报、回撤和流动性。",
951+
"risk_summary": company_risk_summary(symbol),
929952
}
930953
)
931954

@@ -1054,26 +1077,11 @@ def build_advisory_report(
10541077
def render_markdown(report: dict[str, Any]) -> str:
10551078
cadence_label = CADENCE_LABELS_ZH.get(str(report["cadence"]), str(report["cadence"]).title())
10561079
lines = [
1057-
f"# 量化模型推荐{cadence_label}复盘",
1058-
"",
1059-
f"- 日期: `{report['as_of']}`",
1060-
f"- 模式: `{report['mode']}`",
1061-
f"- 受众: `{report['audience_scope']}`",
1062-
f"- AI 状态: `{report['summary']['ai_regime']}`",
1063-
"",
1064-
"## 政策边界",
1065-
"",
1066-
"- 允许下单: `false`",
1067-
"- 允许组合配置: `false`",
1068-
"- 允许个性化建议: `false`",
1069-
"- 允许非个性化模型推荐: `true`",
1070-
"- 允许账户级建议: `false`",
1080+
f"# 量化模型推荐{cadence_label}复盘 - {report['as_of']}",
10711081
"",
10721082
]
10731083
final_decisions = report.get("final_decisions", {})
10741084
if final_decisions:
1075-
lines.extend(["## 本期最终结论", ""])
1076-
lines.append("- 口径: 以 AI信号仓库(AiLongHorizonSignalPipelines)和动量为主,政策/新闻事件用于提高置信度和提示风险。")
10771085
horizon_buckets = final_decisions.get("horizon_buckets", {})
10781086
for horizon, label in (("short", "短线"), ("medium", "中线"), ("long", "长线")):
10791087
symbols = ", ".join(horizon_buckets.get(horizon, [])) or "暂无最终推荐"

src/quant_advisor_research/notifications.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ def _format_final_decisions(report: dict[str, Any]) -> list[str]:
2929
decisions = report.get("final_decisions", {})
3030
picks = decisions.get("recommendations", [])
3131
if not picks:
32-
return ["本期最终推荐:暂无(口径:AI信号仓库 + 动量为主,政策/新闻辅助)"]
33-
lines = ["本期最终推荐(AI信号仓库 + 动量为主,政策/新闻辅助):"]
32+
return ["本期最终推荐:暂无"]
33+
lines = ["本期最终推荐:"]
3434
for item in picks:
3535
lines.append(
3636
"- {symbol} | {horizon} | 综合分={score} | 股票背景:{business}".format(
@@ -102,17 +102,11 @@ def format_telegram_message(
102102
max_recommendations: int = 8,
103103
max_themes: int = 5,
104104
) -> str:
105-
summary = report.get("summary", {})
106105
lines = [
107106
f"量化模型推荐 | {cadence_label(report)} | {report.get('as_of', '')}",
108107
"",
109-
f"模式:{report.get('mode', '')}",
110-
f"来源:{summary.get('source_mode', 'unknown')}",
111-
f"来源事件:{summary.get('source_event_count', 0)}",
112-
"",
113108
*_format_final_decisions(report),
114109
"",
115-
"说明:非个性化模型输出;不包含下单、仓位配置或账户级建议。",
116110
f"完整报告:{report_public_url(report, site_url=site_url)}",
117111
]
118112
return "\n".join(str(line) for line in lines if line is not None)

src/quant_advisor_research/publisher.py

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,8 @@ def render_final_decisions_html(report: dict[str, Any]) -> str:
111111
"""
112112
)
113113

114-
final_intro = "、".join(str(pick.get("symbol", "")) for pick in final_picks) or "暂无"
115114
return f"""
116115
<section class="final-decisions">
117-
<h2>本期最终结论</h2>
118-
<p><strong>最终推荐:</strong>{html.escape(final_intro)}。合成口径以 AI信号仓库(AiLongHorizonSignalPipelines)和动量为主,政策/新闻事件用于提高置信度和提示风险。</p>
119116
<ul class="horizon-list">{''.join(horizon_rows)}</ul>
120117
<div class="final-grid">{''.join(cards)}</div>
121118
</section>
@@ -221,19 +218,16 @@ def render_report_html(report: dict[str, Any]) -> str:
221218
:root {{ color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }}
222219
body {{ margin: 0; background: #f6f7f9; color: #1b1f24; }}
223220
main {{ max-width: 1080px; margin: 0 auto; padding: 32px 20px 56px; }}
224-
.hero {{ border-bottom: 1px solid #d8dee4; padding-bottom: 20px; margin-bottom: 24px; }}
225-
h1 {{ margin: 0 0 12px; font-size: 2rem; line-height: 1.2; }}
226-
.meta {{ display: flex; flex-wrap: wrap; gap: 10px; color: #57606a; }}
227-
.pill {{ border: 1px solid #d0d7de; border-radius: 999px; padding: 5px 10px; background: #fff; }}
228-
.policy {{ background: #fff7ed; border: 1px solid #fed7aa; padding: 14px 16px; margin-bottom: 20px; }}
221+
.hero {{ text-align: center; padding: 8px 0 22px; margin-bottom: 10px; }}
222+
h1 {{ margin: 0; font-size: clamp(2rem, 5vw, 3.25rem); line-height: 1.15; letter-spacing: -0.04em; }}
229223
.warning {{ background: #fff1f2; border: 1px solid #fecdd3; padding: 14px 16px; margin-bottom: 20px; }}
230-
.final-decisions {{ background: #ecfeff; border: 1px solid #67e8f9; padding: 16px; margin-bottom: 20px; border-radius: 8px; }}
231-
.final-decisions > p {{ color: #334155; line-height: 1.55; }}
232-
.horizon-list {{ background: #fff; border: 1px solid #bae6fd; border-radius: 8px; padding: 12px 16px 12px 32px; }}
233-
.final-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 14px; }}
234-
.final-card {{ background: #fff; border: 1px solid #67e8f9; border-radius: 8px; padding: 16px; }}
235-
.final-card h3 {{ margin: 0; font-size: 1.2rem; }}
236-
.final-card h3 span {{ font-size: .85rem; color: #0f766e; margin-left: 8px; }}
224+
.final-decisions {{ margin-bottom: 20px; }}
225+
.horizon-list {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px; list-style: none; padding: 0; margin: 0 0 22px; }}
226+
.horizon-list li {{ background: #fff; border: 1px solid #d8dee4; border-radius: 12px; padding: 12px 14px; }}
227+
.final-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }}
228+
.final-card {{ background: #fff; border: 1px solid #d8dee4; border-radius: 12px; padding: 18px; box-shadow: 0 1px 2px rgb(27 31 36 / 4%); }}
229+
.final-card h3 {{ margin: 0; font-size: 1.35rem; }}
230+
.final-card h3 span {{ font-size: .85rem; color: #57606a; margin-left: 8px; }}
237231
.final-card header p {{ margin: 4px 0 12px; color: #57606a; }}
238232
.final-card p {{ line-height: 1.55; color: #334155; }}
239233
.theme-candidates {{ background: #f0fdf4; border: 1px solid #bbf7d0; padding: 16px; margin-bottom: 20px; border-radius: 8px; }}
@@ -268,17 +262,6 @@ def render_report_html(report: dict[str, Any]) -> str:
268262
<main>
269263
<section class="hero">
270264
<h1>{html.escape(title)}</h1>
271-
<div class="meta">
272-
<span class="pill">模式:{html.escape(report['mode'])}</span>
273-
<span class="pill">受众:{html.escape(report['audience_scope'])}</span>
274-
<span class="pill">AI 状态:{html.escape(str(report['summary']['ai_regime']))}</span>
275-
<span class="pill">来源:{html.escape(source_mode)}</span>
276-
<span class="pill">最终推荐:{len(report.get('final_decisions', {}).get('recommendations', []))}</span>
277-
</div>
278-
</section>
279-
<section class="policy">
280-
<strong>政策边界:</strong>当前只允许非个性化模型推荐;
281-
不允许下单、组合配置、账户适当性判断或个性化建议。
282265
</section>
283266
{warning_html}
284267
{final_decisions_html}

tests/test_advisory_report.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,20 +156,29 @@ def test_contract_rejects_execution_enabled_report() -> None:
156156
validate_advisory_report(report)
157157

158158

159-
def test_render_markdown_contains_policy_section() -> None:
159+
def test_render_markdown_keeps_public_report_direct() -> None:
160160
report = build_advisory_report(
161161
as_of="2026-05-30",
162162
cadence="weekly",
163163
political_events_path=ROOT / "examples/political_events.example.csv",
164164
political_watchlist_path=ROOT / "examples/political_watchlist.example.csv",
165165
ai_signal_path=ROOT / "examples/ai_long_horizon_signal.example.json",
166+
theme_momentum_path=ROOT / "examples/theme_momentum_snapshot.example.json",
166167
)
167168

168169
markdown = render_markdown(report)
169170

170-
assert "## 政策边界" in markdown
171-
assert "允许下单: `false`" in markdown
172-
assert "允许非个性化模型推荐: `true`" in markdown
171+
assert "# 量化模型推荐周度复盘 - 2026-05-30" in markdown
172+
assert "股票背景" in markdown
173+
assert "推荐理由" in markdown
174+
assert "主要风险" in markdown
175+
assert "## 政策边界" not in markdown
176+
assert "允许下单" not in markdown
177+
assert "模式:" not in markdown
178+
assert "受众:" not in markdown
179+
assert "AI 状态" not in markdown
180+
assert "本期最终结论" not in markdown
181+
assert "AiLongHorizonSignalPipelines" not in markdown
173182

174183

175184
def test_contract_rejects_account_action_fields() -> None:
@@ -342,15 +351,17 @@ def test_theme_momentum_snapshot_is_display_context_not_rating_input(tmp_path: P
342351
assert report_with_theme["theme_first_candidates"][2]["symbol"] == "DELL"
343352
assert report_with_theme["final_decisions"]["recommendations"][0]["symbol"] == "MU"
344353
assert report_with_theme["final_decisions"]["recommendations"][0]["business_summary"]
354+
assert "存储周期" in report_with_theme["final_decisions"]["recommendations"][0]["risk_summary"]
345355
assert "ai_signal_score" in report_with_theme["final_decisions"]["recommendations"][0]
346356
assert report_with_theme["final_decisions"]["watchlist"][0]["symbol"] == "DELL"
347357
assert report_with_theme["recommendations"][0]["rating"] == report_without_theme["recommendations"][0]["rating"]
348358
assert report_with_theme["recommendations"][0]["score"] == report_without_theme["recommendations"][0]["score"]
349359

350360
markdown = render_markdown(report_with_theme)
351-
assert "## 本期最终结论" in markdown
361+
assert "## 本期最终结论" not in markdown
352362
assert "股票背景" in markdown
353363
assert "推荐理由" in markdown
364+
assert "AiLongHorizonSignalPipelines" not in markdown
354365
assert "## 主题候选(解释材料,不是最终推荐)" not in markdown
355366
assert "为什么入选" not in markdown
356367
assert "买多少" not in markdown

tests/test_publisher.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,25 @@ def build_sample_report() -> dict:
2020
)
2121

2222

23-
def test_render_report_html_contains_policy_boundary() -> None:
23+
def test_render_report_html_is_direct_public_recommendation_page() -> None:
2424
html = render_report_html(build_sample_report())
2525

26-
assert "政策边界" in html
27-
assert "不允许下单" in html
26+
assert "量化模型推荐周度复盘 - 2026-05-30" in html
27+
assert "text-align: center" in html
2828
assert "2-12周" in html
2929
assert "来源模式:fixture" in html
30-
assert "本期最终结论" in html
3130
assert "股票背景" in html
31+
assert "推荐理由" in html
32+
assert "主要风险" in html
33+
assert "<span class=\"pill\">模式:" not in html
34+
assert "受众:" not in html
35+
assert "AI 状态:" not in html
36+
assert "政策边界" not in html
37+
assert "不允许下单" not in html
38+
assert "本期最终结论" not in html
39+
assert "最终推荐:" not in html
40+
assert "合成口径" not in html
41+
assert "AiLongHorizonSignalPipelines" not in html
3242
assert "背景跟踪(非推荐" not in html
3343
assert "复核清单" not in html
3444
assert "主题候选(解释材料,不是最终推荐)" not in html
@@ -106,12 +116,20 @@ def test_render_report_html_includes_theme_momentum_context() -> None:
106116

107117
html = render_report_html(report)
108118

109-
assert "本期最终结论" in html
110-
assert "最终推荐" in html
119+
assert "本期最终结论" not in html
120+
assert "最终推荐:" not in html
121+
assert "合成口径" not in html
122+
assert "政策边界" not in html
123+
assert "<span class=\"pill\">模式:" not in html
124+
assert "受众:" not in html
125+
assert "AI 状态:" not in html
111126
assert "AI信号仓库" in html
112-
assert "AiLongHorizonSignalPipelines" in html
127+
assert "AiLongHorizonSignalPipelines" not in html
113128
assert "股票背景" in html
114129
assert "推荐理由" in html
130+
assert "主要风险" in html
131+
assert "存储周期" in html
132+
assert "需复核估值、财报、回撤和流动性" not in html
115133
assert "做什么" not in html
116134
assert "为什么有前景" not in html
117135
assert "买多少" not in html
@@ -123,7 +141,7 @@ def test_render_report_html_includes_theme_momentum_context() -> None:
123141
assert "MU" in html
124142

125143

126-
def test_format_telegram_message_contains_themes_policy_and_link() -> None:
144+
def test_format_telegram_message_is_direct_and_links_report() -> None:
127145
from quant_advisor_research.notifications import format_telegram_message
128146

129147
report = build_sample_report()
@@ -158,12 +176,14 @@ def test_format_telegram_message_contains_themes_policy_and_link() -> None:
158176

159177
assert "量化模型推荐" in message
160178
assert "本期最终推荐" in message
161-
assert "AI信号仓库" in message
179+
assert "AI信号仓库" not in message
162180
assert "股票背景" in message
163181
assert "推荐理由" in message
164182
assert "买多少" not in message
165183
assert "主题候选" not in message
166184
assert "事件证据" not in message
185+
assert "模式:" not in message
186+
assert "来源:" not in message
187+
assert "不包含下单" not in message
167188
assert "MU" in message
168-
assert "不包含下单" in message
169189
assert "https://example.com/advisor/2026-05-30-weekly-model-recommendations.html" in message

0 commit comments

Comments
 (0)