Initialize project in MT5 Experts directory

This commit is contained in:
Huthayfa
2026-05-28 19:06:07 +03:00
commit 7545b842a4
21 changed files with 3213 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
venv/
+167
View File
@@ -0,0 +1,167 @@
//+------------------------------------------------------------------+
//| Core/Config.mqh |
//| Universal Multi-Timeframe EA - Configuration & Type Definitions|
//+------------------------------------------------------------------+
#ifndef __CONFIG_MQH__
#define __CONFIG_MQH__
#property strict
//+------------------------------------------------------------------+
//| ENUMERATIONS |
//+------------------------------------------------------------------+
enum ENUM_BIAS
{
BIAS_BULL,
BIAS_BEAR,
BIAS_NEUTRAL
};
enum ENUM_REGIME
{
REGIME_TREND,
REGIME_RANGE,
REGIME_CHOP
};
enum ENUM_PATTERN
{
PATTERN_PIN_BAR,
PATTERN_ENGULFING,
PATTERN_INSIDE_BAR,
PATTERN_NONE
};
enum ENUM_EXIT_REASON
{
EXIT_TP1,
EXIT_TP2,
EXIT_SL,
EXIT_BE,
EXIT_TIME,
EXIT_REGIME_CHANGE,
EXIT_MANUAL,
EXIT_TRAILING_STOP
};
enum ENUM_ASSET_CLASS
{
ASSET_FOREX_MAJOR,
ASSET_FOREX_CROSS,
ASSET_METAL,
ASSET_INDEX,
ASSET_COMMODITY,
ASSET_CRYPTO
};
//+------------------------------------------------------------------+
//| DATA STRUCTURES |
//+------------------------------------------------------------------+
struct SignalData
{
bool isValid;
bool isBuy;
double entryPrice;
double slPrice;
double tp1Price;
double tp2Price;
ENUM_PATTERN pattern;
string patternName;
string rejectionReason;
datetime signalTime;
double atrValue;
};
struct TradeParams
{
double lotSize;
double riskAmount;
double riskPercent;
double slDistance;
double tp1Distance;
double tp2Distance;
double marginRequired;
bool isValid;
string rejectReason;
};
struct AssetProfile
{
ENUM_ASSET_CLASS assetClass;
double atrMultiplierSL;
double maxSpreadPoints;
int londonOpenHour;
int nyOpenHour;
bool trade24_7;
bool skipWeekend;
int sessionStartHour;
int sessionEndHour;
double minVolumeRatio;
double partialCloseRatio;
double beBufferPoints;
double trailingATRMult;
int maxTradeDuration;
string description;
};
struct VWAPState
{
double vwapValue;
double vwapSlope;
datetime sessionStart;
double sumPV;
double sumV;
bool isValid;
};
struct CorrelationData
{
string symbol;
double correlation;
int barsUsed;
datetime calcTime;
};
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| CONSTANTS |
//+------------------------------------------------------------------+
#define MAX_RETRIES 3
#define RETRY_BASE_MS 500
#define CORR_LOOKBACK 50
#define VWAP_SLOPE_BARS 5
#define SWING_LOOKBACK 20
#define VOLUME_MA_PERIOD 20
#define BB_PERIOD 20
#define BB_DEVIATIONS 2.0
#define ADX_PERIOD 14
#define ADX_TREND_LEVEL 25.0
#define ADX_RANGE_LEVEL 20.0
#define ADX_CHOP_LEVEL 15.0
#define ATR_TREND_RATIO 1.0
#define ATR_CHOP_RATIO 0.8
#define VOLUME_CONFIRM 1.2
#define MIN_VOLUME_RATIO 0.8
#define PIN_BAR_WICK_MULT 2.0
#define ENGULF_VOLUME_MULT 1.2
#define PARTIAL_CLOSE_R 1.5
#define BE_BUFFER_ATR_MULT 0.2
#define SLIPPAGE_ATR_MULT 0.5
#define MIN_SLIPPAGE_PTS 10
#define MAX_SLIPPAGE_PTS 50
#define ATR_TO_POINTS(atrValue) ((int)MathRound((atrValue) / _Point))
#define VALIDATE_SHIFT(shift, context) ((shift) >= 1 ? true : (Print("[REPAINT_GUARD] Violation in ", (context), ": shift=", (shift), " < 1. Using shift=1."), false))
#define RELEASE_HANDLE(handle) do { if((handle) != INVALID_HANDLE) { IndicatorRelease(handle); (handle) = INVALID_HANDLE; } } while(0)
//+------------------------------------------------------------------+
#endif // __CONFIG_MQH__
+276
View File
@@ -0,0 +1,276 @@
//+------------------------------------------------------------------+
//| Core/Logger.mqh |
//+------------------------------------------------------------------+
#ifndef __LOGGER_MQH__
#define __LOGGER_MQH__
#include "Config.mqh"
#include "State.mqh"
extern EAState g_state;
class CLogger
{
private:
string m_logPath;
string m_label;
ulong m_magic;
int m_fileTrade;
int m_fileSignal;
int m_fileError;
bool m_initialized;
string m_panelName;
string m_objects[];
int m_objCount;
string TimeStampMicro() const
{
datetime t = TimeCurrent();
long msec = GetTickCount() % 1000;
return TimeToString(t, TIME_DATE|TIME_SECONDS) + "." + IntegerToString(msec, 3, '0');
}
bool EnsureDirectory(string path)
{
string dirs[];
int count = StringSplit(path, '\\', dirs);
string current = "";
for(int i = 0; i < count; i++)
{
if(i > 0) current += "\\";
current += dirs[i];
if(current == "") continue;
if(!FolderCreate(current, 0))
{
int err = GetLastError();
if(err != 183 && err != 0) return false; // 183 = already exists
}
}
return true;
}
int OpenLogFile(string filename, string header)
{
string filepath = m_logPath + filename;
bool exists = FileIsExist(filepath);
int handle = FileOpen(filepath, FILE_WRITE|FILE_READ|FILE_CSV|FILE_COMMON|FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_ANSI, ',');
if(handle == INVALID_HANDLE) return INVALID_HANDLE;
FileSeek(handle, 0, SEEK_END);
if(!exists || FileTell(handle) == 0)
{
FileWrite(handle, header);
FileFlush(handle);
}
return handle;
}
void WriteCSV(int handle, string data)
{
if(handle == INVALID_HANDLE) return;
FileSeek(handle, 0, SEEK_END);
FileWriteString(handle, data + "\r\n");
FileFlush(handle);
}
void CreatePanel()
{
m_panelName = "MTF_Dashboard_" + IntegerToString((int)m_magic);
ObjectCreate(0, m_panelName, OBJ_RECTANGLE_LABEL, 0, 0, 0);
ObjectSetInteger(0, m_panelName, OBJPROP_XDISTANCE, 10);
ObjectSetInteger(0, m_panelName, OBJPROP_YDISTANCE, 30);
ObjectSetInteger(0, m_panelName, OBJPROP_XSIZE, 320);
ObjectSetInteger(0, m_panelName, OBJPROP_YSIZE, 280);
ObjectSetInteger(0, m_panelName, OBJPROP_BGCOLOR, C'20,20,30');
ObjectSetInteger(0, m_panelName, OBJPROP_BORDER_TYPE, BORDER_FLAT);
ObjectSetInteger(0, m_panelName, OBJPROP_COLOR, C'60,60,80');
CreateLabel("Title", 15, 35, "Universal MTF EA v2.0", 12, clrWhite, true);
CreateLabel("Section1", 15, 55, "=== MARKET STATE ===", 10, C'150,150,170');
CreateLabel("BiasLabel", 15, 72, "HTF Bias:", 9, clrSilver);
CreateLabel("BiasValue", 120, 72, "NEUTRAL", 9, clrYellow);
CreateLabel("RegimeLabel", 15, 88, "Regime:", 9, clrSilver);
CreateLabel("RegimeValue", 120, 88, "RANGE", 9, clrYellow);
CreateLabel("MLLabel", 15, 104, "ML Confidence:", 9, clrSilver);
CreateLabel("MLValue", 120, 104, "0.00", 9, clrYellow);
CreateLabel("Section2", 15, 122, "=== PERFORMANCE ===", 10, C'150,150,170');
CreateLabel("DailyLabel", 15, 139, "Daily PnL:", 9, clrSilver);
CreateLabel("DailyValue", 120, 139, "0.00", 9, clrWhite);
CreateLabel("WeeklyLabel", 15, 155, "Weekly PnL:", 9, clrSilver);
CreateLabel("WeeklyValue", 120, 155, "0.00", 9, clrWhite);
CreateLabel("TradesLabel", 15, 171, "Trades Today:", 9, clrSilver);
CreateLabel("TradesValue", 120, 171, "0", 9, clrWhite);
CreateLabel("Section3", 15, 189, "=== RISK STATUS ===", 10, C'150,150,170');
CreateLabel("OpenPosLabel", 15, 206, "Open Positions:", 9, clrSilver);
CreateLabel("OpenPosValue", 120, 206, "0", 9, clrWhite);
CreateLabel("RiskLabel", 15, 222, "Total Risk:", 9, clrSilver);
CreateLabel("RiskValue", 120, 222, "0.00%", 9, clrWhite);
CreateLabel("StatusLabel", 15, 238, "Status:", 9, clrSilver);
CreateLabel("StatusValue", 120, 238, "ACTIVE", 9, clrLime);
CreateLabel("Section4", 15, 256, "=== NEWS ===", 10, C'150,150,170');
CreateLabel("NewsLabel", 15, 273, "Next Event:", 9, clrSilver);
CreateLabel("NewsValue", 120, 273, "None", 9, clrWhite);
m_initialized = true;
}
void CreateLabel(string name, int x, int y, string text, int fontSize, color clr, bool bold = false)
{
string fullName = m_panelName + "_" + name;
ObjectCreate(0, fullName, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, fullName, OBJPROP_XDISTANCE, x);
ObjectSetInteger(0, fullName, OBJPROP_YDISTANCE, y);
ObjectSetString(0, fullName, OBJPROP_FONT, bold ? "Arial Bold" : "Arial");
ObjectSetInteger(0, fullName, OBJPROP_FONTSIZE, fontSize);
ObjectSetInteger(0, fullName, OBJPROP_COLOR, clr);
ObjectSetString(0, fullName, OBJPROP_TEXT, text);
ObjectSetInteger(0, fullName, OBJPROP_SELECTABLE, false);
int idx = ArraySize(m_objects);
ArrayResize(m_objects, idx + 1);
m_objects[idx] = fullName;
}
void UpdateLabel(string name, string text, color clr)
{
string fullName = m_panelName + "_" + name;
if(ObjectFind(0, fullName) >= 0)
{
ObjectSetString(0, fullName, OBJPROP_TEXT, text);
ObjectSetInteger(0, fullName, OBJPROP_COLOR, clr);
}
}
public:
CLogger() : m_fileTrade(INVALID_HANDLE), m_fileSignal(INVALID_HANDLE),
m_fileError(INVALID_HANDLE), m_initialized(false), m_objCount(0) {}
bool Init(string basePath, string label, ulong magic)
{
m_label = label; m_magic = magic; m_logPath = basePath;
if(StringLen(m_logPath) > 0 && StringSubstr(m_logPath, StringLen(m_logPath)-1) != "\\")
m_logPath += "\\";
m_logPath += label + "_" + IntegerToString((int)magic) + "\\";
if(!EnsureDirectory(m_logPath)) m_logPath = "";
string tradeHeader = "Timestamp,Symbol,Direction,EntryPrice,SL,TP1,TP2,Lot,RiskPercent,ATR_Value,Regime,HTF_Bias,Pattern,ExitPrice,ExitReason,PnL_USD,PnL_Percent,Duration_Minutes";
m_fileTrade = OpenLogFile("TradeJournal.csv", tradeHeader);
string signalHeader = "Timestamp,Symbol,HTF_Bias,MTF_Regime,LTF_Pattern,IsValid,RejectionReason";
m_fileSignal = OpenLogFile("SignalLog.csv", signalHeader);
string errorHeader = "Timestamp,Function,ErrorCode,ErrorMessage,RetryCount,Resolution";
m_fileError = OpenLogFile("ErrorLog.csv", errorHeader);
m_initialized = (m_fileTrade != INVALID_HANDLE && m_fileSignal != INVALID_HANDLE && m_fileError != INVALID_HANDLE);
if(m_initialized)
{
Print("[Logger] Audit trail active. Path: ", m_logPath);
CreatePanel();
}
return m_initialized;
}
void Shutdown()
{
if(m_fileTrade != INVALID_HANDLE) { FileClose(m_fileTrade); m_fileTrade = INVALID_HANDLE; }
if(m_fileSignal != INVALID_HANDLE) { FileClose(m_fileSignal); m_fileSignal = INVALID_HANDLE; }
if(m_fileError != INVALID_HANDLE) { FileClose(m_fileError); m_fileError = INVALID_HANDLE; }
for(int i = 0; i < ArraySize(m_objects); i++) ObjectDelete(0, m_objects[i]);
ObjectDelete(0, m_panelName);
Print("[Logger] Log files closed and dashboard cleared.");
}
void LogEvent(string category, string message)
{
if(InpDebugMode) Print("[", category, "] ", message);
}
void LogSignal(const SignalData &signal, const EAState &state)
{
if(m_fileSignal == INVALID_HANDLE) return;
string line = StringFormat("%s,%s,%s,%s,%s,%s,%s",
TimeStampMicro(), _Symbol, EnumToString(state.currentBias),
EnumToString(state.currentRegime), signal.patternName,
signal.isValid ? "YES" : "NO", signal.rejectionReason);
WriteCSV(m_fileSignal, line);
}
void LogError(string function, int code, string message, int retryCount)
{
if(m_fileError == INVALID_HANDLE) return;
string line = StringFormat("%s,%s,%d,%s,%d,%s",
TimeStampMicro(), function, code, message, retryCount, "PENDING");
WriteCSV(m_fileError, line);
}
void LogTradeOpen(const SignalData &signal, const TradeParams &params, ulong ticket)
{
if(m_fileTrade == INVALID_HANDLE) return;
string dir = signal.isBuy ? "BUY" : "SELL";
string line = StringFormat("%s,%s,%s,%.5f,%.5f,%.5f,%.5f,%.2f,%.2f,%.5f,%s,%s,%s,%s,%.2f,%.2f,%d",
TimeStampMicro(), _Symbol, dir, signal.entryPrice, signal.slPrice,
signal.tp1Price, signal.tp2Price, params.lotSize, params.riskPercent,
signal.atrValue, EnumToString(g_state.currentRegime),
EnumToString(g_state.currentBias), signal.patternName, "", "", 0, 0, 0);
WriteCSV(m_fileTrade, line);
}
void LogTradeClose(const EAState &state)
{
if(m_fileTrade == INVALID_HANDLE) return;
string line = StringFormat("%s,%s,,%s,,,,,,,,,%.2f,%s,%.2f,%.0f",
TimeStampMicro(), _Symbol, EnumToString(state.lastExitReason),
state.lastTradePnL, EnumToString(state.lastExitReason),
(state.lastTradePnL / AccountInfoDouble(ACCOUNT_EQUITY)) * 100.0,
(TimeCurrent() - state.lastTradeClose) / 60.0);
WriteCSV(m_fileTrade, line);
}
void UpdateDashboard(const EAState &state)
{
color biasClr = clrYellow;
string biasText = EnumToString(state.currentBias);
if(state.currentBias == BIAS_BULL) biasClr = clrLime;
else if(state.currentBias == BIAS_BEAR) biasClr = clrRed;
UpdateLabel("BiasValue", biasText, biasClr);
color regimeClr = clrYellow;
string regimeText = EnumToString(state.currentRegime);
if(state.currentRegime == REGIME_TREND) regimeClr = clrLime;
else if(state.currentRegime == REGIME_CHOP) regimeClr = clrRed;
UpdateLabel("RegimeValue", regimeText, regimeClr);
color dailyClr = state.dailyPnL >= 0 ? clrLime : clrRed;
UpdateLabel("DailyValue", StringFormat("%.2f", state.dailyPnL), dailyClr);
color weeklyClr = state.weeklyPnL >= 0 ? clrLime : clrRed;
UpdateLabel("WeeklyValue", StringFormat("%.2f", state.weeklyPnL), weeklyClr);
UpdateLabel("TradesValue", IntegerToString(state.totalTradesToday), clrWhite);
UpdateLabel("OpenPosValue", IntegerToString(state.openPositions), state.openPositions > 0 ? clrLime : clrWhite);
color riskClr = state.totalOpenRisk > InpMaxTotalRisk * 0.8 ? clrRed :
state.totalOpenRisk > InpMaxTotalRisk * 0.5 ? clrYellow : clrWhite;
UpdateLabel("RiskValue", StringFormat("%.2f%%", state.totalOpenRisk), riskClr);
string status = "ACTIVE";
color statusClr = clrLime;
if(state.dailyLimitHit) { status = "DAILY LIMIT"; statusClr = clrRed; }
else if(state.weeklyLimitHit) { status = "WEEKLY LIMIT"; statusClr = clrRed; }
else if(state.consecLossHalted) { status = "CONSEC LOSS"; statusClr = clrRed; }
else if(state.circuitBreakerUntil > TimeCurrent()) { status = "HALTED"; statusClr = clrRed; }
UpdateLabel("StatusValue", status, statusClr);
string dash = StringFormat(
"\n=== Universal_MTF_EA v2.0 | %s ===\n"
"Bias: %s | Regime: %s | Volume: %s\n"
"Daily PnL: %.2f | Weekly PnL: %.2f\n"
"Open Pos: %d | Total Risk: %.2f%%\n"
"Last Trade: %.2f (%s)\n"
"Status: %s\n"
"====================",
_Symbol, EnumToString(state.currentBias), EnumToString(state.currentRegime),
state.volumeConfirmed ? "OK" : "LOW", state.dailyPnL, state.weeklyPnL,
state.openPositions, state.totalOpenRisk, state.lastTradePnL,
EnumToString(state.lastExitReason), status);
Comment(dash);
}
};
#endif // __LOGGER_MQH__
+142
View File
@@ -0,0 +1,142 @@
//+------------------------------------------------------------------+
//| Core/State.mqh |
//+------------------------------------------------------------------+
#ifndef __STATE_MQH__
#define __STATE_MQH__
#include "Config.mqh"
struct EAState
{
double dailyPnL;
double weeklyPnL;
double totalOpenRisk;
int consecutiveLosses;
int totalTradesToday;
int totalTradesWeek;
bool dailyLimitHit;
bool weeklyLimitHit;
bool consecLossHalted;
bool spreadHalted;
datetime circuitBreakerUntil;
string circuitBreakerReason;
ENUM_BIAS currentBias;
ENUM_REGIME currentRegime;
ENUM_PATTERN lastPattern;
bool volumeConfirmed;
bool isBarClosedHTF;
bool isBarClosedMTF;
bool isBarClosedLTF;
datetime lastHTFBarTime;
datetime lastMTFBarTime;
datetime lastLTFBarTime;
VWAPState vwapState;
double swingHigh;
double swingLow;
bool bosBullish;
bool bosBearish;
AssetProfile assetProfile;
int openPositions;
double equityAtStart;
double equityAtWeekStart;
datetime lastTradeClose;
ENUM_EXIT_REASON lastExitReason;
double lastTradePnL;
string logDirectory;
bool loggerReady;
datetime lastDashboardUpdate;
};
class CSessionManager
{
private:
datetime m_lastSessionCheck;
int m_serverOffset;
datetime GetGMTTime() const
{
return TimeGMT();
}
public:
CSessionManager() : m_lastSessionCheck(0), m_serverOffset(0) {}
bool Init()
{
datetime serverNow = TimeCurrent();
datetime gmtNow = TimeGMT();
m_serverOffset = (int)((serverNow - gmtNow) / 3600);
if(InpDebugMode)
Print("[SessionManager] Server-GMT offset: ", m_serverOffset, " hours");
return true;
}
bool IsSessionValid(const AssetProfile &profile) const
{
if(profile.trade24_7) return true;
MqlDateTime dt;
TimeToStruct(TimeGMT(), dt);
int currentHour = dt.hour;
int currentDay = dt.day_of_week;
if(profile.skipWeekend && (currentDay == 0 || currentDay == 6))
return false;
if(profile.assetClass == ASSET_METAL && currentDay == 5 && currentHour >= 21)
return false;
if(profile.assetClass == ASSET_METAL && currentDay == 1 && currentHour < 1)
return false;
if(currentHour >= profile.sessionStartHour && currentHour < profile.sessionEndHour)
return true;
return false;
}
bool IsNewSession(const AssetProfile &profile) const
{
MqlDateTime dt;
TimeToStruct(TimeGMT(), dt);
if((profile.assetClass == ASSET_FOREX_MAJOR || profile.assetClass == ASSET_FOREX_CROSS ||
profile.assetClass == ASSET_METAL) && dt.hour == 8 && dt.min == 0)
return true;
if(profile.assetClass == ASSET_INDEX && dt.hour == 13 && dt.min == 30)
return true;
if(profile.assetClass == ASSET_CRYPTO && dt.hour == 0 && dt.min == 0)
return true;
return false;
}
datetime GetSessionStart(const AssetProfile &profile) const
{
datetime gmtNow = TimeGMT();
MqlDateTime dt;
TimeToStruct(gmtNow, dt);
datetime sessionStart = 0;
if(profile.assetClass == ASSET_INDEX)
{
if(dt.hour < 13 || (dt.hour == 13 && dt.min < 30))
sessionStart = StringToTime(StringFormat("%04d.%02d.%02d 13:30:00", dt.year, dt.mon, dt.day)) - 86400;
else
sessionStart = StringToTime(StringFormat("%04d.%02d.%02d 13:30:00", dt.year, dt.mon, dt.day));
}
else
{
if(dt.hour < 8)
sessionStart = StringToTime(StringFormat("%04d.%02d.%02d 08:00:00", dt.year, dt.mon, dt.day)) - 86400;
else
sessionStart = StringToTime(StringFormat("%04d.%02d.%02d 08:00:00", dt.year, dt.mon, dt.day));
}
return sessionStart + (m_serverOffset * 3600);
}
bool IsRolloverTime() const
{
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
if((dt.hour == 23 && dt.min >= 50) || (dt.hour == 0 && dt.min <= 10))
return true;
return false;
}
};
EAState g_state;
CSessionManager g_session;
#endif // __STATE_MQH__
+170
View File
@@ -0,0 +1,170 @@
//+------------------------------------------------------------------+
//| Core/SymbolProfiler.mqh |
//+------------------------------------------------------------------+
#ifndef __SYMBOL_PROFILER_MQH__
#define __SYMBOL_PROFILER_MQH__
#include "Config.mqh"
class CSymbolProfiler
{
public:
bool Init(AssetProfile &profile)
{
string sym = _Symbol;
profile.description = sym;
if(IsMetal(sym))
{
profile.assetClass = ASSET_METAL;
profile.atrMultiplierSL = 2.5;
profile.maxSpreadPoints = 30.0 * _Point * 10;
profile.londonOpenHour = 8;
profile.nyOpenHour = 13;
profile.trade24_7 = false;
profile.skipWeekend = true;
profile.sessionStartHour = 0;
profile.sessionEndHour = 23;
profile.minVolumeRatio = 0.7;
profile.partialCloseRatio = 0.5;
profile.beBufferPoints = 20.0 * _Point * 10;
profile.trailingATRMult = 2.0;
profile.maxTradeDuration = 360;
profile.description = "Precious Metal (XAU/XAG)";
}
else if(IsIndex(sym))
{
profile.assetClass = ASSET_INDEX;
profile.atrMultiplierSL = 3.0;
profile.maxSpreadPoints = 5.0 * _Point;
profile.londonOpenHour = 8;
profile.nyOpenHour = 13;
profile.trade24_7 = false;
profile.skipWeekend = true;
profile.sessionStartHour = 14;
profile.sessionEndHour = 21;
profile.minVolumeRatio = 0.6;
profile.partialCloseRatio = 0.5;
profile.beBufferPoints = 10.0 * _Point;
profile.trailingATRMult = 2.5;
profile.maxTradeDuration = 240;
profile.description = "Equity Index";
}
else if(IsCrypto(sym))
{
profile.assetClass = ASSET_CRYPTO;
profile.atrMultiplierSL = 2.0;
profile.maxSpreadPoints = 50.0 * _Point;
profile.londonOpenHour = 0;
profile.nyOpenHour = 0;
profile.trade24_7 = true;
profile.skipWeekend = false;
profile.sessionStartHour = 0;
profile.sessionEndHour = 23;
profile.minVolumeRatio = 0.5;
profile.partialCloseRatio = 0.5;
profile.beBufferPoints = 50.0 * _Point;
profile.trailingATRMult = 1.5;
profile.maxTradeDuration = 720;
profile.description = "Cryptocurrency";
}
else if(IsCommodity(sym))
{
profile.assetClass = ASSET_COMMODITY;
profile.atrMultiplierSL = 2.0;
profile.maxSpreadPoints = 20.0 * _Point;
profile.londonOpenHour = 8;
profile.nyOpenHour = 13;
profile.trade24_7 = false;
profile.skipWeekend = true;
profile.sessionStartHour = 0;
profile.sessionEndHour = 22;
profile.minVolumeRatio = 0.7;
profile.partialCloseRatio = 0.5;
profile.beBufferPoints = 15.0 * _Point;
profile.trailingATRMult = 2.0;
profile.maxTradeDuration = 360;
profile.description = "Commodity (Oil)";
}
else if(IsForexMajor(sym))
{
profile.assetClass = ASSET_FOREX_MAJOR;
profile.atrMultiplierSL = 1.5;
profile.maxSpreadPoints = 2.0 * _Point * 10;
profile.londonOpenHour = 8;
profile.nyOpenHour = 13;
profile.trade24_7 = false;
profile.skipWeekend = true;
profile.sessionStartHour = 0;
profile.sessionEndHour = 23;
profile.minVolumeRatio = 0.8;
profile.partialCloseRatio = 0.5;
profile.beBufferPoints = 5.0 * _Point * 10;
profile.trailingATRMult = 1.5;
profile.maxTradeDuration = 300;
profile.description = "Forex Major";
}
else
{
profile.assetClass = ASSET_FOREX_CROSS;
profile.atrMultiplierSL = 1.5;
profile.maxSpreadPoints = 3.0 * _Point * 10;
profile.londonOpenHour = 8;
profile.nyOpenHour = 13;
profile.trade24_7 = false;
profile.skipWeekend = true;
profile.sessionStartHour = 0;
profile.sessionEndHour = 23;
profile.minVolumeRatio = 0.8;
profile.partialCloseRatio = 0.5;
profile.beBufferPoints = 5.0 * _Point * 10;
profile.trailingATRMult = 1.5;
profile.maxTradeDuration = 300;
profile.description = "Forex Cross";
}
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(tickSize > 0 && point > 0)
{
double pipMultiplier = (tickSize / point);
profile.maxSpreadPoints *= pipMultiplier;
profile.beBufferPoints *= pipMultiplier;
}
Print("[SymbolProfiler] ", sym, " classified as: ", profile.description);
return true;
}
private:
bool IsMetal(string sym) const
{
return (StringFind(sym, "XAU") >= 0 || StringFind(sym, "XAG") >= 0 ||
StringFind(sym, "GOLD") >= 0 || StringFind(sym, "SILVER") >= 0);
}
bool IsIndex(string sym) const
{
return (StringFind(sym, "US30") >= 0 || StringFind(sym, "NAS") >= 0 ||
StringFind(sym, "SPX") >= 0 || StringFind(sym, "GER") >= 0 ||
StringFind(sym, "UK100") >= 0 || StringFind(sym, "JP225") >= 0 ||
StringFind(sym, "AUS") >= 0);
}
bool IsCrypto(string sym) const
{
return (StringFind(sym, "BTC") >= 0 || StringFind(sym, "ETH") >= 0 ||
StringFind(sym, "XRP") >= 0 || StringFind(sym, "LTC") >= 0 ||
StringFind(sym, "SOL") >= 0);
}
bool IsCommodity(string sym) const
{
return (StringFind(sym, "OIL") >= 0 || StringFind(sym, "BRENT") >= 0 ||
StringFind(sym, "WTI") >= 0 || StringFind(sym, "GAS") >= 0);
}
bool IsForexMajor(string sym) const
{
string majors[] = {"EURUSD", "GBPUSD", "USDJPY", "USDCHF", "AUDUSD",
"USDCAD", "NZDUSD", "EURJPY", "GBPJPY"};
for(int i = 0; i < ArraySize(majors); i++)
if(sym == majors[i]) return true;
return false;
}
};
#endif // __SYMBOL_PROFILER_MQH__
+127
View File
@@ -0,0 +1,127 @@
//+------------------------------------------------------------------+
//| Core/TelegramNotifier.mqh |
//+------------------------------------------------------------------+
#ifndef __TELEGRAM_NOTIFIER_MQH__
#define __TELEGRAM_NOTIFIER_MQH__
#include "Config.mqh"
#include "State.mqh"
class CTelegramNotifier
{
private:
string m_botToken;
string m_chatId;
string m_discordWebhook;
bool m_useTelegram;
bool m_useDiscord;
bool m_initialized;
int m_timeoutMs;
public:
CTelegramNotifier() : m_useTelegram(false), m_useDiscord(false),
m_initialized(false), m_timeoutMs(5000) {}
bool Init(string botToken, string chatId, string discordWebhook = "")
{
m_botToken = botToken;
m_chatId = chatId;
m_discordWebhook = discordWebhook;
m_useTelegram = (StringLen(botToken) > 0 && StringLen(chatId) > 0);
m_useDiscord = (StringLen(discordWebhook) > 0);
if(!m_useTelegram && !m_useDiscord)
{
Print("[TelegramNotifier] No notification channels configured.");
return true;
}
m_initialized = true;
Print("[TelegramNotifier] Initialized | Telegram: ", m_useTelegram ? "ON" : "OFF",
" | Discord: ", m_useDiscord ? "ON" : "OFF");
return true;
}
void SendTradeOpen(const SignalData &signal, const TradeParams &params, ulong ticket)
{
if(!m_initialized) return;
string emoji = signal.isBuy ? "BUY" : "SELL";
string message = StringFormat(
"*NEW TRADE OPENED*\n\nSymbol: %s\nDirection: %s\nEntry: %.5f\nSL: %.5f\nTP1: %.5f\nTP2: %.5f\nLots: %.2f\nRisk: %.2f%%\nTicket: %llu",
_Symbol, emoji, signal.entryPrice, signal.slPrice, signal.tp1Price, signal.tp2Price,
params.lotSize, params.riskPercent, ticket);
SendMessage(message);
}
void SendTradeClose(const EAState &state)
{
if(!m_initialized) return;
string pnlStr = state.lastTradePnL >= 0 ? StringFormat("+%.2f", state.lastTradePnL) : StringFormat("%.2f", state.lastTradePnL);
string message = StringFormat(
"*TRADE CLOSED*\n\nSymbol: %s\nPnL: %s USD\nReason: %s",
_Symbol, pnlStr, EnumToString(state.lastExitReason));
SendMessage(message);
}
void SendCircuitBreaker(const EAState &state)
{
if(!m_initialized) return;
string message = StringFormat(
"*CIRCUIT BREAKER ACTIVATED*\n\nSymbol: %s\nReason: %s\nDaily PnL: %.2f\nWeekly PnL: %.2f\nResumes: %s",
_Symbol, state.circuitBreakerReason, state.dailyPnL, state.weeklyPnL,
TimeToString(state.circuitBreakerUntil, TIME_DATE|TIME_SECONDS));
SendMessage(message);
}
void SendRegimeChange(ENUM_REGIME oldRegime, ENUM_REGIME newRegime)
{
if(!m_initialized) return;
string message = StringFormat(
"*REGIME CHANGE*\n\nSymbol: %s\nFrom: %s\nTo: %s",
_Symbol, EnumToString(oldRegime), EnumToString(newRegime));
SendMessage(message);
}
void SendDailySummary(const EAState &state)
{
if(!m_initialized) return;
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
string message = StringFormat(
"*DAILY SUMMARY*\n\nSymbol: %s\nDaily PnL: %.2f\nWeekly PnL: %.2f\nTrades: %d\nEquity: %.2f\nStatus: %s",
_Symbol, state.dailyPnL, state.weeklyPnL, state.totalTradesToday, equity,
state.circuitBreakerUntil > TimeCurrent() ? "HALTED" : "ACTIVE");
SendMessage(message);
}
void SendMessage(string message)
{
if(m_useTelegram) SendTelegram(message);
if(m_useDiscord) SendDiscord(message);
}
private:
void SendTelegram(string message)
{
string url = "https://api.telegram.org/bot" + m_botToken + "/sendMessage";
string headers;
string data = "chat_id=" + m_chatId + "&text=" + message + "&parse_mode=Markdown";
char dataChar[];
StringToCharArray(data, dataChar);
char result[];
string resultHeaders;
int res = WebRequest("POST", url, headers, 5000, dataChar, result, resultHeaders);
if(res != 200) Print("[TelegramNotifier] Telegram send failed. HTTP: ", res);
}
void SendDiscord(string message)
{
string headers;
string jsonPayload = "{\"content\":\"" + message + "\"}";
char dataChar[];
StringToCharArray(jsonPayload, dataChar);
char result[];
string resultHeaders;
int res = WebRequest("POST", m_discordWebhook, headers, 5000, dataChar, result, resultHeaders);
if(res != 200 && res != 204) Print("[TelegramNotifier] Discord send failed. HTTP: ", res);
}
};
#endif // __TELEGRAM_NOTIFIER_MQH__
+109
View File
@@ -0,0 +1,109 @@
//+------------------------------------------------------------------+
//| Data/PriceEngine.mqh |
//+------------------------------------------------------------------+
#ifndef __PRICE_ENGINE_MQH__
#define __PRICE_ENGINE_MQH__
#include "../Core/Config.mqh"
class CRepaintGuard
{
public:
static bool ValidateShift(int shift, string context)
{
if(shift < 1)
{
Print("[REPAINT_GUARD] BLOCKED in ", context, ": shift=", shift, " < 1.");
return false;
}
return true;
}
};
class CPriceEngine
{
private:
ENUM_TIMEFRAMES m_htf;
ENUM_TIMEFRAMES m_mtf;
ENUM_TIMEFRAMES m_ltf;
MqlRates m_cacheHTF[];
MqlRates m_cacheMTF[];
MqlRates m_cacheLTF[];
datetime m_lastHTFTime;
datetime m_lastMTFTime;
datetime m_lastLTFTime;
int m_cacheSize;
public:
bool Init(ENUM_TIMEFRAMES htf, ENUM_TIMEFRAMES mtf, ENUM_TIMEFRAMES ltf)
{
m_htf = htf; m_mtf = mtf; m_ltf = ltf; m_cacheSize = 100;
ArraySetAsSeries(m_cacheHTF, true);
ArraySetAsSeries(m_cacheMTF, true);
ArraySetAsSeries(m_cacheLTF, true);
m_lastHTFTime = 0; m_lastMTFTime = 0; m_lastLTFTime = 0;
Print("[PriceEngine] Initialized | HTF:", EnumToString(htf), " MTF:", EnumToString(mtf), " LTF:", EnumToString(ltf));
return true;
}
void Release()
{
ArrayFree(m_cacheHTF); ArrayFree(m_cacheMTF); ArrayFree(m_cacheLTF);
}
bool GetClosedBar(ENUM_TIMEFRAMES period, int shift, MqlRates &outRate)
{
if(!CRepaintGuard::ValidateShift(shift, "GetClosedBar")) shift = 1;
MqlRates temp[];
ArraySetAsSeries(temp, true);
int copied = CopyRates(_Symbol, period, 0, shift + 1, temp);
if(copied <= shift || ArraySize(temp) <= shift) return false;
outRate = temp[shift];
return true;
}
bool GetIndicatorBuffer(int handle, int bufferIndex, int shift, int count, double &buffer[])
{
if(handle == INVALID_HANDLE) return false;
if(shift < 1) { CRepaintGuard::ValidateShift(shift, "GetIndicatorBuffer"); shift = 1; }
ArraySetAsSeries(buffer, true);
int copied = CopyBuffer(handle, bufferIndex, shift, count, buffer);
return (copied > 0);
}
bool IsBarClosed(ENUM_TIMEFRAMES period) const
{
datetime currTime = iTime(_Symbol, period, 0);
datetime prevTime = iTime(_Symbol, period, 1);
return (currTime > 0 && prevTime > 0 && currTime != prevTime);
}
void RefreshAll() { RefreshHTF(); RefreshMTF(); RefreshLTF(); }
void RefreshHTF()
{
int copied = CopyRates(_Symbol, m_htf, 0, m_cacheSize, m_cacheHTF);
if(copied > 0) m_lastHTFTime = m_cacheHTF[0].time;
}
void RefreshMTF()
{
int copied = CopyRates(_Symbol, m_mtf, 0, m_cacheSize, m_cacheMTF);
if(copied > 0) m_lastMTFTime = m_cacheMTF[0].time;
}
void RefreshLTF()
{
int copied = CopyRates(_Symbol, m_ltf, 0, m_cacheSize, m_cacheLTF);
if(copied > 0) m_lastLTFTime = m_cacheLTF[0].time;
}
bool GetHTFBar(int shift, MqlRates &rate)
{
if(ArraySize(m_cacheHTF) > shift && shift >= 0) { rate = m_cacheHTF[shift]; return true; }
return GetClosedBar(m_htf, shift, rate);
}
bool GetMTFBar(int shift, MqlRates &rate)
{
if(ArraySize(m_cacheMTF) > shift && shift >= 0) { rate = m_cacheMTF[shift]; return true; }
return GetClosedBar(m_mtf, shift, rate);
}
bool GetLTFBar(int shift, MqlRates &rate)
{
if(ArraySize(m_cacheLTF) > shift && shift >= 0) { rate = m_cacheLTF[shift]; return true; }
return GetClosedBar(m_ltf, shift, rate);
}
};
#endif // __PRICE_ENGINE_MQH__
+78
View File
@@ -0,0 +1,78 @@
//+------------------------------------------------------------------+
//| Data/VWAP_Engine.mqh |
//+------------------------------------------------------------------+
#ifndef __VWAP_ENGINE_MQH__
#define __VWAP_ENGINE_MQH__
#include "../Core/Config.mqh"
#include "../Core/State.mqh"
class CVWAPEngine
{
private:
AssetProfile m_profile;
datetime m_lastSessionStart;
double m_cachedVWAP;
double m_cachedSlope;
public:
bool Init(const AssetProfile &profile)
{
m_profile = profile;
m_lastSessionStart = 0;
m_cachedVWAP = 0;
m_cachedSlope = 0;
return true;
}
void Release() {}
void Calculate(VWAPState &state)
{
datetime sessionStart = g_session.GetSessionStart(m_profile);
if(sessionStart != m_lastSessionStart)
{
m_lastSessionStart = sessionStart;
state.sumPV = 0; state.sumV = 0; state.sessionStart = sessionStart;
m_cachedVWAP = 0; m_cachedSlope = 0;
}
MqlTick ticks[];
int copied = CopyTicksRange(_Symbol, ticks, COPY_TICKS_TRADE, sessionStart, TimeCurrent());
if(copied <= 0) { state.isValid = false; return; }
double sumPV = 0; long sumV = 0;
for(int i = 0; i < copied; i++)
{
double price = (ticks[i].bid + ticks[i].ask) / 2.0;
long volume = (long)ticks[i].volume;
if(volume > 0 && price > 0) { sumPV += price * (double)volume; sumV += volume; }
}
if(sumV > 0)
{
state.vwapValue = sumPV / (double)sumV;
state.sumPV = sumPV; state.sumV = (double)sumV; state.isValid = true;
m_cachedVWAP = state.vwapValue;
CalculateSlope(state);
}
else { state.isValid = false; state.vwapValue = m_cachedVWAP; }
}
private:
void CalculateSlope(VWAPState &state)
{
MqlRates rates[];
ArraySetAsSeries(rates, true);
int copied = CopyRates(_Symbol, PERIOD_M15, 0, VWAP_SLOPE_BARS + 2, rates);
if(copied < VWAP_SLOPE_BARS + 2) { state.vwapSlope = m_cachedSlope; return; }
double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
int n = VWAP_SLOPE_BARS;
for(int i = 1; i <= n; i++)
{
double x = (double)i;
double y = rates[i].close - state.vwapValue;
sumX += x; sumY += y; sumXY += x * y; sumX2 += x * x;
}
double denominator = (n * sumX2 - sumX * sumX);
if(denominator != 0) { state.vwapSlope = (n * sumXY - sumX * sumY) / denominator; m_cachedSlope = state.vwapSlope; }
else { state.vwapSlope = m_cachedSlope; }
}
};
#endif // __VWAP_ENGINE_MQH__
+90
View File
@@ -0,0 +1,90 @@
//+------------------------------------------------------------------+
//| Data/Volatility.mqh |
//+------------------------------------------------------------------+
#ifndef __VOLATILITY_MQH__
#define __VOLATILITY_MQH__
#include "../Core/Config.mqh"
class CVolatility
{
private:
int m_atrPeriod;
int m_atrBaseline;
ENUM_TIMEFRAMES m_htf;
ENUM_TIMEFRAMES m_mtf;
int m_handleATR;
int m_handleADX;
int m_handleBB;
double m_atrCurrent;
double m_atrBaselineValue;
double m_atrRelative;
double m_adxValue;
double m_bbWidth;
public:
bool Init(int atrPeriod, int atrBaseline, ENUM_TIMEFRAMES htf, ENUM_TIMEFRAMES mtf)
{
m_atrPeriod = atrPeriod; m_atrBaseline = atrBaseline; m_htf = htf; m_mtf = mtf;
m_handleATR = iATR(_Symbol, m_mtf, m_atrPeriod);
m_handleADX = iADX(_Symbol, m_mtf, ADX_PERIOD);
m_handleBB = iBands(_Symbol, m_mtf, BB_PERIOD, 0, BB_DEVIATIONS, PRICE_CLOSE);
if(m_handleATR == INVALID_HANDLE || m_handleADX == INVALID_HANDLE || m_handleBB == INVALID_HANDLE)
{
Print("[Volatility] Indicator creation failed");
return false;
}
int warmup = MathMax(atrBaseline, BB_PERIOD) + 10;
double dummy[]; ArraySetAsSeries(dummy, true);
CopyBuffer(m_handleATR, 0, 1, warmup, dummy);
Print("[Volatility] Indicators initialized on ", EnumToString(m_mtf));
return true;
}
void Release()
{
RELEASE_HANDLE(m_handleATR);
RELEASE_HANDLE(m_handleADX);
RELEASE_HANDLE(m_handleBB);
}
void Update()
{
double atrBuf[], adxBuf[], bbUp[], bbLow[], bbMid[];
ArraySetAsSeries(atrBuf, true); ArraySetAsSeries(adxBuf, true);
ArraySetAsSeries(bbUp, true); ArraySetAsSeries(bbLow, true); ArraySetAsSeries(bbMid, true);
if(CopyBuffer(m_handleATR, 0, 1, 1, atrBuf) <= 0) return;
m_atrCurrent = atrBuf[0];
if(CopyBuffer(m_handleADX, 0, 1, 1, adxBuf) <= 0) return;
m_adxValue = adxBuf[0];
if(CopyBuffer(m_handleBB, UPPER_BAND, 1, 1, bbUp) <= 0 ||
CopyBuffer(m_handleBB, LOWER_BAND, 1, 1, bbLow) <= 0 ||
CopyBuffer(m_handleBB, BASE_LINE, 1, 1, bbMid) <= 0) return;
if(bbMid[0] != 0) m_bbWidth = (bbUp[0] - bbLow[0]) / bbMid[0]; else m_bbWidth = 0;
CalculateATRBaseline();
}
double GetRelativeATR() const { return m_atrRelative; }
double GetATR() const { return m_atrCurrent; }
double GetADX() const { return m_adxValue; }
double GetBBWidth() const { return m_bbWidth; }
ENUM_REGIME DetectRegime() const
{
if(m_atrRelative >= ATR_TREND_RATIO && m_adxValue >= ADX_TREND_LEVEL) return REGIME_TREND;
else if(m_atrRelative < ATR_CHOP_RATIO && m_adxValue < ADX_CHOP_LEVEL) return REGIME_CHOP;
else if(m_atrRelative < ATR_TREND_RATIO && m_adxValue < ADX_RANGE_LEVEL) return REGIME_RANGE;
return REGIME_RANGE;
}
private:
void CalculateATRBaseline()
{
double atrValues[]; ArraySetAsSeries(atrValues, true);
if(CopyBuffer(m_handleATR, 0, 1, m_atrBaseline, atrValues) < m_atrBaseline)
{ m_atrRelative = 1.0; return; }
double sum = 0;
for(int i = 0; i < m_atrBaseline; i++) sum += atrValues[i];
m_atrBaselineValue = sum / m_atrBaseline;
if(m_atrBaselineValue > 0) m_atrRelative = m_atrCurrent / m_atrBaselineValue;
else m_atrRelative = 1.0;
}
};
#endif // __VOLATILITY_MQH__
+276
View File
@@ -0,0 +1,276 @@
//+------------------------------------------------------------------+
//| Execution/OrderManager.mqh |
//+------------------------------------------------------------------+
#ifndef __ORDER_MANAGER_MQH__
#define __ORDER_MANAGER_MQH__
#include <Trade\Trade.mqh>
#include "../Core/Config.mqh"
#include "../Core/State.mqh"
#include "../Core/Logger.mqh"
extern CLogger g_logger;
class COrderManager
{
private:
CTrade m_trade;
ulong m_magic;
AssetProfile m_profile;
public:
bool Init(ulong magic, const AssetProfile &profile)
{
m_magic = magic;
m_profile = profile;
m_trade.SetExpertMagicNumber(magic);
m_trade.SetDeviationInPoints(10);
m_trade.SetTypeFilling(ORDER_FILLING_IOC);
m_trade.SetAsyncMode(false);
Print("[OrderManager] Execution layer initialized. Magic: ", magic);
return true;
}
bool ExecuteOrder(const SignalData &signal, const TradeParams &params,
EAState &state, ulong &outTicket)
{
outTicket = 0;
if(!ValidateOrder(signal, params)) return false;
bool useLimit = ShouldUseLimitOrder(signal, state);
if(useLimit) return ExecuteLimitOrder(signal, params, state, outTicket);
else return ExecuteMarketOrder(signal, params, state, outTicket);
}
bool ExecuteMarketOrder(const SignalData &signal, const TradeParams &params,
EAState &state, ulong &outTicket)
{
outTicket = 0;
int slippage = CalculateSlippage(signal.atrValue);
m_trade.SetDeviationInPoints(slippage);
bool success = false;
int retries = 0;
while(retries <= MAX_RETRIES && !success)
{
if(retries > 0)
{
int delayMs = RETRY_BASE_MS * (1 << (retries - 1));
g_logger.LogEvent("ORDER", StringFormat("Retry %d/%d after %d ms", retries, MAX_RETRIES, delayMs));
Sleep(delayMs);
}
if(signal.isBuy)
success = m_trade.Buy(params.lotSize, _Symbol, signal.entryPrice, signal.slPrice, signal.tp1Price, InpEALabel);
else
success = m_trade.Sell(params.lotSize, _Symbol, signal.entryPrice, signal.slPrice, signal.tp1Price, InpEALabel);
if(!success)
{
int err = GetLastError();
g_logger.LogError("OrderManager", err, GetErrorDescription(err), retries);
if(!IsRetriableError(err)) { g_logger.LogEvent("ORDER", "Non-retriable error. Aborting."); break; }
if(err == TRADE_RETCODE_INVALID_STOPS)
{
SignalData mutableSignal = signal;
AdjustStops(mutableSignal);
}
else if(err == TRADE_RETCODE_NO_MONEY) { g_logger.LogEvent("ORDER", "No margin. Aborting."); break; }
else if(err == TRADE_RETCODE_MARKET_CLOSED) { g_logger.LogEvent("ORDER", "Market closed."); break; }
}
else outTicket = m_trade.ResultOrder();
retries++;
}
if(success && outTicket > 0)
{
if(PositionSelectByTicket(outTicket))
{
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double lots = PositionGetDouble(POSITION_VOLUME);
g_logger.LogEvent("ORDER", StringFormat("MARKET ORDER Ticket=%llu Price=%.5f Lots=%.2f", outTicket, openPrice, lots));
return true;
}
}
return false;
}
bool ExecuteLimitOrder(const SignalData &signal, const TradeParams &params,
EAState &state, ulong &outTicket)
{
outTicket = 0;
double limitPrice = CalculateLimitPrice(signal);
double currentPrice = signal.isBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
double maxDistance = signal.atrValue * 0.3;
if(signal.isBuy && limitPrice > currentPrice + maxDistance)
return ExecuteMarketOrder(signal, params, state, outTicket);
if(!signal.isBuy && limitPrice < currentPrice - maxDistance)
return ExecuteMarketOrder(signal, params, state, outTicket);
MqlTradeRequest request = {};
MqlTradeResult result = {};
request.action = TRADE_ACTION_PENDING;
request.symbol = _Symbol;
request.volume = params.lotSize;
request.price = limitPrice;
request.sl = signal.slPrice;
request.tp = signal.tp1Price;
request.deviation = CalculateSlippage(signal.atrValue);
request.magic = m_magic;
request.comment = InpEALabel + "_LIMIT";
request.type = signal.isBuy ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_SELL_LIMIT;
request.type_filling = ORDER_FILLING_IOC;
request.expiration = ORDER_TIME_GTC;
bool success = OrderSend(request, result);
if(success && result.retcode == TRADE_RETCODE_DONE)
{
outTicket = result.order;
g_logger.LogEvent("ORDER", StringFormat("LIMIT ORDER Ticket=%llu Price=%.5f Lots=%.2f", outTicket, limitPrice, params.lotSize));
return true;
}
else
{
int err = GetLastError();
g_logger.LogError("OrderManager", err, "Limit order failed", 0);
return ExecuteMarketOrder(signal, params, state, outTicket);
}
}
void CancelStaleOrders(int maxAgeMinutes = 30)
{
int total = OrdersTotal();
datetime now = TimeCurrent();
for(int i = total - 1; i >= 0; i--)
{
ulong ticket = OrderGetTicket(i);
if(ticket == 0) continue;
if(OrderGetString(ORDER_SYMBOL) != _Symbol) continue;
if(OrderGetInteger(ORDER_MAGIC) != m_magic) continue;
datetime orderTime = (datetime)OrderGetInteger(ORDER_TIME_SETUP);
int ageMinutes = (int)((now - orderTime) / 60);
if(ageMinutes > maxAgeMinutes)
{
MqlTradeRequest request = {};
MqlTradeResult result = {};
request.action = TRADE_ACTION_REMOVE;
request.order = ticket;
if(OrderSend(request, result))
g_logger.LogEvent("ORDER", StringFormat("Cancelled stale order %llu (age: %d min)", ticket, ageMinutes));
}
}
}
private:
bool ShouldUseLimitOrder(const SignalData &signal, const EAState &state)
{
if(state.currentRegime == REGIME_RANGE && InpUseLimitOrders) return true;
if(signal.pattern == PATTERN_PIN_BAR || signal.pattern == PATTERN_INSIDE_BAR) return InpUseLimitOrders;
return false;
}
double CalculateLimitPrice(const SignalData &signal)
{
double currentPrice = signal.isBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
double offset = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * 5;
if(signal.isBuy) return currentPrice - offset;
else return currentPrice + offset;
}
bool ValidateOrder(const SignalData &signal, const TradeParams &params)
{
int stopsLevel = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
double minDist = stopsLevel * _Point;
double slDist = MathAbs(signal.entryPrice - signal.slPrice);
double tpDist = MathAbs(signal.entryPrice - signal.tp1Price);
if(slDist < minDist || tpDist < minDist)
{
g_logger.LogEvent("ORDER", "VALIDATION FAIL: SL/TP too close");
return false;
}
int freezeLevel = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);
if(freezeLevel > 0)
{
double currentPrice = signal.isBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
if(MathAbs(signal.entryPrice - currentPrice) > freezeLevel * _Point * 2)
{
g_logger.LogEvent("ORDER", "VALIDATION FAIL: Entry too far");
return false;
}
}
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
if(params.lotSize < minLot || params.lotSize > maxLot)
{
g_logger.LogEvent("ORDER", StringFormat("VALIDATION FAIL: Lot %.2f outside range", params.lotSize));
return false;
}
return true;
}
int CalculateSlippage(double atrValue) const
{
double slippagePrice = atrValue * SLIPPAGE_ATR_MULT;
int slippagePoints = (int)MathRound(slippagePrice / _Point);
return MathMax(MIN_SLIPPAGE_PTS, MathMin(MAX_SLIPPAGE_PTS, slippagePoints));
}
bool IsRetriableError(int err) const
{
switch(err)
{
case TRADE_RETCODE_REQUOTE:
case TRADE_RETCODE_REJECT:
case TRADE_RETCODE_CANCEL:
case TRADE_RETCODE_TIMEOUT:
case TRADE_RETCODE_INVALID:
case TRADE_RETCODE_INVALID_VOLUME:
case TRADE_RETCODE_INVALID_PRICE:
case TRADE_RETCODE_INVALID_STOPS:
case TRADE_RETCODE_TRADE_DISABLED:
case TRADE_RETCODE_PRICE_OFF:
case TRADE_RETCODE_CONNECTION:
case TRADE_RETCODE_PRICE_CHANGED:
return true;
default: return false;
}
}
void AdjustStops(SignalData &signal)
{
int stopsLevel = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
double minDist = stopsLevel * _Point + _Point * 2;
if(signal.isBuy)
{
signal.slPrice = signal.entryPrice - minDist;
if(signal.tp1Price <= signal.entryPrice + minDist)
signal.tp1Price = signal.entryPrice + minDist * 2;
}
else
{
signal.slPrice = signal.entryPrice + minDist;
if(signal.tp1Price >= signal.entryPrice - minDist)
signal.tp1Price = signal.entryPrice - minDist * 2;
}
}
string GetErrorDescription(int err) const
{
switch(err)
{
case TRADE_RETCODE_REQUOTE: return "Requote";
case TRADE_RETCODE_REJECT: return "Rejected";
case TRADE_RETCODE_CANCEL: return "Canceled";
case TRADE_RETCODE_DONE: return "Done";
case TRADE_RETCODE_DONE_PARTIAL: return "Partial";
case TRADE_RETCODE_ERROR: return "Error";
case TRADE_RETCODE_TIMEOUT: return "Timeout";
case TRADE_RETCODE_INVALID: return "Invalid";
case TRADE_RETCODE_INVALID_VOLUME: return "Invalid Volume";
case TRADE_RETCODE_INVALID_PRICE: return "Invalid Price";
case TRADE_RETCODE_INVALID_STOPS: return "Invalid Stops";
case TRADE_RETCODE_TRADE_DISABLED: return "Trade Disabled";
case TRADE_RETCODE_MARKET_CLOSED: return "Market Closed";
case TRADE_RETCODE_NO_MONEY: return "No Money";
case TRADE_RETCODE_PRICE_OFF: return "Price Off";
case TRADE_RETCODE_CONNECTION: return "No Connection";
case TRADE_RETCODE_PRICE_CHANGED: return "Price Changed";
default: return "Unknown " + IntegerToString(err);
}
}
};
#endif // __ORDER_MANAGER_MQH__
+290
View File
@@ -0,0 +1,290 @@
//+------------------------------------------------------------------+
//| Execution/TradeManager.mqh |
//| Trade Lifecycle: Partial Close, BE, Trailing Stop, Time Exit |
//| MODIFIED: Added TP2 Full Close support |
//+------------------------------------------------------------------+
#ifndef __TRADE_MANAGER_MQH__
#define __TRADE_MANAGER_MQH__
#include <Trade\Trade.mqh>
#include "../Core/Config.mqh"
#include "../Core/State.mqh"
#include "../Core/Logger.mqh"
#include "../Data/Volatility.mqh"
#include "OrderManager.mqh"
extern CLogger g_logger;
extern CVolatility g_volatility;
extern EAState g_state;
class CTradeManager
{
private:
CTrade m_trade;
AssetProfile m_profile;
COrderManager *m_orderMgr;
struct TradeTracking
{
ulong ticket;
datetime openTime;
double entryPrice;
double tp1Price;
double tp2Price;
double initialSL;
double partialLot;
bool tp1Hit;
bool tp2Hit;
bool beSet;
bool trailingActive;
ENUM_REGIME openRegime;
};
TradeTracking m_trades[];
int m_tradeCount;
public:
bool Init(const AssetProfile &profile, COrderManager &orderMgr)
{
m_profile = profile;
m_orderMgr = GetPointer(orderMgr);
m_tradeCount = 0;
ArrayResize(m_trades, 10);
Print("[TradeManager] Lifecycle manager initialized (v2.0 with TP2)");
return true;
}
void ManageOpenPositions(EAState &state, CVolatility &vol)
{
int posTotal = PositionsTotal();
if(posTotal == 0) { state.openPositions = 0; return; }
double atr = vol.GetATR();
if(atr <= 0) atr = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * 10;
for(int i = posTotal - 1; i >= 0; i--)
{
if(PositionGetSymbol(i) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
ulong ticket = PositionGetInteger(POSITION_TICKET);
double entry = PositionGetDouble(POSITION_PRICE_OPEN);
double sl = PositionGetDouble(POSITION_SL);
double tp = PositionGetDouble(POSITION_TP);
double lots = PositionGetDouble(POSITION_VOLUME);
datetime openTime = (datetime)PositionGetInteger(POSITION_TIME);
int type = (int)PositionGetInteger(POSITION_TYPE);
int idx = FindTradeIndex(ticket);
if(idx < 0) idx = RegisterTrade(ticket, entry, tp, sl, openTime);
double currentPrice = (type == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
if(!m_trades[idx].tp1Hit && m_trades[idx].tp1Price > 0)
{
bool hitTP1 = (type == POSITION_TYPE_BUY && currentPrice >= m_trades[idx].tp1Price) ||
(type == POSITION_TYPE_SELL && currentPrice <= m_trades[idx].tp1Price);
if(hitTP1) { m_trades[idx].tp1Hit = true; PartialClose(idx, lots, ticket); }
}
if(m_trades[idx].tp1Hit && !m_trades[idx].tp2Hit && m_trades[idx].tp2Price > 0)
{
bool hitTP2 = (type == POSITION_TYPE_BUY && currentPrice >= m_trades[idx].tp2Price) ||
(type == POSITION_TYPE_SELL && currentPrice <= m_trades[idx].tp2Price);
if(hitTP2)
{
m_trades[idx].tp2Hit = true;
ClosePosition(ticket, EXIT_TP2);
g_logger.LogEvent("TRADE", StringFormat("TP2 Full Close ticket %llu at %.5f", ticket, currentPrice));
RemoveTrade(idx);
continue;
}
}
if(m_trades[idx].tp1Hit && !m_trades[idx].beSet)
SetBreakEven(idx, entry, sl, type, atr);
if(m_trades[idx].beSet && m_trades[idx].trailingActive)
UpdateTrailingStop(idx, currentPrice, type, atr, sl);
if(m_trades[idx].openRegime == REGIME_RANGE)
{
int elapsed = (int)(TimeCurrent() - openTime);
if(elapsed >= m_profile.maxTradeDuration * 60)
{
g_logger.LogEvent("TRADE", StringFormat("Time exit ticket %llu after %d min", ticket, elapsed/60));
ClosePosition(ticket, EXIT_TIME);
RemoveTrade(idx);
continue;
}
}
}
state.openPositions = CountOurPositions();
}
void CheckClosedTrades(EAState &state)
{
for(int i = m_tradeCount - 1; i >= 0; i--)
{
if(!PositionSelectByTicket(m_trades[i].ticket))
{
state.lastTradeClose = TimeCurrent();
state.totalTradesToday++;
state.totalTradesWeek++;
RemoveTrade(i);
}
}
}
void CloseAllPositions(EAState &state, ENUM_EXIT_REASON reason)
{
int posTotal = PositionsTotal();
for(int i = posTotal - 1; i >= 0; i--)
{
if(PositionGetSymbol(i) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
ulong ticket = PositionGetInteger(POSITION_TICKET);
ClosePosition(ticket, reason);
}
ArrayResize(m_trades, 10);
m_tradeCount = 0;
state.openPositions = 0;
}
void CloseRangeTrades(EAState &state)
{
for(int i = m_tradeCount - 1; i >= 0; i--)
{
if(m_trades[i].openRegime == REGIME_RANGE)
{
if(PositionSelectByTicket(m_trades[i].ticket))
ClosePosition(m_trades[i].ticket, EXIT_REGIME_CHANGE);
RemoveTrade(i);
}
}
}
void TightenStops(EAState &state)
{
double atr = g_volatility.GetATR();
for(int i = 0; i < m_tradeCount; i++)
{
if(!PositionSelectByTicket(m_trades[i].ticket)) continue;
double entry = PositionGetDouble(POSITION_PRICE_OPEN);
double currentSL = PositionGetDouble(POSITION_SL);
int type = (int)PositionGetInteger(POSITION_TYPE);
double newSL;
double buffer = atr * 0.5;
if(type == POSITION_TYPE_BUY)
{
newSL = entry + buffer;
if(newSL > currentSL || currentSL == 0)
m_trade.PositionModify(m_trades[i].ticket, newSL, PositionGetDouble(POSITION_TP));
}
else
{
newSL = entry - buffer;
if(newSL < currentSL || currentSL == 0)
m_trade.PositionModify(m_trades[i].ticket, newSL, PositionGetDouble(POSITION_TP));
}
}
}
private:
int FindTradeIndex(ulong ticket) const
{
for(int i = 0; i < m_tradeCount; i++)
if(m_trades[i].ticket == ticket) return i;
return -1;
}
int RegisterTrade(ulong ticket, double entry, double tp1, double sl, datetime time)
{
if(m_tradeCount >= ArraySize(m_trades)) ArrayResize(m_trades, ArraySize(m_trades) + 10);
int idx = m_tradeCount++;
m_trades[idx].ticket = ticket;
m_trades[idx].entryPrice = entry;
m_trades[idx].tp1Price = tp1;
m_trades[idx].initialSL = sl;
m_trades[idx].openTime = time;
m_trades[idx].tp1Hit = false;
m_trades[idx].tp2Hit = false;
m_trades[idx].beSet = false;
m_trades[idx].trailingActive = true;
m_trades[idx].openRegime = g_state.currentRegime;
m_trades[idx].partialLot = 0;
return idx;
}
void RemoveTrade(int idx)
{
if(idx < 0 || idx >= m_tradeCount) return;
for(int i = idx; i < m_tradeCount - 1; i++)
m_trades[i] = m_trades[i + 1];
m_tradeCount--;
}
void PartialClose(int idx, double totalLots, ulong ticket)
{
double closeLots = NormalizeDouble(totalLots * m_profile.partialCloseRatio, 2);
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
if(closeLots < minLot) closeLots = minLot;
if(closeLots >= totalLots) closeLots = totalLots * 0.5;
m_trades[idx].partialLot = closeLots;
if(m_trade.PositionClosePartial(ticket, closeLots))
g_logger.LogEvent("TRADE", StringFormat("Partial close %.2f lots ticket %llu", closeLots, ticket));
else
g_logger.LogEvent("TRADE", StringFormat("Partial close FAILED ticket %llu", ticket));
}
void SetBreakEven(int idx, double entry, double currentSL, int type, double atr)
{
double buffer = atr * BE_BUFFER_ATR_MULT;
double newSL;
if(type == POSITION_TYPE_BUY) newSL = entry + buffer;
else newSL = entry - buffer;
bool shouldMove = (type == POSITION_TYPE_BUY && (newSL > currentSL || currentSL == 0)) ||
(type == POSITION_TYPE_SELL && (newSL < currentSL || currentSL == 0));
if(shouldMove)
{
double currentTP = PositionGetDouble(POSITION_TP);
if(m_trade.PositionModify(m_trades[idx].ticket, newSL, currentTP))
{
m_trades[idx].beSet = true;
g_logger.LogEvent("TRADE", StringFormat("BE set ticket %llu at %.5f", m_trades[idx].ticket, newSL));
}
}
}
void UpdateTrailingStop(int idx, double currentPrice, int type, double atr, double currentSL)
{
double trailDist = atr * m_profile.trailingATRMult;
double newSL;
if(type == POSITION_TYPE_BUY)
{
newSL = currentPrice - trailDist;
if(newSL > currentSL)
{
double currentTP = PositionGetDouble(POSITION_TP);
m_trade.PositionModify(m_trades[idx].ticket, newSL, currentTP);
}
}
else
{
newSL = currentPrice + trailDist;
if(newSL < currentSL || currentSL == 0)
{
double currentTP = PositionGetDouble(POSITION_TP);
m_trade.PositionModify(m_trades[idx].ticket, newSL, currentTP);
}
}
}
void ClosePosition(ulong ticket, ENUM_EXIT_REASON reason)
{
if(m_trade.PositionClose(ticket))
g_logger.LogEvent("TRADE", StringFormat("Closed ticket %llu. Reason: %s", ticket, EnumToString(reason)));
}
int CountOurPositions() const
{
int count = 0;
int total = PositionsTotal();
for(int i = 0; i < total; i++)
if(PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber)
count++;
return count;
}
};
#endif // __TRADE_MANAGER_MQH__
+46
View File
@@ -0,0 +1,46 @@
//+------------------------------------------------------------------+
//| Logic/ContextFilter.mqh |
//+------------------------------------------------------------------+
#ifndef __CONTEXT_FILTER_MQH__
#define __CONTEXT_FILTER_MQH__
#include "../Core/Config.mqh"
#include "../Core/State.mqh"
#include "../Data/Volatility.mqh"
class CContextFilter
{
private:
ENUM_TIMEFRAMES m_mtf;
CVolatility *m_vol;
public:
bool Init(ENUM_TIMEFRAMES mtf, CVolatility &vol)
{
m_mtf = mtf; m_vol = GetPointer(vol);
Print("[ContextFilter] MTF analysis initialized on ", EnumToString(mtf));
return true;
}
void Release() {}
void Analyze(EAState &state)
{
state.volumeConfirmed = CheckVolume();
m_vol.Update();
state.currentRegime = m_vol.DetectRegime();
}
private:
bool CheckVolume()
{
MqlRates rates[]; ArraySetAsSeries(rates, true);
if(CopyRates(_Symbol, m_mtf, 0, VOLUME_MA_PERIOD + 2, rates) < VOLUME_MA_PERIOD + 2) return false;
double sumVol = 0;
for(int i = 1; i <= VOLUME_MA_PERIOD; i++) sumVol += (double)rates[i].tick_volume;
double volMA = sumVol / VOLUME_MA_PERIOD;
double currentVol = (double)rates[1].tick_volume;
if(volMA > 0) return (currentVol >= volMA * MIN_VOLUME_RATIO);
return false;
}
};
#endif // __CONTEXT_FILTER_MQH__
+72
View File
@@ -0,0 +1,72 @@
//+------------------------------------------------------------------+
//| Logic/MacroAudit.mqh |
//+------------------------------------------------------------------+
#ifndef __MACRO_AUDIT_MQH__
#define __MACRO_AUDIT_MQH__
#include "../Core/Config.mqh"
#include "../Core/State.mqh"
#include "../Core/Logger.mqh"
#include "../Data/VWAP_Engine.mqh"
#include "../Data/PriceEngine.mqh"
extern CLogger g_logger;
extern CPriceEngine g_priceEngine;
class CMacroAudit
{
private:
ENUM_TIMEFRAMES m_htf;
CVWAPEngine *m_vwap;
public:
bool Init(ENUM_TIMEFRAMES htf, CVWAPEngine &vwap)
{
m_htf = htf; m_vwap = GetPointer(vwap);
Print("[MacroAudit] HTF analysis initialized on ", EnumToString(htf));
return true;
}
void Release() {}
void Analyze(EAState &state)
{
if(!state.vwapState.isValid) { state.currentBias = BIAS_NEUTRAL; return; }
MqlRates currentBar;
if(!g_priceEngine.GetClosedBar(m_htf, 1, currentBar)) { state.currentBias = BIAS_NEUTRAL; return; }
double price = currentBar.close;
double vwap = state.vwapState.vwapValue;
double slope = state.vwapState.vwapSlope;
bool aboveVWAP = (price > vwap * 1.005);
bool belowVWAP = (price < vwap * 0.995);
bool risingVWAP = (slope > 0);
bool fallingVWAP = (slope < 0);
int highestIdx = iHighest(_Symbol, m_htf, MODE_HIGH, SWING_LOOKBACK, 1);
int lowestIdx = iLowest(_Symbol, m_htf, MODE_LOW, SWING_LOOKBACK, 1);
if(highestIdx < 0 || lowestIdx < 0) { state.currentBias = BIAS_NEUTRAL; return; }
double swingHigh = iHigh(_Symbol, m_htf, highestIdx);
double swingLow = iLow(_Symbol, m_htf, lowestIdx);
state.swingHigh = swingHigh; state.swingLow = swingLow;
bool bullBOS = (currentBar.close > swingHigh);
bool bearBOS = (currentBar.close < swingLow);
bool volConfirmed = false;
int volHandle = iMA(_Symbol, m_htf, 20, 0, MODE_SMA, VOLUME_TICK);
if(volHandle != INVALID_HANDLE)
{
double volMABuf[]; ArraySetAsSeries(volMABuf, true);
if(CopyBuffer(volHandle, 0, 1, 1, volMABuf) > 0)
{
double avgVol = volMABuf[0];
if(avgVol > 0) volConfirmed = (currentBar.tick_volume >= avgVol * VOLUME_CONFIRM);
}
IndicatorRelease(volHandle);
}
state.bosBullish = bullBOS && volConfirmed;
state.bosBearish = bearBOS && volConfirmed;
if(aboveVWAP && risingVWAP && state.bosBullish) state.currentBias = BIAS_BULL;
else if(belowVWAP && fallingVWAP && state.bosBearish) state.currentBias = BIAS_BEAR;
else if((aboveVWAP && risingVWAP) || state.bosBullish) state.currentBias = BIAS_BULL;
else if((belowVWAP && fallingVWAP) || state.bosBearish) state.currentBias = BIAS_BEAR;
else state.currentBias = BIAS_NEUTRAL;
}
};
#endif // __MACRO_AUDIT_MQH__
+163
View File
@@ -0,0 +1,163 @@
//+------------------------------------------------------------------+
//| Logic/MicroTrigger.mqh |
//+------------------------------------------------------------------+
#ifndef __MICRO_TRIGGER_MQH__
#define __MICRO_TRIGGER_MQH__
#include "../Core/Config.mqh"
#include "../Core/State.mqh"
#include "../Data/PriceEngine.mqh"
#include "../Data/Volatility.mqh"
#include "../Core/Logger.mqh"
extern CLogger g_logger;
extern CVolatility g_volatility;
class CMicroTrigger
{
private:
ENUM_TIMEFRAMES m_ltf;
CPriceEngine *m_price;
public:
bool Init(ENUM_TIMEFRAMES ltf, CPriceEngine &price)
{
m_ltf = ltf; m_price = GetPointer(price);
Print("[MicroTrigger] LTF entry logic initialized on ", EnumToString(ltf));
return true;
}
void Release() {}
void GenerateSignal(SignalData &signal, const EAState &state, CPriceEngine &price)
{
signal.isValid = false; signal.isBuy = false; signal.pattern = PATTERN_NONE;
signal.rejectionReason = ""; signal.signalTime = TimeCurrent(); signal.atrValue = 0;
if(state.currentBias == BIAS_NEUTRAL && state.currentRegime != REGIME_RANGE)
{ signal.rejectionReason = "HTF Bias Neutral + Not Range Mode"; return; }
MqlRates bars[4];
if(!price.GetClosedBar(m_ltf, 1, bars[1]) || !price.GetClosedBar(m_ltf, 2, bars[2]))
{ signal.rejectionReason = "Failed to load LTF closed bars"; return; }
if(CheckPinBar(bars[1], state))
{
signal.pattern = PATTERN_PIN_BAR; signal.patternName = "Pin Bar";
signal.isBuy = (bars[1].close > bars[1].open);
if(ValidateDirection(signal, state)) { CalculateLevels(signal, bars[1], state); return; }
}
if(!price.GetClosedBar(m_ltf, 2, bars[2])) { signal.rejectionReason = "Failed to load bar[2]"; return; }
if(CheckEngulfing(bars[1], bars[2]))
{
signal.pattern = PATTERN_ENGULFING; signal.patternName = "Engulfing";
signal.isBuy = (bars[1].close > bars[1].open);
if(ValidateDirection(signal, state)) { CalculateLevels(signal, bars[1], state); return; }
}
if(price.GetClosedBar(m_ltf, 3, bars[3]))
{
if(CheckInsideBarBreakout(bars[1], bars[2], bars[3]))
{
signal.pattern = PATTERN_INSIDE_BAR; signal.patternName = "Inside Bar Breakout";
signal.isBuy = (bars[1].close > bars[2].high);
if(ValidateDirection(signal, state)) { CalculateLevels(signal, bars[1], state); return; }
}
}
signal.rejectionReason = "No valid price action pattern";
}
private:
bool ValidateDirection(SignalData &signal, const EAState &state)
{
if(state.currentRegime == REGIME_RANGE) return true;
if(state.currentBias == BIAS_BULL && !signal.isBuy)
{ signal.isValid = false; signal.rejectionReason = "Bearish signal rejected (HTF Bias: BULL)"; return false; }
if(state.currentBias == BIAS_BEAR && signal.isBuy)
{ signal.isValid = false; signal.rejectionReason = "Bullish signal rejected (HTF Bias: BEAR)"; return false; }
signal.isValid = true; return true;
}
bool CheckPinBar(const MqlRates &bar, const EAState &state)
{
double body = MathAbs(bar.close - bar.open);
double upperWick = bar.high - MathMax(bar.open, bar.close);
double lowerWick = MathMin(bar.open, bar.close) - bar.low;
double range = bar.high - bar.low;
if(range == 0 || body == 0) return false;
bool bullish = (bar.close > bar.open);
if(bullish)
{
bool wickOK = (lowerWick >= body * PIN_BAR_WICK_MULT);
bool closePos = (bar.close >= bar.low + range * 0.7);
bool atLevel = IsAtKeyLevel(bar, state, true);
return wickOK && closePos && atLevel;
}
else
{
bool wickOK = (upperWick >= body * PIN_BAR_WICK_MULT);
bool closePos = (bar.close <= bar.low + range * 0.3);
bool atLevel = IsAtKeyLevel(bar, state, false);
return wickOK && closePos && atLevel;
}
}
bool CheckEngulfing(const MqlRates &curr, const MqlRates &prev)
{
bool bullish = (curr.close > prev.open && curr.open < prev.close);
bool bearish = (curr.close < prev.open && curr.open > prev.close);
if(!bullish && !bearish) return false;
return (curr.tick_volume >= prev.tick_volume * ENGULF_VOLUME_MULT);
}
bool CheckInsideBarBreakout(const MqlRates &breakout, const MqlRates &inside, const MqlRates &mother)
{
bool isInside = (inside.high < mother.high && inside.low > mother.low);
if(!isInside) return false;
bool bullBreak = (breakout.close > inside.high);
bool bearBreak = (breakout.close < inside.low);
return (bullBreak || bearBreak);
}
bool IsAtKeyLevel(const MqlRates &bar, const EAState &state, bool isBullish)
{
double proximity = state.assetProfile.atrMultiplierSL * g_volatility.GetATR() * 0.5;
if(MathAbs(bar.close - state.vwapState.vwapValue) <= proximity) return true;
if(isBullish && MathAbs(bar.low - state.swingLow) <= proximity) return true;
if(!isBullish && MathAbs(bar.high - state.swingHigh) <= proximity) return true;
int maHandle = iMA(_Symbol, m_ltf, 50, 0, MODE_EMA, PRICE_CLOSE);
if(maHandle != INVALID_HANDLE)
{
double maBuf[]; ArraySetAsSeries(maBuf, true);
if(CopyBuffer(maHandle, 0, 1, 1, maBuf) > 0)
{
double ema50 = maBuf[0];
IndicatorRelease(maHandle);
if(MathAbs(bar.close - ema50) <= proximity) return true;
}
IndicatorRelease(maHandle);
}
return false;
}
void CalculateLevels(SignalData &signal, const MqlRates &bar, const EAState &state)
{
double atr = g_volatility.GetATR();
if(atr <= 0) atr = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * 10;
signal.atrValue = atr;
if(signal.isBuy) signal.entryPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
else signal.entryPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double slMult, tp1Mult, tp2Mult;
if(state.currentRegime == REGIME_TREND)
{ slMult = InpTrendATRMult; tp1Mult = InpTrendATRMult * 2.0; tp2Mult = InpTrendATRMult * 4.0; }
else
{ slMult = InpRangeATRMult; tp1Mult = InpRangeATRMult * 1.5; tp2Mult = InpRangeATRMult * 2.5; }
double slDist = atr * slMult;
double tp1Dist = atr * tp1Mult;
double tp2Dist = atr * tp2Mult;
if(signal.isBuy)
{
signal.slPrice = signal.entryPrice - slDist;
signal.tp1Price = signal.entryPrice + tp1Dist;
signal.tp2Price = signal.entryPrice + tp2Dist;
}
else
{
signal.slPrice = signal.entryPrice + slDist;
signal.tp1Price = signal.entryPrice - tp1Dist;
signal.tp2Price = signal.entryPrice - tp2Dist;
}
signal.isValid = true;
}
};
#endif // __MICRO_TRIGGER_MQH__
+140
View File
@@ -0,0 +1,140 @@
//+------------------------------------------------------------------+
//| Logic/NewsFilter.mqh |
//| Economic News Filter |
//+------------------------------------------------------------------+
#ifndef __NEWS_FILTER_MQH__
#define __NEWS_FILTER_MQH__
#include "../Core/Config.mqh"
#include "../Core/State.mqh"
#include "../Core/Logger.mqh"
extern CLogger g_logger;
class CNewsFilter
{
private:
string m_currency;
int m_minutesBefore;
int m_minutesAfter;
bool m_initialized;
struct NewsEvent
{
datetime time;
string currency;
string event;
int impact;
};
NewsEvent m_events[];
datetime m_lastCalendarUpdate;
public:
CNewsFilter() : m_minutesBefore(30), m_minutesAfter(15), m_initialized(false) {}
bool Init(int minutesBefore = 30, int minutesAfter = 15)
{
m_minutesBefore = minutesBefore;
m_minutesAfter = minutesAfter;
string sym = _Symbol;
if(StringFind(sym, "USD") >= 0) m_currency = "USD";
else if(StringFind(sym, "EUR") >= 0) m_currency = "EUR";
else if(StringFind(sym, "GBP") >= 0) m_currency = "GBP";
else if(StringFind(sym, "JPY") >= 0) m_currency = "JPY";
else if(StringFind(sym, "AUD") >= 0) m_currency = "AUD";
else if(StringFind(sym, "CAD") >= 0) m_currency = "CAD";
else if(StringFind(sym, "CHF") >= 0) m_currency = "CHF";
else if(StringFind(sym, "NZD") >= 0) m_currency = "NZD";
else m_currency = "USD";
m_initialized = true;
m_lastCalendarUpdate = 0;
Print("[NewsFilter] Initialized for ", m_currency);
return true;
}
bool IsTradingAllowed()
{
if(!m_initialized) return true;
datetime now = TimeCurrent();
if(now - m_lastCalendarUpdate > 3600) { UpdateCalendar(); m_lastCalendarUpdate = now; }
for(int i = 0; i < ArraySize(m_events); i++)
{
if(m_events[i].impact < 3) continue;
datetime blockStart = m_events[i].time - m_minutesBefore * 60;
datetime blockEnd = m_events[i].time + m_minutesAfter * 60;
if(now >= blockStart && now <= blockEnd)
{
g_logger.LogEvent("NEWS", StringFormat("TRADING BLOCKED: %s at %s", m_events[i].event, TimeToString(m_events[i].time)));
return false;
}
}
return true;
}
private:
void UpdateCalendar()
{
ArrayResize(m_events, 0);
string filename = "NewsCalendar_" + m_currency + ".csv";
if(FileIsExist(filename, FILE_COMMON))
{
int handle = FileOpen(filename, FILE_READ|FILE_CSV|FILE_COMMON, ',');
if(handle != INVALID_HANDLE)
{
while(!FileIsEnding(handle))
{
string dateStr = FileReadString(handle);
string timeStr = FileReadString(handle);
string currency = FileReadString(handle);
string event = FileReadString(handle);
string impactStr = FileReadString(handle);
if(dateStr == "" || timeStr == "") continue;
datetime eventTime = StringToTime(dateStr + " " + timeStr);
int impact = (int)StringToInteger(impactStr);
if(impact >= 3 && (currency == m_currency || currency == "ALL"))
{
int idx = ArraySize(m_events);
ArrayResize(m_events, idx + 1);
m_events[idx].time = eventTime;
m_events[idx].currency = currency;
m_events[idx].event = event;
m_events[idx].impact = impact;
}
}
FileClose(handle);
}
}
if(ArraySize(m_events) == 0) AddBuiltinEvents();
}
void AddBuiltinEvents()
{
datetime now = TimeCurrent();
MqlDateTime dt;
TimeToStruct(now, dt);
for(int monthOffset = 0; monthOffset <= 1; monthOffset++)
{
int year = dt.year;
int month = dt.mon + monthOffset;
if(month > 12) { month = 1; year++; }
datetime firstDay = StringToTime(StringFormat("%04d.%02d.01 00:00:00", year, month));
MqlDateTime firstDt;
TimeToStruct(firstDay, firstDt);
int daysToFriday = (5 - firstDt.day_of_week + 7) % 7;
datetime firstFriday = firstDay + daysToFriday * 86400;
datetime nfpTime = firstFriday + 13 * 3600 + 30 * 60;
if(nfpTime > now - 86400)
{
int idx = ArraySize(m_events);
ArrayResize(m_events, idx + 1);
m_events[idx].time = nfpTime;
m_events[idx].currency = "USD";
m_events[idx].event = "Non-Farm Payrolls";
m_events[idx].impact = 3;
}
}
}
};
#endif // __NEWS_FILTER_MQH__
+266
View File
@@ -0,0 +1,266 @@
//+------------------------------------------------------------------+
//| Logic/RegimeEngine.mqh |
//| Dual-State Logic with ML-based Regime Detection |
//+------------------------------------------------------------------+
#ifndef __REGIME_ENGINE_MQH__
#define __REGIME_ENGINE_MQH__
#include "../Core/Config.mqh"
#include "../Core/State.mqh"
#include "../Core/Logger.mqh"
#include "../Execution/TradeManager.mqh"
extern CLogger g_logger;
extern CTradeManager g_tradeManager;
struct MLFeatureVector
{
double atrRatio;
double adx;
double bbWidth;
double volumeRatio;
double priceMomentum;
};
struct MLRegimeSample
{
MLFeatureVector features;
ENUM_REGIME regime;
};
class CMLRegimeClassifier
{
private:
MLRegimeSample m_trainingData[];
int m_k;
bool m_initialized;
public:
CMLRegimeClassifier() : m_k(5), m_initialized(false) {}
bool Init()
{
LoadDefaultTrainingData();
m_initialized = true;
Print("[MLRegimeClassifier] KNN initialized with ", ArraySize(m_trainingData), " samples");
return true;
}
ENUM_REGIME Predict(const MLFeatureVector &features)
{
if(!m_initialized || ArraySize(m_trainingData) == 0) return REGIME_RANGE;
double distances[];
ArrayResize(distances, ArraySize(m_trainingData));
for(int i = 0; i < ArraySize(m_trainingData); i++)
distances[i] = CalculateDistance(features, m_trainingData[i].features);
int trendVotes = 0, rangeVotes = 0, chopVotes = 0;
for(int k = 0; k < m_k; k++)
{
int nearestIdx = FindMinIndex(distances);
if(nearestIdx < 0) break;
ENUM_REGIME vote = m_trainingData[nearestIdx].regime;
if(vote == REGIME_TREND) trendVotes++;
else if(vote == REGIME_RANGE) rangeVotes++;
else chopVotes++;
distances[nearestIdx] = DBL_MAX;
}
if(trendVotes >= rangeVotes && trendVotes >= chopVotes) return REGIME_TREND;
if(rangeVotes >= trendVotes && rangeVotes >= chopVotes) return REGIME_RANGE;
return REGIME_CHOP;
}
double GetConfidence(const MLFeatureVector &features)
{
if(!m_initialized || ArraySize(m_trainingData) == 0) return 0.5;
double distances[];
ArrayResize(distances, ArraySize(m_trainingData));
for(int i = 0; i < ArraySize(m_trainingData); i++)
distances[i] = CalculateDistance(features, m_trainingData[i].features);
int trendVotes = 0, rangeVotes = 0, chopVotes = 0;
for(int k = 0; k < m_k; k++)
{
int nearestIdx = FindMinIndex(distances);
if(nearestIdx < 0) break;
ENUM_REGIME vote = m_trainingData[nearestIdx].regime;
if(vote == REGIME_TREND) trendVotes++;
else if(vote == REGIME_RANGE) rangeVotes++;
else chopVotes++;
distances[nearestIdx] = DBL_MAX;
}
int maxVotes = MathMax(trendVotes, MathMax(rangeVotes, chopVotes));
return (double)maxVotes / m_k;
}
private:
double CalculateDistance(const MLFeatureVector &a, const MLFeatureVector &b)
{
double d1 = (a.atrRatio - b.atrRatio) / 2.0;
double d2 = (a.adx - b.adx) / 50.0;
double d3 = (a.bbWidth - b.bbWidth) / 0.1;
double d4 = (a.volumeRatio - b.volumeRatio) / 2.0;
double d5 = (a.priceMomentum - b.priceMomentum) / 0.05;
return MathSqrt(d1*d1 + d2*d2 + d3*d3 + d4*d4 + d5*d5);
}
int FindMinIndex(double &arr[])
{
if(ArraySize(arr) == 0) return -1;
int minIdx = 0;
for(int i = 1; i < ArraySize(arr); i++)
if(arr[i] < arr[minIdx]) minIdx = i;
return arr[minIdx] == DBL_MAX ? -1 : minIdx;
}
void LoadDefaultTrainingData()
{
AddSample(2.0, 35.0, 0.08, 1.5, 0.03, REGIME_TREND);
AddSample(1.5, 28.0, 0.06, 1.3, 0.02, REGIME_TREND);
AddSample(3.0, 40.0, 0.12, 2.0, 0.05, REGIME_TREND);
AddSample(0.5, 15.0, 0.02, 0.8, 0.01, REGIME_RANGE);
AddSample(0.7, 18.0, 0.03, 0.9, -0.01, REGIME_RANGE);
AddSample(0.4, 12.0, 0.015, 0.6, 0.005, REGIME_RANGE);
AddSample(0.3, 8.0, 0.01, 0.5, 0.002, REGIME_CHOP);
AddSample(0.6, 10.0, 0.025, 0.7, -0.005, REGIME_CHOP);
AddSample(0.8, 14.0, 0.04, 0.8, 0.008, REGIME_CHOP);
AddSample(1.8, 22.0, 0.05, 1.1, 0.015, REGIME_TREND);
AddSample(0.9, 16.0, 0.035, 0.85, -0.003, REGIME_RANGE);
AddSample(0.2, 5.0, 0.008, 0.4, 0.001, REGIME_CHOP);
}
void AddSample(double atr, double adx, double bbw, double vol, double mom, ENUM_REGIME regime)
{
int idx = ArraySize(m_trainingData);
ArrayResize(m_trainingData, idx + 1);
m_trainingData[idx].features.atrRatio = atr;
m_trainingData[idx].features.adx = adx;
m_trainingData[idx].features.bbWidth = bbw;
m_trainingData[idx].features.volumeRatio = vol;
m_trainingData[idx].features.priceMomentum = mom;
m_trainingData[idx].regime = regime;
}
};
class CRegimeEngine
{
private:
ENUM_REGIME m_lastRegime;
bool m_initialized;
CMLRegimeClassifier m_mlClassifier;
double m_mlConfidence;
public:
bool Init()
{
m_lastRegime = REGIME_RANGE;
m_initialized = true;
m_mlConfidence = 0.0;
if(!m_mlClassifier.Init())
Print("[RegimeEngine] ML classifier init failed. Using traditional method only.");
Print("[RegimeEngine] Dual-state logic initialized (v2.0 with ML)");
return true;
}
void Release() {}
void UpdateState(EAState &state)
{
ENUM_REGIME newRegime = state.currentRegime;
ENUM_REGIME mlRegime = GetMLPrediction(state);
double mlConfidence = m_mlClassifier.GetConfidence(GetCurrentFeatures(state));
if(mlConfidence > 0.6 && mlRegime != newRegime)
{
if(mlRegime == REGIME_CHOP && newRegime != REGIME_CHOP)
{
g_logger.LogEvent("REGIME", StringFormat("ML override: %s -> CHOP (conf: %.2f)", EnumToString(newRegime), mlConfidence));
newRegime = REGIME_CHOP;
}
else if(mlRegime == REGIME_TREND && newRegime == REGIME_RANGE && mlConfidence > 0.75)
{
g_logger.LogEvent("REGIME", StringFormat("ML override: RANGE -> TREND (conf: %.2f)", mlConfidence));
newRegime = REGIME_TREND;
}
}
if(!m_initialized) return;
if(newRegime != m_lastRegime)
{
HandleRegimeChange(m_lastRegime, newRegime, state);
m_lastRegime = newRegime;
}
m_mlConfidence = mlConfidence;
}
string GetStrategyName(const EAState &state) const
{
if(state.currentRegime == REGIME_TREND && state.currentBias != BIAS_NEUTRAL)
return "MOMENTUM (Trend Following)";
else if(state.currentRegime == REGIME_RANGE && state.currentBias == BIAS_NEUTRAL)
return "MEAN REVERSION (Range Trading)";
else if(state.currentRegime == REGIME_CHOP)
return "CAPITAL PRESERVATION (No Trade)";
else
return "MIXED (Caution)";
}
double GetMLConfidence() const { return m_mlConfidence; }
private:
void HandleRegimeChange(ENUM_REGIME oldRegime, ENUM_REGIME newRegime, EAState &state)
{
string msg = StringFormat("REGIME CHANGE: %s -> %s", EnumToString(oldRegime), EnumToString(newRegime));
g_logger.LogEvent("REGIME", msg);
if(newRegime == REGIME_CHOP)
{
g_logger.LogEvent("REGIME", "CHOP detected. Capital preservation mode. Closing ALL.");
g_tradeManager.CloseAllPositions(state, EXIT_REGIME_CHANGE);
return;
}
if(oldRegime == REGIME_TREND && newRegime == REGIME_RANGE)
{
g_logger.LogEvent("REGIME", "Trend->Range. Tightening trailing stops.");
g_tradeManager.TightenStops(state);
}
if(oldRegime == REGIME_RANGE && newRegime == REGIME_TREND)
{
g_logger.LogEvent("REGIME", "Range->Trend. Closing mean-reversion trades.");
g_tradeManager.CloseRangeTrades(state);
}
}
ENUM_REGIME GetMLPrediction(const EAState &state)
{
MLFeatureVector features = GetCurrentFeatures(state);
return m_mlClassifier.Predict(features);
}
MLFeatureVector GetCurrentFeatures(const EAState &state)
{
MLFeatureVector fv;
double atr = g_volatility.GetATR();
double atrBaseline = 0;
int atrHandle = iATR(_Symbol, InpMTF, 14);
if(atrHandle != INVALID_HANDLE)
{
double atrBuf[];
ArraySetAsSeries(atrBuf, true);
if(CopyBuffer(atrHandle, 0, 1, 50, atrBuf) >= 50)
{
double sum = 0;
for(int i = 0; i < 50; i++) sum += atrBuf[i];
atrBaseline = sum / 50.0;
}
IndicatorRelease(atrHandle);
}
fv.atrRatio = (atrBaseline > 0) ? atr / atrBaseline : 1.0;
fv.adx = g_volatility.GetADX();
fv.bbWidth = g_volatility.GetBBWidth();
fv.volumeRatio = state.volumeConfirmed ? 1.2 : 0.8;
MqlRates rates[];
ArraySetAsSeries(rates, true);
if(CopyRates(_Symbol, InpMTF, 1, 6, rates) >= 6)
fv.priceMomentum = (rates[0].close - rates[5].close) / rates[5].close;
else
fv.priceMomentum = 0;
return fv;
}
};
#endif // __REGIME_ENGINE_MQH__
+110
View File
@@ -0,0 +1,110 @@
//+------------------------------------------------------------------+
//| Risk/PortfolioManager.mqh |
//+------------------------------------------------------------------+
#ifndef __PORTFOLIO_MANAGER_MQH__
#define __PORTFOLIO_MANAGER_MQH__
#include "../Core/Config.mqh"
#include "../Core/State.mqh"
#include "../Core/Logger.mqh"
extern CLogger g_logger;
class CPortfolioManager
{
private:
int m_corrLookback;
ENUM_TIMEFRAMES m_mtf;
double m_maxTotalRiskPercent;
public:
bool Init(int lookback, ENUM_TIMEFRAMES mtf)
{
m_corrLookback = lookback; m_mtf = mtf; m_maxTotalRiskPercent = InpMaxTotalRisk;
Print("[PortfolioManager] Correlation lookback: ", lookback, " bars");
return true;
}
void UpdateState(EAState &state)
{
state.openPositions = 0; double totalRiskAmount = 0;
int posTotal = PositionsTotal();
for(int i = posTotal - 1; i >= 0; i--)
{
string sym = PositionGetSymbol(i);
if(sym != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
state.openPositions++;
double lots = PositionGetDouble(POSITION_VOLUME);
double entry = PositionGetDouble(POSITION_PRICE_OPEN);
double sl = PositionGetDouble(POSITION_SL);
double slDist = MathAbs(entry - sl);
double tickValue = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE);
if(tickSize > 0) { double ticks = slDist / tickSize; totalRiskAmount += lots * ticks * tickValue; }
}
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
if(equity > 0) state.totalOpenRisk = (totalRiskAmount / equity) * 100.0;
else state.totalOpenRisk = 0;
}
bool IsCorrelated(const SignalData &signal, const EAState &state)
{
if(!InpUseCorrelationFilter) return false;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
string posSymbol = PositionGetSymbol(i);
if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
if(posSymbol == _Symbol) continue;
double corr = CalculateCorrelation(_Symbol, posSymbol);
if(MathAbs(corr) > 0.7)
{
g_logger.LogEvent("PORTFOLIO", StringFormat("REJECTED: Correlation %.2f with %s", corr, posSymbol));
return true;
}
}
return false;
}
bool CheckExposure(const TradeParams &params, const EAState &state)
{
double projectedRisk = state.totalOpenRisk + params.riskPercent;
if(projectedRisk > m_maxTotalRiskPercent)
{
g_logger.LogEvent("PORTFOLIO", StringFormat("REJECTED: Risk %.2f%% > max %.2f%%", projectedRisk, m_maxTotalRiskPercent));
return false;
}
int forexCount = 0, metalCount = 0, indexCount = 0, cryptoCount = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
string sym = PositionGetSymbol(i);
if(StringFind(sym, "XAU") >= 0 || StringFind(sym, "XAG") >= 0) metalCount++;
else if(StringFind(sym, "BTC") >= 0 || StringFind(sym, "ETH") >= 0) cryptoCount++;
else if(StringFind(sym, "US30") >= 0 || StringFind(sym, "NAS") >= 0 || StringFind(sym, "GER") >= 0) indexCount++;
else forexCount++;
}
ENUM_ASSET_CLASS cls = state.assetProfile.assetClass;
if((cls == ASSET_FOREX_MAJOR || cls == ASSET_FOREX_CROSS) && forexCount >= 2) { g_logger.LogEvent("PORTFOLIO", "REJECTED: Max 2 Forex"); return false; }
if(cls == ASSET_METAL && metalCount >= 1) { g_logger.LogEvent("PORTFOLIO", "REJECTED: Max 1 Metal"); return false; }
if(cls == ASSET_INDEX && indexCount >= 1) { g_logger.LogEvent("PORTFOLIO", "REJECTED: Max 1 Index"); return false; }
if(cls == ASSET_CRYPTO && cryptoCount >= 1) { g_logger.LogEvent("PORTFOLIO", "REJECTED: Max 1 Crypto"); return false; }
return true;
}
private:
double CalculateCorrelation(string sym1, string sym2)
{
double c1[], c2[]; ArraySetAsSeries(c1, true); ArraySetAsSeries(c2, true);
if(CopyClose(sym1, m_mtf, 1, m_corrLookback, c1) < m_corrLookback) return 0;
if(CopyClose(sym2, m_mtf, 1, m_corrLookback, c2) < m_corrLookback) return 0;
double mean1 = 0, mean2 = 0;
for(int i = 0; i < m_corrLookback; i++) { mean1 += c1[i]; mean2 += c2[i]; }
mean1 /= m_corrLookback; mean2 /= m_corrLookback;
double cov = 0, var1 = 0, var2 = 0;
for(int i = 0; i < m_corrLookback; i++)
{ double d1 = c1[i] - mean1; double d2 = c2[i] - mean2; cov += d1 * d2; var1 += d1 * d1; var2 += d2 * d2; }
double std1 = MathSqrt(var1); double std2 = MathSqrt(var2);
if(std1 * std2 == 0) return 0;
return cov / (std1 * std2);
}
};
#endif // __PORTFOLIO_MANAGER_MQH__
+69
View File
@@ -0,0 +1,69 @@
//+------------------------------------------------------------------+
//| Risk/PositionSizer.mqh |
//+------------------------------------------------------------------+
#ifndef __POSITION_SIZER_MQH__
#define __POSITION_SIZER_MQH__
#include "../Core/Config.mqh"
#include "../Core/State.mqh"
class CPositionSizer
{
private:
AssetProfile m_profile;
double m_maxRiskPercent;
public:
bool Init(const AssetProfile &profile, double maxRisk)
{
m_profile = profile; m_maxRiskPercent = maxRisk;
Print("[PositionSizer] Max risk per trade: ", maxRisk, "%");
return true;
}
void Calculate(TradeParams &params, const SignalData &signal, const EAState &state)
{
params.isValid = false; params.rejectReason = "";
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
if(equity <= 0) { params.rejectReason = "Invalid account equity"; return; }
double riskAmount = equity * (m_maxRiskPercent / 100.0);
double slDistance = MathAbs(signal.entryPrice - signal.slPrice);
if(slDistance <= 0) { params.rejectReason = "Invalid SL distance"; return; }
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
if(tickValue <= 0 || tickSize <= 0) { params.rejectReason = "Invalid tick value/size"; return; }
double slTicks = slDistance / tickSize;
double lotSize = riskAmount / (slTicks * tickValue);
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
if(lotStep > 0) lotSize = MathFloor(lotSize / lotStep) * lotStep;
lotSize = MathMax(minLot, MathMin(maxLot, lotSize));
double marginRequired = 0;
double price = signal.entryPrice;
bool marginCalc = OrderCalcMargin(ORDER_TYPE_BUY, _Symbol, lotSize, price, marginRequired);
double freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
if(marginCalc && marginRequired > 0 && freeMargin < marginRequired * 1.2)
{
double maxLotByMargin = (freeMargin / 1.2) / (marginRequired / lotSize);
if(lotStep > 0) lotSize = MathFloor(maxLotByMargin / lotStep) * lotStep;
lotSize = MathMax(minLot, lotSize);
if(lotSize <= minLot) { params.rejectReason = "Insufficient margin"; return; }
marginCalc = OrderCalcMargin(ORDER_TYPE_BUY, _Symbol, lotSize, price, marginRequired);
}
double finalSlTicks = slDistance / tickSize;
double finalRisk = lotSize * finalSlTicks * tickValue;
double finalRiskPercent = (finalRisk / equity) * 100.0;
if(finalRiskPercent > m_maxRiskPercent * 1.1)
{ params.rejectReason = "Risk exceeds max"; return; }
params.lotSize = lotSize; params.riskAmount = finalRisk;
params.riskPercent = finalRiskPercent; params.slDistance = slDistance;
params.tp1Distance = MathAbs(signal.tp1Price - signal.entryPrice);
params.tp2Distance = MathAbs(signal.tp2Price - signal.entryPrice);
params.marginRequired = marginRequired; params.isValid = true;
}
};
#endif // __POSITION_SIZER_MQH__
+170
View File
@@ -0,0 +1,170 @@
//+------------------------------------------------------------------+
//| Risk/Protection.mqh |
//| Circuit Breakers: Daily/Weekly Loss, Consecutive Loss, Spread |
//| MODIFIED: Completed UpdateState with live statistics tracking |
//+------------------------------------------------------------------+
#ifndef __PROTECTION_MQH__
#define __PROTECTION_MQH__
#include "../Core/Config.mqh"
#include "../Core/State.mqh"
#include "../Core/Logger.mqh"
extern CLogger g_logger;
class CProtection
{
private:
double m_maxDailyLoss;
double m_maxWeeklyLoss;
int m_maxConsecLosses;
int m_maxPositions;
double m_maxTotalRisk;
datetime m_lastDailyReset;
datetime m_lastWeeklyReset;
double m_lastEquity;
int m_consecLossCounter;
datetime m_lastTradeTime;
public:
bool Init(double dailyLoss, double weeklyLoss, int consecLoss, int maxPos, double maxRisk)
{
m_maxDailyLoss = dailyLoss;
m_maxWeeklyLoss = weeklyLoss;
m_maxConsecLosses = consecLoss;
m_maxPositions = maxPos;
m_maxTotalRisk = maxRisk;
m_lastDailyReset = 0;
m_lastWeeklyReset = 0;
m_lastEquity = AccountInfoDouble(ACCOUNT_EQUITY);
m_consecLossCounter = 0;
m_lastTradeTime = 0;
Print("[Protection] Circuit breakers active. Daily:", dailyLoss, "% Weekly:", weeklyLoss, "% Consec:", consecLoss);
return true;
}
bool IsCircuitBreakerActive(EAState &state) const
{
if(TimeCurrent() < state.circuitBreakerUntil)
return true;
if(state.circuitBreakerUntil > 0 && TimeCurrent() >= state.circuitBreakerUntil)
{
g_logger.LogEvent("PROTECTION", "Circuit breaker expired. Trading resumed.");
state.circuitBreakerUntil = 0;
state.circuitBreakerReason = "";
state.dailyLimitHit = false;
state.weeklyLimitHit = false;
state.consecLossHalted = false;
}
return false;
}
bool PreTradeCheck(EAState &state) const
{
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
if(equity <= 0) return false;
double dailyLimit = equity * (m_maxDailyLoss / 100.0);
if(state.dailyPnL <= -dailyLimit)
{
ActivateBreaker(state, "Daily Loss Limit", 86400);
state.dailyLimitHit = true;
return false;
}
double weeklyLimit = equity * (m_maxWeeklyLoss / 100.0);
if(state.weeklyPnL <= -weeklyLimit)
{
ActivateBreaker(state, "Weekly Loss Limit", 7 * 86400);
state.weeklyLimitHit = true;
return false;
}
if(state.consecutiveLosses >= m_maxConsecLosses)
{
ActivateBreaker(state, "Consecutive Losses", 86400);
state.consecLossHalted = true;
return false;
}
if(state.openPositions >= m_maxPositions)
return false;
return true;
}
bool IsSpreadAcceptable(const AssetProfile &profile) const
{
if(!InpUseSpreadFilter) return true;
long spreadPoints = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
double spreadPrice = spreadPoints * _Point;
return (spreadPrice <= profile.maxSpreadPoints);
}
void UpdateState(EAState &state)
{
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
if(m_lastEquity > 0 && currentEquity != m_lastEquity)
{
double equityChange = currentEquity - m_lastEquity;
state.dailyPnL += equityChange;
state.weeklyPnL += equityChange;
if(equityChange < 0)
{
if(TimeCurrent() != m_lastTradeTime)
{
m_consecLossCounter++;
state.consecutiveLosses = m_consecLossCounter;
m_lastTradeTime = TimeCurrent();
g_logger.LogEvent("PROTECTION", StringFormat("Loss detected. Consecutive: %d/%d", m_consecLossCounter, m_maxConsecLosses));
}
}
else if(equityChange > 0)
{
if(m_consecLossCounter > 0)
{
m_consecLossCounter = 0;
state.consecutiveLosses = 0;
g_logger.LogEvent("PROTECTION", "Profit detected. Consecutive loss counter reset.");
}
}
}
m_lastEquity = currentEquity;
if(InpDebugMode)
{
g_logger.LogEvent("PROTECTION", StringFormat("State | Daily: %.2f | Weekly: %.2f | Consec: %d | Equity: %.2f",
state.dailyPnL, state.weeklyPnL, state.consecutiveLosses, currentEquity));
}
}
void CheckDailyReset(EAState &state)
{
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
datetime todayStart = StringToTime(StringFormat("%04d.%02d.%02d 00:00:00", dt.year, dt.mon, dt.day));
if(todayStart > m_lastDailyReset)
{
state.dailyPnL = 0;
state.totalTradesToday = 0;
state.consecutiveLosses = 0;
m_consecLossCounter = 0;
m_lastDailyReset = todayStart;
state.equityAtStart = AccountInfoDouble(ACCOUNT_EQUITY);
m_lastEquity = state.equityAtStart;
g_logger.LogEvent("PROTECTION", "Daily counters reset");
}
if(dt.day_of_week == 1 && todayStart > m_lastWeeklyReset)
{
state.weeklyPnL = 0;
state.totalTradesWeek = 0;
m_lastWeeklyReset = todayStart;
state.equityAtWeekStart = AccountInfoDouble(ACCOUNT_EQUITY);
g_logger.LogEvent("PROTECTION", "Weekly counters reset");
}
}
private:
void ActivateBreaker(EAState &state, string reason, int seconds) const
{
state.circuitBreakerUntil = TimeCurrent() + seconds;
state.circuitBreakerReason = reason;
g_logger.LogEvent("PROTECTION", StringFormat("CIRCUIT BREAKER: %s. Halted for %d sec.", reason, seconds));
}
};
#endif // __PROTECTION_MQH__
Binary file not shown.
+451
View File
@@ -0,0 +1,451 @@
//+------------------------------------------------------------------+
//| Universal_MTF_EA.mq5 |
//| Universal Multi-Timeframe Expert Advisor v2.0 |
//+------------------------------------------------------------------+
#property strict
#property copyright "Institutional Quantitative Systems"
#property version "2.000"
#property description "Universal MTF EA v2.0"
//--- Input for magic number
input group "=== EA IDENTIFICATION ==="
input ulong InpMagicNumber = 20250625;
input string InpEALabel = "Universal_MTF";
//+------------------------------------------------------------------+
//| MODULE INCLUDES |
//+------------------------------------------------------------------+
input group "=== RISK MANAGEMENT ==="
input double InpMaxRiskPerTrade = 0.5;
input double InpMaxDailyLoss = 2.0;
input double InpMaxWeeklyLoss = 5.0;
input int InpMaxConsecLosses = 3;
input int InpMaxPositions = 5;
input double InpMaxTotalRisk = 3.0;
input group "=== TIME FRAME CONFIGURATION ==="
input ENUM_TIMEFRAMES InpHTF = PERIOD_H4;
input ENUM_TIMEFRAMES InpMTF = PERIOD_M15;
input ENUM_TIMEFRAMES InpLTF = PERIOD_M5;
input group "=== ATR & VOLATILITY ==="
input int InpATRPeriod = 14;
input int InpATRBaseline = 50;
input double InpTrendATRMult = 1.5;
input double InpRangeATRMult = 1.0;
input double InpTrailingMult = 2.0;
input group "=== SESSION & SYMBOL ==="
input bool InpUseSessionFilter = true;
input bool InpUseSpreadFilter = true;
input bool InpUseCorrelationFilter = true;
input group "=== NEWS FILTER ==="
input bool InpUseNewsFilter = true;
input int InpNewsBlockMinutes = 30;
input int InpNewsResumeMinutes = 15;
input group "=== ORDER EXECUTION ==="
input bool InpUseLimitOrders = true;
input int InpLimitOrderExpiry = 30;
input group "=== TELEGRAM/DISCORD ALERTS ==="
input string InpTelegramBotToken = "";
input string InpTelegramChatId = "";
input string InpDiscordWebhook = "";
input bool InpAlertOnTrade = true;
input bool InpAlertOnCircuitBreaker = true;
input bool InpAlertOnRegimeChange = true;
input bool InpSendDailySummary = true;
input group "=== LOGGING & AUDIT ==="
input string InpLogPath = "Universal_MTF_EA/";
input bool InpDebugMode = false;
input int InpDashboardUpdateSec = 5;
#include "Core/Config.mqh"
#include "Core/State.mqh"
#include "Core/Logger.mqh"
#include "Core/SymbolProfiler.mqh"
#include "Core/TelegramNotifier.mqh"
#include "Data/PriceEngine.mqh"
#include "Data/VWAP_Engine.mqh"
#include "Data/Volatility.mqh"
#include "Execution/OrderManager.mqh"
#include "Execution/TradeManager.mqh"
#include "Logic/MacroAudit.mqh"
#include "Logic/ContextFilter.mqh"
#include "Logic/MicroTrigger.mqh"
#include "Logic/RegimeEngine.mqh"
#include "Logic/NewsFilter.mqh"
#include "Risk/PositionSizer.mqh"
#include "Risk/Protection.mqh"
#include "Risk/PortfolioManager.mqh"
//+------------------------------------------------------------------+
//| MODULE INSTANCES |
//+------------------------------------------------------------------+
CLogger g_logger;
CSymbolProfiler g_profiler;
CTelegramNotifier g_notifier;
CPriceEngine g_priceEngine;
CVWAPEngine g_vwapEngine;
CVolatility g_volatility;
CMacroAudit g_macroAudit;
CContextFilter g_contextFilter;
CMicroTrigger g_microTrigger;
CRegimeEngine g_regimeEngine;
CNewsFilter g_newsFilter;
CPositionSizer g_positionSizer;
CProtection g_protection;
CPortfolioManager g_portfolio;
COrderManager g_orderManager;
CTradeManager g_tradeManager;
//+------------------------------------------------------------------+
//| EXPERT INITIALIZATION |
//+------------------------------------------------------------------+
int OnInit()
{
Print("============================================================");
Print("[Universal_MTF_EA] Initializing v2.000...");
Print("============================================================");
if(!g_logger.Init(InpLogPath, InpEALabel, InpMagicNumber))
{
Print("[CRITICAL] Logger init failed. EA halted.");
return INIT_FAILED;
}
g_logger.LogEvent("SYSTEM", "EA Initialization started v2.0");
if(!g_session.Init())
{
g_logger.LogError("OnInit", 0, "SessionManager init failed", 0);
return INIT_FAILED;
}
if(!g_profiler.Init(g_state.assetProfile))
{
g_logger.LogError("OnInit", 0, "SymbolProfiler init failed", 0);
return INIT_FAILED;
}
g_logger.LogEvent("SYSTEM", StringFormat("Asset: %s", g_state.assetProfile.description));
if(!g_priceEngine.Init(InpHTF, InpMTF, InpLTF))
{
g_logger.LogError("OnInit", 0, "PriceEngine init failed", 0);
return INIT_FAILED;
}
if(!g_vwapEngine.Init(g_state.assetProfile))
{
g_logger.LogError("OnInit", 0, "VWAPEngine init failed", 0);
return INIT_FAILED;
}
if(!g_volatility.Init(InpATRPeriod, InpATRBaseline, InpHTF, InpMTF))
{
g_logger.LogError("OnInit", 0, "Volatility init failed", 0);
return INIT_FAILED;
}
if(!g_macroAudit.Init(InpHTF, g_vwapEngine))
{
g_logger.LogError("OnInit", 0, "MacroAudit init failed", 0);
return INIT_FAILED;
}
if(!g_contextFilter.Init(InpMTF, g_volatility))
{
g_logger.LogError("OnInit", 0, "ContextFilter init failed", 0);
return INIT_FAILED;
}
if(!g_microTrigger.Init(InpLTF, g_priceEngine))
{
g_logger.LogError("OnInit", 0, "MicroTrigger init failed", 0);
return INIT_FAILED;
}
if(!g_regimeEngine.Init())
{
g_logger.LogError("OnInit", 0, "RegimeEngine init failed", 0);
return INIT_FAILED;
}
if(!g_newsFilter.Init(InpNewsBlockMinutes, InpNewsResumeMinutes))
{
g_logger.LogError("OnInit", 0, "NewsFilter init failed", 0);
return INIT_FAILED;
}
if(!g_positionSizer.Init(g_state.assetProfile, InpMaxRiskPerTrade))
{
g_logger.LogError("OnInit", 0, "PositionSizer init failed", 0);
return INIT_FAILED;
}
if(!g_protection.Init(InpMaxDailyLoss, InpMaxWeeklyLoss, InpMaxConsecLosses,
InpMaxPositions, InpMaxTotalRisk))
{
g_logger.LogError("OnInit", 0, "Protection init failed", 0);
return INIT_FAILED;
}
if(!g_portfolio.Init(CORR_LOOKBACK, InpMTF))
{
g_logger.LogError("OnInit", 0, "PortfolioManager init failed", 0);
return INIT_FAILED;
}
if(!g_orderManager.Init(InpMagicNumber, g_state.assetProfile))
{
g_logger.LogError("OnInit", 0, "OrderManager init failed", 0);
return INIT_FAILED;
}
if(!g_tradeManager.Init(g_state.assetProfile, g_orderManager))
{
g_logger.LogError("OnInit", 0, "TradeManager init failed", 0);
return INIT_FAILED;
}
if(!g_notifier.Init(InpTelegramBotToken, InpTelegramChatId, InpDiscordWebhook))
{
g_logger.LogEvent("SYSTEM", "TelegramNotifier init failed or disabled.");
}
g_state.equityAtStart = AccountInfoDouble(ACCOUNT_EQUITY);
g_state.equityAtWeekStart = AccountInfoDouble(ACCOUNT_EQUITY);
g_state.circuitBreakerUntil = 0;
g_state.circuitBreakerReason = "";
g_state.loggerReady = true;
g_state.lastDashboardUpdate = 0;
EventSetMillisecondTimer(30000);
EventSetMillisecondTimer(5000);
EventSetMillisecondTimer(InpDashboardUpdateSec * 1000);
EventSetMillisecondTimer(3600000);
EventSetMillisecondTimer(900000);
g_priceEngine.RefreshAll();
g_vwapEngine.Calculate(g_state.vwapState);
g_volatility.Update();
g_macroAudit.Analyze(g_state);
g_contextFilter.Analyze(g_state);
g_logger.LogEvent("SYSTEM", "EA Initialization completed successfully v2.0");
g_logger.LogEvent("SYSTEM", StringFormat("Symbol: %s | Class: %s | HTF: %s | MTF: %s | LTF: %s",
_Symbol, g_state.assetProfile.description, EnumToString(InpHTF),
EnumToString(InpMTF), EnumToString(InpLTF)));
if(InpAlertOnTrade)
{
g_notifier.SendMessage("*Universal MTF EA v2.0 Started*\n\nSymbol: " + _Symbol +
"\nAsset: " + g_state.assetProfile.description +
"\nTime: " + TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS));
}
Print("[Universal_MTF_EA] Initialization complete. Ready for trading.");
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| EXPERT DEINITIALIZATION |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
Print("============================================================");
Print("[Universal_MTF_EA] Deinitializing... Reason: ", reason);
Print("============================================================");
EventKillTimer();
g_logger.Shutdown();
g_priceEngine.Release();
g_vwapEngine.Release();
g_volatility.Release();
g_macroAudit.Release();
g_contextFilter.Release();
g_microTrigger.Release();
if(InpAlertOnTrade)
{
g_notifier.SendMessage("*Universal MTF EA v2.0 Stopped*\n\nSymbol: " + _Symbol +
"\nReason: " + IntegerToString(reason) +
"\nDaily PnL: " + StringFormat("%.2f", g_state.dailyPnL) +
"\nTime: " + TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS));
}
g_logger.LogEvent("SYSTEM", StringFormat("EA Stopped. Daily: %.2f | Weekly: %.2f | Trades: %d",
g_state.dailyPnL, g_state.weeklyPnL, g_state.totalTradesToday));
Print("[Universal_MTF_EA] Deinitialization complete.");
}
//+------------------------------------------------------------------+
//| EXPERT TICK HANDLER |
//+------------------------------------------------------------------+
void OnTick()
{
if(g_protection.IsCircuitBreakerActive(g_state))
{
g_tradeManager.ManageOpenPositions(g_state, g_volatility);
return;
}
if(InpUseSessionFilter && !g_session.IsSessionValid(g_state.assetProfile))
return;
if(g_session.IsRolloverTime())
return;
if(InpUseNewsFilter && !g_newsFilter.IsTradingAllowed())
return;
static datetime lastLTFTime = 0;
datetime currentLTFTime = iTime(_Symbol, InpLTF, 0);
if(currentLTFTime != lastLTFTime)
{
if(g_priceEngine.IsBarClosed(InpLTF))
{
g_state.isBarClosedLTF = true;
g_state.lastLTFBarTime = currentLTFTime;
g_priceEngine.RefreshLTF();
if(g_state.currentBias != BIAS_NEUTRAL || g_state.currentRegime == REGIME_RANGE)
{
SignalData signal;
g_microTrigger.GenerateSignal(signal, g_state, g_priceEngine);
g_logger.LogSignal(signal, g_state);
if(signal.isValid) ProcessSignal(signal);
}
}
lastLTFTime = currentLTFTime;
}
g_tradeManager.ManageOpenPositions(g_state, g_volatility);
g_portfolio.UpdateState(g_state);
if(InpUseLimitOrders)
{
static datetime lastOrderCheck = 0;
if(TimeCurrent() - lastOrderCheck > 300)
{
g_orderManager.CancelStaleOrders(InpLimitOrderExpiry);
lastOrderCheck = TimeCurrent();
}
}
}
//+------------------------------------------------------------------+
//| TIMER HANDLER |
//+------------------------------------------------------------------+
void OnTimer()
{
static int timerCount = 0;
timerCount++;
if(timerCount % 6 == 0)
{
if(g_priceEngine.IsBarClosed(InpHTF))
{
g_state.isBarClosedHTF = true;
g_state.lastHTFBarTime = iTime(_Symbol, InpHTF, 0);
g_vwapEngine.Calculate(g_state.vwapState);
g_macroAudit.Analyze(g_state);
}
}
if(timerCount % 1 == 0)
{
if(g_priceEngine.IsBarClosed(InpMTF))
{
g_state.isBarClosedMTF = true;
g_state.lastMTFBarTime = iTime(_Symbol, InpMTF, 0);
g_volatility.Update();
g_contextFilter.Analyze(g_state);
g_regimeEngine.UpdateState(g_state);
}
}
if(TimeCurrent() - g_state.lastDashboardUpdate >= InpDashboardUpdateSec)
{
g_logger.UpdateDashboard(g_state);
g_state.lastDashboardUpdate = TimeCurrent();
}
g_protection.CheckDailyReset(g_state);
if(InpSendDailySummary)
{
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
static bool summarySentToday = false;
if(dt.hour == 23 && !summarySentToday)
{
g_notifier.SendDailySummary(g_state);
summarySentToday = true;
}
if(dt.hour == 0) summarySentToday = false;
}
}
//+------------------------------------------------------------------+
//| SIGNAL PROCESSING |
//+------------------------------------------------------------------+
void ProcessSignal(const SignalData &signal)
{
if(InpUseSpreadFilter && !g_protection.IsSpreadAcceptable(g_state.assetProfile))
{
g_logger.LogEvent("FILTER", "Signal rejected: Spread too wide");
return;
}
if(g_state.openPositions >= InpMaxPositions)
{
g_logger.LogEvent("FILTER", StringFormat("Max positions reached (%d)", g_state.openPositions));
return;
}
if(InpUseCorrelationFilter && g_portfolio.IsCorrelated(signal, g_state))
{
g_logger.LogEvent("FILTER", "High correlation");
return;
}
TradeParams tradeParams;
g_positionSizer.Calculate(tradeParams, signal, g_state);
if(!tradeParams.isValid)
{
g_logger.LogEvent("FILTER", StringFormat("Sizing failed: %s", tradeParams.rejectReason));
return;
}
if(!g_portfolio.CheckExposure(tradeParams, g_state))
{
g_logger.LogEvent("FILTER", "Portfolio risk limit exceeded");
return;
}
if(!g_protection.PreTradeCheck(g_state))
{
g_logger.LogEvent("FILTER", StringFormat("Circuit breaker: %s", g_state.circuitBreakerReason));
return;
}
ulong ticket = 0;
bool executed = g_orderManager.ExecuteOrder(signal, tradeParams, g_state, ticket);
if(executed && ticket > 0)
{
g_state.openPositions++;
g_logger.LogTradeOpen(signal, tradeParams, ticket);
g_logger.LogEvent("EXECUTE", StringFormat("Order Ticket=%llu | %s | Lots: %.2f",
ticket, signal.isBuy ? "BUY" : "SELL", tradeParams.lotSize));
if(InpAlertOnTrade) g_notifier.SendTradeOpen(signal, tradeParams, ticket);
}
else
{
g_logger.LogEvent("EXECUTE", "Order execution failed");
}
}
//+------------------------------------------------------------------+
//| TRADE EVENT HANDLER |
//+------------------------------------------------------------------+
void OnTrade()
{
g_tradeManager.CheckClosedTrades(g_state);
g_protection.UpdateState(g_state);
if(g_state.lastTradeClose > 0)
{
g_logger.LogTradeClose(g_state);
if(InpAlertOnTrade) g_notifier.SendTradeClose(g_state);
if(g_state.dailyLimitHit || g_state.weeklyLimitHit || g_state.consecLossHalted)
{
if(InpAlertOnCircuitBreaker) g_notifier.SendCircuitBreaker(g_state);
}
}
}
//+------------------------------------------------------------------+