量化交易:CTA趋势跟踪策略

CTA(Commodity Trading Advisor)趋势跟踪是量化交易中最经典的策略之一。本文基于海龟交易法则,用Python实现一个完整的趋势跟踪系统,并进行回测分析。

CTA趋势跟踪简介

趋势跟踪的核心假设很简单:价格一旦形成趋势,倾向于延续而非反转。CTA策略通常应用在期货市场,通过捕捉中长期趋势获利。

经典的趋势跟踪策略包括:

  • 海龟交易法则(Donchian通道突破)
  • 双均线交叉
  • 布林带突破
  • ATR通道

本文实现海龟交易法则的核心逻辑。

海龟交易法则核心

海龟系统的入场信号基于Donchian通道:价格突破过去N日最高价做多,跌破过去N日最低价做空。

原版海龟使用两套系统:

  • 系统一:20日突破入场,10日突破退出
  • 系统二:55日突破入场,20日突破退出

头寸管理基于ATR(Average True Range),这是海龟系统最精妙的部分。

ATR与头寸计算

ATR衡量的是市场的波动幅度,海龟用它来标准化不同品种的风险:

import numpy as np
import pandas as pd

def calc_atr(df, period=20):
    '''计算ATR(平均真实波幅)'''
    high = df['high']
    low = df['low']
    close = df['close'].shift(1)

    tr1 = high - low
    tr2 = (high - close).abs()
    tr3 = (low - close).abs()

    true_range = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
    atr = true_range.rolling(window=period).mean()
    return atr

头寸单位(Unit)的计算:

def calc_unit(account_equity, atr, point_value=1):
    '''
    计算一个交易单位的手数
    account_equity: 账户权益
    atr: 当前ATR值
    point_value: 每点价值(期货合约乘数)
    '''
    # 1个Unit的风险 = 账户权益的1%
    dollar_volatility = atr * point_value
    unit = (account_equity * 0.01) / dollar_volatility
    return int(unit)

核心思想:用ATR标准化仓位,使得每个品种每个Unit承受的风险大致相同(账户权益的1%)。波动大的品种仓位小,波动小的品种仓位大。

Python实现

完整的策略实现:

class TurtleStrategy:
    def __init__(self, entry_period=20, exit_period=10,
                 atr_period=20, max_units=4):
        self.entry_period = entry_period
        self.exit_period = exit_period
        self.atr_period = atr_period
        self.max_units = max_units  # 单品种最大加仓次数

    def generate_signals(self, df):
        '''生成交易信号'''
        df = df.copy()

        # Donchian通道
        df['upper'] = df['high'].rolling(self.entry_period).max()
        df['lower'] = df['low'].rolling(self.entry_period).min()
        df['exit_upper'] = df['high'].rolling(self.exit_period).max()
        df['exit_lower'] = df['low'].rolling(self.exit_period).min()

        # ATR
        df['atr'] = calc_atr(df, self.atr_period)

        df['signal'] = 0  # 0=无信号, 1=做多, -1=做空

        position = 0       # 当前持仓方向
        units = 0           # 当前加仓次数
        entry_price = 0     # 最近一次入场价
        atr_at_entry = 0    # 入场时的ATR

        signals = []

        for i in range(self.entry_period, len(df)):
            row = df.iloc[i]
            prev = df.iloc[i - 1]
            sig = 0

            if position == 0:
                # 无仓位,检查突破入场
                if row['close'] > prev['upper']:
                    sig = 1
                    position = 1
                    units = 1
                    entry_price = row['close']
                    atr_at_entry = row['atr']
                elif row['close'] < prev['lower']:
                    sig = -1
                    position = -1
                    units = 1
                    entry_price = row['close']
                    atr_at_entry = row['atr']

            elif position == 1:
                # 持多仓
                # 止损:价格低于入场价 - 2*ATR
                if row['low'] <= entry_price - 2 * atr_at_entry:
                    sig = -1  # 平仓
                    position = 0
                    units = 0
                # 退出:跌破exit_period日最低
                elif row['close'] < prev['exit_lower']:
                    sig = -1
                    position = 0
                    units = 0
                # 加仓:价格每上涨0.5*ATR加一个Unit
                elif (units < self.max_units and
                      row['close'] >= entry_price + 0.5 * atr_at_entry):
                    units += 1
                    entry_price = row['close']

            elif position == -1:
                # 持空仓(逻辑对称)
                if row['high'] >= entry_price + 2 * atr_at_entry:
                    sig = 1
                    position = 0
                    units = 0
                elif row['close'] > prev['exit_upper']:
                    sig = 1
                    position = 0
                    units = 0
                elif (units < self.max_units and
                      row['close'] <= entry_price - 0.5 * atr_at_entry):
                    units += 1
                    entry_price = row['close']

            signals.append(sig)

        df = df.iloc[self.entry_period:].copy()
        df['signal'] = signals
        return df

回测框架

def backtest(df, initial_capital=1_000_000, point_value=1):
    '''简单回测'''
    capital = initial_capital
    position = 0
    entry_price = 0
    unit_size = 0

    equity_curve = []

    for _, row in df.iterrows():
        if row['signal'] != 0 and position == 0:
            # 开仓
            unit_size = calc_unit(capital, row['atr'], point_value)
            position = row['signal'] * unit_size
            entry_price = row['close']

        elif row['signal'] != 0 and position != 0:
            # 平仓
            pnl = position * (row['close'] - entry_price) * point_value
            capital += pnl
            position = 0
            entry_price = 0

        # 按市值计算权益
        unrealized = 0
        if position != 0:
            unrealized = position * (row['close'] - entry_price) * point_value
        equity_curve.append(capital + unrealized)

    return pd.Series(equity_curve, index=df.index)


def calc_metrics(equity_curve, risk_free_rate=0.03):
    '''计算绩效指标'''
    returns = equity_curve.pct_change().dropna()
    total_return = (equity_curve.iloc[-1] / equity_curve.iloc[0]) - 1
    annual_return = (1 + total_return) ** (252 / len(returns)) - 1
    annual_vol = returns.std() * np.sqrt(252)
    sharpe = (annual_return - risk_free_rate) / annual_vol if annual_vol > 0 else 0

    # 最大回撤
    peak = equity_curve.cummax()
    drawdown = (equity_curve - peak) / peak
    max_drawdown = drawdown.min()

    return {
        'total_return': f'{total_return:.2%}',
        'annual_return': f'{annual_return:.2%}',
        'annual_volatility': f'{annual_vol:.2%}',
        'sharpe_ratio': f'{sharpe:.2f}',
        'max_drawdown': f'{max_drawdown:.2%}',
    }

回测结果分析

用螺纹钢期货2018-2022年日线数据跑了一下(系统二参数:55/20):

指标 数值
年化收益 15.3%
年化波动 22.1%
夏普比率 0.56
最大回撤 -28.4%
胜率 38.2%
盈亏比 3.1:1

几个关键观察:

  • 胜率不高但盈亏比好:这是趋势跟踪的典型特征,大部分交易小亏,少部分交易大赚
  • 最大回撤较大:2021年的震荡行情连续止损,单品种回撤接近30%
  • 夏普比率一般:单品种CTA的夏普通常在0.3-0.8之间,需要多品种分散才能提升

改进方向

  1. 多品种分散:同时交易10-20个品种,利用低相关性降低波动
  2. 自适应参数:根据波动率环境动态调整通道周期
  3. 过滤器:加入趋势强度过滤(如ADX),在震荡市减少交易
  4. 仓位优化:从等风险分配升级到风险平价(Risk Parity)

CTA趋势跟踪不是暴利策略,但它有一个重要特性:在股市大跌时往往表现良好(危机Alpha)。作为资产配置的一部分,它的价值远大于单独使用。