返回 Expert 笔记
Expert Day 98

基差交易(Basis Trade & Roll)

期现基差、contango/backwardation、Roll 策略、basis 与 funding 关系

2026-08-07
Phase 2 - 统计套利与Alpha Research (Day 89-102)
量化策略套利Basis期现ContangoRoll

日期: 2026-08-07 方向: 量化 / 统计套利 / Alpha 阶段: Phase 2 - 统计套利与Alpha Research (Day 89-102) 标签: #量化策略 #套利 #Basis #期现 #Contango #Roll


今日目标

类型内容
学习期现基差、contango/backwardation、Roll 策略、basis 与 funding 关系
实操用 Deribit/CME 季度合约数据回测 basis trade
产出basis.py — Basis 监控 + Roll 策略 + 回测

一、理论与模型

1.1 期现基差定义

Basis(基差): $$ B_t = F_t - S_t $$

或对数化: $$ b_t = \ln F_t - \ln S_t $$

  • $F_t$:期货价格
  • $S_t$:现货价格

Annualized basis: $$ \text{Annualized} = \frac{F_t - S_t}{S_t} \times \frac{365}{T - t} $$

T 是到期日。

1.2 Cost of Carry 模型

无套利下: $$ F_t = S_t \cdot e^{(r - q)(T - t)} $$

  • $r$:无风险利率
  • $q$:持有现货的 yield(staking、dividend)

加密的特殊:q 可包括 staking yield(ETH ~3%)、lending yield。

Contango:F > S(升水),通常牛市 Backwardation:F < S(贴水),通常熊市或现货稀缺

1.3 Cash-and-carry with futures

类似 perpetual carry:

  • 现货 long S
  • 期货 short F
  • 持有到期:锁定 (F - S) 收益

优势 vs perpetual

  • 锁定到期日,无需每 8h 监控 funding
  • 无强平风险(如果用 fully-funded 期货)
  • 可清晰计算 yield

劣势

  • 资金占用更长(直到 settlement)
  • 季度合约流动性较差
  • Roll 风险(如果想持续 carry)

1.4 Roll Strategy

到期前要 roll 到下一个合约:

Now (June): hold Jul futures
Approaching expiry: close Jul, open Sep

Roll yield: $$ \text{Roll Yield} = \frac{F_{near} - F_{far}}{F_{far}} $$

  • Contango 市场:roll yield 通常负(亏损)
  • Backwardation 市场:roll yield 正(盈利)

1.5 Basis 与 Perpetual Funding 关系

理论: $$ \text{Funding} \approx \frac{F_{perp} - S}{S} \cdot \text{每日次数} $$

季度 basis 与 perpetual funding 一致性:

  • 高 funding → contango basis 应大
  • Backwardation → 负 funding

差异机会:

  • Perpetual funding 已正常化但季度 basis 偏离 → 套利
  • 反向同理

1.6 Implied Yield(隐含收益)

从 basis 反推: $$ y = \frac{1}{T - t} \ln \frac{F_t}{S_t} $$

加密典型 implied yield:

  • 牛市:8-20%
  • 熊市:-5% 到 5%
  • 极端时(FTX 危机后):-15%

1.7 Volatility 期限结构

Basis 隐含波动率期限结构:

  • Spot ATM IV
  • 1m futures IV
  • 3m, 6m IV

期限结构反映市场对未来 vol 的预期。Basis trader 监控 IV term structure。


二、直觉与陷阱

陷阱 1:Roll cost 累积

季度合约每年 roll 4 次。每次 spread 0.3% → 年化 1.2% roll cost。 高频 roll(月度)成本更高。

陷阱 2:到期日大波动

合约到期前 1-2 周流动性集中转移到下一合约:

  • 近月合约 spread 扩大
  • Roll 操作时滑点高
  • 解法:提前 2-3 周 roll,避开高波动期

陷阱 3:CME vs Deribit basis 不同

维度CMEDeribit
合约类型现金交割现金交割 (BTC、ETH)
用户机构(合规要求)全球零售+机构
Basis通常更高较低(更接近 spot)
Capacity中等
监管CFTC离岸

CME basis 受 ETF flow 强烈影响(2024 年 ETF 通过后 CME basis 显著上升)。

陷阱 4:到期日 manipulation

到期前最后 1 小时现货价被操纵以影响结算。 解法

  1. 用 settlement window 较长的合约(如 Deribit 30 分钟 TWAP)
  2. 提前 1 天 close,用 perp 替代

陷阱 5:Basis 不对称收敛

理论 basis 到期收敛到 0,但路径可能:

  • 平稳 linear decay(理想)
  • Mean-reverting(可能扩大后再收敛)
  • Discontinuous(重大事件)

不能假设 linear path。

陷阱 6:现货端的隐藏成本

做 spot leg 需要:

  • 现货保管(cold storage 成本)
  • 未流通损失(错过 staking、airdrop)
  • KYC/AML 限制

机构成本可能 0.5-1% 年化。


三、代码实现

3.1 Basis 监控与回测

# basis.py
"""
Basis Trade Engine
- Spot/futures basis monitoring
- Roll strategy backtester
- Term structure analysis
"""

import numpy as np
import pandas as pd
import requests
from datetime import datetime, timedelta
from typing import Dict, List, Tuple


# ----------------------------- Deribit API -----------------------------
class DeribitClient:
    BASE = "https://www.deribit.com/api/v2/public"

    @staticmethod
    def get_instruments(currency: str = 'BTC', kind: str = 'future') -> List[Dict]:
        r = requests.get(f"{DeribitClient.BASE}/get_instruments",
                          params={'currency': currency, 'kind': kind, 'expired': 'false'})
        return r.json()['result']

    @staticmethod
    def get_ticker(instrument: str) -> Dict:
        r = requests.get(f"{DeribitClient.BASE}/ticker",
                          params={'instrument_name': instrument})
        return r.json()['result']

    @staticmethod
    def get_index_price(currency: str = 'BTC') -> float:
        r = requests.get(f"{DeribitClient.BASE}/get_index_price",
                          params={'index_name': f'{currency.lower()}_usd'})
        return r.json()['result']['index_price']


# ----------------------------- 基差分析 -----------------------------
def analyze_term_structure(currency: str = 'BTC') -> pd.DataFrame:
    instruments = DeribitClient.get_instruments(currency, 'future')
    spot = DeribitClient.get_index_price(currency)

    rows = []
    for inst in instruments:
        if inst['settlement_period'] in ('day', 'week', 'month'):
            try:
                ticker = DeribitClient.get_ticker(inst['instrument_name'])
                expiry_ts = inst['expiration_timestamp'] / 1000
                days_to_expiry = (expiry_ts - datetime.now().timestamp()) / 86400
                if days_to_expiry <= 0:
                    continue

                mark_price = ticker['mark_price']
                basis = (mark_price - spot) / spot
                annualized = basis * 365 / days_to_expiry

                rows.append({
                    'instrument': inst['instrument_name'],
                    'expiry_days': days_to_expiry,
                    'mark_price': mark_price,
                    'spot': spot,
                    'basis_pct': basis,
                    'annualized_basis': annualized,
                    'open_interest': ticker.get('open_interest', 0),
                })
            except Exception as e:
                print(f"  Err {inst['instrument_name']}: {e}")

    return pd.DataFrame(rows).sort_values('expiry_days')


# ----------------------------- Cash and Carry P&L -----------------------------
def cash_and_carry_pnl(spot_qty: float, futures_short_qty: float,
                        spot_entry: float, futures_entry: float,
                        spot_exit: float, futures_exit: float,
                        days_held: int = None) -> Dict:
    spot_pnl = (spot_exit - spot_entry) * spot_qty
    futures_pnl = (futures_entry - futures_exit) * futures_short_qty
    total = spot_pnl + futures_pnl
    capital = spot_qty * spot_entry
    ret_pct = total / capital
    apr = ret_pct * 365 / days_held if days_held else None
    return {
        'spot_pnl': spot_pnl,
        'futures_pnl': futures_pnl,
        'total_pnl': total,
        'return_pct': ret_pct,
        'annualized': apr,
    }


# ----------------------------- Roll Strategy 模拟 -----------------------------
def simulate_roll_strategy(price_path: pd.Series,
                            basis_path: pd.DataFrame,
                            roll_days_before_expiry: int = 14,
                            fee_bps: float = 5) -> pd.DataFrame:
    """
    模拟 perpetual carry 策略(用季度合约 + roll)
    basis_path: columns = ['near_basis', 'far_basis']
    """
    dates = price_path.index
    pnl_series = pd.Series(0.0, index=dates)
    capital = 1.0

    # 简化:每 90 天 roll 一次
    roll_dates = pd.date_range(dates[0], dates[-1], freq='90D')

    for i in range(len(roll_dates) - 1):
        period_start = roll_dates[i]
        period_end = roll_dates[i + 1]
        if period_end > dates[-1]:
            period_end = dates[-1]

        if period_start not in basis_path.index:
            continue

        # 入场基差(near contract)
        entry_basis = basis_path.loc[period_start, 'near_basis']
        # 假设 holding period 收 entry basis
        period_yield = entry_basis * (period_end - period_start).days / 365
        # 减去 roll cost
        roll_cost = 2 * (fee_bps / 10_000)
        net_yield = period_yield - roll_cost
        capital *= (1 + net_yield)

        pnl_series.loc[period_end] = capital - 1

    return pd.DataFrame({'capital': pnl_series, 'cumulative_return': pnl_series})


# ----------------------------- 历史 basis 回测 -----------------------------
def backtest_basis_trade(historical_basis: pd.DataFrame,
                          entry_threshold: float = 0.05,
                          exit_threshold: float = 0.02,
                          fee_bps: float = 10) -> pd.DataFrame:
    """
    historical_basis: index = date, column = 'annualized_basis'
    策略:basis > 5% 入场(cash and carry),basis < 2% 平仓
    """
    df = historical_basis.copy()
    df['signal'] = 0
    position = 0
    entry_basis = None

    for i in range(len(df)):
        b = df.iloc[i]['annualized_basis']
        if position == 0:
            if b > entry_threshold:
                position = 1
                entry_basis = b
        else:
            if b < exit_threshold:
                position = 0
                entry_basis = None
        df.iloc[i, df.columns.get_loc('signal')] = position

    # 简化日 P&L = signal × basis / 365 - fee at trade
    df['daily_yield'] = df['signal'] * df['annualized_basis'] / 365
    turnover = df['signal'].diff().abs().fillna(0)
    df['cost'] = turnover * (fee_bps / 10_000)
    df['net_ret'] = df['daily_yield'] - df['cost']
    df['equity'] = (1 + df['net_ret'].fillna(0)).cumprod()
    return df


# ----------------------------- Roll Yield 分析 -----------------------------
def calculate_roll_yield(near_price: pd.Series, far_price: pd.Series) -> pd.Series:
    """Roll yield = (P_near - P_far) / P_far"""
    return (near_price - far_price) / far_price


# ----------------------------- 主程序 -----------------------------
if __name__ == '__main__':
    print("Term Structure Analysis - BTC")
    print("=" * 60)
    try:
        ts = analyze_term_structure('BTC')
        print(ts.to_string(index=False))
        if len(ts) > 0:
            print(f"\nFront month annualized basis: {ts.iloc[0]['annualized_basis']:.2%}")
    except Exception as e:
        print(f"  API err: {e}")

    print("\nCash and Carry P&L Example")
    pnl = cash_and_carry_pnl(
        spot_qty=10, futures_short_qty=10,
        spot_entry=60000, futures_entry=63000,
        spot_exit=58000, futures_exit=58000,  # converge to spot
        days_held=90)
    for k, v in pnl.items():
        if isinstance(v, float):
            print(f"  {k}: {v:.4f}")

    # 简化历史 basis 回测(合成数据)
    print("\nSimulated Basis Backtest")
    np.random.seed(42)
    dates = pd.date_range('2023-01-01', '2024-12-31', freq='D')
    # 合成 basis:mean-reverting around 0.08
    basis = np.zeros(len(dates))
    basis[0] = 0.08
    for t in range(1, len(dates)):
        basis[t] = 0.95 * basis[t-1] + 0.05 * 0.08 + np.random.normal(0, 0.01)
    basis_df = pd.DataFrame({'annualized_basis': basis}, index=dates)
    bt = backtest_basis_trade(basis_df, entry_threshold=0.10, exit_threshold=0.05)
    print(f"  Total return: {bt['equity'].iloc[-1] - 1:.2%}")
    print(f"  Sharpe: {bt['net_ret'].mean() / bt['net_ret'].std() * np.sqrt(365):.2f}")
    print(f"  Days in market: {(bt['signal'] == 1).sum()} / {len(bt)}")

四、真实数据/案例

案例 1:3AC 的 GBTC basis trade(致命)

3AC 长期做 GBTC 折价 trade:

  • 2020-2021:GBTC 6 个月锁仓后可二级卖出,曾长期 +20% 溢价
  • 操作:在 1 级买 GBTC,6 月后 unlock 到 2 级卖
  • 2021-Q2:溢价突然变 -10% 折价
  • 3AC 杠杆 5x,未及时砍仓

教训:basis trade 看似确定,但路径依赖(lockup、可赎回性)可以毁灭。

案例 2:FTX 倒闭前的 basis 异常

2022-11-08 FTX 危机前:

  • BTC perp funding 跳到 -0.05%(2 个月低位)
  • 季度合约 basis 从 +6% 变 -2%(backwardation)
  • 提示:市场极度恐慌、margin call 压力
  • 反向 basis trade:long 期货 + short 现货(DeFi 借入)能赚 8-10%

案例 3:CME BTC ETF basis 暴涨(2024-Q1)

ETF 通过后 CME basis 显著上升:

  • 2024-01:ETF 通过日,CME 基差跳到 +18% 年化
  • 整个 Q1 维持 12-15%
  • 大量机构资金涌入 carry trade
  • 基差被套利消除到 ~5% by Q3

教训:政策事件触发 basis 异常,机构反应迟缓时有套利窗口。

案例 4:CME Bitcoin basis 与 funding 利差

2024 Q2 CME 基差 8% 年化 vs Binance 永续 funding 5% 年化。

  • 套利:在 Binance 现货 + CME 期货 short
  • 但 CME 要求美国机构资格,零售难以参与
  • 教训:机构通道独立的 alpha 持续存在

案例 5:Backwardation 黄金机会(2022 LUNA)

2022-05-12 LUNA 崩盘:

  • BTC 季度合约一度 backwardation -8% 年化
  • 反向 carry:买 future + 短 spot
  • 但 spot 短借成本高(DeFi 借 USDC 做多 BTC,相反方向)
  • 实际可执行收益 4-6%

五、CEX vs DEX 策略差异

维度CEX BasisDEX Basis
平台Deribit/CME/Binance FuturesdYdX (perp only)、Lyra、Premia
dated futuresDeribit 季度/月度极少(多数 DEX 仅 perp)
成本0.05-0.10% 单边0.10-0.50% + gas
流动性季度合约 OI $1B+更小
结算现金链上自动
DeFi 独有Pendle PT/YT 是 perfect basis

Pendle PT/YT 的 basis 等价

  • PT (Principal Token) ≈ zero-coupon bond
  • 持有 PT 到期 = 锁定到期 yield
  • Implied yield = 1 - PT_price,等价于 basis trade
  • 极简:在二级市场买 PT,到期赎回 1,锁定 yield 无 leg risk

六、风险管理

6.1 Basis trade 风险

风险控制
Roll loss选 backwardation 时 carry,contango 时 reverse
流动性季度合约 OI > $100M 才进
Convergence path监控 mid-life basis 稳定性
现货端KYC/合规、保管
Tail eventLUNA/FTX 时 basis 大幅偏离

6.2 Capacity 估计

CME BTC basis trade 容量:

  • ETF inflow $50B 推动
  • Basis trade 估计 $5-10B 容量
  • 单笔 < $50M(depth)

6.3 资金成本

def basis_break_even_apr(borrow_rate, fees, slippage, savings_rate):
    """basis 净 yield 必须超过:借贷成本 - savings 收益"""
    return borrow_rate - savings_rate + fees + slippage

例:

  • USDC 借 5%,T-bill 4.5% → spread 0.5%
  • Fees 0.20%
  • 总成本 0.7% 年化
  • Basis > 0.7% APR 才有收益(极宽松)
  • 实操要求 basis > 5% 才足以覆盖 risk premium

七、关键速查

Basis 计算

Annualized Basis = (F - S) / S × 365 / days_to_expiry

入场策略

市场操作
Contango (basis > 0)Spot long + Futures short
BackwardationSpot short + Futures long
反向(DeFi 借入做空)借 USDC → 卖 BTC + buy futures

Roll 时机

  • 提前 14-21 天 roll(避开到期 vol)
  • 选 OI 转移已开始的时点
  • 避免周末(流动性差)

八、面试题

Q1:Basis trade 和 funding rate arb 的本质区别?

  • Basis(dated futures):锁定到 T 时刻 yield,convergence 必然(合约设计强制)
  • Funding(perpetual):无到期,funding 实时浮动,方向可逆
  • 比较
    • Basis 更确定(路径风险小)
    • Funding 灵活(随时进出)
    • Basis 适合大资金、长期 carry
    • Funding 适合机敏短期捕捉
  • 结合
    • Funding > basis annualized → 用 perp
    • Basis 显著 > funding → 用 quarterly
    • 跨产品套利:funding 高但 basis 低 → 永续短 + 季度多

Q2:为什么 ETF 通过后 CME basis 暴涨?

  • ETF 创造 / 赎回机制:APs (Authorized Participants) 收 BTC,需 hedge → 卖 CME futures
  • 同时 ETF 直接需求推高 spot
  • Basis = F - S 受双向推动
  • 对应套利者(其他 traders)需求是 short futures + long spot 收 basis
  • 但合规 / 资金规模限制使套利不充分 → basis 持续高位
  • 这是结构性 alpha,可能持续数年

Q3:怎么判断 contango 是 normal 还是 over-extended?

  • Normal contango:5-12% 年化(覆盖借贷成本 + 风险溢价)
  • Over-extended:> 20% 年化,往往伴随:
    • Spot 强劲牛市
    • 杠杆资金涌入 perpetual long
    • 期货 long-skew
  • 判断
    1. 比较历史百分位(90th+ 极端)
    2. 看 funding rate 协同性
    3. 看 OI 增长(资金涌入)
  • 极端 contango 后:往往 mean revert,注意 reversal risk

Q4:Pendle PT 和传统 basis 的关系?

  • PT (Principal Token) 是 yield-bearing 资产的本金部分,到期 1:1 兑底层
  • 二级市场 PT 价格 < 1 → implied yield = 1/PT - 1(年化要 / T)
  • 等价于:买 PT = lock-in yield 到 T,类似 zero-coupon bond
  • 优势 vs 期货 basis:
    • 单 leg(无 spot leg),无 cross-margin 风险
    • 固定到期,无 funding 风险
    • DeFi 原生(无 KYC、24/7)
  • 劣势:
    • 流动性较小
    • 协议风险(Pendle 合约)
    • Yield 资产风险(如果底层是 lent USDC,借贷协议 risk)

Q5:Backwardation 套利的实际困难?

  1. Spot 做空困难:现货空单需要借入,DEX 借贷利率高(10-20%)
  2. Backwardation 通常发生在恐慌期:流动性差、滑点大
  3. 持有 spot 的成本高:保管、放弃 staking yield
  4. Backwardation 不一定收敛:极端情况持续数月
  5. 现货 short 在 CEX 限制:margin trading 有借贷限额
  6. 机构难做:合规要求做空合规复杂
  • 解法:做反向 carry 时控制规模 < 总资本 5%;用 Pendle 反向(卖 PT)

明日预告

Day 99: Liquidation Mining — Aave / Compound 清算机器人。从今天的 basis trade 转到 DeFi 原生套利机会,深度学习清算机制和 bot 实现。