在 EA 交易代码中实现指标的计算·进阶篇
◍ 指标基类的缓冲与历史加载骨架
在 MT5 自建指标类时,先得把缓冲区和历史长度这两个底层变量钉死。CIndicator 构造函数里直接把 m_data_len 和 m_history_len 设为 Bars(m_Symbol,m_Timeframe)-1,也就是当前图表可用 K 线数减 1,这一步决定了后续数组下标从 0 开始对齐。 Create() 方法接收 symbol、timeframe、price 三个参数,若 symbol 传 NULL 就退回到 _Symbol。注意里面用 SymbolInfoInteger 检查品种是否被添加到市场报价窗口,没添加就 SymbolSelect 拉一下,失败直接返回 false——很多人回测时品种没勾选,指标悄无声息地不加载,根子在这。 SetBufferSize 只接受正数 bars 来覆写 m_data_len,传 0 或负数是不生效的。这意味着你想动态限制计算长度,必须显式给大于 0 的值,否则类内部始终按构造时的全历史长度跑。 开 MT5 新建一个继承自 CIndicator 的空类,在 OnCreate 里打印 m_data_len,切换不同品种观察返回值差异,能直观验证这套初始化逻辑。
class="kw">virtual class="type">bool SetBufferSize(class="kw">const class="type">int bars); class=class="str">"cmt">//--- 取得指标数据 class="kw">virtual class="type">int CopyBuffer(class="kw">const class="type">uint buffer_num,class="kw">const class="type">uint start, class="kw">const class="type">uint count, class="type">class="kw">double &double_array[]); class="kw">virtual class="type">class="kw">double GetData(class="kw">const class="type">uint buffer_num,class="kw">const class="type">uint shift); class="kw">protected: class="type">class="kw">double m_source_data[]; CArrayBuffer ar_IndBuffers[]; class="type">int m_buffers; class="type">int m_history_len; class="type">int m_data_len; class=class="str">"cmt">//--- class="type">class="kw">string m_Symbol; ENUM_TIMEFRAMES m_Timeframe; ENUM_APPLIED_PRICE m_Price; class=class="str">"cmt">//--- 设置指标的主要配置 class="kw">virtual class="type">bool SetHistoryLen(class="kw">const class="type">int bars=-class="num">1); class=class="str">"cmt">//--- class="kw">virtual class="type">bool LoadHistory(class="type">void); class="kw">virtual class="type">bool Calculate() { class="kw">return true; } }; CIndicator::CIndicator() : m_buffers(class="num">0), m_Symbol(_Symbol), m_Timeframe(PERIOD_CURRENT), m_Price(PRICE_CLOSE), m_last_load(class="num">0) { m_data_len=m_history_len = Bars(m_Symbol,m_Timeframe)-class="num">1; ArrayFree(ar_IndBuffers); ArrayFree(m_source_data); } class="type">bool CIndicator::Create(class="kw">const class="type">class="kw">string symbol=NULL,class="kw">const ENUM_TIMEFRAMES timeframe=class="num">0,class="kw">const ENUM_APPLIED_PRICE price=class="num">1) { m_Symbol=(symbol==NULL ? _Symbol : symbol); if(!SymbolInfoInteger(m_Symbol,SYMBOL_SELECT)) if(!SymbolSelect(m_Symbol,true)) class="kw">return class="kw">false; class=class="str">"cmt">//--- m_Timeframe=timeframe; m_Price=price; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CIndicator::SetBufferSize(class="kw">const class="type">int bars) { if(bars>class="num">0) m_data_len = bars; else
缓冲区与历史数据的初始化分支
自定义指标类在每次计算前要先确认 Bars 数量,若返回 0 或负值说明品种/周期未就绪,此时必须逐一 Shutdown 缓冲区并返回 false,避免后续数组越界。 当 m_history_len 小于当前数据长度时,会调用 SetHistoryLen 把需求长度补齐;随后对每个缓冲区执行 Resize(m_data_len),任一失败即中断。这套逻辑保证了 MT5 重绘或切换品种时,指标内存不会残留旧引用。 LoadHistory 用 SeriesInfoInteger 取最后一根 K 线时间,若 m_last_load 已不早于该时间且源数组够长,直接返回 true 跳过拷贝——这是减少重复 IO 的关键判断。 价格类型分支里,PRICE_MEDIAN 与 PRICE_TYPICAL 走 CopyRates 后手动算 (H+L)/2 与 (H+L+C)/3,而 OPEN/HIGH/LOW 分支原代码漏写 break,可能穿透执行,复制代码时需手动补断点。外汇与贵金属行情跳空频繁,这类历史加载缺陷在实盘可能倾向导致数组错位,开 MT5 跑一遍断点最稳妥。
m_data_len = Bars(m_Symbol,m_Timeframe); class=class="str">"cmt">//--- if(m_data_len<=class="num">0) { for(class="type">int i=class="num">0;i<m_buffers;i++) ar_IndBuffers[i].Shutdown(); class="kw">return class="kw">false; } class=class="str">"cmt">//--- if(m_history_len<m_data_len) if(!SetHistoryLen(m_data_len)) class="kw">return class="kw">false; class=class="str">"cmt">//--- for(class="type">int i=class="num">0;i<m_buffers;i++) { ar_IndBuffers[i].Shutdown(); if(!ar_IndBuffers[i].Resize(m_data_len)) class="kw">return class="kw">false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CIndicator::LoadHistory(class="type">void) { class="type">class="kw">datetime cur_date=(class="type">class="kw">datetime)SeriesInfoInteger(m_Symbol,m_Timeframe,SERIES_LASTBAR_DATE); if(m_last_load>=cur_date && ArraySize(m_source_data)>=m_history_len) class="kw">return true; class=class="str">"cmt">//--- class="type">MqlRates rates[]; class="type">int total=class="num">0,i; class="kw">switch(m_Price) { case PRICE_CLOSE: total=CopyClose(m_Symbol,m_Timeframe,class="num">1,m_history_len,m_source_data); class="kw">break; case PRICE_OPEN: total=CopyOpen(m_Symbol,m_Timeframe,class="num">1,m_history_len,m_source_data); case PRICE_HIGH: total=CopyHigh(m_Symbol,m_Timeframe,class="num">1,m_history_len,m_source_data); case PRICE_LOW: total=CopyLow(m_Symbol,m_Timeframe,class="num">1,m_history_len,m_source_data); case PRICE_MEDIAN: total=CopyRates(m_Symbol,m_Timeframe,class="num">1,m_history_len,rates); if(total!=ArraySize(m_source_data)) total=ArrayResize(m_source_data,total); for(i=class="num">0;i<total;i++) m_source_data[i]=(rates[i].high+rates[i].low)/class="num">2; class="kw">break; case PRICE_TYPICAL: total=CopyRates(m_Symbol,m_Timeframe,class="num">1,m_history_len,rates); if(total!=ArraySize(m_source_data)) total=ArrayResize(m_source_data,total); for(i=class="num">0;i<total;i++) m_source_data[i]=(rates[i].high+rates[i].low+rates[i].close)/class="num">3; class="kw">break; case PRICE_WEIGHTED: total=CopyRates(m_Symbol,m_Timeframe,class="num">1,m_history_len,rates); if(total!=ArraySize(m_source_data))
「把典型价塞进数组再封装成可复用均线类」
这段逻辑先按 K 线数扩容 m_source_data,再把每根 bar 的 (high+low+2*close)/4 写进去。这个加权典型价给收盘价两倍权重,比普通 (H+L+C)/3 更贴收盘价轨迹,在贵金属 1H 上回看 200 根,对突刺噪声的平滑倾向更明显。 ArrayResize 返回的实际长度赋给 total,循环填完直接 break 退出;若 total<=0 直接返回 false,说明拿不到 rates 数据,MT5 里常见原因是品种休市或历史不全。 后面 CIndicator::CopyBuffer 先做 Calculate() 守卫,buffer 序号越界就释放数组并返回 -1,否则转发给 ar_IndBuffers 对应缓冲区。CMA 类继承它,构造时默认 m_Period=25、m_Shift=0、m_Method=MODE_SMA,并固死 m_buffers=1。 开 MT5 新建 EA 把这段抄进去,改 CMA 构造参数里的 25 为 50,加载到 XAUUSD 看缓冲输出差异,外汇与贵金属杠杆高,参数验证请用模拟盘。
total=ArrayResize(m_source_data,total); for(i=class="num">0;i<total;i++) m_source_data[i]=(rates[i].high+rates[i].low+class="num">2*rates[i].close)/class="num">4; class="kw">break; } class=class="str">"cmt">//--- if(total<=class="num">0) class="kw">return class="kw">false; class=class="str">"cmt">//--- m_last_load=cur_date; class="kw">return (total>class="num">0); } class="type">int CIndicator::CopyBuffer(class="kw">const class="type">uint buffer_num,class="kw">const class="type">uint start,class="kw">const class="type">uint count,class="type">class="kw">double &double_array[]) { if(!Calculate()) class="kw">return -class="num">1; class=class="str">"cmt">//--- if((class="type">int)buffer_num>=m_buffers) { ArrayFree(double_array); class="kw">return -class="num">1; } class=class="str">"cmt">//--- class="kw">return ar_IndBuffers[buffer_num].CopyBuffer(start,count,double_array); } class CMA : class="kw">public CIndicator { class="kw">private: class="type">int m_Period; class="type">int m_Shift; ENUM_MA_METHOD m_Method; class="type">class="kw">datetime m_last_calculate; class="kw">public: CMA(); ~CMA(); class="type">bool Create(class="kw">const class="type">class="kw">string symbol, class="kw">const ENUM_TIMEFRAMES timeframe, class="kw">const class="type">int ma_period, class="kw">const class="type">int ma_shift, class="kw">const ENUM_MA_METHOD ma_method, class="kw">const ENUM_APPLIED_PRICE price=PRICE_CLOSE); class="kw">virtual class="type">bool SetBufferSize(class="kw">const class="type">int bars); class="kw">protected: class="kw">virtual class="type">bool Calculate(); class="kw">virtual class="type">class="kw">double CalculateSMA(class="kw">const class="type">int shift); class="kw">virtual class="type">class="kw">double CalculateEMA(class="kw">const class="type">int shift); class="kw">virtual class="type">class="kw">double CalculateLWMA(class="kw">const class="type">int shift); class="kw">virtual class="type">class="kw">double CalculateSMMA(class="kw">const class="type">int shift); }; CMA::CMA() : m_Period(class="num">25), m_Shift(class="num">0), m_Method(MODE_SMA) { m_buffers=class="num">1; ArrayResize(ar_IndBuffers,class="num">1); } class="type">bool CMA::Create(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int ma_period,class="kw">const class="type">int ma_shift,class="kw">const ENUM_MA_METHOD ma_method,class="kw">const ENUM_APPLIED_PRICE price=class="num">1) {
◍ 移动均线类的初始化与增量计算逻辑
自定义均线指标在 MT5 里先走 Create 流程:若底层 CIndicator 句柄建不出来直接返 false;ma_period 若 <=0 也拒建,避免除零和空缓冲。 接着把周期、偏移、平滑方式存进成员,再按 ma_period 开缓冲,历史长度取 2*ma_period 加上正偏移量,给近端重算留余量。 Calculate 里先用 SERIES_LASTBAR_DATE 拿最后 K 线时间,若日期未变且源数据大小已等于 m_history_len,就跳过省算力。 若跨了根新 K 线,用 Bars 算偏移 shift;shift 超过数据长就清空缓冲重来,否则把缓冲 Shift 往前推,只补算新段。 循环里按 m_Method 分流 SMA/EMA/SMMA/LWMA:超出现有总数就 Add,已存在就 Update,保证只动增量、不整段重算。外汇与贵金属波动大,这类自管缓冲在跳空时可能漏根,上 MT5 跑前先打印 m_history_len 核对。
if(!CIndicator::Create(symbol,timeframe,price)) class="kw">return class="kw">false; class=class="str">"cmt">//--- if(ma_period<=class="num">0) class="kw">return class="kw">false; class=class="str">"cmt">//--- m_Period=ma_period; m_Shift=ma_shift; m_Method=ma_method; class=class="str">"cmt">//--- if(!SetBufferSize(ma_period)) class="kw">return class="kw">false; if(!SetHistoryLen(class="num">2*ma_period+(m_Shift>class="num">0 ? m_Shift : class="num">0))) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CMA::Calculate(class="type">void) { class="type">class="kw">datetime cur_date=(class="type">class="kw">datetime)SeriesInfoInteger(m_Symbol,m_Timeframe,SERIES_LASTBAR_DATE); if(m_last_calculate==cur_date && ArraySize(m_source_data)==m_history_len) class="kw">return true; class=class="str">"cmt">//--- if(!LoadHistory()) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="type">int shift=Bars(m_Symbol,m_Timeframe,m_last_calculate,cur_date)-class="num">1; if(shift>m_data_len) { ar_IndBuffers[class="num">0].Initilize(); shift=m_data_len; } else ar_IndBuffers[class="num">0].Shift(shift); class=class="str">"cmt">//--- for(class="type">int i=(m_data_len-shift);i<m_data_len;i++) { class="type">int data_total=ar_IndBuffers[class="num">0].Total(); class="kw">switch(m_Method) { case MODE_SMA: if(i>=data_total) ar_IndBuffers[class="num">0].Add(CalculateSMA(i+m_Shift)); else ar_IndBuffers[class="num">0].Update(i,CalculateSMA(i+m_Shift)); class="kw">break; case MODE_EMA: if(i>=data_total) ar_IndBuffers[class="num">0].Add(CalculateEMA(i+m_Shift)); else ar_IndBuffers[class="num">0].Update(i,CalculateEMA(i+m_Shift)); class="kw">break; case MODE_SMMA: if(i>=data_total) ar_IndBuffers[class="num">0].Add(CalculateSMMA(i+m_Shift)); else ar_IndBuffers[class="num">0].Update(i,CalculateSMMA(i+m_Shift)); class="kw">break; case MODE_LWMA: if(i>=data_total) ar_IndBuffers[class="num">0].Add(CalculateLWMA(i+m_Shift)); else ar_IndBuffers[class="num">0].Update(i,CalculateLWMA(i+m_Shift)); class="kw">break;
收尾时把日期与缓冲长度锁住
指标计算走到末段,先把当前日期写进 m_last_calculate,再把零号缓冲区的元素总数赋给 m_data_len,这两步决定了下一根 K 线进来时是否跳过重复运算。 从代码看,ar_IndBuffers[0].Total() 返回的是已填充的指标缓冲条数,若你在日志里打印这个值,就能确认每天只重算一次而不是每 tick 重算。 最后 return true 代表本次 Calculate 正常结束,MT5 不会因返回值误报初始化失败;想验证可故意把 m_last_calculate 赋值注释掉,观察 CPU 占用是否随 tick 数线性抬升。
} } class=class="str">"cmt">//--- m_last_calculate=cur_date; m_data_len=ar_IndBuffers[class="num">0].Total(); class=class="str">"cmt">//--- class="kw">return true; }