mirror of
https://github.com/ZhuLinsen/daily_stock_analysis.git
synced 2026-10-06 15:33:33 +08:00
release: prepare v3.14.0
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "daily-stock-analysis-desktop",
|
||||
"version": "3.12.0",
|
||||
"version": "3.14.0",
|
||||
"private": true,
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
+52
-20
@@ -10,26 +10,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
## [Unreleased]
|
||||
|
||||
<!-- 新条目格式:- [类型] 描述(类型取值:新功能/改进/修复/文档/测试/chore)-->
|
||||
- [新功能] Web 首页历史报告区新增重新分析入口,支持基于原始 prompt 重做同一只股票同日期的分析
|
||||
<!-- 每条独立一行追加到本段末尾,无需分类标题,合并时冲突最小 -->
|
||||
- [新功能] Windows/macOS 桌面端新增 GitHub Release 更新提醒,启动后自动检测新版本并支持从设置页手动检查后跳转下载页
|
||||
- [修复] Pipeline Agent 5 个 K 线工具(get_daily_history / analyze_trend / calculate_ma / get_volume_analysis / analyze_pattern)改为 DB-first 加载,消除同一只股票 9x5=45 次重复 HTTP 请求 (Fixes #1066)
|
||||
- [修复] Pipeline Agent 执行前按需预热 240 天 K 线历史到 DB,正常情况下 K 线工具调用无需重复网络请求
|
||||
- [修复] 冻结 target_date 通过 ContextVar 透传到 Pipeline Agent K 线工具线程,消除跨收盘边界时间漂移
|
||||
- [修复] 修复 Windows 桌面端转抄后端 stdout/stderr 时中文日志可能乱码的问题,统一优先使用 UTF-8 并兼容本地代码页回退
|
||||
- [改进] Docker 发布工作流收敛为更清晰的正式发布与手动补发链路,并统一官方 Docker Hub 镜像名为 `zhulinsen/daily_stock_analysis`
|
||||
- [文档] 补充官方镜像拉取、`docker run` 用法与 `.env` / 数据目录映射说明,不再仅覆盖 Compose 部署路径
|
||||
- [改进] Agent 日线工具优先复用本地缓存,并持久化新获取的日线与新闻情报
|
||||
- [修复] GitHub Actions 每日分析工作流补齐 `LLM_CHANNELS`、多 Key 与常用 `LLM_<NAME>_*` 渠道变量透传,避免本地可用的多模型配置在云端定时任务中失效(Fixes #1063, #872)
|
||||
- [文档] 修正 `feishu_sender.py` 中飞书自定义机器人 Webhook 消息格式示例为 interactive card JSON,并补充飞书自动化 Webhook 触发器配置教程(参数 JSON 与 `card.elements[0].text.content` 字段映射)。
|
||||
- [修复] 历史报告详情接口修正 `change_pct` 取值:使用 `is None` 判断避免把 0.0(平盘)当作缺失值丢弃,移除错误的 `change_60d` 兜底,并在 `enhanced_context.realtime` 缺涨跌幅时回退到 `realtime_quote_raw.change_pct` / `pct_chg`,避免历史详情页“不显示涨跌幅” (Fixes #1084)
|
||||
- [修复] DeepSeek 官方渠道预设与示例配置同步到 V4,保留 legacy `deepseek-chat` 默认值并增加废弃提示,同时修正模型发现后旧运行时选择导致保存失败的问题 (Fixes #1108, #1109)
|
||||
- [文档] 优化根 README 结构,保留功能特性、技术栈、快速开始、推送效果、Web、Agent、赞助商和新闻源链接入口,将细配置、交易纪律和基本面语义收口到完整指南,并将 Docker 徽章指向官方镜像页
|
||||
- [文档] 同步英文与繁中 README 的精简入口结构,并补齐完整指南中的 LLM 用量 API 与持仓管理说明
|
||||
- [文档] 调整 AI 协作与 PR 模板中的 README 维护规则,明确 README 非必要不更新,细节优先进入专题文档
|
||||
- [修复] 桌面端打包链路新增 `scripts/check_static_assets.py` 静态资源一致性检查,并在 `build-backend(.ps1|-macos.sh)` 的源 `static/` 与 PyInstaller 产物里各跑一次;同时在后端启动时校验 `index.html` 引用的 `/assets/*.js`/`*.css` 是否真实存在,发现错配时直接在 `logs/desktop.log` 打印明确错误,避免重现 Release 包打开后白屏(Refs #1064 / #1065 / #1050)
|
||||
- [改进] 后端 `/assets/*` 由显式路由托管,资源缺失时返回与请求扩展名匹配的 `text/javascript` / `text/css` 404,而不是被默认 JSON 错误响应误导排查(Refs #1064)
|
||||
- [修复] `kimi-k2.6` 调用自动使用模型要求的 `temperature=1.0`,避免主分析、大盘复盘或 Agent 仍传默认温度导致请求被拒绝(Fixes #1102)
|
||||
|
||||
## [3.14.0] - 2026-04-26
|
||||
|
||||
### 发布亮点
|
||||
|
||||
- 📊 **大盘复盘升级为盘后工作台式结构** — A 股复盘固定输出盘面温度、指数明细、板块 Top 表、新闻催化、明日交易计划和风险提示,减少纯文字复盘的重复与空泛。
|
||||
- 🖥️ **桌面端新增 GitHub Release 更新提醒** — Windows/macOS 桌面端启动后自动检测新版本,也可从设置页手动检查并跳转下载页。
|
||||
- 🤖 **Pipeline Agent 数据加载大幅降噪** — K 线工具改为 DB-first 并预热 240 天历史数据,避免同一只股票重复 HTTP 请求。
|
||||
- 🐳 **Docker 发布链路整理** — 发布工作流收敛为正式发布与手动补发两条路径,官方 Docker Hub 镜像名统一为 `zhulinsen/daily_stock_analysis`。
|
||||
- 🔧 **LLM 渠道与 DeepSeek V4 配置补强** — GitHub Actions 定时分析补齐多渠道变量透传,DeepSeek 官方渠道预设与示例同步到 V4。
|
||||
- 🧩 **桌面端静态资源一致性校验** — 打包链路和运行时都能更早发现静态资源错配,降低 Release 包白屏排查成本。
|
||||
|
||||
### 新功能
|
||||
|
||||
- 🏠 **Web 首页历史报告区新增重新分析入口** — 支持基于原始 prompt 重做同一只股票同日期的分析。
|
||||
- 🖥️ **Windows/macOS 桌面端新增 GitHub Release 更新提醒** — 启动后自动检测新版本,并支持从设置页手动检查后跳转下载页。
|
||||
|
||||
### 改进
|
||||
|
||||
- 📊 **A 股大盘复盘报告改为结构化盘后工作台版式** — 固定输出盘面温度、指数明细、板块 Top 表、新闻催化和明日交易计划。
|
||||
- 🐳 **Docker 发布工作流收敛** — 更清晰地区分正式发布与手动补发链路,并统一官方 Docker Hub 镜像名为 `zhulinsen/daily_stock_analysis`。
|
||||
- 🤖 **Agent 日线工具优先复用本地缓存** — 同时持久化新获取的日线与新闻情报,减少重复数据源调用。
|
||||
|
||||
### 修复
|
||||
|
||||
- 🤖 **Pipeline Agent K 线工具 DB-first 加载** — `get_daily_history` / `analyze_trend` / `calculate_ma` / `get_volume_analysis` / `analyze_pattern` 改为优先读取本地 DB,消除同一只股票 9x5=45 次重复 HTTP 请求(Fixes #1066)。
|
||||
- 🤖 **Pipeline Agent 执行前按需预热 240 天 K 线历史到 DB** — 正常情况下 K 线工具调用无需重复网络请求。
|
||||
- 🕒 **冻结 `target_date` 并通过 ContextVar 透传到 Pipeline Agent K 线工具线程** — 消除跨收盘边界时间漂移。
|
||||
- 🪟 **Windows 桌面端后端日志转抄编码修复** — 转抄 stdout/stderr 时优先使用 UTF-8,并兼容本地代码页回退,避免中文日志乱码。
|
||||
- ⚙️ **GitHub Actions 每日分析工作流补齐 LLM 渠道变量透传** — 支持 `LLM_CHANNELS`、多 Key 与常用 `LLM_<NAME>_*`,避免本地可用的多模型配置在云端定时任务中失效(Fixes #1063, #872)。
|
||||
- 📈 **历史报告详情接口修正 `change_pct` 取值** — 使用 `is None` 判断避免把 0.0(平盘)当作缺失值丢弃,移除错误的 `change_60d` 兜底,并在缺失时回退到原始实时行情字段(Fixes #1084)。
|
||||
- 🔧 **DeepSeek 官方渠道预设与示例配置同步到 V4** — 保留 legacy `deepseek-chat` 默认值并增加废弃提示,同时修正模型发现后旧运行时选择导致保存失败的问题(Fixes #1108, #1109)。
|
||||
- 🧩 **桌面端打包链路新增静态资源一致性检查** — `scripts/check_static_assets.py` 会在源 `static/` 与 PyInstaller 产物中校验 `index.html` 引用的资源是否真实存在,运行时也会在错配时写入明确日志,避免重现 Release 包打开后白屏(Refs #1064 / #1065 / #1050)。
|
||||
- 🧩 **后端 `/assets/*` 改为显式路由托管** — 资源缺失时返回与请求扩展名匹配的 `text/javascript` / `text/css` 404,减少默认 JSON 错误响应带来的排查误导(Refs #1064)。
|
||||
- 🌙 **`kimi-k2.6` 自动使用固定温度** — 主分析、大盘复盘和 Agent 调用该模型时自动使用 `temperature=1.0`,避免模型拒绝默认温度请求(Fixes #1102)。
|
||||
|
||||
### 文档
|
||||
|
||||
- 🐳 **补充官方 Docker 镜像使用说明** — 增加镜像拉取、`docker run` 用法与 `.env` / 数据目录映射说明,不再只覆盖 Compose 部署路径。
|
||||
- 📨 **修正飞书自定义机器人 Webhook 示例** — `feishu_sender.py` 中的示例改为 interactive card JSON,并补充飞书自动化 Webhook 触发器配置教程。
|
||||
- 📚 **优化根 README 结构** — 保留首页级功能特性、技术栈、快速开始、推送效果、Web、Agent、赞助商和新闻源入口,将细配置、交易纪律和基本面语义收口到完整指南,并将 Docker 徽章指向官方镜像页。
|
||||
- 🌐 **同步英文与繁中 README 的精简入口结构** — 同时补齐完整指南中的 LLM 用量 API 与持仓管理说明。
|
||||
- 🤝 **调整 AI 协作与 PR 模板中的 README 维护规则** — 明确 README 非必要不更新,细节优先进入专题文档。
|
||||
|
||||
### 测试
|
||||
|
||||
- 🧪 **稳定市场复盘相关测试的 LiteLLM stub 行为** — 避免本机安装的 LiteLLM 在测试收集顺序变化时影响市场复盘单元测试。
|
||||
- 🧪 **pytest 默认跳过前端依赖目录** — 本地存在 `apps/dsa-web/node_modules` 时不再被后端测试递归扫描,避免发布前 gate 被无关目录拖慢。
|
||||
|
||||
## [3.13.0] - 2026-04-21
|
||||
|
||||
@@ -1294,7 +1323,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
---
|
||||
|
||||
[Unreleased]: https://github.com/ZhuLinsen/daily_stock_analysis/compare/v3.11.0...HEAD
|
||||
[Unreleased]: https://github.com/ZhuLinsen/daily_stock_analysis/compare/v3.14.0...HEAD
|
||||
[3.14.0]: https://github.com/ZhuLinsen/daily_stock_analysis/compare/v3.13.0...v3.14.0
|
||||
[3.13.0]: https://github.com/ZhuLinsen/daily_stock_analysis/compare/v3.12.0...v3.13.0
|
||||
[3.12.0]: https://github.com/ZhuLinsen/daily_stock_analysis/compare/v3.11.0...v3.12.0
|
||||
[3.11.0]: https://github.com/ZhuLinsen/daily_stock_analysis/compare/v3.10.1...v3.11.0
|
||||
[3.10.1]: https://github.com/ZhuLinsen/daily_stock_analysis/compare/v3.10.0...v3.10.1
|
||||
[3.10.0]: https://github.com/ZhuLinsen/daily_stock_analysis/compare/v3.9.0...v3.10.0
|
||||
|
||||
@@ -308,6 +308,7 @@ daily_stock_analysis/
|
||||
> - TickFlow 能力按套餐权限分层:有限权限套餐仍可使用主指数查询;支持 `CN_Equity_A` 标的池查询的套餐才会启用 TickFlow 市场统计。
|
||||
> - 官方 quickstart 已文档化 `quotes.get(universes=["CN_Equity_A"])`,但线上 smoke test 进一步确认:`TICKFLOW_API_KEY` 不等于一定具备该权限,且 `quotes.get(symbols=[...])` 单次存在标的数量限制。
|
||||
> - TickFlow 实际返回的 `change_pct` / `amplitude` 为比例值;系统已在接入层统一转换为百分比值,确保与现有数据源字段语义一致。
|
||||
> - A 股大盘复盘报告采用盘后工作台式结构:固定包含盘面温度、指数明细、板块 Top 表、新闻催化、明日交易计划和风险提示;若部分数据源缺失,则保留可用区块并在对应位置降级展示。
|
||||
> - 字段契约:
|
||||
> - `fundamental_context.belong_boards` = 个股关联板块列表(当前仅 A 股写入;无数据时为 `[]`);
|
||||
> - `fundamental_context.boards.data` = `sector_rankings`(板块涨跌榜,结构 `{top, bottom}`);
|
||||
|
||||
@@ -300,6 +300,7 @@ Default schedule: Every weekday at **18:00 (Beijing Time)** automatic execution.
|
||||
> - TickFlow behavior is capability-based rather than just key-based: limited plans can still enhance main CN indices, while plans with `CN_Equity_A` universe query support also enhance market breadth.
|
||||
> - The official quickstart documents `quotes.get(universes=["CN_Equity_A"])`, but online smoke tests confirmed two additional real-world constraints: universe access depends on plan permissions, and `quotes.get(symbols=[...])` has a per-request symbol limit.
|
||||
> - TickFlow currently returns `change_pct` / `amplitude` as ratio values; this integration normalizes them to the project's percent convention so they match AkShare / Tushare / efinance semantics.
|
||||
> - CN market review reports now use a post-market workstation layout with fixed market-temperature, index detail, sector Top tables, news catalysts, next-session plan, and risk sections. Missing data sources degrade by omitting or simplifying only the affected block.
|
||||
> - Per-stock analysis, realtime quote priority, and sector rankings fallback remain unchanged.
|
||||
|
||||
---
|
||||
|
||||
@@ -23,6 +23,17 @@ testpaths = .
|
||||
python_files = test_*.py
|
||||
python_functions = test_*
|
||||
addopts = -v --tb=short
|
||||
norecursedirs =
|
||||
.git
|
||||
__pycache__
|
||||
.env
|
||||
venv
|
||||
.venv
|
||||
.venv*
|
||||
build
|
||||
dist
|
||||
node_modules
|
||||
*/node_modules/*
|
||||
markers =
|
||||
unit: fast offline unit tests
|
||||
integration: service-level integration tests without external network dependency
|
||||
|
||||
+218
-69
@@ -35,9 +35,11 @@ _ENGLISH_SECTION_PATTERNS = {
|
||||
}
|
||||
|
||||
_CHINESE_SECTION_PATTERNS = {
|
||||
"market_summary": r"###\s*一、市场总结",
|
||||
"index_commentary": r"###\s*二、(?:指数点评|主要指数)",
|
||||
"sector_highlights": r"###\s*四、(?:热点解读|板块表现)",
|
||||
"market_summary": r"###\s*一、(?:盘面总览|市场总结)",
|
||||
"index_commentary": r"###\s*二、(?:指数结构|指数点评|主要指数)",
|
||||
"sector_highlights": r"###\s*三、(?:板块主线|热点解读|板块表现)",
|
||||
"funds_sentiment": r"###\s*四、(?:资金与情绪|资金动向)",
|
||||
"news_catalysts": r"###\s*五、(?:消息催化|后市展望)",
|
||||
}
|
||||
|
||||
|
||||
@@ -424,17 +426,23 @@ Focus on index trend, liquidity, and sector rotation to shape the next-session t
|
||||
if review:
|
||||
logger.info("[大盘] 复盘报告生成成功,长度: %d 字符", len(review))
|
||||
# Inject structured data tables into LLM prose sections
|
||||
return self._inject_data_into_review(review, overview)
|
||||
return self._inject_data_into_review(review, overview, news)
|
||||
else:
|
||||
logger.warning("[大盘] 大模型返回为空,使用模板报告")
|
||||
return self._generate_template_review(overview, news)
|
||||
|
||||
def _inject_data_into_review(self, review: str, overview: MarketOverview) -> str:
|
||||
def _inject_data_into_review(
|
||||
self,
|
||||
review: str,
|
||||
overview: MarketOverview,
|
||||
news: Optional[List] = None,
|
||||
) -> str:
|
||||
"""Inject structured data tables into the corresponding LLM prose sections."""
|
||||
# Build data blocks
|
||||
stats_block = self._build_stats_block(overview)
|
||||
indices_block = self._build_indices_block(overview)
|
||||
sector_block = self._build_sector_block(overview)
|
||||
news_block = self._build_news_block(news or [])
|
||||
patterns = (
|
||||
_ENGLISH_SECTION_PATTERNS
|
||||
if self._get_review_language() == "en"
|
||||
@@ -462,6 +470,13 @@ Focus on index trend, liquidity, and sector rotation to shape the next-session t
|
||||
sector_block,
|
||||
)
|
||||
|
||||
if news_block and "news_catalysts" in patterns:
|
||||
review = self._insert_after_section(
|
||||
review,
|
||||
patterns["news_catalysts"],
|
||||
news_block,
|
||||
)
|
||||
|
||||
return review
|
||||
|
||||
@staticmethod
|
||||
@@ -495,33 +510,44 @@ Focus on index trend, liquidity, and sector rotation to shape the next-session t
|
||||
f"Limit-up **{overview.limit_up_count}** / Limit-down **{overview.limit_down_count}** | "
|
||||
f"Turnover **{overview.total_amount:.0f}** ({self._get_turnover_unit_label()})"
|
||||
)
|
||||
score, label = self._build_market_temperature(overview)
|
||||
participation = overview.up_count + overview.down_count + overview.flat_count
|
||||
up_ratio = overview.up_count / participation if participation else 0.0
|
||||
limit_spread = overview.limit_up_count - overview.limit_down_count
|
||||
lines = [
|
||||
f"> 📈 上涨 **{overview.up_count}** 家 / 下跌 **{overview.down_count}** 家 / "
|
||||
f"平盘 **{overview.flat_count}** 家 | "
|
||||
f"涨停 **{overview.limit_up_count}** / 跌停 **{overview.limit_down_count}** | "
|
||||
f"成交额 **{overview.total_amount:.0f}** 亿"
|
||||
f"> **盘面温度**:{label} **{score}/100** {self._build_temperature_bar(score)}",
|
||||
"",
|
||||
"| 指标 | 数值 | 观察 |",
|
||||
"|------|------|------|",
|
||||
f"| 上涨/下跌/平盘 | {overview.up_count} / {overview.down_count} / {overview.flat_count} | 上涨占比 {up_ratio:.1%} |",
|
||||
f"| 涨停/跌停 | {overview.limit_up_count} / {overview.limit_down_count} | 涨跌停差 {limit_spread:+d} |",
|
||||
f"| 两市成交额 | {overview.total_amount:.0f} 亿 | {self._describe_turnover(overview.total_amount)} |",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
def _build_indices_block(self, overview: MarketOverview) -> str:
|
||||
"""构建指数行情表格(不含振幅)"""
|
||||
"""构建指数行情表格"""
|
||||
if not overview.indices:
|
||||
return ""
|
||||
if self._get_review_language() == "en":
|
||||
lines = [
|
||||
f"| Index | Last | Change % | Turnover ({self._get_turnover_unit_label()}) |",
|
||||
"|-------|------|----------|-----------------|",
|
||||
f"| Index | Last | Change % | Open | High | Low | Amplitude | Turnover ({self._get_turnover_unit_label()}) |",
|
||||
"|-------|------|----------|------|------|-----|-----------|-----------------|",
|
||||
]
|
||||
else:
|
||||
lines = [
|
||||
"| 指数 | 最新 | 涨跌幅 | 成交额(亿) |",
|
||||
"|------|------|--------|-----------|",
|
||||
"| 指数 | 最新 | 涨跌幅 | 开盘 | 最高 | 最低 | 振幅 | 成交额(亿) |",
|
||||
"|------|------|--------|------|------|------|------|-----------|",
|
||||
]
|
||||
for idx in overview.indices:
|
||||
arrow = "🔴" if idx.change_pct < 0 else "🟢" if idx.change_pct > 0 else "⚪"
|
||||
amount_raw = idx.amount or 0.0
|
||||
amount_str = self._format_turnover_value(amount_raw)
|
||||
lines.append(f"| {idx.name} | {idx.current:.2f} | {arrow} {idx.change_pct:+.2f}% | {amount_str} |")
|
||||
lines.append(
|
||||
f"| {idx.name} | {idx.current:.2f} | {arrow} {idx.change_pct:+.2f}% | "
|
||||
f"{self._format_optional_number(idx.open)} | {self._format_optional_number(idx.high)} | "
|
||||
f"{self._format_optional_number(idx.low)} | {self._format_optional_pct(idx.amplitude)} | {amount_str} |"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
def _build_sector_block(self, overview: MarketOverview) -> str:
|
||||
@@ -530,23 +556,145 @@ Focus on index trend, liquidity, and sector rotation to shape the next-session t
|
||||
return ""
|
||||
lines = []
|
||||
if overview.top_sectors:
|
||||
top = " | ".join(
|
||||
[f"**{s['name']}**({s['change_pct']:+.2f}%)" for s in overview.top_sectors[:5]]
|
||||
)
|
||||
if self._get_review_language() == "en":
|
||||
lines.append(f"> 🔥 Leaders: {top}")
|
||||
lines.extend([
|
||||
"#### Leading Sectors",
|
||||
"| Rank | Sector | Change |",
|
||||
"|------|--------|--------|",
|
||||
])
|
||||
else:
|
||||
lines.append(f"> 🔥 领涨: {top}")
|
||||
lines.extend([
|
||||
"#### 领涨板块 Top 5",
|
||||
"| 排名 | 板块 | 涨跌幅 |",
|
||||
"|------|------|--------|",
|
||||
])
|
||||
for rank, sector in enumerate(overview.top_sectors[:5], 1):
|
||||
lines.append(
|
||||
f"| {rank} | {sector.get('name', '-')} | {self._format_signed_pct(sector.get('change_pct'))} |"
|
||||
)
|
||||
if overview.bottom_sectors:
|
||||
bot = " | ".join(
|
||||
[f"**{s['name']}**({s['change_pct']:+.2f}%)" for s in overview.bottom_sectors[:5]]
|
||||
)
|
||||
if lines:
|
||||
lines.append("")
|
||||
if self._get_review_language() == "en":
|
||||
lines.append(f"> 💧 Laggards: {bot}")
|
||||
lines.extend([
|
||||
"#### Lagging Sectors",
|
||||
"| Rank | Sector | Change |",
|
||||
"|------|--------|--------|",
|
||||
])
|
||||
else:
|
||||
lines.append(f"> 💧 领跌: {bot}")
|
||||
lines.extend([
|
||||
"#### 领跌板块 Top 5",
|
||||
"| 排名 | 板块 | 涨跌幅 |",
|
||||
"|------|------|--------|",
|
||||
])
|
||||
for rank, sector in enumerate(overview.bottom_sectors[:5], 1):
|
||||
lines.append(
|
||||
f"| {rank} | {sector.get('name', '-')} | {self._format_signed_pct(sector.get('change_pct'))} |"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
def _build_news_block(self, news: List) -> str:
|
||||
"""Build a compact news catalyst table for the rendered report."""
|
||||
if not news:
|
||||
return ""
|
||||
if self._get_review_language() == "en":
|
||||
lines = [
|
||||
"#### News Catalysts",
|
||||
"| # | Headline | Signal |",
|
||||
"|---|----------|--------|",
|
||||
]
|
||||
else:
|
||||
lines = [
|
||||
"#### 近三日催化线索",
|
||||
"| 序号 | 事件/标题 | 关注点 |",
|
||||
"|------|-----------|--------|",
|
||||
]
|
||||
|
||||
for idx, item in enumerate(news[:5], 1):
|
||||
if hasattr(item, "title"):
|
||||
title = getattr(item, "title", "") or "-"
|
||||
snippet = getattr(item, "snippet", "") or ""
|
||||
else:
|
||||
title = item.get("title", "-") or "-"
|
||||
snippet = item.get("snippet", "") or ""
|
||||
title = self._escape_table_cell(str(title).strip()[:42])
|
||||
signal = self._escape_table_cell(str(snippet).strip().replace("\n", " ")[:58] or "-")
|
||||
lines.append(f"| {idx} | {title} | {signal} |")
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def _format_optional_number(value: float) -> str:
|
||||
return "N/A" if value in (None, 0, 0.0) else f"{value:.2f}"
|
||||
|
||||
@staticmethod
|
||||
def _format_optional_pct(value: float) -> str:
|
||||
return "N/A" if value in (None, 0, 0.0) else f"{value:.2f}%"
|
||||
|
||||
@staticmethod
|
||||
def _format_signed_pct(value: Any) -> str:
|
||||
try:
|
||||
numeric_value = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return "N/A"
|
||||
return f"{numeric_value:+.2f}%"
|
||||
|
||||
@staticmethod
|
||||
def _escape_table_cell(value: str) -> str:
|
||||
return value.replace("|", "\\|")
|
||||
|
||||
@staticmethod
|
||||
def _build_temperature_bar(score: int) -> str:
|
||||
filled = max(0, min(10, round(score / 10)))
|
||||
return "█" * filled + "░" * (10 - filled)
|
||||
|
||||
@staticmethod
|
||||
def _describe_turnover(total_amount: float) -> str:
|
||||
if total_amount >= 15000:
|
||||
return "高活跃度"
|
||||
if total_amount >= 9000:
|
||||
return "中等活跃"
|
||||
if total_amount > 0:
|
||||
return "缩量观望"
|
||||
return "暂无数据"
|
||||
|
||||
def _build_market_temperature(self, overview: MarketOverview) -> tuple[int, str]:
|
||||
participants = overview.up_count + overview.down_count
|
||||
breadth_score = 50
|
||||
if participants:
|
||||
breadth_score = int(overview.up_count / participants * 100)
|
||||
|
||||
index_changes = [idx.change_pct for idx in overview.indices if idx.change_pct is not None]
|
||||
index_score = 50
|
||||
if index_changes:
|
||||
avg_change = sum(index_changes) / len(index_changes)
|
||||
index_score = int(max(0, min(100, 50 + avg_change * 12)))
|
||||
|
||||
limit_total = overview.limit_up_count + overview.limit_down_count
|
||||
limit_score = 50
|
||||
if limit_total:
|
||||
limit_score = int(overview.limit_up_count / limit_total * 100)
|
||||
|
||||
score = int(round(breadth_score * 0.45 + index_score * 0.35 + limit_score * 0.20))
|
||||
if self._get_review_language() == "en":
|
||||
if score >= 70:
|
||||
label = "risk-on"
|
||||
elif score >= 55:
|
||||
label = "constructive"
|
||||
elif score >= 40:
|
||||
label = "mixed"
|
||||
else:
|
||||
label = "defensive"
|
||||
else:
|
||||
if score >= 70:
|
||||
label = "强势"
|
||||
elif score >= 55:
|
||||
label = "偏暖"
|
||||
elif score >= 40:
|
||||
label = "震荡"
|
||||
else:
|
||||
label = "偏弱"
|
||||
return score, label
|
||||
|
||||
def _build_review_prompt(self, overview: MarketOverview, news: List) -> str:
|
||||
"""构建复盘报告 Prompt"""
|
||||
review_language = self._get_review_language()
|
||||
@@ -689,13 +837,15 @@ Output the report content directly, no extra commentary.
|
||||
"""
|
||||
|
||||
# A 股场景使用中文提示语
|
||||
return f"""你是一位专业的A/H/美股市场分析师,请根据以下数据生成一份简洁的大盘复盘报告。
|
||||
return f"""你是一位专业的A/H/美股市场分析师,请根据以下数据生成一份结构化大盘复盘报告。
|
||||
|
||||
【重要】输出要求:
|
||||
- 必须输出纯 Markdown 文本格式
|
||||
- 禁止输出 JSON 格式
|
||||
- 禁止输出代码块
|
||||
- emoji 仅在标题处少量使用(每个标题最多1个)
|
||||
- 报告要像交易员盘后工作台:先给结论,再按数据表、主线、催化、计划展开
|
||||
- 不要重复列出已由系统注入的表格数据;正文负责解释表格背后的含义
|
||||
|
||||
---
|
||||
|
||||
@@ -724,26 +874,28 @@ Output the report content directly, no extra commentary.
|
||||
|
||||
## {overview.date} 大盘复盘
|
||||
|
||||
### 一、市场总结
|
||||
(2-3句话概括今日市场整体表现,包括指数涨跌、成交量变化)
|
||||
> 一句话给出今日市场状态、核心矛盾和明日优先观察方向。
|
||||
|
||||
### 二、指数点评
|
||||
({self._get_index_hint()})
|
||||
### 一、盘面总览
|
||||
(2-3句话概括指数、涨跌家数、成交额和情绪温度,明确“强势/偏暖/震荡/偏弱”判断)
|
||||
|
||||
### 三、资金动向
|
||||
(解读成交额流向的含义)
|
||||
### 二、指数结构
|
||||
({self._get_index_hint()},说明谁在护盘、谁在拖累,以及关键支撑/压力)
|
||||
|
||||
### 四、热点解读
|
||||
(分析领涨领跌板块背后的逻辑和驱动因素)
|
||||
### 三、板块主线
|
||||
(分析领涨/领跌板块背后的逻辑、持续性和是否形成主线)
|
||||
|
||||
### 五、后市展望
|
||||
(结合当前走势和新闻,给出明日市场预判)
|
||||
### 四、资金与情绪
|
||||
(解读成交额、涨跌停结构、市场宽度和风险偏好)
|
||||
|
||||
### 六、风险提示
|
||||
(需要关注的风险点)
|
||||
### 五、消息催化
|
||||
(结合近三日新闻,提炼真正影响明日交易的催化或扰动)
|
||||
|
||||
### 七、策略计划
|
||||
(给出进攻/均衡/防守结论,对应仓位建议,并给出一个触发失效条件;最后补充“建议仅供参考,不构成投资建议”。)
|
||||
### 六、明日交易计划
|
||||
(给出进攻/均衡/防守结论、仓位区间、关注方向、回避方向和一个触发失效条件)
|
||||
|
||||
### 七、风险提示
|
||||
(列出需要关注的风险点;最后补充“建议仅供参考,不构成投资建议”。)
|
||||
|
||||
---
|
||||
|
||||
@@ -828,40 +980,37 @@ Market conditions can change quickly. The data above is for reference only and d
|
||||
"""
|
||||
return report
|
||||
|
||||
stats_section = ""
|
||||
if self.profile.has_market_stats:
|
||||
stats_section = f"""
|
||||
### 三、涨跌统计
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| 上涨家数 | {overview.up_count} |
|
||||
| 下跌家数 | {overview.down_count} |
|
||||
| 涨停 | {overview.limit_up_count} |
|
||||
| 跌停 | {overview.limit_down_count} |
|
||||
| 两市成交额 | {overview.total_amount:.0f}亿 |
|
||||
"""
|
||||
sector_section = ""
|
||||
if self.profile.has_sector_rankings and (top_text or bottom_text):
|
||||
sector_section = f"""
|
||||
### 四、板块表现
|
||||
- **领涨**: {top_text}
|
||||
- **领跌**: {bottom_text}
|
||||
"""
|
||||
market_label = "A股" if self.region == "cn" else "美股"
|
||||
strategy_summary = self._get_strategy_markdown_block(template_language)
|
||||
dashboard_block = self._build_stats_block(overview)
|
||||
indices_block = self._build_indices_block(overview)
|
||||
sector_block = self._build_sector_block(overview)
|
||||
return f"""## {overview.date} 大盘复盘
|
||||
|
||||
### 一、市场总结
|
||||
今日{market_label}市场整体呈现**{market_mood}**态势。
|
||||
> 今日{market_label}市场整体呈现**{market_mood}**态势,优先观察指数承接、成交额变化和板块持续性。
|
||||
|
||||
### 二、主要指数
|
||||
{indices_text}
|
||||
{stats_section}
|
||||
{sector_section}
|
||||
### 五、风险提示
|
||||
市场有风险,投资需谨慎。以上数据仅供参考,不构成投资建议。
|
||||
### 一、盘面总览
|
||||
{dashboard_block or "暂无市场宽度数据。"}
|
||||
|
||||
{strategy_summary}
|
||||
### 二、指数结构
|
||||
{indices_block or indices_text or "暂无指数数据。"}
|
||||
|
||||
### 三、板块主线
|
||||
{sector_block or "- 暂无板块涨跌榜数据。"}
|
||||
|
||||
### 四、资金与情绪
|
||||
- 结合成交额和涨跌家数看,当前更适合等待确认,避免仅凭单一热点追高。
|
||||
|
||||
### 五、消息催化
|
||||
- 暂无可用新闻时,应降低对题材持续性的确定性判断。
|
||||
|
||||
### 六、明日交易计划
|
||||
- **结论**:均衡观察。
|
||||
- **仓位**:控制在中性区间,等待指数与主线共振。
|
||||
- **关注方向**:{top_text or "强于指数的主线板块"}。
|
||||
- **回避方向**:{bottom_text or "连续走弱且缺少修复信号的方向"}。
|
||||
|
||||
### 七、风险提示
|
||||
- 市场有风险,投资需谨慎。以上数据仅供参考,不构成投资建议。
|
||||
|
||||
---
|
||||
*复盘时间: {datetime.now().strftime('%H:%M')}*
|
||||
|
||||
+2
-12
@@ -1,25 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Shared test helper to ensure imports work when litellm is unavailable.
|
||||
"""
|
||||
"""Shared test helper to keep litellm imports lightweight in unit tests."""
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
import types
|
||||
|
||||
|
||||
def ensure_litellm_stub() -> None:
|
||||
"""Install a minimal litellm stub only when litellm is unavailable."""
|
||||
"""Install a minimal litellm stub unless a test already provided one."""
|
||||
if "litellm" in sys.modules:
|
||||
return
|
||||
|
||||
try:
|
||||
if importlib.util.find_spec("litellm") is not None:
|
||||
return
|
||||
except ValueError:
|
||||
# A previously injected incomplete stub may leave __spec__ unset.
|
||||
pass
|
||||
|
||||
litellm_stub = types.ModuleType("litellm")
|
||||
|
||||
class _DummyRouter: # pragma: no cover
|
||||
|
||||
@@ -578,8 +578,8 @@ class TestMarketAnalyzerBypassFix:
|
||||
result = ma.generate_market_review(overview, [])
|
||||
|
||||
assert "## 2026-03-05 大盘复盘" in result
|
||||
assert "### 一、市场总结" in result
|
||||
assert "今日美股市场整体呈现**小幅下跌**态势。" in result
|
||||
assert "### 一、盘面总览" in result
|
||||
assert "今日美股市场整体呈现**小幅下跌**态势" in result
|
||||
assert "### 1. Market Summary" not in result
|
||||
assert "US Market Recap" not in result
|
||||
|
||||
@@ -625,9 +625,67 @@ Sector text.
|
||||
|
||||
assert "Advancers **3200**" in result
|
||||
assert "Turnover **14567** (CNY 100m)" in result
|
||||
assert "| Index | Last | Change % | Turnover (CNY 100m) |" in result
|
||||
assert "Leaders: **AI算力**(+3.25%)" in result
|
||||
assert "Laggards: **煤炭**(-1.12%)" in result
|
||||
assert "| Index | Last | Change % | Open | High | Low | Amplitude | Turnover (CNY 100m) |" in result
|
||||
assert "#### Leading Sectors" in result
|
||||
assert "| 1 | AI算力 | +3.25% |" in result
|
||||
assert "#### Lagging Sectors" in result
|
||||
assert "| 1 | 煤炭 | -1.12% |" in result
|
||||
|
||||
def test_inject_data_into_review_matches_reference_style_chinese_headings(self):
|
||||
from src.market_analyzer import MarketOverview, MarketIndex
|
||||
|
||||
ma = self._make_market_analyzer_with_mock_generate_text(return_value="review")
|
||||
overview = MarketOverview(
|
||||
date="2026-03-05",
|
||||
indices=[
|
||||
MarketIndex(
|
||||
code="000001",
|
||||
name="上证指数",
|
||||
current=3300.0,
|
||||
change=12.0,
|
||||
change_pct=0.36,
|
||||
open=3288.0,
|
||||
high=3312.0,
|
||||
low=3276.0,
|
||||
amount=145000000000.0,
|
||||
amplitude=1.1,
|
||||
)
|
||||
],
|
||||
up_count=3200,
|
||||
down_count=1800,
|
||||
flat_count=100,
|
||||
limit_up_count=88,
|
||||
limit_down_count=5,
|
||||
total_amount=14567.0,
|
||||
top_sectors=[{"name": "AI算力", "change_pct": 3.25}],
|
||||
bottom_sectors=[{"name": "煤炭", "change_pct": -1.12}],
|
||||
)
|
||||
news = [{"title": "AI算力板块走强", "snippet": "算力产业链延续活跃,成交额放大"}]
|
||||
review = """## 2026-03-05 大盘复盘
|
||||
|
||||
### 一、盘面总览
|
||||
总结。
|
||||
|
||||
### 二、指数结构
|
||||
指数。
|
||||
|
||||
### 三、板块主线
|
||||
板块。
|
||||
|
||||
### 五、消息催化
|
||||
新闻。
|
||||
"""
|
||||
|
||||
result = ma._inject_data_into_review(review, overview, news)
|
||||
|
||||
assert "盘面温度" in result
|
||||
assert "| 上涨/下跌/平盘 | 3200 / 1800 / 100 |" in result
|
||||
assert "| 指数 | 最新 | 涨跌幅 | 开盘 | 最高 | 最低 | 振幅 | 成交额(亿) |" in result
|
||||
assert "| 上证指数 | 3300.00 | 🟢 +0.36% | 3288.00 | 3312.00 | 3276.00 | 1.10% | 1450 |" in result
|
||||
assert "#### 领涨板块 Top 5" in result
|
||||
assert "| 1 | AI算力 | +3.25% |" in result
|
||||
assert "#### 近三日催化线索" in result
|
||||
assert "AI算力板块走强" in result
|
||||
|
||||
def test_us_english_indices_do_not_label_turnover_as_cny(self):
|
||||
from src.core.market_profile import US_PROFILE
|
||||
|
||||
@@ -35,8 +35,8 @@ def _build_optional_module_stubs() -> dict[str, ModuleType]:
|
||||
return stubs
|
||||
|
||||
|
||||
with patch.dict(sys.modules, _build_optional_module_stubs()):
|
||||
import src.core.market_review as market_review_module
|
||||
sys.modules.update(_build_optional_module_stubs())
|
||||
import src.core.market_review as market_review_module
|
||||
|
||||
run_market_review = market_review_module.run_market_review
|
||||
|
||||
|
||||
Reference in New Issue
Block a user