This commit is contained in:
2026-09-15 20:02:05 +08:00
parent bfe89ba122
commit 04daeff141
37 changed files with 2674 additions and 1426 deletions

View File

@@ -1,5 +1,13 @@
# IPO、Trend、ZT 策略审计
> **2026-09-15 更新:本文的 ZT 部分已被重构取代。** ZT 改为正T/反T 轮次状态机,
> 持仓数量只以券商快照为准,本地不再重算持仓,因此"数量等式对账"这条路径整体
> 消失。下文 P1-07、P1-09、P2-01 的载体(`libs/state.py` 的 base_/added_ 分桶与
> `strategy/zt/positions.py` 的分支已随模块删除P2-07 所列的旧测试入口也已清理。
> 当前 ZT 设计见 [zt.md](zt.md)。
> **P1-08Trend 超时撤单覆盖手工单与其他策略单)仍未修复**:修复会改变 Trend 的
> 撤单范围ZT 侧已限定为 `zt-` 前缀Trend 侧待单独决定。
复审日期2026-09-13。基线`67b46ca0492cccdda8a41263b727a56df4a637c1` 加本次读取的工作区代码,包含手工修改及未提交文件。保留原文件名以便沿用引用。
范围三个策略及直接相关的委托簿、状态存储、SDK、服务端成交映射、网格和快照模块。本次只更新报告不修改策略、测试或生产数据不连接交易账户。

View File

@@ -1,40 +1,136 @@
# ZT 日内做 T 策略
启用时在账户 YAML 中设置:
正T先买后卖与反T先卖后买共用一套**轮次**状态机。持仓数量、可卖数量、
成本一律以券商快照为准;本地只记录"打算做什么、做到哪一步",不重算持仓。
## 启用
在账户 YAML 中设置:
```yaml
strategy: zt
signal_allow: ["dcm"]
zt_sell_ratio: 0.5
zt_buy_fall_pct: 1.0
zt_max_price: 200
signal_allow: ["dcm"] # zt 只接受 dcm
zt_open_hands: 1 # 建仓与正T买入的手数0 表示不启动策略
zt_sell_ratio: 0.5 # 反T卖出建仓数量的比例
zt_buy_fall_pct: 1.0 # 反T买回较卖出均价回落的百分比
zt_max_price: 200 # 高于此价不新开轮次
zt_t_band_pct: 1.0 # 中性带:建仓价 ±N% 内不动手
zt_max_hold_days: 5 # 单轮最长持有自然日,超期放弃
```
-`dcm` 信号可建立底仓;建仓使用反弹确认,跳过价格高于 `zt_max_price` 的股票。
- 对 dcm 底仓,盈利网格出现回撤时卖出 `zt_sell_ratio` 对应的可用整手;不卖出超过记录底仓的数量。
- 每笔卖出成交直接累计做 T 数量;活动委托结束后,价格较卖出均价回落 `zt_buy_fall_pct`,并经反弹确认,买回实际卖出数量。
- 每只股票每日只做一轮14:50 后不再开新卖单,已卖未买的仓位强制按市价买回,避免隔夜净减仓。
- 仅支持标准 A 股的先卖后买,不把当日新买入股票作为可卖库存。
## 只管自己建的仓
## SQLite 状态存储
**本策略不接管账户已有持仓。** 只有建仓腿由本策略成交、在轮次表里留下
`base_source=opened` 基准的证券才会被管理:
`libs/orderbook.py` 使用标准库 `sqlite3`,数据库路径为
`{qmt_data_dir}/zt_{account_id}_state.db`,每个账户/策略由单个实例串行更新。
启动时仅创建当前表结构和索引,不执行迁移或旧 JSON 导入。
- 账户里其它持仓原样保留,既不写基准、也不做 T日志里以
`[ZT跳过] 未接管持仓 N 只…``[ZT汇总] … 未接管=N …` 体现;
- 建仓只能由 `dcm` 信号触发(`signal_allow`),建仓成交均价即建仓价;
- 升级时如果状态文件里还留着旧版本的"接管"记录,启动会丢弃它们
(已结束的轮次删掉,未平轮次保留到敞口处理完),保证这条规则成立。
两表使用 SDK 同名字段;另有自增主键 `id`,成交表增加从 `remark` 提取的 `order_local_id`
## 两种 T 与 T+1 约束
| 表 | 数据模型 | 索引 |
| | 正T`LONG_T` | 反T`SHORT_T` |
| --- | --- | --- |
| `positions` | `PositionItem``stock_code``stock_name``direction``volume``open_price``open_cost``float_profit``market_value``stock_holder``frozen_volume``can_use_volume``on_road_volume``yesterday_volume``last_price``profit_rate``future_trade_type``expire_date` | `stock_code` 唯一索引 |
| `deals` | `DealItem``stock_code``order_sys_id``ref``order_ref``direction``offset_flag``price``volume``trade_amount``trade_date``trade_time``remark``close_profit`,以及 `order_local_id` | `order_sys_id` 唯一索引;`order_local_id``stock_code``trade_date` |
| 开仓腿 | 买入(低吸) | 卖出(高抛) |
| 平仓腿 | 卖出(高抛) | 买入(低吸) |
| 触发位置 | 现价 ≤ 建仓价×(1`zt_t_band_pct`%) | 现价 ≥ 建仓价×(1+`zt_t_band_pct`%) |
| 买入腿约束 | 可用资金 | — |
| 卖出腿约束 | **底仓可卖数量**`can_use_volume` | 底仓可卖数量 |
`load()` 只更新 `positions``deals``deals_sys_ids` 缓存,无返回值。
`sync_positions(list[PositionItem])` 保存完整持仓快照,同一证券更新时保留自增 ID。
`sync_deals(list[DealItem])` 按系统订单号去重后批量写入;同批重复记录仅写一次
`order_local_id``DealItem.local_order_id``remark` 首段),为空时拒绝写入。
成交日期规范为 `YYYY-MM-DD`,金额缺失时用成交价格乘数量补足。
A股 T+1 决定了正T 能否当天闭环当天买入的份额当天不可卖所以正T 的平仓腿
只能卖底仓原有可用量。卖不动就自然留成隔夜持仓,次日再卖——这正是"允许隔夜"
存在的原因
ZT 使用 `volume/open_price` 保存底仓数量与成本,做 T 轮次从成交历史恢复,
不再使用持仓表的旧状态、底仓订单或补仓字段。买卖方向由 `offset_flag` 计算,
本地订单号从 `remark` 提取。持仓与新增成交在同一事务提交,失败时回滚并恢复内存
## 触发条件
- **正T 开仓**:现价低于中性带下沿,且 `DipWatch` 反弹确认(不接下跌中的飞刀)
- **反T 开仓**:现价高于中性带上沿,且盈利网格出现回撤(`GridTrailingTracker`
`RETREAT`),不追最高点。
- **反T 平仓**:现价 ≤ 卖出均价×(1`zt_buy_fall_pct`%),且反弹确认。
- **正T 平仓**:现价 ≥ 买入均价×(1+`grid_step_pct`%)。
- 平仓腿不受中性带限制正T 开仓受大盘与资金闸门反T 开仓(减仓)不受资金限制。
## 数量
| 腿 | 计划量 | 上限 |
| --- | --- | --- |
| 建仓 / 正T 买入 | `zt_open_hands × 100` | 本轮剩余可用资金,向下取整手 |
| 正T 卖出 | 买入已成交量 | 底仓可卖量,向下取整手 |
| 反T 卖出 | `floor(建仓数量 × zt_sell_ratio / 100) × 100` | 底仓可卖量 |
| 反T 买回 | 卖出已成交量 | 本轮剩余可用资金 |
同一轮 tick 内串行处理所有证券,共享一份剩余资金,不会重复花同一笔钱。
资金买不起一手时返回 0不会强迫成交。
## 轮次与每日限额
状态机:`IDLE → OPENING开仓腿在途→ OPEN待平仓→ CLOSING平仓腿在途→ CLOSED`
- 一只证券任意时刻最多一个未平轮次。
- **每天最多一轮**:当天开过、或有任一条腿成交过,都不再开新轮。
- 允许隔夜:`OPEN` / `CLOSING` 可以跨日持有,不做尾盘强平。
- 超过 `zt_max_hold_days` 自然日仍未平仓 → 告警并把未平敞口**并回底仓数量**
`base_cost` 仍是建仓价),避免在裸敞口上继续开新轮。
## 建底仓与基准成本
- 底仓只能由 `dcm` 信号触发建仓,成交均价写入 `base_cost`
`base_source=opened`);账户已有持仓不会被接管,因此基准只有这一个来源。
- `base_cost` 不随做 T 买卖摊薄;超期放弃时未平敞口并回 `base_qty`
`base_cost` 保持不变。
## 日志
所有日志都带方括号标签,直接 grep 即可定位:
| 标签 | 内容 |
| --- | --- |
| `[ZT启动]` | 启动参数、状态文件路径、未平轮次明细、信号列表 |
| `[ZT成交]` | 逐笔成交入账(腿、数量、价格、编号、累计量、均价);被忽略的非本策略成交 |
| `[ZT状态]` | 轮次阶段流转 |
| `[ZT轮次]` | 一轮结束:结局、买卖均价、价差收益、基准、持有天数、备注 |
| `[ZT决策]` | **每只受管证券每轮一行**:现价、基准、偏离%、类型、阶段、敞口、可卖、持仓成本、最终动作或等待原因 |
| `[ZT下单]` | 实际提交的委托(数量、价格、目标价、订单号) |
| `[ZT跳过]` | 未接管的账户持仓、排除证券、行情无效 |
| `[ZT汇总]` | 本轮账户概览:持仓/管理/新委托数、总资产、可用、大盘与资金闸门、耗时 |
| `[ZT异常]` | 被捕获并降级的错误(含堆栈) |
排查常见问题:
```bash
grep '\[ZT决策\] 600000.SH' 20260915.log # 某只证券每轮为什么没动手
grep '\[ZT下单\]' 20260915.log # 今天实际报出去的委托
grep '\[ZT轮次\]' 20260915.log # 每轮的结果与价差收益
grep '\[ZT跳过\]' 20260915.log | tail -1 # 哪些账户持仓没被接管
grep '\[ZT异常\]' 20260915.log # 被降级的错误
```
## 状态文件
`{qmt_data_dir}/zt_{account_id}_rounds.json`,一只证券一条记录,整文件原子替换。
关键字段:`kind``phase``open_date``base_qty`/`base_cost`/`base_source`
两条腿各自的 `order_id`/`plan_qty`/`filled_qty`/`amount``seen_deal_ids`
`last_trade_date`
- 成交按 `order_sys_id` 幂等累计:重复同步不会重复计入,跨日重启靠
`seen_deal_ids` 记住已成交多少。
- 订单号统一 `zt-` 前缀(`zt-base-` / `zt-entry-` / `zt-exit-`。手工单、IPO 单
和其他策略单不带该前缀,一律不进入本策略轮次。
- 文件损坏时备份为 `.corrupt` 并由券商持仓重建,不阻断启动。
## 委托与容错
- 撤单只限 `zt-` 前缀,超时 300 秒;撤单不会丢轮次状态——余量会让轮次回到
`OPEN`,下一轮按新价重新判断。
- 防重以券商在途委托为唯一依据(同证券同方向有在途委托就不下单)。
- **先落盘意图再发请求**:请求未受理且无成交的轮次会被判为作废,已受理的委托
仍在途、成交照常累计,两种崩溃点都能自愈。
- 单证券异常只跳过该证券;账户快照、轮次推进、行情任一失败只跳过本轮,下轮重试。
## 迁移
旧账本 `zt_{account_id}_state.db``libs/state.py` 已删除,不再读写;旧库保留为
审计记录。不接管账户已有持仓:升级后只有本策略新建立的底仓会进入轮次,
老持仓保持不动。