输入/搜索内容
8
关注
1370
关注者
用 Rust 在 FMZ 实现 D-Man V3 布林带反转DCA策略
交流分享
创建于 2026-08-02 20:37:16  更新于 2026-08-03 16:29:00
 0
 2

img

本文策略用于量化研究和程序设计交流,不构成投资建议。DCA 会在单边行情中累积仓位,止损也无法消除跳空、强平、流动性和交易所故障风险。

一、固定网格在实盘中的三个问题

固定网格看起来很直观:价格每下跌一个固定百分比就补一层,反弹后统一卖出。但市场的波动尺度一直在变化,同一组间距很难长期有效。

第一个问题是网格距离失真。ETH 在低波动阶段可能几天都走不出 1%,固定间距过宽时几乎没有成交;事件行情中几分钟就能波动数个百分点,原来的网格又会过密,短时间内把所有仓位打满。

第二个问题是“补仓规则”经常没有明确的总预算。有些实现只规定每一层比上一层多买多少,却没有在周期开始前锁定最大投入。连续下跌时,策略表面上是在降低均价,实际上可能是在不断扩大无法承受的尾部敞口。

第三个问题是挂单和持仓会脱节。部分成交、撤单未确认、进程重启或人工操作,都可能留下仍在交易所生效的旧订单。若程序只根据最新信号继续创建新网格,就可能出现幽灵挂单、重复平仓或错误接管外部仓位。

D-Man V3 的价值正在于把这三个问题放进同一个周期管理:布林带定义当前波动尺度,DCA 预算在开仓前确定,所有成交层按一个篮子退出;工程实现再用订单归属和持久化状态机处理异常路径。本文给出的是 FMZ 的 Rust语言实用版。

二、D-Man V3 到底动态在哪里

本文参考的是 Hummingbot 当前公开的 directional_trading.dman_v3 框架,它不是简单的“跌了补仓、涨了卖出”,而是让布林带承担两个任务:

  1. 用布林百分位判断价格是否进入统计极值区域;
  2. 用布林带宽度定义每一级 DCA 应该离锚点多远。

这意味着网格距离不再固定。低波动时网格自动收缩,高波动时网格自动扩张。

三、只用闭合 K 线计算布林反转

设布林带中轨、上轨和下轨分别为 MiddleUpperLower

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%,多头计划大致如下:

层级因子相对锚点距离理论价格
L10.050.10%2997
L20.350.70%2979
L30.751.50%2955
L41.252.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=15BBLength=100信号频率、统计窗口和响应速度
周期预算TotalCycleQuote=1000MaxCycleEquityPct=10%限制固定投入及其占账户权益的比例
DCA 形状DcaSpreadFactors=0.05,0.35,0.75,1.25DcaAmountWeights=1,1,1.5,2.5决定各层位置和预算分布
价格退出StopLossBandFactor=3TakeProfitBandFactor=1用同一个波动尺度定义整篮止损与止盈
时间退出MaxHoldingBars=192均值迟迟不回归时终止周期

调参应遵循依赖关系。SignalPeriodMinutesBBLength 共同决定统计窗口覆盖的真实时间;DCA 距离与资金权重必须成对调整;固定预算和权益比例共同决定实际投入上限;止盈止损倍数则基于同一个动态波动尺度。

布林标准差、首次穿越、带宽过滤、DCA 尺度上下限、挂单有效期、冷却期和移动止盈等规则保持固定,可以减少大量难以解释的组合。只有在明确研究某一条规则时,才值得单独放开。

九、它适合什么行情,又怕什么行情

D-Man V3 本质上仍是均值回归策略。它最适合流动性充足、波动反复扩张和收缩、价格经常从带外回到中轨的市场。动态网格可以适应波动尺度变化,但不能判断一次突破究竟是假突破还是新趋势的开始。

最危险的是持续单边行情。价格越走越远时,深层 DCA 会让均价更接近市场,却也会让名义仓位集中在风险最大的阶段。预算上限只能限制投入规模,不能把负期望的趋势逆行交易变成安全交易。

第二类风险来自波动突变。布林半宽取自信号附近的历史窗口,而事件行情可能让未来波动瞬间跳到另一个量级。即使设置止损,真实成交也可能受滑点、流动性和交易所限制造成明显偏离。

第三类风险是成本侵蚀。四层挂单、撤单和整篮退出会产生手续费,永续合约还可能承受资金费率。若单周期目标过小,毛利润很容易被执行成本吃掉。

十、回测应该回答哪些问题

img

对 DCA 策略,只看最终净利润远远不够。更有价值的是把回测拆成一个个独立周期,观察策略在不同市场状态下如何使用资金。

至少应记录:

  • 单周期最大投入和最大保证金占用;
  • 每一层 DCA 的触发率和成交贡献;
  • 周期最大不利波动与最终盈亏;
  • 最长持仓时间和时间退出比例;
  • 连续亏损周期数量;
  • 手续费、滑点和资金费率占毛利润的比例;
  • 止损、止盈、移动止盈、中轨退出和时间退出各自的贡献。

样本还应按市场状态拆分,而不是只跑一个长区间:

场景重点观察
低波动横盘网格是否过窄、利润能否覆盖成本
高波动震荡动态距离是否有效、深层仓位是否频繁触发
持续上涨或下跌最大回撤、止损和预算上限是否真正生效
事件性跳跃滑点、跳空和未成交退出的影响

img

最后再做参数敏感性分析。如果只有一组非常精确的参数盈利,而邻近取值全部失效,通常说明结果依赖样本巧合,而不是稳定机制。

十一、工程可靠性也是策略的一部分

均值回归公式很短,真实交易路径却很长。部分成交、订单查询失败、撤单延迟、进程重启和人工订单都会改变实际风险。因此,本策略坚持几个边界:

  • 同一标的一次只管理一个活动周期;
  • 不自动接管无法证明归属的持仓或挂单;
  • 请求结果不确定时不盲目重发;
  • 退出前先撤销剩余入场单;
  • 按实际持仓量平仓,而不是按计划数量平仓;
  • 杠杆和持仓模式必须事先明确,不能把账户设置当成策略参数优化。

这些规则不会提高信号胜率,却能减少“回测公式正确、实盘订单失控”的差距。对于会逐层增加仓位的策略,这种可靠性本身就是风险控制的一部分。

十二、总结

策略源码

D-Man V3 的核心可以概括为三句话:BB% 寻找统计极值,布林半宽定义 DCA 尺度,所有成交层按平均持仓价作为一个篮子退出。

它比固定网格更适应波动变化,也用周期预算限制了无限补仓,但仍然无法消除均值回归在单边趋势中的尾部风险。真正决定策略能否长期使用的,不只是入场公式,还包括预算约束、执行成本、状态恢复和对异常订单的保守处理。

因此,评价这类策略时,应该把“赚了多少”放到“用了多少资金、承受了多大不利波动、在哪些行情中失效”之后。动态 DCA 的价值不是预测每一次反转,而是在反转假设成立与失败时,都让风险路径尽可能清楚。

img

感谢您的阅读!

相关推荐
评论
全部评论 (0)
暂无数据
暂无数据
  • 1
社区
回测系统
APP 下载
iPhone 下载
© 2015 - ∞ INVENTOR PTE LTD (SG)