Files
Apex_AI_MT5_EA_Optimizer/mql5/TradeLogger.mqh
T
LEGSTECH Optimizer 7a3e13a734 Initial commit: MT5 EA Optimizer v1.0
Full optimization system for LEGSTECH_EA_V2:
- Flask + SocketIO live dashboard (dark premium UI)
- MT5 process control (auto-kill, clean launch, retry)
- HTML report parser (UTF-16 LE, 597 trades, metrics)
- Pre-run validation and actionable error messages
- Analysis engines: Reversal, TimePerfomance, EntryExit, EquityCurve
- Composite scoring (Calmar-primary)
- Mutation engine with knowledge_base.yaml
- Validation gate: IS + Walk-Forward
- Reports folder with HTML/CSV per run
- Double-click launcher batch file
2026-04-13 02:28:09 +00:00

243 lines
8.7 KiB
Plaintext

//+------------------------------------------------------------------+
//| TradeLogger.mqh |
//| Lightweight per-trade MAE/MFE logger for MT5 Strategy Tester |
//| Drop into: MQL5/Include/TradeLogger.mqh |
//| |
//| Integration (2 steps in your EA): |
//| 1. #include <TradeLogger.mqh> // top of your EA file |
//| 2. TL_OnTick(); // inside OnTick() |
//| |
//| Output CSV is written to MQL5/Files/TradeLog_<EA>_<Symbol>.csv |
//+------------------------------------------------------------------+
#property strict
//--- Configuration (override before including if needed)
#ifndef TL_MFE_THRESHOLD_PIPS
#define TL_MFE_THRESHOLD_PIPS 0.0 // minimum MFE to record (0 = record all)
#endif
#ifndef TL_MAX_TRACKED
#define TL_MAX_TRACKED 256 // max simultaneously open trades tracked
#endif
//--- Internal state per tracked position
struct TL_TradeState
{
ulong ticket;
datetime open_time;
double open_price;
double sl;
double tp;
double lot_size;
int direction; // 1=buy, -1=sell
double mfe_price; // most favourable price seen
double mae_price; // most adverse price seen
bool active;
};
static TL_TradeState TL_Positions[TL_MAX_TRACKED];
static int TL_Count = 0;
static int TL_FileHandle = INVALID_HANDLE;
static bool TL_Initialized = false;
static string TL_FilePath = "";
//--- Point-to-pip conversion helper
double TL_PipSize()
{
double ps = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
// For 5-digit brokers, 1 pip = 10 points; for 2-digit (XAUUSD etc), 1 pip = 1 point
if(digits == 3 || digits == 5) return ps * 10.0;
return ps;
}
//--- Internal: open (or reopen) the CSV file
bool TL_OpenFile()
{
if(TL_FileHandle != INVALID_HANDLE) return true;
string ea_name = MQLInfoString(MQL_PROGRAM_NAME);
TL_FilePath = ea_name + "_" + _Symbol + "_TradeLog.csv";
TL_FileHandle = FileOpen(TL_FilePath,
FILE_WRITE | FILE_CSV | FILE_ANSI | FILE_SHARE_READ,
',');
if(TL_FileHandle == INVALID_HANDLE)
{
Print("[TradeLogger] ERROR: Cannot open file '", TL_FilePath,
"' error=", GetLastError());
return false;
}
// Write CSV header
FileWrite(TL_FileHandle,
"ticket","open_time","close_time",
"direction","open_price","close_price",
"sl","tp","lot_size",
"mfe_pips","mae_pips",
"net_pips","net_money",
"duration_minutes",
"commission","swap");
return true;
}
//--- Internal: find slot index for a ticket (-1 = not found)
int TL_FindSlot(ulong ticket)
{
for(int i = 0; i < TL_Count; i++)
if(TL_Positions[i].ticket == ticket && TL_Positions[i].active)
return i;
return -1;
}
//--- Internal: register a newly opened position
void TL_RegisterPosition(ulong ticket)
{
if(TL_Count >= TL_MAX_TRACKED) return; // overflow guard
if(!PositionSelectByTicket(ticket)) return;
TL_TradeState &s = TL_Positions[TL_Count];
s.ticket = ticket;
s.open_time = (datetime)PositionGetInteger(POSITION_TIME);
s.open_price = PositionGetDouble(POSITION_PRICE_OPEN);
s.sl = PositionGetDouble(POSITION_SL);
s.tp = PositionGetDouble(POSITION_TP);
s.lot_size = PositionGetDouble(POSITION_VOLUME);
s.direction = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) ? 1 : -1;
s.mfe_price = s.open_price;
s.mae_price = s.open_price;
s.active = true;
TL_Count++;
}
//--- Internal: flush a closed trade to CSV
void TL_FlushClosed(int idx)
{
if(!TL_OpenFile()) return;
TL_TradeState &s = TL_Positions[idx];
// Retrieve closed deal data from history
if(!HistorySelectByPosition(s.ticket)) return;
int deals = HistoryDealsTotal();
if(deals < 2) return; // need at least open + close deal
// Find the closing deal (last deal in history for this position)
ulong close_deal = 0;
double close_price = 0;
double net_money = 0;
double commission = 0;
double swap_val = 0;
datetime close_time = 0;
for(int d = deals - 1; d >= 0; d--)
{
ulong deal_ticket = HistoryDealGetTicket(d);
if(HistoryDealGetInteger(deal_ticket, DEAL_ENTRY) == DEAL_ENTRY_OUT ||
HistoryDealGetInteger(deal_ticket, DEAL_ENTRY) == DEAL_ENTRY_INOUT)
{
close_deal = deal_ticket;
close_price = HistoryDealGetDouble(deal_ticket, DEAL_PRICE);
net_money = HistoryDealGetDouble(deal_ticket, DEAL_PROFIT);
commission = HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION);
swap_val = HistoryDealGetDouble(deal_ticket, DEAL_SWAP);
close_time = (datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME);
break;
}
}
if(close_deal == 0) return;
double pip = TL_PipSize();
double mfe_pips = (s.mfe_price - s.open_price) * s.direction / pip;
double mae_pips = (s.open_price - s.mae_price) * s.direction / pip;
double net_pips = (close_price - s.open_price) * s.direction / pip;
int dur_min = (int)((close_time - s.open_time) / 60);
FileWrite(TL_FileHandle,
(string)s.ticket,
TimeToString(s.open_time, TIME_DATE|TIME_MINUTES),
TimeToString(close_time, TIME_DATE|TIME_MINUTES),
(s.direction == 1 ? "buy" : "sell"),
DoubleToString(s.open_price, _Digits),
DoubleToString(close_price, _Digits),
DoubleToString(s.sl, _Digits),
DoubleToString(s.tp, _Digits),
DoubleToString(s.lot_size, 2),
DoubleToString(MathMax(0, mfe_pips), 2),
DoubleToString(MathMax(0, mae_pips), 2),
DoubleToString(net_pips, 2),
DoubleToString(net_money, 2),
(string)dur_min,
DoubleToString(commission, 2),
DoubleToString(swap_val, 2));
FileFlush(TL_FileHandle); // flush after each trade — safe even if tester aborts
}
//+------------------------------------------------------------------+
//| TL_OnTick() — Call this inside your EA's OnTick() |
//+------------------------------------------------------------------+
void TL_OnTick()
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
// --- Update running MFE/MAE for all tracked positions ---
for(int i = 0; i < TL_Count; i++)
{
if(!TL_Positions[i].active) continue;
ulong ticket = TL_Positions[i].ticket;
// Check if position is still open
if(!PositionSelectByTicket(ticket))
{
// Position closed — flush to CSV then deactivate
TL_FlushClosed(i);
TL_Positions[i].active = false;
continue;
}
double current_price = (TL_Positions[i].direction == 1) ? bid : ask;
// Update MFE (best price in trade direction)
if(TL_Positions[i].direction == 1) // BUY: higher is better
TL_Positions[i].mfe_price = MathMax(TL_Positions[i].mfe_price, current_price);
else // SELL: lower is better
TL_Positions[i].mfe_price = MathMin(TL_Positions[i].mfe_price, current_price);
// Update MAE (worst price against trade direction)
if(TL_Positions[i].direction == 1) // BUY: lower is worse
TL_Positions[i].mae_price = MathMin(TL_Positions[i].mae_price, current_price);
else // SELL: higher is worse
TL_Positions[i].mae_price = MathMax(TL_Positions[i].mae_price, current_price);
}
// --- Register any newly opened positions not yet tracked ---
int total = PositionsTotal();
for(int p = 0; p < total; p++)
{
ulong ticket = PositionGetTicket(p);
if(ticket == 0) continue;
if(!PositionSelectByTicket(ticket)) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(TL_FindSlot(ticket) == -1)
TL_RegisterPosition(ticket);
}
}
//+------------------------------------------------------------------+
//| TL_Deinit() — Optionally call in OnDeinit() to close file |
//+------------------------------------------------------------------+
void TL_Deinit()
{
if(TL_FileHandle != INVALID_HANDLE)
{
FileClose(TL_FileHandle);
TL_FileHandle = INVALID_HANDLE;
}
Print("[TradeLogger] Log written to: ", TL_FilePath);
}
//+------------------------------------------------------------------+