本文策略用于量化研究和程序设计交流,不构成投资建议。DCA 会在单边行情中累积仓位,止损也无法消除跳空、强平、流动性和交易所故障风险。
一、固定网格在实盘中的三个问题
固定网格看起来很直观:价格每下跌一个固定百分比就补一层,反弹后统一卖出。但市场的波动尺度一直在变化,同一组间距很难长期有效。
第一个问题是网格距离失真。ETH 在低波动阶段可能几天都走不出 1%,固定间距过宽时几乎没有成交;事件行情中几分钟就能波动数个百分点,原来的网格又会过密,短时间内把所有仓位打满。
第二个问题是“补仓规则”经常没有明确的总预算。有些实现只规定每一层比上一层多买多少,却没有在周期开始前锁定最大投入。连续下跌时,策略表面上是在降低均价,实际上可能是在不断扩大无法承受的尾部敞口。
第三个问题是挂单和持仓会脱节。部分成交、撤单未确认、进程重启或人工操作,都可能留下仍在交易所生效的旧订单。若程序只根据最新信号继续创建新网格,就可能出现幽灵挂单、重复平仓或错误接管外部仓位。
D-Man V3 的价值正在于把这三个问题放进同一个周期管理:布林带定义当前波动尺度,DCA 预算在开仓前确定,所有成交层按一个篮子退出;工程实现再用订单归属和持久化状态机处理异常路径。本文给出的是 FMZ 的 Rust语言实用版。
二、D-Man V3 到底动态在哪里
它不是简单的“跌了补仓、涨了卖出”,而是让布林带承担两个任务:
- 用布林百分位判断价格是否进入统计极值区域;
- 用布林带宽度定义每一级 DCA 应该离锚点多远。
这意味着网格距离不再固定。低波动时网格自动收缩,高波动时网格自动扩张。
三、只用闭合 K 线计算布林反转
设布林带中轨、上轨和下轨分别为 Middle、Upper、Lower:
text
BB% = (Close - Lower) / (Upper - Lower)
BB% 的含义很直观:
0对应下轨;0.5对应中轨;1对应上轨;- 小于
0表示收盘价跌破下轨; - 大于
1表示收盘价突破上轨。
实用版只把 BBLength 暴露到界面;其余信号规则固定为经过测试的内部常量:
text
界面参数:BBLength = 100
内部规则:BB_STD = 2.0,LONG_THRESHOLD = 0,SHORT_THRESHOLD = 1
内部规则:REQUIRE_CROSS = true
FMZ 的 GetRecords(symbol, period, limit) 能直接请求指定合约和周期的 K 线。Rust 版取倒数第二根作为当前闭合 K 线,最后一根仍在形成中的 K 线不参与信号。FMZ GetRecords 文档
信号函数本身很小:
rust
fn detect_signal(
current: &Bands,
previous: &Bands,
long_threshold: f64,
short_threshold: f64,
require_cross: bool,
) -> i32 {
let mut long_now = current.bbp <= long_threshold;
let mut short_now = current.bbp >= short_threshold;
if require_cross {
long_now = long_now && previous.bbp > long_threshold;
short_now = short_now && previous.bbp < short_threshold;
}
if long_now && !short_now {
1
} else if short_now && !long_now {
-1
} else {
0
}
}
“首次穿越”很重要。价格若连续运行在下轨之外,均值回归策略不应该每根 K 线都增加一个新周期。这个过滤器不能判断趋势何时结束,但可以阻止重复暴露无限增长。
四、布林半宽决定 DCA 网格尺度
先计算布林带半宽占中轨的原始比例,再裁剪为策略实际使用的动态尺度:
text
HalfWidthRatio = (Upper - Lower) / (2 × Middle)
ScaleRatio = clamp(HalfWidthRatio, 0.2%, 8%)
以信号出现时的最新成交价为锚点:
text
LongPrice[i] = AnchorPrice × (1 - Factor[i] × ScaleRatio)
ShortPrice[i] = AnchorPrice × (1 + Factor[i] × ScaleRatio)
基线使用四个距离因子:
text
0.05, 0.35, 0.75, 1.25
假设 ETH 为 3000 USDT,信号时半宽为 2%,多头计划大致如下:
| 层级 | 因子 | 相对锚点距离 | 理论价格 |
|---|---|---|---|
| L1 | 0.05 | 0.10% | 2997 |
| L2 | 0.35 | 0.70% | 2979 |
| L3 | 0.75 | 1.50% | 2955 |
| L4 | 1.25 | 2.50% | 2925 |
ScaleRatio 对应 Rust 源码中的 scale_ratio。设置上下限有两个原因:窄带环境下,多个理论价格可能经过 tick size 量化后重叠;极端波动下,网格又可能扩散到失去执行意义的位置。
价格量化也不是普通四舍五入:
rust
fn round_price(runtime: &Runtime, price: f64, buy: bool) -> f64 {
round_decimals(
quantize(
price,
runtime.market.price_step,
!buy, // 买单向下,卖单向上
),
decimal_places(
runtime.market.price_step,
runtime.market.price_precision,
),
)
}
买单向下取整、卖单向上取整,避免量化把挂单推向更激进的一侧。所有配置层的价格必须严格单调;只要有两层量化到同一个价格,整个周期就会被拒绝。
五、预算上限比“补仓倍数”更重要
动态 DCA 最容易被误解成无限补仓。这里在创建周期时就固定最大预算:
text
CycleBudget = min(
TotalCycleQuote,
AccountEquity × MaxCycleEquityPct
)
默认资金权重为:
text
1, 1, 1.5, 2.5
默认四层分别获得约 16.67%、16.67%、25% 和 41.66% 的周期预算。越深层权重越高,但策略不会在配置层数之外临时追加新层,也不会在亏损后扩大总预算。
FMZ 合约接口的 amount 通常是张数,不能把 USDT 金额直接传给下单函数。Rust 版从 GetMarkets() 读取:
TickSize / AmountSize;PricePrecision / AmountPrecision;MinQty / MaxQty;MinNotional / MaxNotional;CtVal / CtValCcy。
然后把报价币预算换算成合约张数。FMZ GetMarkets 文档
核心规划可以简化为:
rust
let scale_ratio = clamp(
bands.half_width_ratio,
DCA_SCALE_MIN_PCT / 100.0,
DCA_SCALE_MAX_PCT / 100.0,
);
let budget = TotalCycleQuote.min(
equity * MaxCycleEquityPct / 100.0,
);
for (spread, weight) in spreads.iter().zip(weights.iter()) {
let raw_price = if signal == 1 {
anchor_price * (1.0 - spread * scale_ratio)
} else {
anchor_price * (1.0 + spread * scale_ratio)
};
let quote = budget * weight / weight_total;
let amount = amount_from_quote(runtime, quote, raw_price)?;
// 随后检查价格、张数和名义金额约束
}
只要某一层小于最小张数、超过最大张数或违反名义金额限制,程序拒绝整个周期,不留下缺层计划。
六、把所有成交层作为一个篮子退出
DCA 各层不一定全部成交。若分别管理每层止盈,很容易留下碎片持仓和孤立挂单。这里直接使用交易所返回的平均持仓价,将已成交层合并为一个篮子。
动态退出距离同样使用信号时确定的 ScaleRatio:
text
止损距离 = 3.00 × ScaleRatio
止盈距离 = 1.00 × ScaleRatio
移动止盈启动 = 0.80 × ScaleRatio
移动止盈回撤 = 0.25 × ScaleRatio
此外还支持:
- 价格回到中轨且篮子收益不低于内部常量
MIN_PROFIT_PCT=0.15%时退出; - 持有超过
MaxHoldingBars时退出; - 达到内部移动止盈启动线后,按
ScaleRatio计算允许回撤。
多空收益必须分开计算:
rust
fn position_return(position: &PositionView, price: f64) -> f64 {
if position.price <= 0.0 {
0.0
} else if position.side == 1 {
price / position.price - 1.0
} else {
1.0 - price / position.price
}
}
如果空头也使用 price / entry - 1,价格上涨反而会显示为盈利。这类方向错误很适合交给 Rust 单元测试固定下来。
七、从信号到退出:把交易看成一个周期
一个可靠的 DCA 策略不能把“下单成功”当成唯一状态。挂单可能部分成交,请求可能超时,策略进程也可能在订单已经到达交易所后中断。因此,Rust 版把每次反转交易建模为一个完整周期:
text
IDLE
-> PLACING
-> ACTIVE
-> CANCELING_FOR_EXIT
-> SUBMITTING_EXIT
-> EXITING
-> IDLE
入场前先保存 PLACING 和当前待提交层级;拿到有效订单 ID 后,才把订单纳入周期。若请求已经发出却没有返回 ID,策略会暂停,而不是假设失败后再次发送。
退出也有明确顺序:先停止增加仓位,撤销剩余 DCA 挂单,确认撤单完成,再按交易所返回的实际持仓量提交整篮平仓。持仓连续两次确认为零后,周期才真正结束。
订单 ID 会随周期持久化。策略重启后先恢复订单归属,再核对挂单和持仓;如果发现无法证明属于本周期的订单,或者实际持仓方向与周期方向不一致,就进入 HALT 等待人工处理。这种保守设计牺牲了一部分自动恢复能力,换来的是不盲目补发订单。
八、真正值得调整的核心参数
参数并不是越多越专业。对这个策略而言,真正影响行为的参数可以归为四组:信号速度、周期预算、DCA 形状和退出尺度。
| 决策 | 主要参数与默认值 | 解决的问题 |
|---|---|---|
| 信号尺度 | SignalPeriodMinutes=15,BBLength=100 | 信号频率、统计窗口和响应速度 |
| 周期预算 | TotalCycleQuote=1000,MaxCycleEquityPct=10% | 限制固定投入及其占账户权益的比例 |
| DCA 形状 | DcaSpreadFactors=0.05,0.35,0.75,1.25,DcaAmountWeights=1,1,1.5,2.5 | 决定各层位置和预算分布 |
| 价格退出 | StopLossBandFactor=3,TakeProfitBandFactor=1 | 用同一个波动尺度定义整篮止损与止盈 |
| 时间退出 | MaxHoldingBars=192 | 均值迟迟不回归时终止周期 |
调参应遵循依赖关系。SignalPeriodMinutes 和 BBLength 共同决定统计窗口覆盖的真实时间;DCA 距离与资金权重必须成对调整;固定预算和权益比例共同决定实际投入上限;止盈止损倍数则基于同一个动态波动尺度。
布林标准差、首次穿越、带宽过滤、DCA 尺度上下限、挂单有效期、冷却期和移动止盈等规则保持固定,可以减少大量难以解释的组合。只有在明确研究某一条规则时,才值得单独放开。
九、它适合什么行情,又怕什么行情
D-Man V3 本质上仍是均值回归策略。它最适合流动性充足、波动反复扩张和收缩、价格经常从带外回到中轨的市场。动态网格可以适应波动尺度变化,但不能判断一次突破究竟是假突破还是新趋势的开始。
最危险的是持续单边行情。价格越走越远时,深层 DCA 会让均价更接近市场,却也会让名义仓位集中在风险最大的阶段。预算上限只能限制投入规模,不能把负期望的趋势逆行交易变成安全交易。
第二类风险来自波动突变。布林半宽取自信号附近的历史窗口,而事件行情可能让未来波动瞬间跳到另一个量级。即使设置止损,真实成交也可能受滑点、流动性和交易所限制造成明显偏离。
第三类风险是成本侵蚀。四层挂单、撤单和整篮退出会产生手续费,永续合约还可能承受资金费率。若单周期目标过小,毛利润很容易被执行成本吃掉。
十、回测应该回答哪些问题
对 DCA 策略,只看最终净利润远远不够。更有价值的是把回测拆成一个个独立周期,观察策略在不同市场状态下如何使用资金。
至少应记录:
- 单周期最大投入和最大保证金占用;
- 每一层 DCA 的触发率和成交贡献;
- 周期最大不利波动与最终盈亏;
- 最长持仓时间和时间退出比例;
- 连续亏损周期数量;
- 手续费、滑点和资金费率占毛利润的比例;
- 止损、止盈、移动止盈、中轨退出和时间退出各自的贡献。
样本还应按市场状态拆分,而不是只跑一个长区间:
| 场景 | 重点观察 |
|---|---|
| 低波动横盘 | 网格是否过窄、利润能否覆盖成本 |
| 高波动震荡 | 动态距离是否有效、深层仓位是否频繁触发 |
| 持续上涨或下跌 | 最大回撤、止损和预算上限是否真正生效 |
| 事件性跳跃 | 滑点、跳空和未成交退出的影响 |
最后再做参数敏感性分析。如果只有一组非常精确的参数盈利,而邻近取值全部失效,通常说明结果依赖样本巧合,而不是稳定机制。
十一、工程可靠性也是策略的一部分
均值回归公式很短,真实交易路径却很长。部分成交、订单查询失败、撤单延迟、进程重启和人工订单都会改变实际风险。因此,本策略坚持几个边界:
- 同一标的一次只管理一个活动周期;
- 不自动接管无法证明归属的持仓或挂单;
- 请求结果不确定时不盲目重发;
- 退出前先撤销剩余入场单;
- 按实际持仓量平仓,而不是按计划数量平仓;
- 杠杆和持仓模式必须事先明确,不能把账户设置当成策略参数优化。
这些规则不会提高信号胜率,却能减少“回测公式正确、实盘订单失控”的差距。对于会逐层增加仓位的策略,这种可靠性本身就是风险控制的一部分。
十二、总结
D-Man V3 的核心可以概括为三句话:BB% 寻找统计极值,布林半宽定义 DCA 尺度,所有成交层按平均持仓价作为一个篮子退出。
它比固定网格更适应波动变化,也用周期预算限制了无限补仓,但仍然无法消除均值回归在单边趋势中的尾部风险。真正决定策略能否长期使用的,不只是入场公式,还包括预算约束、执行成本、状态恢复和对异常订单的保守处理。
因此,评价这类策略时,应该把“赚了多少”放到“用了多少资金、承受了多大不利波动、在哪些行情中失效”之后。动态 DCA 的价值不是预测每一次反转,而是在反转假设成立与失败时,都让风险路径尽可能清楚。
感谢您的阅读!
- 1




