实战案例一:基于Avellaneda-Stoikov模型的BTC永续合约做市

做市策略,说白了就是「低买高卖,赚差价」。但在永续合约市场里,这事没那么简单。你不仅要考虑价格波动,还得盯着资金费率、持仓限额、滑点这些乱七八糟的东西。

我个人习惯,做任何策略之前先找个靠谱的数学模型打底。Avellaneda-Stoikov模型,就是做市领域里最经典的那一个。今天咱们就拿它开刀,跑一个BTC永续合约的实战案例。

1. 模型核心逻辑

Avellaneda-Stoikov模型的核心思想很简单:你的报价应该随着库存风险动态调整。手里BTC多了,就压低买价、抬高卖价,逼自己出货;手里BTC少了,反过来操作。

公式长这样:

δ_b = (1/2) * γ * σ² * T + (1/γ) * ln(1 + γ/q)
δ_a = (1/2) * γ * σ² * T + (1/γ) * ln(1 + γ/q)

其中:

  • δ_b / δ_a:买一价和卖一价相对于中间价的偏移量
  • γ:风险厌恶系数(你有多怕亏钱)
  • σ:波动率(市场有多疯)
  • T:持仓时间窗口(你打算持多久)
  • q:当前库存(手里有多少BTC)

嗯,这里要注意:γ 和 σ 是模型的两个命门。我在项目中遇到过,γ设得太小,报价几乎不动,库存风险直接爆炸;设得太大,报价来回跳,成交率低得可怜。

2. 实战环境搭建

咱们用Python + Binance永续合约API来做。先装依赖:

pip install ccxt pandas numpy matplotlib

然后初始化交易所连接:

import ccxt
import pandas as pd
import numpy as np

exchange = ccxt.binance({
    'apiKey': '你的API_KEY',
    'secret': '你的SECRET',
    'options': {'defaultType': 'future'}
})
⚠️ 注意:永续合约API权限要单独开。别问我怎么知道的——我曾经用现货API去下单永续合约,报错报了一下午。

3. 数据准备与参数估计

模型需要实时波动率。我习惯用5分钟K线的对数收益率来算:

def fetch_volatility(symbol='BTC/USDT', limit=100):
    ohlcv = exchange.fetch_ohlcv(symbol, '5m', limit=limit)
    closes = [c[4] for c in ohlcv]
    returns = np.diff(np.log(closes))
    return np.std(returns) * np.sqrt(12)  # 年化波动率

你想想看,为什么用5分钟?因为永续合约的订单簿变化极快,用日线算波动率,等你反应过来,仓位早没了。

参数方面,我一般这样设初始值:

参数 初始值 说明
γ 0.1 风险厌恶系数,先保守一点
T 1/24 持仓时间窗口,按1小时算
最小价差 0.5 USDT 防止报价太密被吃手续费
最大持仓 10 BTC 风控红线,到了就停止做市

4. 核心做市逻辑实现

这是整个策略的心脏。我直接贴代码,然后一句句讲:

def calculate_quotes(mid_price, inventory, gamma, sigma, T):
    # 计算买卖偏移量
    delta = 0.5 * gamma * sigma**2 * T
    
    # 库存调整项
    inventory_adj = (1/gamma) * np.log(1 + gamma / max(inventory, 0.01))
    
    bid_price = mid_price - delta - inventory_adj
    ask_price = mid_price + delta + inventory_adj
    
    return bid_price, ask_price

这里有个坑:inventory 不能为0,否则 log(1 + γ/0) 直接报错。我加了个 max(inventory, 0.01) 来兜底。曾经因为这个bug,策略在空仓时直接崩了,回测数据全废。

实际下单时,还要考虑手续费和滑点:

def place_orders(symbol, bid_price, ask_price, size):
    # 扣掉手续费后的实际成交价
    fee_rate = 0.0004  # Binance永续合约maker费率
    bid_net = bid_price * (1 - fee_rate)
    ask_net = ask_price * (1 + fee_rate)
    
    # 检查价差是否覆盖成本
    if ask_net - bid_net < 0.5:
        print('价差太小,跳过本轮报价')
        return
    
    # 下单逻辑...
    exchange.create_limit_buy_order(symbol, size, bid_price)
    exchange.create_limit_sell_order(symbol, size, ask_price)
💡 小技巧:永续合约的maker费率通常比taker低很多。我建议只挂限价单吃maker返佣,别去市价单吃手续费,否则利润全给交易所了。

5. 回测框架搭建

回测我用的是逐笔成交数据,不是K线。为什么?因为做市策略对成交顺序极其敏感,K线会抹掉太多细节。

def backtest(trades, gamma=0.1, sigma=0.5, T=1/24):
    inventory = 0
    cash = 100000  # 初始资金10万USDT
    pnl_log = []
    
    for trade in trades:
        mid_price = (trade['bid'] + trade['ask']) / 2
        bid_q, ask_q = calculate_quotes(mid_price, inventory, gamma, sigma, T)
        
        # 模拟成交
        if trade['price'] <= bid_q:
            inventory += trade['size']
            cash -= trade['price'] * trade['size']
        elif trade['price'] >= ask_q:
            inventory -= trade['size']
            cash += trade['price'] * trade['size']
        
        # 每日结算盈亏
        pnl = cash + inventory * mid_price
        pnl_log.append(pnl)
    
    return pnl_log

回测结果出来后,我一般看三个指标:

  • 夏普比率:大于1才算合格
  • 最大回撤:不能超过5%
  • 成交率:挂单成交比例,低于30%说明报价太激进

6. 核心知识体系

下面这张图,是我做这个案例时梳理的完整流程。你照着走一遍,基本不会跑偏:

Avellaneda-Stoikov 做市策略核心流程 ① 数据输入 ② 参数估计 ③ 报价计算 ④ 订单管理 ⑤ 风控检查 关键参数说明 • σ:波动率(5分钟K线) • γ:风险厌恶系数(0.05-0.2) • T:持仓时间窗口(1/24天) • q:当前库存(BTC数量) • δ:报价偏移量 ⚠️ 常见陷阱 1. 库存为0时公式报错 2. 波动率突变导致报价失真 3. 手续费吃掉微薄利润 4. 资金费率影响持仓成本 5. 滑点导致实际成交价偏离

7. 避坑指南

做这个案例,我踩过的坑比赚到的钱还多。挑几个重点说说:

  • 资金费率:永续合约每8小时结算一次资金费率。如果费率是正的,多头要付钱给空头。做市策略如果持仓过夜,可能被费率吃掉全部利润。我建议在费率结算前平掉大部分仓位。
  • 滑点问题:回测时觉得利润不错,一上实盘就亏。为什么?因为回测用的是成交价,实盘挂单可能根本成交不了。我后来在回测里加了成交概率模型,才勉强接近真实情况。
  • 波动率突变:BTC经常在几分钟内波动5%以上。模型用的历史波动率根本跟不上。我后来加了实时波动率监控,一旦检测到异常,直接暂停做市。

核心结论:Avellaneda-Stoikov模型是个好起点,但别指望它直接赚钱。你需要根据市场情况动态调参,加上风控和资金管理,才能跑出稳定的收益曲线。

好了,这一章的内容就到这。代码你拿去跑跑看,有问题随时调。记住,做市不是印钞机,是用数学和纪律换钱的苦活。

交易系统化学习资料 微信Strategy888888