# 基金行情数据底座接入开发计划 > ⚠️ **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 中记录基线对比结果。