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