Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/publish_advisory_site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ jobs:
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
NOTIFY_LANG: ${{ vars.NOTIFY_LANG || 'zh' }}
SITE_URL: ${{ github.event.inputs.site_url || 'https://quantstrategylab.github.io/QuantAdvisorResearch' }}
REPORT_PATH: ${{ steps.build_report.outputs.report_path }}
run: |
Expand Down
7 changes: 5 additions & 2 deletions docs/notification_format.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
repository secrets, `scripts/notify_advisory_telegram.py` sends a short summary
after a successful Pages deployment. Missing secrets skip notification without
failing the publish job.
- Locale: the public HTML/RSS/Telegram copy defaults to Simplified Chinese
(`zh-CN`). JSON contract keys remain in English for downstream stability.
- Locale: public HTML/RSS defaults to Simplified Chinese (`zh-CN`). Telegram
accepts `NOTIFY_LANG=zh|en` and defaults to Chinese. English Telegram keeps
stable decision facts in English and links to the source-language report,
rather than mixing untranslated natural-language rationale into the message.
JSON contract keys remain in English for downstream stability.

## Boundary

Expand Down
1 change: 1 addition & 0 deletions docs/notification_format.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,6 @@ Subject: 智慧投顾研究周度复盘 - 2026-05-30
- GitHub Pages:`.github/workflows/publish_advisory_site.yml` 发布 HTML、JSON、Markdown 和 RSS。
- RSS:`scripts/publish_advisory_site.py` 生成 `feed.xml`。
- Telegram:可选。如果仓库 secrets 配置了 `TELEGRAM_BOT_TOKEN` 和 `TELEGRAM_CHAT_ID`,`scripts/notify_advisory_telegram.py` 会在 Pages 部署成功后发送短摘要;如果缺少任一 secret,会跳过通知但不让发布失败。
- 通知语言:Telegram 支持 `NOTIFY_LANG=zh|en`,默认中文。英文通知只展示可稳定翻译的决策事实并链接到原始报告,不会把尚未英文化的中文理由混进英文消息;公开 HTML/RSS 目前仍默认简体中文。

通知默认只展示最终推荐、股票背景、推荐理由、周期、综合分和完整报告链接;不能包含订单、目标仓位、目标股数、账户适当性或账户级配置建议。主题候选只保留在 JSON/Markdown 审计材料中,不进入默认通知摘要。
7 changes: 6 additions & 1 deletion scripts/notify_advisory_telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@ def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Send a non-personalized advisory report summary to Telegram.")
parser.add_argument("--report", required=True, help="Advisory report JSON path")
parser.add_argument("--site-url", default="https://quantstrategylab.github.io/QuantAdvisorResearch")
parser.add_argument(
"--lang",
default=os.environ.get("NOTIFY_LANG", "zh"),
help="Notification language: zh or en. Defaults to NOTIFY_LANG or zh.",
)
parser.add_argument("--dry-run", action="store_true", help="Print the message instead of sending")
args = parser.parse_args(argv)

report = json.loads(Path(args.report).read_text(encoding="utf-8"))
message = format_telegram_message(report, site_url=args.site_url)
message = format_telegram_message(report, site_url=args.site_url, lang=args.lang)
if args.dry_run:
print(message)
return 0
Expand Down
65 changes: 65 additions & 0 deletions src/quant_advisor_research/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@
from .publisher import cadence_label, report_filename


def notification_locale(value: object | None = None) -> str:
"""Normalize the compact operator-notification locale."""

return "zh" if str(value or "zh").strip().lower().replace("_", "-").startswith("zh") else "en"


def _english_cadence_label(report: dict[str, Any]) -> str:
cadence = str(report.get("cadence", "")).strip().lower()
return {"weekly": "Weekly", "monthly": "Monthly"}.get(cadence, cadence.title() or "Advisory")


def _english_horizon_label(item: dict[str, Any]) -> str:
horizon = str(item.get("primary_horizon", "")).strip().lower()
return {"short": "Short term", "medium": "Medium term", "long": "Long term"}.get(
horizon,
horizon.replace("_", " ").title() or "Unspecified horizon",
)


def report_public_url(report: dict[str, Any], *, site_url: str) -> str:
return f"{site_url.rstrip('/')}/{quote(report_filename(report))}"

Expand Down Expand Up @@ -51,6 +70,40 @@ def _format_final_decisions(report: dict[str, Any]) -> list[str]:
return lines


def _format_final_decisions_en(report: dict[str, Any]) -> list[str]:
"""Render an English summary without embedding Chinese source prose.

Recommendation background and rationale are produced as Chinese natural
language in the report artifact today. An English notification therefore
keeps only stable identifiers and numeric decision facts, then points the
reader to the full report instead of presenting a mixed-language summary.
"""

decisions = report.get("final_decisions", {})
picks = decisions.get("recommendations", [])
if not picks:
return ["Recommendations: none for this period"]
lines = ["Recommendations:"]
for item in picks:
lines.append(
"- {symbol} | {horizon} | Composite score={score}".format(
symbol=item.get("symbol", ""),
horizon=_english_horizon_label(item),
score=display_number(item.get("combined_score")),
)
)
buckets = decisions.get("horizon_buckets", {})
lines.append(
"Horizons: short={short}; medium={medium}; long={long}".format(
short=", ".join(buckets.get("short", [])) or "none",
medium=", ".join(buckets.get("medium", [])) or "none",
long=", ".join(buckets.get("long", [])) or "none",
)
)
lines.append("Rationale and source-language background are available in the linked report.")
return lines


def _format_theme_candidates(report: dict[str, Any], *, limit: int) -> list[str]:
candidates = report.get("theme_first_candidates", [])[:limit]
if not candidates:
Expand Down Expand Up @@ -101,7 +154,19 @@ def format_telegram_message(
site_url: str,
max_recommendations: int = 8,
max_themes: int = 5,
lang: str | None = None,
) -> str:
locale = notification_locale(lang)
if locale == "en":
lines = [
f"Quant Advisor Research | {_english_cadence_label(report)} | {report.get('as_of', '')}",
"",
*_format_final_decisions_en(report),
"",
f"Full report: {report_public_url(report, site_url=site_url)}",
]
return "\n".join(str(line) for line in lines if line is not None)

lines = [
f"智慧投顾研究系统 | {cadence_label(report)} | {report.get('as_of', '')}",
"",
Expand Down
32 changes: 32 additions & 0 deletions tests/test_publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from copy import deepcopy
from pathlib import Path
import re

from quant_advisor_research.advisory_report import build_advisory_report, write_json
from quant_advisor_research.publisher import (
Expand Down Expand Up @@ -492,3 +493,34 @@ def test_format_telegram_message_is_direct_and_links_report() -> None:
assert "不包含下单" not in message
assert "MU" in message
assert "https://example.com/advisor/2026-05-30-weekly-model-recommendations.html" in message


def test_format_telegram_message_can_render_an_english_summary_without_chinese_prose() -> None:
from quant_advisor_research.notifications import format_telegram_message

report = build_sample_report()
report["final_decisions"] = {
"recommendations": [
{
"symbol": "MU",
"primary_horizon": "medium",
"primary_horizon_label": "中线",
"combined_score": 0.82,
"business_summary": "科技 / HBM / 存储",
"prospect_summary": "中文理由不应混进英文通知。",
}
],
"horizon_buckets": {"short": [], "medium": ["MU"], "long": []},
}

message = format_telegram_message(
report,
site_url="https://example.com/advisor",
lang="en-US",
)

assert "Quant Advisor Research | Weekly | 2026-05-30" in message
assert "MU | Medium term | Composite score=0.82" in message
assert "Rationale and source-language background" in message
assert "Full report: https://example.com/advisor/2026-05-30-weekly-model-recommendations.html" in message
assert not re.search(r"[\u4e00-\u9fff]", message)
Loading