release: prepare v3.14.0

This commit is contained in:
zhulinsen
2026-04-26 21:57:09 +08:00
parent 4574c52b04
commit c3d07ceb87
9 changed files with 351 additions and 109 deletions
+1 -1
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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}`);
+1
View File
@@ -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.
---
+11
View File
@@ -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
View File
@@ -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
View File
@@ -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
+63 -5
View File
@@ -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
+2 -2
View File
@@ -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