mql5/Scripts/TrainPriceActionNN.mq5
abel324d 91212bae34
2026-01-26 17:17:54 +03:00

420 lines
16 KiB
MQL5

//+------------------------------------------------------------------+
//| TrainPriceActionNN.mq5 |
//| Copyright 2025, Google Gemini |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Google Gemini"
#property link "https://www.google.com"
#property version "3.00"
#property script_show_inputs
#include <NeuralNet.mqh>
input int InpEpochs = 500;
input int InpLookback = 144;
input string InpHiddenLayerConfig = "256,128,64";
input double InpTrainingRate = 0.001;
input double InpTrainingDecay = 0.98;
input double InpLambda = 0.0001; // L2 Regularization (Weight Decay)
input double InpJitter = 0.05; // Nudge magnitude when stuck
input int InpMaxBars = 5000000;
input int InpPredictionHorizon = 144;
// Session Times (Broker Server Hour)
input int InpAsianStart = 0;
input int InpAsianEnd = 8;
input int InpLondonStart = 8;
input int InpLondonEnd = 16;
input int InpNYStart = 13;
input int InpNYEnd = 22;
CNeuralNet *Net;
int GlobalTopology[];
int handleATR_D1, handleEMA50, handleEMA200;
struct Sample {
double inputs[];
double target;
};
struct DaySession {
double aH, aL;
double lH, lL;
double nH, nL;
bool aValid, lValid, nValid;
};
// Map: DayIndex (Time/86400) -> Stats
// Since time is large, we map [0..NumDays].
// We need 'StartDayTime' for index 0.
DaySession sessions[];
datetime baseTime;
void ComputeSessions(const MqlRates &rates[], int total)
{
// 1. Find Date Range
datetime first = rates[total-1].time; // Oldest
datetime last = rates[0].time; // Newest
// Align to Day Start
first = first - (first % 86400);
baseTime = first;
int numDays = (int)((last - first) / 86400) + 2; // +buffer
ArrayResize(sessions, numDays);
// Initialize
for(int k=0; k<numDays; k++) {
sessions[k].aH = -1; sessions[k].aL = 999999;
sessions[k].lH = -1; sessions[k].lL = 999999;
sessions[k].nH = -1; sessions[k].nL = 999999;
sessions[k].aValid=false; sessions[k].lValid=false; sessions[k].nValid=false;
}
Print("Pre-calculating sessions for ", numDays, " days...");
// Scan all bars
for(int i=0; i<total; i++) {
datetime t = rates[i].time;
int dayIdx = (int)((t - baseTime) / 86400);
if(dayIdx < 0 || dayIdx >= numDays) continue;
MqlDateTime dt; TimeToStruct(t, dt);
int h = dt.hour;
double H = rates[i].high;
double L = rates[i].low;
// Update stats for that day
// Asian
if(h >= InpAsianStart && h < InpAsianEnd) {
if(H > sessions[dayIdx].aH || sessions[dayIdx].aH == -1) sessions[dayIdx].aH = H;
if(L < sessions[dayIdx].aL) sessions[dayIdx].aL = L;
sessions[dayIdx].aValid = true;
}
// London
if(h >= InpLondonStart && h < InpLondonEnd) {
if(H > sessions[dayIdx].lH || sessions[dayIdx].lH == -1) sessions[dayIdx].lH = H;
if(L < sessions[dayIdx].lL) sessions[dayIdx].lL = L;
sessions[dayIdx].lValid = true;
}
// NY
if(h >= InpNYStart && h < InpNYEnd) {
if(H > sessions[dayIdx].nH || sessions[dayIdx].nH == -1) sessions[dayIdx].nH = H;
if(L < sessions[dayIdx].nL) sessions[dayIdx].nL = L;
sessions[dayIdx].nValid = true;
}
}
}
void OnStart()
{
Print("--- Advanced Training: v3.0 Optimized ---");
Print( "InpEpochs: ", InpEpochs,
"\nInpHiddenLayerConfig: ", InpHiddenLayerConfig,
"\nInpTrainingRate: ", InpTrainingRate,
"\nInpTrainingDecay: ", InpTrainingDecay,
"\nInpLookback: " ,InpLookback,
"\nInpMaxBars: " , InpMaxBars,
"\nInpPredictionHorizon: " , InpPredictionHorizon);
handleATR_D1 = iATR(_Symbol, PERIOD_D1, 14);
handleEMA50 = iMA(_Symbol, PERIOD_H1, 50, 0, MODE_EMA, PRICE_CLOSE);
handleEMA200 = iMA(_Symbol, PERIOD_H1, 200, 0, MODE_EMA, PRICE_CLOSE);
MqlRates rates[];
ArraySetAsSeries(rates, true);
int needed = InpMaxBars + InpPredictionHorizon + InpLookback + 5000;
int copied = CopyRates(_Symbol, PERIOD_H1, 0, needed, rates);
if(copied < 5000) return;
double ema50[], ema200[];
ArraySetAsSeries(ema50, true);
ArraySetAsSeries(ema200, true);
CopyBuffer(handleEMA50, 0,0,copied, ema50);
CopyBuffer(handleEMA200, 0,0,copied, ema200);
// --- Log Training Data Info ---
datetime startT = rates[copied-1].time;
datetime endT = rates[0].time;
double totalDays = (double)(endT - startT) / 86400.0;
Print("--------------------------------------------------");
PrintFormat("Training Data Loaded: %d Bars", copied);
Print("Date Range: " + TimeToString(startT) + " to " + TimeToString(endT));
PrintFormat("Total Duration: %.2f Days", totalDays);
Print("--------------------------------------------------");
Print("Inputs Reference:");
PrintFormat("MaxBars: %d", InpMaxBars);
PrintFormat("Prediction Horizon: %d bars", InpPredictionHorizon);
PrintFormat("Lookback: %d bars", InpLookback);
PrintFormat("Hidden Layers: %s", InpHiddenLayerConfig);
PrintFormat("Training Rate: %.4f (Decay: %.3f)", InpTrainingRate, InpTrainingDecay);
Print("--------------------------------------------------");
// Precalc Sessions
ComputeSessions(rates, copied);
int featuresPerBar = 17;
int inputSize = InpLookback * featuresPerBar;
string result[];
int hiddenLayerCount = StringSplit(InpHiddenLayerConfig, ',', result);
ArrayResize(GlobalTopology, 1 + hiddenLayerCount + 1);
GlobalTopology[0] = inputSize;
for(int i=0; i<hiddenLayerCount; i++) GlobalTopology[i+1] = (int)StringToInteger(result[i]);
GlobalTopology[ArraySize(GlobalTopology)-1] = 1;
Net = new CNeuralNet(GlobalTopology, ACT_LEAKY_RELU);
Net.SetLearningRate(InpTrainingRate);
Net.SetLambda(InpLambda);
Sample samples[];
int pos=0, neg=0, flat=0;
int startIdx = InpPredictionHorizon;
int endIdx = copied - InpLookback - 1;
if(endIdx - startIdx > InpMaxBars) endIdx = startIdx + InpMaxBars;
for(int i = startIdx; i <= endIdx; i++)
{
// Stride: Train every 20 minutes (Minute 0, 20, 40)
MqlDateTime dt; TimeToStruct(rates[i].time, dt);
//if(dt.min % 20 != 0) continue;
// -- D1 ATR --
double d1ATR_Arr[1];
if(CopyBuffer(handleATR_D1, 0, rates[i].time, 1, d1ATR_Arr)<=0) continue;
double dailyATR = d1ATR_Arr[0];
double targetTP = 1.0 * dailyATR;
double targetSL = 0.5 * dailyATR;
double entryPrice = rates[i].close;
double label = 0;
int firstBuyTP = 99999, firstBuySL = 99999;
int firstSellTP = 99999, firstSellSL = 99999;
for(int k=1; k<=InpPredictionHorizon; k++) {
int f = i-k; if(f<0) break;
double h = rates[f].high;
double l = rates[f].low;
if(firstBuyTP==99999 && h >= entryPrice + targetTP) firstBuyTP = k;
if(firstBuySL==99999 && l <= entryPrice - targetSL) firstBuySL = k;
if(firstSellTP==99999 && l <= entryPrice - targetTP) firstSellTP = k;
if(firstSellSL==99999 && h >= entryPrice + targetSL) firstSellSL = k;
}
if(firstBuyTP < firstBuySL) label = 1.0; // Buy Win
else if(firstSellTP < firstSellSL) label = -1.0; // Sell Win
else label = 0.0;
// -- Features --
double inputs[];
ArrayResize(inputs, inputSize);
for(int k=0; k<InpLookback; k++) {
int p = i + k;
if(p >= copied) break;
int off = k * featuresPerBar;
double o = rates[p].open;
double c = rates[p].close;
double h = rates[p].high;
double l = rates[p].low;
double pH = rates[p+1].high;
double pL = rates[p+1].low;
inputs[off+0] = (c - o)/o * 100.0;
inputs[off+1] = (h - o)/o * 100.0;
inputs[off+2] = (l - o)/o * 100.0;
inputs[off+3] = ((h - pH)/pH) * 100.0;
inputs[off+4] = ((l - pL)/pL) * 100.0;
inputs[off+5] = (rates[p].tick_volume - rates[p+1].tick_volume) / (rates[p+1].tick_volume + 1.0) * 10.0; // Completed Bar Volume difference normalized
inputs[off+6] = ((c - ema50[p])/ema50[p]) * 100.0;
inputs[off+7] = ((c - ema200[p])/ema200[p]) * 100.0;
// -- Optimized Session Lookup --
datetime pTime = rates[p].time;
int dayIdx = (int)((pTime - baseTime) / 86400);
MqlDateTime pDt; TimeToStruct(pTime, pDt);
int pHour = pDt.hour;
// Asian: If pTime >= AsianEnd, use Today. Else Yesterday.
double aH, aL;
int aIdx = (pHour >= InpAsianEnd) ? dayIdx : dayIdx - 1;
if(aIdx>=0 && sessions[aIdx].aValid) { aH=sessions[aIdx].aH; aL=sessions[aIdx].aL; }
else { aH=rates[p].high; aL=rates[p].low; } // Fallback
// London
double lH, lL;
int lIdx = (pHour >= InpLondonEnd) ? dayIdx : dayIdx - 1;
if(lIdx>=0 && sessions[lIdx].lValid) { lH=sessions[lIdx].lH; lL=sessions[lIdx].lL; }
else { lH=rates[p].high; lL=rates[p].low; }
// NY
double nH, nL;
int nIdx = (pHour >= InpNYEnd) ? dayIdx : dayIdx - 1;
if(nIdx>=0 && sessions[nIdx].nValid) { nH=sessions[nIdx].nH; nL=sessions[nIdx].nL; }
else { nH=rates[p].high; nL=rates[p].low; }
inputs[off+8] = ((c - aH)/aH) * 100.0;
inputs[off+9] = ((c - aL)/aH) * 100.0;
inputs[off+10] = ((c - lH)/lH) * 100.0;
inputs[off+11] = ((c - lL)/lH) * 100.0;
inputs[off+12] = ((c - nH)/nH) * 100.0;
inputs[off+13] = ((c - nL)/nH) * 100.0;
inputs[off+14] = (double)pDt.day_of_week / 7.0;
inputs[off+15] = (double)pDt.hour / 24.0;
inputs[off+16] = (double)pDt.min / 60.0;
}
Sample s;
s.target = label;
ArrayResize(s.inputs, inputSize);
ArrayCopy(s.inputs, inputs);
int n=ArraySize(samples);
ArrayResize(samples, n+1);
samples[n] = s;
if(label > 0.1) pos++; else if(label < -0.1) neg++; else flat++;
}
PrintFormat("Collected: Pos=%d Neg=%d Flat=%d", pos, neg, flat);
// Balance
int targetCount = (pos + neg) / 2;
Sample balanced[];
for(int k=0; k<ArraySize(samples); k++) {
if(samples[k].target == 0) {
if(MathRand()%100 < 100.0 * targetCount/flat) {
int b=ArraySize(balanced); ArrayResize(balanced, b+1);
balanced[b]=samples[k];
}
} else {
int b=ArraySize(balanced); ArrayResize(balanced, b+1);
balanced[b]=samples[k];
}
}
PrintFormat("Balanced to %d", ArraySize(balanced));
// Train
int total = ArraySize(balanced);
int nTrain = (int)(total * 0.8);
int indices[]; ArrayResize(indices, nTrain);
for(int k=0; k<nTrain; k++) indices[k]=k;
double bestValAcc = 0;
double lastValAcc = 0;
int stuckCount = 0;
for(int e=0; e<InpEpochs; e++) {
// Shuffle training indices
for(int k=nTrain-1; k>0; k--) { int r=MathRand()%(k+1); int t=indices[k]; indices[k]=indices[r]; indices[r]=t; }
for(int k=0; k<nTrain; k++) {
double t[1]; t[0]=balanced[indices[k]].target;
Net.FeedForward(balanced[indices[k]].inputs);
Net.BackProp(t);
}
// Calculate Validation Accuracy
int corr=0, tot=0;
for(int v=nTrain; v<total; v++) {
Net.FeedForward(balanced[v].inputs);
double res[]; Net.GetResults(res);
if(MathAbs(balanced[v].target)>0.1) {
tot++;
if(balanced[v].target * res[0] > 0) corr++;
}
}
double valAcc = (tot>0)?(double)corr/tot*100:0;
if(e%10==0 || e==InpEpochs-1) {
PrintFormat("Epoch %d Val: %.1f%% (Best: %.1f%%) - LR: %.6f", e, valAcc, bestValAcc, Net.eta);
}
// Handle "Best Model" Persistence
if(valAcc > bestValAcc + 0.05) {
bestValAcc = valAcc;
SaveModelWithHeader("PriceActionNN_v3.bin");
PrintFormat("--- NEW BEST MODEL SAVED! Accuracy: %.2f%%", bestValAcc);
stuckCount = 0;
} else {
stuckCount++;
}
// If no improvement for 30 epochs relative to current state, NUDGE
if(stuckCount >= 30 && InpJitter > 0) {
PrintFormat("STUCK DETECTED! (Current: %.2f%% stayed flat). Nudging weights...", valAcc);
Net.Jitter(InpJitter);
stuckCount = 0;
Net.SetLearningRate(Net.eta * 1.5);
}
Net.SetLearningRate(Net.eta * InpTrainingDecay);
}
string fullPath = TerminalInfoString(TERMINAL_COMMONDATA_PATH) + "\\Files\\PriceActionNN_v3.bin";
Print("--------------------------------------------------");
Print("TRAINING FINISHED.");
PrintFormat("Peak Validation Accuracy Achieved: %.2f%%", bestValAcc);
PrintFormat("Topology Used: %d inputs, %s hidden, 1 output", GlobalTopology[0], InpHiddenLayerConfig);
Print("Final Best Model at: " + fullPath);
Print("--------------------------------------------------");
SaveModelWithHeader("PriceActionNN_v3.bin");
delete Net; Net = NULL;
IndicatorRelease(handleATR_D1);
IndicatorRelease(handleEMA50);
IndicatorRelease(handleEMA200);
}
void OnDeinit(const int reason)
{
if(CheckPointer(Net) == POINTER_DYNAMIC) {
Print("Script stopped/interrupted. Saving current weights...");
SaveModelWithHeader("PriceActionNN_v3.bin");
delete Net;
Net = NULL;
}
IndicatorRelease(handleATR_D1);
IndicatorRelease(handleEMA50);
IndicatorRelease(handleEMA200);
}
void SaveModelWithHeader(string filename)
{
int handle = FileOpen(filename, FILE_WRITE|FILE_BIN|FILE_COMMON);
if(handle == INVALID_HANDLE) { Print("Error opening file for write: ", filename); return; }
// Header Data
int version = 2; // Version 2 supports Metadata
FileWriteInteger(handle, version);
FileWriteInteger(handle, InpLookback);
FileWriteInteger(handle, ArraySize(GlobalTopology));
for(int i=0; i<ArraySize(GlobalTopology); i++) FileWriteInteger(handle, GlobalTopology[i]);
// Risk Settings
FileWriteDouble(handle, 1.0); // Target Multiplier
FileWriteDouble(handle, 0.5); // Stop Multiplier
// Session Times (Only used in Advanced, but kept for consistent header structure or omitted?
// PriceActionNN simple version doesn't use Session Inputs as strictly.
// Actually, `TrainPriceActionNN` DOES use sessions (Lines 23-28).
FileWriteInteger(handle, InpAsianStart);
FileWriteInteger(handle, InpAsianEnd);
FileWriteInteger(handle, InpLondonStart);
FileWriteInteger(handle, InpLondonEnd);
FileWriteInteger(handle, InpNYStart);
FileWriteInteger(handle, InpNYEnd);
if(Net.Save(handle)) Print("Saved Model + Metadata to ", filename);
else Print("Error saving weights!");
FileClose(handle);
}