实战案例一:基于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'}
})
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)
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. 核心知识体系
下面这张图,是我做这个案例时梳理的完整流程。你照着走一遍,基本不会跑偏:
7. 避坑指南
做这个案例,我踩过的坑比赚到的钱还多。挑几个重点说说:
- 资金费率:永续合约每8小时结算一次资金费率。如果费率是正的,多头要付钱给空头。做市策略如果持仓过夜,可能被费率吃掉全部利润。我建议在费率结算前平掉大部分仓位。
- 滑点问题:回测时觉得利润不错,一上实盘就亏。为什么?因为回测用的是成交价,实盘挂单可能根本成交不了。我后来在回测里加了成交概率模型,才勉强接近真实情况。
- 波动率突变:BTC经常在几分钟内波动5%以上。模型用的历史波动率根本跟不上。我后来加了实时波动率监控,一旦检测到异常,直接暂停做市。
核心结论:Avellaneda-Stoikov模型是个好起点,但别指望它直接赚钱。你需要根据市场情况动态调参,加上风控和资金管理,才能跑出稳定的收益曲线。
好了,这一章的内容就到这。代码你拿去跑跑看,有问题随时调。记住,做市不是印钞机,是用数学和纪律换钱的苦活。