7a3e13a734
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
243 lines
8.7 KiB
Plaintext
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);
|
|
}
|
|
//+------------------------------------------------------------------+
|