feat(selection): 迁移知行B1选股策略

This commit is contained in:
yuxuanhui
2026-08-08 22:41:45 +08:00
parent 0c999fb828
commit e9d06df5de
32 changed files with 2423 additions and 0 deletions
+1
View File
@@ -9,6 +9,7 @@
| [目录与模块边界](./directory-structure.md) | 包结构、bounded context 和导入边界 |
| [配置与运行时](./configuration-and-runtime.md) | `Settings`、应用工厂和部署环境 |
| [市场数据同步](./market-data-sync.md) | Tushare qfq、PostgreSQL、CSV 快照和一次性 Job 契约 |
| [历史选股](./selection.md) | selection bounded context、目标交易日、qfq 读取和信号结果契约 |
| [HTTP 契约](./http-api-contracts.md) | 路由组合、响应模型和同源 API 路径 |
| [错误处理](./error-handling.md) | 当前 FastAPI 错误行为及跨层错误传递 |
| [质量与测试](./quality-guidelines.md) | Ruff、Pyright、pytest 及禁止模式 |
+83
View File
@@ -0,0 +1,83 @@
# 历史选股代码规格
## Scenario: `zhixing_b1` 历史公式评估
### 1. Scope / Trigger
- 触发:新增 `modules/selection` bounded context,基于 PostgreSQL 已保存的 qfq
日线执行历史知行 B1 公式。
- 边界:selection 只读市场事实并返回领域评估结果;不负责市场数据同步、信号
持久化、批次调度、HTTP 路由或前端展示。
### 2. Signatures
- `MarketDataReader.load_history(ts_code: str, target_trade_date: date) -> StockHistory`
- `ZhixingB1Strategy.evaluate(history: StockHistory, target_trade_date: date) -> SelectionEvaluation`
- `EvaluateZhixingB1.execute(ts_code: str, target_trade_date: date) -> SelectionEvaluation`
- `SelectionSignal.identity -> tuple[str, date, str, str]`
### 3. Contracts
- `StockHistory.bars` 必须是升序、去重的 qfq 行情,且不得包含目标交易日之后的
数据;`daily_basic` 按交易日保存同日可空指标。
- `SelectionBar` 使用有限的 `float | None` 表示 OHLCV;目标日的 open/high/low/
close/volume 任一缺失时不得生成信号。
- 策略名称固定为 `zhixing_b1`;7 个分类固定为
`zhixing_b1_oversold_turn`、`zhixing_b1_oversold_volume`、
`zhixing_b1_original_b1`、`zhixing_b1_extreme_volume`、
`zhixing_b1_pullback_white`、`zhixing_b1_pullback_super`、
`zhixing_b1_pullback_yellow`。
- 同一股票同一交易日可以返回多个分类;唯一身份是
`(ts_code, target_trade_date, strategy, category)`,返回顺序遵循
`ZHIXING_B1_SIGNAL_ORDER`。
- PostgreSQL reader 必须参数化查询 `source_adj = 'qfq'` 且
`trade_date <= target_trade_date`,左连接同日 `market_daily_basic`;不得回退到
CSV、Tushare 或当前最后一行。
- 评估状态区分 `selected`、`no_signal`、`insufficient_history`、
`missing_target_bar` 和 `data_error`。业务状态不是异常,数据库读取失败才映射为
`data_error`。
### 4. Validation & Error Matrix
| 条件 | 行为 |
| --- | --- |
| 少于公式最小暖机长度 | 返回 `insufficient_history`,不返回信号 |
| 目标日没有 qfq bar 或 OHLCV 不完整 | 返回 `missing_target_bar` |
| 目标日数据完整但无分类命中 | 返回 `no_signal` |
| PostgreSQL 读取失败 | 返回 `data_error`,保留股票和目标日上下文 |
| rolling 窗口不足 | 只使用已到达的交易行;`EVERY` 等需要完整窗口的条件不命中 |
| 除零、NaN 或无穷中间值 | 转为 NaN/False,不得静默制造命中 |
### 5. Good / Base / Bad Cases
- Good:给定历史目标日,reader 只返回该日及之前的 qfq 行,策略返回可序列化详情
和全部命中分类。
- Base:同日多分类命中时每类都保留稳定身份;同日 `daily_basic` 缺失只影响实际
依赖该指标的条件。
- Bad:用当前最新市值覆盖历史 K 线、取 bars 最后一行代替显式目标日,或把 7 类
mask OR 成一个结果后丢失分类。
### 6. Tests Required
- 指标单元测试:rolling 暖机、交易日 `REF`、窗口边界、除零、NaN、宽幅代码参数。
- 策略单元测试:目标日截断、暖机/缺失状态、7 个 mask 独立存在和同日多分类身份。
- reader 单元测试:参数化 SQL、qfq 过滤、目标日截断、升序映射和 left join 可空值。
- golden 测试:离线固定 fixture 可重复运行;不得在测试运行时导入旧项目或访问生产库。
### 7. Wrong vs Correct
#### Wrong
```python
# 会产生历史前视数据,并丢弃同日的其他分类。
target = history.bars[-1]
first = next(category for category in categories if masks[category].iloc[-1])
```
#### Correct
```python
# 用显式交易日定位,并保留所有独立 mask 的命中。
target_index = frame.index[frame["trade_date"] == target_trade_date][0]
matched = tuple(category for category in signal_order if masks[category].iloc[target_index])
```
@@ -0,0 +1,9 @@
{"file":".trellis/spec/backend/index.md","reason":"检查新增选股上下文是否遵循后端入口和模块边界。"}
{"file":".trellis/spec/backend/directory-structure.md","reason":"检查 domain、application、infrastructure、presentation 的依赖方向和目录职责。"}
{"file":".trellis/spec/backend/market-data-sync.md","reason":"检查 reader 是否只读 qfq 市场事实,并正确处理目标日、六年窗口和数据缺失。"}
{"file":".trellis/spec/backend/error-handling.md","reason":"检查数据错误是否可识别、未被吞掉,并与无信号状态区分。"}
{"file":".trellis/spec/backend/quality-guidelines.md","reason":"执行并核对 Ruff、Pyright、pytest 及直接依赖声明。"}
{"file":"docs/adr/0001-bounded-context-first-modular-monolith.md","reason":"检查没有跨上下文引入全局层或把业务规则放入 shared。"}
{"file":"docs/adr/0003-postgresql-as-market-data-store.md","reason":"检查没有把 CSV、旧项目最新市值或非 qfq 数据作为运行时事实。"}
{"file":"docs/adr/0004-tushare-six-year-snapshot-sync.md","reason":"检查历史目标日、有效数据和同步资格约束没有被策略实现绕过。"}
{"file":".trellis/tasks/08-08-migrate-zhixing-b1/research/legacy-zhixing-b1.md","reason":"检查公式迁移、7 个分类、旧实现差异和 fixture 证据是否保留。"}
@@ -0,0 +1,217 @@
# 知行 B1 选股策略迁移设计
## 1. 设计目标
在新项目中建立 `selection` bounded context,完成 `zhixing_b1` 的第一条公式级
垂直切片:策略可以接收明确的目标交易日和历史行情,按通达信公式计算 7 个
子信号,并返回可解释、可重复、可区分的多分类信号结果。
本任务不创建信号数据库表、HTTP API、前端页面或全市场批次调度。信号模型先
提供稳定身份和后续持久化所需的契约。
## 2. 上下文与依赖方向
新增目录:
```text
zhixing-server/src/zhixing_server/modules/selection/
├── domain/
│ ├── models.py # 行情输入、策略信号、评估结果
│ ├── ports.py # 市场历史读取端口
│ ├── indicators.py # TDX 风格滚动/递推指标原语
│ ├── zhixing_b1.py # 知行 B1 公式及 7 个子信号
│ └── __init__.py
├── application/
│ ├── evaluate.py # 单股票、明确目标日的策略用例
│ └── __init__.py
├── infrastructure/
│ ├── postgres_reader.py # 只读 PostgreSQL 市场数据适配器
│ └── __init__.py
└── presentation/
└── __init__.py
```
- `selection.domain` 不导入 FastAPI、Psycopg 或 PostgreSQL 适配器。
- `selection.domain.ports` 定义策略需要的最小 `MarketDataReader`,不复用旧项目
的 CSV repository,也不让策略直接拼 SQL。
- `selection.infrastructure.postgres_reader` 只读 `market_stock`、
`market_daily_bar` 和 `market_daily_basic`;市场数据写入仍归 `market_data`
bounded context 所有。
- `selection.application` 负责把目标代码、目标交易日交给 reader 和纯领域策略,
将数据缺失、无信号和基础设施错误区分开。
- 暂不把 `selection` 接入顶层 HTTP router 或 FastAPI 应用组合,避免首期引入
未确定的产品 API 契约。
## 3. 领域模型
### 3.1 输入模型
定义面向策略的只读分析模型,不把 PostgreSQL 的 `Decimal`、Tushare 字段名或
Pandas DataFrame 暴露给应用调用方:
- `SelectionBar`:`trade_date`、`open`、`high`、`low`、`close`、`volume`,价格
和成交量在适配器边界转换为与旧项目一致的有限 `float`。
- `SelectionDailyBasic`:`trade_date`、可选 `turnover_rate`、`total_mv` 等策略
可能使用的同日指标。
- `StockHistory`:股票代码、名称、升序 bars、按日期索引的 daily basic;只包含
`trade_date <= target_trade_date` 的记录。
`MarketDataReader.load_history(ts_code, target_trade_date)` 必须保证:
1. bars 按交易日升序、去重,且只来自 `source_adj = 'qfq'`;
2. 不把目标日之后的数据泄露给策略;
3. 返回足够的历史 warm-up。首期直接返回数据库保留窗口内截至目标日的全部可用
行情,避免人为截断导致 EMA/KDJ 与旧实现不一致;
4. 目标日没有有效 bar 时返回可识别的缺失状态,而不是把更早日期伪装成目标日;
5. 同日 `daily_basic` 缺失保留为可观察的缺失值,只有公式实际需要的字段才影响
可选条件。
### 3.2 信号模型
定义 `ZhixingB1Category` 的 7 个稳定语义分类,外部值不再使用旧的
`xg_composite` 前缀:
- `zhixing_b1_oversold_turn`
- `zhixing_b1_oversold_volume`
- `zhixing_b1_original_b1`
- `zhixing_b1_extreme_volume`
- `zhixing_b1_pullback_white`
- `zhixing_b1_pullback_super`
- `zhixing_b1_pullback_yellow`
`SelectionSignal` 至少包含:
- `ts_code`、`name`、`target_trade_date`;
- `strategy = "zhixing_b1"`;
- 一个 `ZhixingB1Category`;
- qfq `close`;
- 可序列化的关键详情,如 J、RSI、知行白线/黄线、振幅、成交量比和命中的
公式标签。
稳定身份为 `(ts_code, target_trade_date, strategy, category)`。同一行数据可以
产生多个 category,返回顺序固定为公式文件中 7 个子信号的优先级顺序。
### 3.3 评估结果
用例返回带状态的 `SelectionEvaluation`,至少区分:
- `selected`:至少一个子信号命中;
- `no_signal`:目标日数据完整但没有子信号命中;
- `insufficient_history`:少于公式要求的最小暖机长度;
- `missing_target_bar`:目标交易日没有 qfq 日线;
- `data_error`:市场数据适配器发生不可恢复的读取错误。
`no_signal`、`insufficient_history` 和 `missing_target_bar` 不是异常;数据库连接
或 SQL 失败才转换为带上下文的基础设施错误。
## 4. 公式实现策略
### 4.1 计算层
为保持与旧实现及通达信公式的数值语义一致,首期使用直接依赖的 Pandas/NumPy
实现向量化指标。当前 `tushare` 已将 Pandas/NumPy 带入锁文件,但新代码直接
使用它们,因此实施阶段将把 `pandas` 和 `numpy` 声明为后端直接依赖并更新
`uv.lock`。
在 `selection.domain.indicators` 内实现或迁移以下原语,并用纯输入测试锁定边界:
- `MA`、`EMA`、`LLV`、`HHV`、`SMA`、`REF`;
- `EXIST`、`EVERY`、`COUNT`、`HHVBARS`、`BARSLAST`、`CROSS`;
- TDX 风格 KDJ、RSI、知行白线/黄线;
- 板块宽幅判定及振幅区间/放宽系数;
- 大绿棒、缩量、异动、趋势、回踩和 BBI 派生条件。
旧项目的 `prepare_xg_indicators()` 可以作为迁移起点,但不得原样保留对旧项目
`SignalCategory`、`zgnb` 包或旧 CSV 字段的导入。所有跨公式共享原语先归入
`selection` 上下文,等第二个策略迁移时再根据真实复用情况决定是否上移到
`shared`。
### 4.2 7 个子信号
将旧实现的 7 个 mask 逐一迁移为命名清晰的领域计算步骤,计算结果保留每个
mask,而不是先 OR 成单一 `_存在B` 后只取第一项。最终组合逻辑为:
```text
all_matches = [category for category in priority_order if category.mask(target_row)]
```
每个 mask 必须与通达信公式逐段对照;旧实现已经存在的 v1203 调整(例如上涨
十字星的涨幅限制、原始 B1 的放宽缩量分支)作为有意语义保留,并在测试名或
fixture 说明中标明。
### 4.3 缺失值和暖机
- 公式所需 rolling 窗口不足时遵循旧实现的窗口语义,不用当前行的未来数据补齐。
- 目标日需要的 OHLCV 缺失时不生成信号。
- 中间指标出现 NaN 时,比较型条件默认不命中;除零场景显式转为 NaN/False,
不让异常被静默吞掉。
- 不使用 `except Exception` 将单个子信号错误转成全局无信号;公式实现错误应
让测试或应用调用失败可见。
## 5. PostgreSQL 只读适配器
`PostgresMarketDataReader` 使用现有 `Settings.database_url`,通过参数化 SQL
读取:
```sql
SELECT
bar.ts_code, bar.trade_date, bar.open, bar.high, bar.low, bar.close,
bar.vol, basic.turnover_rate, basic.total_mv
FROM market_daily_bar AS bar
LEFT JOIN market_daily_basic AS basic
ON basic.ts_code = bar.ts_code
AND basic.trade_date = bar.trade_date
WHERE bar.ts_code = %s
AND bar.source_adj = 'qfq'
AND bar.trade_date <= %s
ORDER BY bar.trade_date
```
适配器只负责查询、字段映射、排序和缺失状态;不写数据库、不回退到 CSV、不
调用 Tushare。查询整个六年保留窗口是首期的正确性优先选择,后续全市场运行
若证明有性能压力再引入可配置 warm-up 窗口和批量读取。
## 6. 验证策略
### 6.1 公式单元测试
为每个原语和 7 个 mask 提供边界案例,至少包含:
- rolling 窗口刚好不足、刚好满足和超过;
- `high == low`、前收为零、成交量为零、NaN/None;
- 30/68/普通代码的幅度参数;
- 大绿棒在 15 日前/后、当前最大量切换;
- 上涨十字星涨幅小于 4% 与超过 4%;
- 同一行同时满足多个 mask。
### 6.2 历史 golden
从旧项目现有 `data/raw` 中选择少量公开行情样本,抽取为新项目测试 fixture,
不让测试运行时依赖旧项目目录。固定 fixture 包含:
- 升序 qfq OHLCV CSV;
- 目标交易日和股票代码;
- 旧实现/人工确认的命中分类集合;
- 关键详情允许小数误差的期望值。
golden 只验证固定样本,不把旧实现当成新实现的运行时依赖。对于旧实现当前
只保留第一个子信号的行为,golden 记录“新实现返回全部命中分类”的有意差异。
### 6.3 端口和适配器测试
- Fake reader 测试应用用例只传入目标日前数据,并区分无信号、目标日缺失和
基础设施错误。
- PostgreSQL reader 使用 mock/fake connection 验证参数化查询、qfq 过滤、升序
映射和基本指标左连接;不访问真实网络。
- 如使用 PostgreSQL 集成测试,沿用 `ZHIXING_TEST_DATABASE_URL` marker,且
不把它作为普通单测必需条件。
## 7. 兼容性、回滚与后续演进
- 不修改旧项目目录和旧 SQLite 数据;迁移结果通过 `zhixing_b1` 新身份区分。
- 首期不创建数据库迁移,因此回滚只需移除新 selection 模块和直接依赖,不影响
已有 market-data 表及同步任务。
- 后续增加策略时,优先复用真实验证后确认的 `selection.domain.indicators`;
不预先建立跨上下文的全局指标工具箱。
- 后续实现信号持久化时,可直接使用稳定身份 `(ts_code, date, strategy,
category)` 建立唯一键;本任务不提前锁定表结构或 HTTP 字段。
@@ -0,0 +1,10 @@
{"file":".trellis/spec/backend/index.md","reason":"实现 selection bounded context 前确认后端分层、开发前检查与质量入口。"}
{"file":".trellis/spec/backend/directory-structure.md","reason":"按 domain/application/infrastructure/presentation 边界创建选股上下文,并保持导入方向。"}
{"file":".trellis/spec/backend/market-data-sync.md","reason":"读取 market_daily_bar 和 market_daily_basic 时保留 qfq、六年窗口、目标日新鲜度和数据事实源契约。"}
{"file":".trellis/spec/backend/error-handling.md","reason":"区分无信号、目标数据缺失和数据库读取失败,避免把异常静默成空结果。"}
{"file":".trellis/spec/backend/quality-guidelines.md","reason":"遵循 Python 3.12、严格类型、Ruff、Pyright 和 pytest 质量要求。"}
{"file":"docs/adr/0001-bounded-context-first-modular-monolith.md","reason":"确认选股能力应作为明确 bounded context 演进,而不是创建全局 service 或 utils。"}
{"file":"docs/adr/0003-postgresql-as-market-data-store.md","reason":"确认 PostgreSQL 是策略事实源,价格使用 qfq,当前股票池和历史分析语义不被策略迁移改变。"}
{"file":"docs/adr/0004-tushare-six-year-snapshot-sync.md","reason":"确认策略只消费有效目标日数据、六年窗口和覆盖率资格,不改同步契约。"}
{"file":"CONTEXT.md","reason":"使用现有市场数据和分析语义,补充 zhixing_b1、子信号与公式语义的项目术语。"}
{"file":".trellis/tasks/08-08-migrate-zhixing-b1/research/legacy-zhixing-b1.md","reason":"实现时按已核对的旧公式、v1203 差异和有意行为变化迁移,不把旧项目运行时导入新项目。"}
@@ -0,0 +1,153 @@
# 知行 B1 选股策略实施计划
## 实施原则
- 只修改 `zhixing-system`;旧项目只读,不回写、不重命名、不提交旧项目数据。
- 先写行为测试,再补最小实现;每一步保持 `uv run pytest` 可定位失败范围。
- `zhixing_b1` 是新策略身份;不要把 `xg_composite` 作为新模块的对外名称。
- 公式语义优先于旧 Python 的偶然行为;每个有意差异都要在 fixture 或测试名中
留下证据。
- 不启动 HTTP、前端、信号持久化或全市场批次编排;它们属于后续任务。
## 1. 依赖与模块骨架
- 在 `zhixing-server/pyproject.toml` 声明直接依赖 `pandas` 和 `numpy`,执行
`uv lock`,确认锁文件与 Python 3.12 环境一致。
- 创建 `modules/selection/{domain,application,infrastructure,presentation}`
包和上下文 README,保持 domain 不导入 FastAPI/Psycopg。
- 先新增领域模型、端口和评估状态类型,再接入基础设施。
验证:
```bash
cd zhixing-server
uv lock --check
uv run python -c "import numpy, pandas; print(numpy.__version__, pandas.__version__)"
```
回滚点:依赖或包骨架若无法通过 Ruff/Pyright,先撤销骨架,不触碰
`modules/market_data`。
## 2. 迁移公式原语
- 从旧项目 `shared/indicators.py`、`kdj.py`、`rsi.py`、`zhixing.py` 和
`market_type.py` 提取必要实现到 `selection/domain/indicators.py`。
- 先覆盖 `MA/EMA/LLV/HHV/SMA/REF/EXIST/EVERY/COUNT/HHVBARS/BARSLAST/CROSS`,
再实现 KDJ、RSI、知行线和板块幅度参数。
- 每个公共函数增加完整类型、参数/返回值/边界说明;NaN、除零和不足窗口行为
用测试锁定。
- 不引入 Numba、SciPy 或其他旧项目专用依赖;首期 Pandas/NumPy 足够保持公式
计算的向量化和数值接近。
验证:
```bash
cd zhixing-server
uv run pytest tests/unit/selection/test_indicators.py
```
## 3. 实现 `ZhixingB1Strategy`
- 将旧 `prepare_xg_indicators()` 拆成可读的领域计算步骤:基础线、振幅、KDJ/RSI、
缩量、大绿棒、异动、趋势、距离/回踩、7 个子信号。
- 策略类固定 `name = "zhixing_b1"`,公开入口显式接收 `StockHistory` 和
`target_trade_date`。
- 对目标交易日定位使用日期索引,不使用 DataFrame 最后一行猜测目标日期。
- 保留 7 个 mask 的全部命中,按公式顺序产生多个 `SelectionSignal`,不可使用
旧代码中的 `break`。
- 详情只写可序列化、与目标行相关的关键指标;不要保存整张 DataFrame。
- 对目标日缺失、历史不足、无信号分别返回评估状态;公式计算异常向上暴露,
不静默转换为空结果。
验证:
```bash
cd zhixing-server
uv run pytest tests/unit/selection/test_zhixing_b1.py
```
回滚点:若结果数量明显偏离 golden,保留公式原语测试和差异报告,回滚策略
编排层,不回退到旧 `xg_composite` 命名。
## 4. 建立市场数据读取端口与 PostgreSQL 适配器
- 在 `selection/domain/ports.py` 定义只读 `MarketDataReader`。
- 在 `selection/infrastructure/postgres_reader.py` 实现参数化查询,读取 qfq
`market_daily_bar`,左连接同日 `market_daily_basic`,按日期升序映射为
`StockHistory`。
- 从 `Settings` 注入连接串;不直接读取环境变量,不调用 Tushare,不回退 CSV。
- 使用数据库事实表的 `source_adj = 'qfq'` 过滤,拒绝目标日之后的行。
- 连接失败转换为带股票和目标日上下文的基础设施错误;目标日无 bar 属于可识别
的业务状态。
验证:
```bash
cd zhixing-server
uv run pytest tests/unit/selection/test_postgres_reader.py
```
若增加 PostgreSQL 集成覆盖:
```bash
cd zhixing-server
uv run pytest -m integration tests/integration/test_selection_reader.py
```
回滚点:只读适配器失败时删除 selection 适配器即可;不得修改已有市场数据表、
同步事务或 Alembic migration。
## 5. 应用用例与 Fake reader
- 实现 `EvaluateZhixingB1`,输入 `ts_code`、`target_trade_date` 和 reader,输出
`SelectionEvaluation`。
- 用例只负责读取、调用领域策略和映射错误;不负责全市场循环、保存信号或 HTTP
响应。
- 提供 Fake reader 测试目标日期截断、缺失目标行、历史不足、无信号、选中多分类
和基础设施错误。
验证:
```bash
cd zhixing-server
uv run pytest tests/unit/selection/test_evaluate.py
```
## 6. 固定 fixture 与 golden 对比
- 从旧项目现有 `data/raw` 中选取少量股票和目标日期,抽取最小 OHLCV CSV,放入
`zhixing-server/tests/fixtures/selection/zhixing_b1/`。
- 将旧实现或人工确认结果固化为 JSON,包含目标日期、命中分类集合和关键详情的
容差范围;测试运行时不导入旧项目。
- 至少包含普通代码和宽幅代码,并加入一个人工构造的同日多信号样本。
- 明确记录旧实现“只保留第一个分类”与新实现“保留全部分类”的差异。
验证:
```bash
cd zhixing-server
uv run pytest tests/integration/test_zhixing_b1_golden.py
```
## 7. 完整质量检查与规划复核
实现结束后运行:
```bash
cd zhixing-server
uv run ruff format --check .
uv run ruff check .
uv run pyright
uv run pytest
```
并检查:
- `rg -n "xg_composite|zgnb\." src/zhixing_server/modules/selection tests` 只在
迁移说明或兼容性测试中出现,不成为新领域运行时依赖;
- 旧项目工作区没有被修改;
- 没有新增 HTTP 路由、前端文件、信号表 migration 或调度入口;
- golden、单元测试和端口测试都能在无网络、无生产数据库条件下运行。
完成 planning 后,先向用户展示 `prd.md`、`design.md` 和本文件摘要;只有用户
明确批准最新 planning summary,才能执行 `task.py start` 并进入实现阶段。
@@ -0,0 +1,89 @@
# 迁移知行B1选股策略
## Goal
在新项目中迁移旧项目的 `xg_composite` 选股逻辑,并将新策略名称统一为
`zhixing_b1`,让系统能够基于 PostgreSQL 中的历史市场数据执行可复现的知行
B1 选股。
首期采用“公式级垂直切片”:从通达信公式、指标计算、7 个子信号、信号结果、
市场数据读取到历史验证用例打通一条完整链路;不在本任务内铺开其他策略或完整
前端产品能力。
## Background and confirmed facts
- 旧项目的 `xg_composite` 对应通达信选股公式,包含 7 个子信号;实现位于
`zgnb-project/src/zgnb/domain/strategy/xg_composite.py`,公式原文位于
`zgnb-project/docs/references/formulas/tongdaxin_xuangu_formula.txt`。
- 旧项目的共享指标实现位于 `zgnb-project/src/zgnb/shared/xg_indicators.py`,
其中包含知行线、BBI、KDJ、RSI、振幅、趋势、回踩和 7 个子信号条件。
- 旧项目目前只返回第一个命中的 XG 子信号;本任务以公式/业务意图为准,允许
同一股票同一交易日产生多条不同分类的信号。
- 新项目已确定 PostgreSQL 为市场数据事实源、价格使用 qfq 日线、股票池为当前
沪深非 ST A 股,并保留 6 年数据窗口。
- 新项目当前已有 `market_daily_bar`、`market_daily_basic` 和市场数据领域端口,
但还没有策略读取端口、选股信号模型或策略 API。
## Requirements
### R1. 迁移策略身份
- 新策略的业务标识为 `zhixing_b1`。
- 领域逻辑不得继续依赖旧项目的 `xg_composite` 命名作为对外策略身份。
- 7 个子信号保留独立分类,并可在同一股票同一交易日同时出现。
### R2. 公式语义
- 以通达信公式和已确认的 v1203 业务调整为主要依据,旧 Python 实现作为迁移
参考和差异线索。
- 不复制旧流程中将最新市值写入全部历史 K 线、按当前日期查询历史 B1 信号等
不能支持历史重放的行为。
- 对公式中涉及的代码板块、涨跌幅放宽系数、振幅区间、缩量、大绿棒、趋势、
回踩和 7 个子信号条件建立可测试的实现。
### R3. 市场数据读取
- 策略使用市场数据领域端口读取目标交易日之前的足够 warm-up 日线数据。
- OHLCV 指标使用 `market_daily_bar` 的 qfq 价格与成交量;换手率等估值/交易
条件使用同一交易日的 `market_daily_basic` 快照。
- 策略计算不得直接依赖 PostgreSQL、Pandas SQL 查询或具体 HTTP 层实现。
- 历史选股必须显式使用 `target_trade_date`,不得隐式退化为“当前最后一行”。
### R4. 信号结果
- 信号至少包含股票、交易日、策略标识、子信号分类、收盘价和可解释的关键指标
详情。
- 信号提供由“股票、交易日、策略标识、子信号分类”组成的稳定身份;不同子信号
分类不得被合并丢失,为后续持久化提供幂等依据。
### R5. 验证
- 为共享指标和每个子信号建立边界条件测试,覆盖缺数据、暖机期、除零和板块
参数差异。
- 提供固定历史样本的策略级验证,能够判断新实现是否符合公式语义,并明确记录
与旧实现的有意差异。
- 验证不得访问真实 Tushare、生产数据库或依赖实时网络。
## Out of scope
- `bowl_rebound`、`b1`、`b1b2`、`brick_chart` 的迁移。
- 完整选股批次编排、自动调度、前端页面和图表生成。
- 选股信号 PostgreSQL 表、信号持久化实现和 HTTP API。
- 实盘交易、回测收益评价和策略参数优化。
- 为解决本任务而改变既有市场数据同步的股票池、qfq 或六年保留契约。
## Acceptance Criteria
- [ ] 新项目存在名为 `zhixing_b1` 的领域策略,并能在显式目标交易日上运行。
- [ ] 7 个子信号均有独立分类;同一股票同日多信号不会互相覆盖,并具有稳定身份。
- [ ] 策略只通过市场数据端口获得 qfq 日线和同日交易指标,不直接耦合存储实现。
- [ ] 公式关键分支、暖机边界、缺失值和除零场景均有自动化测试。
- [ ] 固定历史样本验证可重复运行,并以固定 golden 结果对比新旧实现或公式差异。
- [ ] 关键公式分支同时有人工确认案例和单元测试,golden 对比范围保持为少量固定样本。
- [ ] 未实现其他策略、前端页面或实时数据能力,且不改变现有市场数据同步契约。
## Notes
- Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
- Lightweight tasks can remain PRD-only.
- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`.
@@ -0,0 +1,50 @@
# 旧项目知行 B1 逻辑研究
## 来源
- 公式原文:`/Users/yuxuanhui/bcc-github/quant-project/zgnb/zgnb-project/docs/references/formulas/tongdaxin_xuangu_formula.txt`
- 旧策略入口:`/Users/yuxuanhui/bcc-github/quant-project/zgnb/zgnb-project/src/zgnb/domain/strategy/xg_composite.py`
- 旧指标实现:`/Users/yuxuanhui/bcc-github/quant-project/zgnb/zgnb-project/src/zgnb/shared/xg_indicators.py`
- 旧数据字段转换:`/Users/yuxuanhui/bcc-github/quant-project/zgnb/zgnb-project/src/zgnb/infrastructure/data_source/tushare_adapter.py`
## 已确认逻辑
`xg_composite` 先计算趋势白线、大哥黄线、BBI、短期/长期振荡器、KDJ、RSI、
振幅区间、缩量、大绿棒、异动、趋势和回踩条件,再执行 7 个子信号 mask:
1. 超卖缩量拐头 B
2. 超卖缩量 B
3. 原始 B1
4. 超卖超缩量 B
5. 回踩白线 B
6. 回踩超级 B
7. 回踩黄线 B
旧实现将 7 个 mask OR 为 `_存在B`,在 `select()` 中按优先级找到第一个匹配后
`break`。新任务明确改为返回全部命中分类。
## 迁移时必须保留的语义
- 数据升序;`REF` 使用前一交易日,不能用自然日偏移。
- `趋势白线 = EMA(EMA(C, 10), 10)`。
- `大哥黄线 = (MA(C,14)+MA(C,28)+MA(C,57)+MA(C,114))/4`。
- `短期` 使用 3 日最低价和 3 日最高收盘价,`长期` 使用 21 日窗口。
- 宽幅代码为 `68`、`30`、`4`、`8`、`9` 开头;普通代码如果最近 200 行内出现
超过 15% 的上涨,也使用宽幅参数。
- v1203 调整包含上涨十字星涨幅上限和原始 B1 的适当缩量分支。
## 旧实现与新任务的有意差异
- 新策略标识为 `zhixing_b1`,不再使用 `xg_composite` 作为运行时名称。
- 新分类值使用 `zhixing_b1_*` 前缀,不依赖旧 `SignalCategory.XG_*`。
- 新实现保留同一股票同一交易日的全部命中分类。
- 新实现目标日由显式 `target_trade_date` 决定,不取数据最后一行作为隐式目标。
- 新实现只使用新项目 PostgreSQL 的 qfq 日线;旧项目 CSV 中的 `market_cap` 是
初始化时取到的最新市值复制值,不能作为历史事实。
- 新测试运行时不导入旧项目;旧项目只用于生成固定 fixture 和 golden 期望。
## 现有 fixture 线索
旧项目已有完整 CSV 和 SQLite 信号结果,可从 `data/raw` 选取少量普通代码、宽幅
代码和已命中日期作为固定样本。由于旧实现没有记录同日多分类命中,需另加人工
构造样本验证新任务要求的多信号结果。
@@ -0,0 +1,26 @@
{
"id": "migrate-zhixing-b1",
"name": "migrate-zhixing-b1",
"title": "迁移知行B1选股策略",
"description": "",
"status": "in_progress",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "yuxuanhui",
"assignee": "yuxuanhui",
"createdAt": "2026-08-08",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}