总算解决了触发问题,是个还很简单粗暴的玩意。之前曾见识过一位高人用这种交易手法做的很不错,所以一直妄想模仿他的逻辑开发EA出来,也许这段历程还需要很久,也许这辈子都做不出来。MQL5源码奉上,请朋友们模拟盘把玩就好,或在此基础上继续开发,切勿实盘赌行情。
//+------------------------------------------------------------------+
//| PriceAction_EA.mq5 |
//| 非农CPI事件驱动锁仓交易法 |
//| 纯价格行为模式 - 全自动 |
//| 开发者:ChinaLingbao@outlook.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026"
#property version "1.10"
#property strict
//+------------------------------------------------------------------+
//| 输入参数 |
//+------------------------------------------------------------------+
input string EventTimeStr = "2026.06.05 15:30"; // 测试时间(服务器) - 改成当前时间+3分钟
input double FixedLotSize = 0.01; // 固定手数
input double TakeProfitRatio = 3.0; // 止盈盈亏比
input int PreEventMinutes = 5; // 提前开仓分钟数
input int PriceActionWaitSeconds = 120; // 价格行为等待秒数(2分钟)
input int PriceActionMinPoints = 500; // 最小波动点数(5美元)
input double VolumeSurgeRatio = 1.0; // 成交量激增倍数
input int RiskPoints = 1000; // 风险点数(1点=0.01美元, 1000点=10美元)
input int Slippage = 100; // 允许滑点
input int MagicNumber = 20260605; // EA魔术号
//+------------------------------------------------------------------+
//| 全局变量 |
//+------------------------------------------------------------------+
datetime eventTime;
datetime preTradeTime;
datetime priceActionDeadline;
datetime decisionTimeOut;
bool tradesOpened = false;
bool decisionMade = false;
int keptDirection = 0;
ulong buyTicket = 0;
ulong sellTicket = 0;
// 新增全局变量 - 记录事件公布时的价格
datetime eventPublishedTime;
double eventStartPrice = 0;
bool priceRecorded = false;
//+------------------------------------------------------------------+
//| 初始化 |
//+------------------------------------------------------------------+
int OnInit()
{
if(!ParseTime(EventTimeStr, eventTime))
{
Print("ERROR: Time format error. Use YYYY.MM.DD HH:MM");
return INIT_FAILED;
}
preTradeTime = eventTime - PreEventMinutes * 60;
priceActionDeadline = eventTime + PriceActionWaitSeconds;
decisionTimeOut = eventTime + 600;
Print("============================================");
Print("PriceAction_EA Loaded");
Print("Event Time: ", TimeToString(eventTime));
Print("Open Time: ", TimeToString(preTradeTime));
Print("Decision Deadline: ", TimeToString(priceActionDeadline));
Print("============================================");
Print("Risk Points: ", RiskPoints, " (", RiskPoints/100.0, " USD)");
Print("Take Profit: ", RiskPoints * TakeProfitRatio, " (", RiskPoints * TakeProfitRatio/100.0, " USD)");
Print("Price Threshold: ", PriceActionMinPoints, " (", PriceActionMinPoints/100.0, " USD)");
Print("Volume Ratio: ", VolumeSurgeRatio);
Print("============================================");
// 重置标志
priceRecorded = false;
eventStartPrice = 0;
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| 反初始化 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
Print("EA Unloaded");
}
//+------------------------------------------------------------------+
//| 主函数 |
//+------------------------------------------------------------------+
void OnTick()
{
datetime now = TimeCurrent();
// 阶段1: 开锁仓
if(!tradesOpened && now >= preTradeTime && now < eventTime)
{
OpenLockPositions();
tradesOpened = true;
Print("[OPEN] Lock positions completed");
}
// 阶段2: 决策
if(tradesOpened && !decisionMade && now >= eventTime)
{
// 第一次进入决策阶段时,记录事件公布时的价格
if(!priceRecorded)
{
eventPublishedTime = now;
MqlTick tick;
if(SymbolInfoTick(_Symbol, tick))
{
eventStartPrice = (tick.ask + tick.bid) / 2;
priceRecorded = true;
Print("[RECORD] Event published at: ", TimeToString(eventPublishedTime), " Start Price: ", eventStartPrice);
}
else
{
Print("[ERROR] Failed to get start price");
}
}
if(now > decisionTimeOut)
{
CloseAllPositions();
decisionMade = true;
Print("[TIMEOUT] Decision timeout, all closed");
return;
}
if(now >= priceActionDeadline)
{
int decision = 0;
string reason = "";
decision = GetPriceActionDecision(reason);
ExecuteDecision(decision, reason);
decisionMade = true;
}
}
// 阶段3: 持仓管理
if(decisionMade && keptDirection != 0)
{
CheckTakeProfit();
ManageTrailingStop();
}
}
//+------------------------------------------------------------------+
//| 开锁仓订单 |
//+------------------------------------------------------------------+
void OpenLockPositions()
{
MqlTick tick;
if(!SymbolInfoTick(_Symbol, tick))
{
Print("[ERROR] Failed to get tick");
return;
}
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(point == 0) point = 0.01;
double riskPoints = RiskPoints;
double tpPoints = riskPoints * TakeProfitRatio;
double tpBuy = tick.ask + tpPoints * point;
double tpSell = tick.bid - tpPoints * point;
buyTicket = OpenOrder(ORDER_TYPE_BUY, FixedLotSize, tick.ask, 0, tpBuy, "Lock Buy");
sellTicket = OpenOrder(ORDER_TYPE_SELL, FixedLotSize, tick.bid, 0, tpSell, "Lock Sell");
if(buyTicket > 0)
Print("[ORDER] Buy: ", buyTicket, " @ ", tick.ask);
if(sellTicket > 0)
Print("[ORDER] Sell: ", sellTicket, " @ ", tick.bid);
}
//+------------------------------------------------------------------+
//| 价格行为决策 - 修复版:使用记录的事件公布价格 |
//+------------------------------------------------------------------+
int GetPriceActionDecision(string &reason)
{
// 获取当前价格
MqlTick tick;
if(!SymbolInfoTick(_Symbol, tick))
{
reason = "Failed to get current tick";
return 0;
}
double currentPrice = (tick.ask + tick.bid) / 2;
// 检查是否已记录公布价格
if(eventStartPrice == 0)
{
reason = "Event start price not recorded";
return 0;
}
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(point == 0) point = 0.01;
// 计算从公布到现在的价格变化(点数)
double priceChange = (currentPrice - eventStartPrice) / point;
// 获取成交量数据
MqlRates rates[];
int copied = CopyRates(_Symbol, PERIOD_M1, 0, 2, rates);
if(copied < 2)
{
reason = "Failed to get volume data";
return 0;
}
long currentVolume = rates[0].tick_volume;
long previousVolume = rates[1].tick_volume;
double volumeRatio = (previousVolume > 0) ? (double)currentVolume / previousVolume : 1.0;
Print("[PA] Start Price: ", eventStartPrice);
Print("[PA] Current Price: ", currentPrice);
Print("[PA] Price Change: ", DoubleToString(priceChange, 0), " points (", DoubleToString(MathAbs(priceChange)/100.0, 2), " USD)");
Print("[PA] Volume Ratio: ", DoubleToString(volumeRatio, 1));
Print("[PA] Threshold: ", PriceActionMinPoints, " points (", PriceActionMinPoints/100.0, " USD)");
// 判断方向
if(MathAbs(priceChange) >= PriceActionMinPoints && volumeRatio >= VolumeSurgeRatio)
{
if(priceChange > 0)
{
reason = StringFormat("Price UP %.0f points + Volume %.1f -> BULLISH", priceChange, volumeRatio);
Print("[DECISION] ✅ BULLISH - Keep LONG");
return 1;
}
else
{
reason = StringFormat("Price DOWN %.0f points + Volume %.1f -> BEARISH", MathAbs(priceChange), volumeRatio);
Print("[DECISION] ✅ BEARISH - Keep SHORT");
return -1;
}
}
reason = StringFormat("Conditions not met: Price Change %.0f (need %d), Volume Ratio %.1f (need %.1f)",
MathAbs(priceChange), PriceActionMinPoints, volumeRatio, VolumeSurgeRatio);
Print("[DECISION] ❌ ", reason);
return 0;
}
//+------------------------------------------------------------------+
//| 执行决策 |
//+------------------------------------------------------------------+
void ExecuteDecision(int decision, string reason)
{
Print("[DECISION] ", reason);
if(decision == 1)
{
CloseOrder(sellTicket);
keptDirection = 1;
Print("[EXECUTE] Keep LONG, closed SHORT");
}
else if(decision == -1)
{
CloseOrder(buyTicket);
keptDirection = -1;
Print("[EXECUTE] Keep SHORT, closed LONG");
}
else
{
CloseOrder(buyTicket);
CloseOrder(sellTicket);
keptDirection = 0;
Print("[EXECUTE] No clear signal, all closed");
}
}
//+------------------------------------------------------------------+
//| 检查止盈 |
//+------------------------------------------------------------------+
void CheckTakeProfit()
{
if(keptDirection == 0) return;
ulong ticket = (keptDirection == 1) ? buyTicket : sellTicket;
if(!PositionSelectByTicket(ticket)) return;
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double currentPrice = (keptDirection == 1) ?
SymbolInfoDouble(_Symbol, SYMBOL_BID) :
SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(point == 0) point = 0.01;
double profitPoints = (keptDirection == 1) ?
(currentPrice - openPrice) / point :
(openPrice - currentPrice) / point;
double targetPoints = RiskPoints * TakeProfitRatio;
if(profitPoints >= targetPoints)
{
CloseOrder(ticket);
Print("[TAKE PROFIT] ", profitPoints, " points");
keptDirection = 0;
}
}
//+------------------------------------------------------------------+
//| 移动止损 - 已添加 IOC 填充模式 |
//+------------------------------------------------------------------+
void ManageTrailingStop()
{
if(keptDirection == 0) return;
ulong ticket = (keptDirection == 1) ? buyTicket : sellTicket;
if(!PositionSelectByTicket(ticket)) return;
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double currentSL = PositionGetDouble(POSITION_SL);
double currentPrice = (keptDirection == 1) ?
SymbolInfoDouble(_Symbol, SYMBOL_BID) :
SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(point == 0) point = 0.01;
double profitPoints = (keptDirection == 1) ?
(currentPrice - openPrice) / point :
(openPrice - currentPrice) / point;
int trailStart = 300;
int trailStep = 50;
if(profitPoints >= trailStart)
{
double newSL;
if(keptDirection == 1)
newSL = currentPrice - trailStep * point;
else
newSL = currentPrice + trailStep * point;
if(newSL > currentSL)
{
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_SLTP;
req.position = ticket;
req.sl = newSL;
req.tp = PositionGetDouble(POSITION_TP);
req.symbol = _Symbol;
req.magic = MagicNumber;
req.type_filling = ORDER_FILLING_IOC;
if(OrderSend(req, res))
{
if(res.retcode == TRADE_RETCODE_DONE)
Print("[TRAIL STOP] Updated to: ", DoubleToString(newSL, _Digits));
else
Print("[TRAIL STOP] Failed, retcode: ", res.retcode);
}
else
{
Print("[TRAIL STOP] OrderSend error: ", GetLastError());
}
}
}
}
//+------------------------------------------------------------------+
//| 全部平仓 |
//+------------------------------------------------------------------+
void CloseAllPositions()
{
CloseOrder(buyTicket);
CloseOrder(sellTicket);
keptDirection = 0;
}
//+------------------------------------------------------------------+
//| 开单函数 - 使用 IOC 填充模式 |
//+------------------------------------------------------------------+
ulong OpenOrder(ENUM_ORDER_TYPE type, double volume, double price, double sl, double tp, string comment)
{
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.symbol = _Symbol;
req.volume = volume;
req.type = type;
req.price = price;
req.sl = sl;
req.tp = tp;
req.deviation = Slippage;
req.magic = MagicNumber;
req.comment = comment;
req.type_filling = ORDER_FILLING_IOC;
if(OrderSend(req, res))
{
if(res.retcode == TRADE_RETCODE_DONE)
return res.order;
else
Print("Open order failed, retcode: ", res.retcode);
}
else
{
Print("Open order failed, error: ", GetLastError());
}
return 0;
}
//+------------------------------------------------------------------+
//| 平仓函数 - 添加 IOC 填充模式 |
//+------------------------------------------------------------------+
void CloseOrder(ulong ticket)
{
if(ticket == 0) return;
if(!PositionSelectByTicket(ticket)) return;
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.symbol = PositionGetString(POSITION_SYMBOL);
req.volume = PositionGetDouble(POSITION_VOLUME);
req.deviation = Slippage;
req.position = ticket;
req.type_filling = ORDER_FILLING_IOC;
ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
if(posType == POSITION_TYPE_BUY)
{
req.type = ORDER_TYPE_SELL;
req.price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
}
else
{
req.type = ORDER_TYPE_BUY;
req.price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
}
if(OrderSend(req, res))
{
if(res.retcode == TRADE_RETCODE_DONE)
Print("[CLOSE] Order ", ticket, " closed");
else
Print("[CLOSE] Failed, retcode: ", res.retcode);
}
else
{
Print("[CLOSE] OrderSend error: ", GetLastError());
}
}
//+------------------------------------------------------------------+
//| 解析时间 |
//+------------------------------------------------------------------+
bool ParseTime(string str, datetime &out)
{
if(StringLen(str) < 16) return false;
int year = (int)StringToInteger(StringSubstr(str, 0, 4));
int mon = (int)StringToInteger(StringSubstr(str, 5, 2));
int day = (int)StringToInteger(StringSubstr(str, 8, 2));
int hour = (int)StringToInteger(StringSubstr(str, 11, 2));
int min = (int)StringToInteger(StringSubstr(str, 14, 2));
if(year < 2000 || mon < 1 || mon > 12 || day < 1 || day > 31) return false;
if(hour < 0 || hour > 23 || min < 0 || min > 59) return false;
MqlDateTime dt = {0};
dt.year = year;
dt.mon = mon;
dt.day = day;
dt.hour = hour;
dt.min = min;
dt.sec = 0;
out = StructToTime(dt);
return true;
}
//+------------------------------------------------------------------+ |