CTA(Commodity Trading Advisor,商品交易顾问)是量化策略里的一个大类,核心交易标的是期货。虽然名字里带"商品",但实际上 CTA 策略覆盖的远不止大宗商品:股指期货、国债期货、外汇期货都在射程范围内。CTA 策略分两大流派:趋势跟踪(价格涨了就做多,跌了就做空)和统计套利(找相关品种之间的价差偏离,赌它回归)。这篇文章从分类讲起,两条线都覆盖,各附 Python 代码。

CTA 策略的三种类型

按照交易逻辑,CTA 策略可以分成三类。

趋势跟踪(Trend Following)是 CTA 里最主流的一类,全球管理期货基金里超过 70% 的资金规模走的是趋势路线。逻辑很直觉:如果一个品种价格在上涨,说明多头力量占优,做多;价格在下跌,空头占优,做空。用一句老话概括就是"截断亏损,让利润奔跑"。趋势跟踪的核心假设是"价格动量会延续":过去涨的品种在近期继续涨的概率高于 50%。

统计套利 / 均值回归(Statistical Arbitrage / Mean Reversion)走相反的路线:找两个价格高度相关的品种,当它们之间的价差偏离历史正常水平时,做空偏贵的、做多偏便宜的,等价差回归。这类策略不赌市场方向,只赌"关系恢复正常"。

高频做市(Market Making)通过挂限价单提供流动性,赚取买卖价差。对延迟和基础设施的要求极高,不在本文讨论范围。

趋势跟踪和统计套利是两种几乎相反的思维方式,适用的市场环境也不同:

趋势跟踪统计套利
核心逻辑强者恒强,弱者恒弱偏离终将回归
胜率低(通常 30-40%)高(通常 60-70%)
盈亏比高(赢的时候赚很多)低(每笔利润有限)
适用市场有明确趋势的行情震荡、区间波动的行情
回撤来源震荡市反复止损价差不回归(关系破裂)
典型夏普0.3 - 0.81.0 - 2.0

CTA 策略有一个被机构投资者看重的特性:危机 alpha。2008 年金融危机期间,全球股市暴跌,但趋势跟踪策略因为做空股指期货和商品期货赚了不少。SG Trend Index 在 2008 年录得 +21% 的回报,同期标普 500 下跌 37%。这种和股票市场的负相关性,让 CTA 成为机构投资组合里的"保险"配置。

趋势跟踪:双均线交叉系统

趋势跟踪最经典的实现方式是均线交叉。具体来说是用两条不同周期的移动平均线:一条短期(比如 20 日),一条长期(比如 60 日)。当短期均线从下方穿过长期均线(“金叉”),说明价格的短期动量向上,做多;当短期均线从上方穿下来(“死叉”),做空。

参数选择的直觉:短期均线是噪声过滤器,它过滤掉日间波动,保留近期趋势方向;长期均线是趋势确认器,它代表更长时间尺度上的方向。短期均线太短(比如 5 日),每天的随机波动都会触发信号,来回被"洗";长期均线太长(比如 200 日),信号严重滞后,趋势都走完了才发出信号。20/60 是一个常见的中等频率组合。

下面用 Python 模拟一段带有趋势和震荡交替出现的期货价格序列,然后跑双均线交叉策略:

import numpy as np

def simulate_trend_following(n_days=1000, short_window=20, long_window=60):
    np.random.seed(42)

    # 模拟期货价格:5 个阶段交替出现趋势和震荡
    regimes = [
        (0, 150, 0.0008, 0.012),     # 上涨趋势
        (150, 300, -0.0001, 0.018),   # 震荡
        (300, 500, -0.0006, 0.013),   # 下跌趋势
        (500, 700, 0.0001, 0.020),    # 震荡
        (700, 1000, 0.0007, 0.011),   # 上涨趋势
    ]

    returns = np.zeros(n_days)
    for start, end, mu, sigma in regimes:
        returns[start:end] = np.random.normal(mu, sigma, end - start)

    price = 100 * np.exp(np.cumsum(returns))

    # 计算均线
    ma_short = np.array([np.mean(price[max(0, i-short_window+1):i+1])
                         if i >= short_window - 1 else np.nan
                         for i in range(n_days)])
    ma_long = np.array([np.mean(price[max(0, i-long_window+1):i+1])
                        if i >= long_window - 1 else np.nan
                        for i in range(n_days)])

    # 信号:短均线 > 长均线做多,反之做空
    position = np.zeros(n_days)
    for i in range(long_window, n_days):
        position[i] = 1 if ma_short[i] > ma_long[i] else -1

    # 策略收益
    strategy_ret = position[:-1] * returns[1:]
    strategy_ret = np.insert(strategy_ret, 0, 0)
    strategy_equity = np.exp(np.cumsum(strategy_ret))
    buyhold_equity = price / price[0]

    # 统计指标
    active_ret = strategy_ret[long_window:]
    sharpe = np.mean(active_ret) / np.std(active_ret) * np.sqrt(252)
    peak = np.maximum.accumulate(strategy_equity)
    max_dd = np.min((strategy_equity - peak) / peak)

    print(f"策略终值: {strategy_equity[-1]:.2f}x")
    print(f"买入持有终值: {buyhold_equity[-1]:.2f}x")
    print(f"夏普比率: {sharpe:.2f}")
    print(f"最大回撤: {max_dd:.1%}")

simulate_trend_following()

运行结果:

策略终值: 1.35x
买入持有终值: 1.51x
夏普比率: 0.34
最大回撤: -42.0%
终值夏普比率最大回撤胜率平均盈亏比
趋势跟踪1.35x0.34-42.0%35.3%2.6:1
买入持有1.51x----

模拟一共触发了 17 笔交易,只有 6 笔盈利,胜率 35.3%,但赢的时候平均赚的是亏的时候的 2.6 倍。这正是趋势跟踪的典型特征:大部分时间在小亏(震荡期均线反复交叉),少数大趋势带来的利润覆盖所有亏损。

趋势跟踪策略的净值曲线:均线交叉信号 vs 买入持有

趋势跟踪的致命弱点

从模拟结果可以看到,策略终值(1.35x)甚至低于买入持有(1.51x)。这不是说趋势跟踪没用,而是因为这段模拟中震荡期占了将近一半时间。震荡市是趋势跟踪的天敌:价格没有明确方向,均线反复交叉,策略不断开仓又止损,交易成本和滑点持续消耗资金。这种成本被称为"趋势税"。

趋势跟踪的利润分布极度集中:17 笔交易里只有 6 笔盈利,剩下 11 笔都在亏。如果你错过了那几笔大趋势(比如因为参数选择不当而晚进场),整体收益可能直接变成负数。

参数敏感性是另一个问题。把短期均线从 20 日改到 15 日或 25 日,结果可能差很多。但不能因此去优化参数让回测最好看,这就是回测陷阱里的过拟合。实操中的做法是:在多个品种上用同一组参数,靠品种分散来平滑单品种的参数风险。

ATR 仓位管理

趋势跟踪策略通常配合 ATR(Average True Range,平均真实波幅) 来做仓位管理。ATR 衡量的是品种最近 N 天的平均每日波动幅度。波动大的品种(比如原油),每天涨跌 3%,仓位就要小;波动小的品种(比如国债期货),每天涨跌 0.3%,仓位可以大。

常用的规则是:每笔交易的风险敞口 = 1 个 ATR 对应本金的 1%。如果账户有 100 万,1% 是 1 万元。某品种的 ATR 是 50 点,每点价值 10 元,那最多开 1 万 / (50 × 10) = 20 手。这样不管交易什么品种,每笔止损的金额是一样的。

TA-LibATR 函数可以直接计算这个指标。

期货套利策略

CTA 里的套利策略和配对交易方法论一致:找协整关系 → 构建价差 → 价差偏离时入场 → 回归时出场。区别在于标的从股票换成了期货合约,套利类型主要分两种。

跨期套利

跨期套利(Calendar Spread)交易的是同一品种不同到期月的合约之间的价差。比如螺纹钢 2025 年 5 月合约和 2025 年 10 月合约,它们跟踪的是同一个商品,但到期时间不同。

近远月合约的价差反映的是持仓成本(仓储、资金利率)和市场对未来供需的预期。正常情况下远月合约比近月贵(期货升水 / contango),价差相对稳定。当价差因为短期供需冲击、交割月临近等原因偏离正常水平时,套利机会出现。

跨品种套利

跨品种套利(Intercommodity Spread)交易的是两个不同但相关的品种之间的价差。几个经典组合:

  • 大豆压榨价差:豆粕 + 豆油 vs 大豆。大豆压榨后产出豆粕和豆油,三者之间有物理上的数量关系,价差偏离意味着压榨利润异常
  • 螺纹钢 vs 铁矿石:钢铁生产的上下游关系,价差代表钢厂利润
  • WTI vs Brent 原油:两个不同产地的原油基准,受区域供需和运输成本影响

套利信号:Z-score 方法

不管是跨期还是跨品种,信号生成的逻辑都一样:计算价差的滚动 Z-score,偏离超过阈值就入场。

import numpy as np

def simulate_calendar_spread():
    np.random.seed(123)
    n = 500

    # 模拟两个相关期货合约(近月 vs 远月)
    common = np.cumsum(np.random.normal(0.0002, 0.012, n))
    near = 3500 + common * 100 + np.cumsum(np.random.normal(0, 0.8, n))

    # 远月 = 近月 + 升水 + 均值回复噪声
    spread_noise = np.zeros(n)
    for i in range(1, n):
        spread_noise[i] = 0.93 * spread_noise[i-1] + np.random.normal(0, 2.5)
    far = near + 30 + spread_noise

    spread = far - near
    lookback = 40

    # 滚动 Z-score
    z_score = np.full(n, np.nan)
    for i in range(lookback, n):
        window = spread[i-lookback:i]
        std_w = np.std(window)
        if std_w > 0:
            z_score[i] = (spread[i] - np.mean(window)) / std_w

    # 信号
    position = np.zeros(n)
    for i in range(lookback + 1, n):
        if np.isnan(z_score[i]):
            position[i] = position[i-1]
            continue
        if position[i-1] == 0:
            if z_score[i] > 2:    position[i] = -1   # 价差过大,做空价差
            elif z_score[i] < -2: position[i] = +1   # 价差过小,做多价差
        elif position[i-1] == 1:
            if abs(z_score[i]) < 0.5:  position[i] = 0    # 回归,平仓
            elif z_score[i] < -4:      position[i] = 0    # 止损
            else:                      position[i] = 1
        elif position[i-1] == -1:
            if abs(z_score[i]) < 0.5:  position[i] = 0
            elif z_score[i] > 4:       position[i] = 0
            else:                      position[i] = -1

    # 盈亏
    spread_ret = np.diff(spread)
    start = lookback + 1
    daily_pnl = position[start:-1] * spread_ret[start:]
    daily_pnl = daily_pnl[~np.isnan(daily_pnl)]
    n_trades = int(np.sum(np.abs(np.diff(position[start:])) > 0))

    print(f"累计盈亏: {np.sum(daily_pnl):.2f}")
    print(f"交易次数: {n_trades} ({n_trades//2} 个来回)")
    print(f"年化夏普: {np.mean(daily_pnl)/np.std(daily_pnl)*np.sqrt(252):.2f}")

simulate_calendar_spread()

运行结果:

累计盈亏: 96.14
交易次数: 34 (17 个来回)
年化夏普: 2.49

17 个来回交易,年化夏普 2.49。累计盈亏 96 个价差点,相对于价差均值(约 32 点)大概是 3 倍标准差的波动范围赚了 3 轮。和趋势跟踪的 0.34 形成鲜明对比:套利策略胜率高、夏普高,但每笔利润有限(价差的波动范围有天花板),而趋势跟踪在大行情里的单笔利润可以非常大。

期货跨期套利的价差和 Z-score 信号

这个回测做了大幅简化:合成数据的价差是我们直接构造成均值回归的(AR(1) 过程),所以跳过了协整检验步骤。实盘中必须先做协整检验,否则价差可能根本不回归。代码里也没扣交易成本和滑点,没有考虑保证金变化和合约滚动。实际表现会差一些。关于回测中常见的各种坑,参考回测陷阱大全。Z-score 方法的完整实现(包括协整检验、滚动对冲比率)在配对交易入门里有详细讲解。

CTA 策略的实操要点

合约滚动。期货合约有到期日,比如螺纹钢 2025 年 5 月合约在 5 月中旬到期。如果你的趋势策略在 4 月发出做多信号,持仓到 5 月就必须在到期前把仓位从 5 月合约转移到 10 月合约(“移仓换月”)。滚动时近远月价差会产生成本或收益,长期累积下来是一笔不可忽视的隐性成本。

保证金和杠杆。期货天生带杠杆:10% 的保证金比例意味着你用 10 万元的保证金可以控制 100 万元面值的合约。如果不做任何资金管理,价格变动 1% 就是保证金的 10% 变动。CTA 基金通常只动用 20-30% 的总资金做保证金,剩余 70-80% 的资金放在国债等无风险资产上吃利息。这样实际的投资组合杠杆只有 2-3 倍,不是名义上的 10 倍。

多品种分散。单个品种可能大半年没有趋势,策略一直在亏趋势税。但如果同时交易 30 个品种(股指、国债、黄金、原油、农产品、金属),某些品种处于趋势行情的概率就大得多。分散是 CTA 基金控制回撤的核心手段。

评估 CTA 策略的表现,除了夏普比率,Calmar 比率(年化收益率除以最大回撤的绝对值,比如年化 15% / 最大回撤 30% = 0.5)也很常用,因为 CTA 策略的回撤特征比股票策略更重要。

CTA 策略和波动率交易策略的一个本质区别:波动率交易赌的是"波动幅度"会比市场定价大还是小,CTA 赌的是"价格方向"会不会延续。两者可以组合使用:CTA 做方向性收益来源,波动率策略做风险管理工具。