commit 7545b842a4c269c46fd96da6806db4d69be9a27e Author: Huthayfa Date: Thu May 28 19:06:07 2026 +0300 Initialize project in MT5 Experts directory diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7275bb --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +venv/ diff --git a/Core/Config.mqh b/Core/Config.mqh new file mode 100644 index 0000000..0fd2ba5 --- /dev/null +++ b/Core/Config.mqh @@ -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__ diff --git a/Core/Logger.mqh b/Core/Logger.mqh new file mode 100644 index 0000000..96b675f --- /dev/null +++ b/Core/Logger.mqh @@ -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 ¶ms, 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__ diff --git a/Core/State.mqh b/Core/State.mqh new file mode 100644 index 0000000..14d3a33 --- /dev/null +++ b/Core/State.mqh @@ -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__ diff --git a/Core/SymbolProfiler.mqh b/Core/SymbolProfiler.mqh new file mode 100644 index 0000000..b556efb --- /dev/null +++ b/Core/SymbolProfiler.mqh @@ -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__ diff --git a/Core/TelegramNotifier.mqh b/Core/TelegramNotifier.mqh new file mode 100644 index 0000000..910a2e9 --- /dev/null +++ b/Core/TelegramNotifier.mqh @@ -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 ¶ms, 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__ diff --git a/Data/PriceEngine.mqh b/Data/PriceEngine.mqh new file mode 100644 index 0000000..4f493f6 --- /dev/null +++ b/Data/PriceEngine.mqh @@ -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__ diff --git a/Data/VWAP_Engine.mqh b/Data/VWAP_Engine.mqh new file mode 100644 index 0000000..184315b --- /dev/null +++ b/Data/VWAP_Engine.mqh @@ -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__ diff --git a/Data/Volatility.mqh b/Data/Volatility.mqh new file mode 100644 index 0000000..d069a68 --- /dev/null +++ b/Data/Volatility.mqh @@ -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__ diff --git a/Execution/OrderManager.mqh b/Execution/OrderManager.mqh new file mode 100644 index 0000000..730db58 --- /dev/null +++ b/Execution/OrderManager.mqh @@ -0,0 +1,276 @@ +//+------------------------------------------------------------------+ +//| Execution/OrderManager.mqh | +//+------------------------------------------------------------------+ +#ifndef __ORDER_MANAGER_MQH__ +#define __ORDER_MANAGER_MQH__ + +#include +#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 ¶ms, + 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 ¶ms, + 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 ¶ms, + 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 ¶ms) + { + 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__ diff --git a/Execution/TradeManager.mqh b/Execution/TradeManager.mqh new file mode 100644 index 0000000..8869be6 --- /dev/null +++ b/Execution/TradeManager.mqh @@ -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 +#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__ diff --git a/Logic/ContextFilter.mqh b/Logic/ContextFilter.mqh new file mode 100644 index 0000000..23dd7cc --- /dev/null +++ b/Logic/ContextFilter.mqh @@ -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__ diff --git a/Logic/MacroAudit.mqh b/Logic/MacroAudit.mqh new file mode 100644 index 0000000..8b65e0e --- /dev/null +++ b/Logic/MacroAudit.mqh @@ -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__ diff --git a/Logic/MicroTrigger.mqh b/Logic/MicroTrigger.mqh new file mode 100644 index 0000000..31cc48a --- /dev/null +++ b/Logic/MicroTrigger.mqh @@ -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__ diff --git a/Logic/NewsFilter.mqh b/Logic/NewsFilter.mqh new file mode 100644 index 0000000..0291343 --- /dev/null +++ b/Logic/NewsFilter.mqh @@ -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__ diff --git a/Logic/RegimeEngine.mqh b/Logic/RegimeEngine.mqh new file mode 100644 index 0000000..74144f1 --- /dev/null +++ b/Logic/RegimeEngine.mqh @@ -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__ diff --git a/Risk/PortfolioManager.mqh b/Risk/PortfolioManager.mqh new file mode 100644 index 0000000..4ac39d4 --- /dev/null +++ b/Risk/PortfolioManager.mqh @@ -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 ¶ms, 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__ diff --git a/Risk/PositionSizer.mqh b/Risk/PositionSizer.mqh new file mode 100644 index 0000000..7264c95 --- /dev/null +++ b/Risk/PositionSizer.mqh @@ -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 ¶ms, 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__ diff --git a/Risk/Protection.mqh b/Risk/Protection.mqh new file mode 100644 index 0000000..3afe5cd --- /dev/null +++ b/Risk/Protection.mqh @@ -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__ diff --git a/Universal_MTF_EA.ex5 b/Universal_MTF_EA.ex5 new file mode 100644 index 0000000..e309848 Binary files /dev/null and b/Universal_MTF_EA.ex5 differ diff --git a/Universal_MTF_EA.mq5 b/Universal_MTF_EA.mq5 new file mode 100644 index 0000000..2d95b41 --- /dev/null +++ b/Universal_MTF_EA.mq5 @@ -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); + } + } +} + +//+------------------------------------------------------------------+