Initialize project in MT5 Experts directory
This commit is contained in:
+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__
|
||||
Reference in New Issue
Block a user