自适应行情跟踪方法的实际评估(基础篇)
自适应跟踪到底能不能在 MT5 跑通
很多交易者把自适应行情跟踪当成黑箱,其实在 MetaTrader 5 里它是一套可复现的逻辑。2017 年 11 月 2 日有一份公开示例,实测被查看 7272 次、评论 24 条,说明这套思路在真实盘前至少经得起社区推敲。 把它搬进 MT5 验证并不复杂:新建 EA 后挂上自适应均线,再把跟踪止损步长绑定到波动率指标,就能看到仓位随行情呼吸。外汇与贵金属杠杆高,回测通过不代表实盘稳,参数错配可能放大滑点。 开 MT5 先跑一遍自带自适应样本,比看十篇推演有用。重点看两个数字:跟踪触发后的最大回撤、以及单位时间内的重复开仓次数。
◍ 用频谱视角拆价格序列
这套思路最早由 Vladimir Kravchuk 在 2001–2002 年的《Currency speculator》杂志里讲清楚:把数字滤波器和离散时间序列的频谱估值直接搬进交易系统。价格图上那些乱跳的实时报价,数学上属于非解析函数,形状可以任意。 傅里叶定理给了个硬底子结论——有限时间区间上的任意函数都能展开成无限多个正弦波的叠加。所以一段行情不管多不规则,都能被唯一地映射成频率函数,也就是它的频谱。 对非随机信号,做傅里叶变换就能从时域切到频域,直接算出频谱。而随机过程不能这么干,得用功率谱密度(PSD)来描述,它并不是过程本身的傅里叶变换,而是其自相关函数的频域表达。外汇和贵金属这类高波动品种,随机成分占比高,拿 PSD 看周期比裸看 K 线更不容易被骗线。
「自适应频谱跟踪的底层逻辑与指标矩阵」
滤波本质是对信号频谱的定向修正,可在一定频带内放大或削弱分量。MT5 里常见的 MA、RSI、Momentum、Stochastic 全是数字滤波器,其转移函数增益随频率变化,但绝大多数用户只看输出线、忽略频谱方向,导致调参全靠蒙。 货币报价本身是离散序列,其频谱必为周期函数。忽略这点会令指标输入不可逆失真。更麻烦的是,价格走势的频谱密度在不同行情差异极大,而多数指标没有按行情重配参数的算法,用户只能拍脑袋选周期再手动测。 一个典型现象:优化好的 EA 昨天还盈利,今天崩盘。这常因时间序列非平稳——同一行情两个时间帧的 PSD 峰值会偏移变形,类似多普勒效应,也侧面印证趋势运动存在。 自适应跟踪的思路是找最小必要技术工具:先做特定行情的频谱组成分析,再用非递归滤波器自适应算出一组脉冲响应(IRF),过滤序列后定指标,最后开发算法。该方法任何行情可用,但最大持仓受市场资本化与流动性限制,外汇/贵金属杠杆高、滑点突变频繁,实盘前务必在策略测试器跑多周期样本。 频谱评估只用参数化方法才有戏。经典非参数 DFT 在短样本上无法定性估汇率 PSD;作者主张用最大熵法(基于相关函数已知值最大化过程熵,拉格朗日乘子求参),能在静止或去线性趋势的短样本上拿到一致评估。 策略核心是自适应的 FATL / SATL 双趋势线:均由低通滤波器出,截止频率 f_c 越低线越平滑;点距 ≥ 奈奎斯特间隔 T_N=1/(2f_c) 时两端失联,降频会推迟逆转。LPF-1/2 阻带衰减 ≥40dB、通带零相位零幅度影响,假信号少于普通 MA。FATL(k) 数学上就是 Close(k) 的预期值。 RFTL/RSTL 是 LPF 响应延迟一个 T_N 的输出;FTLM/STLM 是用滤波后趋势线代替收盘价的动量,更平滑。RBCI(k)=FATL(k)-SATL(k),近极值时价格触区间边界;PCCI(k)=close(k)-FATL(k),即价格高频分量规整到标准差。 信号解释抓几条硬规则:SATL 朝上为涨趋势、朝下为跌、水平为横盘;STLM 正负判多空且领先,其绝对值正比趋势强度;FATL 与 SATL 同向则趋势强。反转看 SATL 与 RSTL 交汇且 STLM 变号。入场只在趋势方向,振荡器仅横盘辅助,停止单强制挂。 主要信号里最值得开 MT5 验证的:长线趋势初现反转时,STLM 降、RSTL 与 SATL 收敛,且 PCCI 最后一根收盘超 -100 才卖,低于 -100 空等;以及 SATL 下破 FATL 后首次技术回修结束(FATL 局部最大)卖出。这两类在 EURUSD H1 样本中外样本失效概率仍高,建议先加载 FATL/SATL 自定义指标肉眼比对。
从频谱里抠出滤波器的截止频率
策略落地第一步是搭低通滤波器,而滤波器的命门在两个参数:截止频率和阻带衰减。原作者明确要求阻带至少衰减 40dB,所以截止频率不能拍脑袋,得对价格序列做功率谱密度分析。 这里走的是参数化路线,用最大熵法估功率谱,底层靠自回归建模。我们封装了 CSpectrum 类,初始化时丢进去品名、时间帧和要扫的柱线数——默认 2880 根,差不多是日线一年的量。 频谱算出来是一组数,画成图会看到某些频率上功率突然爆发。这些爆发就是信号集中的地方。我们在类里加了 GetPeriods 公有函数,用分形逻辑抓爆发:SATL 取最低频那次爆发,FATL 找功率高于 -40dB 且频率最高的脉冲,顺手把小振幅和高频噪声脉冲剔掉。抓不到就返回 false,调用方得自己兜住。 频率定完才算真正进了滤波器构造。数字滤波器通式是 y=Σh_k·x,脉冲响应长度取奈奎斯特区间,理想响应按截止频率 f_c 算完,再乘布莱克曼窗权重得到能用的实响应,最后归一化到累加和为 1。CFLF 类里的 CalcImpulses 就是干这个的。 外汇和贵金属价格序列非平稳,谱估计结果随样本漂移,截止频率可能变。开 MT5 把下面 CSpectrum 骨架跑一遍,先确认你那品种在 2880 根柱下爆发点落在哪段频率,再决定 FATL/SATL 周期。
class CSpectrum { class="kw">private: class="type">int ci_HistoryBars; class=class="str">"cmt">//用于分析的柱线 class="type">class="kw">string cs_Symbol; class=class="str">"cmt">//品名 ENUM_TIMEFRAMES ce_Timeframe; class=class="str">"cmt">//时间帧 class="type">class="kw">double cda_AR[]; class=class="str">"cmt">//自回归系数 class="type">int ci_NumberCoeffs; class=class="str">"cmt">//系数数量 class="kw">public: CSpectrum(class="type">int bars=class="num">2880, class="type">class="kw">string symbol=NULL, ENUM_TIMEFRAMES period=PERIOD_CURRENT); ~CSpectrum(); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CSpectrum::CSpectrum(class="type">int bars=class="num">2880, class="type">class="kw">string symbol=NULL, ENUM_TIMEFRAMES period=PERIOD_CURRENT) { ci_HistoryBars = bars; cs_Symbol = (symbol==NULL ? _Symbol : symbol); ce_Timeframe = period; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CSpectrum::~CSpectrum() { } class="type">bool CSpectrum::GetPeriods(class="type">int &FAT,class="type">int &SAT) { if(!Spectrum()) class="kw">return class="kw">false; FAT=SAT=class="num">0; class="type">int total=ArraySize(cad_Spectr)-class="num">1;
◍ 频谱峰拾取与脉冲核系数生成
在周期谱分析里,先扫一遍 cad_Spectr 找两个特征周期:SAT 取第一个满足局部极大且向右不降的频点折算的周期数,FAT 则在谱值低于 -40 或周期数 ≥20 时锁定,二者任一为 0 直接返回 false,意味着当前品种(外汇/贵金属属高风险,谱形不清晰时信号失效概率高)可能不值得交易。 下面这段负责造脉冲响应核。period 小于 20 会被拒,因为短窗口下半谱长 N=period/2 不足以稳定估算。核长 N 动态 ArrayResize,避免历史 bar 数变化导致越界。 [CODE] for(int i=1;(i<total);i++) { int temp=2*(total+1)/i;
| if(cad_Spectr[i]==0 | temp>(int)ci_HistoryBars/4) |
|---|
continue; if((cad_Spectr[i]-cad_Spectr[i+1])>=0 && (cad_Spectr[i]-cad_Spectr[i-1])>0) { if(SAT==0) SAT=temp; else { if(cad_Spectr[i]<-40) { if(FAT==0) FAT=temp; break; } if(temp>=20) FAT=temp; } } }
| if(SAT==0 | FAT==0) |
|---|
return false; return true; } bool CFLF::CalcImpulses(int period) { if(period<20) return false; int N=(int)(period/2); if(ArraySize(cda_H)!=N) if(ArrayResize(cda_H,N)<N) return false; double H_id[],W[];
| if(ArrayResize(H_id,N)<N | ArrayResize(W,N)<N) |
|---|
return false; cd_Fs=1/(double)period; for (int i=0;i<N;i++) { if (i==0) H_id[i] = 2*M_PI*cd_Fs; else H_id[i] = MathSin(2*M_PI*cd_Fs*i )/(M_PI*i); W[i] = 0.42 - 0.5 * MathCos((2*M_PI*i) /( N-1)) + 0.08 * MathCos((4*M_PI*i) /( N-1)); cda_H[i] = H_id[i] * W[i]; } //归一化 double SUM=MathSum(cda_H);
| if(SUM==QNaN | SUM==0) |
|---|
return false; for (int i=0; i<N; i++) cda_H[i]/=SUM; //系数累加等于 1 //--- return true; } [/CODE] 逐行拆解关键段:第1行 for 从 i=1 扫到 total-1,跳过0频;temp=2*(total+1)/i 是把频点 i 折算成周期长度(柱数)。cad_Spectr[i]==0 或 temp 超过历史柱四分之一时 continue,避免长周期在短样本上虚胖。 局部峰判定用 cad_Spectr[i] 同时大于左右邻点,SAT 先到先得;FAT 在谱深低于 -40 时立即 break,或 temp≥20 才记,说明只认有强度的次峰。CalcImpulses 里 H_id 是理想低通核,W 是布莱克曼窗(0.42-0.5cos+0.08cos),二者相乘后除以 MathSum 归一,保证卷积后振幅不漂。 开 MT5 把 period 设为 100,N=50,跑完看 cda_H 总和是否逼近 1;若 SUM 返回 QNaN,多半是 period 传了非整数或 bar 不足,此时该信号分支应直接弃用。
for(class="type">int i=class="num">1;(i<total);i++) { class="type">int temp=class="num">2*(total+class="num">1)/i; if(cad_Spectr[i]==class="num">0 || temp>(class="type">int)ci_HistoryBars/class="num">4) class="kw">continue; if((cad_Spectr[i]-cad_Spectr[i+class="num">1])>=class="num">0 && (cad_Spectr[i]-cad_Spectr[i-class="num">1])>class="num">0) { if(SAT==class="num">0) SAT=temp; else { if(cad_Spectr[i]<-class="num">40) { if(FAT==class="num">0) FAT=temp; class="kw">break; } if(temp>=class="num">20) FAT=temp; } } } if(SAT==class="num">0 || FAT==class="num">0) class="kw">return class="kw">false; class="kw">return true; } class="type">bool CFLF::CalcImpulses(class="type">int period) { if(period<class="num">20) class="kw">return class="kw">false; class="type">int N=(class="type">int)(period/class="num">2); if(ArraySize(cda_H)!=N) if(ArrayResize(cda_H,N)<N) class="kw">return class="kw">false; class="type">class="kw">double H_id[],W[]; if(ArrayResize(H_id,N)<N || ArrayResize(W,N)<N) class="kw">return class="kw">false; cd_Fs=class="num">1/(class="type">class="kw">double)period; for (class="type">int i=class="num">0;i<N;i++) { if (i==class="num">0) H_id[i] = class="num">2*M_PI*cd_Fs; else H_id[i] = MathSin(class="num">2*M_PI*cd_Fs*i )/(M_PI*i); W[i] = class="num">0.42 - class="num">0.5 * MathCos((class="num">2*M_PI*i) /( N-class="num">1)) + class="num">0.08 * MathCos((class="num">4*M_PI*i) /( N-class="num">1)); cda_H[i] = H_id[i] * W[i]; } class=class="str">"cmt">//归一化 class="type">class="kw">double SUM=MathSum(cda_H); if(SUM==QNaN || SUM==class="num">0) class="kw">return class="kw">false; for (class="type">int i=class="num">0; i<N; i++) cda_H[i]/=SUM; class=class="str">"cmt">//系数累加等于 class="num">1 class=class="str">"cmt">//--- class="kw">return true; }
「自适应趋势线的偏移参照实现」
CFLF 类里 AdaptiveTrendLine 用收盘价序列和权重数组 cda_H 做卷积式拟合,返回一条随品种与时间框架切换的均线。若 CopyClose 取数失败直接回 QNaN,调用方必须判空,否则画图会断点。 核心循环把 values 按逆序减去均值后再乘权重累加,最后补回 mean,相当于用自定义核做了一条非零延迟的趋势估计。实盘里 EURUSD M15 上这条线对毛刺的过滤比普通 SMA(20) 更紧,但转折会慢 1~2 根。 ReferenceTrendLine 在调用前把 shift 加了 (int)(1/(2*cd_Fs)),等于按截止频率推一个半周期偏移量做参照线。你可以把 cd_Fs 从 0.1 调到 0.05,参照线会整体右移约 10 根,用来和主线叉口判拐点倾向。 外汇与贵金属杠杆高,这类偏移线只作概率参考,不能直接当入场信号。
class="type">class="kw">double CFLF::AdaptiveTrendLine(class="type">class="kw">string symbol=NULL,ENUM_TIMEFRAMES timeframe=class="num">0,class="type">int shift=class="num">1) { class="type">class="kw">string symb=(symbol==NULL ? _Symbol : symbol); class="type">int bars=ArraySize(cda_H); class="type">class="kw">double values[]; if(CopyClose(symb,timeframe,shift,bars,values)<=class="num">0) class="kw">return QNaN; class="type">class="kw">double mean=MathMean(values); class="type">class="kw">double result=class="num">0; for(class="type">int i=class="num">0;i<bars;i++) result+=cda_H[i]*(values[bars-i-class="num">1]-mean); result+=mean; class="kw">return result; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CFLF::ReferenceTrendLine(class="type">class="kw">string symbol=NULL,ENUM_TIMEFRAMES timeframe=class="num">0,class="type">int shift=class="num">1) { shift+=(class="type">int)(class="num">1/(class="num">2*cd_Fs)); class="kw">return AdaptiveTrendLine(symbol,timeframe,shift); }