3.4 避免常见的回测陷阱

3.4 避兔常见的回测陷阱

基于过去发生的历史信息产生历史交易,并考察这些交易业绩的过程,称为回测。计算机算法产生交易的回测看似简单,但实际上很容易出错。历史业绩高估(相对于已发生的现实交易)是回测的常见错误。我们已经知道,使用有存活偏差数据会导致回测业绩高估。然而,还有一些与如何编写回测程序以及如何构造交易策略相关的常见回测陷阱。我介绍其中最常见的两种,以及规避它们的方法。

3. 4. 1 前视偏差

前视偏差是指使用交易完成之后的信息。例如,“在日最低价的1%之内买入股票”的交易规则,就有前视偏差,因为在当日市场收盘前,是不可能知道日最低价的。又如,使用全部数据回归得来的系数,来产生一个基于前后两段价格序列的线性回归模型的交易信号,同样有前视偏差。

我们如何避免前视偏差呢?使用“滞后”的历史数据来计算策略信号,可以避免前视偏差。滞后数据系列意味着,在计算移动平均值、最高价、最低价、成交量等指标时,只使用“上一”交易期限的收盘数据。(当然,如果策略只在交易期限结束时触发,那就无需使用滞后数据。)

使用Excel 等“所见即所得”的程序比MATLAB 更容易避免前视偏差。因为在Excel 中很容易将不同列的数据对齐,并确保每个单元格里的公式只使用当前行之前的数据。Excel 的单元格高亮显示功能,可以让我们直接查看使用当日数据所产生交易信号的情况。(双击带公式的单元格,该公式所使用数据所在的单元格会高亮显示。)

即便非常谨慎,在编写回测程序时还是有可能犯前视偏差错误。有些错误非常微妙、难以避免,特别是在使用MATLAB 的时候。最好用下列方法对回测程序做最后检查:使用所有历史数据运行程序,把推荐头寸存入文件A (目标文件包含程序每天生成的所有推荐头寸)。然后移除最近N 天的历史数据,即如果原始数据中的最后一天是T 日,移除后的数据中的最后一天就是T-N 日, N 可以是10 天到100 天不等。再次运行回测程序并将结果存入另一个文件B。移除文件A 的最后N 行,此时文件A 与文件B 的行数(天数)相同,最后一天均为T-N 日。最后,比较文件A 和文件B 中的头寸。如果头寸不一致,说明回测程序中存在前视偏差,必须找出并改正,因为头寸不一致意味着,误将那N 天移除了的数据也加入了文件A 中头寸的计算。我将在例3. 6 中介绍这个稍微有点绕的过程。

3. 4. 2 数据迁就偏差

第2 章提到,因迁就历史数据的噪声而过度优化模型参数,造成策略的回测业绩高于未来业绩,即为数据迁就偏差。在基于历史数据的统计预测模型中,数据迁就偏差非常普遍,而在金融中,因为独立数据的量非常有限,所以这个问题就更加严重。高频数据倒是非常多,但只对高频模型有用。尽管股票市场数据可以追溯到20 世纪初,但只有最近十年的数据才适用于构建预测模型。此外,如果发生了第2 章讨论过的状态转换,最近几年的旧数据可能也没法用。独立数据的量越少,交易模型中用到的可调整参数就应该越少。

根据经验,我在计算移动平均值时不会超过5 个参数,包括如建仓清仓阀值、持有期限、回溯期限等定量指标。此外,并非所有的数据迁就偏差都是由参数优化造成的。用相同数据集重复做回测时,还会影响模型的定性决策选择,如在开盘还是收盘时建仓、是否隔夜持仓、交易大盘股还是中盘股等。通常,这些可以优化回测业绩的定性选择,对未来未必是最优的。构建数据驱动模型时,几乎不可能完全消除数据迁就偏差,但有几种方法可以降低这种偏差。

样本含量

降低数据迁就偏差的最基本方法是,按照要优化的自由参数个数,使用足够多的回测数据。根据经验规则,通常假定优化参数所需的数据点个数,是模型中自由参数个数的252 倍(一年交易天数为152 天。这个假定未基于任何统计调查结果,完全是由经验所得)。例如,回测三参数的当日交易模型,至少要用三年的日价格数据。如果是分钟交易模型,则至少需要7 个月( 2521390 年)的分钟数据(每天交易分钟数6. 5 x 60=390 )。注意,如果是一个日交易模型并且有了7 个月的分钟数据点,实际上也只有7X21=147 个有效数据(每月交易天数21 ),要回测三参数模型是不够的。

样本外测试

将历史数据根据时间先后分为两段,后一段数据用于样本外测试。构建模型时,参数优化和定性选择使用前一段数据(称为“训练集勺,所得模型的测试使用后一段数据(称为“测试集”)。(两段数据的大小要大致相等,若数据量不够,则至少用三分之一的数据做测试。训练集的最小含量由上一节提到的经验规则决定。)在理想情况下,基于训练集的最优参数和决策,对于测试集也是最优的,不过实际上很难做到这一点。但测试集上的业绩起码要合理。否则,模型就存在数据迁就偏差,需要进一步简化并减少参数。

动态参数优化是更严密、也更复杂的样本外测试方法。这种方法通过使参数不断适应变化的历史数据,来消除数据迁就偏差。(参见“无参数交易模型”)

无参数交易模型

我曾为之工作过的一个投资经理,很热哀于宣传他的交易模型“没有任何自由参数”。由于业内的保密行规,他没有透露过吴多的技术细节。

后来,我开始明白无自由参数交易模型的含义了,并不是说不包含任何参数,如不包含计算价格趋势时要用的回溯期或确定建仓或清仓时间的间佳,这是不可能的。真正的含义是所有这些参数都在一个移动回溯窗口中进行动态优化。假如你问z “模型中有固定的盈利上限吗?”交易员会诚实地回答z “没有,盈利上限并不是一个输入参数,它是由模型本身所决定的。”

无参数交易模型的优势在于,能够减少多参数输入模型的过皮优化风险〈数据迁就偏差〉。因此,回测业绩应与未来交易的实际业绩是加吻合。

〈注意,参数优化并不一定是挑选那个回测业绩最好的参数集。通常,是应该是基于不同参数集的某种平均来做交易决定。〉

在下达下一条指令前,及时对所有参数进行优化,是对计算的一个很大考验,若要进行多维优化就灵加困难了。因此,在开始研究例7.1 所述的状态转换模型之前,我自己极少使用无参数交易模型。状态转换模型基本上是元参数的〈我在优化时忽略了一些参数,不是因为任何技术上的困难,而是时间有限〉。在这个例子中,我怎么在几分钟之内完成回测参数优化的呢?我使用了高端田测交易平台( Alphacet Discovery)。

“改编自我的搏客文章《无参数交易模型机见epchan. blogspot. com/2008/05/parameterless-trading-models. html 。

最靠谱的样本外测试方法,很多交易员都很熟悉,叫做“仿真交易”。用尚未发生的真实数据运行模型,是最可靠的测试方式。仿真交易不仅能做真实准确的样本外测试,同时也常常可以发现模型中的前视偏差,以及各种与操作相关的问题。第5 章将详细讨论仿真交易。

对于一个需要通过测试来核实的公开策略,从策略的公开日到测试日的这段时间,是不折不扣的样本外测试期。只要不在样本外测试期优化模型的参数,这段时间跟策略的仿真交易的效果一样好。

例3. 6 GLD 和GDX 的配对交易

本例展示如何将数据分成训练集和测试集。我们将回测一个配对交易策略,在训练集上优化参数,在测试集上观察效果。

GLD 代表黄金的现货价格, GDX 是一揽子采企企业股票,两者的价格是高度相关的,故GLD 和GDX 可用于做配对交易。我在博客中运用协整分析对ETF 的这一自己对交易进行了详细讨论(见epchan. blogspot. com/2006/11/reader-suggested-possible-trading. html )。不过我要到第7 幸才会讲训练集上的协整分析,结果表明, GLD 多头和GDX 空头所形成的差价呈均佳回归。通过训练集上的回归分析可得出GLD 和GDX 之间的对冲比率,并设定西己对交易策略进出市场的阅佳。从后面可以看到,阀佳在训练集上的优化会改变策略在测试集上的业绩。〈程序文件可从epchan. com/book/ example3 _ 6. m 下载,数据文件为GLD. xls 和GLD. xls 。〉程序使用滞后命令,将时间序列滞后一期,这在epchan. com/book 中也能找到。还会使用“普通最小二来法( QLS )”命令进行线性回归,在spatial-econometrics. com 上可免费打包下载。

使用MATLAB

%清除工作空间已有变量

clear;

%将” GLD. xis”读入MATLAB

[num, txt]=xlsread('GLD’>;

%第一列(从第二行开始〉是交易日,格式为mm/dd/yyyy

tdayl=txt(2:end, 1);

%将时间格式转化为yyyymmdd

tdayl= ..

datestr(datenum(tdayl ,’ mm/dd/yyyy丁,’ yyyymmdd') ;

%将数据字符串转化为单元型变量,再转化为数值型交量

tdayl = str2double(cellstr( tdayl>;

%最后一列是调整后收盘价

adjclsl = num(:, end);

%读入” GDX. xls”[

num, txt]=xlsread('GDX');

%第一列〈从第二行开始〉是交易日,格式为mm/dd/yyyy

tday2=txt(2: end, 1);

%将时间格式转化为yyyymmdd

tday2= ???

datestr(datenum( tday2 ,’ mm/dd/yyyy吵,’ yyyymmdd');

%将数据字符串转化为单元型变量,再转化为数位型变量

tday2 = str2double( cellstr( tday2>;

%最后一列是调整后收盘价

adjcls2=num(:, end);

%找到两组数据的交集并按升序排列

[tday, idxl, idx2]=intersect(tdayl, tday2);

cll= adjclsl (idxl) ;

cl2=adjcls2(idx2);

trainset=l:252; %定义训练集下标

%定义测评集下标

testset =trains et (end) + 1 : length ( tday) ;

%用回归函数计算得到对冲比率

results= ols( ell ( trainset) , cl2 ( trainset> s

hedgeRatio=results. beta;

%差价= GLD一对冲比率铃GDX

spread= ell - hedgeRatio 铃cl2;

plot(spread( trainset>

figure;

plot( spread( testset> 

figure;

%训练集平均差价

spreadMean =mean (spread ( trainset>;

%训练集差价标准差

spreadStd =std (spread ( trainset>;

%差价标准化(用z-scores 方法〉

zscore= (spread- spreadMean). I spreadStd;

%在组合价值向下跌攻2 倍标准差时,购买此差价组合

longs= zscore< =一2;

%当组合价值上升超过2 倍标准差时,做空该差价组合

shorts= zscore> = 2;

%当组合价值回到1 倍标准差以内时,清仓exits=abs(zscore)<= 1;

%初始化头寸数纽

positi。ns=NaN<length(tday), 2);

%多头入市

positi。ns(shorts, : ) =rep mat ( [ -1 1 J , [length (find (shorts> 1]);

%空头入市

positions<longs, : ) = repmat( [1 一口,[ length(find (longs> 1]);

%清仓

positions(exits, : ) =zeros(length(find(exits>, 2);

%确保继续持仓,除非出现清仓信号

% positions= fillMissingData(positions);

cl= [ell cl2 J ; %合并两个价格序列

dailyret = (cl 一lagl (cl)). /lagl (cl);

pnl = sum(lagl (positions).祷dailyret, 2) ;

%训练集的夏普比应该是2. 3

sharpeTrainset= ???sqrt(252 )铃mean(pnl(trainset(2: end>)? I std ( pnl ( trainset ( 2 : end>>

%测评集的夏普比应该是1. 5

sharpeTestset= sqrt(252 )铸mean( pnl ( testset) ) . I std ( pnl (tests et>

plot(cumsum(pnl( testset>>;

%保存头寸文件以使检查数据先窥偏差

save example3_6_positions positions;

文件lagl. m:

function y= lagl ( x)

% y=lag(x)

if (isnumeric(x))

%第一个元素填充为NaN

y= [NaN( I ,size( x, 2)); x(l: end-I, : ) J; elseif (ischar( x))

%第一个元素填充为”

y=[repmat(””,[1 size(x,2)]);x0:end-1, :)];else error (’Can only benumeric or char array') ;

End

此配对交易策咯在训练集和测试集上的夏普比率都很高,因此可认为它是无数据迁就偏差的。但也许还有进一步改进的空间。若把建仓阀佳改为1 倍标准差、清仓阅值改为o. 5 倍标准差,训练集上的夏普比率会上升到2. 9 ,测试集上的夏普比率会上升到2. 10 显然,这一间位集更佳。

不过,在训练集上进行参数优化也许会降低测试集上的业绩。这种情况下,应选择使得训练集和测试集上的业绩结果都较好〈也许不是最好〉的参数集。我没有将交易成本考虑在内(下一节会讨论交易成本〉。读者可以自己做练习。由于这一策略不是频繁交易,因此,交易成本对所得的l:普比率影响并不大。

为观察这一策咯是如何工作的,读者可参见图7.4 所显示的差价,第7 幸将结合乎稳性和协整性进行进一步讨论。你将看到差价走势呈现出很明显的均值回归。因此,不断地低买高卖是很管用的。

最后,还要检测任何可能的数据前视偏差。在上面的MATLAB 代码“ cl2=adjcls2 (idx2 );”之后,添加以下代码。

%将最近的交易日数据移除掉

cutoff= 60; %移除最近60 天的数据

tday(end-cutoff+l :end, : ) =[];

cll(end-cutoff+l :end, : ) =[];

cl2(end-cutoff+l :end, : ) =[];

将以下代码放在上面MATLAB 程序的结尾,取代“save example3_6_positions positions,,。

%检测数据先窥偏差的第二步。

ldoutput= load(' example3_6_positions');

oldoutput. positions( end-cutoff+ 1 : end, : ) = [];

if (any( positions~=oldoutput. positions))

fprintf(l, 'Program has look-forward-bias! \n');

End

将新的代码保存为“ example3 _ 6 _ 1. m ”希运行,你会发现“ Program has look-forward-bias,,语句不会被打印出来,这说明算法通过测试了。

敏感性分析

在完成模型的参数和各种特征的优化、通过测试集的检验之后,可以通过改变这些参数或改变模型的定性决策,来观察模型业绩在训练集和测试集上的变化。如果业绩变化很大,在参数取任何其他值时业绩都很糟糕,模型很有可能存在数据迁就偏差。

各种简化模型的方法都值得尝试。决定是否交易真的需要五个不同的条件吗?逐个移除这些条件,模型在训练集上的业绩会在哪个临界点显著降低呢?更重要的是,移除这些条件时,模型在测试集上的业绩会相应降低吗?即使模型在训练集上的业绩可能会降低,只要没有显著降低,就应该尽可能地移除更多的条件、约束和参数。(但不能为了提升测试集上的业绩而增加条件和参数,或调整参数值,如果这样做了,就等于把测试集当成训练集使用了,并且有可能使模型再度产生数据迁就偏差。)

在简化了参数集和条件,并确保样本外测试的业绩在参数和条件微小变化时不受显著影响之后,应考虑将资金分配到不同的参数值和条件集。这种资金在参数上的平均化,将进一步确保模型的真实交易业绩与回测业绩不会相差太大。