Initialize project in MT5 Experts directory
This commit is contained in:
@@ -0,0 +1 @@
|
||||
venv/
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| Core/Config.mqh |
|
||||
//| Universal Multi-Timeframe EA - Configuration & Type Definitions|
|
||||
//+------------------------------------------------------------------+
|
||||
#ifndef __CONFIG_MQH__
|
||||
#define __CONFIG_MQH__
|
||||
|
||||
#property strict
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| ENUMERATIONS |
|
||||
//+------------------------------------------------------------------+
|
||||
enum ENUM_BIAS
|
||||
{
|
||||
BIAS_BULL,
|
||||
BIAS_BEAR,
|
||||
BIAS_NEUTRAL
|
||||
};
|
||||
|
||||
enum ENUM_REGIME
|
||||
{
|
||||
REGIME_TREND,
|
||||
REGIME_RANGE,
|
||||
REGIME_CHOP
|
||||
};
|
||||
|
||||
enum ENUM_PATTERN
|
||||
{
|
||||
PATTERN_PIN_BAR,
|
||||
PATTERN_ENGULFING,
|
||||
PATTERN_INSIDE_BAR,
|
||||
PATTERN_NONE
|
||||
};
|
||||
|
||||
enum ENUM_EXIT_REASON
|
||||
{
|
||||
EXIT_TP1,
|
||||
EXIT_TP2,
|
||||
EXIT_SL,
|
||||
EXIT_BE,
|
||||
EXIT_TIME,
|
||||
EXIT_REGIME_CHANGE,
|
||||
EXIT_MANUAL,
|
||||
EXIT_TRAILING_STOP
|
||||
};
|
||||
|
||||
enum ENUM_ASSET_CLASS
|
||||
{
|
||||
ASSET_FOREX_MAJOR,
|
||||
ASSET_FOREX_CROSS,
|
||||
ASSET_METAL,
|
||||
ASSET_INDEX,
|
||||
ASSET_COMMODITY,
|
||||
ASSET_CRYPTO
|
||||
};
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| DATA STRUCTURES |
|
||||
//+------------------------------------------------------------------+
|
||||
struct SignalData
|
||||
{
|
||||
bool isValid;
|
||||
bool isBuy;
|
||||
double entryPrice;
|
||||
double slPrice;
|
||||
double tp1Price;
|
||||
double tp2Price;
|
||||
ENUM_PATTERN pattern;
|
||||
string patternName;
|
||||
string rejectionReason;
|
||||
datetime signalTime;
|
||||
double atrValue;
|
||||
};
|
||||
|
||||
struct TradeParams
|
||||
{
|
||||
double lotSize;
|
||||
double riskAmount;
|
||||
double riskPercent;
|
||||
double slDistance;
|
||||
double tp1Distance;
|
||||
double tp2Distance;
|
||||
double marginRequired;
|
||||
bool isValid;
|
||||
string rejectReason;
|
||||
};
|
||||
|
||||
struct AssetProfile
|
||||
{
|
||||
ENUM_ASSET_CLASS assetClass;
|
||||
double atrMultiplierSL;
|
||||
double maxSpreadPoints;
|
||||
int londonOpenHour;
|
||||
int nyOpenHour;
|
||||
bool trade24_7;
|
||||
bool skipWeekend;
|
||||
int sessionStartHour;
|
||||
int sessionEndHour;
|
||||
double minVolumeRatio;
|
||||
double partialCloseRatio;
|
||||
double beBufferPoints;
|
||||
double trailingATRMult;
|
||||
int maxTradeDuration;
|
||||
string description;
|
||||
};
|
||||
|
||||
struct VWAPState
|
||||
{
|
||||
double vwapValue;
|
||||
double vwapSlope;
|
||||
datetime sessionStart;
|
||||
double sumPV;
|
||||
double sumV;
|
||||
bool isValid;
|
||||
};
|
||||
|
||||
struct CorrelationData
|
||||
{
|
||||
string symbol;
|
||||
double correlation;
|
||||
int barsUsed;
|
||||
datetime calcTime;
|
||||
};
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//+------------------------------------------------------------------+
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| CONSTANTS |
|
||||
//+------------------------------------------------------------------+
|
||||
#define MAX_RETRIES 3
|
||||
#define RETRY_BASE_MS 500
|
||||
#define CORR_LOOKBACK 50
|
||||
#define VWAP_SLOPE_BARS 5
|
||||
#define SWING_LOOKBACK 20
|
||||
#define VOLUME_MA_PERIOD 20
|
||||
#define BB_PERIOD 20
|
||||
#define BB_DEVIATIONS 2.0
|
||||
#define ADX_PERIOD 14
|
||||
#define ADX_TREND_LEVEL 25.0
|
||||
#define ADX_RANGE_LEVEL 20.0
|
||||
#define ADX_CHOP_LEVEL 15.0
|
||||
#define ATR_TREND_RATIO 1.0
|
||||
#define ATR_CHOP_RATIO 0.8
|
||||
#define VOLUME_CONFIRM 1.2
|
||||
#define MIN_VOLUME_RATIO 0.8
|
||||
#define PIN_BAR_WICK_MULT 2.0
|
||||
#define ENGULF_VOLUME_MULT 1.2
|
||||
#define PARTIAL_CLOSE_R 1.5
|
||||
#define BE_BUFFER_ATR_MULT 0.2
|
||||
#define SLIPPAGE_ATR_MULT 0.5
|
||||
#define MIN_SLIPPAGE_PTS 10
|
||||
#define MAX_SLIPPAGE_PTS 50
|
||||
|
||||
#define ATR_TO_POINTS(atrValue) ((int)MathRound((atrValue) / _Point))
|
||||
#define VALIDATE_SHIFT(shift, context) ((shift) >= 1 ? true : (Print("[REPAINT_GUARD] Violation in ", (context), ": shift=", (shift), " < 1. Using shift=1."), false))
|
||||
#define RELEASE_HANDLE(handle) do { if((handle) != INVALID_HANDLE) { IndicatorRelease(handle); (handle) = INVALID_HANDLE; } } while(0)
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
#endif // __CONFIG_MQH__
|
||||
+276
@@ -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__
|
||||
+142
@@ -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__
|
||||
@@ -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__
|
||||
@@ -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__
|
||||
@@ -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__
|
||||
@@ -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__
|
||||
@@ -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__
|
||||
@@ -0,0 +1,276 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| Execution/OrderManager.mqh |
|
||||
//+------------------------------------------------------------------+
|
||||
#ifndef __ORDER_MANAGER_MQH__
|
||||
#define __ORDER_MANAGER_MQH__
|
||||
|
||||
#include <Trade\Trade.mqh>
|
||||
#include "../Core/Config.mqh"
|
||||
#include "../Core/State.mqh"
|
||||
#include "../Core/Logger.mqh"
|
||||
|
||||
extern CLogger g_logger;
|
||||
|
||||
class COrderManager
|
||||
{
|
||||
private:
|
||||
CTrade m_trade;
|
||||
ulong m_magic;
|
||||
AssetProfile m_profile;
|
||||
|
||||
public:
|
||||
bool Init(ulong magic, const AssetProfile &profile)
|
||||
{
|
||||
m_magic = magic;
|
||||
m_profile = profile;
|
||||
m_trade.SetExpertMagicNumber(magic);
|
||||
m_trade.SetDeviationInPoints(10);
|
||||
m_trade.SetTypeFilling(ORDER_FILLING_IOC);
|
||||
m_trade.SetAsyncMode(false);
|
||||
Print("[OrderManager] Execution layer initialized. Magic: ", magic);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ExecuteOrder(const SignalData &signal, const TradeParams ¶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__
|
||||
@@ -0,0 +1,290 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| Execution/TradeManager.mqh |
|
||||
//| Trade Lifecycle: Partial Close, BE, Trailing Stop, Time Exit |
|
||||
//| MODIFIED: Added TP2 Full Close support |
|
||||
//+------------------------------------------------------------------+
|
||||
#ifndef __TRADE_MANAGER_MQH__
|
||||
#define __TRADE_MANAGER_MQH__
|
||||
|
||||
#include <Trade\Trade.mqh>
|
||||
#include "../Core/Config.mqh"
|
||||
#include "../Core/State.mqh"
|
||||
#include "../Core/Logger.mqh"
|
||||
#include "../Data/Volatility.mqh"
|
||||
#include "OrderManager.mqh"
|
||||
|
||||
extern CLogger g_logger;
|
||||
extern CVolatility g_volatility;
|
||||
extern EAState g_state;
|
||||
|
||||
class CTradeManager
|
||||
{
|
||||
private:
|
||||
CTrade m_trade;
|
||||
AssetProfile m_profile;
|
||||
COrderManager *m_orderMgr;
|
||||
|
||||
struct TradeTracking
|
||||
{
|
||||
ulong ticket;
|
||||
datetime openTime;
|
||||
double entryPrice;
|
||||
double tp1Price;
|
||||
double tp2Price;
|
||||
double initialSL;
|
||||
double partialLot;
|
||||
bool tp1Hit;
|
||||
bool tp2Hit;
|
||||
bool beSet;
|
||||
bool trailingActive;
|
||||
ENUM_REGIME openRegime;
|
||||
};
|
||||
|
||||
TradeTracking m_trades[];
|
||||
int m_tradeCount;
|
||||
|
||||
public:
|
||||
bool Init(const AssetProfile &profile, COrderManager &orderMgr)
|
||||
{
|
||||
m_profile = profile;
|
||||
m_orderMgr = GetPointer(orderMgr);
|
||||
m_tradeCount = 0;
|
||||
ArrayResize(m_trades, 10);
|
||||
Print("[TradeManager] Lifecycle manager initialized (v2.0 with TP2)");
|
||||
return true;
|
||||
}
|
||||
|
||||
void ManageOpenPositions(EAState &state, CVolatility &vol)
|
||||
{
|
||||
int posTotal = PositionsTotal();
|
||||
if(posTotal == 0) { state.openPositions = 0; return; }
|
||||
double atr = vol.GetATR();
|
||||
if(atr <= 0) atr = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * 10;
|
||||
for(int i = posTotal - 1; i >= 0; i--)
|
||||
{
|
||||
if(PositionGetSymbol(i) != _Symbol) continue;
|
||||
if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
|
||||
ulong ticket = PositionGetInteger(POSITION_TICKET);
|
||||
double entry = PositionGetDouble(POSITION_PRICE_OPEN);
|
||||
double sl = PositionGetDouble(POSITION_SL);
|
||||
double tp = PositionGetDouble(POSITION_TP);
|
||||
double lots = PositionGetDouble(POSITION_VOLUME);
|
||||
datetime openTime = (datetime)PositionGetInteger(POSITION_TIME);
|
||||
int type = (int)PositionGetInteger(POSITION_TYPE);
|
||||
int idx = FindTradeIndex(ticket);
|
||||
if(idx < 0) idx = RegisterTrade(ticket, entry, tp, sl, openTime);
|
||||
double currentPrice = (type == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
||||
if(!m_trades[idx].tp1Hit && m_trades[idx].tp1Price > 0)
|
||||
{
|
||||
bool hitTP1 = (type == POSITION_TYPE_BUY && currentPrice >= m_trades[idx].tp1Price) ||
|
||||
(type == POSITION_TYPE_SELL && currentPrice <= m_trades[idx].tp1Price);
|
||||
if(hitTP1) { m_trades[idx].tp1Hit = true; PartialClose(idx, lots, ticket); }
|
||||
}
|
||||
if(m_trades[idx].tp1Hit && !m_trades[idx].tp2Hit && m_trades[idx].tp2Price > 0)
|
||||
{
|
||||
bool hitTP2 = (type == POSITION_TYPE_BUY && currentPrice >= m_trades[idx].tp2Price) ||
|
||||
(type == POSITION_TYPE_SELL && currentPrice <= m_trades[idx].tp2Price);
|
||||
if(hitTP2)
|
||||
{
|
||||
m_trades[idx].tp2Hit = true;
|
||||
ClosePosition(ticket, EXIT_TP2);
|
||||
g_logger.LogEvent("TRADE", StringFormat("TP2 Full Close ticket %llu at %.5f", ticket, currentPrice));
|
||||
RemoveTrade(idx);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if(m_trades[idx].tp1Hit && !m_trades[idx].beSet)
|
||||
SetBreakEven(idx, entry, sl, type, atr);
|
||||
if(m_trades[idx].beSet && m_trades[idx].trailingActive)
|
||||
UpdateTrailingStop(idx, currentPrice, type, atr, sl);
|
||||
if(m_trades[idx].openRegime == REGIME_RANGE)
|
||||
{
|
||||
int elapsed = (int)(TimeCurrent() - openTime);
|
||||
if(elapsed >= m_profile.maxTradeDuration * 60)
|
||||
{
|
||||
g_logger.LogEvent("TRADE", StringFormat("Time exit ticket %llu after %d min", ticket, elapsed/60));
|
||||
ClosePosition(ticket, EXIT_TIME);
|
||||
RemoveTrade(idx);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
state.openPositions = CountOurPositions();
|
||||
}
|
||||
|
||||
void CheckClosedTrades(EAState &state)
|
||||
{
|
||||
for(int i = m_tradeCount - 1; i >= 0; i--)
|
||||
{
|
||||
if(!PositionSelectByTicket(m_trades[i].ticket))
|
||||
{
|
||||
state.lastTradeClose = TimeCurrent();
|
||||
state.totalTradesToday++;
|
||||
state.totalTradesWeek++;
|
||||
RemoveTrade(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CloseAllPositions(EAState &state, ENUM_EXIT_REASON reason)
|
||||
{
|
||||
int posTotal = PositionsTotal();
|
||||
for(int i = posTotal - 1; i >= 0; i--)
|
||||
{
|
||||
if(PositionGetSymbol(i) != _Symbol) continue;
|
||||
if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
|
||||
ulong ticket = PositionGetInteger(POSITION_TICKET);
|
||||
ClosePosition(ticket, reason);
|
||||
}
|
||||
ArrayResize(m_trades, 10);
|
||||
m_tradeCount = 0;
|
||||
state.openPositions = 0;
|
||||
}
|
||||
|
||||
void CloseRangeTrades(EAState &state)
|
||||
{
|
||||
for(int i = m_tradeCount - 1; i >= 0; i--)
|
||||
{
|
||||
if(m_trades[i].openRegime == REGIME_RANGE)
|
||||
{
|
||||
if(PositionSelectByTicket(m_trades[i].ticket))
|
||||
ClosePosition(m_trades[i].ticket, EXIT_REGIME_CHANGE);
|
||||
RemoveTrade(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TightenStops(EAState &state)
|
||||
{
|
||||
double atr = g_volatility.GetATR();
|
||||
for(int i = 0; i < m_tradeCount; i++)
|
||||
{
|
||||
if(!PositionSelectByTicket(m_trades[i].ticket)) continue;
|
||||
double entry = PositionGetDouble(POSITION_PRICE_OPEN);
|
||||
double currentSL = PositionGetDouble(POSITION_SL);
|
||||
int type = (int)PositionGetInteger(POSITION_TYPE);
|
||||
double newSL;
|
||||
double buffer = atr * 0.5;
|
||||
if(type == POSITION_TYPE_BUY)
|
||||
{
|
||||
newSL = entry + buffer;
|
||||
if(newSL > currentSL || currentSL == 0)
|
||||
m_trade.PositionModify(m_trades[i].ticket, newSL, PositionGetDouble(POSITION_TP));
|
||||
}
|
||||
else
|
||||
{
|
||||
newSL = entry - buffer;
|
||||
if(newSL < currentSL || currentSL == 0)
|
||||
m_trade.PositionModify(m_trades[i].ticket, newSL, PositionGetDouble(POSITION_TP));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
int FindTradeIndex(ulong ticket) const
|
||||
{
|
||||
for(int i = 0; i < m_tradeCount; i++)
|
||||
if(m_trades[i].ticket == ticket) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int RegisterTrade(ulong ticket, double entry, double tp1, double sl, datetime time)
|
||||
{
|
||||
if(m_tradeCount >= ArraySize(m_trades)) ArrayResize(m_trades, ArraySize(m_trades) + 10);
|
||||
int idx = m_tradeCount++;
|
||||
m_trades[idx].ticket = ticket;
|
||||
m_trades[idx].entryPrice = entry;
|
||||
m_trades[idx].tp1Price = tp1;
|
||||
m_trades[idx].initialSL = sl;
|
||||
m_trades[idx].openTime = time;
|
||||
m_trades[idx].tp1Hit = false;
|
||||
m_trades[idx].tp2Hit = false;
|
||||
m_trades[idx].beSet = false;
|
||||
m_trades[idx].trailingActive = true;
|
||||
m_trades[idx].openRegime = g_state.currentRegime;
|
||||
m_trades[idx].partialLot = 0;
|
||||
return idx;
|
||||
}
|
||||
|
||||
void RemoveTrade(int idx)
|
||||
{
|
||||
if(idx < 0 || idx >= m_tradeCount) return;
|
||||
for(int i = idx; i < m_tradeCount - 1; i++)
|
||||
m_trades[i] = m_trades[i + 1];
|
||||
m_tradeCount--;
|
||||
}
|
||||
|
||||
void PartialClose(int idx, double totalLots, ulong ticket)
|
||||
{
|
||||
double closeLots = NormalizeDouble(totalLots * m_profile.partialCloseRatio, 2);
|
||||
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
|
||||
if(closeLots < minLot) closeLots = minLot;
|
||||
if(closeLots >= totalLots) closeLots = totalLots * 0.5;
|
||||
m_trades[idx].partialLot = closeLots;
|
||||
if(m_trade.PositionClosePartial(ticket, closeLots))
|
||||
g_logger.LogEvent("TRADE", StringFormat("Partial close %.2f lots ticket %llu", closeLots, ticket));
|
||||
else
|
||||
g_logger.LogEvent("TRADE", StringFormat("Partial close FAILED ticket %llu", ticket));
|
||||
}
|
||||
|
||||
void SetBreakEven(int idx, double entry, double currentSL, int type, double atr)
|
||||
{
|
||||
double buffer = atr * BE_BUFFER_ATR_MULT;
|
||||
double newSL;
|
||||
if(type == POSITION_TYPE_BUY) newSL = entry + buffer;
|
||||
else newSL = entry - buffer;
|
||||
bool shouldMove = (type == POSITION_TYPE_BUY && (newSL > currentSL || currentSL == 0)) ||
|
||||
(type == POSITION_TYPE_SELL && (newSL < currentSL || currentSL == 0));
|
||||
if(shouldMove)
|
||||
{
|
||||
double currentTP = PositionGetDouble(POSITION_TP);
|
||||
if(m_trade.PositionModify(m_trades[idx].ticket, newSL, currentTP))
|
||||
{
|
||||
m_trades[idx].beSet = true;
|
||||
g_logger.LogEvent("TRADE", StringFormat("BE set ticket %llu at %.5f", m_trades[idx].ticket, newSL));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateTrailingStop(int idx, double currentPrice, int type, double atr, double currentSL)
|
||||
{
|
||||
double trailDist = atr * m_profile.trailingATRMult;
|
||||
double newSL;
|
||||
if(type == POSITION_TYPE_BUY)
|
||||
{
|
||||
newSL = currentPrice - trailDist;
|
||||
if(newSL > currentSL)
|
||||
{
|
||||
double currentTP = PositionGetDouble(POSITION_TP);
|
||||
m_trade.PositionModify(m_trades[idx].ticket, newSL, currentTP);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
newSL = currentPrice + trailDist;
|
||||
if(newSL < currentSL || currentSL == 0)
|
||||
{
|
||||
double currentTP = PositionGetDouble(POSITION_TP);
|
||||
m_trade.PositionModify(m_trades[idx].ticket, newSL, currentTP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ClosePosition(ulong ticket, ENUM_EXIT_REASON reason)
|
||||
{
|
||||
if(m_trade.PositionClose(ticket))
|
||||
g_logger.LogEvent("TRADE", StringFormat("Closed ticket %llu. Reason: %s", ticket, EnumToString(reason)));
|
||||
}
|
||||
|
||||
int CountOurPositions() const
|
||||
{
|
||||
int count = 0;
|
||||
int total = PositionsTotal();
|
||||
for(int i = 0; i < total; i++)
|
||||
if(PositionGetSymbol(i) == _Symbol && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber)
|
||||
count++;
|
||||
return count;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // __TRADE_MANAGER_MQH__
|
||||
@@ -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__
|
||||
@@ -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__
|
||||
@@ -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__
|
||||
@@ -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__
|
||||
@@ -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__
|
||||
@@ -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__
|
||||
@@ -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__
|
||||
@@ -0,0 +1,170 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| Risk/Protection.mqh |
|
||||
//| Circuit Breakers: Daily/Weekly Loss, Consecutive Loss, Spread |
|
||||
//| MODIFIED: Completed UpdateState with live statistics tracking |
|
||||
//+------------------------------------------------------------------+
|
||||
#ifndef __PROTECTION_MQH__
|
||||
#define __PROTECTION_MQH__
|
||||
|
||||
#include "../Core/Config.mqh"
|
||||
#include "../Core/State.mqh"
|
||||
#include "../Core/Logger.mqh"
|
||||
|
||||
extern CLogger g_logger;
|
||||
|
||||
class CProtection
|
||||
{
|
||||
private:
|
||||
double m_maxDailyLoss;
|
||||
double m_maxWeeklyLoss;
|
||||
int m_maxConsecLosses;
|
||||
int m_maxPositions;
|
||||
double m_maxTotalRisk;
|
||||
datetime m_lastDailyReset;
|
||||
datetime m_lastWeeklyReset;
|
||||
double m_lastEquity;
|
||||
int m_consecLossCounter;
|
||||
datetime m_lastTradeTime;
|
||||
|
||||
public:
|
||||
bool Init(double dailyLoss, double weeklyLoss, int consecLoss, int maxPos, double maxRisk)
|
||||
{
|
||||
m_maxDailyLoss = dailyLoss;
|
||||
m_maxWeeklyLoss = weeklyLoss;
|
||||
m_maxConsecLosses = consecLoss;
|
||||
m_maxPositions = maxPos;
|
||||
m_maxTotalRisk = maxRisk;
|
||||
m_lastDailyReset = 0;
|
||||
m_lastWeeklyReset = 0;
|
||||
m_lastEquity = AccountInfoDouble(ACCOUNT_EQUITY);
|
||||
m_consecLossCounter = 0;
|
||||
m_lastTradeTime = 0;
|
||||
Print("[Protection] Circuit breakers active. Daily:", dailyLoss, "% Weekly:", weeklyLoss, "% Consec:", consecLoss);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsCircuitBreakerActive(EAState &state) const
|
||||
{
|
||||
if(TimeCurrent() < state.circuitBreakerUntil)
|
||||
return true;
|
||||
if(state.circuitBreakerUntil > 0 && TimeCurrent() >= state.circuitBreakerUntil)
|
||||
{
|
||||
g_logger.LogEvent("PROTECTION", "Circuit breaker expired. Trading resumed.");
|
||||
state.circuitBreakerUntil = 0;
|
||||
state.circuitBreakerReason = "";
|
||||
state.dailyLimitHit = false;
|
||||
state.weeklyLimitHit = false;
|
||||
state.consecLossHalted = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PreTradeCheck(EAState &state) const
|
||||
{
|
||||
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
||||
if(equity <= 0) return false;
|
||||
double dailyLimit = equity * (m_maxDailyLoss / 100.0);
|
||||
if(state.dailyPnL <= -dailyLimit)
|
||||
{
|
||||
ActivateBreaker(state, "Daily Loss Limit", 86400);
|
||||
state.dailyLimitHit = true;
|
||||
return false;
|
||||
}
|
||||
double weeklyLimit = equity * (m_maxWeeklyLoss / 100.0);
|
||||
if(state.weeklyPnL <= -weeklyLimit)
|
||||
{
|
||||
ActivateBreaker(state, "Weekly Loss Limit", 7 * 86400);
|
||||
state.weeklyLimitHit = true;
|
||||
return false;
|
||||
}
|
||||
if(state.consecutiveLosses >= m_maxConsecLosses)
|
||||
{
|
||||
ActivateBreaker(state, "Consecutive Losses", 86400);
|
||||
state.consecLossHalted = true;
|
||||
return false;
|
||||
}
|
||||
if(state.openPositions >= m_maxPositions)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsSpreadAcceptable(const AssetProfile &profile) const
|
||||
{
|
||||
if(!InpUseSpreadFilter) return true;
|
||||
long spreadPoints = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
|
||||
double spreadPrice = spreadPoints * _Point;
|
||||
return (spreadPrice <= profile.maxSpreadPoints);
|
||||
}
|
||||
|
||||
void UpdateState(EAState &state)
|
||||
{
|
||||
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
|
||||
if(m_lastEquity > 0 && currentEquity != m_lastEquity)
|
||||
{
|
||||
double equityChange = currentEquity - m_lastEquity;
|
||||
state.dailyPnL += equityChange;
|
||||
state.weeklyPnL += equityChange;
|
||||
if(equityChange < 0)
|
||||
{
|
||||
if(TimeCurrent() != m_lastTradeTime)
|
||||
{
|
||||
m_consecLossCounter++;
|
||||
state.consecutiveLosses = m_consecLossCounter;
|
||||
m_lastTradeTime = TimeCurrent();
|
||||
g_logger.LogEvent("PROTECTION", StringFormat("Loss detected. Consecutive: %d/%d", m_consecLossCounter, m_maxConsecLosses));
|
||||
}
|
||||
}
|
||||
else if(equityChange > 0)
|
||||
{
|
||||
if(m_consecLossCounter > 0)
|
||||
{
|
||||
m_consecLossCounter = 0;
|
||||
state.consecutiveLosses = 0;
|
||||
g_logger.LogEvent("PROTECTION", "Profit detected. Consecutive loss counter reset.");
|
||||
}
|
||||
}
|
||||
}
|
||||
m_lastEquity = currentEquity;
|
||||
if(InpDebugMode)
|
||||
{
|
||||
g_logger.LogEvent("PROTECTION", StringFormat("State | Daily: %.2f | Weekly: %.2f | Consec: %d | Equity: %.2f",
|
||||
state.dailyPnL, state.weeklyPnL, state.consecutiveLosses, currentEquity));
|
||||
}
|
||||
}
|
||||
|
||||
void CheckDailyReset(EAState &state)
|
||||
{
|
||||
MqlDateTime dt;
|
||||
TimeToStruct(TimeCurrent(), dt);
|
||||
datetime todayStart = StringToTime(StringFormat("%04d.%02d.%02d 00:00:00", dt.year, dt.mon, dt.day));
|
||||
if(todayStart > m_lastDailyReset)
|
||||
{
|
||||
state.dailyPnL = 0;
|
||||
state.totalTradesToday = 0;
|
||||
state.consecutiveLosses = 0;
|
||||
m_consecLossCounter = 0;
|
||||
m_lastDailyReset = todayStart;
|
||||
state.equityAtStart = AccountInfoDouble(ACCOUNT_EQUITY);
|
||||
m_lastEquity = state.equityAtStart;
|
||||
g_logger.LogEvent("PROTECTION", "Daily counters reset");
|
||||
}
|
||||
if(dt.day_of_week == 1 && todayStart > m_lastWeeklyReset)
|
||||
{
|
||||
state.weeklyPnL = 0;
|
||||
state.totalTradesWeek = 0;
|
||||
m_lastWeeklyReset = todayStart;
|
||||
state.equityAtWeekStart = AccountInfoDouble(ACCOUNT_EQUITY);
|
||||
g_logger.LogEvent("PROTECTION", "Weekly counters reset");
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void ActivateBreaker(EAState &state, string reason, int seconds) const
|
||||
{
|
||||
state.circuitBreakerUntil = TimeCurrent() + seconds;
|
||||
state.circuitBreakerReason = reason;
|
||||
g_logger.LogEvent("PROTECTION", StringFormat("CIRCUIT BREAKER: %s. Halted for %d sec.", reason, seconds));
|
||||
}
|
||||
};
|
||||
|
||||
#endif // __PROTECTION_MQH__
|
||||
Binary file not shown.
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
Reference in New Issue
Block a user