Files
group_fqcd_jr/docs/12-基金行情数据底座接入开发计划.md
lzf_0626 36c7a9d8d2 文档:审查报告入库 + 全量校对补注
## 新入库(`docs/演示用/`)

- `代码库全面审查报告-2026-09-14.md`
- `代码修改方案-2026-09-14.md`
- `记忆系统排查报告-2026-09-14.md`
- `记忆系统修复文档-2026-09-14.md`
- `文档一致性审计报告-2026-09-14.md`
- `多Worker接入方案-2026-09-14.md`

## 全量校对(32 个既有文档 + `AGENTS.md`)

跨 39 个文件、**1125 insertions / 148 deletions**。

⚠️ **这批改动同样不是本次会话写的**。我抽样核对过性质:是**实质内容补充**而不是
格式/换行转换。例如 `docs/44-演示流程.md` 新增两条"2026-09-14 补注":

- `启动金融Agent平台.bat` 只在**桌面**上,仓库里只有 `启动平台.bat` 这一份
  (两份由同一个 `tools/make_launcher_bat.py` 产出,改完 `start.ps1` 重跑它一起更新);
- `advisor_t`(9020) 与 `offsite_t`(9006) **不在 `tools/seed_test_rbac.py` 的演示用户里**
  (那里只有 `cust_t`/`risk_t`/`admin_t`/`review_t` 四个),由 `grant_*.py` 系列创建,
  **重跑种子不会重建它们** —— 换机器时这两个账号登录失败,要先查 `sys_user` 有没有这两行,
  而不是查密码。

这两条都是对的地方,与我这一路踩到的现象一致(我确实用到了 `advisor_t`/`offsite_t`)。

**我没有逐字审阅全部 39 个文件**,只抽样确认了改动性质与规模。若其中有需要复核的段落,
请指明文件,我逐处核对。
2026-09-14 20:36:00 +08:00

289 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 基金行情数据底座接入开发计划
> ⚠️ **MVP 完成标准已过期。** 文中「F0-F4 全部通过」为当时快照;实际 F0-F6 已完成。另有部分「当前实现」的描述已随底座演进而变化。
>
> (此批注由 2026-09-11 只读审计加入;原文未改动。详见 `docs/superpowers/ARCHIVE-2026-09-11-文档清理归档.md` §3)
>
> ---
> ## ⚠️ 2026-09-14 补充:本文**完全没写**的两件运行期事实(重要)
>
> **1. 行情有效期只有 15 分钟 —— 这是演示/验收最容易翻的一环。**
> `app/service/trade_service.py` 的 `MAX_QUOTE_AGE = timedelta(minutes=15)`(L79)。
> 超时后**所有委托一律 503 `FUND_QUOTE_UNAVAILABLE`**(`app/core/errors.py:260-264`),
> **且没有自动刷新**。症状看起来像"行情源坏了",实际只是数据放久了。
> 补刷命令(**立即生效、无需重启服务**):
> ```powershell
> .\.venv\Scripts\python.exe tools\sync_market_prices.py
> ```
>
> **2. 两个行情/净值同步脚本未被本文登记**:
> - `tools/sync_market_prices.py` —— 刷实时行情(上条)。
> - `tools/sync_nav_history.py` —— 刷历史净值(访客端 `GET /api/v1/products/{product_code}/nav-history` 用它)。
>
> **3. 工具超时的依据**:`query_fund_quote` 的工具超时是 **15 秒**(`app/service/agent/bootstrap.py:278`)。
> 理由(代码内注释):`EastmoneyAdapterFactory` 单码最坏预算 = 4.0s + 0.2s(退避) + 4.0s(重试) = 8.2s,
> 15s ≈ 8.2 × 1.8。**旧的 5 秒会在适配器自己的重试预算之前就撞工具超时**,永远拿不到结果。
>
> **启动链**:`启动金融Agent平台.bat`(桌面)/ `启动平台.bat`(仓库)会按序做:找解释器 →
> 检查 MySQL/Redis/Milvus → **刷新行情** → 起 API 与 Agent Worker → 等 API 应答 → 开浏览器。
> **解释器门槛是「版本 ≥ 3.11 且能 `import fastapi, sqlalchemy, asyncmy, pydantic`」**,
> 不能只看 `--version`(曾因此选中 3.10 环境,报错却出现在行情刷新那步,看着像行情源坏了)。
> 版本:v1.0
> 目标:将外部基金行情能力接入公共 MVC+S 底座,供客服、投顾、风控 Agent 统一只读使用。
> 当前范围:场内基金模拟交易相关行情;场外运营数据不纳入本计划。
## 一、建设目标
将外部行情脚本改造成公共底座能力:
```text
东方财富 Adapter
→ FundQuoteService
→ ToolRegistry / ToolExecutor
→ 客服、投顾、风控 Agent
```
业务组员只调用统一工具,不直接访问东方财富接口、HTTP 客户端、缓存或数据库。
## 二、明确不做的事情
- 第一阶段不修改已有数据库表、字段和交易流程;
- 不把行情快照写入场内交易表;
- 不实现代客下单、成交确认或持仓修改;
- 不让 Agent 直接调用外部行情接口;
- 不在本阶段创建可运行示例 Agent;
- 不把场外基金运营数据混入本行情服务。
## 三、现有脚本拆分映射
| 原 `hq.py` 能力 | 新底座位置 | 处理要求 |
|---|---|---|
| 东方财富 HTTP 调用 | `app/infrastructure/fund_market_adapter.py` | 改为异步、超时、重试、响应校验 |
| 交易时段判断 | `FundQuoteService` | 固定北京时间并可测试注入时钟 |
| 基金名称缓存 | 行情缓存适配器 | 允许 Redis,不使用进程全局作为唯一缓存 |
| 历史净值和收益率合并 | `app/service/fund_quote_service.py` | 统一 DTO、Decimal、降级标识 |
| 中文字典输出 | Controller/Agent 展示层 | 内部 DTO 使用英文稳定字段 |
| 固定南方基金代码 | 配置或代码白名单 | 先保留白名单,后续配置中心化 |
## 四、阶段计划
### F0:基线和契约冻结
**目标**:确认接入不会破坏现有底座和数据库规则。
**任务**:
- 对照 `AGENTS.md`、00 基线和 02 建表设计;
- 确认本阶段只新增 Python 模块、工具注册和测试;
- 定义 `FundQuoteQuery`、`FundQuote`、`FundQuoteSource` DTO;
- 定义错误分类:外部行情不可用返回可识别的降级结果,不泄露供应商异常;
- 确认行情工具权限码为 `fund:quote:read`。
**验收**:
- DTO 字段、类型和空值规则确定;
- 没有数据库迁移;
- 没有修改既有表和字段;
- 接口契约可被 Service、ToolExecutor 和测试共同引用。
### F1:外部行情 Adapter
**目标**:把 `hq.py` 的外部请求改造成底座 Infrastructure 适配器。
**新增文件**:
```text
app/infrastructure/fund_market_adapter.py
app/core/fund_contracts.py
tests/unit/infrastructure/test_fund_market_adapter.py
```
**任务**:
- 使用 `httpx.AsyncClient`,禁止同步网络调用阻塞 Worker;
- 封装名称、实时行情、历史净值、区间收益率四类请求;
- 统一请求超时、有限重试和指数退避;
- 校验外部 JSON 结构和基金代码;
- 将 HTTP、JSON、字段缺失转换为 `RecoverableAgentError` 或降级结果;
- 供应商日志只记录接口类别、基金数量和异常类型,不记录完整响应正文;
- 使用 `Decimal` 保存净值和收益率;
- 所有时间统一使用 Asia/Shanghai 语义。
**验收**:
- 外部接口成功、超时、HTTP 错误、非法 JSON、字段缺失均有测试;
- 测试不依赖真实东方财富网络,使用 `httpx.MockTransport`;
- Adapter 不导入 Agent、Controller 或数据库 Session。
### F2:FundQuoteService
**目标**:将多个外部接口结果合并成稳定的行情业务结果。
**新增文件**:
```text
app/service/fund_quote_service.py
tests/unit/service/test_fund_quote_service.py
```
**任务**:
- 实现按基金代码、基金类型、日期和数量查询;
- 保留场内基金白名单;
- 合并实时行情和历史净值;
- 盘中使用实时行情,非盘中使用最新收盘净值;
- 返回 `quote_source`、`quote_time`、`is_intraday`、`degraded`;
- 单个产品失败不得导致全部产品无原因失败;
- 外部行情不可用时返回结构化降级结果;
- 不将实时行情解释为成交、委托或持仓结果。
**验收**:
- 交易时段和非交易时段测试通过;
- 基金类型筛选、数量限制、非法日期和未知类型测试通过;
- 实时失败回退历史、历史失败返回降级结果的测试通过;
- Service 不直接解析 HTTP 响应细节。
### F3:缓存和健康状态
**目标**:降低外部接口压力,并让组员得到可识别的降级状态。
**任务**:
- 基金名称缓存建议 1 天;
- 收盘净值缓存建议 5-30 分钟;
- 实时行情缓存建议 30-60 秒;
- Redis 可用时使用 Redis,不可用时回退无缓存请求或结构化降级;
- 不将进程级字典作为多 Worker 唯一缓存;
- 增加行情数据源健康状态,不把供应商故障伪装成正常行情;
- 记录数据源成功率、延迟、降级次数和最后成功时间。
**验收**:
- Redis 正常、Redis 不可用、缓存过期和缓存击穿测试通过;
- 缓存故障不阻塞主流程;
- 返回结果能够区分实时、缓存、收盘和降级来源。
### F4:注册公共只读工具
**目标**:让所有业务 Agent 通过统一工具使用行情。
**工具契约**:
```text
工具名:query_fund_quote
权限码:fund:quote:read
只读:是
允许角色:customer、advisor、operator、risk_operator、admin
超时:5 秒
```
**任务**:
- 定义严格的 Pydantic 输入模型;
- 在 `bootstrap` 中注册 `ToolDefinition`;
- 由 `ToolExecutor` 统一检查权限、角色、意图白名单和超时;
- 结果自动生成工具来源引用和审计记录;
- `AgentDefinition.allowed_tools` 只能缩小权限;
- 业务 Agent 不能绕过工具调用 Adapter。
**验收**:
- 正常调用、未授权、未列入意图白名单、参数错误、超时和外部失败均有测试;
- 工具结果不含供应商原始异常和敏感配置;
- 来源引用能通过公共合规审查。
### F5:配置中心和管理规则
**目标**:把基金白名单、数据源开关、缓存时间和限流参数逐步纳入配置治理。
**第一阶段**:
- 外部接口地址、超时和密钥引用写入 `.env` 或运行配置;
- 基金白名单保留在代码常量,避免未经审核扩大产品范围。
**后续阶段**:
- 使用 `platform_config_item` 增加行情配置命名空间;
- 配置发布、审核、激活、回滚必须经过已有配置中心;
- 配置变更写审计并通过 Outbox 通知缓存失效;
- 不通过配置中心扩大 Agent 的工具权限上限。
**验收**:
- 配置缺失时失败关闭或使用明确的安全默认值;
- 密钥只使用 `secret_ref`,不返回明文;
- 发布版本可追溯、可回滚。
**当前实现**:`RuntimeConfigService.fund_quote()` 读取当前激活版本的
`namespace=fund_market、config_key=default`;`query_fund_quote` 工具加载该配置,
`FundQuoteRuntimeConfig` 校验基金白名单和缓存 TTL。配置缺失、类型错误或配置中心异常时使用代码
默认值,不会因配置中心异常导致行情功能整体不可用。
### F6:业务组员接入
**目标**:客服、投顾和风控 Agent 只消费统一行情工具。
**组员需要做**:
- 在 `AgentDefinition.allowed_tools` 声明 `query_fund_quote`;
- 在对应意图白名单中声明 `fund_quote`;
- 在 `handle()` 中调用 `self.call_tool(...)`;
- 对行情缺失、降级和非交易时段给出业务解释;
- 补充本业务域的权限和边界测试。
**组员禁止做**:
- 直接导入 `hq.py`;
- 直接调用东方财富 URL;
- 自己读取行情缓存或解析外部 JSON;
- 根据行情直接下单或修改交易数据;
- 把行情数据写入交易表。
### F7:是否落行情快照表
**判断条件**:只有在需要历史回测、监管留痕、行情对账或跨进程长期查询时才进入本阶段。
**候选新增表**:
```text
fund_quote_snapshot
fund_quote_source_record
```
**前置要求**:
- 先更新 00、02 数据库文档;
- 证明没有修改任何既有表名和字段定义;
- 设计唯一键、来源、采集时间、有效时间和幂等策略;
- 增加 Alembic 迁移、schema fingerprint 和真实 MySQL 验收;
- 只新增表,不改写场内交易表。
## 五、建议开发顺序和完成标准
```text
F0 契约冻结
→ F1 Adapter
→ F2 Service
→ F3 缓存/健康
→ F4 公共工具
→ F5 配置治理
→ F6 业务接入
→ F7 可选落库
```
MVP 完成标准:F0-F4 全部通过;不要求新增数据库表。长期版本再评估 F5-F7。
## 六、测试命令
```powershell
python -m pytest tests/unit/infrastructure/test_fund_market_adapter.py tests/unit/service/test_fund_quote_service.py -q -p no:cacheprovider
python -m pytest -q -p no:cacheprovider
python -m ruff check app tests tools alembic
python -m mypy app
python tools/audit_schema.py
```
任何数据库变更都必须额外执行 schema fingerprint,并在 TODO 中记录基线对比结果。