2026-01-16 11:34:58 +03:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| NeuralNetEA.mq5 |
|
|
|
|
|
//| Copyright 2025, Google Gemini |
|
|
|
|
|
//| https://www.mql5.com |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
#property copyright "Copyright 2025, Google Gemini"
|
|
|
|
|
#property link "https://www.google.com"
|
2026-01-21 13:07:37 +03:00
|
|
|
#property version "3.00"
|
2026-01-16 16:42:10 +03:00
|
|
|
|
2026-01-16 11:34:58 +03:00
|
|
|
#include <Trade\Trade.mqh>
|
|
|
|
|
#include <NeuralNet.mqh>
|
|
|
|
|
|
2026-01-21 13:07:37 +03:00
|
|
|
input string InpModelFile = "PriceActionNN_v3.bin";
|
|
|
|
|
input double InpConfidence = 0.8;
|
|
|
|
|
input double InpLotSize = 0.1;
|
2026-01-16 11:34:58 +03:00
|
|
|
input int InpMagic = 123456;
|
2026-01-25 16:54:03 +00:00
|
|
|
|
|
|
|
|
// --- Model Parameters (Loaded from File) ---
|
|
|
|
|
int gLookback;
|
|
|
|
|
int gAsianStart, gAsianEnd;
|
|
|
|
|
int gLondonStart, gLondonEnd;
|
|
|
|
|
int gNYStart, gNYEnd;
|
|
|
|
|
double gTargetTP_Mult, gTargetSL_Mult;
|
2026-01-16 11:34:58 +03:00
|
|
|
|
|
|
|
|
CTrade Trade;
|
|
|
|
|
CNeuralNet *Net;
|
|
|
|
|
int GlobalTopology[];
|
|
|
|
|
datetime lastBarTime = 0;
|
2026-01-21 13:07:37 +03:00
|
|
|
int handleEMA50, handleEMA200, handleATR_D1;
|
2026-01-16 11:34:58 +03:00
|
|
|
|
2026-01-25 16:54:03 +00:00
|
|
|
// Forward Decl
|
|
|
|
|
bool LoadModelWithHeader(string filename);
|
|
|
|
|
|
2026-01-16 11:34:58 +03:00
|
|
|
int OnInit()
|
|
|
|
|
{
|
|
|
|
|
Trade.SetExpertMagicNumber(InpMagic);
|
2026-01-21 13:07:37 +03:00
|
|
|
handleEMA50 = iMA(_Symbol, _Period, 50, 0, MODE_EMA, PRICE_CLOSE);
|
|
|
|
|
handleEMA200 = iMA(_Symbol, _Period, 200, 0, MODE_EMA, PRICE_CLOSE);
|
|
|
|
|
handleATR_D1 = iATR(_Symbol, PERIOD_D1, 14);
|
|
|
|
|
if(handleEMA50 == INVALID_HANDLE || handleEMA200 == INVALID_HANDLE || handleATR_D1 == INVALID_HANDLE) return(INIT_FAILED);
|
2026-01-16 11:34:58 +03:00
|
|
|
|
2026-01-25 16:54:03 +00:00
|
|
|
if(!LoadModelWithHeader(InpModelFile)) {
|
|
|
|
|
Print("Fatal Error: Could not load model and metadata.");
|
|
|
|
|
return(INIT_FAILED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Print("Model Loaded Successfully.");
|
|
|
|
|
PrintFormat("Config: Lookback=%d, Risk=%.1f/%.1f ATR", gLookback, gTargetSL_Mult, gTargetTP_Mult);
|
2026-01-16 11:34:58 +03:00
|
|
|
|
|
|
|
|
return(INIT_SUCCEEDED);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void OnDeinit(const int reason)
|
|
|
|
|
{
|
|
|
|
|
if(CheckPointer(Net) != POINTER_INVALID) delete Net;
|
2026-01-21 13:07:37 +03:00
|
|
|
IndicatorRelease(handleEMA50);
|
|
|
|
|
IndicatorRelease(handleEMA200);
|
|
|
|
|
IndicatorRelease(handleATR_D1);
|
2026-01-16 11:34:58 +03:00
|
|
|
}
|
|
|
|
|
|
2026-01-21 13:07:37 +03:00
|
|
|
// Optimized GetSession: Only scans necessary period
|
|
|
|
|
void GetSession(MqlRates &rates[], int currentIdx, int startHour, int endHour, double &H, double &L)
|
|
|
|
|
{
|
|
|
|
|
// Need Past Finished Session.
|
|
|
|
|
// currentIdx is index in 'rates'. rates[0] is newest.
|
|
|
|
|
// If current Hour >= endHour, we scan today (until index reaches endHour).
|
|
|
|
|
// If current Hour < endHour, we scan Yesterday.
|
|
|
|
|
|
|
|
|
|
// Simplification for EA:
|
|
|
|
|
// Just scan 24h back from currentIdx, and verify Hour check.
|
|
|
|
|
|
|
|
|
|
H = -1; L = 999999;
|
|
|
|
|
bool found = false;
|
|
|
|
|
|
|
|
|
|
datetime t = rates[currentIdx].time;
|
|
|
|
|
MqlDateTime dt; TimeToStruct(t, dt);
|
|
|
|
|
int curH = dt.hour;
|
|
|
|
|
|
|
|
|
|
int scanStart = 0; // bars back from currentIdx
|
|
|
|
|
|
|
|
|
|
// Logic:
|
|
|
|
|
// If currently In Session or Before Session -> We need YESTERDAY's session.
|
|
|
|
|
// If currently After Session -> We need TODAY's session.
|
|
|
|
|
|
|
|
|
|
bool lookForYesterday = (curH < endHour);
|
|
|
|
|
|
|
|
|
|
// We scan back up to 2880 bars (2 days) to be safe
|
|
|
|
|
for(int k=1; k<2000; k++) {
|
|
|
|
|
int idx = currentIdx + k;
|
|
|
|
|
if(idx >= ArraySize(rates)) break;
|
|
|
|
|
|
|
|
|
|
datetime subT = rates[idx].time;
|
|
|
|
|
MqlDateTime subDt; TimeToStruct(subT, subDt);
|
|
|
|
|
int h = subDt.hour;
|
|
|
|
|
|
|
|
|
|
// Check if this bar belongs to the Target Session
|
|
|
|
|
if(h >= startHour && h < endHour) {
|
|
|
|
|
// Found a bar in session.
|
|
|
|
|
// Is it the RIGHT day?
|
|
|
|
|
// If we lookForYesterday, day_of_year must be != dt.day_of_year
|
|
|
|
|
// (Edge case: New Year)
|
|
|
|
|
|
|
|
|
|
bool isSameDay = (subDt.day_of_year == dt.day_of_year && subDt.year == dt.year);
|
|
|
|
|
|
|
|
|
|
if(lookForYesterday && isSameDay) continue; // Skip today's bars
|
|
|
|
|
|
|
|
|
|
// If we are here, it's valid.
|
|
|
|
|
if(rates[idx].high > H || H == -1) H = rates[idx].high;
|
|
|
|
|
if(rates[idx].low < L) L = rates[idx].low;
|
|
|
|
|
found = true;
|
|
|
|
|
} else {
|
|
|
|
|
// If we were inside the session loop and now we are OUT (h < startHour), we are done?
|
|
|
|
|
// Careful with day boundaries.
|
|
|
|
|
if(found) {
|
|
|
|
|
// We found session bars, now we exited the session window. It's fully scanned.
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(H == -1) { H=rates[currentIdx].high; L=rates[currentIdx].low; } // Fallback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-01-16 11:34:58 +03:00
|
|
|
void OnTick()
|
|
|
|
|
{
|
|
|
|
|
datetime time = iTime(_Symbol, _Period, 0);
|
|
|
|
|
if(time == lastBarTime) return;
|
|
|
|
|
lastBarTime = time;
|
|
|
|
|
|
2026-01-26 17:17:50 +03:00
|
|
|
int count = gLookback + 2000;
|
2026-01-16 11:34:58 +03:00
|
|
|
MqlRates rates[];
|
|
|
|
|
ArraySetAsSeries(rates, true);
|
|
|
|
|
|
2026-01-21 13:07:37 +03:00
|
|
|
double ema50[], ema200[];
|
|
|
|
|
ArraySetAsSeries(ema50, true);
|
|
|
|
|
ArraySetAsSeries(ema200, true);
|
|
|
|
|
|
|
|
|
|
if(CopyRates(_Symbol, _Period, 0, count, rates) < count ||
|
2026-01-25 16:54:03 +00:00
|
|
|
CopyBuffer(handleEMA50, 0, 0, gLookback+1, ema50) < gLookback ||
|
|
|
|
|
CopyBuffer(handleEMA200, 0, 0, gLookback+1, ema200) < gLookback) return;
|
2026-01-21 13:07:37 +03:00
|
|
|
|
|
|
|
|
// Pre-calc sessions for Current Time (Index 0)
|
|
|
|
|
|
|
|
|
|
int featuresPerBar = 17;
|
2026-01-25 16:54:03 +00:00
|
|
|
int inputSize = gLookback * featuresPerBar;
|
2026-01-21 13:07:37 +03:00
|
|
|
double inputs[];
|
2026-01-16 11:34:58 +03:00
|
|
|
ArrayResize(inputs, inputSize);
|
|
|
|
|
|
2026-01-21 13:07:37 +03:00
|
|
|
// Cache to avoid rescanning if we are in same context?
|
|
|
|
|
double prevAH=-1.0;
|
|
|
|
|
double prevAL=0.0, prevLH=0.0, prevLL=0.0, prevNH=0.0, prevNL=0.0;
|
|
|
|
|
int prevHour = -1;
|
|
|
|
|
|
2026-01-25 16:54:03 +00:00
|
|
|
for(int k=0; k<gLookback; k++) {
|
2026-01-21 13:07:37 +03:00
|
|
|
int off = k * featuresPerBar;
|
|
|
|
|
|
|
|
|
|
double o = rates[k].open;
|
|
|
|
|
double c = rates[k].close;
|
|
|
|
|
double h = rates[k].high;
|
|
|
|
|
double l = rates[k].low;
|
|
|
|
|
double pH = rates[k+1].high;
|
|
|
|
|
double pL = rates[k+1].low;
|
|
|
|
|
|
|
|
|
|
inputs[off+0] = (c - o)/o * 1000.0;
|
|
|
|
|
inputs[off+1] = (h - o)/o * 1000.0;
|
|
|
|
|
inputs[off+2] = (l - o)/o * 1000.0;
|
|
|
|
|
inputs[off+3] = ((h - pH)/pH) * 1000.0;
|
|
|
|
|
inputs[off+4] = ((l - pL)/pL) * 1000.0;
|
2026-01-21 15:39:36 +03:00
|
|
|
inputs[off+5] = (rates[k+1].tick_volume - rates[k+2].tick_volume) / (rates[k+2].tick_volume + 1.0) * 10.0; // Completed Bar Volume difference normalized
|
2026-01-21 13:07:37 +03:00
|
|
|
inputs[off+6] = ((c - ema50[k])/ema50[k]) * 1000.0;
|
|
|
|
|
inputs[off+7] = ((c - ema200[k])/ema200[k]) * 1000.0;
|
|
|
|
|
|
|
|
|
|
// Session Logic
|
|
|
|
|
datetime t = rates[k].time;
|
|
|
|
|
MqlDateTime dt; TimeToStruct(t, dt);
|
|
|
|
|
|
|
|
|
|
double aH, aL, lH, lL, nH, nL;
|
|
|
|
|
|
|
|
|
|
// Optimization: If hour is same as prev k (very likely for M1), levels are same.
|
|
|
|
|
if(dt.hour == prevHour && prevAH != -1) {
|
|
|
|
|
aH = prevAH; aL = prevAL;
|
|
|
|
|
lH = prevLH; lL = prevLL;
|
|
|
|
|
nH = prevNH; nL = prevNL;
|
|
|
|
|
} else {
|
2026-01-25 16:54:03 +00:00
|
|
|
GetSession(rates, k, gAsianStart, gAsianEnd, aH, aL);
|
|
|
|
|
GetSession(rates, k, gLondonStart, gLondonEnd, lH, lL);
|
|
|
|
|
GetSession(rates, k, gNYStart, gNYEnd, nH, nL);
|
2026-01-21 13:07:37 +03:00
|
|
|
prevHour = dt.hour;
|
|
|
|
|
prevAH=aH; prevAL=aL; prevLH=lH; prevLL=lL; prevNH=nH; prevNL=nL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inputs[off+8] = ((c - aH)/aH) * 1000.0;
|
|
|
|
|
inputs[off+9] = ((c - aL)/aH) * 1000.0;
|
|
|
|
|
inputs[off+10] = ((c - lH)/lH) * 1000.0;
|
|
|
|
|
inputs[off+11] = ((c - lL)/lH) * 1000.0;
|
|
|
|
|
inputs[off+12] = ((c - nH)/nH) * 1000.0;
|
|
|
|
|
inputs[off+13] = ((c - nL)/nH) * 1000.0;
|
|
|
|
|
|
|
|
|
|
inputs[off+14] = (double)dt.day_of_week / 7.0;
|
|
|
|
|
inputs[off+15] = (double)dt.hour / 24.0;
|
|
|
|
|
inputs[off+16] = (double)dt.min / 60.0;
|
2026-01-16 11:34:58 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Net.FeedForward(inputs);
|
2026-01-21 13:07:37 +03:00
|
|
|
double results[]; Net.GetResults(results);
|
2026-01-16 11:34:58 +03:00
|
|
|
double signal = results[0];
|
|
|
|
|
|
2026-01-21 13:07:37 +03:00
|
|
|
// Get current and previous D1 ATR
|
|
|
|
|
double atrArray[2];
|
|
|
|
|
if(CopyBuffer(handleATR_D1, 0, 0, 2, atrArray) < 2) return;
|
|
|
|
|
|
|
|
|
|
double currentDayATR = atrArray[0];
|
|
|
|
|
double prevDayATR = atrArray[1];
|
|
|
|
|
|
|
|
|
|
// Use larger of current or previous day's ATR
|
2026-01-25 16:54:03 +00:00
|
|
|
// Use larger of current or previous day's ATR
|
|
|
|
|
|
2026-01-26 17:17:50 +03:00
|
|
|
CopyBuffer(handleATR_D1, 0, 0, 2, atrArray);
|
2026-01-25 16:54:03 +00:00
|
|
|
double atrToUse = (atrArray[0] > atrArray[1]) ? atrArray[0] : atrArray[1];
|
|
|
|
|
|
|
|
|
|
double distTP = atrToUse * gTargetTP_Mult;
|
|
|
|
|
double distSL = atrToUse * gTargetSL_Mult;
|
2026-01-21 13:07:37 +03:00
|
|
|
|
|
|
|
|
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
|
|
|
|
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
|
|
|
|
2026-01-25 16:54:03 +00:00
|
|
|
Print("Signal: ", signal, " | ATR: ", DoubleToString(atrToUse, _Digits), " | TP: ", DoubleToString(distTP, _Digits), " SL: ", DoubleToString(distSL, _Digits));
|
2026-01-21 13:07:37 +03:00
|
|
|
|
|
|
|
|
if(signal > InpConfidence && !PositionSelect(_Symbol)) {
|
2026-01-25 16:54:03 +00:00
|
|
|
Trade.Buy(InpLotSize, _Symbol, 0, ask - distSL, ask + distTP, "NN Buy");
|
2026-01-16 11:34:58 +03:00
|
|
|
}
|
2026-01-21 13:07:37 +03:00
|
|
|
else if(signal < -InpConfidence && !PositionSelect(_Symbol)) {
|
2026-01-25 16:54:03 +00:00
|
|
|
Trade.Sell(InpLotSize, _Symbol, 0, bid + distSL, bid - distTP, "NN Sell");
|
2026-01-16 11:34:58 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-25 16:54:03 +00:00
|
|
|
|
|
|
|
|
// Load Model with Header
|
|
|
|
|
bool LoadModelWithHeader(string filename)
|
|
|
|
|
{
|
|
|
|
|
int handle = FileOpen(filename, FILE_READ|FILE_BIN|FILE_COMMON);
|
|
|
|
|
if(handle == INVALID_HANDLE) { Print("Error opening model file."); return false; }
|
|
|
|
|
|
|
|
|
|
int version = FileReadInteger(handle);
|
|
|
|
|
if(version != 2) { Print("Error: Model version match (Expected 2)"); FileClose(handle); return false; }
|
|
|
|
|
|
|
|
|
|
gLookback = FileReadInteger(handle);
|
|
|
|
|
int topologyCnt = FileReadInteger(handle);
|
|
|
|
|
ArrayResize(GlobalTopology, topologyCnt);
|
|
|
|
|
for(int i=0; i<topologyCnt; i++) GlobalTopology[i] = FileReadInteger(handle);
|
|
|
|
|
|
|
|
|
|
gTargetTP_Mult = FileReadDouble(handle);
|
|
|
|
|
gTargetSL_Mult = FileReadDouble(handle);
|
|
|
|
|
|
|
|
|
|
gAsianStart = FileReadInteger(handle);
|
|
|
|
|
gAsianEnd = FileReadInteger(handle);
|
|
|
|
|
gLondonStart = FileReadInteger(handle);
|
|
|
|
|
gLondonEnd = FileReadInteger(handle);
|
|
|
|
|
gNYStart = FileReadInteger(handle);
|
|
|
|
|
gNYEnd = FileReadInteger(handle);
|
|
|
|
|
|
|
|
|
|
// Create Net with loaded Topology
|
|
|
|
|
if(CheckPointer(Net) != POINTER_INVALID) delete Net;
|
|
|
|
|
Net = new CNeuralNet(GlobalTopology);
|
|
|
|
|
|
|
|
|
|
// Load Weights
|
|
|
|
|
if(!Net.Load(handle, GlobalTopology)) { FileClose(handle); return false; }
|
|
|
|
|
|
|
|
|
|
FileClose(handle);
|
|
|
|
|
return true;
|
|
|
|
|
}
|