---
title: EMA 追踪任务化与读模型说明
doc_type: feature-implementation
domain: market
keywords:
  - EMA Tracker
  - 自定义 EMA
  - 任务物化
  - mkt_ema_tracker_record
summary: 说明 EMA Tracker 如何从“查询时现算”改成“后台任务物化 + 页面只读结果”。
when_to_read:
  - 要改 EMA Tracker 查询或同步逻辑
  - 想确认多来源日线查询结果、自定义 EMA 和 Tracker 命中记录的职责边界
read_next:
  - ../operations/README.md
  - ../business/data-fetch-tasks.md
---
# EMA 追踪任务化与读模型说明

这篇文档只讲 EMA Tracker 这条链路现在怎么落。

如果你现在关心的是“为什么页面不能再现场重算”，或者“日线同步后哪些本地产物必须已经就绪”，先看这里。读完后如果要跑任务或做手工验收，再去 `operations/`。

## 现在的正式口径

EMA Tracker 已经改成两段本地任务产物：

1. 先读取最近窗口内的多来源日线查询结果，按股票重算自定义 `EMA13 / 34 / 120`
2. 再把“回踩 EMA120”命中的记录物化到专用读模型表

页面层现在只调用 `query_ema_tracker` 读取结果，不再在查询链里做下面这些事：

- 不再全市场扫描多来源日线查询结果
- 不再按股票现算 EMA
- 不再在查询里回写 `stock_indicator_ema`
- 不再先算完整结果再内存分页

## 表与职责

这条链当前有 2 张正式落点：

- `stock_indicator_ema`
  负责承接自定义 `EMA13/34/120` 结果，供其他行情读模型和图表复用
- `mkt_ema_tracker_record`
  负责承接 EMA Tracker 命中结果，是页面查询的正式读模型

`mkt_ema_tracker_record` 当前按 `(stock_code, market, trade_date)` 唯一。

它只保存命中记录，不保存全市场全量快照。当前命中口径固定为“最近 30 天窗口内、偏离度不高于 10% 的 EMA120 回踩候选”；页面查询再在这批物化结果上做更窄的 `tolerancePct` 过滤。

## 任务链

当前正式任务链如下：

```mermaid
flowchart LR
    A["tushare.stk_factor_pro.sync\n落 stk_factor_pro 单日快照"] --> B["ema.tracker.sync\n重算最近 30 天自定义 EMA + 命中记录"]
    B --> C["stock_indicator_ema"]
    B --> D["mkt_ema_tracker_record"]
    D --> E["query_ema_tracker\n只读分页查询"]
```

约束：

- `tushare.stk_factor_pro.sync` 仍然只负责 `stk_factor_pro` 单日快照事实
- `ema.tracker.sync` 是纯本地后处理任务，不直接访问外部 provider
- `query_ema_tracker` 只能读 `mkt_ema_tracker_record`

## 查询 ready 口径

页面查询会先用正式交易日 helper 推导“最新已完成且 daily ready 的交易日”，然后再要求同一天的 `ema-tracker:<market>:<trade_date>` 同步状态已经成功。

这意味着：

- 如果最新交易日的 Tracker 物化任务还没跑，页面会直接报“结果未就绪”
- 不允许再回退到查询时现场补算
- 如果最新交易日本来就没有命中记录，只要 `ema.tracker.sync` 成功，也应返回空列表而不是报错

## 为什么不用原后端的查询实现

原后端里 `EmaTrackerBusinessService.queryPullbackToM120()` 仍然保留了运行时计算。

这次只借原项目的“任务化产物”思路，不复用它的查询时现算逻辑。原因很简单：

- 当前桌面端的性能问题正是由查询时全市场重算导致的
- 把旧查询逻辑原样迁入，只会换个位置继续慢

一句话：

`原项目可借的是任务链，不是运行时查询。`
