English Русский Deutsch 日本語
preview
构建动态多品种EA(第三部分):均值回归与动量策略

构建动态多品种EA(第三部分):均值回归与动量策略

MetaTrader 5示例 |
43 0
Hlomohang John Borotho
Hlomohang John Borotho

引言

本文将演示如何构建一套适配市场动态的交易逻辑:在趋势延续时避免重复信号,利用统计阈值实现精准入场,并同时对多个品种进行实时规模化交易。交易者最常遇到的问题之一就是信号疲劳:在强势趋势或震荡行情中,系统持续发出同方向入场信号,导致过度交易与无谓亏损。在快速或方向不明的市场中,缺乏行情背景与动量判断的策略很容易被假信号、滞后入场以及高相关性货币对之间不合理的风险分配所拖累。

为解决这些问题,我们提出了一套动态多品种交易框架,在单一智能引擎中融合均值回归动量策略。该系统不再单纯依赖传统指标,而是通过Z‑分数识别具有统计显著性的价格偏离,并结合动量区间阈值来决定入场、观望或重新入场。通过实时追踪交易方向、入场进度及品种专属条件,EA可以根据当前价格走势的完成程度,有效暂停或继续开仓。

本文将逐步讲解这些思路的技术实现,重点介绍如何高效管理多品种逻辑、确保只在最优条件下开仓,并减少由市场噪音驱动的决策。无论您是在构建稳健的EA,还是优化自主交易系统,这套方法都能实现更明智的交易选择、更精准的入场时机,以及更适配现代市场环境的动态风险控制。


EA逻辑

均值回归是一种交易理念,其核心思想是:价格在长期内往往会向其平均值(“均值”)靠拢。该理论认为,无论大幅上涨还是大幅下跌,价格的极端波动都是暂时的,最终会向历史常态水平回归。在金融市场中,这类极端走势通常由短期供需失衡、对消息面的过度反应或流动性冲击所引发。均值回归策略的核心思路,就是利用这些暂时性的价格错位:识别出资产在统计层面处于超买或超卖状态,然后反向开仓,预期价格将向均值回归。

在实现层面,均值回归通常借助Z‑分数等统计工具。Z‑分数以标准差为单位,衡量当前价格偏离移动平均线(MA)的程度。Z‑分数为较大正值时说明价格远高于均值(可能处于超买状态),而Z‑分数为较小负值时则说明价格远低于均值(可能处于超卖状态)。交易者会设定阈值(如±2.0),用于判断偏离程度是否大到值得入场。当价格突破该阈值,且出现动量减弱、反转信号等附加条件时,便可开仓,静待价格向均值回归。该策略在区间震荡等具备均值回归特性的市场中效果最好,但是必须配合严格的风控:一旦遭遇单边趋势行情,回归信号极易失效,需要提前做好防范措施。

动量交易是一种利用现有市场趋势的延续来获取利润的交易策略。其核心思想是:在某一方向上已出现强劲走势的资产,更有可能延续该方向,而非立即反转(回归)。这种市场行为通常由羊群心理、机构资金流向、消息面情绪或技术面突破所驱动。使用动量策略的交易者,会在强势单边行情的初期或中期入场,顺势持仓,直至出现动能衰竭或反转信号。

在实现层面,动量策略通常基于特定周期内的价格变化速率构建。衡量方式包括:简单价格差值(如当前价格与N根K线前价格之差)、动量震荡指标,或用于捕捉价格涨跌速度与加速度的自定义指标。在多品种交易系统中,可对每个品种单独计算动量,并通过阈值判断当前行情强度是否值得入场。一旦动量得到确认,便顺势开仓;只要动量保持强劲且未衰竭,策略可继续加仓持有。


入门指南

//+------------------------------------------------------------------+
//|                                                Dyna Mean&Mom.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Trade/Trade.mqh>
#include <Math/Stat/Math.mqh>
CTrade trade;

与往常一样,我们首先引入EA运行所需的核心类库。本次的不同之处在于,我们额外引入了<Math/Stat/Math.mqh>库。该库为我们提供了强大的统计工具,包括均值、标准差、Z‑分数计算等功能,这些是实现均值回归策略与动量策略的核心基础。

//+------------------------------------------------------------------+
//|  Enhanced Mean-Reversion + Momentum EA                           |
//+------------------------------------------------------------------+

//--- Input settings
input string Symbols = "XAUUSD,GBPUSD,USDCAD,USDJPY";
input int    TakeProfit = 150;        // TP in points
input int    StopLoss = 100;           // SL in points
input int    MAPeriod = 20;
input int    MomentumPeriod = 5;
input double Z_Threshold = 2.0;
input double Mom_Threshold = 1.5;     // Price change in standard deviations
input double RiskPercent_High = 1.5, RiskPercent_Mod = 1.0, RiskPercent_Low = 0.5;

在代码的这一部分,我们为EA定义输入参数。用户可通过这些参数指定交易品种列表(如黄金兑美元、英镑兑美元),设置止盈、止损、风险比例等关键风险管理参数,并通过统计类输入值精细调整策略行为。可调整的参数包括:移动平均周期、动量测算周期、Z‑分数阈值与动量强度阈值。这些输入参数共同作用,让EA能够适配不同市场环境,并在多货币对上实现风险管控。

//--- Global parameters
string symb_List[];
int Num_symbs = 0;

// Indicator handles arrays
int MA_hndl[];
int STDev_hndl[];
int ATR_hndl[];

这里,我们声明贯穿整个EA使用的全局变量。其中包括symb_List[](用于存储交易品种列表的数组)和Num_symbs(用于存储待处理品种总数量)。此外,我们还定义了指标句柄数组:MA_hndl[](移动平均线指标句柄)、STDev_hndl[](标准差指标句柄)和ATR_hndl[](平均真实波幅指标句柄)在运行过程中,这些句柄用于高效管理与访问每个品种的指标数据。

//+------------------------------------------------------------------+
//| Expert initialization                                            |
//+------------------------------------------------------------------+
int OnInit() {
    //--- Split symbol list
    ushort separator = StringGetCharacter(",", 0);
    StringSplit(Symbols, separator, symb_List);
    Num_symbs = ArraySize(symb_List);

    //--- Resize arrays
    ArrayResize(MA_hndl, Num_symbs);
    ArrayResize(STDev_hndl, Num_symbs);
    ArrayResize(ATR_hndl, Num_symbs);

    //--- Prepare each symbol
    for (int i = 0; i < Num_symbs; i++) {
        string symbol = symb_List[i];
        StringTrimLeft(symbol);
        StringTrimRight(symbol);
        
        //--- Create indicator handles
        MA_hndl[i] = iMA(symbol, PERIOD_H1, MAPeriod, 0, MODE_SMA, PRICE_CLOSE);
        STDev_hndl[i] = iStdDev(symbol, PERIOD_H1, MAPeriod, 0, MODE_SMA, PRICE_CLOSE);
        ATR_hndl[i] = iATR(symbol, PERIOD_H1, 14);
        
        if (MA_hndl[i] == INVALID_HANDLE || STDev_hndl[i] == INVALID_HANDLE || ATR_hndl[i] == INVALID_HANDLE) {
            Print("Failed to create indicator handles for ", symbol);
            return INIT_FAILED;
        }
    }
    
    //--- Set magic number for trade identification
    trade.SetExpertMagicNumber(54321);
    
    return INIT_SUCCEEDED;
}

在这部分代码中,我们对加载到图表时的EA进行初始化。首先,我们将用户定义的交易品种字符串分割为数组,并统计需要处理的品种总数。随后,调整指标句柄数组的大小,使其与品种数量匹配。针对每一个品种,我们去除多余的空格,并分别为移动平均线(MA)、标准差(STDev)、平均真实波幅(ATR)创建指标句柄,所有指标均统一采用1小时(H1)时间框架计算。若任意指标句柄初始化失败,EA将打印错误信息并终止运行。最后,通过trade.SetExpertMagicNumber(54321)设置唯一的magic数字,用于标识由本EA执行的交易订单。

//+------------------------------------------------------------------+
//|          Mean and Momentum Signal Generator                      |
//+------------------------------------------------------------------+
void MeanAndMomentum(string symbol, int idx) {
    //--- Get current price data
    MqlRates current[];
    if(CopyRates(symbol, PERIOD_H1, 0, 1, current) < 1) return;
    double close = current[0].close;
    
    //--- Get historical price for momentum calculation
    MqlRates historical[];
    if(CopyRates(symbol, PERIOD_H1, MomentumPeriod, 1, historical) < 1) return;
    double histClose = historical[0].close;
    
    //--- Get indicator values
    double ma[1], stddev[1], atr[1];
    if(CopyBuffer(MA_hndl[idx], 0, 0, 1, ma) < 1) return;
    if(CopyBuffer(STDev_hndl[idx], 0, 0, 1, stddev) < 1) return;
    if(CopyBuffer(ATR_hndl[idx], 0, 0, 1, atr) < 1) return;
    
    //--- Calculate metrics
    double momentum = close - histClose;
    double zscore = (stddev[0] > 0) ? (close - ma[0]) / stddev[0] : 0;
    double momThreshold = Mom_Threshold * stddev[0]; // Dynamic momentum threshold
    
    //--- Determine signal type
    int signal = 0;
    double riskPercent = 0;
    
    bool meanReversionLong = (zscore < -Z_Threshold);
    bool meanReversionShort = (zscore > Z_Threshold);
    bool momentumLong = (momentum > momThreshold);
    bool momentumShort = (momentum < -momThreshold);
    
    //--- Signal priority: Momentum > Mean Reversion
    if(momentumLong && meanReversionLong) {
        signal = 1;
        riskPercent = RiskPercent_High; // Strong signal
    }
    else if(momentumShort && meanReversionShort) {
        signal = -1;
        riskPercent = RiskPercent_High;
    }
    else if(momentumLong) {
        signal = 1;
        riskPercent = RiskPercent_Mod;
    }
    else if(momentumShort) {
        signal = -1;
        riskPercent = RiskPercent_Mod;
    }
    else if(meanReversionLong) {
        signal = 1;
        riskPercent = RiskPercent_Low;
    }
    else if(meanReversionShort) {
        signal = -1;
        riskPercent = RiskPercent_Low;
    }
    
    //--- Exit if no signal
    if(signal == 0) return;
    
    //--- Check existing positions
    if(PositionSelect(symbol)) {
        long positionType = PositionGetInteger(POSITION_TYPE);
        if((positionType == POSITION_TYPE_BUY && signal == 1) || 
           (positionType == POSITION_TYPE_SELL && signal == -1)) {
            return; // Already in position in same direction
        }
        else {
            // Close opposite position before opening new one
            trade.PositionClose(symbol);
            Sleep(100); // Allow time for order execution
        }
    }
    
    //--- Calculate position size
    double lotSize = CalculatePositionSize(symbol, riskPercent, atr[0]);
    if(lotSize <= 0) return;
    
    //--- Execute trade
    ExecuteTrade(signal == 1 ? ORDER_TYPE_BUY : ORDER_TYPE_SELL, symbol, lotSize);
}

MeanAndMomentum()函数通过结合均值回归与动量指标,为指定交易品种生成交易信号。函数首先获取最新收盘价(Close),以及“动量周期”(MomentumPeriod)对应的K线之前的历史价格,用于计算动量值。同时,通过已初始化的指标句柄,获取移动平均线、标准差与平均真实波幅数值。基于这些数据,函数计算出Z‑分数(反映当前价格相对于均值的标准差偏离程度)与动量(即价格随时间的变化幅度)。函数还会通过标准差缩放计算动态动量阈值,使系统能够适应市场波动率的变化。

完成指标计算后,函数将判断是否满足交易信号条件。它会检测Z‑分数是否发出均值回归机会信号(价格大幅偏离均值),或动量是否足够强劲以确认趋势行情。信号逻辑采用分层机制:优先选择均值回归与动量方向一致的交易机会,并采用最高的风险比例(RiskPercentage_High)。如果仅满足其中的一个条件,系统则采用较低的风险比例。这种分层设计能帮助EA筛选更高质量的交易机会,并根据信号强度调整持仓敞口。

最后,函数会检查当前品种的现有持仓:如果已存在同方向持仓,则避免重复开仓;如果存在反方向持仓,则先平仓再新建订单。函数随后基于ATR风险模型计算合适的手数,并根据确定的信号方向执行交易。该结构确保交易具备统计优势,能根据波动率合理设置仓位,且不会与现有持仓产生冲突。

//+------------------------------------------------------------------+
//| Calculate position size based on risk and volatility             |
//+------------------------------------------------------------------+
double CalculatePositionSize(string symbol, double riskPercent, double atrValue) {
    double balance = AccountInfoDouble(ACCOUNT_BALANCE);
    double riskAmount = balance * (riskPercent / 100.0);
    
    double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
    double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE_LOSS);
    double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
    
    if(point <= 0 || tickValue <= 0 || tickSize <= 0) {
        Print("Invalid symbol parameters for ", symbol);
        return 0;
    }
    
    // Use ATR-based stop loss
    double slDistance = atrValue * 1.5;
    double lossPerLot = slDistance * (tickValue / tickSize);
    
    if(lossPerLot <= 0) {
        Print("Invalid loss calculation for ", symbol);
        return 0;
    }
    
    double lots = riskAmount / lossPerLot;
    lots = NormalizeLots(symbol, lots);
    
    return lots;
}

CalculatePositionSize()函数根据交易者的账户余额、设定的风险比例,以及由平均真实波幅衡量的当前市场波动率,计算交易的最优下单手数。函数首先计算交易者愿意承担的风险资金(riskAmount),即风险比例乘以账户总余额。接下来,函数获取指定交易品种的核心交易参数,包括点值、跳动点价值、最小报价单位,这些参数是将价格波动转换为资金风险的必要依据。

基于上述参数,函数会将ATR值乘以1.5来估算止损距离,从而为账户留出适配当前市场波动率的缓冲空间。根据该止损距离与品种对应的跳动点价值,计算标准手每单的潜在亏损。接下来,用风险资金除以每标准手的预估亏损,得到目标手数,确保交易在设定的风险承受范围内。最后,函数调用NormalizeLots(),根据交易品种的下单限制对手数进行规范化取整,并返回最终结果。该方法确保每笔交易的仓位同时适配账户资金状况与当前市场波动条件。

//+------------------------------------------------------------------+
//| Normalize lot size to broker requirements                        |
//+------------------------------------------------------------------+
double NormalizeLots(string symbol, double lots) {
    double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
    double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
    double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
    
    if(lotStep > 0) {
        lots = MathRound(lots / lotStep) * lotStep;
    }
    
    lots = MathMax(minLot, MathMin(maxLot, lots));
    return lots;
}

这里,该函数仅根据券商要求对交易手数进行合规化处理。

//+------------------------------------------------------------------+
//| Execute trade with proper risk management                        |
//+------------------------------------------------------------------+
void ExecuteTrade(ENUM_ORDER_TYPE tradeType, string symbol, double lotSize) {
    double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
    double price = (tradeType == ORDER_TYPE_BUY) ? 
                   SymbolInfoDouble(symbol, SYMBOL_ASK) : 
                   SymbolInfoDouble(symbol, SYMBOL_BID);
    
    // Get current ATR for dynamic stop levels
    double atr[1];
    int idx = ArrayPosition(symbol);
    if(idx >= 0 && CopyBuffer(ATR_hndl[idx], 0, 0, 1, atr) > 0) {
        double slDistance = atr[0] * 1.5;
        double tpDistance = atr[0] * 2.5;
        
        double sl = (tradeType == ORDER_TYPE_BUY) ? 
                    price - slDistance : 
                    price + slDistance;
                    
        double tp = (tradeType == ORDER_TYPE_BUY) ? 
                    price + tpDistance : 
                    price - tpDistance;
        
        trade.PositionOpen(symbol, tradeType, lotSize, price, sl, tp, "MR-Mom System");
    }
    else {
        // Fallback to fixed stops if ATR fails
        double sl = (tradeType == ORDER_TYPE_BUY) ? 
                    price - (StopLoss * point) : 
                    price + (StopLoss * point);
                    
        double tp = (tradeType == ORDER_TYPE_BUY) ? 
                    price + (TakeProfit * point) : 
                    price - (TakeProfit * point);
        
        trade.PositionOpen(symbol, tradeType, lotSize, price, sl, tp, "MR-Mom System");
    }
}

ExecuteTrade()函数负责根据交易方向、交易品种和计算好的手数,执行带合理风险控制的交易下单。首先获取当前市场价格:买入订单取卖出价(Ask),卖出订单取买入价(Bid),然后尝试读取该品种最新的ATR数值。如果ATR数据有效,函数将基于它动态计算止损和止盈价位 —— 止损价位为1.5倍ATR,止盈价位为2.5倍ATR,以此确保风险和收益比例适配当前市场波动率。接下来,使用"trade.PositionOpen()"方法执行下单操作,并附带计算好的止损止盈价位与用于识别订单的备注标签。

//+------------------------------------------------------------------+
//| Find symbol position in array                                    |
//+------------------------------------------------------------------+
int ArrayPosition(string symbol) {
    for(int i = 0; i < Num_symbs; i++) {
        if(symb_List[i] == symbol) return i;
    }
    return -1;
}
该函数用于在symb_List数组中查找指定的交易品种,并返回其索引位置,如果未找到,则返回-1。其作用是定位品种在数组中的位置,以便准确调用对应的指标句柄。
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){
   if(isNewBar()){
      for(int i = 0; i < Num_symbs; i++) {
         MeanAndMomentum(symb_List[i], i);
      }
   }
}
最后,OnTick()函数是整个EA的核心驱动,每当收到一个新的市场报价(tick)时就会触发执行。为了避免在每个报价到来时都重复计算,函数会先通过isNewBar()判断是否形成了新的K线,从而确保每根K线只评估一次信号。当确认新K线形成时,函数会遍历所有指定品种,并对每个品种调用MeanAndMomentum(),让EA能够根据最新市场数据,同步评估多品种的交易机会。



回测结果

本次回测采用1小时(H1)时间框架,测试周期为2个月(2025年5月1日 — 2025年6月20日),使用以下输入参数:

  • 止盈点数 = 972
  • 止损点数 = 846
  • 移动平均周期(MA)= 80
  • 动量周期 = 43
  • Z-分数阈值 = 3.0
  • 标准差价格变化 = 4.05
  • 高风险比例 = 9.75%
  • 中等风险比例 = 10.0%
  • 低风险比例 = 4.65%


结论

总而言之,我们设计并搭建了一套动态多品种EA,融合均值回归动量双重交易策略,从而智能适配不断变化的市场环境。通过运用移动平均线、标准差(Z-分数)、基于ATR的波动率测算等统计工具,我们搭建出一套可同时分析多个交易品种、理性制定交易决策的量化系统。系统独立监控每一个交易品种,能够结合价格走势、市场波动率与趋势强弱,生成差异化的交易信号。该策略优先在动量与均值回归信号方向一致的高确定性行情中入场,并采用风险自适应仓位算法,高效管控整体持仓风险。

综上所述,这款EA为搭建基于统计逻辑、具备自适应能力的多货币量化交易体系奠定了坚实基础。模块化结构与分层决策框架,使其具备良好的拓展性与可定制性,无论是剥头皮交易还是波段交易策略,均可灵活适配。结合实时波动率与动量追踪机制,系统的入场信号既具备统计学依据,又能灵敏贴合当下市场状态。在合理优化与持续监控的前提下,该系统有潜力在各类行情环境中,实现风控稳定、表现稳健的交易效果。

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/18037

附加的文件 |
Dyna_MeaniMom.mq5 (10.24 KB)
交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
通过协整股票实现统计套利(第一部分):恩格尔 - 格兰杰检验与约翰森协整检验 通过协整股票实现统计套利(第一部分):恩格尔 - 格兰杰检验与约翰森协整检验
本文旨在以适合交易者且通俗易懂的方式,介绍最常用的协整检验方法,并附带一份解读检验结果的简易指南。恩格尔 - 格兰杰检验与约翰森协整检验,能够识别出具备长期联动关系、且在统计上显著的资产配对或资产组合。约翰森检验尤其适用于包含三种及以上资产的投资组合,因其可一次性测算出所有协整向量的强度。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
混沌优化算法(COA):续篇 混沌优化算法(COA):续篇
我们继续对混沌优化算法进行讲解。本文第二部分将介绍该算法实现的实操细节、测试过程及相关结论。