mql5/Experts/Advisors/DualEA/Scripts/AssessPromotion.mq5

563 lines
22 KiB
MQL5
Raw Permalink Normal View History

2025-10-16 18:04:00 -04:00
//+------------------------------------------------------------------+
//| AssessPromotion.mq5 - DualEA Promotion Gate Evaluator |
//| Comprehensive evaluation script for PaperEA → LiveEA promotion |
//+------------------------------------------------------------------+
#property copyright "DualEA Promotion System"
#property version "1.00"
#property strict
//+------------------------------------------------------------------+
//| INCLUDES |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\HistoryOrderInfo.mqh>
#include <Files\File.mqh>
// Removed ArrayObj since we use native arrays
//+------------------------------------------------------------------+
//| PROMOTION CRITERIA CONFIGURATION |
//+------------------------------------------------------------------+
struct SPromotionCriteria
{
// Minimum thresholds for promotion
int min_trades; // Minimum number of trades required
double min_hit_rate; // Minimum win rate (0.0-1.0)
double min_expectancy_r; // Minimum expectancy in R-multiples
double max_drawdown_pct; // Maximum drawdown percentage
double min_sharpe_ratio; // Minimum Sharpe ratio
double min_profit_factor; // Minimum profit factor
int min_trading_days; // Minimum trading days
double min_avg_trade_r; // Minimum average trade in R
double max_consecutive_losses; // Maximum consecutive losses
// Constructor with defaults
SPromotionCriteria() :
min_trades(50),
min_hit_rate(0.55),
min_expectancy_r(0.2),
max_drawdown_pct(15.0),
min_sharpe_ratio(1.0),
min_profit_factor(1.3),
min_trading_days(30),
min_avg_trade_r(0.1),
max_consecutive_losses(10)
{}
};
//+------------------------------------------------------------------+
//| TRADE STATISTICS STRUCTURE |
//+------------------------------------------------------------------+
struct STradeStats
{
int total_trades;
int winning_trades;
int losing_trades;
double hit_rate;
double gross_profit;
double gross_loss;
double net_profit;
double max_drawdown;
double sharpe_ratio;
double profit_factor;
double expectancy_r;
double avg_trade_r;
int consecutive_losses;
int trading_days;
datetime first_trade;
datetime last_trade;
// Constructor
STradeStats() :
total_trades(0), winning_trades(0), losing_trades(0), hit_rate(0.0),
gross_profit(0.0), gross_loss(0.0), net_profit(0.0),
max_drawdown(0.0), sharpe_ratio(0.0), profit_factor(0.0),
expectancy_r(0.0), avg_trade_r(0.0), consecutive_losses(0),
trading_days(0), first_trade(0), last_trade(0)
{}
};
//+------------------------------------------------------------------+
//| PROMOTION RESULT STRUCTURE |
//+------------------------------------------------------------------+
struct SPromotionResult
{
bool passed;
string symbol;
string timeframe;
string failure_reason;
STradeStats stats;
SPromotionCriteria criteria;
// Detailed pass/fail breakdown
bool trades_pass;
bool hit_rate_pass;
bool expectancy_pass;
bool drawdown_pass;
bool sharpe_pass;
bool profit_factor_pass;
bool trading_days_pass;
bool avg_trade_pass;
bool consecutive_losses_pass;
SPromotionResult() : passed(false), trades_pass(false), hit_rate_pass(false),
expectancy_pass(false), drawdown_pass(false), sharpe_pass(false),
profit_factor_pass(false), trading_days_pass(false),
avg_trade_pass(false), consecutive_losses_pass(false) {}
};
//+------------------------------------------------------------------+
//| GLOBAL VARIABLES |
//+------------------------------------------------------------------+
SPromotionCriteria g_criteria;
string g_report_path = "DualEA\\promotion_report.txt";
string g_telemetry_path = "DualEA\\telemetry";
//+------------------------------------------------------------------+
//| SCRIPT ENTRY POINT |
//+------------------------------------------------------------------+
void OnStart()
{
Print("=== DualEA Promotion Assessment Starting ===");
// Load criteria from config file if exists
LoadPromotionCriteria();
// Get all symbols and timeframes from telemetry
string symbols[];
int timeframes[];
GetAvailableSymbolsTimeframes(symbols, timeframes);
if(ArraySize(symbols) == 0)
{
Print("No telemetry data found. Ensure PaperEA has been running.");
return;
}
// Process each symbol/timeframe combination
SPromotionResult results[];
for(int i = 0; i < ArraySize(symbols); i++)
{
for(int j = 0; j < ArraySize(timeframes); j++)
{
string symbol = symbols[i];
int timeframe = timeframes[j];
SPromotionResult r;
if(EvaluatePromotion(symbol, timeframe, r))
{
int n = ArraySize(results);
ArrayResize(results, n+1);
results[n] = r;
}
}
}
// Generate comprehensive report
GenerateReport(results);
// No dynamic allocations used; nothing to cleanup
Print("=== Promotion Assessment Complete ===");
}
//+------------------------------------------------------------------+
//| LOAD PROMOTION CRITERIA FROM CONFIG |
//+------------------------------------------------------------------+
void LoadPromotionCriteria()
{
string config_path = "DualEA\\promotion_criteria.json";
int handle = FileOpen(config_path, FILE_READ | FILE_COMMON | FILE_TXT | FILE_ANSI);
if(handle != INVALID_HANDLE)
{
string content = "";
while(!FileIsEnding(handle))
{
content += FileReadString(handle) + "\n";
}
FileClose(handle);
// Parse JSON (basic implementation)
ParseCriteriaFromJSON(content);
Print("Loaded promotion criteria from config file");
}
else
{
Print("Using default promotion criteria");
}
}
//+------------------------------------------------------------------+
//| PARSE CRITERIA FROM JSON |
//+------------------------------------------------------------------+
void ParseCriteriaFromJSON(const string& json)
{
// Basic JSON parsing - extract key values
string key;
key = "\"min_trades\":";
int pos = StringFind(json, key);
if(pos >= 0) g_criteria.min_trades = (int)StringToDouble(StringSubstr(json, pos + StringLen(key), 10));
key = "\"min_hit_rate\":";
pos = StringFind(json, key);
if(pos >= 0) g_criteria.min_hit_rate = StringToDouble(StringSubstr(json, pos + StringLen(key), 10));
key = "\"min_expectancy_r\":";
pos = StringFind(json, key);
if(pos >= 0) g_criteria.min_expectancy_r = StringToDouble(StringSubstr(json, pos + StringLen(key), 10));
key = "\"max_drawdown_pct\":";
pos = StringFind(json, key);
if(pos >= 0) g_criteria.max_drawdown_pct = StringToDouble(StringSubstr(json, pos + StringLen(key), 10));
key = "\"min_sharpe_ratio\":";
pos = StringFind(json, key);
if(pos >= 0) g_criteria.min_sharpe_ratio = StringToDouble(StringSubstr(json, pos + StringLen(key), 10));
key = "\"min_profit_factor\":";
pos = StringFind(json, key);
if(pos >= 0) g_criteria.min_profit_factor = StringToDouble(StringSubstr(json, pos + StringLen(key), 10));
}
//+------------------------------------------------------------------+
//| GET AVAILABLE SYMBOLS AND TIMEFRAMES FROM TELEMETRY |
//+------------------------------------------------------------------+
// Helper: add unique string to array
bool AddUniqueString(string &arr[], const string value)
{
for(int i=0;i<ArraySize(arr);++i) if(arr[i]==value) return false;
int n=ArraySize(arr); ArrayResize(arr,n+1); arr[n]=value; return true;
}
// Helper: add unique int to array
bool AddUniqueInt(int &arr[], const int value)
{
for(int i=0;i<ArraySize(arr);++i) if(arr[i]==value) return false;
int n=ArraySize(arr); ArrayResize(arr,n+1); arr[n]=value; return true;
}
void GetAvailableSymbolsTimeframes(string &symbols[], int &timeframes[])
{
string search_path = g_telemetry_path + "\\*.csv";
// iterate files using FileFindFirst/Next
string file;
// Use Windows API to find files
string path = TerminalInfoString(TERMINAL_COMMONDATA_PATH) + "\\" + search_path;
// Scan directory for telemetry files
long search_handle = FileFindFirst(path, file);
if(search_handle != INVALID_HANDLE)
{
do
{
// Parse symbol and timeframe from filename format: SYMBOL_TF_YYYYMMDD.csv
string parts[];
int part_count = StringSplit(file, '_', parts);
if(part_count >= 2)
{
string symbol = parts[0];
int timeframe = (int)StringToInteger(parts[1]);
// Add unique symbols and timeframes
AddUniqueString(symbols, symbol);
AddUniqueInt(timeframes, timeframe);
}
}
while(FileFindNext(search_handle, file));
FileFindClose(search_handle);
}
}
//+------------------------------------------------------------------+
//| EVALUATE PROMOTION FOR SPECIFIC SYMBOL/TIMEFRAME |
//+------------------------------------------------------------------+
bool EvaluatePromotion(const string symbol, const int timeframe, SPromotionResult &result)
{
result.symbol = symbol;
result.timeframe = EnumToString((ENUM_TIMEFRAMES)timeframe);
result.criteria = g_criteria;
// Load trade history from telemetry
if(!LoadTradeHistory(symbol, timeframe, result.stats))
{
return false;
}
// Evaluate each criterion
EvaluateCriteria(result);
// Determine overall pass/fail
result.passed = result.trades_pass && result.hit_rate_pass && result.expectancy_pass &&
result.drawdown_pass && result.sharpe_pass && result.profit_factor_pass &&
result.trading_days_pass && result.avg_trade_pass && result.consecutive_losses_pass;
if(!result.passed)
{
BuildFailureReason(result);
}
return true;
}
//+------------------------------------------------------------------+
//| LOAD TRADE HISTORY FROM TELEMETRY |
//+------------------------------------------------------------------+
bool LoadTradeHistory(const string symbol, const int timeframe, STradeStats& stats)
{
string filename = StringFormat("%s\\%s_%d_telemetry.csv", g_telemetry_path, symbol, timeframe);
string filepath = TerminalInfoString(TERMINAL_COMMONDATA_PATH) + "\\" + filename;
int handle = FileOpen(filepath, FILE_READ | FILE_TXT | FILE_ANSI);
if(handle == INVALID_HANDLE)
{
PrintFormat("No telemetry data for %s %s", symbol, EnumToString((ENUM_TIMEFRAMES)timeframe));
return false;
}
// Skip header
FileReadString(handle);
double equity_curve[];
double daily_returns[];
int consecutive_losses = 0;
int max_consecutive_losses = 0;
while(!FileIsEnding(handle))
{
string line = FileReadString(handle);
if(StringLen(line) < 10) continue;
string fields[];
int field_count = StringSplit(line, ',', fields);
if(field_count < 10) continue;
// Parse trade data
string event_type = fields[4];
if(event_type == "trade_close")
{
stats.total_trades++;
double profit = StringToDouble(fields[7]);
double risk_r = StringToDouble(fields[8]);
if(profit > 0)
{
stats.winning_trades++;
stats.gross_profit += profit;
consecutive_losses = 0;
}
else
{
stats.losing_trades++;
stats.gross_loss += MathAbs(profit);
consecutive_losses++;
max_consecutive_losses = MathMax(max_consecutive_losses, consecutive_losses);
}
// Track first/last trade times
datetime trade_time = (datetime)StringToInteger(fields[0]);
if(stats.first_trade == 0 || trade_time < stats.first_trade)
stats.first_trade = trade_time;
if(trade_time > stats.last_trade)
stats.last_trade = trade_time;
// Build equity curve for calculations
int n = ArraySize(equity_curve);
ArrayResize(equity_curve, n+1);
equity_curve[n] = stats.gross_profit - stats.gross_loss;
}
}
FileClose(handle);
// Calculate derived statistics
stats.net_profit = stats.gross_profit - stats.gross_loss;
stats.consecutive_losses = max_consecutive_losses;
if(stats.total_trades > 0)
{
stats.hit_rate = (double)stats.winning_trades / (double)stats.total_trades;
stats.profit_factor = stats.gross_loss > 0 ? stats.gross_profit / stats.gross_loss : 999;
// Calculate expectancy in R
double total_r = 0;
if(stats.winning_trades > 0 && stats.losing_trades > 0)
{
double avg_win = stats.gross_profit / stats.winning_trades;
double avg_loss = stats.gross_loss / stats.losing_trades;
stats.expectancy_r = (stats.hit_rate * avg_win) - ((1 - stats.hit_rate) * avg_loss);
stats.avg_trade_r = stats.net_profit / stats.total_trades;
}
// Calculate max drawdown from equity curve
if(ArraySize(equity_curve) > 0)
{
double peak = equity_curve[0];
double max_dd = 0;
for(int i = 0; i < ArraySize(equity_curve); i++)
{
double current = equity_curve[i];
if(current > peak) peak = current;
double dd = peak - current;
if(dd > max_dd) max_dd = dd;
}
stats.max_drawdown = max_dd;
}
// Calculate Sharpe ratio (simplified)
if(ArraySize(equity_curve) > 1)
{
double sum_returns = 0;
double sum_sq_returns = 0;
int return_count = 0;
for(int i = 1; i < ArraySize(equity_curve); i++)
{
double ret = equity_curve[i] - equity_curve[i-1];
sum_returns += ret;
sum_sq_returns += ret * ret;
return_count++;
}
if(return_count > 0)
{
double avg_return = sum_returns / return_count;
double variance = (sum_sq_returns / return_count) - (avg_return * avg_return);
if(variance > 0)
{
stats.sharpe_ratio = avg_return / MathSqrt(variance);
}
}
}
// Calculate trading days
if(stats.first_trade > 0 && stats.last_trade > 0)
{
stats.trading_days = (int)((stats.last_trade - stats.first_trade) / (24 * 3600));
}
}
return true;
}
//+------------------------------------------------------------------+
//| EVALUATE PROMOTION CRITERIA |
//+------------------------------------------------------------------+
void EvaluateCriteria(SPromotionResult &result)
{
result.trades_pass = result.stats.total_trades >= result.criteria.min_trades;
result.hit_rate_pass = result.stats.hit_rate >= result.criteria.min_hit_rate;
result.expectancy_pass = result.stats.expectancy_r >= result.criteria.min_expectancy_r;
result.drawdown_pass = result.stats.max_drawdown <= result.criteria.max_drawdown_pct;
result.sharpe_pass = result.stats.sharpe_ratio >= result.criteria.min_sharpe_ratio;
result.profit_factor_pass = result.stats.profit_factor >= result.criteria.min_profit_factor;
result.trading_days_pass = result.stats.trading_days >= result.criteria.min_trading_days;
result.avg_trade_pass = result.stats.avg_trade_r >= result.criteria.min_avg_trade_r;
result.consecutive_losses_pass = result.stats.consecutive_losses <= result.criteria.max_consecutive_losses;
}
//+------------------------------------------------------------------+
//| BUILD FAILURE REASON STRING |
//+------------------------------------------------------------------+
void BuildFailureReason(SPromotionResult &result)
{
string failures[];
if(!result.trades_pass){int n=ArraySize(failures);ArrayResize(failures,n+1);failures[n]=StringFormat("Min trades: %d < %d", result.stats.total_trades, result.criteria.min_trades);}
if(!result.hit_rate_pass){int n=ArraySize(failures);ArrayResize(failures,n+1);failures[n]=StringFormat("Hit rate: %.2f%% < %.2f%%", result.stats.hit_rate * 100, result.criteria.min_hit_rate * 100);}
if(!result.expectancy_pass){int n=ArraySize(failures);ArrayResize(failures,n+1);failures[n]=StringFormat("Expectancy: %.2fR < %.2fR", result.stats.expectancy_r, result.criteria.min_expectancy_r);}
if(!result.drawdown_pass){int n=ArraySize(failures);ArrayResize(failures,n+1);failures[n]=StringFormat("Max DD: %.2f%% > %.2f%%", result.stats.max_drawdown, result.criteria.max_drawdown_pct);}
if(!result.sharpe_pass){int n=ArraySize(failures);ArrayResize(failures,n+1);failures[n]=StringFormat("Sharpe: %.2f < %.2f", result.stats.sharpe_ratio, result.criteria.min_sharpe_ratio);}
if(!result.profit_factor_pass){int n=ArraySize(failures);ArrayResize(failures,n+1);failures[n]=StringFormat("PF: %.2f < %.2f", result.stats.profit_factor, result.criteria.min_profit_factor);}
if(!result.trading_days_pass){int n=ArraySize(failures);ArrayResize(failures,n+1);failures[n]=StringFormat("Days: %d < %d", result.stats.trading_days, result.criteria.min_trading_days);}
if(!result.avg_trade_pass){int n=ArraySize(failures);ArrayResize(failures,n+1);failures[n]=StringFormat("Avg trade: %.2fR < %.2fR", result.stats.avg_trade_r, result.criteria.min_avg_trade_r);}
if(!result.consecutive_losses_pass){int n=ArraySize(failures);ArrayResize(failures,n+1);failures[n]=StringFormat("Consecutive losses: %d > %d", result.stats.consecutive_losses, result.criteria.max_consecutive_losses);}
// Build failure string
for(int i = 0; i < ArraySize(failures); i++)
{
if(i > 0) result.failure_reason += "; ";
result.failure_reason += failures[i];
}
}
//+------------------------------------------------------------------+
//| GENERATE COMPREHENSIVE REPORT |
//+------------------------------------------------------------------+
void GenerateReport(SPromotionResult &results[])
{
string report_path = TerminalInfoString(TERMINAL_COMMONDATA_PATH) + "\\" + g_report_path;
int handle = FileOpen(report_path, FILE_WRITE | FILE_TXT | FILE_ANSI);
if(handle == INVALID_HANDLE)
{
Print("Failed to create promotion report");
return;
}
FileWriteString(handle, "DUALEA PROMOTION ASSESSMENT REPORT\n");
FileWriteString(handle, "=================================\n\n");
FileWriteString(handle, StringFormat("Generated: %s\n", TimeToString(TimeCurrent())));
FileWriteString(handle, StringFormat("Evaluation Period: %s to %s\n\n",
TimeToString(GetOldestTradeDate(results)), TimeToString(TimeCurrent())));
int passed_count = 0;
int total_count = ArraySize(results);
for(int i = 0; i < total_count; i++)
{
SPromotionResult result = results[i];
FileWriteString(handle, StringFormat("=== %s %s ===\n", result.symbol, result.timeframe));
FileWriteString(handle, StringFormat("Status: %s\n", result.passed ? "PASS" : "FAIL"));
if(result.passed) passed_count++;
FileWriteString(handle, StringFormat("Total Trades: %d\n", result.stats.total_trades));
FileWriteString(handle, StringFormat("Win Rate: %.2f%%\n", result.stats.hit_rate * 100));
FileWriteString(handle, StringFormat("Expectancy: %.2fR\n", result.stats.expectancy_r));
FileWriteString(handle, StringFormat("Max Drawdown: %.2f%%\n", result.stats.max_drawdown));
FileWriteString(handle, StringFormat("Sharpe Ratio: %.2f\n", result.stats.sharpe_ratio));
FileWriteString(handle, StringFormat("Profit Factor: %.2f\n", result.stats.profit_factor));
FileWriteString(handle, StringFormat("Trading Days: %d\n", result.stats.trading_days));
FileWriteString(handle, StringFormat("Avg Trade: %.2fR\n", result.stats.avg_trade_r));
FileWriteString(handle, StringFormat("Consecutive Losses: %d\n", result.stats.consecutive_losses));
if(!result.passed)
{
FileWriteString(handle, StringFormat("Failure Reasons: %s\n", result.failure_reason));
}
FileWriteString(handle, "\n");
}
FileWriteString(handle, StringFormat("\nSUMMARY: %d/%d combinations passed promotion criteria\n",
passed_count, total_count));
FileWriteString(handle, StringFormat("Overall Status: %s\n",
passed_count == total_count && total_count > 0 ? "READY FOR LIVE" : "NOT READY"));
FileClose(handle);
Print(StringFormat("Promotion report generated: %s", report_path));
Print(StringFormat("Summary: %d/%d combinations passed", passed_count, total_count));
}
//+------------------------------------------------------------------+
//| GET OLDEST TRADE DATE FROM RESULTS |
//+------------------------------------------------------------------+
datetime GetOldestTradeDate(SPromotionResult &results[])
{
datetime oldest = TimeCurrent();
for(int i = 0; i < ArraySize(results); i++)
{
SPromotionResult result = results[i];
if(result.stats.first_trade > 0 && result.stats.first_trade < oldest)
{
oldest = result.stats.first_trade;
}
}
return oldest;
}