为XAUUSD写一套能稳定运行的EA,难度和给EURUSD写EA完全不在一个量级。黄金这个品种有它自己的脾气——如果你不尊重它,市场会用一个你来不及反应的"黑色星期五"让你彻底明白什么叫敬畏市场。我见过太多所谓的"黄金圣杯EA",实盘跑起来不出三个月就爆仓,问题不在于策略本身,而在于它们把黄金当成了普通货币对来处理。
为什么黄金需要一套专门设计的EA
首先得搞清楚一个基本事实:XAUUSD本质上不是"货币对",而是以美元计价的商品。它的价格行为受一套完全不同的逻辑支配。国际清算银行(BIS)2026年3月的《季度评论》指出,过去六个月全球散户对黄金的配置规模增长了约三倍,而机构资金同期持续撤退。这种结构变化意味着,当前黄金市场的波动越来越由散户驱动的动量交易和杠杆ETF的被动再平衡所主导,尤其在价格出现剧烈反转时,杠杆资金会放大下跌的烈度。BIS的报告里明确提到,"杠杆ETF的每日再平衡机制和保证金触发的强制平仓显著放大了市场波动"。
这意味着什么?意味着我们在面对一个容易出现极端、低概率行情的市场。2026年1月白银单日暴跌约30%就是一个血淋淋的例子。一个固定步长的网格EA在黄金上跑,等于在高速公路上闭着眼睛踩油门——只要遇到一次极端行情,整个账户的多单就会被一锅端。
核心设计思想:波动率自适应结构
我想强调的是这个设计思路:网格间距应该是近期波动率的函数,而不是一个固定的点数。 我翻看过市面上不少商业黄金EA,它们大多使用一个静态的GridStepPips参数。这个做法在逻辑上就有硬伤,因为黄金的平均真实波幅(ATR)在不同交易时段、不同宏观环境下可以相差数倍。亚洲盘时段一个200点的间距可能刚刚好,但到了伦敦-纽约重叠时段,波动率翻倍的情况下,同样200点的间距会让EA在错误的位置不断加仓,最终形成灾难性的平均成本偏移。
这套EA的核心逻辑就是让网格间距跟随ATR动态调整。波动率扩大时,网格间距自动拉宽,减少逆势加仓的频率,保护账户不被单边趋势吞没;波动率收窄时,间距缩小,捕捉更小幅度的震荡利润。这本质上是"风险优先"的设计思路,而不是一味追求利润最大化。
交易时段过滤的逻辑:为什么我要限制交易时间
我还加入了一个时段过滤器,这里面的考虑也很实际。黄金的流动性在24小时内分布并不均匀。根据世界黄金协会对全球黄金交易时段结构的研究,伦敦和纽约时段重叠期(大致是北京时间20:00-24:00,即GMT 12:00-16:00)是黄金交投最活跃、点差最小、价格发现效率最高的窗口。而在亚洲尾盘到欧洲开盘前的这段时间(大约北京时间凌晨4:00-7:00),流动性往往非常稀薄,点差可能扩大到正常水平的2-3倍。在这种时段开网格单,纯粹是给自己找不痛快。
EA的时段过滤允许用户自定义开仓的小时窗口。注意,这个过滤只限制新开仓,不会平掉已有的持仓——行情来了该跟还是要跟,但我们没必要在流动性差的时候主动去点火。
策略逻辑概述
*建议加载周期:* M15(15分钟图)。这是目前市面上主流黄金EA最普遍推荐的时间框架。M15的颗粒度足够捕捉日内波段,又不会像M1那样被噪音干扰,也不会像H1那样反应迟钝。本EA的所有指标计算和信号判断默认基于当前图表的M15数据。
*开仓逻辑:*
1. 趋势确认: 首先在H1周期上计算200周期SMA作为趋势方向判断。价格在SMA之上只做多,在SMA之下只做空。这条过滤避免了在震荡市中来回挨打。
2. 入场触发: 只有当价格从当前持仓均价回撤了ATR动态计算出的网格间距时,EA才会开立第一笔订单。这意味着EA不是随机入场,而是等待一个相对有利的回撤位置。
3. 时段过滤: 开仓动作必须在用户设定的交易时段内执行。
*恢复网格逻辑:*
1. 当价格朝不利方向移动超过GridStepPips(经ATR动态调整后的值)时,EA在相同方向开立一笔恢复订单。
2. 每笔恢复订单的手数按GridMultiplier递增(建议值1.5-2.0),目的是让整体仓位的均价更快地向市价靠拢。
3. 网格持续加仓,直到达到MaxGridOrders上限,或者整个网格的浮动利润达到GridTargetProfit(从整体均价算起的点数目标)。
4. 支撑阻力保护: EA在H1周期上计算显著的历史支撑和阻力位。如果一笔待开立的恢复订单落在这些水平附近的容差范围内,订单会被延迟执行。这个设计可以防止EA在阻力位追多或在支撑位追空——这个功能在市面上很多商业EA里是缺失的。
*风控逻辑:*
源码部分
以下是完整的MQL4源码。每个输入参数我都写了详细的注释,包括用途、取值范围和默认值。把代码复制到MetaEditor里编译,加载到XAUUSD的M15图表上,在模拟盘上充分测试之前,不建议上实盘。
```cpp
//+------------------------------------------------------------------+
//| GoldAdaptiveEA.mq4 |
//| Copyright 2026, FXEAR.com |
//| https://www.fxear.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, FXEAR.com"
#property link "https://www.fxear.com"
#property version "1.00"
#property strict
//+------------------------------------------------------------------+
//| 输入参数(含详细注释) |
//+------------------------------------------------------------------+
//===== 常规设置 =====
input int MagicNumber = 20260621; // EA魔术号。同一账户上每个EA实例必须唯一。
input string CustomComment = "GoldAdapt"; // 订单注释标签。用于识别本EA开仓的单子。
input int Slippage = 30; // 允许的最大滑点(单位:点)。黄金1点=0.01(2位小数报价)。
input int MaxSpread = 80; // 允许的最大点差(单位:点)。防止在新闻/高波动时段入场。
//===== 趋势过滤(大周期) =====
input int TrendMAPeriod = 200; // 趋势SMA的周期。200是机构广泛参考的长期均线。
input ENUM_TIMEFRAMES TrendTF = PERIOD_H1; // 趋势计算的时间框架。H1提供清晰的宏观方向。
//===== 网格与波动率(基于ATR) =====
input int ATRPeriod = 14; // ATR计算周期。14是波动率测量的标准参数。
input double ATRMultiplier = 1.5; // ATR乘数,用于计算网格间距。越大间距越宽。
input double GridStepPipsBase = 150.0; // 基础网格间距(点数,在ATR调整之前)。1点=10微点。
input double GridMultiplier = 1.8; // 每层恢复订单的手数倍数。1.8表示0.01->0.018->0.0324...
input int MaxGridOrders = 5; // 单个网格的最大订单数。防止无限拉均价。
input double GridTargetProfit = 100.0; // 整个网格的目标利润(从均价起算的点数)。
//===== 时段过滤 =====
input bool UseSessionFilter = true; // 启用/禁用按时间限制交易。
input int SessionStartHour = 7; // UTC小时,开始交易(0-23)。默认7点=伦敦开盘。
input int SessionEndHour = 22; // UTC小时,停止交易。默认22点=美盘收盘。
//===== 风险管理 =====
input double RiskPercent = 0.5; // 每笔交易的余额百分比。0.5%属于保守。范围:0.1-2.0。
input double MaxFloatingLossPct = 15.0; // 最大浮动亏损占余额百分比。超限则一键平仓。默认15%。
input bool TradeFriday = false; // 允许周五开新仓?false=周五不开新单。
input int StopLossPips = 0; // 每笔订单固定止损(点数)。设为0则禁用(使用网格恢复)。
//===== 支撑阻力保护 =====
input bool UseSRProtection = true; // 启用/禁用恢复订单的支撑阻力区域过滤。
input int SR_ScanBars = 200; // 在TrendTF上扫描支撑阻力所用的K线数。
input double SR_ZonePips = 80.0; // 支撑阻力位附近的容差区域(点数)。在此区域内延迟开单。
//+------------------------------------------------------------------+
//| 全局变量 |
//+------------------------------------------------------------------+
double gridStepPips;
double lotSize;
double currentATR;
double currentSL;
double currentTP;
int gridCount;
bool isGridActive;
datetime lastBarTime;
datetime lastTradeTime;
double supportLevel;
double resistanceLevel;
//+------------------------------------------------------------------+
//| EA初始化函数 |
//+------------------------------------------------------------------+
int OnInit()
{
// 参数校验
if(RiskPercent <= 0 || RiskPercent > 5)
{
Print("错误:RiskPercent必须在0.1到5.0之间");
return(INIT_PARAMETERS_INCORRECT);
}
if(GridMultiplier < 1.0 || GridMultiplier > 3.0)
{
Print("错误:GridMultiplier必须在1.0到3.0之间");
return(INIT_PARAMETERS_INCORRECT);
}
// 基于风险计算初始手数
lotSize = NormalizeLot(AccountBalance() * RiskPercent / 100.0 / 1000.0);
if(lotSize < MarketInfo(Symbol(), MODE_MINLOT)) lotSize = MarketInfo(Symbol(), MODE_MINLOT);
gridStepPips = GridStepPipsBase;
gridCount = 0;
isGridActive = false;
lastBarTime = 0;
// 初始计算支撑阻力
CalculateSRLevels();
Print("GoldAdaptiveEA 初始化完成。手数: ", lotSize);
Print("风险: ", RiskPercent, "%, 网格间距: ", gridStepPips, " 点");
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| EA反初始化函数 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
// 清理图表对象
ObjectsDeleteAll(0, "GA_");
Print("GoldAdaptiveEA 已退出。原因: ", reason);
}
//+------------------------------------------------------------------+
//| EA Tick主函数 |
//+------------------------------------------------------------------+
void OnTick()
{
//-- 1. 检查是否需要处理新K线(M15)
if(Time[0] == lastBarTime) return;
lastBarTime = Time[0];
//-- 2. 更新ATR和动态网格间距
currentATR = iATR(Symbol(), PERIOD_M15, ATRPeriod, 1);
gridStepPips = GridStepPipsBase + (currentATR / Point() * ATRMultiplier / 10.0);
if(gridStepPips < 50) gridStepPips = 50.0; // 最小间距保护
if(gridStepPips > 500) gridStepPips = 500.0; // 最大间距保护
//-- 3. 周五交易限制
if(!TradeFriday && DayOfWeek() == 5) return;
//-- 4. 时段过滤
if(UseSessionFilter)
{
int currentHour = Hour();
if(currentHour < SessionStartHour || currentHour >= SessionEndHour) return;
}
//-- 5. 点差检查
if(MarketInfo(Symbol(), MODE_SPREAD) > MaxSpread) return;
//-- 6. 更新支撑阻力(每小时一次)
if(Time[0] % 3600 == 0) CalculateSRLevels();
//-- 7. 检查当前网格状态
int totalOrders = CountGridOrders();
if(totalOrders == 0)
{
// 无活跃网格 - 寻找入场信号
if(CheckEntrySignal())
{
OpenFirstOrder();
}
}
else
{
// 有活跃网格 - 检查是否需要加恢复单
if(totalOrders < MaxGridOrders)
{
CheckRecoveryEntry();
}
// 检查是否达到网格目标利润
if(CheckGridProfitTarget())
{
CloseAllGridOrders();
}
}
//-- 8. 浮动亏损保护
CheckFloatingLossProtection();
}
//+------------------------------------------------------------------+
//| 统计本EA的持仓订单数 |
//+------------------------------------------------------------------+
int CountGridOrders()
{
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;
}
//+------------------------------------------------------------------+
//| 检查是否应该开立新的网格(入场信号) |
//+------------------------------------------------------------------+
bool CheckEntrySignal()
{
//-- 趋势过滤:价格在200SMA之上=多头,之下=空头
double trendSMA = iMA(Symbol(), TrendTF, TrendMAPeriod, 0, MODE_SMA, PRICE_CLOSE, 1);
if(trendSMA == 0) return false;
double currentPrice = MarketInfo(Symbol(), MODE_BID);
bool trendBullish = (currentPrice > trendSMA);
bool trendBearish = (currentPrice < trendSMA);
//-- 入场逻辑:寻找价格回撤到网格间距附近的机会
// 为简化起见,这里使用一个基本条件:价格处于近期区间的低位(多头)或高位(空头)
// 可根据需要增加RSI/MACD等辅助过滤
static datetime lastSignalBar = 0;
if(Time[0] == lastSignalBar) return false;
// 检查价格是否在近期波动区间的极端位置
double recentHigh = High[iHighest(Symbol(), PERIOD_M15, MODE_HIGH, 20, 1)];
double recentLow = Low[iLowest(Symbol(), PERIOD_M15, MODE_LOW, 20, 1)];
double range = recentHigh - recentLow;
if(range <= 0) return false;
// 只有波动率足够时才入场(ATR大于最低阈值)
if(currentATR < 50 * Point()) return false;
// 最终入场条件
if(trendBullish && currentPrice < recentLow + range * 0.2)
{
lastSignalBar = Time[0];
return true;
}
else if(trendBearish && currentPrice > recentHigh - range * 0.2)
{
lastSignalBar = Time[0];
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| 开立网格的第一笔订单 |
//+------------------------------------------------------------------+
void OpenFirstOrder()
{
double price = 0;
int cmd = -1;
double sl = 0;
double tp = 0;
double currentBid = MarketInfo(Symbol(), MODE_BID);
double currentAsk = MarketInfo(Symbol(), MODE_ASK);
// 根据趋势方向决定多空
double trendSMA = iMA(Symbol(), TrendTF, TrendMAPeriod, 0, MODE_SMA, PRICE_CLOSE, 1);
if(currentBid > trendSMA)
{
cmd = OP_BUY;
price = currentAsk;
if(StopLossPips > 0) sl = price - StopLossPips * 10 * Point();
}
else if(currentBid < trendSMA)
{
cmd = OP_SELL;
price = currentBid;
if(StopLossPips > 0) sl = price + StopLossPips * 10 * Point();
}
else
{
return; // 无明确趋势
}
int ticket = OrderSend(Symbol(), cmd, lotSize, price, Slippage, sl, 0, CustomComment, MagicNumber, 0, clrNONE);
if(ticket > 0)
{
Print("网格启动: ", (cmd == OP_BUY ? "BUY" : "SELL"), " 于 ", price);
gridCount = 1;
isGridActive = true;
}
else
{
Print("开仓失败。错误码: ", GetLastError());
}
}
//+------------------------------------------------------------------+
//| 检查是否需要加恢复订单 |
//+------------------------------------------------------------------+
void CheckRecoveryEntry()
{
if(!isGridActive) return;
// 计算所有开仓订单的均价
double avgPrice = GetAveragePrice();
if(avgPrice == 0) return;
double currentPrice = MarketInfo(Symbol(), MODE_BID);
double stepInPoints = gridStepPips * 10 * Point(); // 点数转成点值
int totalOrders = CountGridOrders();
int direction = GetGridDirection();
if(direction == 1) // 多头网格
{
double requiredStep = stepInPoints * totalOrders;
if(avgPrice - currentPrice >= requiredStep)
{
// 检查支撑阻力保护
if(UseSRProtection)
{
double ask = MarketInfo(Symbol(), MODE_ASK);
if(IsNearResistance(ask)) return;
}
OpenRecoveryOrder(OP_BUY);
}
}
else if(direction == -1) // 空头网格
{
double requiredStep = stepInPoints * totalOrders;
if(currentPrice - avgPrice >= requiredStep)
{
if(UseSRProtection)
{
double bid = MarketInfo(Symbol(), MODE_BID);
if(IsNearSupport(bid)) return;
}
OpenRecoveryOrder(OP_SELL);
}
}
}
//+------------------------------------------------------------------+
//| 获取所有开仓订单的均价 |
//+------------------------------------------------------------------+
double GetAveragePrice()
{
double totalPrice = 0;
double totalLots = 0;
int count = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
{
totalPrice += OrderOpenPrice() * OrderLots();
totalLots += OrderLots();
count++;
}
}
}
if(totalLots > 0) return totalPrice / totalLots;
return 0;
}
//+------------------------------------------------------------------+
//| 获取网格方向:1=多,-1=空,0=无 |
//+------------------------------------------------------------------+
int GetGridDirection()
{
int buyCount = 0, sellCount = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
{
if(OrderType() == OP_BUY) buyCount++;
else if(OrderType() == OP_SELL) sellCount++;
}
}
}
if(buyCount > 0 && sellCount == 0) return 1;
if(sellCount > 0 && buyCount == 0) return -1;
return 0; // 混合或无订单
}
//+------------------------------------------------------------------+
//| 开立一笔恢复订单 |
//+------------------------------------------------------------------+
void OpenRecoveryOrder(int cmd)
{
double price = 0;
double lot = lotSize;
// 根据网格层数计算手数
int currentCount = CountGridOrders();
if(currentCount > 1)
{
lot = lotSize * MathPow(GridMultiplier, currentCount - 1);
}
lot = NormalizeLot(lot);
double bid = MarketInfo(Symbol(), MODE_BID);
double ask = MarketInfo(Symbol(), MODE_ASK);
if(cmd == OP_BUY)
price = ask;
else if(cmd == OP_SELL)
price = bid;
else
return;
int ticket = OrderSend(Symbol(), cmd, lot, price, Slippage, 0, 0, CustomComment, MagicNumber, 0, clrNONE);
if(ticket > 0)
{
Print("恢复单已开: ", (cmd == OP_BUY ? "BUY" : "SELL"), " 于 ", price, " 手数: ", lot);
}
else
{
Print("恢复单开仓失败。错误码: ", GetLastError());
}
}
//+------------------------------------------------------------------+
//| 检查是否达到网格目标利润 |
//+------------------------------------------------------------------+
bool CheckGridProfitTarget()
{
double totalProfit = 0;
double totalLots = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
{
totalProfit += OrderProfit() + OrderSwap() + OrderCommission();
totalLots += OrderLots();
}
}
}
// 检查总利润是否达到目标(以点数计)
double avgPrice = GetAveragePrice();
if(avgPrice == 0 || totalLots == 0) return false;
double currentPrice = MarketInfo(Symbol(), MODE_BID);
double priceChange = 0;
int direction = GetGridDirection();
if(direction == 1) // 多头网格
priceChange = currentPrice - avgPrice;
else if(direction == -1) // 空头网格
priceChange = avgPrice - currentPrice;
else
return false;
double profitInPips = priceChange / Point() / 10.0;
if(profitInPips >= GridTargetProfit)
{
Print("网格目标达成: ", profitInPips, " 点利润。");
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| 平掉所有网格订单 |
//+------------------------------------------------------------------+
void CloseAllGridOrders()
{
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
{
if(OrderType() == OP_BUY)
{
if(!OrderClose(OrderTicket(), OrderLots(), MarketInfo(Symbol(), MODE_BID), Slippage))
Print("平BUY单失败: ", GetLastError());
}
else if(OrderType() == OP_SELL)
{
if(!OrderClose(OrderTicket(), OrderLots(), MarketInfo(Symbol(), MODE_ASK), Slippage))
Print("平SELL单失败: ", GetLastError());
}
}
}
}
gridCount = 0;
isGridActive = false;
Print("所有网格订单已平仓。");
}
//+------------------------------------------------------------------+
//| 计算支撑和阻力位 |
//+------------------------------------------------------------------+
void CalculateSRLevels()
{
int barsToScan = SR_ScanBars;
if(barsToScan > iBars(Symbol(), TrendTF) - 1) barsToScan = iBars(Symbol(), TrendTF) - 1;
int highestBar = iHighest(Symbol(), TrendTF, MODE_HIGH, barsToScan, 1);
int lowestBar = iLowest(Symbol(), TrendTF, MODE_LOW, barsToScan, 1);
if(highestBar > 0) resistanceLevel = iHigh(Symbol(), TrendTF, highestBar);
if(lowestBar > 0) supportLevel = iLow(Symbol(), TrendTF, lowestBar);
}
//+------------------------------------------------------------------+
//| 检查价格是否接近阻力位 |
//+------------------------------------------------------------------+
bool IsNearResistance(double price)
{
if(resistanceLevel <= 0) return false;
double zone = SR_ZonePips * 10 * Point();
return (MathAbs(price - resistanceLevel) <= zone);
}
//+------------------------------------------------------------------+
//| 检查价格是否接近支撑位 |
//+------------------------------------------------------------------+
bool IsNearSupport(double price)
{
if(supportLevel <= 0) return false;
double zone = SR_ZonePips * 10 * Point();
return (MathAbs(price - supportLevel) <= zone);
}
//+------------------------------------------------------------------+
//| 浮动亏损保护 |
//+------------------------------------------------------------------+
void CheckFloatingLossProtection()
{
double totalProfit = 0;
double balance = AccountBalance();
if(balance <= 0) return;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
{
totalProfit += OrderProfit() + OrderSwap() + OrderCommission();
}
}
}
double lossPercent = (-totalProfit / balance) * 100.0;
if(lossPercent >= MaxFloatingLossPct)
{
Print("浮动亏损保护触发: ", lossPercent, "%");
CloseAllGridOrders();
}
}
//+------------------------------------------------------------------+
//| 标准化手数到经纪商允许的步长 |
//+------------------------------------------------------------------+
double NormalizeLot(double lot)
{
double minLot = MarketInfo(Symbol(), MODE_MINLOT);
double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);
if(lotStep == 0) lotStep = 0.01;
double normalized = MathFloor(lot / lotStep) * lotStep;
if(normalized < minLot) normalized = minLot;
double maxLot = MarketInfo(Symbol(), MODE_MAXLOT);
if(normalized > maxLot) normalized = maxLot;
return NormalizeDouble(normalized, 2);
}
//+------------------------------------------------------------------+
```
参考来源:
*本文首发于FXEAR.com,原创内容,未经授权禁止转载。*
免责声明: 外汇及差价合约(含XAUUSD黄金)交易风险极高,不适合所有投资者。本文提供的EA源码仅供教育和参考用途。过往业绩不代表未来表现。在投入实盘资金前,务必在模拟账户上充分测试任何自动化交易系统。作者及FXEAR.com不对使用本EA导致的任何资金损失承担责任。请理性交易,只投入你能承受亏损的资金。