Skip to content

Commit e5f25f1

Browse files
committed
Simplify public advisory output
1 parent 42e20dc commit e5f25f1

5 files changed

Lines changed: 39 additions & 137 deletions

File tree

src/quant_advisor_research/advisory_report.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ def company_profile(symbol: Any, name: Any = "") -> dict[str, str]:
333333
display_name = str(name or symbol_text).strip() or symbol_text
334334
return {
335335
"business": f"{display_name}{symbol_text})为当前观察股票池标的。",
336-
"prospect": "前景需要结合行业需求、公司财报、估值、事件证据和价格趋势继续复核。",
336+
"prospect": "推荐理由需要结合行业需求、公司财报、估值和价格趋势继续复核。",
337337
}
338338

339339

@@ -926,7 +926,6 @@ def build_final_decisions(
926926
"prospect_summary": profile["prospect"],
927927
"why_selected": dedupe(reasons),
928928
"risk_summary": rec.get("risk_notes", ["需复核估值、财报、回撤和流动性。"])[0] if rec else "需复核估值、财报、回撤和流动性。",
929-
"buy_amount_guidance": "不提供买入数量或仓位;这是非个性化模型研究,不知道你的账户规模、风险承受能力和现有持仓。",
930929
}
931930
)
932931

@@ -1074,7 +1073,7 @@ def render_markdown(report: dict[str, Any]) -> str:
10741073
final_decisions = report.get("final_decisions", {})
10751074
if final_decisions:
10761075
lines.extend(["## 本期最终结论", ""])
1077-
lines.append("- 口径: 以 AI信号仓库(AiLongHorizonSignalPipelines)和动量为主,政策/新闻事件用于提高置信度和提示风险;不包含买入数量、仓位或账户级配置。")
1076+
lines.append("- 口径: 以 AI信号仓库(AiLongHorizonSignalPipelines)和动量为主,政策/新闻事件用于提高置信度和提示风险。")
10781077
horizon_buckets = final_decisions.get("horizon_buckets", {})
10791078
for horizon, label in (("short", "短线"), ("medium", "中线"), ("long", "长线")):
10801079
symbols = ", ".join(horizon_buckets.get(horizon, [])) or "暂无最终推荐"
@@ -1086,23 +1085,19 @@ def render_markdown(report: dict[str, Any]) -> str:
10861085
f"### {pick.get('symbol')} - {pick.get('action_label')}",
10871086
"",
10881087
f"- 周期: {pick.get('primary_horizon_label')}({pick.get('primary_horizon_window')})",
1089-
f"- 做什么: {pick.get('business_summary')}",
1090-
f"- 为什么有前景: {pick.get('prospect_summary')}",
1088+
f"- 股票背景: {pick.get('business_summary')}",
1089+
f"- 推荐理由: {pick.get('prospect_summary')}",
10911090
"- 多源依据:",
10921091
]
10931092
)
10941093
lines.extend(f" - {reason}" for reason in pick.get("why_selected", []))
10951094
lines.extend(
10961095
[
10971096
f"- 主要风险: {pick.get('risk_summary')}",
1098-
f"- 买多少: {pick.get('buy_amount_guidance')}",
10991097
"",
11001098
]
11011099
)
1102-
if final_decisions.get("watchlist"):
1103-
watch_symbols = ", ".join(item.get("symbol", "") for item in final_decisions["watchlist"])
1104-
lines.append(f"- 观察名单,不是最终推荐: {watch_symbols}")
1105-
lines.append("")
1100+
return "\n".join(lines).rstrip() + "\n"
11061101
theme_candidates = report.get("theme_first_candidates", [])
11071102
if theme_candidates:
11081103
lines.extend(["## 主题候选(解释材料,不是最终推荐)", ""])

src/quant_advisor_research/notifications.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,27 +29,25 @@ 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 [
33-
"本期最终推荐:暂无(口径:AI信号仓库 + 动量为主,政策/新闻辅助)",
34-
"买多少:不提供买入数量、仓位或账户级配置。",
35-
]
32+
return ["本期最终推荐:暂无(口径:AI信号仓库 + 动量为主,政策/新闻辅助)"]
3633
lines = ["本期最终推荐(AI信号仓库 + 动量为主,政策/新闻辅助):"]
3734
for item in picks:
3835
lines.append(
39-
"- {symbol} | {horizon} | 综合分={score} | 做什么:{business}".format(
36+
"- {symbol} | {horizon} | 综合分={score} | 股票背景:{business}".format(
4037
symbol=item.get("symbol", ""),
4138
horizon=item.get("primary_horizon_label", ""),
4239
score=display_number(item.get("combined_score")),
4340
business=item.get("business_summary", ""),
4441
)
4542
)
43+
if item.get("prospect_summary"):
44+
lines.append(f" 推荐理由:{item.get('prospect_summary')}")
4645
buckets = decisions.get("horizon_buckets", {})
4746
lines.append("周期:短线={short};中线={medium};长线={long}".format(
4847
short=", ".join(buckets.get("short", [])) or "暂无",
4948
medium=", ".join(buckets.get("medium", [])) or "暂无",
5049
long=", ".join(buckets.get("long", [])) or "暂无",
5150
))
52-
lines.append("买多少:不提供买入数量、仓位或账户级配置。")
5351
return lines
5452

5553

@@ -114,12 +112,6 @@ def format_telegram_message(
114112
"",
115113
*_format_final_decisions(report),
116114
"",
117-
*_format_themes(report, limit=max_themes),
118-
"",
119-
*_format_theme_candidates(report, limit=max_recommendations),
120-
"",
121-
*_format_recommendations(report, limit=max_recommendations),
122-
"",
123115
"说明:非个性化模型输出;不包含下单、仓位配置或账户级建议。",
124116
f"完整报告:{report_public_url(report, site_url=site_url)}",
125117
]

src/quant_advisor_research/publisher.py

Lines changed: 5 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ def render_final_decisions_html(report: dict[str, Any]) -> str:
7373
if not decisions:
7474
return ""
7575
final_picks = decisions.get("recommendations", [])
76-
watchlist = decisions.get("watchlist", [])
7776
buckets = decisions.get("horizon_buckets", {})
7877
horizon_rows = []
7978
for horizon, label, window in (
@@ -102,49 +101,23 @@ def render_final_decisions_html(report: dict[str, Any]) -> str:
102101
<div><dt>政策/新闻</dt><dd>{html.escape(display_number(pick.get('source_score')))}</dd></div>
103102
<div><dt>动量</dt><dd>{html.escape(display_number(pick.get('momentum_score')))}</dd></div>
104103
<div><dt>AI信号仓库</dt><dd>{html.escape(display_number(pick.get('ai_signal_score')))}</dd></div>
105-
<div><dt>仓位/数量</dt><dd>不提供</dd></div>
106104
</dl>
107-
<p><strong>做什么:</strong>{html.escape(str(pick.get('business_summary', '')))}</p>
108-
<p><strong>为什么有前景:</strong>{html.escape(str(pick.get('prospect_summary', '')))}</p>
105+
<p><strong>股票背景:</strong>{html.escape(str(pick.get('business_summary', '')))}</p>
106+
<p><strong>推荐理由:</strong>{html.escape(str(pick.get('prospect_summary', '')))}</p>
109107
<p><strong>多源依据:</strong></p>
110108
<ul>{reasons}</ul>
111109
<p><strong>主要风险:</strong>{html.escape(str(pick.get('risk_summary', '')))}</p>
112-
<p class="position-note"><strong>买多少:</strong>{html.escape(str(pick.get('buy_amount_guidance', '不提供买入数量或仓位。')))}</p>
113110
</article>
114111
"""
115112
)
116113

117-
watch_rows = []
118-
for item in watchlist:
119-
watch_rows.append(
120-
"<tr>"
121-
f"<td>{html.escape(str(item.get('symbol', '')))}</td>"
122-
f"<td>{html.escape(str(item.get('primary_horizon_label', '')))}</td>"
123-
f"<td>{html.escape(display_number(item.get('combined_score')))}</td>"
124-
f"<td>{html.escape(str(item.get('business_summary', '')))}</td>"
125-
f"<td>{html.escape(str(item.get('prospect_summary', '')))}</td>"
126-
"</tr>"
127-
)
128-
watch_html = ""
129-
if watch_rows:
130-
watch_html = f"""
131-
<h3>观察名单(不是最终推荐)</h3>
132-
<p>这些标的可能有主题或动量,但事件证据、风险或推荐层级还不足,不作为本期最终推荐。</p>
133-
<table>
134-
<thead><tr><th>股票</th><th>周期</th><th>综合分</th><th>做什么</th><th>为什么可跟踪</th></tr></thead>
135-
<tbody>{''.join(watch_rows)}</tbody>
136-
</table>
137-
"""
138-
139114
final_intro = "、".join(str(pick.get("symbol", "")) for pick in final_picks) or "暂无"
140115
return f"""
141116
<section class="final-decisions">
142117
<h2>本期最终结论</h2>
143118
<p><strong>最终推荐:</strong>{html.escape(final_intro)}。合成口径以 AI信号仓库(AiLongHorizonSignalPipelines)和动量为主,政策/新闻事件用于提高置信度和提示风险。</p>
144-
<p><strong>买多少:</strong>不提供买入金额、股数、仓位或账户级配置;这里只能作为非个性化模型研究。</p>
145119
<ul class="horizon-list">{''.join(horizon_rows)}</ul>
146120
<div class="final-grid">{''.join(cards)}</div>
147-
{watch_html}
148121
</section>
149122
"""
150123

@@ -236,52 +209,7 @@ def render_report_html(report: dict[str, Any]) -> str:
236209
<ul>{warning_items}</ul>
237210
</section>
238211
"""
239-
theme_candidates_html = render_theme_first_candidates_html(report)
240-
theme_momentum_html = render_theme_momentum_html(report)
241212
final_decisions_html = render_final_decisions_html(report)
242-
recommendation_cards = []
243-
background_count = 0
244-
for rec in report["recommendations"]:
245-
reasons = "\n".join(f"<li>{html.escape(reason)}</li>" for reason in rec["reasons"])
246-
risks = "\n".join(f"<li>{html.escape(risk)}</li>" for risk in rec["risk_notes"])
247-
refs = "\n".join(
248-
f'<li><a href="{html.escape(ref)}">{html.escape(ref)}</a></li>' if ref.startswith("http") else f"<li>{html.escape(ref)}</li>"
249-
for ref in rec["evidence_refs"]
250-
)
251-
if rec.get("recommendation_tier") == "monitor":
252-
background_count += 1
253-
continue
254-
card_html = f"""
255-
<article class="recommendation">
256-
<header>
257-
<h2>{html.escape(rec['symbol'])} <span>{html.escape(rec['rating_label'])}</span></h2>
258-
<p>{html.escape(rec.get('name') or rec['symbol'])}</p>
259-
</header>
260-
<dl>
261-
<div><dt>周期</dt><dd>{html.escape(rec['primary_horizon_label'])}</dd></div>
262-
<div><dt>窗口</dt><dd>{html.escape(rec['primary_horizon_window'])}</dd></div>
263-
<div><dt>层级</dt><dd>{html.escape(rec['recommendation_tier_label'])}</dd></div>
264-
<div><dt>来源</dt><dd>{html.escape(rec['source_confidence_label'])}</dd></div>
265-
<div><dt>风格</dt><dd>{html.escape(rec['strategy_style'])}</dd></div>
266-
<div><dt>分数</dt><dd>{rec['score']}</dd></div>
267-
<div><dt>证据</dt><dd>{rec['evidence_score']}</dd></div>
268-
<div><dt>风险</dt><dd>{rec['risk_score']}</dd></div>
269-
</dl>
270-
<p class="horizon-note">{html.escape(rec['horizon_note'])}</p>
271-
<h3>推荐理由</h3>
272-
<ul>{reasons}</ul>
273-
<h3>风险提示</h3>
274-
<ul>{risks}</ul>
275-
<h3>证据链接</h3>
276-
<ul>{refs}</ul>
277-
</article>
278-
"""
279-
recommendation_cards.append(card_html)
280-
background_html = ""
281-
if background_count:
282-
background_html = f"""
283-
<p class="monitor-note">另有 {background_count} 个仅监控标的未在页面展开;完整 JSON 保留用于复盘。页面只展示推荐、观察和来源核验标的。</p>
284-
"""
285213
return f"""<!doctype html>
286214
<html lang="zh-CN">
287215
<head>
@@ -308,7 +236,6 @@ def render_report_html(report: dict[str, Any]) -> str:
308236
.final-card h3 span {{ font-size: .85rem; color: #0f766e; margin-left: 8px; }}
309237
.final-card header p {{ margin: 4px 0 12px; color: #57606a; }}
310238
.final-card p {{ line-height: 1.55; color: #334155; }}
311-
.position-note {{ background: #fff7ed; border: 1px solid #fed7aa; border-radius: 8px; padding: 10px 12px; }}
312239
.theme-candidates {{ background: #f0fdf4; border: 1px solid #bbf7d0; padding: 16px; margin-bottom: 20px; border-radius: 8px; }}
313240
.candidate-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; }}
314241
.candidate-card {{ background: #fff; border: 1px solid #bbf7d0; border-radius: 8px; padding: 14px; }}
@@ -346,8 +273,7 @@ def render_report_html(report: dict[str, Any]) -> str:
346273
<span class="pill">受众:{html.escape(report['audience_scope'])}</span>
347274
<span class="pill">AI 状态:{html.escape(str(report['summary']['ai_regime']))}</span>
348275
<span class="pill">来源:{html.escape(source_mode)}</span>
349-
<span class="pill">主题候选:{report['summary'].get('theme_first_candidate_count', 0)}</span>
350-
<span class="pill">推荐数:{report['summary']['recommendation_count']}</span>
276+
<span class="pill">最终推荐:{len(report.get('final_decisions', {}).get('recommendations', []))}</span>
351277
</div>
352278
</section>
353279
<section class="policy">
@@ -356,15 +282,6 @@ def render_report_html(report: dict[str, Any]) -> str:
356282
</section>
357283
{warning_html}
358284
{final_decisions_html}
359-
{theme_candidates_html}
360-
{theme_momentum_html}
361-
<section class="recommendation-section">
362-
<h2>推荐与观察</h2>
363-
<p>这里只展开已进入推荐、观察或来源核验的标的;纯监控标的不作为本期结论展示。
364-
“先核验来源”表示低置信来源不能直接升级。</p>
365-
</section>
366-
{''.join(recommendation_cards)}
367-
{background_html}
368285
</main>
369286
</body>
370287
</html>
@@ -376,14 +293,13 @@ def render_index_html(reports: list[dict[str, Any]]) -> str:
376293
for report in sorted(reports, key=lambda item: item["as_of"], reverse=True):
377294
filename = report_filename(report)
378295
top_symbols = ", ".join(report["summary"].get("top_recommended_symbols", []))
379-
theme_candidate_symbols = ", ".join(report["summary"].get("top_theme_candidate_symbols", []))
380296
top_themes = format_theme_ids(report["summary"].get("top_theme_ids", []))
381297
source_mode = str(report["summary"].get("source_mode", "unknown"))
382298
items.append(
383299
f"""
384300
<li>
385301
<a href="{html.escape(filename)}">{html.escape(report['as_of'])} {html.escape(cadence_label(report))}复盘</a>
386-
<span>来源:{html.escape(source_mode)}主题{html.escape(top_themes or '无')}主题候选:{html.escape(theme_candidate_symbols or '无')};推荐{html.escape(top_symbols or '无')}</span>
302+
<span>来源:{html.escape(source_mode)}主要信号{html.escape(top_themes or '无')}最终推荐{html.escape(top_symbols or '无')}</span>
387303
</li>
388304
"""
389305
)
@@ -431,12 +347,11 @@ def render_feed_xml(reports: list[dict[str, Any]], *, site_url: str, feed_title:
431347
ET.SubElement(item, "guid").text = link
432348
ET.SubElement(item, "pubDate").text = format_datetime(report["generated_at"])
433349
top_symbols = ", ".join(report["summary"].get("top_recommended_symbols", []))
434-
theme_candidate_symbols = ", ".join(report["summary"].get("top_theme_candidate_symbols", []))
435350
top_themes = format_theme_ids(report["summary"].get("top_theme_ids", []))
436351
source_mode = str(report["summary"].get("source_mode", "unknown"))
437352
ET.SubElement(item, "description").text = (
438353
f"模式={report['mode']};来源={source_mode};主题={top_themes or '无'};"
439-
f"主题候选={theme_candidate_symbols or '无'};推荐={top_symbols or '无'}。"
354+
f"最终推荐={top_symbols or '无'}。"
440355
"非个性化模型输出;不包含下单、仓位配置或账户级建议。"
441356
)
442357
rss = ET.Element("rss", {"version": "2.0"})

tests/test_advisory_report.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -349,10 +349,9 @@ def test_theme_momentum_snapshot_is_display_context_not_rating_input(tmp_path: P
349349

350350
markdown = render_markdown(report_with_theme)
351351
assert "## 本期最终结论" in markdown
352-
assert "## 主题候选(解释材料,不是最终推荐)" in markdown
353-
assert "为什么入选" in markdown
354-
assert "买入数量、仓位" in markdown
355-
assert "## 主题动量" in markdown
356-
assert "HBM / 存储" in markdown
357-
assert "事件证据" in markdown
358-
assert "DELL" in markdown
352+
assert "股票背景" in markdown
353+
assert "推荐理由" in markdown
354+
assert "## 主题候选(解释材料,不是最终推荐)" not in markdown
355+
assert "为什么入选" not in markdown
356+
assert "买多少" not in markdown
357+
assert "## 主题动量" not in markdown

tests/test_publisher.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def build_sample_report() -> dict:
1616
political_events_path=ROOT / "examples/political_events.example.csv",
1717
political_watchlist_path=ROOT / "examples/political_watchlist.example.csv",
1818
ai_signal_path=ROOT / "examples/ai_long_horizon_signal.example.json",
19+
theme_momentum_path=ROOT / "examples/theme_momentum_snapshot.example.json",
1920
)
2021

2122

@@ -24,12 +25,13 @@ def test_render_report_html_contains_policy_boundary() -> None:
2425

2526
assert "政策边界" in html
2627
assert "不允许下单" in html
27-
assert "EVT1" in html
2828
assert "2-12周" in html
2929
assert "来源模式:fixture" in html
30-
assert "仅监控标的未在页面展开" in html
30+
assert "本期最终结论" in html
31+
assert "股票背景" in html
3132
assert "背景跟踪(非推荐" not in html
3233
assert "复核清单" not in html
34+
assert "主题候选(解释材料,不是最终推荐)" not in html
3335

3436

3537
def test_render_feed_xml_contains_report_item() -> None:
@@ -61,6 +63,7 @@ def test_render_report_html_includes_theme_momentum_context() -> None:
6163
political_events_path=ROOT / "examples/political_events.example.csv",
6264
political_watchlist_path=ROOT / "examples/political_watchlist.example.csv",
6365
ai_signal_path=ROOT / "examples/ai_long_horizon_signal.example.json",
66+
theme_momentum_path=ROOT / "examples/theme_momentum_snapshot.example.json",
6467
)
6568
report["summary"]["top_theme_ids"] = ["hbm_memory"]
6669
report["theme_momentum"] = {
@@ -105,18 +108,17 @@ def test_render_report_html_includes_theme_momentum_context() -> None:
105108

106109
assert "本期最终结论" in html
107110
assert "最终推荐" in html
108-
assert "买多少" in html
109-
assert "不提供买入金额" in html
110111
assert "AI信号仓库" in html
111112
assert "AiLongHorizonSignalPipelines" in html
112-
assert "主题候选(解释材料,不是最终推荐)" in html
113-
assert "最终推荐以上方“本期最终结论”为准" in html
114-
assert "+28.0%" in html
115-
assert "为什么入选" in html
116-
assert "主题动量" in html
117-
assert "HBM / 存储" in html
118-
assert "事件证据" in html
119-
assert "暂无明确事件催化" in html
113+
assert "股票背景" in html
114+
assert "推荐理由" in html
115+
assert "做什么" not in html
116+
assert "为什么有前景" not in html
117+
assert "买多少" not in html
118+
assert "仓位/数量" not in html
119+
assert "主题候选(解释材料,不是最终推荐)" not in html
120+
assert "主题动量" not in html
121+
assert "事件证据" not in html
120122
assert "hbm_memory" not in html
121123
assert "MU" in html
122124

@@ -156,13 +158,12 @@ def test_format_telegram_message_contains_themes_policy_and_link() -> None:
156158

157159
assert "量化模型推荐" in message
158160
assert "本期最终推荐" in message
159-
assert "买多少:不提供" in message
160161
assert "AI信号仓库" in message
161-
assert "主题候选(不是最终推荐)" in message
162-
assert "近3月" in message
163-
assert "事件证据" in message
164-
assert "为什么" in message
165-
assert "HBM / 存储" in message
162+
assert "股票背景" in message
163+
assert "推荐理由" in message
164+
assert "买多少" not in message
165+
assert "主题候选" not in message
166+
assert "事件证据" not in message
166167
assert "MU" in message
167168
assert "不包含下单" in message
168169
assert "https://example.com/advisor/2026-05-30-weekly-model-recommendations.html" in message

0 commit comments

Comments
 (0)