# P19：历史指数回测 + 任务引擎模拟

> 版本：v1.0 | 日期：2026-03-29 | 来源：本轮对话需求讨论

---

## 1. 背景

任务引擎（P17 + OS 分级分配）已实现，但缺乏历史验证手段。需要：
- 回测过去一个月的指数变化，验证指数算法的稳定性
- 模拟任务引擎运行一个月，验证分级分配、升级、转移逻辑的合理性
- 最终数据落库（`test_zqyy_app`），可在小程序和管理后台中查看

## 2. 需求拆分

### 2.1 指数历史回测（Phase 1）

**目标**：给 4 个指数任务加 `as_of_date` 参数，支持"假装今天是 X 日"重算指数。

**涉及任务**：
| 任务 | 输入数据源 | 时间依赖点 |
|------|-----------|-----------|
| `DWS_WINBACK_INDEX` (WBI) | `dws_member_visit_detail` + `dws_member_consumption_summary` | 距上次到店天数、到店频率衰减 |
| `DWS_NEWCONV_INDEX` (NCI) | `dws_member_visit_detail` + `dws_member_consumption_summary` | 新客首次到店后的天数 |
| `DWS_RELATION_INDEX` (RS/OS/MS/ML) | `dwd_assistant_service_log` | 服务记录的时间衰减（halflife） |
| `DWS_SPENDING_POWER_INDEX` (SPI) | `dws_member_consumption_summary` | 消费金额时间窗口 |

**改动要点**：
- 每个指数任务的 `_do_extract()` 中，将 `NOW()` / `CURRENT_DATE` 替换为 `as_of_date` 参数
- 衰减计算中的"距今天数"改为"距 as_of_date 天数"
- 输出表新增 `calc_date` 字段（或复用 `calc_time`），标记是哪天的快照

**回测参数**：
- 时间范围：过去 30 天（2026-02-27 ~ 2026-03-29）
- 回测间隔：每 6 小时一个快照（共 120 个快照点）
- 数据落库：每个快照覆盖写入 ETL 测试库的指数表（delete-before-insert by calc_date）

**CLI 接口设计**：
```bash
# 单次回测（指定日期）
python -m cli.main --tasks DWS_WINBACK_INDEX --as-of-date "2026-03-01"

# 批量回测（日期范围 + 间隔）
python scripts/ops/backtest_indexes.py \
  --start "2026-02-27" --end "2026-03-29" \
  --interval-hours 6 \
  --store-id 2790685415443269
```

### 2.2 任务引擎模拟（Phase 2）

**目标**：基于回测的指数快照，模拟任务引擎运行一个月，数据落入业务测试库。

**模拟参数**：
- 时间范围：2026-02-27 ~ 2026-03-29
- 模拟粒度：每小时一次（720 次循环）
- 指数数据：使用 Phase 1 回测的快照（每 6 小时更新一次，中间小时复用最近快照）
- 回店判定：使用 DWD 真实服务记录（`dwd_assistant_service_log.create_time`）

**模拟流程（每小时）**：
```
1. 设置模拟时钟 sim_time
2. 如果 sim_time 是 6 小时整点 → 切换到对应的指数快照
3. 检查 DWD 服务记录中 create_time 在 [sim_time-1h, sim_time] 的记录
   → 匹配 active 召回任务 → 标记 completed（completed_at = sim_time）
4. 检查过期任务（expires_at < sim_time）→ 标记 abandoned
5. 执行任务生成逻辑（OS 分级分配）：
   a. MAIN 助教：生成召回/关系构建任务
   b. COMANAGE：仅生成关系构建；检查升级条件（升级倍数 ≥ 3）
   c. 转移检查（升级倍数 ≥ 5）
6. 已完成的召回任务 → 生成 follow_up_visit（48h 保留期）
7. 记录当小时的任务快照
```

**数据落库**：
- 所有任务写入 `biz.coach_tasks`（`created_at` 用模拟时钟值）
- 历史记录写入 `biz.coach_task_history`
- 转移日志写入 `biz.coach_task_transfer_log`

**期望输出**：
1. 每天的任务数量变化（按类型分组）
2. 一个月后各类型任务的最终分布
3. COMANAGE 升级触发次数和时间点
4. POOL 转移触发次数和时间点
5. 回访任务的生成数量和完成率
6. 每个助教的任务负载分布

### 2.3 输出报告

**脚本输出**：
- 控制台：每天一行摘要（日期 | 新增 | 完成 | 升级 | 转移 | 总 active）
- CSV 文件：`export/backtest/task_simulation_daily.csv`（每天快照）
- JSON 文件：`export/backtest/task_simulation_summary.json`（最终统计）

## 3. 技术约束

- 指数回测和任务模拟都在测试库执行（`test_etl_feiqiu` / `test_zqyy_app`）
- 模拟脚本放 `scripts/ops/`，遵循现有脚本规范
- 环境变量从根 `.env` 加载（`load_dotenv`）
- 指数回测需要 ETL 库连接（`PG_DSN`），任务模拟需要业务库连接（`APP_DB_DSN`）
- 模拟前清空 `coach_tasks` / `coach_task_history` / `coach_task_transfer_log`

## 4. 实施顺序

1. Phase 1a：给 `RelationIndexTask`（RS/OS/MS/ML）加 `as_of_date` 支持
2. Phase 1b：给 `WinbackIndexTask`（WBI）加 `as_of_date` 支持
3. Phase 1c：给 `NewconvIndexTask`（NCI）加 `as_of_date` 支持
4. Phase 1d：给 `SpendingPowerIndexTask`（SPI）加 `as_of_date` 支持
5. Phase 1e：编写批量回测脚本 `backtest_indexes.py`
6. Phase 2：编写任务模拟脚本 `simulate_task_engine.py`

## 5. 依赖

- P17：助教客户归属与任务生成引擎（已完成）
- OS 分级分配改动（本轮已完成）
- DWS_TASK_ENGINE ETL 任务（本轮已完成）
- DWD 层服务记录数据（已有）

## 6. 关键文件参考

- 指数任务：`apps/etl/connectors/feiqiu/tasks/dws/index/`
- 任务生成器：`apps/backend/app/services/task_generator.py`
- 召回检测器：`apps/backend/app/services/recall_detector.py`
- FDW 查询：`apps/backend/app/services/fdw_queries.py`
- 参数配置：`biz.cfg_task_generator_params`（16 条参数）
