218 lines
9.3 KiB
Markdown
218 lines
9.3 KiB
Markdown
# 知行 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 字段。
|