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;
| |||
}
|