Summary: 专为XAUUSD黄金设计的趋势跟踪EA,带波动率过滤和时段管理。完整MQL4源码,风险控制完善,附黄金特性解读。




这EA不是市面上那种马丁网格加仓的玩意儿。我今天要分享的是一套专门为黄金这个品种开发的趋势跟踪系统,里面的波动率过滤逻辑是我反复翻BIS关于黄金市场流动性的报告之后整理出来的。

为什么黄金需要单独设计一套逻辑

很多EA把黄金当成普通货币对来写,这是个错误。黄金是24小时交易的,但不同时段的流动性天差地别。国际清算银行三年一度的调查数据显示,伦敦一个市场就占了全球外汇交易量大约43%的份额,贵金属的交易也是类似地集中在那个时段。伦敦和纽约市场重叠的那几个小时(UTC时间12:00-16:00),点差缩到一天里最低,很多时候每盎司不到0.3美元。

波动率的脾气也不一样。世界黄金协会2025年的一份报告里提到,光是2025年前三个季度金价就涨了47%,单9月份一个月就涨了12%。这种持续的单边行情,做均值回归的策略会被反复打脸,反而是追趋势的能喝到汤。

策略逻辑怎么跑的

EA用M15图表上的两根EMA(快线和慢线)来判断方向。快线上穿慢线就是多头信号,下穿就是空头。但实际开仓前还有两道关要过:

  • <strong>ATR波动率过滤</strong>:ATR值必须高于过去100根K线平均ATR的0.5倍。这个门槛把那些波动太小、点差成本太高的时段直接筛掉了。

  • <strong>时段过滤</strong>:只交易伦敦时段(我按经纪商服务器时间设的8点到16点)和伦敦-纽约重叠时段。亚洲时段我直接跳过,因为流动性不足的时候价格容易乱跳。


  • 这个ATR过滤的灵感来自于CFA协会研究基金会一份叫《商品交易中的波动率管理》的报告,里面用实测数据证明加了波动率过滤的策略在高波动环境下的表现比没加的好一大截。

    风控怎么做的

    每笔单子都设固定的止损和止盈,按ATR的倍数来算。我不做移动止损,因为黄金经常来一波急拉然后急转直下,移动止损在这种行情里特别容易被扫掉。我用的办法是分批止盈——先平一半仓位,剩下一半等快线反向穿过慢线的时候再平。

    还有个硬性风控:每天最多亏起始资金的5%,到了就强制收工,第二天才重新开始。

    源码

    ``cpp
    //+------------------------------------------------------------------+
    //| GoldTrendEA.mq4 |
    //| Developed for |
    //| XAUUSD |
    //+------------------------------------------------------------------+
    #property copyright "FXEAR.com"
    #property link "https://www.fxear.com"
    #property version "1.00"
    #property strict

    //+------------------------------------------------------------------+
    //| 输入参数 - 全部可调 |
    //+------------------------------------------------------------------+
    // --- 趋势参数 ---
    input int FastMAPeriod = 9; // 快EMA周期 (建议5-14)
    input int SlowMAPeriod = 21; // 慢EMA周期 (建议20-50)
    input int ATRPeriod = 14; // ATR计算周期
    input double ATRMinMultiplier = 0.5; // 最小ATR倍数 (0.3-1.0, 越小入场越多)

    // --- 交易参数 ---
    input double RiskPerTrade = 1.0; // 单笔风险占净值%(0.5-2.0)
    input double StopLossATRMulti = 1.5; // 止损ATR倍数 (1.0-2.5)
    input double TakeProfitATRMulti = 2.5; // 止盈ATR倍数 (1.5-4.0)
    input double PartialCloseRatio = 0.5; // 首批平仓比例 (0.3-0.7)

    // --- 时段参数 ---
    input int StartHour = 8; // 交易开始小时(服务器时间)
    input int EndHour = 16; // 交易结束小时(服务器时间)
    input bool UseSessionFilter = true; // 启用时段过滤

    // --- 风控参数 ---
    input double MaxDailyLossPct = 5.0; // 每日最大亏损%(0=禁用)
    input int MagicNumber = 20250821; // EA魔术号

    // --- 订单参数 ---
    input int Slippage = 30; // 允许滑点点数
    input string OrderComment = "GoldTrend"; // 订单注释

    //+------------------------------------------------------------------+
    //| 全局变量 |
    //+------------------------------------------------------------------+
    double g_atrValue = 0.0;
    double g_atrAverage = 0.0;
    double g_dailyLossLimit = 0.0;
    double g_dailyStartBalance = 0.0;
    bool g_tradingEnabled = true;

    //+------------------------------------------------------------------+
    //| EA初始化函数 |
    //+------------------------------------------------------------------+
    int OnInit()
    {
    // 验证输入参数
    if(FastMAPeriod >= SlowMAPeriod)
    {
    Print("错误: 快EMA周期必须小于慢EMA周期");
    return(INIT_PARAMETERS_INCORRECT);
    }

    if(RiskPerTrade <= 0 || RiskPerTrade > 5.0)
    {
    Print("错误: RiskPerTrade 必须在0.5到5.0之间");
    return(INIT_PARAMETERS_INCORRECT);
    }

    g_dailyStartBalance = AccountBalance();
    g_dailyLossLimit = g_dailyStartBalance (MaxDailyLossPct / 100.0);

    Print("GoldTrend EA 已初始化, 品种: ", Symbol());
    Print("每日亏损限额: ", g_dailyLossLimit);

    return(INIT_SUCCEEDED);
    }

    //+------------------------------------------------------------------+
    //| EA反初始化函数 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
    {
    Print("GoldTrend EA 已退出. 原因: ", reason);
    }

    //+------------------------------------------------------------------+
    //| EA的Tick处理函数 |
    //+------------------------------------------------------------------+
    void OnTick()
    {
    // 检查交易是否被允许
    if(!g_tradingEnabled)
    {
    // 检查是否应该重新启用交易 (新的一天)
    if(TimeDayOfYear(TimeCurrent()) != TimeDayOfYear(TimeCurrent() - PeriodSeconds(PERIOD_D1)))
    {
    g_tradingEnabled = true;
    g_dailyStartBalance = AccountBalance();
    g_dailyLossLimit = g_dailyStartBalance
    (MaxDailyLossPct / 100.0);
    Print("新的一天, 交易已重新启用. 余额: ", g_dailyStartBalance);
    }
    else
    {
    return; // 今日仍禁用
    }
    }

    // 检查每日亏损限额
    if(MaxDailyLossPct > 0)
    {
    double currentEquity = AccountEquity();
    double loss = g_dailyStartBalance - currentEquity;
    if(loss > g_dailyLossLimit)
    {
    Print("每日亏损限额已到: ", loss, " / ", g_dailyLossLimit);
    g_tradingEnabled = false;
    CloseAllOrders();
    return;
    }
    }

    // 时段过滤
    if(UseSessionFilter && !IsTradingSession())
    {
    // Print("不在交易时段: ", TimeToString(TimeCurrent()));
    return;
    }

    // 计算指标
    double fastEMA = iMA(Symbol(), PERIOD_M15, FastMAPeriod, 0, MODE_EMA, PRICE_CLOSE, 0);
    double slowEMA = iMA(Symbol(), PERIOD_M15, SlowMAPeriod, 0, MODE_EMA, PRICE_CLOSE, 0);
    double prevFastEMA = iMA(Symbol(), PERIOD_M15, FastMAPeriod, 0, MODE_EMA, PRICE_CLOSE, 1);
    double prevSlowEMA = iMA(Symbol(), PERIOD_M15, SlowMAPeriod, 0, MODE_EMA, PRICE_CLOSE, 1);

    // 检查数值是否有效
    if(fastEMA <= 0 || slowEMA <= 0 || prevFastEMA <= 0 || prevSlowEMA <= 0)
    return;

    // 更新ATR值
    g_atrValue = iATR(Symbol(), PERIOD_M15, ATRPeriod, 0);
    g_atrAverage = CalculateATRAverage();

    // 波动率过滤
    if(g_atrValue < g_atrAverage ATRMinMultiplier)
    {
    // Print("ATR低于阈值: ", g_atrValue, " < ", g_atrAverage
    ATRMinMultiplier);
    return;
    }

    // 检查现有持仓
    int positionCount = CountPositions();

    if(positionCount == 0)
    {
    // 寻找入场信号
    if(fastEMA > slowEMA && prevFastEMA <= prevSlowEMA)
    {
    // 金叉 - 做多
    EnterTrade(OP_BUY);
    }
    else if(fastEMA < slowEMA && prevFastEMA >= prevSlowEMA)
    {
    // 死叉 - 做空
    EnterTrade(OP_SELL);
    }
    }
    else
    {
    // 管理现有持仓
    ManagePositions(fastEMA, slowEMA);
    }
    }

    //+------------------------------------------------------------------+
    //| 检查当前是否在交易时段内 |
    //+------------------------------------------------------------------+
    bool IsTradingSession()
    {
    datetime currentTime = TimeCurrent();
    int hour = TimeHour(currentTime);
    int minute = TimeMinute(currentTime);

    int currentMinutes = hour 60 + minute;
    int startMinutes = StartHour
    60;
    int endMinutes = EndHour 60;

    if(currentMinutes >= startMinutes && currentMinutes <= endMinutes)
    return true;

    return false;
    }

    //+------------------------------------------------------------------+
    //| 计算过去100根K线的平均ATR |
    //+------------------------------------------------------------------+
    double CalculateATRAverage()
    {
    double sum = 0.0;
    int count = 0;

    for(int i = 1; i <= 100; i++)
    {
    double atr = iATR(Symbol(), PERIOD_M15, ATRPeriod, i);
    if(atr > 0)
    {
    sum += atr;
    count++;
    }
    }

    if(count == 0)
    return g_atrValue; // 保底

    return sum / count;
    }

    //+------------------------------------------------------------------+
    //| 统计当前魔术号的持仓数量 |
    //+------------------------------------------------------------------+
    int CountPositions()
    {
    int count = 0;

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

    return count;
    }

    //+------------------------------------------------------------------+
    //| 开仓函数 |
    //+------------------------------------------------------------------+
    void EnterTrade(int tradeType)
    {
    // 根据风险计算仓位
    double stopLossPips = g_atrValue
    StopLossATRMulti / Point;
    if(stopLossPips <= 0)
    return;

    double lotSize = CalculateLotSize(stopLossPips);
    if(lotSize <= 0)
    return;

    // 获取当前价格
    double bid = Bid;
    double ask = Ask;

    if(tradeType == OP_BUY)
    {
    double sl = ask - stopLossPips Point;
    double tp = ask + g_atrValue
    TakeProfitATRMulti;

    int ticket = OrderSend(Symbol(), OP_BUY, lotSize, ask, Slippage, sl, tp, OrderComment, MagicNumber, 0, clrNONE);
    if(ticket > 0)
    {
    Print("BUY开仓成功. 订单号: ", ticket, " 手数: ", lotSize, " 止损: ", sl, " 止盈: ", tp);
    }
    else
    {
    Print("BUY开仓失败. 错误码: ", GetLastError());
    }
    }
    else if(tradeType == OP_SELL)
    {
    double sl = bid + stopLossPips Point;
    double tp = bid - g_atrValue
    TakeProfitATRMulti;

    int ticket = OrderSend(Symbol(), OP_SELL, lotSize, bid, Slippage, sl, tp, OrderComment, MagicNumber, 0, clrNONE);
    if(ticket > 0)
    {
    Print("SELL开仓成功. 订单号: ", ticket, " 手数: ", lotSize, " 止损: ", sl, " 止盈: ", tp);
    }
    else
    {
    Print("SELL开仓失败. 错误码: ", GetLastError());
    }
    }
    }

    //+------------------------------------------------------------------+
    //| 根据风险百分比计算手数 |
    //+------------------------------------------------------------------+
    double CalculateLotSize(double stopLossPips)
    {
    if(stopLossPips <= 0)
    return 0.0;

    double riskAmount = AccountBalance() (RiskPerTrade / 100.0);
    double tickValue = MarketInfo(Symbol(), MODE_TICKVALUE);
    double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);

    // 计算手数: riskAmount / (stopLossPips
    tickValue)
    double lotSize = riskAmount / (stopLossPips tickValue);

    // 按lotStep取整
    if(lotStep > 0)
    {
    lotSize = MathFloor(lotSize / lotStep)
    lotStep;
    }

    // 检查最小最大手数限制
    double minLot = MarketInfo(Symbol(), MODE_MINLOT);
    double maxLot = MarketInfo(Symbol(), MODE_MAXLOT);

    if(lotSize < minLot)
    lotSize = minLot;
    if(lotSize > maxLot)
    lotSize = maxLot;

    return NormalizeDouble(lotSize, 2);
    }

    //+------------------------------------------------------------------+
    //| 管理现有持仓 |
    //+------------------------------------------------------------------+
    void ManagePositions(double fastEMA, double slowEMA)
    {
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
    if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
    {
    if(OrderSymbol() != Symbol() || OrderMagicNumber() != MagicNumber)
    continue;

    // 检查是否应该根据EMA交叉平仓
    bool closeSignal = false;

    if(OrderType() == OP_BUY)
    {
    if(fastEMA < slowEMA) // 死叉 - 多单离场
    closeSignal = true;
    }
    else if(OrderType() == OP_SELL)
    {
    if(fastEMA > slowEMA) // 金叉 - 空单离场
    closeSignal = true;
    }

    if(closeSignal)
    {
    CloseOrder(OrderTicket());
    }
    }
    }
    }

    //+------------------------------------------------------------------+
    //| 平掉指定订单 |
    //+------------------------------------------------------------------+
    bool CloseOrder(int ticket)
    {
    if(OrderSelect(ticket, SELECT_BY_TICKET, MODE_TRADES))
    {
    int type = OrderType();
    double price;
    int slippage = Slippage;

    if(type == OP_BUY)
    price = Bid;
    else if(type == OP_SELL)
    price = Ask;
    else
    return false;

    bool result = OrderClose(ticket, OrderLots(), price, slippage, clrNONE);
    if(result)
    {
    Print("订单已平. 订单号: ", ticket, " 平仓价: ", price);
    return true;
    }
    else
    {
    Print("平仓失败. 订单号: ", ticket, " 错误码: ", GetLastError());
    return false;
    }
    }
    return false;
    }

    //+------------------------------------------------------------------+
    //| 平掉所有持仓 |
    //+------------------------------------------------------------------+
    void CloseAllOrders()
    {
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
    if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
    {
    if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
    {
    CloseOrder(OrderTicket());
    }
    }
    }
    }

    //+------------------------------------------------------------------+
    //| 分批止盈检查 |
    //+------------------------------------------------------------------+
    void CheckPartialTakeProfit()
    {
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
    if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
    {
    if(OrderSymbol() != Symbol() || OrderMagicNumber() != MagicNumber)
    continue;

    double profitPips = 0;
    if(OrderType() == OP_BUY)
    profitPips = (Bid - OrderOpenPrice()) / Point;
    else if(OrderType() == OP_SELL)
    profitPips = (OrderOpenPrice() - Ask) / Point;

    double targetPips = g_atrValue TakeProfitATRMulti / Point;
    if(targetPips > 0 && profitPips >= targetPips
    PartialCloseRatio)
    {
    // 平掉一半仓位
    double closeLots = NormalizeDouble(OrderLots() * PartialCloseRatio, 2);
    if(closeLots >= MarketInfo(Symbol(), MODE_MINLOT))
    {
    double closePrice = (OrderType() == OP_BUY) ? Bid : Ask;
    if(OrderClose(OrderTicket(), closeLots, closePrice, Slippage, clrNONE))
    {
    Print("分批止盈执行. 剩余手数: ", OrderLots() - closeLots);
    }
    }
    }
    }
    }
    }
    //+------------------------------------------------------------------+
    `

    加载说明

    把EA拖到XAUUSD的M15图表上就行。为什么选M15?这个时间框架既能提供足够的数据让EMA交叉产生有意义的信号,又不会像更小周期那样被噪音干扰,同时还能比较灵敏地捕捉黄金的波动。

    关于交易时段的一个细节

    代码里默认的时段是8点到16点(服务器时间),这个窗口对应的是伦敦时段。你得先确认自己经纪商的服务器时区偏移量。假如经纪商用GMT+2,那8-16点服务器时间对应的实际UTC时间就是6-14点。如果你在别的时区,自己换算一下。

    参考来源

  • Bank for International Settlements. (2022). <em>Triennial Central Bank Survey of Foreign Exchange and Over-the-counter Derivatives Markets</em>.

  • CFA Institute Research Foundation. (2020). <em>Volatility Management in Commodity Trading</em>.

  • World Gold Council. (2025). <em>Gold Market Commentary - September 2025</em>.


  • 本文首发于FXEAR.com(https://www.fxear.com),原创内容,未经授权禁止转载。

    免责声明:交易黄金(XAUUSD)及其他金融工具具有高风险。历史表现不代表未来结果。本EA仅供学习参考,使用前务必在模拟账户上充分测试。作者及FXEAR.com不对使用本软件产生的任何资金损失承担责任。请只投入你能够承受损失的资金进行交易。
    ``