32 KiB
A 股 ETF 自适应网格策略 v2
1. 策略概述
1.1 一句话
在 ETF 的低位区间越跌越买,用 ATR 决定的格距把仓位铺在 10 个价位上(底仓 1 档 + 补仓 9 档);
等全部持仓的平均成本涨够 min_profit_pct(默认 1%)后整仓卖出,赚这一段反弹。
1.2 赚什么、不赚什么
| 说明 | |
|---|---|
| 赚什么 | 区间内的反复波动。单轮收益 = 持仓规模 × min_profit_pct,所以越跌越买反而放大了单轮收益 |
| 不赚什么 | 单边趋势。单边下跌时深档被动持仓,单边上涨时过早卖空仓位 |
| 不做什么 | 不预测方向、不做择时、不做行业轮动、不设置止损 |
1.3 四个设计要点
- ATR 自适应格距 —— 格距随标的波动率自动伸缩,不用手工设价格区间。
- 底仓 + 9 档补仓 —— 共 10 个价位,每档 1000 股,固定不缩放。
- 反弹确认入场 —— 进入低位区后不立刻买,等从低点反弹 0.5% 才建网(防接飞刀)。
- 整仓止盈为主、单档兜底为辅 —— 主出口吃整段反弹,副出口防止浅档利润被反复吐回。
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% |
三条性质:
- 只在底仓时出场价最低不过锚点上方 0.52 ~ 0.73 个格距——基本就在建网位置附近, 不会被推到锚点上方一大截。
- 建仓越深,出场价越低(满仓时已在锚点下方约 4 个格距),因为加仓不断拉低平均成本。
- 占用回报稳定在 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. 配置表
三层:全局默认 → 标的白名单(每标的覆盖)→ 状态。
# ---------- 全局默认 ----------
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 | 模拟盘联调 | 跑通"观察 → 建网 → 逐档成交 → 整仓止盈 → 重建"完整循环 |