563 lines
22 KiB
MQL5
563 lines
22 KiB
MQL5
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| 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;
|
||
|
|
}
|