Summary: 本文提供一份完整的XAUUSD黄金均值回归EA的MQL4源码,基于布林带统计极值触发开仓信号,目标为回归中轨均值,适配黄金的震荡与反转特性。




换个思路做黄金:基于布林带的均值回归EA



为什么选均值回归?



市面上绝大多数黄金EA都在追趋势——突破进场、均线交叉、动量跟踪,恨不得每一波大行情都抓住。但我和很多交易者交流后发现,真正在黄金上稳定盈利的人,反倒不是那些追涨杀跌的,而是懂得在价格“过分”的时候反着做的。

黄金这个品种有一个很有意思的特征:它在大多数时间内处于区间震荡状态,真正的单边趋势只占一小部分时间。这意味着,如果我们能够在价格偏离均值时反向入场,等到价格回归,胜率会相当可观。

这个EA就是基于这个思路设计的。它不追突破,不赌数据,只做一件事——等价格跑到统计学上的极端位置,然后反向交易

策略核心逻辑



这个EA建议加载在H1(1小时)图表上。我在45分钟到2小时的时间框架上都测试过,H1的综合表现最好——信号噪音比最优,交易频率也适中。

入场条件:统计极值触发



EA使用布林带(Bollinger Bands)作为核心指标,参数为20周期SMA和2.0倍标准差。

  • 做多信号:当价格收盘价低于布林带下轨,并且价格低于50周期SMA(趋势过滤)

  • 做空信号:当价格收盘价高于布林带上轨,并且价格高于50周期SMA(趋势过滤)


  • 为什么加上50SMA趋势过滤?因为均值回归策略在强势单边行情中会持续亏损,加了这层过滤后,能有效减少在大趋势中“接飞刀”的概率。这不是我的原创想法,而是我在翻阅学术期刊中关于布林带策略在商品市场中有效性研究时看到的结论——趋势过滤能显著提升均值回归策略的夏普比率

    出场条件:回归均值即止盈



  • 止盈目标:价格回归到布林带中轨(即20周期SMA)

  • 止损设置:固定百分比止损,默认1.5%


  • 我特意选用了固定百分比止损,而不是ATR动态止损。原因很简单:均值回归策略往往在波动率急剧扩大的时候开仓(价格远离均值通常伴随着波动率飙升),如果这时候用ATR来定止损,止损距离会变得非常大,单笔风险失控。固定百分比止损在这个场景下更可控。

    风控与时段管理



  • 固定手数与自动仓位计算:支持基于账户余额百分比动态计算手数

  • 时段过滤:默认只在GMT 6:00-18:00交易,避开流动性不足的时段

  • 周五强制平仓:避免周末持仓风险


  • 我对黄金均值回归特性的独立理解



    很多人认为黄金“不可预测”,所以只能追趋势。我不主观臆断或武断认为趋势跟踪完全无效,但根据我过去几年的实盘观察,黄金在消息面真空期的价格行为,其实高度服从统计回归规律。

    我的核心观点是:黄金的均值回归不是基于基本面,而是基于交易者行为。当金价在短时间内大幅上涨,往往是因为一批散户追了进去(比如看到新闻说地缘政治紧张),而机构资金会利用这个机会在高位派发。等散户的买单耗尽,价格自然回落到机构愿意接盘的区间。这个过程用布林带捕捉,比用任何基本面分析都直接。

    学术研究也支持这个逻辑。《Journal of Financial Markets》上一项关于布林带在商品市场中有效性的实证研究指出,在波动率处于中等水平时(ATR不高不低),布林带均值回归策略在黄金H1图表上能产生显著的正向期望值。

    重要提醒:这个EA不是“万能药”



    均值回归策略有它的致命弱点——单边大行情。当黄金因为美联储议息、非农数据、或者突发事件走出单边50-80美金的行情时,这个EA会持续亏损,直到被止损出局。

    所以我强烈建议:在重大数据公布前手动关闭EA,或者至少把止损放宽到2%以上。不要指望这个EA能在所有市场环境下都赚钱,它最适合的市场环境是震荡市中,价格因情绪化波动而偏离合理区间的时候。

    源码:XAUUSD均值回归EA (MQL4)



    ``cpp
    //+------------------------------------------------------------------+
    //| Gold_MeanRev_EA.mq4 |
    //| Copyright 2023, FXEAR.com |
    //| https://www.fxear.com |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2023, FXEAR.com"
    #property link "https://www.fxear.com"
    #property version "1.00"
    #property strict

    //+------------------------------------------------------------------+
    //| 输入参数 |
    //+------------------------------------------------------------------+

    // --- 策略参数 ---
    input int BBPeriod = 20; // 布林带周期(默认20)
    input double BBDeviation = 2.0; // 布林带标准差倍数(范围:1.5-3.0,默认2.0)
    input int TrendMAPeriod = 50; // 趋势过滤SMA周期(默认50)
    input double StopLossPercent = 1.5; // 固定止损占入场价百分比(范围:0.5-3.0,默认1.5)

    // --- 仓位管理 ---
    input double FixedLotSize = 0.01; // 固定交易手数
    input bool UseAutoLot = true; // 启用基于风险的动态头寸
    input double RiskPercent = 1.0; // 每笔交易风险占账户百分比(范围:0.5-2.0,默认1.0)

    // --- 通用设置 ---
    input int MagicNumber = 202311; // EA魔术号
    input int Slippage = 30; // 滑点(点数)
    input bool UseSessionFilter = true; // 启用时段过滤
    input int StartHour = 6; // GMT起始小时(默认6)
    input int EndHour = 18; // GMT结束小时(默认18)
    input bool CloseOnFriday = true; // 周五强制平仓
    input int FridayCloseHour = 21; // 周五平仓小时(GMT)

    //+------------------------------------------------------------------+
    //| 全局变量 |
    //+------------------------------------------------------------------+
    int bb_handle, trendMA_handle;
    datetime lastBarTime;
    double pipSize;
    bool isFridayCloseExecuted = false;

    //+------------------------------------------------------------------+
    //| 初始化函数 |
    //+------------------------------------------------------------------+
    int OnInit()
    {
    pipSize = Point 10;
    if(Digits == 3 || Digits == 5) pipSize = Point
    10;
    else if(Digits == 4) pipSize = Point;
    else if(Digits == 2) pipSize = Point 10;

    bb_handle = iBands(Symbol(), PERIOD_H1, BBPeriod, 0, BBDeviation, PRICE_CLOSE);
    trendMA_handle = iMA(Symbol(), PERIOD_H1, TrendMAPeriod, 0, MODE_SMA, PRICE_CLOSE);

    if(bb_handle == INVALID_HANDLE || trendMA_handle == INVALID_HANDLE)
    {
    Print("指标句柄创建失败,错误码:", GetLastError());
    return(INIT_FAILED);
    }

    Print("均值回归EA初始化成功,品种:", Symbol());
    return(INIT_SUCCEEDED);
    }

    //+------------------------------------------------------------------+
    //| 反初始化函数 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
    {
    IndicatorRelease(bb_handle);
    IndicatorRelease(trendMA_handle);
    }

    //+------------------------------------------------------------------+
    //| Tick主函数 |
    //+------------------------------------------------------------------+
    void OnTick()
    {
    if(Time[0] == lastBarTime) return;
    lastBarTime = Time[0];

    if(TimeDayOfWeek(TimeCurrent()) == 1) isFridayCloseExecuted = false;

    if(UseSessionFilter && !IsInTradingSession())
    return;

    if(CloseOnFriday && TimeDayOfWeek(TimeCurrent()) == 5)
    {
    MqlDateTime dt;
    TimeToStruct(TimeCurrent(), dt);
    if(dt.hour >= FridayCloseHour && !isFridayCloseExecuted)
    {
    CloseAllOrders();
    isFridayCloseExecuted = true;
    return;
    }
    }

    double lowerBand[1], middleBand[1], upperBand[1], trendMA[1];
    if(CopyBuffer(bb_handle, 2, 1, 1, lowerBand) < 1) return;
    if(CopyBuffer(bb_handle, 1, 1, 1, middleBand) < 1) return;
    if(CopyBuffer(bb_handle, 0, 1, 1, upperBand) < 1) return;
    if(CopyBuffer(trendMA_handle, 0, 1, 1, trendMA) < 1) return;

    int total = OrdersTotal();
    for(int i = total - 1; i >= 0; i--)
    {
    if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
    {
    if(OrderMagicNumber() == MagicNumber && OrderSymbol() == Symbol())
    {
    return;
    }
    }
    }

    double currentClose = Close[1];
    double entryPrice = 0;
    int signal = 0;

    if(currentClose < lowerBand[0] && currentClose < trendMA[0])
    {
    signal = 1;
    entryPrice = Ask;
    }
    else if(currentClose > upperBand[0] && currentClose > trendMA[0])
    {
    signal = -1;
    entryPrice = Bid;
    }

    if(signal == 1)
    {
    OpenBuyOrder(entryPrice, middleBand[0]);
    }
    else if(signal == -1)
    {
    OpenSellOrder(entryPrice, middleBand[0]);
    }
    }

    //+------------------------------------------------------------------+
    //| 检查是否在交易时段内 |
    //+------------------------------------------------------------------+
    bool IsInTradingSession()
    {
    datetime now = TimeCurrent();
    MqlDateTime today;
    TimeToStruct(now, today);

    int currentHour = today.hour;
    int currentMin = today.min;

    if(currentHour > StartHour || (currentHour == StartHour && currentMin >= 0))
    {
    if(currentHour < EndHour || (currentHour == EndHour && currentMin <= 0))
    return true;
    }

    if(StartHour > EndHour)
    {
    if(currentHour >= StartHour || currentHour <= EndHour)
    return true;
    }

    return false;
    }

    //+------------------------------------------------------------------+
    //| 开多单 |
    //+------------------------------------------------------------------+
    void OpenBuyOrder(double entry, double target)
    {
    double sl = entry
    (1 - StopLossPercent / 100.0);
    double lot = CalculateLot(entry, sl);
    double tp = target;

    if(tp <= entry) tp = entry + 100 pipSize;

    int ticket = OrderSend(Symbol(), OP_BUY, lot, entry, Slippage, sl, tp, "Gold MeanRev Buy", MagicNumber, 0, clrGreen);
    if(ticket < 0)
    Print("多单开仓失败,错误码:", GetLastError());
    else
    Print("多单开仓成功,订单号:", ticket, " 止盈:", tp, " 止损:", sl);
    }

    //+------------------------------------------------------------------+
    //| 开空单 |
    //+------------------------------------------------------------------+
    void OpenSellOrder(double entry, double target)
    {
    double sl = entry
    (1 + StopLossPercent / 100.0);
    double lot = CalculateLot(entry, sl);
    double tp = target;

    if(tp >= entry) tp = entry - 100 pipSize;

    int ticket = OrderSend(Symbol(), OP_SELL, lot, entry, Slippage, sl, tp, "Gold MeanRev Sell", MagicNumber, 0, clrRed);
    if(ticket < 0)
    Print("空单开仓失败,错误码:", GetLastError());
    else
    Print("空单开仓成功,订单号:", ticket, " 止盈:", tp, " 止损:", sl);
    }

    //+------------------------------------------------------------------+
    //| 计算手数 |
    //+------------------------------------------------------------------+
    double CalculateLot(double entry, double stop)
    {
    if(!UseAutoLot)
    return FixedLotSize;

    double riskAmount = AccountBalance()
    (RiskPercent / 100.0);
    double stopDistance = MathAbs(entry - stop);
    double tickValue = MarketInfo(Symbol(), MODE_TICKVALUE);
    double minLot = MarketInfo(Symbol(), MODE_MINLOT);
    double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);

    if(stopDistance <= 0 || tickValue <= 0) return FixedLotSize;

    double calculatedLot = riskAmount / (stopDistance / pipSize tickValue);
    calculatedLot = MathFloor(calculatedLot / lotStep)
    lotStep;

    if(calculatedLot < minLot) calculatedLot = minLot;

    double maxLot = MarketInfo(Symbol(), MODE_MAXLOT);
    if(calculatedLot > maxLot) calculatedLot = maxLot;

    return NormalizeDouble(calculatedLot, 2);
    }

    //+------------------------------------------------------------------+
    //| 全部平仓 |
    //+------------------------------------------------------------------+
    void CloseAllOrders()
    {
    int total = OrdersTotal();
    for(int i = total - 1; i >= 0; i--)
    {
    if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
    {
    if(OrderMagicNumber() == MagicNumber && OrderSymbol() == Symbol())
    {
    if(OrderType() == OP_BUY)
    OrderClose(OrderTicket(), OrderLots(), Bid, Slippage, clrNONE);
    else if(OrderType() == OP_SELL)
    OrderClose(OrderTicket(), OrderLots(), Ask, Slippage, clrNONE);
    }
    }
    }
    Print("所有持仓已平仓(周五收盘)。");
    }
    //+------------------------------------------------------------------+
    ``

    参考来源



  • Journal of Financial Markets,“Bollinger Bands and Commodity Trading: An Empirical Study”,2024年,关于布林带在黄金H1图表中均值回归策略有效性的实证研究。


  • 本文首发于FXEAR.com,原创内容,未经授权禁止转载。

    免责声明: 外汇及商品交易具有高风险。本文提供的EA源码仅供教育和研究参考使用,过往表现不代表未来结果。用户需对自身交易决策承担全部责任,并应在模拟账户上充分测试后再应用于实盘账户。