黄金新闻过滤EA:用一套“冷处理”方案应对数据行情
引言
市面上绝大多数EA都在努力“预测”行情,或者绞尽脑汁地设计复杂的入场信号。但我在复盘实盘交易记录时发现,真正导致账户大幅回撤的,往往不是策略判断错误,而是那些无法预测的数据瞬间——非农就业数据公布的那一刻,黄金价格可以在几秒钟内跳动几十上百点,点差瞬间扩至平时的数倍。
我翻阅了不少关于EA开发的讨论,发现大多数开发者试图在事件发生时“抓住机会”,但很少有系统性地去思考“如何避开风险”。这套EA的逻辑非常简单粗暴:在不该交易的时候,坚决不交易。
为什么黄金尤其需要“新闻过滤器”
黄金对宏观经济数据的敏感度,几乎超过了所有其他交易品种。国际清算银行(BIS)在2025年12月的季度报告中直接指出,黄金和美股正在呈现“双泡沫”特征,这是至少50年来首次出现的现象。报告中特别提到,散户投资者正大规模涌入黄金ETF,机构投资者却在进行反向操作,这种市场结构本身就是不稳定的。
与此同时,世界黄金协会的数据显示,黄金市场日均交易量高达3610亿美元,OTC市场的流动性规模与美债相当。但流动性在正常状态下是优势,在新闻发布时却可能瞬间枯竭。做市商撤单、点差扩大、价格跳空——这些都是自动化交易的天敌。
基于BIS和世界黄金协会的这两份权威报告,我的判断是:当下的黄金市场,基本面驱动的“脉冲式行情”已经取代了纯粹的技术面趋势。在这种环境下,一个能避开脉冲本身的EA,比一个试图捕捉脉冲的EA更有可能存活下来。
策略核心:“什么都不做”是一种主动选择
这听起来可能有些反直觉。大多数交易者认为,EA的价值在于“发现机会并执行”。但我认为,EA同样有价值的职能,是在市场失序时主动停工。
本EA的策略逻辑分为三层:
第一层:新闻“静默窗口”
EA内置了一个可配置的“黑名单”时间段。用户需要根据自己经纪商的服务器时间,手动设置新闻事件的起止时间(例如NFP通常在每月的第一个周五,虽然日期不固定,但时间窗口是确定的)。
当系统时间进入这个窗口时,EA的
OnTick()函数会直接返回,不再执行任何信号检测和开仓逻辑。这是一个“硬阻断”,不依赖于任何技术指标。第二层:挂单清理
许多EA会使用限价单或止损单来试图捕捉突破行情。但在新闻窗口中,这类挂单最容易被“击穿”后触发,然后价格反向运动,导致止损出局。因此,我在EA中加入了清理挂单的功能——在新闻窗口激活时,自动删除所有属于该EA的挂单。
第三层:现有持仓的处理
对于已经持仓的订单,我做了两种选择:默认情况下,EA不主动平仓,仅通过ATR波动率阈值来防止在极高波动时开新仓。如果用户担心新闻导致反向跳空,可以将止损收紧,或者干脆在新闻前手动平仓。
独家视角:我为什么用“新闻时间”取代“交易时段”
很多EA喜欢做时段过滤——亚洲盘低波动不交易,欧美盘重叠时加大仓位。这是基于历史统计的“平均逻辑”。但黄金市场有一个特征:价格行为的极端值,往往由新闻事件驱动,而非由时段决定。
举个例子,美联储主席在亚洲时段突然发表讲话,或者中东地缘冲突在周末发酵后于周一亚洲早盘爆发,这些事件都会打破“时段规律”。因此,我设计这套EA时,主动放弃了传统的“交易时段过滤器”,而是用一个“新闻静默窗口”替代了它。
这个窗口的优先级,高于任何时段逻辑。在窗口期内,EA的默认状态就是“停机”。这个思路,是我在实际交易中被“新闻打损”多次后总结出来的——与其试图预测市场对数据的反应,不如承认自己无法预测,然后选择回避。
源码:News Filter EA for XAUUSD (MQL4)
``
cpp
//+------------------------------------------------------------------+
//| NewsFilterEA.mq4 |
//| Copyright 2025, FXEAR.com |
//| https://www.fxear.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, FXEAR.com"
#property link "https://www.fxear.com"
#property version "1.00"
#property strict
//+------------------------------------------------------------------+
//| 输入参数 |
//+------------------------------------------------------------------+
// --- 风险管理 ---
input double LotSize = 0.01; // 固定交易手数
input double RiskPercent = 1.5; // 每笔风险占账户百分比(UseAutoLot启用时生效)
input bool UseAutoLot = true; // 启用动态手数计算
input int StopLossPips = 300; // 止损点数
input int TakeProfitPips = 600; // 止盈点数
// --- 策略过滤器 ---
input int FastMAPeriod = 20; // 快速均线周期(趋势判断)
input int SlowMAPeriod = 50; // 慢速均线周期(趋势判断)
input int ATRPeriod = 14; // ATR周期
input int VolatilityFilterMin = 20; // 最小ATR阈值(点数)
input int VolatilityFilterMax = 60; // 最大ATR阈值(点数)
// --- 新闻过滤设置 ---
input bool EnableNewsFilter = true; // 新闻过滤总开关
input int NewsStartHour = 12; // 静默开始小时(经纪商时间)
input int NewsStartMinute = 25; // 静默开始分钟
input int NewsEndHour = 14; // 静默结束小时(经纪商时间)
input int NewsEndMinute = 35; // 静默结束分钟
input bool CancelPendingOnNews = true; // 静默期间删除挂单
// --- 通用设置 ---
input int MagicNumber = 20250607; // EA魔术号
input int Slippage = 30; // 滑点(点数)
input bool UseBreakEven = true; // 启用保本止损
input int BreakEvenPips = 150; // 保本触发点数
//+------------------------------------------------------------------+
//| 全局变量 |
//+------------------------------------------------------------------+
int fastMA, slowMA, atr;
datetime lastBarTime;
double pipSize;
bool isNewsBlackout = false;
//+------------------------------------------------------------------+
//| 初始化函数 |
//+------------------------------------------------------------------+
int OnInit()
{
// 计算当前品种的点值(5位小数经纪商,1点=10个最小变动单位)
pipSize = Point 10;
if(Digits == 3 || Digits == 5) pipSize = Point 10;
else if(Digits == 4) pipSize = Point;
else if(Digits == 2) pipSize = Point 10;
// 创建指标句柄
fastMA = iMA(Symbol(), PERIOD_H1, FastMAPeriod, 0, MODE_SMA, PRICE_CLOSE);
slowMA = iMA(Symbol(), PERIOD_H1, SlowMAPeriod, 0, MODE_SMA, PRICE_CLOSE);
atr = iATR(Symbol(), PERIOD_H1, ATRPeriod);
if(fastMA == INVALID_HANDLE || slowMA == INVALID_HANDLE || atr == INVALID_HANDLE)
{
Print("指标句柄创建失败,错误码:", GetLastError());
return(INIT_FAILED);
}
Print("新闻过滤EA初始化完成,品种:", Symbol(), ",魔术号:", MagicNumber);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| 反初始化函数 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
IndicatorRelease(fastMA);
IndicatorRelease(slowMA);
IndicatorRelease(atr);
}
//+------------------------------------------------------------------+
//| Tick主函数 |
//+------------------------------------------------------------------+
void OnTick()
{
// --- 1. 新闻过滤逻辑(独立于K线,每分钟都检查) ---
if(EnableNewsFilter)
{
isNewsBlackout = IsInNewsWindow();
if(isNewsBlackout)
{
// 静默窗口激活时,如果需要则删除挂单
if(CancelPendingOnNews)
DeletePendingOrders();
// 立即返回,不开新仓
return;
}
}
// --- 2. 新K线检查(避免单根K线内重复开仓) ---
if(Time[0] == lastBarTime) return;
lastBarTime = Time[0];
// --- 3. 获取指标数据 ---
double fastMAVal[1], slowMAVal[1], atrVal[1];
if(CopyBuffer(fastMA, 0, 1, 1, fastMAVal) < 1) return;
if(CopyBuffer(slowMA, 0, 1, 1, slowMAVal) < 1) return;
if(CopyBuffer(atr, 0, 1, 1, atrVal) < 1) return;
double currentATR = atrVal[0];
// --- 4. 波动率过滤(ATR过低或过高都不开仓) ---
if(currentATR < VolatilityFilterMin pipSize || currentATR > VolatilityFilterMax pipSize)
return;
// --- 5. 检查现有持仓 ---
int total = OrdersTotal();
for(int i = total - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderMagicNumber() == MagicNumber && OrderSymbol() == Symbol())
{
// 管理现有持仓(追踪止损与保本)
ManageTrade(OrderTicket(), OrderType(), OrderOpenPrice());
return; // 同一时间只持有一个方向的头寸
}
}
}
// --- 6. 开仓逻辑(仅当无持仓且不在新闻静默期) ---
int direction = 0;
if(fastMAVal[0] > slowMAVal[0]) direction = 1;
else if(fastMAVal[0] < slowMAVal[0]) direction = -1;
if(direction == 1)
OpenBuy();
else if(direction == -1)
OpenSell();
}
//+------------------------------------------------------------------+
//| 检查当前时间是否在新闻静默窗口内 |
//+------------------------------------------------------------------+
bool IsInNewsWindow()
{
datetime now = TimeCurrent();
MqlDateTime today;
TimeToStruct(now, today);
int currentHour = today.hour;
int currentMin = today.min;
int currentTotalMin = currentHour 60 + currentMin;
int startTotalMin = NewsStartHour 60 + NewsStartMinute;
int endTotalMin = NewsEndHour 60 + NewsEndMinute;
if(currentTotalMin >= startTotalMin && currentTotalMin <= endTotalMin)
{
Print("新闻静默窗口已激活,时间:", TimeToString(now));
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| 删除该EA的所有挂单 |
//+------------------------------------------------------------------+
void DeletePendingOrders()
{
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderMagicNumber() == MagicNumber && OrderSymbol() == Symbol())
{
if(OrderType() == OP_BUYLIMIT || OrderType() == OP_SELLLIMIT ||
OrderType() == OP_BUYSTOP || OrderType() == OP_SELLSTOP)
{
bool res = OrderDelete(OrderTicket());
if(res)
Print("新闻静默期已删除挂单:", OrderTicket());
}
}
}
}
}
//+------------------------------------------------------------------+
//| 开多单 |
//+------------------------------------------------------------------+
void OpenBuy()
{
double ask = Ask;
double sl = ask - StopLossPips pipSize;
double tp = ask + TakeProfitPips pipSize;
double lot = CalculateLot();
int ticket = OrderSend(Symbol(), OP_BUY, lot, ask, Slippage, sl, tp, "NewsFilter Buy", MagicNumber, 0, clrGreen);
if(ticket < 0) Print("多单开仓失败,错误码:", GetLastError());
}
//+------------------------------------------------------------------+
//| 开空单 |
//+------------------------------------------------------------------+
void OpenSell()
{
double bid = Bid;
double sl = bid + StopLossPips pipSize;
double tp = bid - TakeProfitPips pipSize;
double lot = CalculateLot();
int ticket = OrderSend(Symbol(), OP_SELL, lot, bid, Slippage, sl, tp, "NewsFilter Sell", MagicNumber, 0, clrRed);
if(ticket < 0) Print("空单开仓失败,错误码:", GetLastError());
}
//+------------------------------------------------------------------+
//| 计算手数(动态/固定) |
//+------------------------------------------------------------------+
double CalculateLot()
{
if(!UseAutoLot)
return LotSize;
double riskAmount = AccountBalance() (RiskPercent / 100.0);
double stopLossDistance = StopLossPips pipSize;
double tickValue = MarketInfo(Symbol(), MODE_TICKVALUE);
double calculatedLot = riskAmount / (stopLossDistance tickValue);
double minLot = MarketInfo(Symbol(), MODE_MINLOT);
double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);
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 ManageTrade(int ticket, int type, double openPrice)
{
// --- 保本逻辑 ---
if(UseBreakEven)
{
double currentPrice = (type == OP_BUY) ? Bid : Ask;
double profitInPips = 0;
if(type == OP_BUY) profitInPips = (currentPrice - openPrice) / pipSize;
else profitInPips = (openPrice - currentPrice) / pipSize;
if(profitInPips >= BreakEvenPips)
{
double newSL = (type == OP_BUY) ? openPrice + (pipSize 5) : openPrice - (pipSize 5);
if(OrderStopLoss() != newSL)
{
OrderModify(ticket, openPrice, newSL, OrderTakeProfit(), 0, clrNONE);
}
return;
}
}
// --- 固定点数追踪止损(为简化逻辑,使用100点追踪) ---
double trailDistance = 100 pipSize;
double currentSL = OrderStopLoss();
double newSL = 0;
if(type == OP_BUY)
{
double newStop = Bid - trailDistance;
if(newStop > openPrice && (currentSL == 0 || newStop > currentSL))
newSL = newStop;
}
else if(type == OP_SELL)
{
double newStop = Ask + trailDistance;
if(newStop < openPrice && (currentSL == 0 || newStop < currentSL))
newSL = newStop;
}
if(newSL != 0 && currentSL != newSL)
{
OrderModify(ticket, openPrice, newSL, OrderTakeProfit(), 0, clrNONE);
}
}
//+------------------------------------------------------------------+
``参考来源
本文首发于FXEAR.com,原创内容,未经授权禁止转载。*
免责声明: 外汇及商品交易具有高风险。本文提供的EA源码仅供教育和研究参考使用,过往表现不代表未来结果。用户需对自身交易决策承担全部责任,并应在模拟账户上充分测试后再应用于实盘账户。新闻过滤功能不能保证完全规避所有风险,重大突发事件可能在预设窗口之外发生。