做量化这几年,我真正产生“必须把对冲交给机器来执行”这个念头,是在 2022 年 5 月的一个深夜。那晚,我用一个简单的现货持有策略,手里攥着几个 BTC 现货多头,准备在永续合约上手动开空做对冲,结果行情 5 分钟内暴跌近 8%,我盯着盘口排队、撤单、重新挂单,等空单全部成交,现货已经浮亏了大约 0.8 个 BTC。手动对冲在极端行情里根本没有胜算,这个教训买得太贵。所以后来我动手写了一套自动对冲系统,命名为 AutoHedge——它能持续监控现货与合约头寸,自动计算对冲数量,并下发限价单或市价单完成动态调仓。本文就是这套系统从立项、架构设计到实盘验证的全过程复盘,希望能给正在做仓位管理和风险对冲的朋友一些可复用的设计思路。
1. 从一次深夜爆仓到 AutoHedge 立项
1.1 手动对冲的三个致命点
那天晚上我反复复盘,发现自己不是不专业,而是在极端行情下手动操作有结构性劣势。
第一是延迟。盘口剧烈波动时,限价单成交速度极不稳定,我眼睁睁看着价格穿过止损区间,等空单排队成交,均价已经比目标差了很远。第二是情绪和纪律。亏损扩大时人会本能地倾向于“等等反弹再空”,结果反弹没来,浮亏却进一步扩大。第三是多合约维度的问题,如果一个策略同时持有 BTC 现货、ETH 现货、几个山寨币现货,要在永续合约上分别对冲,还要分配保证金、控制总风险敞口,靠手工盯盘和表格记录根本维持不了几周,人不是机器,注意力总有上限。
这三点叠加的结果,就是手动对冲在行情快速波动时,实际效果会大打折扣。我做了一个简单统计:在没有系统辅助的情况下,从决定执行对冲到全部下单完成,平均耗时 40 秒以上;而在 5 分钟级别的暴跌行情中,这 40 秒足以让滑点从 0.2% 膨胀到 1.5%,对冲成本被显著放大。
1.2 立项目标与产品边界
AutoHedge 的定位不是一套“自动化策略”,而是一个风险执行工具。它的核心目标有两个:其一,持续盯住现货多头总敞口,并计算出对应的永续合约空头目标持仓;其二,当实际对冲持仓与目标的偏差超过阈值时,自动下单修正,把组合的净敞口始终压在一个很小的区间内。
立项时我也刻意划清了产品边界。AutoHedge 不做行情预测,不自动开平初始仓位,不改写交易策略本身。它只负责一件事:在你已经决定持有一篮子现货的前提下,用机器把“对冲仓位的动态维护”这件事做扎实。边界越清晰,系统越不容易失控,后来实盘验证也证明了这一点。
2. AutoHedge 系统架构:四层模块与事件驱动消息流
2.1 数据接入层
整个系统我习惯分成四层来看,最底层负责数据接入。这一层要解决的是“账户实际状态到底是什么”的问题,包括现货账户的币种余额、冻结数量,以及永续合约账户的持仓张数、可平仓数量、保证金仓位和当前标记价格。
数据源的实现原则是“WebSocket 推送为主、REST 拉取为辅”。WebSocket 提供实时的 tick 级行情和成交推送,REST 则负责定期做一次全量快照,用于校准和兜底。拿 BTCUSDT 永续合约举例,合约乘数一般是 0.0001 BTC,1 张合约对应的名义价值会随价格变动;系统的仓位计算模块必须把“张数 x 合约乘数 x 最新标记价格”实时折算成名义价值,而不是简单地把张数当币数处理。
这里有个很容易忽略的细节:推送消息里的“持仓量”更新是增量的,某笔成交可能只撮合了 0.5 张,如果你在本地累加失败,后续计算就会漂移。所以我在数据接入层里做了一个“本地状态修正”的定时任务,每 60 秒拉一次全量账户快照,把推送过来的增量结果与全量快照做比对,偏差超过单笔最小精度后立即强制校准。
2.2 仓位与风险计算层
这一层接收数据接入层清洗后的仓位快照,统一计算组合的风险敞口。
我不直接使用交易所返回的单边持仓,而是自己维护一份“名义价值映射表”:现货按币种市值计入多头敞口,永续空单按标记价格计入空头敞口,然后再算净 delta。这样设计的好处是,即使某个币种同时存在现货和合约两笔仓位,系统也能准确判断对冲缺口。
为防止单点故障导致错误下单,计算层会做三重复核:交易所全量快照、WebSocket增量累计值、上一轮的计算结果,三者两两一致才允许进入决策层。这个设计在开发初期被我自己删掉过一次,后来一次真实故障证明它是必须的,后面在第 6 节里会详细讲。
2.3 决策层与调仓边界判断
决策层拿到的输入很简单:当前实际对冲比例、目标对冲比例、以及允许的偏差区间。系统只在偏差超过下边界或上边界时触发调仓信号,不做连续频繁的下单。
这个“只在边界外行动”的设计逻辑很关键。如果每来一次价格波动就立刻调整对冲,系统会陷入无休止的重新平衡,交易手续费和资金费用会吞噬收益。更合理的做法是设一个“死区”:只要实际对冲比例在目标值的 95% 到 105% 之间,就认为系统处于可接受状态,不产生任何订单。一旦超出死区,才重新计算目标张数并下单。死区宽度我自己实证后用 5% 相对偏差,现货与合约不会出现大的裸暴露,交易频率也基本控制在每天 1 到 3 次。
2.4 订单执行层
订单执行层是最容易出 bug 的地方。它有几种执行路径:限价单挂被动单、吃单的 taker 市价单、以及带 algo 参数的“只做 maker”委托。我的默认策略是优先用限价单做被动成交,因为吃单手续费普遍更高,长期对冲成本差很多。为了确保挂出去的单不会在行情突变时成交不了,执行层还会加一条兜底路径:如果限价单超过 2 秒仍未成交,系统会撤销并重挂一个新价格,或者直接降级为 taker 单,具体取决于行情偏离幅度。
整套系统各层之间用消息队列异步传递事件,订单状态和仓位状态的事件队列全部落库。这样即便进程崩溃重启,也能从数据库恢复上一轮的真实状态,不会出现“订单实际没成交,但内存里以为成交了”的灾难。
3. 核心模型:delta 中性、对冲比例与动态调仓边界
3.1 对冲比例:从现货数量到合约张数
要做 delta 中性对冲,第一件事是把现货数量换算成永续合约张数。
以 1 个 BTC 现货为例,假设 BTC 当前价格为 65000 USDT,永续合约乘数为 0.0001 BTC/张,那么 1 张合约代表 0.0001 个 BTC,1 个 BTC 现货等于 10000 张合约的名义多头。计算目标是持有 10000 张永续空单,实现净敞口为零。
公式可以写成:
目标空单张数 = 现货数量 × 现货标记价格 /(合约乘数 × 永续标记价格)如果两个价格近似相等,公式就退化为“现货数量 / 合约乘数”。实际处理时我会让系统同时拿现货标记价和永续标记价分别计算,防止上场价格出现短暂脱锚导致张数偏差。交割合约与永续合约的乘数可能不同,所以这块不能写死,必须做成配置项。
3.2 动态调仓:死区与触发阈值
delta 中性不是算术上的“张数相等”就行,还要考虑调仓成本。所以我引入了两个阈值:下边界和上边界。
举个例子,假设目标空单是 10000 张,允许偏差 5%,那么实际空单在 9500 到 10500 张之间都算正常。当价格下跌导致现货多头在组合中的权重变大,实际空单相对不足,偏差超过上边界时,系统开追加空单;反过来,如果现货价格上涨,空单相对过多,系统平掉部分空单。
死区的好处是避免了“行情每跳一次就触发一次调仓”。以太坊、山寨币对 BTC 汇率波动大的情况下,死区调整到 8% 到 10% 也合理;主流币种 5% 左右足够。对低频对冲需求来说,这个参数并不需要很精细,关键是让它自适应价格波动的噪声。
3.3 资金费率、滑点与总成本核算
很多人以为对冲的代价只有手续费,其实资金费率才是长期成本的大头。永续合约每 8 小时结算一次资金费率,多头和空头之间相互支付。做 delta 中性对冲时,如果你持有空单,资金费率是正数,你会收到资金费;如果资金费率是负数,你就要支付资金费。
实盘中的最优情况是在资金费率整体为正的行情里持续做空对冲,这样既能防下跌,还能顺便赚一笔资金费;但资金费率由多空持仓比例决定,方向不可控。系统里我单独记录一笔“资金费率收支日志”,每周统计一次净收入,并把它作为对冲总成本的一部分。滑点方面,回测中我统一按 0.05% 到 0.1% 的 taker 滑点估计,实盘来看限价单被动成交的实际滑点通常还要小一些。
4. 核心代码实现与回测框架搭建
4.1 仓位快照与对冲缺口计算
这一节我挑几个核心模块的代码讲实现思路。首先是仓位快照的计算。为了简洁,这里给出一个简化版的 Python 示例:
from dataclasses import dataclass @dataclass class Position: symbol: str side: str # LONG / SHORT / FLAT contract_qty: float # 合约张数 mark_price: float contract_value: float # 每张合约对应的币数量,BTC永续通常为 0.0001 def spot_notional(spot_amount: float, spot_price: float) -> float: return spot_amount * spot_price def perp_notional(contract_qty: float, contract_value: float, mark_price: float) -> float: return contract_qty * contract_value * mark_price def hedge_ratio(spot_s, spot_p, perp_qty, perp_value, perp_p) -> float: s_notional = spot_notional(spot_s, spot_p) p_notional = perp_notional(perp_qty, perp_value, perp_p) if s_notional == 0: return 1.0 return p_notional / s_notionalhedge_ratio就是当前空头名义价值与现货多头名义价值的比值,目标值是 1.0。系统启动时先按公式算出目标空单张数,如果实际张数与目标张数偏差超过死区,就生成调仓信号。
def rebalance_signal(spot_qty, spot_price, perp_pos, target_ratio=1.0, dead_band=0.05): goal_notional = spot_qty * spot_price goal_qty = goal_notional / (perp_pos.contract_value * perp_pos.mark_price) current_ratio = perp_pos.contract_qty / goal_qty if goal_qty else 0 if current_ratio < target_ratio - dead_band: return "HEDGE_ADD", goal_qty - perp_pos.contract_qty if current_ratio > target_ratio + dead_band: return "HEDGE_REDUCE", perp_pos.contract_qty - goal_qty return "NO_ACTION", 04.2 订单执行与失败重试
订单执行不能简单地提交一个市价单就完事。我封装了一个OrderExecutor,统一处理下单、等待成交、超时撤单、重试。
import time class OrderExecutor: def __init__(self, client, max_retry=3): self.client = client self.max_retry = max_retry def execute(self, side: str, qty: float, timeout: float = 2.0): if qty <= 0: return {"status": "SKIP", "qty": 0} last_err = None for attempt in range(self.max_retry): try: order = self.client.post_limit_order(side, qty) deadline = time.time() + timeout while time.time() < deadline: status = self.client.get_order_status(order_id=order["order_id"]) if status == "FILLED": return {"status": "FILLED", "qty": qty} if status == "CANCELED": break time.sleep(0.1) self.client.cancel_order(order_id=order["order_id"]) except Exception as e: last_err = e time.sleep(0.5 * (attempt + 1)) return {"status": "FAILED", "err": last_err}这个执行器有几个易错点:一是撤单后要等服务端确认,不要立刻再次提交相同价格的单,否则可能在接口层面撞雷;二是每次下单前要重新拉一次仓位,因为行情跳动中本地缓存的可用数量可能已经变化。
4.3 基于历史 tick 的回测框架
回测我用的是自研的轻量级事件循环,比直接套现成平台更可控。核心逻辑很简单:按时间戳回放每一条历史行情,驱动引擎的on_tick方法,模拟所有仓位变化和手续费。
def run_backtest(engine, ticks, fee_rate=0.0005): equity_curve = [] for _, tick in ticks.iterrows(): engine.on_tick(tick) if engine.should_rebalance(): notional = engine.target_notional() fee = notional * fee_rate engine.apply_fee(fee) engine.rebalance() equity_curve.append(engine.portfolio_value()) return equity_curve回测数据我建议至少取包含完整牛熊周期的两年日线,外加三次极端波动小时级数据。回测的目的不是证明策略赚钱,而是验证系统在极端行情下是否会“追涨杀跌”、是否会在快速波动中反复触发无意义的调仓。如果回测结果里一只 24 小时内触发调仓超过 20 次,那明显死区设窄了。
5. 回测与实盘数据:这套系统到底能省多少钱
5.1 三种典型行情的回测对比
我在回测里重点看了三种典型行情,分别是温和下跌、V 型反转、单边上涨,外加一个极端闪崩场景。以下是我用模拟回测算出的对比数据,只作为系统行为验证,不代表真实业绩承诺。
| 场景 | 时间跨度 | 未对冲现货收益 | 对冲后组合收益 | 未对冲最大回撤 | 对冲后最大回撤 |
|---|---|---|---|---|---|
| 温和下跌 | 6 个月 | -15.6% | -1.8% | 22.4% | 2.1% |
| V 型反转 | 3 个月 | +12.3% | +2.7% | 18.9% | 2.9% |
| 单边上涨 | 4 个月 | +28.6% | +6.4% | 9.2% | 3.4% |
| 闪崩时刻 | 24 小时 | -19.1% | -1.3% | 19.1% | 1.3% |
从表中能看到,对冲的主要价值不在于放大收益,而在于把最大回撤从超过 20% 压到 3% 以内。代价是单边上涨行情里会明显损失一部分上行收益,这是 delta 中性策略的天然特性,可能很多人接受不了,但如果你管理的是需要稳定净值曲线的资金,这点代价完全值得。
5.2 小仓位实盘验证结果
回测只是第一步,真正能说明问题的是小仓位实盘。我拿 3.2 个 BTC 的总现货敞口跑了一个月,主要监控三件事:自动触发次数、资金费率净收入、以及系统整体可用率。
| 周 | 平均持仓规模 | 自动调仓次数 | 资金费率净收入(BTC) | 最高未对冲回撤 | 对冲后回撤 |
|---|---|---|---|---|---|
| 第 1 周 | 3.2 BTC | 5 | +0.0041 | 3.1% | 0.38% |
| 第 2 周 | 3.2 BTC | 4 | +0.0028 | 4.2% | 0.52% |
| 第 3 周 | 3.2 BTC | 7 | -0.0009 | 5.3% | 0.66% |
| 第 4 周 | 3.2 BTC | 3 | +0.0035 | 2.8% | 0.31% |
一个月下来,系统自动完成了 19 次调仓,没有任何一次需要人工干预。期间有一次行情剧烈波动,系统连续触发两次补空单动作,整体滑点控制在 0.09%,比手动操作时的 1.5% 低了一个数量级。这让我确信,自动对冲的核心价值不是预测涨跌,而是把风险执行从“人肉事件”变成“程序事件”,稳定性和纪律性都远胜于手动。
6. 落在工程细节上的坑与解法
6.1 WebSocket 断线后的仓位漂移
第一次实盘记录第三天就出了幺蛾子。WebSocket 因为网络波动断开重连后,增量推送漏了一段成交消息,系统本地维护的空单数量比实际少了 20 张。这 20 张的缺口虽然不大,但由于我要的不是精确到 1 张,而是偏差比例超过 5%,这个错误差点触发一个错误的平仓单。
我后来加了双重保险:一是每次重连后立即拉全量账户快照做基准值,而不是接着本地累计的下游继续;二是把“上一次成功交易后的仓位快照”写进 SQLite,进程启动时先读数据库恢复,再和交易所快照对账。这两个改动加完后,仓位漂移问题基本绝迹。
6.2 API 限流、部分成交与幂等设计
交易所 API 普遍有请求频率限制,比如查询账户一分钟最多 60 次,下单接口更严格。我的系统在极端行情下会高频重试,如果不控制,很容易触发 429 限流,甚至被临时封禁接口。
解决办法是分层限流:普通查询频率控制在每秒 1 次,下单频率每分钟不超过 10 次,同时所有重试都采用“指数退避 + 随机抖动”的方式。另外,下单必须做幂等设计。每次调仓信号生成时,我会生成一个client_order_id,服务端记录这个 id 及其目标张数;如果进程中途崩溃,重启后不会重复提交同一张单,而是先查询该 id 是否已经成交,再决定是继续等还是重新建仓。这一点对实盘安全至关重要,一旦漏单,实际敞口会完全偏离预期。
6.3 资金费率节点的调仓冲动
资金费率结算的时刻,价格往往会出现短暂的流动性波动。起初系统在费率结算前后会频繁触发调仓,明明波动幅度不大,却白白交了不少手续费。
我调整了策略:在资金费率结算前 5 分钟进入“冻结窗口”,所有调仓信号都延迟到结算完成后再评估。因为结算前后 1 分钟内盘口薄、滑点大,这时候做调仓属于高买低卖,系统本来就不该参与。这个改动把每周交易次数从 15 次降到了 7 次,长期算下来节约的手续费相当可观。
6.4 风险闸门:手动接管与全局熔断
自动化系统最怕的不是“不会交易”,而是“在错误的路线上越走越远”。所以我加了三个风险闸门。
第一是全局开关,我称之为 master switch,任何告警触发后都能一键暂停所有下单。第二是单日调仓次数上限,默认是 30 次,超过后系统直接进入观察模式,不再自动下单。第三是资金费率持续为负时特别提醒,因为空头长期倒贴资金费的话,对冲策略的持有成本会变大,这时可能需要策略层重新评估是否继续对冲。
这三个闸门至今触发过两次,一次是交易所回传订单状态异常,一次是 DJI 式行情插针导致短期波动率激增。触发后系统自动降级为只监控不下单,我排查无误后再手动恢复,整个过程没有出现明显亏损。
关于 AutoHedge 后续的扩展,我自己已经在试验的方向是把对冲目标从“固定的 100% delta 中性”改成“允许保留一定的风险预算”。比如熊市里保持 100% 对冲,让组合净值完全平滑;牛市里只对冲 50%,保留部分上行收益。这个需求和策略团队的目标直接相关,所以我在架构里把目标对冲比例设计成外部可配置项,后续只要接入另一份行情预测信号就能实现动态调节。量化系统的价值从来不是一次开发完事,而是不断在真实市场里校准边界、控制成本、优化执行,AutoHedge 目前就是按照这个思路继续迭代的。