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之间,需要多品种分散才能提升
改进方向
- 多品种分散:同时交易10-20个品种,利用低相关性降低波动
- 自适应参数:根据波动率环境动态调整通道周期
- 过滤器:加入趋势强度过滤(如ADX),在震荡市减少交易
- 仓位优化:从等风险分配升级到风险平价(Risk Parity)
CTA趋势跟踪不是暴利策略,但它有一个重要特性:在股市大跌时往往表现良好(危机Alpha)。作为资产配置的一部分,它的价值远大于单独使用。