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

32 KiB
Raw Blame History

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/daily127 根日线)。 人工选择标的时要求:规模大、成交额充足、行为以震荡为主、无清盘风险。

1.5 指标

只用截至前一交易日的已收盘日线,禁止未来数据。

指标 定义
MMA60 最近 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.pyDipWatch

情形 行为
现价 > 入场门槛 价格在入场区之上,清除观察状态
现价 ≤ 入场门槛 且无观察点 记下现价作为观察低点,开始观察
后续价格更低 刷新观察低点,并重置有效期
(现价 观察低点) / 观察低点 ≥ 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/trendhandle_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.pyDipWatch(与 §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.pycalculate_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.pyGridTrailingTracker

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: falseT+1当日买入的份额当日不可卖。

  • is_t0: trueT+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. 配置表

三层:全局默认 → 标的白名单(每标的覆盖)→ 状态

# ---------- 全局默认 ----------
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_t0buy_sharesatr_multiplierinner_step),缺失即报错, 不得回落到全局默认。
  • max_adds ∈ [0,9]0 = 只有底仓);add_pctmin_profit_pctinner_gridsrebound_pctmin_grid_pctchannel_periodchannel_pct 均 > 0max_grid_span_pct ∈ (0,100]。
  • buy_shares 为 100 的整数倍且 > 0max_shares ≥ buy_shares × 10,否则启动告警。
  • rebound_pct 必须小于该标的建网时的格距百分比,否则告警。
  • 标的段只允许覆盖上表列出的键;试图覆盖 add_pctcommission_rate 一类账户级参数一律报错。
  • 未知配置键(含 defaults 与任一标的段)一律报错。

为什么 atr_multiplierinner_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/trendstrategy/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 ≤ highlow ≤ 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 模拟盘联调 跑通"观察 → 建网 → 逐档成交 → 整仓止盈 → 重建"完整循环