2021年5月的那次夜间插针,我盯了四个小时的盘,最后还是没扛住。现货在半小时内跌了12%,合约空单因为保证金率跌破维持线被交易所自动平仓,对冲腿凭空消失,账户净值直接击穿止损线。第二天复盘时我意识到,问题不在行情判断,而是对冲动作慢了一步,仓位管理也没有自动化——当市场骤变时,人脑和手速根本跟不上。
那之后我开始写AutoHedge,一个以Delta中性为底层的自动对冲执行框架。目标很纯粹:让"对冲"这件事不再依赖盯盘和手速,而是靠一套可回测、可监控、可熔断的代码闭环。这篇文章把我从需求梳理、架构设计到回测踩坑、实盘运营的全过程整理出来,给正在做量化对冲或准备入场的读者一个参考。
1. 我为什么决定自己写一个AutoHedge:从一次亏损到一套系统
1.1 手工对冲的三大痛点
先说说我之前的对冲方式。如果你和我一样,手里持有现货,又担心短期回调,最自然的操作是开一笔等额的合约空单。听起来很简单,但实际上有三个绕不开的问题。
第一,开仓时机的选择。很多人会在感觉"差不多到顶了"的时候开空,但感觉这东西在波动率放大的行情里极不可靠。我在2021年4月就试过提前开空,结果现货继续涨,空单浮亏填掉了现货的浮盈,净值曲线反而更难看。这本质上是择时问题,手工操作很难避免。
第二,仓位动态平衡的缺失。现货涨了,空单还是原来的数量,Delta就不再为零,整个组合的敞口会漂移。比如你1 BTC现货配1 BTC空单,币价涨了10%,现货增值0.1 BTC,但空单还是1 BTC,净敞口变成+0.1 BTC。如果继续上涨,这个正Delta敞口会越滚越大。手工调整需要每时每刻盯盘,精力根本撑不住。
第三,风险事件的响应速度。插针行情、清算瀑布、资金费率剧烈变化,这些事件从发生到结束往往只有几分钟甚至几十秒。手工操作从发现到下单,最快也要几秒,而且在极端行情下交易所的界面还可能卡顿、下单按钮失效。这根本不是纪律问题,是人机交互的物理瓶颈。
1.2 市面方案和自研的取舍
决定自研之前,我也研究过现成的对冲工具。一类是交易所自带的策略委托,比如币安的策略网格、OKX的组合交易,优点是开箱即用,缺点是参数黑盒、无法做跨市场对冲,也无法回测。另一类是第三方商业软件,功能看起来齐全,但要么收费按年化抽成,要么代码不开源,万一行情接口变化或策略有bug,你只能干等客服回复。
自研AutoHedge的决策逻辑其实很简单:对冲策略属于"低容错"场景,我需要100%掌握资金流向和异常处理逻辑。即使初期写出来的代码粗糙一点,但只要我能看到每一条日志、每一个订单状态,出了问题就能定位修复。作为一个有编程基础的非科班交易者,我选择了Python搭底、交易所官方API对接、SQLite记录交易流水,这样能最快跑通闭环。
AutoHedge的定位不是全自动赚钱机器,而是一个"风险控制器":它不做方向性预测,只负责把组合敞口控制在一个可接受的范围内,同时尽量减少对冲成本。这个定位听起来保守,但对个人交易者来说,活下来比赚得多重要得多。
2. AutoHedge的中枢:对冲比例动态计算引擎
2.1 从Delta中性说起——对冲不是简单地反向开单
AutoHedge的核心逻辑建立在Delta中性这个金融工程概念上。简单说,Delta衡量的是持仓价值对价格变动的敏感度:现货持仓的Delta是正1,一份做空合约的Delta是负1。当两者数量相等时,组合Delta为0,理论上无论价格怎么涨跌,总资产净值都不变。
但加密市场的现实远比教科书复杂。合约存在资金费率,多头和空头之间定期支付费用,这让"中性"状态本身在流血;合约还有强平机制,如果你的保证金不足,空单会被交易所强行平掉,对冲腿瞬间消失;更麻烦的是基差——合约价格和现货价格之间始终存在价差,开平仓时这层价差会变成实际成本。
所以我从一开始就没打算做"精确到每个tick都中性"的策略,而是把Delta控制在一个动态区间内。比如现货价值10万U,合约空单价值9.8万U,净敞口为+0.2万U(约2%),这个敞口在正常波动下是可接受的。AutoHedge每隔几秒计算一次组合Delta,只在偏离度超过阈值时才触发调整,这样既控制风险,又不会因为频繁交易把利润全交给手续费。
2.2 为什么固定比例对冲不靠谱:波动率自适应的调仓阈值
我第一版AutoHedge用的是固定阈值:Delta绝对值超过2%就调仓。实盘跑了一周后发现问题很大——在波动率极低的横盘期,价格很少触及阈值,系统长时间不动作这没问题;但一旦波动率放大,2%的偏离可能在一分钟内反复触发,止损单和补仓单互相打架,手续费累计起来非常吓人。
后来我给系统加了一个波动率自适应的调仓阈值,核心是用ATR(Average True Range,平均真实波幅)来动态衡量当前市场状态。ATR高说明波动剧烈,阈值要放宽;ATR低说明市场平稳,阈值可以收紧。公式大致是:
threshold = base_rate × ATR_period / ATR_baseline
其中base_rate是基础比例,比如1.5%;ATR_period是当前时间窗口的ATR值;ATR_baseline是过去一段时间的平均ATR。当波动率翻倍时,阈值也接近翻倍,系统就不会在剧烈震荡里被反复打脸。
这个参数直接影响对冲频率和成本,需要在回测里精细调参。我最终选的是base_rate在1.2%到2.0%之间,ATR周期用15分钟,基线周期用24小时。这个组合在BTC和ETH上都跑出了比较平滑的净值曲线,但如果你要应用到其他币种,建议从头重新回测,不要直接抄参数。
2.3 计算引擎的代码骨架
AutoHedge的调度循环并不复杂,核心就是"读取行情→计算敞口→决策→执行"。下面这段代码是我简化后的引擎骨架,去掉了断线重连和风控分支,只保留最关键的计算逻辑:
import time import numpy as np class HedgeEngine: def __init__(self, base_rate=0.015, atr_period=900, atr_baseline=86400): self.base_rate = base_rate self.atr_period = atr_period self.atr_baseline = atr_baseline self.position = {"spot": 0.0, "short": 0.0} self.prices = [] def update_prices(self, spot_price, contract_price): self.prices.append(spot_price) if len(self.prices) > 5000: self.prices.pop(0) # 计算ATR,这里用简化版:近期平均波动幅度 if len(self.prices) < 20: return 0, 0 recent = self.prices[-20:] atr_now = np.mean(np.abs(np.diff(recent))) baseline = np.mean(np.abs(np.diff(self.prices[-200:]))) # 动态阈值 threshold = self.base_rate * (atr_now / baseline if baseline else 1.0) return threshold, atr_now def calc_delta(self, spot_price, contract_price): # 现货Delta为持仓金额,空单Delta为负的持仓金额 spot_delta = self.position["spot"] short_delta = -self.position["short"] net_delta = spot_delta + short_delta total_value = spot_delta + abs(short_delta) return net_delta / total_value if total_value else 0 def should_hedge(self, net_delta_ratio, threshold): return abs(net_delta_ratio) > threshold这段代码里我刻意把ATR算得很粗糙,实际生产版本还要考虑资金费率、订单深度和保证金余额。不过核心思想已经体现出来:先计算当前净敞口比例,再和动态阈值比较,决定要不要触发对冲操作。
说句实话,计算引擎本身不难写,真正难的是下一步:当触发对冲信号后,怎么把这个信号变成一个成交快、损耗小的真实订单。
3. 订单执行层:延迟与滑点的实战博弈
3.1 为什么不能一口气下大单:订单拆分与冰山委托
我第一版AutoHedge在执行对冲时是"信号触发就一次性下单"。有一次现货持仓5 BTC,系统判断需要增加1个BTC的空单,我直接下了一个1 BTC的市价空单,结果成交均价比起盘口最优价差了将近0.3%。在流动性好的BTC永续合约上,1 BTC的单子其实不算太大,但因为是市价单,吃掉了好几档卖单,滑点就这样白白交出去了。
之后我改成了订单拆分加冰山委托的思路。具体做法是:把目标对冲数量拆成若干小单,每单数量不超过盘口前五档总量的10%,然后用限价单挂在盘口附近。每一次只挂一部分,成交后再挂下一部分,就像冰山一样,海面上只露出一角,市场不会因为你的单子出现明显波动。
这里有个实时性矛盾:拆分得太碎,每单之间要等待成交确认,整个对冲动作可能拖几十秒,价格早就变了;拆分得太粗,又失去了降低冲击的意义。我实测下来,对BTC永续来说,单笔5000到10000美元的限价单是比较平衡的区间,ETH的话可以适当缩小。
3.2 跨交易所时钟同步问题
AutoHedge不只是在一个交易所运行。我经常用币安现货加Bybit合约做对冲,因为这两家的深度最好,资金费率也相对合理。但跨交易所之后,一个以前没注意到的问题浮出水面:订单同步执行的时间差。
这里说的不是网络延迟那么简单,而是两个交易所的撮合时钟并不一致。我曾在回测里假设双腿订单同时成交,但实盘中币安先成交、Bybit延迟1.2秒才成交,这1.2秒里价格剧烈波动的话,对冲比例就偏了。
解决方法分三层:第一,在本地维护一个统一逻辑时钟,所有订单都以本地收到成交回执的时间为准,而不是交易所服务器时间;第二,下单时先发速度较慢的交易所,再发速度快的,让两边的成交时间尽量对齐;第三,如果两条腿的成交时间差超过设定阈值(比如500毫秒),系统自动标记这次对冲为"延迟执行",并在之后补偿调整。
这套逻辑写起来繁琐,但它决定了对冲动作是否真的"同步"。如果只看交易所返回的时间戳,你会被它们各自的上报延迟骗得团团转。
3.3 订单执行失败后的补偿逻辑:最容易被忽略的环节
订单可能失败的原因很多:余额不足、仓位限制、交易所接口限流、网络中断。AutoHedge必须对每一种失败情况有明确反应,否则对冲信号发出去了,仓位却没变,系统还傻乎乎地继续等。
我的方案是给每个对冲信号绑定一个状态机。状态包括:PENDING、PARTIAL_FILLED、FILLED、FAILED、CANCELLED。当订单状态在超时时间内未完全成交,进入补偿分支:
- 如果是部分成交,先用剩余数量的50%重新挂单,同时收紧阈值;
- 如果完全失败,立即重新计算当前敞口,若偏离度仍然超过阈值,则切换备选交易所下单;
- 如果连续三次补偿都失败,直接触发全局熔断,停止所有策略动作并发送报警通知。
这段补偿逻辑是我踩了很多坑才补上的。最初版本只处理了"成功"和"失败"两种情况,结果遇到一次币安API限流,对冲信号发了但没成交,现货下跌时系统毫无防备。现在我看任何策略代码,第一个问号就是:这个订单如果失败了,你的系统接下来会做什么?
4. 回测框架里的坑:模拟器永远比实盘温柔
4.1 手续费和资金费率建模,差一点就差很多
AutoHedge的盈利模型本质上是在"避险收益"和"对冲成本"之间做权衡。如果回测里少算了成本,实盘一定会给你一个惨痛的教训。
手续费这块,很多新手只算开平仓的手续费率,忘了合约的吃单和挂单费率不同,也忘了资金费率是定时收取的。我回测第一版时把手续费统一按0.04%算,结果实盘跑下来成本比回测高了近40%,原因就是部分成交在盘口深度不足时变成了吃单,吃了更高的taker费率,而且资金费率的影响完全被我忽略掉了。
资金费率更阴险。它是每8小时收取一次的,多空双方根据持仓方向互相支付。如果你永远持有对冲空单,那么在资金费率为正的市场里,你要不断向多头支付资金费,一年累计下来可能是本金的5%到15%。回测时必须把历史资金费率数据拉进来,逐期结算,而不是干脆不考虑。
我后来用了一个很笨但很有效的方式:把交易所API提供的历史资金费率逐条存库,回测时按时时间戳逐期扣款。这样虽然慢,但至少回测出来的成本曲线和实盘的一致性高很多。
4.2 撮合延迟和"价差幻觉"
回测中最容易给人安全感的是撮合模型。用tick级逐笔撮合当然最真实,但数据量大、处理时间长;用K线撮合又太乐观——它假设你总能在K线内的最优价格成交,忽略了真实订单簿的深度和排队时间。
我踩过一个典型的"价差幻觉":回测里现货和合约的价差序列看起来很平滑,AutoHedge每次调仓都能拿到不错的价差收益,年化回测超过30%。但实盘里,价差序列因为延迟和滑点变得毛糙,同样的策略年化直接掉到8%以下。这个问题不是说策略不行,而是回测撮合假设太乐观。
我的解决办法是:在回测引擎里加了一个"悲观滑点模型",即每笔限价单成交价都比最优买价/卖价差0.05%到0.1%,市价单再额外加一个基于订单簿深度的冲击成本。这样回测出来的收益比实盘略微悲观一点,但至少不会给你虚假的信心。
4.3 回测和实盘的预期收益差:我的实测对照表
这里放一个我整理的回测与实盘对照数据(BTC永续对冲策略,3个月周期)。为了避免争议,我把绝对收益做模糊化处理,只看相对关系:
| 指标 | 回测(乐观撮合) | 回测(悲观滑点) | 实盘 |
|---|---|---|---|
| 年化收益率 | 32.5% | 14.2% | 11.8% |
| 最大回撤 | 3.8% | 5.6% | 7.2% |
| 月度调仓次数 | 18 | 23 | 27 |
| 总手续费成本率 | 1.4% | 2.7% | 3.1% |
可以看到,即使加了悲观滑点,实盘收益率还是比回测低了两个多点,主要原因是极端行情下的滑点比模型预估的更大,以及API限流导致的补偿交易额外消耗了手续费。这张表我每次优化策略都会更新,它是一面诚实的镜子。
5. 实盘部署三个月后的风险清单
5.1 极端行情下的熔断开关
AutoHedge上线三个月,最惊险的一次是某交易所插针,BTC在10分钟内跌了8%,然后又迅速拉回。系统正常触发了很多次调仓,但因为波动太剧烈,订单排队和补偿逻辑频繁启动,导致手续费消耗远超预期。这时候如果没有熔断机制,策略会在混乱中不断交易,白白亏损。
我在系统里设了三个熔断条件,任何一个满足就停止所有新对冲动作:
- 单日累计调仓超过50次;
- 账户权益单日跌幅超过3%;
- 连续3次下单失败。
熔断不是终点,熔断后还要能平滑恢复。我的做法是等待10分钟冷却期,冷却期结束后重新计算敞口,如果偏离度在可控范围就以挂单方式慢慢调仓,而不是一次性追价。
5.2 仓位上限与保证金监控
对冲策略最怕的不是方向错了,而是保证金链断裂。如果你的空单保证金不足,交易所强平后,现货就完全暴露在下跌风险中,这是所有对冲策略的噩梦。
AutoHedge每笔调仓前都会查询合约账户的维持保证金率,如果当前保证金率低于警戒值(我设为30%),系统不允许继续加仓空单,哪怕对冲信号已经触发。如果保证金率继续下滑到20%,系统会主动减仓一部分空单,降低强制平仓风险。
这个逻辑听起来像是常识,但实盘中很容易被忽略。因为对冲单在盈利时保证金率反而会下降(未实现亏损在空单上是负的),很多人盯着"赚钱"就忘了保证金率已经逼近强平线。我特意在Telegram加了告警机器人,每10分钟推送一次保证金率,低于35%就发黄色预警,低于25%发红色预警加电话语音提示。
5.3 那些让我差点爆仓的细节
最后分享三个实盘细节,都是代码之外的教训。
第一,交易所的API限流规则每次更新都可能让你的程序突然失效。有一阵子交易所调整了WebSocket推送频率,我的行情更新从原来的50毫秒一次变成200毫秒一次,系统感知价格变化变慢了,而我还误以为是行情本身波动变小。后来加了一个行情推送心跳监控,超过1秒没收到行情就报警。
第二,同一个策略,在不同币种上的表现差异可能非常大。AutoHedge在BTC上稳定运行了三个月,在ETH上却频繁触发熔断。原因是ETH的合约深度不如BTC,同样的下单量对盘口冲击更大。这提醒我,参数和风控阈值必须逐币种单独设置,不能偷懒复制。
第三,也是最容易被忽视的——本地服务器的时间同步。有一段时间我的日志时间戳和交易所时间戳对不上,导致我在排查问题时无法判断先后顺序。后来给服务器配了NTP自动同步,才解决这个问题。别小看这种基础事务,排查故障时时间线乱了,一切都会变得低效。
AutoHedge到现在也没有变成"无人值守的赚钱机器",它更像一个深夜值班的守夜人。我的工作从盯盘和手动下单,变成了观察日志、调试参数、优化执行逻辑。这套系统帮我避免了几次大额回撤,也让我在暴涨暴跌时能安心睡觉。如果你的情况和我类似——持仓中有现货需要保护,又不想成为24小时看盘的工具人,那么从Delta中性这个基础概念出发,逐步搭建一套属于自己的AutoHedge,是一个值得投入的方向。记住,回测里赚到的不是真钱,能在实盘的意外中活下来的系统,才算过了第一关。