33 KiB
旧系统回测模块功能梳理
整理日期:2026-09-07。目标:为后续将 worldquant-constract-system 的回测能力重构到 wq-alpha-system 提供功能和实现依据。本阶段仅整理文档,不实施迁移、不启动真实回测。
代码基线:旧仓库提交 08edb0baa380eeb8102e74f8d0cff7b8987df1e3,检查时工作区干净。以下“默认值”指该代码基线的默认配置,不代表历史所有版本或生产环境的实际设置。未发现旧仓库根目录及 apps/backend/ 下的 .env,没有读取运行中进程配置或查询真实账户限额。文中 8 个任务、10 条表达式的上限是旧代码的校验和注释口径,未向当前 WorldQuant 平台重新核验。
1. 核心结论
旧系统并不是一个所有入口都共用的回测中心,而是三套执行管理方式并存:
- 预回测列表、模板研究:生产者从数据库取表达式,通过统一
BacktestQueueService提交;默认共享 6 个任务槽位,每任务通常 8 条表达式。 - Factory:每个执行实例使用独立
BacktestScheduler和模拟器;默认 3 个任务并发,每任务最多 8 条表达式。 - CLI:从配置文件读取表达式,独立模拟器执行,本地 JSON 文件记录进度。常规命令在默认配置下实际是 6 个任务并发,每任务 8 条表达式,因为旧兼容参数
--limit-multi=6会覆盖配置中的 3。
所以不能回答成“整个系统最多并发 6 个”或“最多 48 个 Alpha”。6×8=48 只是统一队列满批时所承载的表达式数;Factory、CLI、其他进程以及浏览器里的平台操作没有纳入这个队列的共享预算。平台真正如何执行批内表达式,也不能由本地槽位数推出。[S1–S5]
旧系统最值得保留的能力是:待回测表达式管理、参数分组与切批、空槽补位、结果回调、业务批次关联、暂停后继续提交,以及基于 progress URL 的结果找回。最需要在重构时重新定义的是:账户级并发、逐表达式状态、任务持久化、结果归属和“回测成功”的口径。
2. 管理对象与边界
| 对象 | 在旧系统中的含义 | 主要保存位置 |
|---|---|---|
| 表达式及回测参数 | 一次 Alpha 模拟的输入;相同表达式配不同参数仍可形成不同实验 | pre_simulate_expressions、Factory/模板研究表达式表、CLI 输入 JSON |
| 平台提交任务 | 一次向 simulations 接口提交的单个或多个配置;本地并发控制以此为单位 | BacktestTask / 模拟器内部 task;统一队列部分主要在内存 |
| 业务批次 | 给表达式和 Alpha 归组的业务标识,并不等于一个平台任务 | batch_tracking、表达式 batch_id、Alpha 业务字段 |
| 研究执行 | Factory 一次执行、模板一次采样或深度回测;可包含多个平台任务 | 各业务模块专用表 |
| 平台模拟标识 | 提交返回的 progress_url;多模拟还有 parent/child simulation ID |
运行时对象、结果对象、部分错误日志 |
| Alpha 结果 | 平台生成的 alpha_id、表达式、指标、检查摘要等 |
alphas,alpha_id 唯一 |
SimulationConfig 包含 expression、decay、region、universe、neutralization、instrument type、delay、truncation、pasteurization、unit/nan handling、language、visualization、maxTrade。旧序列化固定提交 type=REGULAR,不能直接据此认定支持 SUPER 回测。[S6]
同一平台任务按四个字段保持一致:region / delay / language / instrument_type。按这些字段分组后再切批;不是把所有参数都相同的配置才合并。模拟器的批量入口负责自动分组,run_single_task() 要求上游已经分好组,并再次检查一致性与数量。[S6、S7]
3. 用户以前怎样管理回测
3.1 预回测列表:从表达式池提交
典型流程是:准备待回测表达式并关联业务批次 → 在列表选择表达式、指定批次或执行全部 → 后台生产者读取数据库 → 分组、按 8 条切批 → 等待队列空位 → 提交任务 → 回调更新状态和 Alpha 归属。
启动支持 expression_ids、batch_id、include_completed。前两者都不传时,范围是全部匹配记录;默认查询只排除 completed,因此 failed 和遗留 running 也可能重新进入提交,而不只是 pending。API 先返回,再异步执行。[S8]
状态实际写入 pending → running → completed/failed,但 Schema、状态筛选和部分统计只认识 pending/completed/failed。这使“正在运行”在列表统计中没有完整表达,不能把统计缺口当作记录丢失。[S8]
页面提供“停止推送”,含义是停止后续批次进入队列。已经提交的任务继续运行;未提交部分保持或恢复为 pending。没有针对单个已运行任务的远端取消,也没有预回测列表专用的 pause/resume 工作流;继续执行主要靠再次启动。[S8]
3.2 模板研究:采样、评估、深度回测
模板研究采用业务表管理研究任务、采样记录及表达式:
- 生成采样预览及表达式,研究任务进入
pending_sampling。 - 启动采样,后台生产者以每批 8 条向统一队列提交,进入
sampling。 - 回调逐条保存 Alpha、指标、
done/failed和passed;业务层据此统计样本与通过情况,供密度评估使用。 - 深度回测先生成全量表达式,执行记录进入
generated;这个动作本身不启动回测。 - 用户再启动深度回测,进入
running,同样按每批 8 条使用共享队列,最终更新研究与深度执行统计。
暂停通过内存 stop flag 阻止继续推送,已提交部分继续完成。采样恢复从 stopped 的 pending 表达式继续;深度启动接受 generated/stopped,继续未提交部分。业务记录持久化不等于运行中的队列任务持久化:没有据此证明进程崩溃后可以自动接续所有平台任务。[S9]
3.3 Factory:独立的自动研究调度
创建 Factory execution 时,先生成一阶表达式和 JSON,返回 pending_review,不会立即回测。用户启动后,独立调度器读取 pending 表达式,混合轮转一阶/二阶任务,维护运行中的批次集合。完成结果写到 FactoryExpression.alpha_id/backtest_status/passed,并衔接减枝、通过统计与增量二阶生成;没有待执行和可生成工作时结束 execution。[S3]
暂停 API 写数据库 paused,调度循环读取该状态后停止补充任务;停止还会发送内存 stop event,并写 stopped/end_time。启动或恢复会把遗留 running 表达式重置为 pending 后重新调度。这是重新提交候选表达式,不能等同于继续轮询先前已被平台接受的任务。[S10]
3.4 CLI:文件驱动回测
CLI 读取配置文件,把未处理配置交给独立模拟器。状态保存在 src/cli/.progress/{source}.progress.json,包括最后处理位置、失败配置、状态和时间戳;--resume 从已记录位置之后继续。status 命令用于查看和清理进度文件。[S4、S11]
第一次 Ctrl+C 设置停止标志;模拟器只在向线程池提交任务时检查它,已经排入线程池的工作不会因该标志自动取消。因为配置会较快地一次性排入线程池,所以“暂停”不保证立即停止后续平台提交。进度主要在模拟器整次返回后更新,并非每个 child 完成后就可靠写检查点。[S7、S11]
CLI 还提供 retry-timeout:从失败配置中识别轮询超时,查询原模拟是否完成、找回 Alpha,并输出 .retry.json / .retry_failed.json。这是结果补取路径,不等于通用自动重试队列。[S11]
4. 并发到底是多少
4.1 默认值与生效范围
| 层级或入口 | 默认并发任务数 | 默认单任务表达式数 | 配置与边界 |
|---|---|---|---|
通用 SimulationSettings |
3 | 8 | max_concurrent_tasks 校验 1–8;max_alphas_per_task 校验 1–10;可从 .env 读取 |
统一 BacktestQueueService |
6 | 输入模型允许 1–10 | 自行初始化 6 个 semaphore、6 个线程和并发为 6 的模拟器;覆盖通用并发默认 3 |
| 预回测列表 | 共享上行 6 | 8 | producer 对 batch_size 再取 min(batch_size, 8) |
| 模板采样、深度回测 | 共享上行 6 | 8 | 两个服务的 DEFAULT_BATCH_SIZE=8,没有各自独立的队列预算 |
| Factory 单个 scheduler | 3 | 最多 8 | API 传 simulation_settings.max_concurrent_tasks,批大小取 8 与配置上限的较小值 |
| CLI 常规命令、所有默认值 | 实际 6 | 8 | 默认 --limit-multi=6 与通用配置 3 不相等,触发覆盖;实际切批由模拟器配置决定 |
| 裸调用模拟器且不覆盖 | 3 | 8 | 使用通用配置;单个实例内部生效 |
以上均为代码默认值。[S1–S5]
CLI 的兼容逻辑尤其容易误解:它先读取 --max-concurrent-tasks 或通用配置,再判断 limit_multi != simulation_settings.max_concurrent_tasks,若不相等就用 limit_multi 覆盖。因而即使显式指定了新参数,也可能被默认旧参数覆盖;--limit-children=10 虽仍显示,却不决定模拟器实际切批的默认 8。[S4、S7]
4.2 统一队列如何占槽和补位
flowchart TD
A[数据库表达式:预回测 / 模板研究] --> B[生产者按参数分组并切批]
B --> C[wait_for_slot 等待提交容量]
C --> D[submit_task 创建内存异步任务]
D --> E[获取 asyncio semaphore]
E --> F[线程池调用同步模拟器]
F --> G[模拟器 threading semaphore]
G --> H[提交平台任务并轮询结果]
H --> I[获取 Alpha 详情并保存]
I --> J[队列再次执行结果保存]
J --> K[释放信号量并移除运行登记]
K --> L[通知生产者补位]
K --> M[执行业务完成回调]
L --> C
这里有三层容量控制:生产者按未完成任务数量做背压、队列的异步信号量与线程池控制工作数量、模拟器的线程信号量控制该实例内部执行。提交和轮询都在同一个占槽周期内,遇到 429 后 sleep 也继续占槽。不是 POST 返回后就立即释放槽位。[S2、S7]
实际代码在释放执行槽位、移除 _running_tasks 并通知生产者后才执行业务回调,因此下一批可以在上一批回调完成前开始。旧设计文档中“回调完成后再补位”的描述不完全符合当前实现。[S2]
生产者 wait_for_slot() 使用 condition 等待,完成任务会 notify_all();默认每 60 秒醒来检查关闭状态。它不原子预留名额,submit_task() 本身也不保证所有调用者先经过这一等待,因此多生产者唤醒竞争时,已登记任务数量可能超过软限制;实际执行仍被底层信号量约束。没有发现按业务来源分配配额、优先级或持久化公平调度机制。[S2]
4.3 动态修改槽位的实际效果
监控界面可以把槽位设置为 1–8,后端 update_max_slots() 只修改内存 _max_slots:
- 降到 3:使用
wait_for_slot()的生产者会等未完成任务减少后再补充,已有任务继续运行。 - 升到 8:允许更多任务被登记,但初始化的 semaphore、线程池和模拟器仍为 6;不能据此宣称实际执行能力变为 8。
- 进程重启后重新按默认 6 初始化,动态修改没有持久化。
“设置值”“已登记未完成数”“真正执行数”应分别理解,旧监控把它们部分合并显示了。[S2]
4.4 为什么不是账户级全局限流
统一队列只是 Python 进程内单例。Factory 和 CLI 创建独立模拟器,不共享它的 semaphore;多个后端进程也会各自创建一份队列。例如统一队列 6 个加一个 Factory 的 3 个,代码上没有共同的 8 槽账户预算来协调。是否被平台接受、何时返回 429,由平台当时状态决定。[S2–S4]
旧代码有并发数量限制和遇限流等待,但没有由这些模块共同使用的账户级请求速率、每日额度预算或自适应降并发控制。不能把“最多 8”理解为自动识别账户剩余名额。
5. 提交、轮询和失败处理
5.1 正常路径
模拟器对一个配置提交 JSON 对象,对多个配置提交数组;要求返回 HTTP 201 和 Location。单模拟直接轮询 progress URL 获取 Alpha;多模拟先轮询父任务得到 children,再在本地逐个轮询 child。这里“逐个”指本地查询顺序,不表示平台按顺序运行表达式。[S7]
成功结果包含 alpha_id、表达式、progress URL、simulation ID、parent ID、child index、完成时间和可选完整指标。取得 Alpha 后另行请求详情,尝试写入本地数据库;详情获取或保存异常不会自动把平台已完成结果改为失败。[S6、S7、S12]
5.2 重试与超时
| 场景 | 旧实现处理 |
|---|---|
| 提交返回 429 | 固定等待 8 秒重试,不采用该响应的 Retry-After;提交循环最多 100 次 |
| 提交返回 401 | 强制重新认证,成功后等待 2 秒重试 |
| 提交网络异常 | 默认等待轮询间隔 5 秒后重试,受提交次数上限约束 |
| 提交其他非 201 或缺少 Location | 抛出 API 异常;不进入正常轮询 |
| 单模拟 / 父模拟轮询 | 最多 300 次;默认间隔 5 秒,依据 Retry-After 调整;429 默认回退 8 秒 |
| child 轮询 | 每个最多 100 次;404 直接生成失败结果 |
| 轮询 401 | 尝试检查会话并重新登录;不同于提交阶段的强制认证路径 |
| child 的 DAILY_SIMULATION_LIMIT WARNING | 旧实现特殊映射为 COMPLETE,并尝试取 Alpha;其他 WARNING 判失败 |
| 队列完成回调 | 最多等待 120 秒,超时/异常记录错误,不自动重试 |
300×5 秒只能作为无额外请求耗时、无 Retry-After 变化时的粗略等待量,不是严格的 25 分钟墙钟超时;父任务轮询和各 child 轮询也有各自预算。队列没有独立的整个任务总时限。[S7、S2]
这些机制是 HTTP/轮询层的重试。业务 task 失败后,统一队列不会按 retry_count 自动重新入队;重新提交失败表达式和找回已提交结果是另两类操作,应分开理解。
6. 结果、状态和批次如何关联
6.1 结果保存
模拟器已经尝试保存完整详情,统一队列随后还会再保存一次。批量保存按 alpha_id 去重,用 SQLite ON CONFLICT DO NOTHING,遵循先写入者保留的策略;失败时退回查询后插入。因此重复落库通常不会重复插入相同 Alpha,但也不保证把旧记录刷新到最新。[S7、S12]
队列对缺详情的 COMPLETE 结果会构造基础指标,并把部分缺失值写成 0。_persist_alphas_sync() 返回的 alpha_ids 来自模拟器结果,而不是数据库逐条成功确认;保存函数返回失败统计也不必然抛出。因此“Alpha ID 已返回”不能证明“完整指标已可靠入库”。[S2、S12]
6.2 成功统计存在多个口径
| 位置 | 现有口径 | 影响 |
|---|---|---|
队列 completed_count |
执行未抛异常即加 1 | 一个批内全部结果 FAILED,也可能计入已完成任务 |
队列 failed_count |
整体执行抛异常才加 1 | 不是失败表达式总数 |
BacktestResult.is_success |
success_count > 0 且没有整体 error |
部分成功也算任务成功 |
| 预回测完成回调 | 按 task 的 is_success 更新整批表达式 | 只要一条成功,整批可能全被标记 completed |
| 模板研究回调 | 逐结果更新表达式 done/failed 和 passed | 比预回测整批状态更细,但仍依赖回调成功 |
CLI SimulationBatchResult |
成功数按 Alpha 累加,task_count 按平台任务数 | success_rate 的分子分母单位不同,不能直接作正确成功率 |
“执行完成”“平台成功”“详情完整”“落库成功”“业务筛选通过”在重构时必须分别定义。[S2、S6–S9]
6.3 批次是归组标签,不是执行状态机
batch_tracking 保存 batch_id、名称、来源、说明与时间,没有任务状态、重试次数或执行检查点。表达式通过字符串 batch_id 归属批次;Alpha 通过 business_type/business_id 关联,缺少数据库外键保证。[S13]
批次页提供搜索、分页、详情、改名、删除和统计。统计分两部分:按表达式 batch_id 聚合状态,按 Alpha.business_type='batch' 且 business_id 匹配统计 Alpha。删除批次仅删除批次元数据,保留表达式和 Alpha,可能留下悬空业务引用。[S13]
预回测回调还按数据库查出的表达式顺序与成功 alpha_ids 顺序进行位置配对。发生部分失败或结果缺失时,不能保证这种配对正确,尤其当同一个 task 包含多个业务批次时;迁移时应使用明确的表达式 ID 与结果对应关系。[S8]
7. 停止、恢复与错误找回
| 动作 | 实际改变什么 | 不能据此保证什么 |
|---|---|---|
| 预回测停止推送 | 停止 producer 后续提交 | 不取消已进入队列或平台的任务 |
| 模板研究暂停/继续 | stop flag 停止生产;继续 pending 表达式 | 不保证重启后自动恢复 running 任务 |
| Factory 暂停/停止 | 数据库状态及内存 event 控制调度 | 不代表平台停止;重置 running 再提交可能重复实验 |
| CLI Ctrl+C / resume | 停止标志及本地位置文件 | 不保证已排队工作被取消或逐结果实时保存进度 |
| 队列关闭 | 停止接收,最多等待 30 秒,超时取消异步任务 | cancel() 不保证终止线程内同步网络工作,也不保证回调已完成 |
| 错误恢复页 | 查询既有平台任务、补取并保存 Alpha | 不重新提交回测,不修复原表达式状态或原研究任务状态 |
队列运行任务、producer 管理状态、槽位设置和 callback error 列表主要存在内存;数据库虽然保存了表达式状态,却没有完整持久化的统一执行日志。生产者 manager 只有一个当前 producer 指针,启动 API 没有拒绝重复运行的明确保护,重复启动可能覆盖控制对象而让旧 producer 继续运行。[S2、S8–S11]
7.1 错误恢复页的范围
错误写到按日期划分的 apps/backend/src/logs/backtest_errors/backtest_errors_YYYY-MM-DD.jsonl。每条含时间、task_id、错误文本、progress URL、来源、表达式数和配置摘要。[S14]
恢复资格靠错误文本包含 429 / rate limit / polling timeout / Too Many Requests,并能取得 progress URL。用户选择日期、任务和目标批次后,服务查询原父模拟;未完成返回 still_running,完成则读取 children、找回 Alpha 详情并更新或插入数据库。它是人工发起的一次结果找回,未完成时要再次发起,不是后台持续恢复器。[S14]
边界:恢复路径按父任务 children 读取,不能据此认定兼容所有单模拟响应;无 progress URL 的失败无法走此路径。恢复不会更新原 pre_simulate_expressions 或研究表达式状态,没有任务级恢复闭环。
恢复后的 Alpha 使用 business_type='error_recovery',普通批次页却只统计 business_type='batch',所以恢复成功也可能不出现在目标批次 Alpha 数中。恢复已有 Alpha 会覆盖指标及业务归属;目标 batch_id 直接写入,缺少存在性校验。[S13、S14]
8. 页面、接口与可观测性
| 入口 | 功能 | 主要接口或控制位置 |
|---|---|---|
| 预回测列表 | 选中/批次/全部启动、停止推送、状态查看 | /api/v1/pre-simulate/backtest、/backtest/stop、/backtest/producer/status |
| 回测监控 | 槽位、运行任务及表达式配置、progress URL、累计完成/失败、回调错误 | /api/v1/pre-simulate/backtest/stats、/backtest/slots |
| 错误日志 | 按日期查询原始错误 | /api/v1/pre-simulate/backtest/error-logs/dates 及日期查询 |
| 错误恢复 | 日期与任务选择、目标批次、恢复结果 | /api/v1/backtest-errors/dates、/{date}、/recover |
| 批次管理 | 业务归组元数据与统计 | api/batch_tracking.py |
| 模板研究 / Factory | 专属执行状态、表达式和通过统计 | 各自 API 与业务表 |
| CLI status | 文件级进度查询、清理 | 本地 .progress JSON |
上表省略相同前缀的路径均相对其本行首项;最终接口拼接可从源文件定位。[S8–S11、S13–S15]
监控页自动刷新默认关闭,开启后每 2 秒查询队列统计。预回测列表则在进入页面、启动或人工刷新时加载状态。监控仅涵盖统一队列,不是 Factory/CLI 的全局运行视图。[S15]
running_tasks 统计的是已登记、尚未移除的异步 task,其中可能包括等待 semaphore 的任务;不包括已被移除但仍在回调中的任务。启动 API 的 tasks_submitted 还按每 10 条表达式估算,实际生产者却按每 8 条并分组,因此返回值不是实际提交任务数。[S2、S8]
9. 与旧设计文档的差异
旧仓库 docs/backtest-queue-service-design.md 是设计与阶段记录,不能直接作为最新功能说明:
| 旧文档容易造成的理解 | 当前代码事实 |
|---|---|
| 统一队列未来统一 Factory/CLI | 两者仍独立;模板研究已经接入共享队列 |
| 6 槽、每 task 最多 10 条即可概括 | 底层配置默认 3/8,队列覆盖为 6,常用 producer 是每批 8 |
| 修改最大槽位即可改变实际并发 | 当前只修改软提交限制,底层执行资源未同步调整 |
| 回调完成后再补位 | 当前移除任务、通知补位后才运行回调 |
| 回调前完成 alphas 保存即数据安全 | 保存失败可只返回统计;回测成功与持久化成功未形成可靠闭环 |
| 已完成计数等于成功回测数 | 队列正常返回、逐 Alpha 成功和业务通过是不同口径 |
以本次源码证据为准;旧文档保留作历史背景,未作修改。
10. 后续重构需要保留的功能与待决策事项
以下是从旧功能提炼的后续候选范围,不是已批准的实现方案。当前新项目计划将回测列在后续阶段,本次不改变首期只读平台的边界,也不要求迁移旧数据库。
| 功能主题 | 应保留的用户能力 | 后续需要明确的规则 |
|---|---|---|
| 表达式池 | 待执行列表、按选择/批次启动、参数预览 | 重复实验如何识别;是否允许运行中再次提交 |
| 业务批次 | 命名、来源、结果归组、统计 | 批次与执行记录分开;一个 Alpha 能否归属多个实验 |
| 调度 | 同参数分组、切批、空槽补位 | 单账户共同预算;软等待数与真实运行数分开;公平性 |
| 配置 | 并发和单批数量调整 | 统一配置来源、变更何时生效、是否持久化 |
| 状态 | 查看总体及逐表达式进度 | 平台状态、落库状态、业务通过状态分别存储 |
| 暂停与恢复 | 停止继续提交、恢复未完成工作 | 停止生产/本地取消/远端取消分别定义;崩溃后先对账再决定重提 |
| 错误处理 | 展示原因、重试失败、找回平台结果 | 可重试分类、次数与总时限;有无 progress URL 分流 |
| 结果管理 | Alpha 指标、原输入、结果关联 | ID 映射、缺失值保持未知、可靠保存与重复回调幂等 |
| 研究编排 | 采样、密度评估、深度回测、Factory 后处理 | 哪些首批迁移、哪些保持为业务层;不要把减枝塞进通用执行器 |
| 监控 | 队列、任务详情、错误、运行时间 | 统一覆盖所有入口;重启后历史仍可查询 |
后续设计至少应验证:混合参数分批、单表达式与多表达式响应、多个来源争用名额、运行中升降并发、部分成功、详情/落库失败、429/401/超时、暂停后继续、重复启动、进程重启和找回结果后的原任务状态修复。本轮未执行这些运行验证。
11. 源码证据索引与验证范围
以下均为旧仓库本机绝对路径;代码行号对应前述提交。引用提供关键起点,相关逻辑见对应函数及邻近定义。
- S1 配置:SimulationSettings。
- S2 队列:初始化、提交与执行、保存、动态槽位与关闭、背压等待、任务结果模型。
- S3 Factory 调度:调度器、创建仅准备、调用模拟器。
- S4 CLI 配置:参数默认、兼容覆盖、独立模拟器。
- S5 生产者批量:预回测、采样、深度回测。
- S6 模型:分组字段及配置、结果及统计。
- S7 模拟器:实例信号量与会话、批量执行、单任务提交、单模拟轮询、父模拟轮询、子模拟轮询、分组算法。
- S8 预回测管理:生产者管理、提交流程、回调及取数、启动 API、控制 API、Schema 状态。
- S9 模板研究:采样预览、采样推送、采样恢复、深度生命周期、完成回调、研究模型。
- S10 Factory 控制:暂停、启动与恢复、停止、结果模型。
- S11 CLI 恢复:进度文件、停止控制、位置更新、状态命令、超时结果找回。
- S12 Alpha 保存:详情读取、指标落库字段、批量插入去重。
- S13 批次:批次模型、统计、删除行为、Alpha 业务归属。
- S14 错误找回:错误日志、恢复筛选、查询原模拟、保存及改归属、API。
- S15 监控与生命周期:监控页、刷新频率、列表刷新、应用初始化、应用关闭。
验证方式:对照旧代码、默认配置、前端控制入口与历史设计文档;关键并发、槽位更新、回调顺序及结果保存逻辑做了直接源码抽查,并检查本文引用的本地文件与行号有效性。没有启动旧服务、调用平台回测/恢复接口、修改旧库或运行真实账户测试。本文描述的是代码现状及其可推导限制,不将其表述为已验证的线上运行效果。