В мире высокочастотного трейдинга и алгоритмических стратегий понимание взаимосвязей между различными торговыми инструментами является ключевым фактором успеха. Корреляция валютных пар — это статистическая мера, показывающая, насколько сильно и в каком направлении движутся две пары относительно друг друга. Положительная корреляция означает, что пары движутся в одном направлении, отрицательная — в противоположных, а нулевая — что их движения независимы.
Создание индикатора тепловой карты корреляции в MQL5 позволяет визуализировать эти сложные взаимосвязи в удобном и интуитивно понятном формате. В этой статье мы, как Senior Quant Developer, разработаем такой индикатор, используя мощь многомерных массивов MQL5 для эффективного хранения и обработки данных, а также графические объекты для построения интерактивной тепловой карты прямо на графике MetaTrader 5.
Этот подход не только демонстрирует продвинутые возможности MQL5 в работе с данными и графикой, но и предоставляет трейдерам ценный инструмент для управления рисками, диверсификации портфеля и выявления торговых возможностей, например, при реализации стратегий парного трейдинга или арбитража.
Исходный код
Ниже представлен полный исходный код индикатора. Он включает в себя функции для парсинга списка валютных пар, расчета коэффициента корреляции Пирсона и динамического создания/обновления графических объектов на графике для формирования тепловой карты.
//+------------------------------------------------------------------+
//| CorrelationHeatmap.mq5 |
//| Copyright 2023, FinFluct.com |
//| https://finfluct.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, FinFluct.com"
#property link "https://finfluct.com"
#property version "1.00"
#property indicator_chart_window
#property indicator_plots 0 // No indicator buffers needed, we use chart objects
//--- Input parameters
input string InpPairs = "EURUSD,GBPUSD,USDJPY,AUDUSD,USDCAD,NZDUSD,USDCHF"; // Comma-separated list of currency pairs
input ENUM_TIMEFRAMES InpTimeframe = PERIOD_H1; // Timeframe for correlation calculation
input int InpCorrelationPeriod = 200; // Number of bars for correlation calculation
input int InpDisplayOffset_X = 50; // X offset for heatmap display from chart's left edge
input int InpDisplayOffset_Y = 50; // Y offset for heatmap display from chart's top edge
input int InpCellSize = 40; // Size of each cell (square) in pixels
input string InpObjectNamePrefix = "CorrHeatmap_"; // Prefix for chart objects to manage them
//--- Global variables
string g_pairs[]; // Array to store parsed currency pairs
int g_num_pairs; // Number of parsed pairs
double g_correlations[][]; // Multidimensional array for correlations: g_correlations[row][col]
long g_last_calculated_bar = 0; // Last bar index when correlation was calculated
//+------------------------------------------------------------------+
//| Indicator initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
// Parse the input string of pairs into an array
ParsePairs(InpPairs, g_pairs);
g_num_pairs = ArraySize(g_pairs);
if (g_num_pairs < 2)
{
Print("Error: At least two currency pairs are required for correlation heatmap.");
return INIT_FAILED;
}
// Resize the multidimensional correlation array
ArrayResize(g_correlations, g_num_pairs);
for (int i = 0; i < g_num_pairs; i++)
{
ArrayResize(g_correlations[i], g_num_pairs);
}
// Create heatmap objects (labels) for displaying correlation values
for (int i = 0; i < g_num_pairs; i++)
{
for (int j = 0; j < g_num_pairs; j++)
{
string obj_name = InpObjectNamePrefix + IntegerToString(i) + "_" + IntegerToString(j);
if (!ObjectCreate(0, obj_name, OBJ_LABEL, 0, 0, 0))
{
Print("Error creating object '", obj_name, "': ", GetLastError());
return INIT_FAILED;
}
ObjectSetInteger(0, obj_name, OBJPROP_XDISTANCE, InpDisplayOffset_X + j * InpCellSize);
ObjectSetInteger(0, obj_name, OBJPROP_YDISTANCE, InpDisplayOffset_Y + i * InpCellSize);
ObjectSetInteger(0, obj_name, OBJPROP_XSIZE, InpCellSize);
ObjectSetInteger(0, obj_name, OBJPROP_YSIZE, InpCellSize);
ObjectSetInteger(0, obj_name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, obj_name, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, obj_name, OBJPROP_BACK, true); // Make it a background object
ObjectSetInteger(0, obj_name, OBJPROP_FONTSIZE, 8);
ObjectSetString(0, obj_name, OBJPROP_FONT, "Arial");
ObjectSetInteger(0, obj_name, OBJPROP_ALIGN, ALIGN_CENTER);
ObjectSetString(0, obj_name, OBJPROP_TEXT, "--"); // Initial text
ObjectSetInteger(0, obj_name, OBJPROP_BGCOLOR, clrLightGray); // Initial background color
ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlack); // Initial text color
}
}
// Create row and column headers for the heatmap
for (int i = 0; i < g_num_pairs; i++)
{
// Row header (pair name on the left)
string row_header_name = InpObjectNamePrefix + "HeaderRow_" + IntegerToString(i);
if (!ObjectCreate(0, row_header_name, OBJ_LABEL, 0, 0, 0)) return INIT_FAILED;
ObjectSetInteger(0, row_header_name, OBJPROP_XDISTANCE, InpDisplayOffset_X - InpCellSize);
ObjectSetInteger(0, row_header_name, OBJPROP_YDISTANCE, InpDisplayOffset_Y + i * InpCellSize);
ObjectSetInteger(0, row_header_name, OBJPROP_XSIZE, InpCellSize);
ObjectSetInteger(0, row_header_name, OBJPROP_YSIZE, InpCellSize);
ObjectSetInteger(0, row_header_name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, row_header_name, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, row_header_name, OBJPROP_BACK, true);
ObjectSetInteger(0, row_header_name, OBJPROP_FONTSIZE, 8);
ObjectSetString(0, row_header_name, OBJPROP_FONT, "Arial");
ObjectSetInteger(0, row_header_name, OBJPROP_ALIGN, ALIGN_CENTER);
ObjectSetString(0, row_header_name, OBJPROP_TEXT, g_pairs[i]);
ObjectSetInteger(0, row_header_name, OBJPROP_COLOR, clrBlack);
// Column header (pair name on the top)
string col_header_name = InpObjectNamePrefix + "HeaderCol_" + IntegerToString(i);
if (!ObjectCreate(0, col_header_name, OBJ_LABEL, 0, 0, 0)) return INIT_FAILED;
ObjectSetInteger(0, col_header_name, OBJPROP_XDISTANCE, InpDisplayOffset_X + i * InpCellSize);
ObjectSetInteger(0, col_header_name, OBJPROP_YDISTANCE, InpDisplayOffset_Y - InpCellSize);
ObjectSetInteger(0, col_header_name, OBJPROP_XSIZE, InpCellSize);
ObjectSetInteger(0, col_header_name, OBJPROP_YSIZE, InpCellSize);
ObjectSetInteger(0, col_header_name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, col_header_name, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, col_header_name, OBJPROP_BACK, true);
ObjectSetInteger(0, col_header_name, OBJPROP_FONTSIZE, 8);
ObjectSetString(0, col_header_name, OBJPROP_FONT, "Arial");
ObjectSetInteger(0, col_header_name, OBJPROP_ALIGN, ALIGN_CENTER);
ObjectSetString(0, col_header_name, OBJPROP_TEXT, g_pairs[i]);
ObjectSetInteger(0, col_header_name, OBJPROP_COLOR, clrBlack);
}
ChartRedraw(); // Redraw chart to show new objects
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Indicator deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
// Delete all objects created by this indicator
for (int i = ObjectsTotal(0) - 1; i >= 0; i--)
{
string obj_name = ObjectName(0, i);
if (StringFind(obj_name, InpObjectNamePrefix) == 0) // Check if object name starts with our prefix
{
ObjectDelete(0, obj_name);
}
}
ChartRedraw(); // Redraw chart to remove objects
}
//+------------------------------------------------------------------+
//| Indicator calculation function |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[])
{
// Check if enough bars for calculation
if (rates_total < InpCorrelationPeriod)
{
return 0;
}
// Only recalculate on a new bar to save resources
if (g_last_calculated_bar == time[rates_total - 1])
{
return rates_total;
}
g_last_calculated_bar = time[rates_total - 1];
// Calculate correlations for all pairs
for (int i = 0; i < g_num_pairs; i++)
{
for (int j = 0; j < g_num_pairs; j++)
{
if (i == j)
{
g_correlations[i][j] = 1.0; // Correlation of a pair with itself is 1
}
else if (i > j)
{
// Use symmetry: corr(A,B) = corr(B,A) to avoid redundant calculations
g_correlations[i][j] = g_correlations[j][i];
}
else
{
// Calculate correlation for unique pairs
g_correlations[i][j] = CalculateCorrelation(g_pairs[i], g_pairs[j], InpTimeframe, InpCorrelationPeriod);
}
// Update chart object with calculated correlation value and color
string obj_name = InpObjectNamePrefix + IntegerToString(i) + "_" + IntegerToString(j);
ObjectSetString(0, obj_name, OBJPROP_TEXT, DoubleToString(g_correlations[i][j], 2));
ObjectSetInteger(0, obj_name, OBJPROP_BGCOLOR, GetCorrelationColor(g_correlations[i][j]));
// Adjust text color for better contrast based on background color
ObjectSetInteger(0, obj_name, OBJPROP_COLOR, (MathAbs(g_correlations[i][j]) > 0.7 || MathAbs(g_correlations[i][j]) < 0.3) ? clrWhite : clrBlack);
}
}
ChartRedraw(); // Redraw chart to update heatmap
return rates_total;
}
//+------------------------------------------------------------------+
//| Helper function to parse a comma-separated string of pairs |
//+------------------------------------------------------------------+
void ParsePairs(string pairs_str, string& pairs_array[])
{
int count = 0;
int start = 0;
int end = StringFind(pairs_str, ",", start);
while (end != -1)
{
string pair = StringSubstr(pairs_str, start, end - start);
StringTrim(pair); // Remove leading/trailing spaces
if (StringLen(pair) > 0)
{
ArrayResize(pairs_array, count + 1);
pairs_array[count] = pair;
count++;
}
start = end + 1;
end = StringFind(pairs_str, ",", start);
}
// Add the last pair after the last comma (or if no commas)
string last_pair = StringSubstr(pairs_str, start);
StringTrim(last_pair);
if (StringLen(last_pair) > 0)
{
ArrayResize(pairs_array, count + 1);
pairs_array[count] = last_pair;
}
}
//+------------------------------------------------------------------+
//| Function to calculate Pearson correlation coefficient |
//+------------------------------------------------------------------+
double CalculateCorrelation(string symbol1, string symbol2, ENUM_TIMEFRAMES timeframe, int period)
{
double data1[];
double data2[];
// Get close prices for symbol1. CopyClose(symbol, timeframe, start_pos, count, array)
// start_pos = 0 means current bar, 1 means previous bar, etc.
// We need 'period' bars ending at the previous complete bar for calculation.
// So, we copy 'period' bars starting from 1 bar ago.
if (CopyClose(symbol1, timeframe, 1, period, data1) != period)
{
Print("Failed to get ", period, " bars for ", symbol1, " on ", EnumToString(timeframe));
return 0.0;
}
// Get close prices for symbol2
if (CopyClose(symbol2, timeframe, 1, period, data2) != period)
{
Print("Failed to get ", period, " bars for ", symbol2, " on ", EnumToString(timeframe));
return 0.0;
}
if (ArraySize(data1) != period || ArraySize(data2) != period)
{
Print("Data size mismatch for correlation calculation for ", symbol1, " and ", symbol2);
return 0.0;
}
// Calculate log returns. returns[i] = log(price[i+1] / price[i])
// data[0] is oldest, data[period-1] is newest from CopyClose(..., 1, period, ...)
double returns1[];
double returns2[];
ArrayResize(returns1, period - 1);
ArrayResize(returns2, period - 1);
for (int i = 0; i < period - 1; i++)
{
// Ensure no division by zero or log of non-positive number
if (data1[i] <= 0 || data2[i] <= 0)
{
Print("Non-positive price encountered for ", symbol1, " or ", symbol2);
return 0.0;
}
returns1[i] = MathLog(data1[i + 1] / data1[i]);
returns2[i] = MathLog(data2[i + 1] / data2[i]);
}
// Calculate means of returns
double mean1 = 0.0;
double mean2 = 0.0;
for (int i = 0; i < period - 1; i++)
{
mean1 += returns1[i];
mean2 += returns2[i];
}
mean1 /= (period - 1);
mean2 /= (period - 1);
// Calculate covariance and standard deviations
double covariance = 0.0;
double std_dev1_sq = 0.0;
double std_dev2_sq = 0.0;
for (int i = 0; i < period - 1; i++)
{
double diff1 = returns1[i] - mean1;
double diff2 = returns2[i] - mean2;
covariance += diff1 * diff2;
std_dev1_sq += diff1 * diff1;
std_dev2_sq += diff2 * diff2;
}
double std_dev1 = MathSqrt(std_dev1_sq / (period - 1));
double std_dev2 = MathSqrt(std_dev2_sq / (period - 1));
// Avoid division by zero if no variance (e.g., all returns are the same)
if (std_dev1 == 0.0 || std_dev2 == 0.0)
{
return 0.0;
}
return covariance / ((period - 1) * std_dev1 * std_dev2);
}
//+------------------------------------------------------------------+
//| Helper function to get a color based on correlation value |
//+------------------------------------------------------------------+
color GetCorrelationColor(double correlation)
{
// Normalize correlation from [-1, 1] to [0, 1]
// -1 -> 0 (Red)
// 0 -> 0.5 (Yellow)
// 1 -> 1 (Green)
double normalized_corr = (correlation + 1.0) / 2.0;
int r, g, b;
if (normalized_corr < 0.5)
{
// Interpolate from Red (0,0,0) to Yellow (1,1,0)
// normalized_corr ranges from 0 to 0.5
// Scale to 0 to 1 for interpolation: normalized_corr * 2
r = 255;
g = (int)(255 * (normalized_corr * 2));
b = 0;
}
else
{
// Interpolate from Yellow (1,1,0) to Green (0,1,0)
// normalized_corr ranges from 0.5 to 1
// Scale to 0 to 1 for interpolation: (normalized_corr - 0.5) * 2
r = (int)(255 * (1 - (normalized_corr - 0.5) * 2));
g = 255;
b = 0;
}
return (color)RGB(r, g, b);
}
Разбор параметров
Индикатор предоставляет несколько входных параметров для настройки его поведения и внешнего вида:
InpPairs: Строка, содержащая список валютных пар, разделенных запятыми (например, "EURUSD,GBPUSD,USDJPY"). Индикатор будет рассчитывать корреляцию между всеми парами из этого списка. Убедитесь, что все указанные пары доступны у вашего брокера.InpTimeframe: Таймфрейм, на котором будут собираться данные для расчета корреляции (например,PERIOD_H1для часового графика).InpCorrelationPeriod: Количество баров (свечей), используемых для расчета коэффициента корреляции. Большее значение дает более сглаженную, но менее оперативную корреляцию.InpDisplayOffset_X: Смещение тепловой карты от левого края графика по оси X в пикселях.InpDisplayOffset_Y: Смещение тепловой карты от верхнего края графика по оси Y в пикселях.InpCellSize: Размер каждой ячейки тепловой карты в пикселях. Это влияет на общий размер и читаемость карты.InpObjectNamePrefix: Префикс для имен всех графических объектов, создаваемых индикатором. Это помогает избежать конфликтов с другими объектами на графике и упрощает их удаление при деинициализации.
Как запустить
Для использования индикатора тепловой карты корреляции выполните следующие шаги:
- Сохраните код: Скопируйте предоставленный выше MQL5 код.
- Откройте MetaEditor: В MetaTrader 5 нажмите
F4, чтобы открыть MetaEditor. - Создайте новый индикатор: В MetaEditor выберите
File -> New -> Custom Indicator. Дайте ему имя, например,CorrelationHeatmap, и нажмитеFinish. - Вставьте код: Удалите весь сгенерированный код и вставьте скопированный код в новый файл
CorrelationHeatmap.mq5. - Скомпилируйте: Нажмите
Compile(илиF7). Убедитесь, что нет ошибок компиляции. - Прикрепите к графику: Вернитесь в MetaTrader 5. Откройте любой график. В окне
NavigatorнайдитеIndicators -> Custom -> CorrelationHeatmap. Перетащите его на график. - Настройте параметры: В появившемся окне настроек индикатора перейдите на вкладку
Inputs. Здесь вы можете изменить список валютных пар, таймфрейм, период корреляции и параметры отображения. НажмитеOK. - Интерпретация: На графике появится тепловая карта. Каждая ячейка будет показывать коэффициент корреляции между соответствующими валютными парами. Цвет ячейки будет меняться от красного (сильная отрицательная корреляция) через желтый (нулевая корреляция) до зеленого (сильная положительная корреляция). Значения корреляции обновляются с каждым новым баром на текущем графике.
Этот индикатор является мощным инструментом для анализа рынка. Он может быть использован как самостоятельный инструмент, так и в сочетании с другими индикаторами, например, с адаптивным скользящим средним Кауфмана (KAMA) для подтверждения трендов или для выявления дивергенций. Для более сложных аналитических задач, требующих машинного обучения, вы можете рассмотреть интеграцию MQL5 с Python через сокеты или передачу данных через ZeroMQ (ZMQ) сокеты, что позволит использовать продвинутые библиотеки Python для глубокого анализа корреляций и других рыночных данных.




