做量化交易这些年,我最大的感触是:策略模型再漂亮,落到执行层面全是脏活累活。尤其是持仓一多,各个品种的风险敞口像章鱼触手一样乱伸,手工盯盘对冲不仅累,还容易出错。所以我花了挺长时间折腾了一个自动对冲系统,取名AutoHedge,专门解决多资产持仓下的自动套期保值问题。
这个系统说简单也简单,核心就三件事:实时盯着你的现货持仓和期货/合约仓位,算出当前净敞口,然后按你设定的阈值自动开仓、平仓、调仓,把整体风险拉回中性区间。说复杂也复杂,因为牵扯到数据接入、保证金计算、资金费率处理、极端行情保护、交易所接口限频排查等等一堆细节。这篇文章我就把这个项目的完整思路和实操过程拆开来讲,从设计目标到核心算法代码,再到我踩过的坑,一次性说清楚。
这套东西适合谁?如果你有现货+合约多账户持仓,或者你在做市、挖矿、LP流动性提供这类有基础敞口的业务,又或者你纯粹想研究程序化对冲策略,AutoHedge 的思路都能直接拿来改。不需要很深的数学功底,但至少要懂点 Python,知道什么是多仓空仓,能看懂K线。
1. 为什么我会做 AutoHedge:手工对冲的痛点和自动化思路
1.1 手工对冲到底有多折腾
先说说我最初的痛点。2022年我开始做加密资产的量化交易,策略是现货买入主流币种,然后在永续合约上做空对冲,赚取资金费率和小幅价差。这套逻辑本身没问题,但跑起来之后,我发现操作量巨大:
- 持仓币种有十几个,每个币种的现货市值和合约仓位都在变化,敞口计算全靠Excel表格手动更新,一天下来光填表就花掉大半个小时。
- 行情波动的时候,对冲比率很快就失真了。比如 BTC 涨了,现货市值变大,原来开的空单就变少了,风险敞口重新暴露,你得赶紧补空单。这种操作在剧烈行情里一天要做十几次,人的反应速度和情绪控制根本跟不上。
- 最难受的是资金费率的快照时间点。交易所每小时收一次资金费率,如果费率变成极端的正值,你手里拿着空单反而要倒贴钱。手工操作时,你很难在费率变化发生的瞬间做出最优决策。
我的第一版对冲策略是拿 Excel 加手动下单完成的,结果三个月下来,人力成本不算,光是因为反应慢导致的滑点和敞口暴露亏损,就让我下决心把整个过程自动化。
这里也说一个常被新手忽略的点:所谓的“对冲”,不是简单地在合约市场开一个方向相反的单子就叫对冲,你还要考虑对冲比率、调仓频率、资金费率、强平价距离这四样东西。手动操作只能做到“大方向相反”,根本做不到“动态平衡”,而动态平衡才是降低风险敞口的关键。
1.2 AutoHedge 的设计目标
我把需求整理了一遍,最终确定了 AutoHedge 的几个核心设计目标,后面所有代码和模块都是围绕它们展开的:
- 全自动风险敞口计算:系统要能同时读取多个交易所的现货账户和合约账户数据,实时算出每个币种的净敞口市值,不需要人工干预。
- 可配置的对冲策略:不同币种、不同市场环境,对冲比率需求不一样。有些币种需要完全对冲(Delta中性),有些只需要部分对冲(比如对冲80%),系统要支持灵活配置。
- 阈值触发的动态调仓:不是时时刻刻都在下单选调仓,而是当敞口偏离目标值超过设定阈值时才触发调仓,减少手续费损耗和滑点成本。
- 资金费率感知:在资金费率极端的时候,自动降低对冲仓位或调整方向,让系统不吃费率的亏。
- 风控保护:所有策略都得有硬性风控线,比如单笔下单量上限、最大未实现亏损限制、极端行情自动暂停等,不能让人工智障变成人工灾难。
这几个目标定完之后,整个项目的技术选型就顺理成章了。我最终采用了 Python 作为主语言,原因是生态成熟,ccxt、pandas、numpy 这些库都能直接用;数据存储用 PostgreSQL 方便回测分析;消息通知接入企业微信和 Telegram,方便实时监控告警。系统跑在一台 4核8G 的云服务器上,到现在都还很稳定。
2. 系统架构与数据模块设计
2.1 整体架构分层
AutoHedge 从逻辑上分成四层:数据层、计算层、执行层、监控层。每一层职责单一,层与层之间通过消息队列解耦,这样就算某个模块挂了,也不会影响其他模块正常运行。
- 数据层:负责对接交易所API,采集行情数据、持仓数据、资金费率数据、订单簿深度等。这一层是系统的“眼睛”,要求实时性和稳定性兼顾。
- 计算层:抽取数据层传来的数据,计算风险敞口、对冲比率、保证金占用、预估强平价等指标。这一层是系统的“大脑”。
- 执行层:接收计算层发出的调仓指令,负责下单、撤单、改单,并处理交易接口返回的各种异常状态。这一层是系统的“手脚”。
- 监控层:采集系统运行日志、下单记录、异常告警,统一推送到运维人员手上。
层与层之间我用 Redis 做消息缓冲,计算层算出的调仓信号会先推送到 Redis 队列,执行层异步消费执行。好处很明显:就算交易所API偶发抽风,信号也不会丢,重新连接后照样执行,不会因为进程重启就漏掉关键操作。
2.2 行情接入的选型经验
行情接入我花了很大功夫选型。直接用交易所原生WebSocket是最实时、最稳定的方式,但对开发要求高,而且每个交易所的协议格式都不一样。用第三方聚合数据服务(比如Ticker服务商)方便,但往往有1-2秒的延迟,而且收费不便宜。
最终我的做法是折中:主行情通道用交易所原生WebSocket,备用通道用REST轮询兜底,频率控制在2秒一次轮询。主通道断了自动切换备用通道,切换过程中系统继续跑,不中断计算。这个设计在实际情况里救了我好几次,比如某头部交易所的WebSocket偶尔会有长达几十秒的推送空窗,如果没有底,对冲信号就会因为数据陈旧而失真。
数据落地方面,我做了两级存储:热数据放 Redis,TTL 设8小时,主要用于实时计算;冷数据定期归档到 PostgreSQL,主要用于事后分析和回测验证。这里我强烈建议做量化的人养成数据落盘的习惯,哪怕只是存个K线快照,等到出问题复盘的时候你就知道这有多重要了。
2.3 持仓管理与交易所兼容层
交易所账户数据的接入算是整个项目里最没技术含量但又最繁琐的部分。每个交易所的持仓数据结构不一样,有的返回字典,有的返回列表,有的仓位字段叫position,有的叫positions,还有的币本位和U本位合约字段字段完全不同。
我基于 ccxt 库封装了一套统一持仓接口,把不同交易所的持仓数据标准化成统一格式:
class Position: def __init__(self, symbol, side, amount, entry_price, mark_price, leverage): self.symbol = symbol # 交易对,如 BTC/USDT self.side = side # long 或 short self.amount = amount # 持仓数量,单位为币 self.entry_price = entry_price # 开仓均价 self.mark_price = mark_price # 标记价格 self.leverage = leverage # 杠杆倍数很多人在做多账户的时候喜欢每个交易所单独写一套逻辑,我非常不建议这样做。统一抽象层虽然前期要多写一点代码,但后面新增交易所、优化策略的时候,收益是成倍的。我后面接第三个交易所时,核心代码完全没动,只花半小时写了接口适配。
3. 对冲策略的数学原理与参数设定
3.1 从 Delta 中性到实操简化
AutoHedge 的理论根基是期权定价里的 Delta 中性策略。简单说,Delta 衡量的是资产价格每变动1块钱,你的持仓组合会跟着变动多少钱。如果整个组合的 Delta 为 0,那资产价格不管怎么涨跌,你的总资产价值都不变,这就是中性对冲。
但咱们做现货加合约对冲的时候,可以不搞那么复杂的数学模型,直接用简化版公式:
组合净敞口 = 现货市值 - 合约空单市值当这个净敞口接近0时,现货涨跌和合约涨跌基本互相抵消,组合整体波动变得很小。举个例子,你持有价值1万美元的 BTC 现货,同时在合约市场开了1万美元等值的 BTC 空单,那无论 BTC 价格怎么波动,你的总资产基本稳定,这就是典型的 Delta 中性。
实际操作中,完全的中性不代表最优,因为资金费率、手续费、滑点都会侵蚀收益。所以我的策略里引入了一个“目标敞口比例”参数target_exposure_ratio,默认设为 0(完全中性),但你可以根据市场判断调整成 0.1、0.2 这样的值,代表保留10%的净多头敞口,让自己在牛市里能多赚一点,熊市里则调成负值。
我还做了一个 Beta 修正。如果你同时持有多个币种,不同币种的波动率不一样,BTC 涨 1% 和某个小币种涨 1% 带来的风险完全不一样。为了统一衡量,我以 BTC 为基准,给每个币种算一个 Beta 系数,公式是:
对冲数量 = (现货市值 × Beta) / 合约面值小币种的 Beta 通常大于1,意味着它比 BTC 波动更大,所以对冲时你需要的空单市值要比现货市值略大一些。Beta 系数的计算很简单,用币种对 BTC 的近30天日收益率回归就行,我在系统里每个月自动重算一次。
3.2 动态调仓阈值怎么定
调仓频率是个双刃剑。调得太频繁,手续费和滑点吃掉利润;调得太慢,风险敞口长期暴露,遇到单边行情净值回撤巨大。所以阈值设定要综合考虑三个因素:交易成本、波动率、资金费率水平。
我的做法是设置一个“死区”机制,也就是:只有当净敞口偏离目标值超过某个百分比时,系统才执行调仓。这个百分比我经过回测,取的是 3%-5% 区间,具体数值每个币种单独配置。大币种滑点小、流动性好,阈值可以设小一点(比如3%),小币种阈值设大一点(比如8%),避免频繁调仓产生大量手续费。
调仓的目标不是一次性把仓位全调到位,而是采用“部分调仓”策略。比如当前偏离达到5%的阈值,我并不会一下子把仓位调整到完全中性,而是只调整偏离部分的 50%,给后续价格波动留出缓冲空间,避免刚调完仓价格反弹又得反向操作。这个“半程调仓”的经验是实盘里摸索出来的,单纯回测不容易体现它的价值。
3.3 资金费率感知策略
永续合约的资金费率是 AutoHedge 必须处理的一个特殊变量。资金费率每隔一段时间(一般是8小时,也有1小时和4小时的)结算一次,多空双方互相支付资金费用。费率为正时,多头付钱给空头;费率为负时,空头付钱给多头。
如果你的对冲策略是持有大量空单,那资金费率为负时你就要持续付钱,时间一长,这可不是小数目。AutoHedge 的处理方式是:
- 当资金费率绝对值超过设定阈值(比如0.05%)时,系统会增加或减少对冲仓位,利用费率变化赚取额外的现金流。
- 具体逻辑是:费率为显著正值,且你持有现货多头,适合持有更多空单来收取资金费;费率为显著负值时,则减少空单数量,甚至反向操作。
实盘统计下来,这个策略一年大概能额外增厚年化 2%-4% 的收益。虽然不是暴利,但对冲策略本来也不是追求暴利,积少成多非常香。
下面是 AutoHedge 的参数总览表,我刚部署时的初始值可以参考:
| 参数名 | 默认值 | 说明 |
|---|---|---|
| target_exposure_ratio | 0 | 目标敞口比例,0为完全中性 |
| rebalance_deadzone | 0.05 | 偏离阈值,超过5%才调仓 |
| partial_rebalance_ratio | 0.5 | 每次只调整偏离部分的50% |
| beta_window_days | 30 | Beta计算回看窗口,单位天 |
| funding_rate_threshold | 0.0005 | 资金费率干预阈值,0.05% |
| max_order_size_usd | 5000 | 单笔最大下单市值,单位U |
| max_unrealized_loss | 0.08 | 最大未实现亏损比例,8% |
这些参数不是拍脑袋定的,我后面用一年的历史数据做过参数敏感性回测,像 rebalance_deadzone 从 2% 到 10% 都测过,最后发现5%附近是夏普比率和最大回撤的平衡点。新手可以直接用这些默认值起步,等积累了自己的实盘数据再做调整。
4. 核心代码实现与实盘部署实录
4.1 调度框架:确保系统永不沉睡
整个系统的运行中枢是一个基于事件循环的调度器,我用的是 asyncio 加无限循环结构,每一轮循环负责拉取数据、计算敞口、判断是否调仓、下发指令,然后休眠固定时间进入下一轮。之所以用 asyncio 而不是直接多线程,是因为 IO 密集型任务用协程效率更高,而且能避免多线程共享数据的锁问题,代码写起来干净很多。
调度频率我设置的是 5 秒一轮。这个频率是根据交易所 WebSocket 推送频率和成交量综合权衡的结果,跑高频不现实,也没必要,5秒足够捕捉到较大的价格异动,又不会因为频繁调仓造成无谓的手续费。核心调度代码长这样:
import asyncio async def main_loop(): while True: try: # 1. 拉取所有账户持仓和行情 positions = await data_layer.fetch_all_positions() # 2. 计算每个币种的风险敞口 exposures = calc_layer.calculate_exposures(positions) # 3. 生成调仓信号 signals = calc_layer.generate_rebalance_signals(exposures) # 4. 有信号就交给执行层处理 if signals and risk_manager.is_safe(): await executor.execute_signals(signals) # 5. 状态更新和告警 monitor.push_status(exposures) except Exception as e: monitor.alert(f"Main loop error: {e}") await asyncio.sleep(2) await asyncio.sleep(5)这版本看着简单,但实际上我迭代了很多次。最初每个模块都直接写在主循环里,结果任何一个交易所API抽风,整个循环就卡死,系统直接罢工。后来全部改成模块化 + 超时管理,每个外部调用都有超时中断,单模块异常不影响全局。
4.2 对冲信号计算:从数据到指令
calc_layer 是系统最业务核心的模块,负责把账户数据转换成可执行的调仓指令。它的核心逻辑分三步:计算当前敞口、和目标敞口做差、判断是否触发调仓阈值。
def calculate_exposures(positions): exposures = {} for symbol, pos in positions.items(): spot_value = pos.spot_amount * pos.mark_price # 现货市值 future_value = pos.future_amount * pos.mark_price * pos.future_direction # future_direction: 1 表示多,-1 表示空 net_exposure = spot_value + future_value # 净敞口 beta = get_beta(symbol) # 获取币种Beta adjusted_exposure = net_exposure * beta # Beta修正后的敞口 exposures[symbol] = { "spot": spot_value, "future": future_value, "net": net_exposure, "adjusted": adjusted_exposure, "target": spot_value * config.target_ratio, } return exposures def generate_rebalance_signals(exposures): signals = [] for symbol, exp in exposures.items(): deviation = (exp["adjusted"] - exp["target"]) / (exp["spot"] or 1) if abs(deviation) > config.rebalance_deadzone: size = exp["spot"] * deviation * config.partial_rebalance_ratio signal = { "symbol": symbol, "side": "sell_future" if deviation > 0 else "buy_future", "size": size / exp["mark_price"], # 换算成币数量 "reason": "deviation too large", } signals.append(signal) return signals这里有一个细节很重要:计算对冲数量时的方向问题。如果你的净敞口是正的,说明你现货多头多于合约空单,风险在于价格下跌。这时候要开空单或者加空单来对冲;反过来,如果你的净敞口是负的,说明你空单开多了,价格一涨就会亏,这时候需要买入平掉部分空单。代码里用side字段表达这两个方向,执行层根据这个字段来决定是开空还是平空。
4.3 执行层:抠细节才能少出幺蛾子
executor 是整个系统里和交易所API打交道的部分,也是最容易出现各种“人工智障”行为的地方。我在这个模块花了非常多的时间打磨细节,说几个影响最大的经验。
第一,下单前必须重新拉一次实时价格,用最新价格计算下单数量,而不是用计算层传回的旧价格。因为计算层到执行层之间有网络延迟和消息队列缓冲,几秒钟的时间差在波动大的时候会造成下单数量偏差。这一点看起来微不足道,实盘里积累下来差距很明显。
第二,所有下单请求都必须带上超时和重试机制,并且要处理好幂等性。交易所接口偶尔超时是常态,但超时之后你不知道订单到底成交没有。如果盲目重发,就会造成重复下单,仓位瞬间翻倍。我的处理方式是为每个订单生成唯一ID,发送前先把订单写入本地数据库,状态标记为“待确认”,收到交易所确认后更新为“已成交”,如果超时就用订单ID去交易所查询真实状态,查询到了再更新。这个“本地幂等表”的设计让我躲过了好多次灾难。
第三,下单时直接设置好止损止盈保护单,不要依赖策略层再去处理。策略进程挂了,至少止损单还在交易所服务端兜底。止损价格我的习惯是放在距离开仓价5%-8%的位置,太近了容易被正常波动扫掉,太远了起不到保护作用。
执行层的一个核心函数长这样:
async def place_order_with_retry(symbol, side, amount, price=None): order_id = generate_local_order_id() save_order_state(order_id, "pending", symbol, side, amount) for attempt in range(3): try: resp = await exchange.create_order(symbol, "limit", side, amount, price) if resp and resp.get("id"): update_order_state(order_id, "submitted", resp["id"]) return resp["id"] except asyncio.TimeoutError: # 查询订单是否存在,避免重复下单 order_status = await exchange.fetch_order(order_id) if order_status and order_status.get("status") == "closed": update_order_state(order_id, "filled") return order_id except Exception as e: log_error(e) await asyncio.sleep(1) alert(f"Order failed after 3 attempts: {symbol} {side} {amount}") return None4.4 风控模块:最后一道安全绳
风控模块我来单独讲讲,因为这是整个系统里我最有心得的部分。很多做量化的人喜欢把风控写在策略里,策略判断不准开仓就不开仓,但在我看来这是远远不够的,风控必须是独立于策略之外的强制层。
AutoHedge 的风控模块主要做四件事:
- 杠杆和保证金监控:实时计算每个账户的维持保证金率和预估强平价,当维持保证金率低于交易所要求的150%时,系统自动发出告警并暂停开新仓;低于120%时,自动减仓降低杠杆。
- 单笔和单日亏损上限:如果单日累计未实现亏损超过了设定的最大回撤比例(默认8%),系统自动进入“避险模式”,把所有敞口全部对冲掉,等市场稳定后再由人工恢复运行。
- 下单频率限制:所有调仓指令在到达交易所之前都会经过一个令牌桶限流器。当5分钟内下单次数超过预设上限(比如20次)时,后续指令会被拦截,防止策略失控时疯狂刷单烧手续费。
- 运行状态自检:系统每隔一段时间会检查自己是否还在从交易所正常接收数据,如果超过60秒没有收到任何行情推送,说明数据通道可能断了,系统会主动暂停所有新调仓,防止在“盲”的状态下乱动仓位。
class RiskManager: def __init__(self): self.daily_loss_limit = 0.08 self.max_orders_per_5min = 20 self.order_timestamps = [] def is_safe(self, current_pnl, margin_ratio): # 检查未实现亏损是否超限 if current_pnl < -self.daily_loss_limit * account.total_balance: return False # 检查保证金率是否安全 if margin_ratio < 1.5: return False # 检查下单频率 self.order_timestamps = [t for t in self.order_timestamps if t > time.time() - 300] if len(self.order_timestamps) >= self.max_orders_per_5min: return False return True这个模块的价值我用一句话总结:策略负责赚钱,风控负责保证你还有命花这个钱。策略错了顶多亏一点,风控失效是会爆仓归零的。
5. 实盘部署与调试:那些坑你躲不掉的
5.1 服务器与运维的选址教训
系统的运行环境我一开始用的是自己的笔记本,后来发现自己电脑一关机系统就停,而且家里的公网IP也不稳定,果断迁移到了云服务器。
选云服务器这件事上,我踩过最大的坑是盲目选了离自己最近的区域,结果访问交易所API延迟高达200多毫秒,对系统响应速度影响很大。后来换到了交易所服务器所在的同区域(这个根据地缘和实测数据选,不要盲目信广告),API延迟降到了20毫秒以内,性能提升非常明显。
运维方面,我强烈建议给系统跑一个进程守护工具。我用的是 supervisord,配置了自动重启,加上一个独立看门狗脚本,每5分钟检查一次主进程是否响应,不响应就强制重启,另外磁盘满了自动清理日志。这套组合简单有效,挂过那么多次,靠它面板上基本上没有出现过超过10分钟的宕机。
5.2 常见问题排查表
直接整理一个我在运行过程中遇到过的问题和解决方式,大家照着排查能省很多事:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 系统长期不调仓 | 偏离阈值设太大或数据更新中断 | 检查行情推送是否正常,临时调小threshold观察 |
| 下单后持仓方向反了 | 计算净敞口时方向符号理解反 | 核对exchange返回的direction字段含义 |
| 重复下单导致仓位超过预期 | 超时重试时没有幂等控制 | 检查本地订单表,同步交易所真实持仓校准 |
| 资金费率为负时仓位不变 | funding_rate_threshold设太高 | 降低阈值到0.01%附近并检查计算逻辑 |
| 某币种敞口永远偏离目标 | Beta值异常,常见于新上币 | 手动剔除该币种或重置Beta为1 |
| API突然大量报错 | 触发了交易所限频 | 降低轮询频率,改用WebSocket,增加本地缓存 |
| 系统在凌晨突然爆出大额亏损 | 低流动性时执行滑点巨大 | 限制非交易时段的开仓行为,或设定最大滑点保护 |
这里特别提醒一下,任何时候改动代码之前,先拍一张当前所有持仓的快照,包括交易所端和本地数据库的。万一改挂了,还能手动核对仓位,不至于两眼一抹黑。
5.3 回测与实盘的差距
AutoHedge 正式上实盘之前,我用历史数据做了整整两个月的回测,回测结果非常漂亮,年化收益稳稳的,最大回撤也没超过3%。但一上实盘,前两周就连续出了好几个问题,回测和实盘的差距让我头大。
主要的差距有三个来源:
- 回测里的手续费和滑点设置太理想化。我给的回测手续费是交易所标准费率,实际跑起来因为下单量小、部分订单吃不到盘口,综合滑点成本比回测高了将近一倍。
- 回测没有模拟资金费率结算的动态变化。我用的是历史平均资金费率,但实盘里资金费率是随时变化的,而且极端时刻的费率变化对收益影响极大。
- 回测没有网络延迟和API故障。实盘里频繁出现的信息延迟、订单状态不一致、限频报错,在回测里完全不存在,而这些因素对策略执行的影响远比策略本身的参数更显著。
我的经验是:回测收益至少要打六折才是合理的实盘预期。另外,在上实盘前,一定要准备足够小的初始资金跑一两周“影子模式”,只记录信号不真下单,确认信号质量和执行逻辑没问题后再切换到真金白银模式。我见过太多人一上来就大资金跑,结果策略没跑明白,钱先亏完了。
6. 项目迭代方向与我的个人体会
AutoHedge 目前跑了大半年,中间迭代了很多版本。有人问我会不会考虑做成一个通用的开源产品,我的想法是,量化工具的个性化和场景化太强了,与其指望现成的,不如根据自己的需求打磨一套。
目前我在研究和试验的迭代方向有几个:一是接入更多衍生品工具,比如期权做更精细的希腊字母对冲,而不仅仅是简单的现货合约对冲;二是把机器学习波动率预测加进去,让系统能预测未来一段时间波动率的变化并提前调整阈值和敞口目标;三是完善多账户管理能力,现在一个实例只管理一套账户组合,下一步想支持同策略多账户自动部署、自动资金分配。
最后,如果让我总结做 AutoHedge 这个项目最核心的体会,我会说两句话。
第一,量化系统设计的核心不是追求收益的最大化,而是追求风险的可控和过程的自动化。一个能安安稳稳跑一年不惹大事的系统,远比一个短期收益爆发但动不动就要人工救火的系统有价值得多。
第二,自动化不是目的,解放注意力才是目的。把那些重复、机械、容易受情绪影响的执行工作交给代码,把精力放在策略迭代和风险控制这样真正有创造性的工作上,这才是 AutoHedge 给我最大的回报。
如果看完这篇分享,你也准备做一个类似的对冲系统,我的建议是从最小可用版本开始,先跑通一个币种、一个交易所的完整闭环,再去加复杂功能和更多市场。系统能力永远是在不断的踩坑和迭代里长出来的,不是一开始设计出来的。