This commit is contained in:
2026-09-19 19:45:43 +08:00
parent 7183cb45f8
commit 8131b158b4
60 changed files with 7669 additions and 909 deletions

656
docs/etf.md Normal file
View File

@@ -0,0 +1,656 @@
# 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 | 模拟盘联调 | 跑通"观察 → 建网 → 逐档成交 → 整仓止盈 → 重建"完整循环 |