chore: prepare v3.24.0 release

This commit is contained in:
ZhuLinsen
2026-06-28 21:08:26 +08:00
parent 29fa05049d
commit dbfd8c1fb8
4 changed files with 120 additions and 62 deletions

View File

@@ -9,70 +9,69 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
## [Unreleased]
- [改进] TickFlow 扩展为可选 A 股日 K、实时行情、股票列表/名称数据源,并为日 K 请求增加 count、完整性校验和批量预取缓存保护。
- [新功能] #1777 新增台股三大法人institutional flows资料层 fetcher `TwInstitutionalFetcher`:上市走 TWSE T86 legacy `rwd` 端点、上柜走 TPEx OpenAPI正规化外资/投信/自营商/三大法人每日买卖超(单位股数,民国↔西元日期转换有单测),按日期+市场单日缓存,失败/限流/空响应一律 fail-open`.TW`/`.TWO` 生效、严格 additive不改动现有市场流程、不接报告/Web/评分/`capital_flow_signal`。资料来源为政府开放资料OGDL v1
- [修复] API 异步批量分析共享概念板块排行缓存,避免同批多股重复拉取全市场概念排行。
- [文档] 补齐概念板块排行字段契约与通知报告行业/概念类型列展示说明。
- [新功能] #1742 新增信号归因分析功能dashboard.signal_attribution解释推荐理由的构成技术指标、新闻舆情、基本面、市场环境的贡献度以及最强看多/看空信号)。支持默认通知报告和 Jinja2 模板渲染,包含中英文国际化标签。归一化函数在 _parse_response() 和 parse_dashboard_json() 中显式调用,确保有效非零贡献度归一化到 100all-zero 保留为 0表示无有效信号
- [改进] Agent 路径同步:更新 executor.py 和 decision_agent.py 的 prompt确保 agent/multi-agent 分析时也生成 signal_attribution 字段。
- [新功能] #1815 Phase 2 大盘复盘新增 `jp`/`kr` 市场支持日经225/TOPIX、KOSPI/KOSDAQ 指数复盘,扩展 `MARKET_REVIEW_REGION`、交易日过滤、Web 设置枚举、市场 profile/strategy、文档与回归测试。
- [改进] #1815 Phase 1 硬化日本/韩国 suffix-only 个股 MVP集中 JP/KR/TW suffix 识别规则,扩充日韩股票种子索引,并为 yfinance 报价/基本面上下文补充市场、币种与数据质量元数据。
- [文档] #1815 补充 JP/KR/TW suffix-only MVP 在外部 API、provider/model/base URL 与运行时配置上的边界说明:当前为结构化字段兼容验证且可回退到旧链路。
- [文档] #1815 细化 PR 提交流程约束:.github/PULL_REQUEST_TEMPLATE.md 补充 Head CI 一致性、Web 设置变更可视证据、第三方兼容性声明与回滚说明要求,避免描述与验证状态/变更影响不一致。
- [修复] 修复通知 Markdown 表格转换在空单元格后将后续内容错配到错误表头的问题。
- [改进] #1815 Phase 3 收敛 JP/KR Portfolio 与 Market Light 边界JP/KR 持仓快照标记 partial/limitationsMarket Light 告警继续限定 cn/hk/us并同步 Web 选项、文档和测试。
- [修复] 修复 Market Light 区域归一化仍拒绝 `jp`/`kr`,导致 JP/KR 大盘复盘历史快照读取回归失败的问题。
- [文档] #1815 明确 JP/KR Phase 3 收敛时的兼容与回退路径:`MARKET_REVIEW_REGION=jp/kr` 仅扩展复盘输入Market Light 告警、LLM provider/model/base URL、运行时配置持久化与清理语义保持不变并补充官方来源、当前 LiteLLM 依赖窗口与回归测试证据。
- [文档] #1815 集中补充 `MARKET_REVIEW_REGION` 保存/校验/回退矩阵、旧 `both` 三市场边界到 `cn,hk,us` 的迁移说明、JP/KR yfinance 指数依赖边界,以及 Market Light 告警与设置页 UI 变更的可替代验证证据;补充冲突解决后最终 head 的 backend gate 与 Web lint/build 验证结论。
- [修复] 将 Docker 可安装的 Longbridge SDK 版本固定为 0.2.75,避免 `longbridge>=0.2.77` 从包索引消失后导致 docker-build 失败。
- [修复] 持仓快照今日估值改为受限并发预取多只持仓实时价,减少持仓较多时 Web 组合页面刷新超时。
- [修复] Web 首页重新分析完成后自动切换到同一股票最新生成的报告,避免仍停留在旧报告内容。
- [改进] AlphaSift 默认依赖 pin 更新到 `0a7b9cd59e81718f851890535241bc105d4ddc64`,纳入上游日线数据源健康度、硬过滤诊断、策略评估摘要和 LLM 排名解析稳定性改进。
- [改进] AlphaSift 热点默认走 DSA EastMoney 兜底 provider选股运行期默认提高日线抓取重试并在状态接口暴露 snapshot/daily source health便于定位不稳定数据源。
- [文档] 新增数据源稳定性与故障处理图示,说明 Tushare、TickFlow、AkShare、Efinance、YFinance、Longbridge 等已接入源的使用场景、fallback 链路和推荐配置。
- [新功能] #1754 在 Web AI 建议页新增单股信号时间线,并为自动生成与历史回填的 DecisionSignal 写入默认 decision_profile metadata。
- [修复] 默认通知报告补充展示 `dashboard.phase_decision` 盘中决策护栏字段,避免与模板渲染路径展示不一致。
- [修复] 修复 Windows 环境下 Web/Desktop 静态 JS 资源可能被识别为 `text/plain` 导致前端黑屏的问题。
- [改进] Web 设置页新增首次启动配置检查卡,串联基础配置状态、自选股入口、模型配置入口和一次简短试跑。
- [改进] 通知报告的分析结果摘要不再展开 AI 决策信号明细,完整信号保留在个股详情和单股报告中。
- [新功能] #1595 P1.5 新增 Provider Cache Capability Registry按 provider、api surface、gateway 和 verification status 建模 prompt cache 能力,未知 OpenAI-compatible route 默认 telemetry only。
- [改进] #1595 P1 新增 prompt cache telemetry / analysis-path hints / diagnostics 最小配置,默认不改变 provider 请求 shape并复用 LLM usage HMAC secret 做 domain-separated cache hint 派生。
- [新功能] 大盘复盘、Web 报告页和通知关联板块补齐概念板块排行与概念信号展示。
- [改进] 将 Docker Compose 默认内存建议从 512M 提升到 1G并补充低配部署说明。
- [改进] 每日分析 workflow 兼容误将 `STOCK_LIST` 配到同名 Environment variables 的场景,同时保留 Repository variables 作为推荐配置入口。
- [新功能] #1743 Phase 3 新增 reserved Hermes 本地 HTTP generation 渠道,提供 JSON generation、no-proxy 本地调用、saved secret endpoint 绑定,并明确不支持 stream/SSE、tools、Vision、Agent tools 与 remote Hermes。
- [新功能] #1772 新增台湾台股suffix-only 个股分析 MVP**市场识别与数据路由层**):手输 `.TW`TWSE 上市)/ `.TWO`TPEx 上柜)代码可走 YFinance 日线与近实时行情补充市场识别、交易日历XTAI / Asia/Taipei、Prompt 语义与能力边界文档;加权指数 `^TWII`、柜买指数 `^TWOII`。台股股票索引/种子、Web 自动补全与告警(大盘红绿灯)市场放行作为后续 PR。
- [文档] #1772 明确本次为台股 suffix 仅路由兼容改造,对齐 #1718 日韩模式;不涉及 provider/model/base URL/运行时配置变更;回退方式为 revert 本次改动或移除 tw 入口恢复既有行为。
- [新功能] #1772 台股 `tw` 纳入 DecisionSignal / Portfolio / Intelligence 服务层与 API 市场枚举VALID_MARKETS / _ALLOWED_MARKETS + Pydantic Literal + api_spec.json修复数据层 MVP 下 tw 分析在 pipeline 自动抽取 DecisionSignal 时被 _normalize_market 静默丢弃的缺陷,并同步放行 DecisionSignal/Portfolio 前端市场类型与筛选及相关专题文档,对齐 #1720 日韩;告警(大盘红绿灯)市场仍为 cn/hk/us。
<!-- 新条目格式:- [类型] 描述(类型取值:新功能/改进/修复/文档/测试/chore-->
<!-- 每条独立一行追加到本段末尾,无需分类标题,合并时冲突最小 -->
- [修复] 修复日股/韩股历史列表重建市场阶段摘要时将 non_trading 等结果阶段误传为 analysis_phase 导致列表查询失败的问题。
- [新功能] 支持 SCHEDULE_TIMES 多时间定时推送,并让 Web/API/Desktop 长运行进程保存调度配置后热启停或重建 runtime scheduler。
- [改进] Web 设置页以“定时任务”面板维护多时间定时推送、状态刷新和立即执行一次,不再向用户直接暴露 SCHEDULE_TIMES 等内部配置键。
- [修复] 修复 `--serve --schedule` 下 CLI 调度器与 Web/API runtime scheduler 状态脱节的问题,并避免设置页把失败执行时间显示为上次成功。
- [修复] runtime scheduler 的立即执行接口在已有分析运行时返回 409 忙碌状态Web 设置页不再把未排队的请求提示为已提交
- [修复] 避免 runtime scheduler 重建定时任务时重复立即运行事件监控,减少重复告警和后台任务状态丢失
- [修复] Web/API runtime scheduler 接管 `--serve --schedule` 后保留 `--dry-run``--no-notify` 等启动参数语义
- [改进] Web 历史报告详情不再内嵌展示 AI 建议卡片,结构化决策信号集中在 AI 建议页查询,并保留按来源报告 ID 筛选或 URL 参数精确定位入口
- [改进] 新增 GenerationBackend Phase 1 抽象与 LiteLLM backend 配置,默认保持普通分析、`generate_text()` 和 Agent Chat 的 LiteLLM 行为不变
- [新功能] #1743 Phase 2 新增显式 opt-in 的 `codex_cli` 本地 CLI generation backend固定安全 preset、结构化错误、fallback、stream 降级和 usage unavailable contract。
- [改进] `GENERATION_BACKEND=codex_cli` 下普通分析与大盘复盘不再因缺少 LiteLLM API Key 被误判不可用local CLI 失败会暴露结构化错误或按配置回退 LiteLLM。
- [改进] `codex_cli` preset 改用 `--output-last-message` 文件读取最终响应stdout/stderr 仅作为诊断预览,避免 Codex CLI session 元数据混入主分析 JSON。
- [修复] `codex_cli` preset 不再把 Codex CLI 同时打印到 stdout 的最终响应重复计入输出上限,也不在 `stdout_preview` 暴露重复的最终响应内容
- [修复] 恢复主分析 JSON 解析的宽松 schema fallback 语义,完整报告 schema 校验失败时继续按 raw JSON 解析,避免有效字段被错误降级为文本 fallback
- [改进] 本地 CLI backend 对诊断 stdout/stderr 与最终响应实行执行期总量上限,并为新增 generation backend 数字配置补齐最大值校验
- [文档] 补充本地 CLI backend 隐私边界、非离线模型说明、Docker/CI 登录态限制,以及 `codex_cli` experimental/limited 状态
- [修复] unsupported `GENERATION_BACKEND``generate_text()` 与大盘复盘路径中显式报配置错误,避免被当成空响应或模板报告 fallback
- [改进] Web 设置页明确 `AGENT_GENERATION_BACKEND=auto` 当前使用 LiteLLM 工具调用路径,避免暗示已有动态 backend 选择
- [改进] Web 首页“任务已存在”重复分析提示新增手动关闭按钮与 5 秒后自动消失
- [修复] `main.py --serve-only` 在低配主机上因 uvicorn 在 3.0s 启动自检窗口内才惰性 import 应用litellm + 整个 app 树)导致超时退出、容器反复重启;改为在计时前于调用线程预先 import app 对象再交给 uvicorn启动自检不再误杀慢启动
- [修复] Docker 镜像预置 efinance 缓存目录efinance/data属主给非 root 运行用户 dsa修复 A 股 efinance 数据源因写 search-cache.json 触发 PermissionError 而每次抓取失败降级的问题
- [修复] Docker 部署中 Web 设置页保存自定义 Webhook 模板时自动转义 `$content_json` 等应用占位符,并在运行时还原,避免 Compose 重新部署将其展开为空
- [修复] 修复 Web 回测运行未传分析日期范围、股票代码未归一化导致后端成功返回但结果为空的问题,并为空候选和行情不足返回诊断信息。
- [文档] 补充回测请求链路说明:`analysis_date_from/analysis_date_to``code` 的输入边界、归一化与筛选顺序,以及历史行情不足或候选集为空时回测返回成功响应,在 `message``diagnostics`(含 `empty_reason`)中提供可诊断信息,并同步更新 `docs/full-guide.md``docs/full-guide_EN.md` 示例。
- [修复] 回测代码匹配新增非法市场后缀/长度兜底:如 `600519.HK``600519.SZ``SH000001` 不再静默回落到其它有效代码,并在日期筛选重跑时对齐旧回测结果的分析日期,避免历史快照日期命中但结果列表仍为空。
## [3.24.0] - 2026-06-28
### 发布亮点
- feat: 扩展台股、日股、韩股市场支持,覆盖台股 suffix-only 分析、台股三大法人资料层、JP/KR 大盘复盘和跨服务市场枚举
- feat: 新增 GenerationBackend 抽象、`codex_cli` 本地 CLI backend、reserved Hermes 本地 HTTP 渠道和 prompt cache capability registry
- feat: Web/API/Desktop 支持多时间定时推送与 runtime scheduler 热重建Web 设置页补齐首次启动检查与定时任务面板
- feat: 报告链路补齐信号归因、单股信号时间线、概念板块排行和通知/报告关联板块展示
- fix: 修复 Docker/启动探针、静态资源 MIME、回测空结果、组合估值、通知 Markdown、AlphaSift 数据源和测试环境隔离等稳定性问题
### 新功能
- 新增台股 suffix-only 个股分析 MVP`.TW`/`.TWO` 代码可走 YFinance 日线与近实时行情,并补齐市场识别、交易日历和 Prompt 能力边界
- 台股 `tw` 纳入 DecisionSignal、Portfolio、Intelligence 服务层、API 枚举和 Web 筛选,避免台股分析信号被市场归一化静默丢弃
- 新增台股三大法人资料层 fetcher `TwInstitutionalFetcher`,支持 TWSE/TPEx 来源、日期转换、单日缓存和 fail-open 退化
- 大盘复盘新增 `jp`/`kr` 市场支持日经225/TOPIX、KOSPI/KOSDAQ 指数复盘,并扩展 `MARKET_REVIEW_REGION`、交易日过滤和 Web 设置枚举
- 新增 GenerationBackend Phase 1 抽象和显式 opt-in 的 `codex_cli` 本地 CLI generation backend提供结构化错误、fallback、stream 降级和 usage unavailable contract
- 新增 reserved Hermes 本地 HTTP generation 渠道,提供 JSON generation、no-proxy 本地调用和 saved secret endpoint 绑定
- 新增 Provider Cache Capability Registry按 provider、API surface、gateway 与 verification status 建模 prompt cache 能力
- 支持 `SCHEDULE_TIMES` 多时间定时推送,长运行 Web/API/Desktop 进程保存调度配置后可热启停或重建 runtime scheduler
- 新增信号归因分析和 Web AI 建议页单股信号时间线,并为自动生成与历史回填的 DecisionSignal 写入默认 `decision_profile` metadata
- 大盘复盘、Web 报告页和通知关联板块补齐概念板块排行与概念信号展示
### 改进
- TickFlow 扩展为可选 A 股日 K、实时行情、股票列表/名称数据源,并增加 count、完整性校验和批量预取缓存保护。
- 硬化 JP/KR/TW suffix 识别、日韩股票种子索引、YFinance 报价/基本面上下文,以及 JP/KR Portfolio 与 Market Light 边界。
- Web 设置页新增首次启动配置检查卡与定时任务面板,隐藏内部 `SCHEDULE_TIMES` 键,并改善重复任务提示的关闭与自动消失体验。
- Web 历史报告详情不再内嵌 AI 建议卡片,结构化决策信号集中到 AI 建议页,并保留来源报告 ID/URL 参数精确定位。
- `GENERATION_BACKEND=codex_cli` 下普通分析与大盘复盘不再因缺少 LiteLLM API Key 被误判不可用,并改用 `--output-last-message` 文件读取最终响应。
- 本地 CLI backend 对 stdout/stderr 诊断预览和最终响应实行执行期总量上限,并补齐新增 generation backend 数字配置最大值校验。
- AlphaSift 默认依赖 pin 更新到 `0a7b9cd59e81718f851890535241bc105d4ddc64`,并默认走 DSA EastMoney 兜底 provider、暴露 source health 诊断。
- Docker Compose 默认内存建议提升到 1G每日分析 workflow 兼容误将 `STOCK_LIST` 配到同名 Environment variables 的场景。
- Agent 路径同步 signal attribution prompt通知报告摘要不再展开 AI 决策信号明细,完整信号保留在个股详情与单股报告。
### 修复
- API 异步批量分析共享概念板块排行缓存,避免同批多股重复拉取全市场概念排行。
- 修复通知 Markdown 表格转换在空单元格后将后续内容错配到错误表头的问题。
- 修复 Market Light 区域归一化拒绝 `jp`/`kr`、日韩历史列表市场阶段摘要误传 `analysis_phase` 和默认通知报告缺少 `dashboard.phase_decision` 的问题。
- 固定 Docker 可安装的 Longbridge SDK 版本为 0.2.75,并修复 Docker 镜像中 efinance 缓存目录属主导致 A 股数据源降级的问题。
- 持仓快照今日估值改为受限并发预取实时价,减少持仓较多时 Web 组合页面刷新超时。
- Web 首页重新分析完成后自动切换到同一股票最新报告,并修复 Windows 环境下 Web/Desktop 静态 JS 资源可能以 `text/plain` 返回导致黑屏的问题。
- 修复 `--serve --schedule` 与 Web/API runtime scheduler 状态脱节、立即执行忙碌状态误提示、重建定时任务重复监听和启动参数语义丢失。
- 修复 `main.py --serve-only` 在低配主机上因惰性 import 应用超出 uvicorn 启动自检窗口而反复重启的问题。
- 修复 Web 回测未传分析日期范围、股票代码未归一化导致成功响应但结果为空的问题,并为空候选、行情不足和非法后缀提供诊断信息。
- 修复 unsupported `GENERATION_BACKEND` 被当成空响应/模板 fallback、`codex_cli` stdout 重复计入输出上限和主分析 JSON schema fallback 语义回退的问题。
- Docker 部署中 Web 设置页保存自定义 Webhook 模板时会转义 `$content_json` 等占位符,并在运行时还原,避免 Compose 重新部署展开为空。
### 文档
- 补齐概念板块排行字段契约、通知报告行业/概念类型列展示和数据源稳定性与故障处理图示。
- 补充 JP/KR/TW suffix-only MVP、`MARKET_REVIEW_REGION` 保存/校验/回退矩阵、Market Light 边界和 PR 提交流程约束。
- 补充本地 CLI backend 隐私边界、非离线模型说明、Docker/CI 登录态限制和 `codex_cli` experimental/limited 状态。
- 补充回测请求链路说明,并同步更新 `docs/full-guide.md``docs/full-guide_EN.md` 示例。
### 测试
- 新增/更新台股、JP/KR 大盘复盘、GenerationBackend、`codex_cli`、Hermes、本地 CLI、runtime scheduler、回测和概念板块排行相关回归测试。
- 加强 `tests/test_analysis_api_contract.py``tests/test_analysis_history.py``tests/test_backtest_service.py` 的临时 `.env` 隔离,避免本地真实 `.env` 污染系统配置测试。
## [3.23.0] - 2026-06-20
@@ -1891,7 +1890,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
---
[Unreleased]: https://github.com/ZhuLinsen/daily_stock_analysis/compare/v3.22.0...HEAD
[Unreleased]: https://github.com/ZhuLinsen/daily_stock_analysis/compare/v3.24.0...HEAD
[3.24.0]: https://github.com/ZhuLinsen/daily_stock_analysis/compare/v3.23.0...v3.24.0
[3.23.0]: https://github.com/ZhuLinsen/daily_stock_analysis/compare/v3.22.0...v3.23.0
[3.22.0]: https://github.com/ZhuLinsen/daily_stock_analysis/compare/v3.21.1...v3.22.0
[3.21.1]: https://github.com/ZhuLinsen/daily_stock_analysis/compare/v3.21.0...v3.21.1
[3.21.0]: https://github.com/ZhuLinsen/daily_stock_analysis/compare/v3.20.0...v3.21.0

View File

@@ -5,12 +5,19 @@ import asyncio
from concurrent.futures import Future
from datetime import datetime
import json
import os
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import ANY, MagicMock, patch
_ORIGINAL_ENVIRON = dict(os.environ)
_MODULE_TEMP_DIR = tempfile.TemporaryDirectory()
_MODULE_ENV_FILE = Path(_MODULE_TEMP_DIR.name) / ".env"
_MODULE_ENV_FILE.write_text("STOCK_LIST=600519,000001\n", encoding="utf-8")
os.environ["ENV_FILE"] = str(_MODULE_ENV_FILE)
from tests.litellm_stub import ensure_litellm_stub
ensure_litellm_stub()
@@ -44,6 +51,19 @@ from src.services.image_stock_extractor import _call_litellm_vision
from src.services.task_queue import AnalysisTaskQueue, TaskStatus
def tearDownModule() -> None:
current_test = os.environ.get("PYTEST_CURRENT_TEST")
for key in list(os.environ):
if key == "PYTEST_CURRENT_TEST":
continue
if key not in _ORIGINAL_ENVIRON:
os.environ.pop(key, None)
os.environ.update(_ORIGINAL_ENVIRON)
if current_test is not None:
os.environ["PYTEST_CURRENT_TEST"] = current_test
_MODULE_TEMP_DIR.cleanup()
def _analysis_context_pack_overview() -> dict:
return {
"pack_version": "1.0",

View File

@@ -124,6 +124,18 @@ class AnalysisHistoryTestCase(unittest.TestCase):
auth._auth_enabled = False
self._temp_dir = tempfile.TemporaryDirectory()
self._db_path = os.path.join(self._temp_dir.name, "test_analysis_history.db")
self._original_env = {
key: os.environ.get(key)
for key in (
"ENV_FILE",
"DATABASE_PATH",
)
}
self._env_path = os.path.join(self._temp_dir.name, ".env")
with open(self._env_path, "w", encoding="utf-8") as env_file:
env_file.write("STOCK_LIST=600519,000001\n")
os.environ["ENV_FILE"] = self._env_path
os.environ["DATABASE_PATH"] = self._db_path
Config._instance = None
@@ -132,7 +144,13 @@ class AnalysisHistoryTestCase(unittest.TestCase):
def tearDown(self) -> None:
"""清理资源"""
Config._instance = None
DatabaseManager.reset_instance()
for key, value in self._original_env.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
self._temp_dir.cleanup()
def _build_result(self) -> AnalysisResult:

View File

@@ -24,6 +24,19 @@ class BacktestServiceTestCase(unittest.TestCase):
def setUp(self) -> None:
self._temp_dir = tempfile.TemporaryDirectory()
self._db_path = os.path.join(self._temp_dir.name, "test_backtest_service.db")
self._original_env = {
key: os.environ.get(key)
for key in (
"ENV_FILE",
"DATABASE_PATH",
"BACKTEST_EVAL_WINDOW_DAYS",
)
}
self._env_path = os.path.join(self._temp_dir.name, ".env")
with open(self._env_path, "w", encoding="utf-8") as env_file:
env_file.write("STOCK_LIST=600519,000001\n")
os.environ["ENV_FILE"] = self._env_path
os.environ["DATABASE_PATH"] = self._db_path
os.environ["BACKTEST_EVAL_WINDOW_DAYS"] = "3"
@@ -145,7 +158,13 @@ class BacktestServiceTestCase(unittest.TestCase):
session.commit()
def tearDown(self) -> None:
Config._instance = None
DatabaseManager.reset_instance()
for key, value in self._original_env.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
self._temp_dir.cleanup()
def _count_results(self) -> int: