Files
worldquant-alpha-system/docs/legacy-backtest-module.md
T

33 KiB
Raw Blame History

旧系统回测模块功能梳理

整理日期: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 模板研究:采样、评估、深度回测

模板研究采用业务表管理研究任务、采样记录及表达式:

  1. 生成采样预览及表达式,研究任务进入 pending_sampling。
  2. 启动采样,后台生产者以每批 8 条向统一队列提交,进入 sampling。
  3. 回调逐条保存 Alpha、指标、done/failed 和 passed;业务层据此统计样本与通过情况,供密度评估使用。
  4. 深度回测先生成全量表达式,执行记录进入 generated;这个动作本身不启动回测。
  5. 用户再启动深度回测,进入 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. 源码证据索引与验证范围

以下均为旧仓库本机绝对路径;代码行号对应前述提交。引用提供关键起点,相关逻辑见对应函数及邻近定义。

验证方式:对照旧代码、默认配置、前端控制入口与历史设计文档;关键并发、槽位更新、回调顺序及结果保存逻辑做了直接源码抽查,并检查本文引用的本地文件与行号有效性。没有启动旧服务、调用平台回测/恢复接口、修改旧库或运行真实账户测试。本文描述的是代码现状及其可推导限制,不将其表述为已验证的线上运行效果。