Files
big-qmt/docs/etf.md
2026-09-19 19:45:43 +08:00

657 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# A 股 ETF 自适应网格策略 v2
---
## 1. 策略概述
### 1.1 一句话
在 ETF 的低位区间**越跌越买**,用 ATR 决定的格距把仓位铺在 10 个价位上(底仓 1 档 + 补仓 9 档);
等**全部持仓的平均成本**涨够 `min_profit_pct`(默认 1%)后**整仓卖出**,赚这一段反弹。
### 1.2 赚什么、不赚什么
| | 说明 |
| --- | --- |
| **赚什么** | 区间内的反复波动。单轮收益 = 持仓规模 × `min_profit_pct`,所以**越跌越买反而放大了单轮收益** |
| **不赚什么** | 单边趋势。单边下跌时深档被动持仓,单边上涨时过早卖空仓位 |
| **不做什么** | 不预测方向、不做择时、不做行业轮动、**不设置止损** |
### 1.3 四个设计要点
1. **ATR 自适应格距** —— 格距随标的波动率自动伸缩,不用手工设价格区间。
2. **底仓 + 9 档补仓** —— 共 10 个价位,每档 1000 股,固定不缩放。
3. **反弹确认入场** —— 进入低位区后不立刻买,等从低点反弹 0.5% 才建网(防接飞刀)。
4. **整仓止盈为主、单档兜底为辅** —— 主出口吃整段反弹,副出口防止浅档利润被反复吐回。
### 1.4 标的(示例)
| 代码 | 名称 | 现价 | 近 127 日区间 | 振幅 | 结算 |
| --- | --- | --- | --- | --- | --- |
| 588000.SH | 华夏上证科创板 50 成份 ETF | 1.744 | 1.316 ~ 2.390 | 81.6% | T+1 |
| 510300.SH | 沪深 300ETF 华泰柏瑞 | 4.582 | 4.405 ~ 5.095 | 15.7% | T+1 |
| 518880.SH | 黄金 ETF | 9.009 | 8.224 ~ 10.345 | 25.8% | T+0 |
> 价格为 2026-09-18 收盘,来自 `http://139.224.247.176:13499/etf/daily`127 根日线)。
> 人工选择标的时要求:规模大、成交额充足、行为以震荡为主、无清盘风险。
### 1.5 指标
只用截至前一交易日的已收盘日线,禁止未来数据。
| 指标 | 定义 |
| --- | --- |
| `M`MA60 | 最近 60 根收盘价均值,作为**不可突破的网顶** |
| `ATR`(默认 14 日) | `TR = max(最高−最低, abs(最高−前收盘), abs(最低−前收盘))`;前 14 个 TR 取均值作初值,之后 Wilder 平滑 `ATR = (前ATR×13 + 当日TR) / 14` |
| 区间通道 | 近 20 日的 `[最低价, 最高价]` |
---
## 2. 观察规则
**只在空网格(无持仓)时观察。** 观察的目的是确认"跌势是否还在继续",避免在下跌途中接刀。
### 2.1 入场门槛
```
通道门槛 = 区间下沿 + (区间上沿 区间下沿) × 15%
入场门槛 = min(通道门槛, MA60)
```
`min()` 同时表达两件事:**不在均线上方建网**,且**处在近 20 日区间最低的 15% 以内**。
> MA60 只作上限、不作低位判定。实测把门槛设在均线附近会让成交发生在下跌趋势的
> 中途而非区间低位,每笔净收益从 86.8 元降到 63.2 元。
### 2.2 观察状态机(复用 `libs/watch.py` 的 `DipWatch`
| 情形 | 行为 |
| --- | --- |
| `现价 > 入场门槛` | 价格在入场区之上,**清除观察状态** |
| `现价 ≤ 入场门槛` 且无观察点 | 记下现价作为观察低点,开始观察 |
| 后续价格更低 | **刷新观察低点**,并重置有效期 |
| `(现价 观察低点) / 观察低点 ≥ 0.5%` | **确认反弹 → 可以建网**,观察状态随之清除 |
| 观察超过 600 秒未触发 | 作废,重新开始观察 |
```
watch = DipWatch(expire_seconds=600, rebound_threshold=0.5)
watch.triggered(tag, code, price, now) -> bool # True 即确认
watch.forget(code) # 价格回到入场区上方时必须调用
```
- 观察键就是**证券代码**,天然逐标的隔离,不会互相污染低点。
- 库的 `triggered` 已处理"首次观察 / 刷新低点 / 过期重开 / 达标触发并清除"四个分支,
调用方**不要重复实现**,只负责在价格离开入场区时 `forget`
### 2.3 反弹阈值取值
约束:**必须小于该标的的格距百分比**1.35% ~ 1.78%),否则确认价已越过下一档,
锚点上移会削弱后续格子的覆盖。
| 取值 | 效果 | 风险 |
| --- | --- | --- |
| 0.3% | 几乎不放过任何入场 | 单边下跌中连续接刀,等于没有防护 |
| **0.5%(默认)** | 确认成本约为最小格距的三分之一 | 极端快跌中仍会建网 |
| 1.5% | 只在明确反转后建网 | 超过多数标的格距,系统性错过 V 型反转 |
---
## 3. 买入规则
### 3.1 锚点
**只有"反弹确认"能产生锚点**,锚点就是确认那一刻的现价:
```
锚点 = 反弹确认时的现价
```
`锚点 ≤ 入场门槛` 约束(因为只有进入入场区才会开始观察)。
> 持仓期间**绝不重设锚点**。不允许因为价格变动把锚点平移到当前价——
> 那等于跳过防接飞的确认,正是 §2 要避免的行为。
### 3.2 格距
```
格距 G = max( ATR14 × atr_multiplier, MA60 × 0.5%, 0.001 )
```
- 向上取整到 **0.001 元**ETF 最小报价单位)。
- 百分比下限防止低波动期格距被 ATR 压到无意义0.001 的硬下限保证格距恒为正。
- **`atr_multiplier` 逐标的标定**,见 §3.4。
### 3.3 底仓
建网时只买**一档底仓**,即 **1000 股**按锚点价成交§3.5)。
后续的 9 档由 §4 的**百分比补仓**逐档建立,**不预挂价位**。
因此建网时**不需要算出一串价位**,只需要确定锚点与格距 `G`§3.2
- **锚点**是底仓成本与后续"自上一档跌幅"的起点;
- **格距 `G`** 不参与补仓触发只用于两件事§3.4 的跨度健康度检查、
§5.2 副出口的内层格距标定。
### 3.4 ATR 倍数必须逐标的标定
格距本身不触发交易,但它决定**整条阶梯铺多宽**`9 × G`),所以仍必须逐标的标定。
| 标的 | 现价 | MA60 | ATR14 | ATR 倍数 | 格距 | 格距% | 阶梯跨度9G | 跨度/现价 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 588000.SH | 1.744 | 1.856 | 0.0603 | **0.5** | 0.031 | 1.78% | 0.279 | **16.0%** |
| 510300.SH | 4.582 | 4.712 | 0.0613 | 1.0 | 0.062 | 1.35% | 0.558 | 12.2% |
| 518880.SH | 9.009 | 8.799 | 0.1376 | 1.0 | 0.138 | 1.53% | 1.242 | 13.8% |
**588000 必须用 0.5 倍**:它 127 日振幅 81.6%ATR 相对价格的比例远高于另外两只,
按 1.0 倍算出的阶梯跨度是现价的 31.5%(格距 0.061),深档基本不可达。降到 0.5 倍后跨度回到 16.0%
与另外两只同量级。
标定要同时兼顾**两个不同的失效方向**
| 失效方向 | 判据 | 后果 | 修法 |
| --- | --- | --- | --- |
| **跨度轴**(波动率主导) | `9 × G / 锚点` 过大 | 深档不可达、资金长期闲置 | 下调该标的 ATR 倍数 |
| **佣金轴**(价格水平主导) | 单档金额过小 | 每轮收益被固定佣金吃掉 | 提高每格股数,或剔除低价标的 |
**判据**:跨度落在 10% ~ 20%、单档金额让佣金占比 ≤ 0.3%。标定后三只在 12.2% ~ 16.0%。
> 注意标定后**高波动标的的百分比格距反而最小**,所以不能按"波动大就放宽格距"的直觉调参。
### 3.5 底仓挂单
锚点确定后,**底仓(档位 0按锚点价挂限价单**,保证首笔立即成交。
若连底仓都挂不出去(资金或可用份额不足),**撤销锚点**,下一轮重新触发——
不允许留下挂不出单的"死锚点"。
---
## 4. 补仓规则
**按百分比补仓,不按预设价位挂单。** 口径与 `strategy/trend``handle_loss` 一致
`strategy/trend/positions.py`):用**亏损百分比**判断触发,用 `add_watch` 做**反弹确认**
确认后才买入。
### 4.1 触发条件
```
自上一档跌幅 = (上一档成交价 现价) / 上一档成交价 × 100
触发补仓: 自上一档跌幅 ≥ add_pct (默认 3.0%
且 add_watch.triggered(...) == True (反弹确认,防接飞刀)
```
| 项 | 规则 |
| --- | --- |
| 上一档 | 首次是**底仓成交价**,之后是**最近一次补仓的成交价** |
| 补仓价 | **反弹确认时的现价**(不是预设价位),因此每次补仓都买在反弹确认点 |
| 每档数量 | **1000 股10 手)**,固定,不按资金缩放 |
| 补仓次数 | 最多 **9 次**(底仓 1 + 补仓 9 = 10 档) |
| 单标的上限 | **10000 股** |
| 重复 | 同一时刻该标的只允许一笔在途买单 |
**为什么按百分比而不是预设价位**
- 百分比自动适配价格水平,不需要为每只标的预先算出一串价位;
- **阶梯随下跌自动收窄**——平均成本被不断拉低,所以"再跌 3%"对应的绝对距离会变短;
而固定价位阶梯的间距是恒定的,深档会越拉越远;
-`trend` 同源,两个策略的补仓语义一致,便于统一维护。
### 4.2 防接飞刀:反弹确认
补仓同样使用 `libs/watch.py``DipWatch`(与 §2 建仓观察同一套机制,但**独立的观察实例**
```
add_watch = DipWatch(expire_seconds=watch_seconds, rebound_threshold=rebound_pct)
add_watch.triggered("补仓", code, price, now) -> bool
add_watch.forget(code) # 补仓成功或标的离开观察后调用
```
| 情形 | 行为 |
| --- | --- |
| 跌幅未达 `add_pct` | 不观察,等待 |
| 跌幅达到 `add_pct`,且无观察点 | 以现价启动观察(记观察低点) |
| 观察中价格继续下跌 | 刷新观察低点 |
| 从观察低点反弹 ≥ `rebound_pct` | **确认补仓**,买入 1000 股,并 `forget` |
**参数关系必须满足**
```
rebound_pct < add_pct < 格距% × N
0.5% 3.0% 1.35% ~ 1.78%×1.7 ~ 2.2
```
- `rebound_pct` 必须**小于** `add_pct`,否则反弹确认价会回到上一档之上,条件自相矛盾。
- `add_pct` 建议取 **1.5 ~ 2.5 个格距**,让每次补仓跨越一格以上,避免同一区域反复成交。
### 4.3 add_pct 取值取舍
3% 下每次补仓距上一档的绝对距离(以格距计价):
| 标的 | 格距% | `add_pct=2%` | `add_pct=3%` |
| --- | --- | --- | --- |
| 588000.SH | 1.78% | 1.1 G | 1.7 G |
| 510300.SH | 1.35% | 1.5 G | 2.2 G |
| 518880.SH | 1.53% | 1.3 G | 2.0 G |
`add_pct=3%` 对应 1.7 ~ 2.2 个格距,且积累了足够跌幅空间;代价是 10 档打满需要
**累计回撤约 24%**(见 §4.4)。若希望阶梯更浅,可下调到 2%(打满约 16.6%)。
### 4.4 阶梯与资金
`add_pct = 3%`、每档 1000 股展开(基准 = 入场门槛):
| 标的 | 档 | 补仓价 | 累计回撤 | 累计股数 | 平均成本 | 出场价(+1% | 净利 | 占用资金 | 占用回报 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 588000.SH | 1 | 1.632 | 0.0% | 1000 | 1.632 | 1.648 | 6 元 | 1,632 元 | 0.37% |
| 588000.SH | 3 | 1.536 | 5.9% | 3000 | 1.584 | 1.600 | 29 元 | 4,751 元 | 0.61% |
| 588000.SH | 5 | 1.445 | 11.5% | 5000 | 1.537 | 1.553 | 49 元 | 7,686 元 | 0.64% |
| 588000.SH | 7 | 1.360 | 16.7% | 7000 | 1.493 | 1.507 | 61 元 | 10,448 元 | 0.58% |
| 588000.SH | 10 | 1.241 | **24.0%** | 10000 | 1.429 | 1.443 | 88 元 | 14,287 元 | 0.62% |
| 510300.SH | 1 | 4.518 | 0.0% | 1000 | 4.518 | 4.563 | 35 元 | 4,518 元 | 0.77% |
| 510300.SH | 3 | 4.251 | 5.9% | 3000 | 4.384 | 4.428 | 113 元 | 13,151 元 | 0.86% |
| 510300.SH | 5 | 3.999 | 11.5% | 5000 | 4.255 | 4.297 | 181 元 | 21,273 元 | 0.85% |
| 510300.SH | 7 | 3.763 | 16.7% | 7000 | 4.131 | 4.172 | 245 元 | 28,915 元 | 0.85% |
| 510300.SH | 10 | 3.434 | **24.0%** | 10000 | 3.954 | 3.993 | 329 元 | 39,539 元 | 0.83% |
| 518880.SH | 1 | 8.799 | 0.0% | 1000 | 8.799 | 8.887 | 78 元 | 8,799 元 | 0.89% |
| 518880.SH | 3 | 8.279 | 5.9% | 3000 | 8.538 | 8.623 | 233 元 | 25,613 元 | 0.91% |
| 518880.SH | 5 | 7.790 | 11.5% | 5000 | 8.287 | 8.370 | 378 元 | 41,434 元 | 0.91% |
| 518880.SH | 7 | 7.329 | 16.7% | 7000 | 8.046 | 8.126 | 511 元 | 56,319 元 | 0.91% |
| 518880.SH | 10 | 6.689 | **24.0%** | 10000 | 7.701 | 7.778 | 694 元 | 77,013 元 | 0.90% |
**满网占用10 档打满)**588000 = 14,287 元、510300 = 39,539 元、518880 = 77,013 元,
**三只合计 130,839 元**
> 本表是**规模参考**,不是准入条件。实盘资金约束由 §4.6 的共享预算处理。
> 占用回报稳定在 **0.37% ~ 0.91%**,随建仓加深略微上升——因为平均成本被摊低,
> 同样 1% 的盈亏率对应的绝对涨幅变小。
### 4.5 不预挂补仓单
**补仓单不预先挂在下方价位上**,只在反弹确认后按现价买入。理由:
- 百分比补仓的触发条件是"从上一档跌足 `add_pct`",这是一个**状态条件**,不是一个价位;
- 预挂低价单会在下跌途中被连续打穿,等于自动接刀,与 §4.2 的确认机制冲突。
因此补仓的执行顺序固定为:**判断跌幅 → 启动/刷新观察 → 反弹确认 → 按现价买入**。
### 4.6 资金
```
可用预算 = 券商可用资金 账户总额 × 现金安全线 所有标的的待确认买单预留
```
- 必须**先扣除所有标的**的未确认买单,不能遍历到后面的标的才发现钱不够。
- 一轮内多标的按配置顺序消耗同一预算。
### 4.7 打满后的规则
补仓次数达到 9 次(共 10 档、10,000 股)后:
| 情形 | 规则 |
| --- | --- |
| 已满 10 档 | **停止补仓**。不再补仓、不再重设基准,只等主出口清仓 |
| 副出口卖掉某一档 | 允许按同一百分比规则重新补回该档,阶梯恢复循环 |
| 主出口成交(整仓清空) | 阶梯解除;价格再次进入入场区并完成反弹确认时,以新基准重建 |
**理由**:允许打满后继续重建基准,等于在单边下跌中不断向趋势加仓,风险无上限。
停止补仓把最坏持仓锁定在 10,000 股。
---
## 5. 卖出规则
**双出口**:整仓主出口 + 单档副出口。
| 出口 | 触发条件 | 卖出范围 | 角色 |
| --- | --- | --- | --- |
| **主出口** | 盈亏率 ≥ `min_profit_pct` | **全部持仓** | 主力收益来源 |
| **副出口** | 某档盈利从峰值回落,且峰值已抬到 `inner_grids` 格 | **只卖该档** | 兜住"反弹只够一档"的小波动 |
### 5.1 主出口:按盈亏率整仓止盈
**按盈亏率判断,不用绝对价位。**`strategy/trend` 的止盈口径一致
`libs/calc.py``calculate_min_profit_rate` + `GridTrailingTracker`
```
盈亏率 = (现价 平均成本) / 平均成本 × 100 # 百分点
平均成本 = Σ(该档成本 × 该档股数) / Σ(该档股数)
触发整仓卖出: 盈亏率 ≥ min_profit_pct (默认 1.0%
```
- 卖出**全部持仓**,一次清空网格;之后网格解除,价格重新进入入场区时以新锚点重建。
- **目标价只是盈亏率的换算结果**,不参与决策:
`出场价 = 平均成本 × (1 + min_profit_pct / 100)`
- 盈亏率的分母是**平均成本**(会随加仓下降),不是锚点。所以出场价天然跟着成本走,
不需要额外加任何锚点偏移。
**为什么不用"平均成本 + N × G"**:那个写法在只成交底仓时会算出
`锚点 + 2G`(比锚点还高 2 个格距)——底仓买在锚点,此时还没有任何摊薄,
"+2G"就只能等价格涨回锚点上方。按盈亏率算就自然得多:底仓只要涨 1% 即可了结
(换算成格距只有 0.52G ~ 0.73G,仍在锚点附近,可达)。
**为什么不用趋势的固定档位表**`calculate_min_profit_rate` 按价格分档
≥300 元 3%、≥200 元 5%、≥100 元 7%、其余 9%)。我们的 ETF 价格都在 10 元以下,
会全部落到 9% 档——对均值回归的网格太苛刻。ETF 的合理下限由**佣金**决定,见下。
**保底约束:盈亏率必须高于双边佣金率。** 单档 1000 股时:
| 标的 | 单档金额 | 双边佣金率 | `min_profit_pct = 1%` 够吗 |
| --- | --- | --- | --- |
| 588000.SH | 1,744 元 | **0.573%** | 够(毛利 17.4 元,净 7.4 元) |
| 510300.SH | 4,582 元 | 0.218% | 够(毛利 45.8 元,净 35.8 元) |
| 518880.SH | 9,009 元 | 0.111% | 够(毛利 90.1 元,净 80.1 元) |
`min_profit_pct = 1%` 高于三只标的最小仓位的佣金率,所以**任何成交档数下净利都为正**。
仓位越大佣金率越低,约束越松:
```
双边佣金率 = 2 × max(5, 金额 × 佣金率) / 金额
1 档(1000股) → 0.573% / 0.218% / 0.111%
10 档(10000股) → 0.060% / 0.060% / 0.060%
```
> **不要为了迁就资金而调低 `min_profit_pct`**:低于单档佣金率就会产生"赚了差价亏了手续费"的
> 无效交易。佣金率为 0.573% 的 588000 是最紧的一只。
**出场价与收益**`min_profit_pct = 1%`,按底部 10 档逐级成交):
| 标的 | 已成交档数 | 持仓 | 平均成本 | 出场价 | 出场价相对锚点 | 净利 | 占用资金 | 占用回报 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 588000.SH | 1 | 1000 | 1.632 | 1.648 | +0.52 G | 6 元 | 1,632 元 | 0.37% |
| 588000.SH | 2 | 2000 | 1.616 | 1.633 | +0.03 G | 18 元 | 3,233 元 | 0.56% |
| 588000.SH | 6 | 6000 | 1.554 | 1.570 | 2.00 G | 58 元 | 9,327 元 | 0.62% |
| 588000.SH | 10 | 10000 | 1.493 | 1.507 | **4.03 G** | 90 元 | 14,925 元 | 0.60% |
| 510300.SH | 1 | 1000 | 4.518 | 4.563 | +0.73 G | 35 元 | 4,518 元 | 0.77% |
| 510300.SH | 2 | 2000 | 4.487 | 4.532 | +0.23 G | 75 元 | 8,974 元 | 0.84% |
| 510300.SH | 6 | 6000 | 4.363 | 4.407 | 1.79 G | 226 元 | 26,178 元 | 0.86% |
| 510300.SH | 10 | 10000 | 4.239 | 4.281 | **3.82 G** | 357 元 | 42,390 元 | 0.84% |
| 518880.SH | 1 | 1000 | 8.799 | 8.887 | +0.64 G | 78 元 | 8,799 元 | 0.89% |
| 518880.SH | 2 | 2000 | 8.730 | 8.817 | +0.13 G | 159 元 | 17,460 元 | 0.91% |
| 518880.SH | 6 | 6000 | 8.454 | 8.539 | 1.88 G | 465 元 | 50,724 元 | 0.92% |
| 518880.SH | 10 | 10000 | 8.178 | 8.260 | **3.91 G** | 745 元 | 81,780 元 | 0.91% |
**三条性质**
1. **只在底仓时出场价最低不过锚点上方 0.52 ~ 0.73 个格距**——基本就在建网位置附近,
不会被推到锚点上方一大截。
2. **建仓越深,出场价越低**(满仓时已在锚点下方约 4 个格距),因为加仓不断拉低平均成本。
3. **占用回报稳定在 0.37% ~ 0.92%**,且随建仓加深略微上升。
> **主出口不用峰值回撤**:把整个持仓当观察键、用 `GridTrailingTracker` 驱动整仓出手
> 是可行的,但实测没有收益、还拖慢周转——固定目标 3 轮 / 均持仓 3.8 日,
> 峰值回撤 2 轮 / 均持仓 8.5 日,期末还压着 3,000 股。整仓层面用盈亏率阈值更干脆。
> **主出口不用峰值回撤**:把整个持仓当观察键、用 `GridTrailingTracker` 驱动整仓出手
> 是可行的,但实测没有收益、还拖慢周转——固定目标 3 轮 / 均持仓 3.8 日,
> 峰值回撤 2 轮 / 均持仓 8.5 日,期末还压着 3,000 股。整仓层面用固定目标更干脆。
### 5.2 副出口:单档峰值回撤
复用 `libs/grid_take_profit.py``GridTrailingTracker`
```
tracker = GridTrailingTracker(step=inner_step) # step = 该标的的内层格距(百分点)
观察键 = f"etf:{code}:level:{level}" # 每个档位一个独立观察键
每轮对该档调用 observe(键, 该档盈亏率%)
ARMED / RAISED / STEADY → 不动
RETREAT → 若 peak_grid >= inner_grids则卖出该档
```
- `inner_step` 是**盈亏率的百分点跨度**,独立于外层格距,**必须小于或接近标的的格距百分比**
否则峰值永远抬不起来,副出口形同虚设。
- `inner_grids`(默认 2表示"要求峰值至少抬到第几格"才允许回撤卖出。
- 卖出**只卖该档**`min(该档持仓, 券商可卖份额)`,向下取整到 100 股)。
- **峰值必须在卖出成功后才 `clear(键)`**;下单失败或撤单时必须保留峰值。
- 该档清空后 `clear`,价格回落到该档价位时重新挂买单;重新建仓从 `ARMED` 重新开始。
- **它不替代主出口**,只处理"整仓目标还没到、但某一档已明显获利"的情况。实测在当前参数下
很少触发(三个标的各 3 轮里触发 0~1 次),所以它是**保险而非收益来源**。
**内层格距取值**`inner_step = clamp(0.5 × 格距%, 0.5, 2.0)`,逐标的配置:
| 标的 | 格距% | 内层格距 |
| --- | --- | --- |
| 588000.SH | 1.78% | 0.9%(精算 0.89 |
| 510300.SH | 1.35% | 0.7%(精算 0.68 |
| 518880.SH | 1.53% | 0.8%(精算 0.77 |
**收益区间**:峰值抬到第 2 格意味着盈亏率曾达到 `[2×内层格距, 3×内层格距)`
回撤到第 1 格时落在 `[1×内层格距, 2×内层格距)`,所以**实际落袋在 1~2 个内层格距之间**
| 标的 | 单档金额 | 内层格距 | 保底1 格)净利 | 占用回报 | 上界2 格)净利 | 占用回报 |
| --- | --- | --- | --- | --- | --- | --- |
| 588000.SH | 1,744 元 | 0.9% | 5.7 元 | **0.33%** | 21.4 元 | 1.23% |
| 510300.SH | 4,582 元 | 0.7% | 22.1 元 | **0.48%** | 54.1 元 | 1.18% |
| 518880.SH | 9,009 元 | 0.8% | 62.1 元 | **0.69%** | 134.1 元 | 1.49% |
**保底那一列才是下限**,双边佣金固定 10 元,三只都为正。若内层格距取到 0.5 及以下,
588000 的保底净利就会接近零,所以下限取 0.5 而非更低。
### 5.3 结算制度
由**每标的的 `is_t0` 字段**决定,必填、无隐式默认:
- `is_t0: false`T+1当日买入的份额当日不可卖。
- `is_t0: true`T+0当日买入的份额当日可卖。
- **`min_hold_days`(默认 1仅对 `is_t0: false` 的标的生效**:买入日当天不挂卖单,
最早次一交易日卖出。**`is_t0: true` 的标的跳过本条**,否则 T+0 的当日回转能力
会被这条规则彻底抵消,配置 `is_t0` 也就失去意义。
- **主出口受 T+1 约束**:若部分持仓是当日买入的,当日不能整仓卖出;
此时按"当日可卖份额"上限卖出,剩余部分次日再用同一目标价尝试。
- 两种情形下**券商可用份额都是唯一权威**,本地状态不得放宽。
> 上线前须与券商逐一确认。`518880.SH`(黄金 ETF等商品、债券、货币、跨境 ETF 通常支持
> T+0股票型 ETF 为 T+1。声明错误会导致柜台拒单。
### 5.4 费用
```
单边佣金 = max(最低佣金, 金额 × 佣金率) # 默认 max(5, 金额×0.0003)
```
主出口一次卖出覆盖整个持仓,佣金不是约束:
| 标的 | 持仓档数 | 毛利 | 佣金合计 | 净利 | 佣金占毛利 |
| --- | --- | --- | --- | --- | --- |
| 588000.SH | 2 | 123 元 | 15 元 | 108 元 | 12% |
| 510300.SH | 2 | 248 元 | 15 元 | 233 元 | 6% |
| 518880.SH | 2 | 552 元 | 15 元 | 537 元 | 3% |
即使只成交 2 档(占用 2,675 元),双边佣金 15 元也只占毛利的 12%。
对照 100 股588000 单档仅 174 元,一次往返佣金就占 5.73%,会把副出口的利润整个吃掉。
---
## 6. 配置表
三层:**全局默认 → 标的白名单(每标的覆盖)→ 状态**。
```yaml
# ---------- 全局默认 ----------
defaults:
atr_period: 14
min_grid_pct: 0.5
max_grid_span_pct: 40 # 跨度健康度告警线(非闸门)
channel_period: 20
channel_pct: 15
rebound_pct: 0.5 # 反弹确认阈值(建仓与补仓共用)
add_pct: 3.0 # 补仓:自上一档再跌该百分比即触发(需配合反弹确认)
max_adds: 9 # 补仓次数上限(底仓另计,共 10 档)
watch_seconds: 600
min_profit_pct: 1.0 # 主出口:盈亏率 >= 该值即整仓清掉
inner_grids: 2.0 # 副出口:峰值至少抬到第 N 格才允许回撤卖出
min_hold_days: 1
max_hold_days: 0 # 0 = 不止损
commission_rate: 0.0003
min_commission: 5.0
max_tick_age_seconds: 90
# ---------- 标的白名单(键即标的,顺序即资金优先级)----------
# 键必须与接口返回的 ts_code 完全一致588000.SH / 159915.SZ
symbols:
"588000.SH":
is_t0: false
buy_shares: 1000
max_shares: 10000 # 10 个价位 × 1000 股
atr_multiplier: 0.5 # 该标的 ATR 相对价格偏高,必须收窄
inner_step: 0.9
"510300.SH":
is_t0: false
buy_shares: 1000
max_shares: 10000
atr_multiplier: 1.0
inner_step: 0.7
"518880.SH":
is_t0: true # 黄金 ETF 支持当日回转
buy_shares: 1000
max_shares: 10000
atr_multiplier: 1.0
inner_step: 0.8
```
**全局参数**
| 参数 | 默认 | 说明 |
| --- | --- | --- |
| `add_pct` | 3.0 | 补仓触发:自上一档成交价再跌该百分比(%),须大于 `rebound_pct` |
| `max_adds` | 9 | 补仓次数上限。总档数 = `max_adds + 1`(含底仓),允许 0 ~ 9 |
| `atr_period` | 14 | ATR 周期 |
| `min_grid_pct` | 0.5 | 格距百分比下限 |
| `max_grid_span_pct` | 40 | 跨度健康度告警线。`9×G/锚点` 落在 10% ~ 20% 为佳;超线只告警并建议下调 `atr_multiplier`**不阻止建网** |
| `channel_period` | 20 | 区间通道回看天数 |
| `channel_pct` | 15 | 通道低位门槛:距区间下沿的百分比以内 |
| `rebound_pct` | 0.5 | 反弹确认阈值,**须小于该标的格距百分比** |
| `watch_seconds` | 600 | 观察有效期 |
| `min_profit_pct` | 1.0 | 主出口:盈亏率 ≥ 该值即整仓清掉(必须是百分点) |
| `inner_grids` | 2.0 | 副出口:峰值至少抬到第 N 格 |
| `min_hold_days` | 1 | 买入日当天不挂卖单;`is_t0: true` 的标跳过 |
| `max_hold_days` | 0 | 0 = 不止损 |
| `commission_rate` / `min_commission` | 0.0003 / 5.0 | 费用模型(账户级) |
| `max_tick_age_seconds` | 90 | 行情有效期,超时或非当天不交易 |
**逐标的参数**
| 参数 | 必填 | 默认 | 说明 |
| --- | --- | --- | --- |
| `is_t0` | **是** | — | `true` = 当日买入当日可卖;`false` = T+1 |
| `buy_shares` | **是** | — | 每档每次买入股数100 的整数倍 |
| `atr_multiplier` | **是** | — | 格距的 ATR 倍数必须逐标的标定§3.4 |
| `inner_step` | **是** | — | 内层格距(盈亏率百分点),决定副出口的利润刻度 |
| `max_shares` | 否 | 10000 | 单标的总持仓上限,须 ≥ `buy_shares × 10` |
| `inner_grids` | 否 | 继承 | 覆盖副出口的峰值格要求 |
| `max_grid_span_pct` | 否 | 继承 | 覆盖跨度告警线 |
| `rebound_pct` | 否 | 继承 | 覆盖反弹阈值 |
| `max_hold_days` | 否 | 继承 | 覆盖时间退出开关 |
**配置校验**
- 四项**必须显式给出**`is_t0``buy_shares``atr_multiplier``inner_step`),缺失即报错,
不得回落到全局默认。
- `max_adds` ∈ [0,9]0 = 只有底仓);`add_pct``min_profit_pct``inner_grids``rebound_pct``min_grid_pct`
`channel_period``channel_pct` 均 > 0`max_grid_span_pct` ∈ (0,100]。
- `buy_shares` 为 100 的整数倍且 > 0`max_shares ≥ buy_shares × 10`,否则启动告警。
- `rebound_pct` 必须**小于该标的建网时的格距百分比**,否则告警。
- 标的段只允许覆盖上表列出的键;试图覆盖 `add_pct``commission_rate`
一类账户级参数一律报错。
- 未知配置键(含 `defaults` 与任一标的段)一律报错。
**为什么 `atr_multiplier` 与 `inner_step` 必填**:它们各自决定一个失效方向——
前者定跨度(跨度轴),后者定副出口能否被触发。两者都没有安全的全局默认值。
**为什么 `min_profit_pct` 用 1%**:它必须高于单档 1000 股的双边佣金率(最高的是 588000 的 0.573%
否则会产生"赚了差价亏了手续费"的无效交易。1% 留了约一倍的余量同时远小于格距1.35% ~ 1.78%
不会把出场推到下一个档位之外。
---
## 7. 文件规划
### 7.1 目录结构
```
py-client/
├── etc/
│ └── etf.yaml # 本策略的独立配置§6
└── strategy/etf/ # 本策略实现
├── __init__.py
├── boot.py # 入口装配(入口名 etf
├── config.py # 配置读取与校验§6
├── data.py # 历史日线接口适配
├── indicators.py # MA60 / ATR / 区间通道 / 格距
├── engine.py # 决策主循环:观察→建网→补仓→卖出
└── state.py # 状态持久化:锚点、冻结格距、档位、委托
```
### 7.2 复用约定
| 库 | 类 | 承担的行为 | 对应 |
| --- | --- | --- | --- |
| `libs/watch.py` | `DipWatch` | 观察与反弹确认 | §2.2 |
| `libs/grid_take_profit.py` | `GridTrailingTracker` | 副出口的单档峰值回撤 | §5.2 |
**不得修改这两个共享库。** 它们同时被 `strategy/trend``strategy/zt` 使用,
改动会波及那两个策略。ETF 侧若需要额外行为,**在 ETF 内包装,不动库本身**。
### 7.3 状态文件
```
{qmt_data_dir}/etf/{账户SHA256}/state.json
```
- 保存内容:每标的的**锚点**、**底仓成本**、各档持仓(数量、成本、买入日)、
**上一档成交价**§4.1 的百分比补仓基准),以及**至多一笔未确认买单**。
- **补仓是逐笔串行的**(同一时刻该标的只允许一笔在途买单),因此 pending 只需要一个槽位,
不需要按档位建表——这是改成百分比补仓后的简化。
- **副出口的峰值格只存活在内存**,不落盘。`GridTrailingTracker` 是进程内对象,
重启后峰值从 `ARMED` 重新建立(即当前盈亏率所在格),**不会立刻触发卖出**。
这是可接受的:副出口只是保险,晚一轮触发不影响结果。
- **主出口完全无状态**:出场价由已核对的档位实时算出,重启后行为与重启前一致。
- **先保存意图,再发起请求**。请求成功、超时或失败都不自动解锁;必须收到终态回报
**且**券商持仓与累计成交量相符,才能继续。
- 部分成交按实际数量核对;**买入必须取得实际成交均价**才能计入平均成本,
否则主出口的目标价会算错(未核对的成交不参与计算)。
- 状态损坏时**拒绝启动并报错**,不允许静默重建为空状态。
- 券商持仓与本地档位合计不一致时,**以券商快照为准**重建:清仓则重置,
否则整体接管为锚点档,保证账实相符。
- 状态结构带**版本号**。旧版本迁移时按券商快照重建档位表,**不做任何自动卖出**。
### 7.4 数据来源
```
GET http://139.224.247.176:13499/etf/daily?code=<证券代码>
```
- 只接受**单个** `code`;逗号分隔的多代码查询返回非 JSON 错误页。
- 实际返回**一维数组**,按 `trade_date` 倒序,字段含
`open / high / low / close / pre_close / trade_date / ts_code`
- 旧版 `{code, message, details}` 包装对象已不再返回,适配层对两种形式都兼容。
- 接口**未声明复权口径**,按原始价格计算。
- 每标的每日取一次并缓存,截取最近 120 根;取数失败至少间隔 5 分钟再重试。
**数据校验**——任一不满足即**放弃该标的当轮交易**,不得降级、不得改用其他证券数据:
| 校验项 | 规则 |
| --- | --- |
| 证券归属 | 每行 `ts_code` 与请求代码完全一致 |
| 未来数据 | `trade_date >= 今天` 的行一律剔除,盘中不得混入未收盘日线 |
| 重复日期 | 同一日期出现两次即放弃整批 |
| OHLC 有效性 | 四价均为有限正数,且 `low ≤ open ≤ high``low ≤ close ≤ high` |
| 样本量 | 至少 60 根ATR14 需 61 根) |
| 新鲜度 | 最后日线距今超过 15 个自然日即放弃 |
**实时行情**:下单前必须确认最新价为有限正数,且行情时间戳为当天、距今不超过 90 秒。
### 7.5 待办
| # | 事项 | 说明 |
| --- | --- | --- |
| 1 | `config.py` 代码校验放宽 | 现有正则 `5[0-9]{5}\.SH` **不含 `56xxxx` / `58xxxx`**,会拒掉 `588000.SH`,须改为 `5[0-9]{5}` |
| 2 | 用 3~5 年日线重跑 | 现接口只给 127 根,主出口只有 3 轮完整往返,**期望收益未经证明** |
| 3 | 与券商确认 | 逐标的确认 T+0 / T+1 与最低佣金实收标准 |
| 4 | 分红除息方案 | 接口无复权价。优先推动接口改造;否则检测除息缺口后整体下移锚点与各档成本,当日不交易;无法确认分红金额则移除该标的 |
| 5 | 模拟盘联调 | 跑通"观察 → 建网 → 逐档成交 → 整仓止盈 → 重建"完整循环 |