Разработка системы оценки портфеля из нескольких советников с учетом корреляции в MQL5
Введение
Вы создали несколько советников, каждый из которых показывает сильную кривую эквити в Тестере стратегий. Вы уверены в них и поэтому запускаете их все на одном счете. Затем наступает реальность: резкое движение доллара одновременно выбивает из игры половину ваших советников, а просадка оказывается в три раза больше, чем предсказывал любой отдельный бэктест. Что произошло?
Ответ обманчиво прост: советники были коррелированы, а их корреляция никогда не измерялась.
Многие разработчики MQL5 оптимизируют отдельные стратегии, но не оценивают их взаимодействие в портфеле. Это все равно что быть шеф-поваром, который в совершенстве владеет десятью рецептами, но никогда не задумывается, уместны ли они в одном меню. Портфель из пяти скальперов по золоту — не диверсификация, а концентрация, замаскированная под разнообразие.
В этой статье будет создан практический скрипт на MQL5, решающий эту проблему. В результате у вас будет готовый оценщик портфеля, который:
- считывает кривые эквити из отчетов о бэктесте нескольких советников;
- вычисляет матрицу корреляции Пирсона для каждой пары стратегий;
- анализирует временное покрытие портфеля по часам торговли и дням недели;
- вычисляет единую сводную оценку, показывающую, устойчив ли портфель или содержит опасно перекрывающиеся стратегии.
- не требует внешних библиотек — не нужны ни Python, ни R, все работает нативно внутри MetaTrader 5.
Проблема — почему отдельные бэктесты искажают картину риска портфеля
Начнем с конкретного сценария, знакомого каждому трейдеру, использующему несколько советников. Предположим, оптимизированы три советника:
- EA_A торгует EURUSD на таймфрейме H1, используя стратегию возврата к среднему. Чистая прибыль: 45 000 долларов. Максимальная просадка: 8%.
- EA_B торгует GBPUSD на таймфрейме H1, используя стратегию импульсного пробоя. Чистая прибыль: 38 000 долларов. Максимальная просадка: 7%.
- EA_C торгует AUDUSD на таймфрейме H4, используя канальную стратегию. Чистая прибыль: 29 000 долларов. Максимальная просадка: 6%.
На бумаге это выглядит как диверсифицированный портфель. Три разные валютные пары, два разных таймфрейма и три разные стратегии. Можно было бы наивно ожидать, что совокупная просадка должна усредниться — возможно, до 7% в целом.
Но вот что упускает такой упрощенный расчет: в парах EURUSD, GBPUSD и AUDUSD доллар США выступает либо базовой, либо котируемой валютой. Когда Федеральная резервная система объявляет неожиданное решение по ставке, все три валютные пары резко движутся в одном направлении. "Диверсифицированный" портфель внезапно ведет себя как единая концентрированная ставка против доллара.
Именно здесь корреляционный анализ приобретает решающее значение. Если измерить дневную доходность EA_A и EA_B, можно обнаружить коэффициент корреляции Пирсона +0,72 — это означает, что они приносят прибыль и убытки примерно в одни и те же дни. Добавление EA_B в портфель с EA_A дает гораздо меньший эффект диверсификации, чем предполагают отдельные бэктесты.
Ключевой вывод таков: качество портфеля не сводится к сумме качеств отдельных стратегий. Оно определяется взаимодействием этих стратегий.
Что такое хороший портфель из нескольких советников
Прежде чем писать код, необходимо определить, что означает "хороший портфель". Обширное тестирование по нескольким классам активов выявило три параметра, лучше всего определяющих устойчивость портфеля в реальных условиях:
Критерий 1 — низкая корреляция между стратегиями
Коэффициент корреляции Пирсона измеряет линейную зависимость между двумя рядами доходности. Он изменяется от -1,0 (идеального движения в противоположных направлениях), до +1,0 (идеального сонаправленного движения). При формировании портфеля следует ориентироваться на следующие диапазоны:
- |r| < 0,35: отлично — стратегии в значительной степени независимы;
- 0,35 ≤ |r| < 0,60: приемлемо — наблюдается некоторое перекрытие, но ситуация остается контролируемой;
- |r| ≥ 0,60: опасно — стратегии фактически дублируют рисковую экспозицию.
Следует учитывать, что отрицательные корреляции ценны, а не проблематичны. Советник с коэффициентом r = -0,40 относительно остальной части портфеля выступает естественным хеджем. Такой советник, как правило, приносит прибыль, когда у остальных возникает просадка. Именно к этому стремятся управляющие институциональными портфелями.
Критерий 2 — временное покрытие
Рынки ведут себя по-разному в разные часы и дни. Портфель, в котором все советники торгуют только с 14:00 до 18:00 по UTC со вторника по четверг, имеет опасные "слепые зоны". Идеальное покрытие означает: (1) в каждой основной сессии — азиатской, лондонской и нью-йоркской — активен как минимум один советник; (2) в каждый будний день есть хотя бы некоторая активность; и (3) торговля распределена по нескольким временным интервалам, а не сконцентрирована.
Критерий 3 — диверсификация по классам активов
Даже полностью некоррелированные стратегии в одном классе активов могут пострадать от одного секторального шока. Настоящая устойчивость требует распределения экспозиции по разным рыночным категориям: основным валютным парам Forex, индексам, сырьевым товарам, акциям и, возможно, облигациям.
Оценщик портфеля количественно оценит все три аспекта и объединит их в единый прикладной показатель.
Архитектура оценщика портфеля
Полное решение состоит из одного скрипта MQL5, разделенного на четыре логических модуля:
- загрузчик данных — считывает дневные P&L из CSV-файлов, по одному на каждого советника;
- модуль корреляции — вычисляет полную матрицу корреляции Пирсона размером N×N;
- анализатор покрытия — строит карту торговой активности по часам (0-23) и дням недели (пн-пт);
- функция оценки — объединяет все показатели во взвешенную сводную оценку.
Создадим каждый модуль шаг за шагом.
Модуль 1 — загрузчик данных
Сначала нужна структура данных для хранения дневной доходности каждого советника. Используется простая структура, а данные считываются из CSV-файлов, подготовленных пользователем на основе отчетов по бэктестам.
Ожидаемый формат CSV прост. Каждый файл содержит по одной строке на торговый день со следующими столбцами: Date, DailyPnL, TradeHour и TradeWeekday. Этот формат можно с минимальными усилиями сгенерировать на основе отчета Тестера стратегий.
Определим свойства скрипта и глобальные структуры данных:
#property script_show_inputs //--- Input parameters input string InpFileList = "EA_EUR.csv,EA_NDX.csv,EA_XAU.csv,EA_OIL.csv,EA_JPY.csv"; input string InpSeparator = ","; input int InpMinDays = 60; //--- Data structures struct SEAData { string name; double dailyPnL[]; int tradeHours[]; int tradeDays[]; int totalDays; double netProfit; double maxDD; }; SEAData g_eaData[]; int g_numEAs = 0;
А теперь — функция загрузки. Функция открывает CSV-файл, считывает каждую строку, накапливает дневные P&L и рассчитывает максимальную просадку как процент от пика эквити:
bool LoadEAData(string filename, SEAData &data) { data.name = filename; data.totalDays = 0; data.netProfit = 0; data.maxDD = 0; int handle = FileOpen(filename, FILE_READ|FILE_CSV|FILE_ANSI, ','); if(handle == INVALID_HANDLE) { PrintFormat("Error: Cannot open %s. Code: %d", filename, GetLastError()); return false; } //--- Skip header line if(!FileIsEnding(handle)) { while(!FileIsLineEnding(handle) && !FileIsEnding(handle)) FileReadString(handle); } double tempPnL[]; int tempHours[]; int tempDays[]; int count = 0; while(!FileIsEnding(handle)) { string dateStr = FileReadString(handle); if(dateStr == "") break; double pnl = FileReadNumber(handle); int hour = (int)FileReadNumber(handle); int wday = (int)FileReadNumber(handle); count++; ArrayResize(tempPnL, count, 100); ArrayResize(tempHours, count, 100); ArrayResize(tempDays, count, 100); tempPnL[count - 1] = pnl; tempHours[count - 1] = hour; tempDays[count - 1] = wday; data.netProfit += pnl; } FileClose(handle); if(count < InpMinDays) { PrintFormat("Warning: %s has only %d days (minimum: %d)", filename, count, InpMinDays); return false; } ArrayResize(data.dailyPnL, count); ArrayResize(data.tradeHours, count); ArrayResize(data.tradeDays, count); ArrayCopy(data.dailyPnL, tempPnL); ArrayCopy(data.tradeHours, tempHours); ArrayCopy(data.tradeDays, tempDays); data.totalDays = count; //--- Calculate max drawdown double peak = 0, equity = 0; for(int i = 0; i < count; i++) { equity += data.dailyPnL[i]; if(equity > peak) peak = equity; double dd = (peak > 0) ? (peak - equity) / peak * 100 : 0; if(dd > data.maxDD) data.maxDD = dd; } PrintFormat("Loaded %s: %d days | Net: $%.2f | MaxDD: %.2f%%", filename, count, data.netProfit, data.maxDD); return true; }
Обратите внимание: максимальная просадка рассчитывается как процент от пика эквити. Это стандартный показатель, используемый сервисом "Сигналы MQL5" и профессиональными управляющими фондами. Просадка 10% означает, что эквити в какой-то момент опустилась на 10% ниже максимального зафиксированного уровня.
Модуль 2 — модуль корреляции
Это ядро оценщика портфеля. Коэффициент корреляции Пирсона между двумя рядами X и Y определяется как
r = Sum((Xi - Xmean)(Yi - Ymean)) / sqrt(Sum((Xi - Xmean)^2) * Sum((Yi - Ymean)^2))
Проще говоря, измеряется степень синхронности двух рядов доходности в диапазоне [-1, +1]. Реализация выглядит так:
double CalcPearsonCorrelation(const double &seriesA[], const double &seriesB[], int length) { if(length < 2) return 0.0; double meanA = 0, meanB = 0; for(int i = 0; i < length; i++) { meanA += seriesA[i]; meanB += seriesB[i]; } meanA /= length; meanB /= length; double covAB = 0, varA = 0, varB = 0; for(int i = 0; i < length; i++) { double dA = seriesA[i] - meanA; double dB = seriesB[i] - meanB; covAB += dA * dB; varA += dA * dA; varB += dB * dB; } double denom = MathSqrt(varA * varB); if(denom < 1e-10) return 0.0; return covAB / denom; }
Теперь строится полная матрица размером N×N. Именно здесь появляются ключевые выводы о портфеле:
double g_corrMatrix[]; int g_matrixSize = 0; void BuildCorrelationMatrix() { g_matrixSize = g_numEAs; ArrayResize(g_corrMatrix, g_matrixSize * g_matrixSize); ArrayInitialize(g_corrMatrix, 0.0); for(int i = 0; i < g_numEAs; i++) { for(int j = 0; j < g_numEAs; j++) { if(i == j) { g_corrMatrix[i * g_matrixSize + j] = 1.0; continue; } int len = MathMin(g_eaData[i].totalDays, g_eaData[j].totalDays); double r = CalcPearsonCorrelation(g_eaData[i].dailyPnL, g_eaData[j].dailyPnL, len); g_corrMatrix[i * g_matrixSize + j] = r; } } //--- Print the matrix PrintFormat("=== CORRELATION MATRIX (%d EAs) ===", g_numEAs); string header = " "; for(int j = 0; j < g_numEAs; j++) header += StringFormat("%-10s", g_eaData[j].name); Print(header); for(int i = 0; i < g_numEAs; i++) { string row = StringFormat("%-10s", g_eaData[i].name); for(int j = 0; j < g_numEAs; j++) { double val = g_corrMatrix[i * g_matrixSize + j]; row += StringFormat("%+.3f ", val); } Print(row); } }
Если запустить код для портфеля из пяти советников, на вкладке "Эксперты" отобразится полная матрица корреляции, как показано на рис. 1:

Рис. 1. Матрица корреляции и оценка портфеля на вкладке "Эксперты"
Эта матрица отражает зависимости между стратегиями. Наибольшая абсолютная корреляция составляет 0,312, между XAU и OIL. Это ниже порога 0,35, поэтому портфель сформирован грамотно. Отрицательные значения, например -0,201 для OIL и JPY, указывают на естественное хеджирование, что полезно.
Модуль 3 — анализатор покрытия
Портфель, торгующий только во время нью-йоркской сессии, уязвим к ночным ценовым разрывам. Анализатор покрытия строит карту активности советников по 24 часам суток и 5 торговым дням недели:
SCoverageResult AnalyzeCoverage()
{
SCoverageResult result;
ArrayInitialize(result.hourMap, 0);
ArrayInitialize(result.dayMap, 0);
for(int ea = 0; ea < g_numEAs; ea++)
{
bool hourSeen[24];
bool daySeen[5];
ArrayInitialize(hourSeen, false);
ArrayInitialize(daySeen, false);
for(int i = 0; i < g_eaData[ea].totalDays; i++)
{
int h = g_eaData[ea].tradeHours[i];
int d = g_eaData[ea].tradeDays[i];
if(h >= 0 && h < 24 && !hourSeen[h])
{
hourSeen[h] = true;
result.hourMap[h]++;
}
if(d >= 0 && d < 5 && !daySeen[d])
{
daySeen[d] = true;
result.dayMap[d]++;
}
}
}
result.hoursActive = 0;
result.daysActive = 0;
for(int h = 0; h < 24; h++)
if(result.hourMap[h] > 0)
result.hoursActive++;
for(int d = 0; d < 5; d++)
if(result.dayMap[d] > 0)
result.daysActive++;
double hourRatio = result.hoursActive / 24.0;
double dayRatio = result.daysActive / 5.0;
result.coverageScore = hourRatio * 0.70 + dayRatio * 0.30;
//--- Print coverage
Print("\n=== COVERAGE ANALYSIS ===");
PrintFormat("Hours covered: %d/24 | Days: %d/5", result.hoursActive, result.daysActive);
string hourBar = "Hour Map: ";
for(int h = 0; h < 24; h++)
{
if(result.hourMap[h] == 0)
hourBar += ".";
else
hourBar += IntegerToString(MathMin(result.hourMap[h], 9));
}
Print(hourBar);
Print(" 000000000011111111112222");
Print(" 012345678901234567890123");
string dayNames[] = {"Mon", "Tue", "Wed", "Thu", "Fri"};
for(int d = 0; d < 5; d++)
PrintFormat(" %s: %d EAs active", dayNames[d], result.dayMap[d]);
PrintFormat("Coverage Score: %.3f", result.coverageScore);
return result;
} В результате на вкладке "Эксперты" создается наглядная текстовая карта активности. Каждая цифра в "карте часов" показывает, сколько советников активны в соответствующий час UTC. Точки обозначают пробелы — часы с нулевым покрытием. В устойчивом портфеле активность распределена по большинству часов, а число пробелов не превышает 1-2.

Рис. 2. Анализ покрытия с картой по часам и разбивкой по дням недели на вкладке "Эксперты"
Здесь сразу видно, что 00:00 UTC — "слепая зона", а четверг — самый слабый день. Это практически применимый вывод: любой новый советник-кандидат в идеале должен усилить покрытие именно в этих пробелах.
Модуль 4 — совокупная оценка портфеля
Теперь объединим в единую оценку. Ключевое проектное решение — как назначить вес каждому аспекту. После тестирования различных конфигураций именно эти веса лучше всего предсказывают устойчивость на данных вне обучающей выборки:
- оценка корреляции (50%) — наиболее важный фактор, штрафует портфели с высокой корреляцией между стратегиями;
- оценка покрытия (25%) — поощряет портфели, охватывающие больше торговых часов и дней недели;
- оценка диверсификации (25%) — поощряет портфели, охватывающие несколько классов активов.
struct SPortfolioScore { double corrScore; double coverScore; double diversityScore; double compositeScore; string grade; int highCorrPairs; double avgAbsCorr; };
Функция CalcPortfolioScore() вычисляет оценки по всем трем аспектам и объединяет их с использованием настраиваемых весов:
SPortfolioScore CalcPortfolioScore(SCoverageResult &coverage)
{
SPortfolioScore score;
//=== 1. Correlation Score ===
double sumAbsCorr = 0;
int pairCount = 0;
score.highCorrPairs = 0;
for(int i = 0; i < g_numEAs; i++)
{
for(int j = i + 1; j < g_numEAs; j++)
{
double r = g_corrMatrix[i * g_matrixSize + j];
sumAbsCorr += MathAbs(r);
pairCount++;
if(MathAbs(r) >= InpCorrThresh)
score.highCorrPairs++;
}
}
score.avgAbsCorr = (pairCount > 0) ? (sumAbsCorr / pairCount) : 0;
score.corrScore = MathMax(0, (1.0 - score.avgAbsCorr * 2.0)) * 100;
//=== 2. Coverage Score ===
score.coverScore = coverage.coverageScore * 100;
//=== 3. Diversity Score ===
string families[];
int numFamilies = 0;
for(int i = 0; i < g_numEAs; i++)
{
string family = DetectAssetFamily(g_eaData[i].name);
bool found = false;
for(int f = 0; f < numFamilies; f++)
if(families[f] == family)
{ found = true; break; }
if(!found)
{
numFamilies++;
ArrayResize(families, numFamilies);
families[numFamilies - 1] = family;
}
}
score.diversityScore = MathMin(numFamilies * 20.0, 100.0);
//=== 4. Composite ===
score.compositeScore = score.corrScore * InpWeightCorr +
score.coverScore * InpWeightCov +
score.diversityScore * InpWeightDiv;
//=== 5. Grade ===
if(score.compositeScore >= 90) score.grade = "A+";
else if(score.compositeScore >= 80) score.grade = "A";
else if(score.compositeScore >= 70) score.grade = "B";
else if(score.compositeScore >= 60) score.grade = "C";
else if(score.compositeScore >= 50) score.grade = "D";
else score.grade = "F";
return score;
} Вспомогательная функция DetectAssetFamily() классифицирует каждого советника по типу инструмента, которым он торгует. Она ищет в имени файла советника известные идентификаторы валют, индексов, металлов, энергоносителей и акций:
string DetectAssetFamily(string eaName) { string upper = eaName; StringToUpper(upper); if(StringFind(upper, "EUR") >= 0 || StringFind(upper, "GBP") >= 0 || StringFind(upper, "JPY") >= 0 || StringFind(upper, "AUD") >= 0 || StringFind(upper, "CHF") >= 0 || StringFind(upper, "NZD") >= 0 || StringFind(upper, "CAD") >= 0) return "FOREX"; if(StringFind(upper, "NDX") >= 0 || StringFind(upper, "SPX") >= 0 || StringFind(upper, "DAX") >= 0 || StringFind(upper, "NJ225") >= 0 || StringFind(upper, "NAS") >= 0) return "INDICES"; if(StringFind(upper, "XAU") >= 0 || StringFind(upper, "XAG") >= 0 || StringFind(upper, "GOLD") >= 0) return "METALS"; if(StringFind(upper, "OIL") >= 0 || StringFind(upper, "XTI") >= 0 || StringFind(upper, "BRENT") >= 0 || StringFind(upper, "XNG") >= 0) return "ENERGY"; if(StringFind(upper, "AMZN") >= 0 || StringFind(upper, "TSLA") >= 0 || StringFind(upper, "NVDA") >= 0 || StringFind(upper, "AAPL") >= 0 || StringFind(upper, "GOOGL") >= 0 || StringFind(upper, "XLV") >= 0 || StringFind(upper, "XLP") >= 0) return "STOCKS"; return "OTHER"; }
Основной скрипт — объединяем все вместе
Функция OnStart() управляет всем рабочим процессом. Она разбирает список файлов и пытается загрузить каждый CSV-файл. Если файлы отсутствуют, генерируются тестовые данные, чтобы скрипт мог запускаться без ручной настройки.
void OnStart() { Print("========================================"); Print(" PORTFOLIO SCORER v1.0"); Print(" Multi-EA Correlation & Coverage Tool"); Print("========================================"); //--- Parse file list string fileList[]; ushort sep = StringGetCharacter(InpSeparator, 0); int numFiles = StringSplit(InpFileList, sep, fileList); if(numFiles < 2) { Alert("Specify at least 2 EA CSV files."); return; } ArrayResize(g_eaData, numFiles); g_numEAs = 0; for(int i = 0; i < numFiles; i++) { string fname = fileList[i]; StringTrimLeft(fname); StringTrimRight(fname); if(LoadEAData(fname, g_eaData[g_numEAs])) g_numEAs++; } if(g_numEAs < 2) { //--- Files not found: auto-generate Print(">> Not enough valid files. Auto-generating samples..."); if(!EnsureSampleFiles(fileList, numFiles)) { Alert("Could not generate sample files."); return; } //--- Retry loading with new files numFiles = ArraySize(fileList); ArrayResize(g_eaData, numFiles); g_numEAs = 0; for(int i = 0; i < numFiles; i++) { string fn = fileList[i]; StringTrimLeft(fn); StringTrimRight(fn); if(LoadEAData(fn, g_eaData[g_numEAs])) g_numEAs++; } if(g_numEAs < 2) { Alert("Failed to load generated samples."); return; } } PrintFormat("\nSuccessfully loaded %d EAs.\n", g_numEAs); //--- Build correlation matrix BuildCorrelationMatrix(); //--- Analyze coverage SCoverageResult coverage = AnalyzeCoverage(); //--- Calculate score SPortfolioScore result = CalcPortfolioScore(coverage); //--- Final report Print("\n========================================"); Print(" PORTFOLIO SCORE REPORT"); Print("========================================"); PrintFormat(" EAs in portfolio: %d", g_numEAs); PrintFormat(" Avg |correlation|: %.3f", result.avgAbsCorr); PrintFormat(" High-corr pairs: %d", result.highCorrPairs); Print("----------------------------------------"); PrintFormat(" Correlation Score: %.1f / 100", result.corrScore); PrintFormat(" Coverage Score: %.1f / 100", result.coverScore); PrintFormat(" Diversity Score: %.1f / 100", result.diversityScore); Print("----------------------------------------"); PrintFormat(" COMPOSITE SCORE: %.1f / 100", result.compositeScore); PrintFormat(" GRADE: %s", result.grade); Print("========================================"); //--- Actionable recommendations Print("\n--- RECOMMENDATIONS ---"); if(result.corrScore < 50) Print("!! CRITICAL: High inter-strategy correlation. Review the matrix."); if(result.highCorrPairs > 0) PrintFormat(" %d pair(s) exceed the %.2f correlation threshold.", result.highCorrPairs, InpCorrThresh); if(result.coverScore < 70) Print(">> TIP: Add EAs active in uncovered hours/days to reduce blind spots."); if(result.diversityScore < 60) Print(">> TIP: Diversify across more asset classes (forex, metals, indices)."); if(result.compositeScore >= 80) Print("++ Portfolio is well-constructed. Maintain quality when adding new EAs."); Print("\n========================================"); Print(" Scoring complete."); Print("========================================"); }
В итоговом отчете об оценке портфеля на вкладке "Эксперты" отображаются оценки по всем трем аспектам, сводный результат и практические рекомендации:

Рис. 3. Итоговый отчет об оценке портфеля со сводной оценкой и рекомендациями
Подготовка данных — практический порядок действий
Чтобы использовать оценщик портфеля, вам понадобятся CSV-файлы с дневными P&L по каждому советнику. Вот как их создать:
- запустите бэктест в Тестере стратегий с нужными настройками;
- экспортируйте отчет через контекстное меню — щелкните правой кнопкой мыши по вкладке "Бэктест";
- обработайте сделки: сгруппируйте их по дате, подсчитайте суммарную прибыль за каждый день и зафиксируйте наиболее частые для торговли час и день недели.
Чтобы автоматизировать этот этап, используйте вспомогательную функцию, экспортирующую дневные P&L из обработчика OnTester() советника. Эта функция не входит в скрипт PortfolioScorer — ее нужно добавить в собственный советник и вызывать из OnTester().
//+------------------------------------------------------------------+ //| Export daily P&L to CSV from backtest | //| Add this to YOUR EA and call from OnTester() | //+------------------------------------------------------------------+ void ExportDailyPnL(string filename) { int handle = FileOpen(filename, FILE_WRITE|FILE_CSV|FILE_ANSI, ','); if(handle == INVALID_HANDLE) return; FileWriteString(handle, "Date,DailyPnL,TradeHour,TradeWeekday\n"); HistorySelect(0, TimeCurrent()); int totalDeals = HistoryDealsTotal(); datetime lastDate = 0; double dayPnL = 0; int dayHour = 12; for(int i = 0; i < totalDeals; i++) { ulong ticket = HistoryDealGetTicket(i); if(ticket == 0) continue; double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT); datetime dealTime = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME); MqlDateTime dt; TimeToStruct(dealTime, dt); datetime dateOnly = StringToTime(StringFormat("%04d.%02d.%02d", dt.year, dt.mon, dt.day)); if(dateOnly != lastDate && lastDate != 0) { MqlDateTime ld; TimeToStruct(lastDate, ld); FileWriteString(handle, StringFormat("%04d.%02d.%02d,%.2f,%d,%d\n", ld.year, ld.mon, ld.day, dayPnL, dayHour, ld.day_of_week - 1)); dayPnL = 0; } lastDate = dateOnly; dayPnL += profit; dayHour = dt.hour; } //--- Write last day if(lastDate != 0) { MqlDateTime ld; TimeToStruct(lastDate, ld); FileWriteString(handle, StringFormat("%04d.%02d.%02d,%.2f,%d,%d\n", ld.year, ld.mon, ld.day, dayPnL, dayHour, ld.day_of_week - 1)); } FileClose(handle); PrintFormat("Exported daily P&L to %s", filename); }
Вызовите функцию ExportDailyPnL("EA_MyStrategy.csv") из функции OnTester() в советнике. После проведения бэктестов всех советников CSV-файлы будут готовы в папке MQL5\Files терминала.
Интерпретация результатов и принятие решений
Оценщик портфеля выдает оценку от A+ до F. Однако истинная ценность заключается в оценках по отдельным аспектам и в том, как они помогают принять следующее решение:
Сценарий 1 — высокая сводная оценка (80+), оценка A
Портфель составлен грамотно. Советники слабо коррелируют между собой, покрытие широко, и портфель охватывает несколько классов активов. Старайтесь сохранять это качество: при добавлении нового советника повторно запускайте оценщик портфеля, чтобы убедиться, что оценка не ухудшается.
Сценарий 2 — низкая оценка корреляции, менее 50
Главная проблема — избыточность. Просмотрите матрицу корреляции, чтобы найти пару с наибольшим значением |r|. Возможны три варианта: удалить один из коррелирующих советников, заменить его советником, работающим с другим классом активов, или скорректировать размер лота, чтобы уменьшить вес избыточной стратегии.
Сценарий 3 — низкая оценка покрытия, менее 70
Во временном покрытии портфеля есть слепые зоны. Проверьте карту активности по часам на наличие пробелов. Если в часы 0-7 UTC нет торгов, рассмотрите возможность добавить советник, торгующий во время азиатской сессии. Если покрытие в четверг слабое, ищите стратегии, генерирующие сигналы именно в четверг.
Сценарий 4 — низкая оценка диверсификации, ниже 60
Портфель сконцентрирован в слишком малом числе классов активов. Если все советники торгуют на Forex, рассмотрите возможность добавить стратегию по золоту, индексам или акциям. Даже небольшое выделение капитала на другой рынок может значительно повысить устойчивость портфеля.
На практике лучшим кандидатом на добавление может оказаться советник, который улучшает оценку портфеля, а не тот, у которого самая высокая отдельная прибыль. Советник со скромной доходностью, но отрицательной корреляцией с существующим портфелем может сильнее способствовать долгосрочной стабильности, чем высокодоходный советник, движущийся синхронно со всеми остальными.
Расширение методики
Представленный здесь оценщик портфеля — основа для дальнейшего развития. Несколько улучшений могут сделать его еще эффективнее:
- Скользящие окна корреляции: вместо одного значения корреляции за весь бэктест рассчитывайте корреляции на скользящих 60-дневных окнах. Это показывает, стабильны ли корреляции или меняются со временем — нестабильная корреляция является скрытым фактором риска.
- Моделирование методом Монте-Карло: случайным образом перемешивайте последовательности дневных P&L и пересчитывайте оценку тысячи раз. В результате получается доверительный интервал для сводной оценки.
- Оптимизация размера лота: зная матрицу корреляции, можно рассчитать на ее основе оптимальные размеры лотов, минимизирующие дисперсию портфеля. По сути, это оптимизация Марковица по модели "среднее — дисперсия", примененная к торговым стратегиям вместо акций.
- Панель мониторинга в реальном времени: преобразуйте скрипт в советник, работающий на графике в реальном времени, непрерывно отслеживающий корреляцию между работающими стратегиями и выдающий алерты при резком превышении корреляцией порогового значения.
Заключение
Создание прибыльных отдельных советников — лишь половина дела. Другая, и, пожалуй, более важная половина — формирование портфеля, в котором советники дополняют, а не дублируют друг друга.
В этой статье создан полнофункциональный оценщик портфеля на MQL5, количественно оценивающий качество портфеля по трем аспектам: межстратегической корреляции, временному покрытию и диверсификации по классам активов. Инструмент считывает дневные P&L из CSV-файлов бэктестов, вычисляет полную матрицу корреляции Пирсона, строит карту торговой активности по часам и дням недели и выдает сводную оценку, сразу показывающую качество портфеля.
Основные выводы для практиков:
- Перед объединением советников измерьте их корреляцию. Портфель из пяти коррелирующих стратегий не становится в пять раз надежнее — он может оказаться в пять раз уязвимее.
- Пробелы в покрытии позволяют принимать практические решения. Используйте карты активности по часам и дням недели, чтобы точно определить, где портфелю требуется усиление.
- Диверсификация по классам активов обязательна для серьезных алгоритмических портфелей. Секторальная концентрация — "тихий убийца" систем с несколькими советниками.
- Наилучший советник для добавления — тот, который в наибольшей степени повышает оценку портфеля, а не обязательно тот, у которого самый высокий отдельный фактор прибыли.
Исходный код оценщика портфеля доступен в MQL5 CodeBase: Оценщик портфеля — анализатор корреляции и покрытия для нескольких советников. Скачайте его, адаптируйте к своим стратегиям и начните оценивать то, что большинство трейдеров игнорирует: качество самого портфеля, а не только качество его составляющих.
Переход от "создания советников" к "проектированию портфелей" — это переход от мышления частного трейдера к институциональному подходу. Этот скрипт — ваш первый шаг в этом направлении.
В таблице ниже перечислены все файлы исходного кода, прилагаемые к статье.
Имя файла | Описание |
|---|---|
PortfolioScorer.mq5 | Основной скрипт, объединяющий все модули. Он обрабатывает данные, рассчитывает сводную оценку и формирует итоговый отчет о состоянии портфеля на вкладке "Эксперты". |
| Data_Loader.mq5 | Модуль 1: считывает дневные P&L из предоставленных CSV-файлов, рассчитывает максимальную просадку и структурирует данные для анализа. |
| Correlation_Engine.mq5 | Модуль 2: вычисляет коэффициент корреляции Пирсона между всеми парами стратегий и строит матрицу корреляции размером N×N. |
| Coverage_Analyzer.mq5 | Модуль 3: анализирует и строит карту торговой активности по часам (0-23) и дням недели (пн-пт), чтобы выявить временные слепые зоны в портфеле. |
| Utility_ExportDailyPnL.mq5 | Автономный служебный скрипт, который можно легко подключить к OnTester() собственных советников для экспорта дневных P&L в CSV-формат для бэктестов. |
- MetaQuotes, "Справочник MQL5 — Файловые операции", Документация MQL5: Файлы
- MetaQuotes, "Справочник MQL5 — Математические функции", Документация MQL5: Математика
- MetaQuotes, "Свойства сделок", Документация MQL5: Свойства сделок
- Исходный код оценщика портфеля, MQL5 CodeBase : Portfolio Scorer — Multi-EA Correlation and Coverage Analyzer
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21955
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
От новичка до эксперта: Автоматизация внутридневных стратегий
Использование Экономического календаря MQL5 для фильтрации новостей (Часть 3): Сохранение работы при перезапусках терминала во время новостного окна
Разработка научно обоснованного сеточного советника в MQL5: Почему большинство сеточных советников терпят неудачу и что доказал Таранто
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования