Измеряем главное (Часть 1): Декомпозиция портфельного риска в MQL5
Введение
Есть такой момент, который почти каждый трейдер, торгующий несколькими символами, переживает хотя бы раз. Открыты три позиции, размер каждой тщательно рассчитан. Установлены стоп-лоссы. Риск по каждой из сделок составляет всего один процент. По стандартному расчету совокупная рисковая экспозиция составляет три процента: вполне управляемый, контролируемый и разумный уровень.
Затем публикуется отчет о занятости в несельскохозяйственном секторе США.
Курс EURUSD падает на восемьдесят пипсов за тридцать секунд. Курс GBPUSD тут же следует за ним, падая на семьдесят пипсов. Цена на золото ненадолго подскакивает, а затем разворачивается вместе с остальными активами на фоне резкого укрепления доллара. Все три позиции одновременно пострадали от движения в одном направлении, вызванного одним и тем же событием. Три риска по 1% менее чем за минуту превратились в единый убыток в 3%.
Это не невезение. Так действует корреляция — как и всегда, тихо и незаметно, до того момента, когда она приобретает решающее значение. Большинство трейдеров игнорируют корреляцию из-за ограничений доступных средств: инструменты измерения риска рассчитаны на отдельные торговые инструменты, а не на портфели.
Эта статья — первая часть серии из десяти статей под названием "Измерение того, что действительно важно: декомпозиция портфельного риска в MQL5". К концу серии вы создадите готовую к внедрению систему измерения портфельного риска, позволяющую оценивать его так, как это делают трейдеры, применяющие количественные методы. В ней используются ковариационные матрицы, разложение по собственным значениям и библиотека линейной алгебры OpenBLAS, интегрированная в последние сборки MetaTrader 5.
В Части 1 рассматриваются основы: что может и чего не может отразить волатильность отдельного инструмента; почему ковариация — минимально необходимый инструмент для корректной оценки риска по нескольким символам; и как измерить разрыв между наивной и истинной оценками портфельного риска на основе текущих рыночных данных. Практический проект этой части — скрипт PortfolioRiskAnalyzer.mq5, который вы будете создавать шаг за шагом по ходу объяснения. К концу этой части вы узнаете, как работают векторы и матрицы MQL5, как получать данные по нескольким символам и как рассчитывать логарифмическую доходность. Вы также узнаете, как вызывать матричные операции, ускоряемые с помощью OpenBLAS, для расчета истинного портфельного риска. Полный скрипт приложен к статье и доступен для скачивания. Давайте начнём с постановки задачи.
Проблема: одномерная линейка для многомерного мира
Стандартное отклонение лежит в основе практически всех мер волатильности в розничном трейдинге. Оно берёт один временной ряд, измеряет разброс доходностей относительно их среднего значения и даёт в результате одно число. Это число действительно полезно. Оно показывает, насколько активными или спокойными были движения цены инструмента. Оно лежит в основе расчета размера позиции на основе ATR, ширины полос Боллинджера и десятков других инструментов, которыми трейдеры пользуются ежедневно.
Ограничение стандартного отклонения не в том, что оно неверно. Дело в том, что оно уже не дает полной картины, как только у вас открыто больше одной позиции. Стандартное отклонение является одномерным. Оно отражает величину колебаний одного инструмента. Оно ничего не говорит о том, как движения двух инструментов связаны между собой.
Рассмотрим пары EURUSD и GBPUSD. Обе валютные пары реагируют на один и тот же основной фактор — укрепление или ослабление индекса доллара США. Когда курс доллара резко растёт, курсы обеих валютных пар, как правило, падают одновременно. Когда доллар ослабевает, курсы обеих валютных пар, как правило, растут одновременно. Их движения не идентичны, но и далеко не независимы друг от друга. На обе пары влияет общий фактор риска, поэтому позиции по ним нельзя считать двумя независимыми рисками.
Стандартное отклонение показывает, что курс EURUSD в среднем изменялся на 0,42% в час, а курс GBPUSD — на 0,51% в час. Но стандартное отклонение никогда не покажет, что в те часы, когда курс EURUSD резко падал, курс GBPUSD примерно в 70% случаев также падал. Именно эта склонность к совместному движению, выраженная показателем 70%, позволяет определить, действительно ли ваши две позиции обеспечивают диверсификацию или незаметно усиливают один и тот же лежащий в их основе риск. Волатильность каждого отдельного инструмента совершенно не отражает эту взаимосвязь.
Математическим объектом, отражающим эту зависимость, является ковариационная матрица. Прежде чем вычислять эту матрицу, нужно понять, что она собой представляет.
Что отражает ковариация
Ковариация между двумя инструментами показывает, насколько их доходности склонны изменяться совместно с течением времени. Для двух рядов доходности X и Y с выборочными средними μₓ и μᵧ выборочная ковариация равна:
Cov(X, Y) = (1 / N−1) × Σᵢ (Xᵢ − μₓ)(Yᵢ − μᵧ)
На каждом баре нужно взять отклонение X от собственного среднего значения и умножить его на отклонение Y от собственного среднего значения. Если оба показателя отклонились в сторону увеличения на одном и том же баре, то произведение будет положительным. Если оба показателя отклонились в сторону понижения, то произведение также будет положительным. Если один показатель вырос, а другой снизился, то произведение будет отрицательным. Суммируя все эти произведения и деля полученную сумму на N − 1, получаем меру средней склонности доходностей двух инструментов совместно отклоняться от своих средних значений.
Большая положительная ковариация означает, что инструменты демонстрируют сильную синхронность движения. Ковариация, близкая к нулю, означает, что они изменяются независимо друг от друга. Отрицательная ковариация означает, что они, как правило, компенсируют друг друга, и именно это обеспечивает настоящую диверсификацию.
Теперь рассмотрим случай с тремя инструментами. Вместо одного числа вы получаете матрицу размером 3×3: каждой паре инструментов соответствует отдельный элемент, а на диагонали расположены дисперсии отдельных инструментов:
| EURUSD | GBPUSD | XAUUSD | |
|---|---|---|---|
| EURUSD | Var(EUR) | Cov(EUR,GBP) | Cov(EUR,XAU) |
| GBPUSD | Cov(GBP,EUR) | Var(GBP) | Cov(GBP,XAU) |
| XAUUSD | Cov(XAU,EUR) | Cov(XAU,GBP) | Var(XAU) |
Это ковариационная матрица. Она не заменяет показатели волатильности отдельных инструментов — они по-прежнему представлены на диагонали в виде значений дисперсии. Она добавляет информацию о взаимосвязях, представленных внедиагональными элементами, которые не учитываются в показателях волатильности отдельных инструментов. Для трёх инструментов матрица содержит три диагональных элемента и три уникальные пары внедиагональных элементов. Для десяти инструментов матрица имеет размер 10×10 и отражает сорок пять уникальных попарных взаимосвязей. Матрицу можно масштабировать, чтобы обрабатывать их все одновременно.
Практические последствия игнорирования этих внедиагональных элементов прослеживаются в формуле дисперсии портфеля.
Формула дисперсии портфеля: наглядное представление разрыва в оценке риска
Если у вас есть три инструмента с весами w₁, w₂ и w₃ — причём каждый вес представляет долю общего риска, приходящуюся на данный инструмент, — то истинная дисперсия портфеля равна:
True Variance = wᵀ × Σ × w
Где Σ — полная ковариационная матрица, а w — вектор весов. В развёрнутом виде для трёх инструментов эта формула выглядит так:
True Variance = w₁²Var₁ + w₂²Var₂ + w₃²Var₃ + 2w₁w₂Cov(1,2) + 2w₁w₃Cov(1,3) + 2w₂w₃Cov(2,3)
Большинство трейдеров, даже не осознавая этого, вычисляют только первые три слагаемых:
Naive Variance = w₁²Var₁ + w₂²Var₂ + w₃²Var₃
Разница заключается исключительно в перекрестных членах. Каждый перекрестный член представляет собой попарную ковариацию, умноженную на два (поскольку при полном раскрытии матричного выражения каждая пара учитывается дважды) и на произведение двух весов. Когда инструменты положительно коррелируют — как это почти всегда бывает с EURUSD и GBPUSD, — эти перекрестные члены положительны, а значит, истинная дисперсия портфеля превышает наивную оценку, полученную суммированием. Ваш реальный риск выше, чем вы рассчитали.
Когда некоторые инструменты отрицательно коррелируют, перекрестные члены снижают истинную дисперсию ниже наивной оценки, полученной суммированием. Это снижение и есть подлинная выгода от диверсификации — то, что вы получаете, но не измеряете, когда работаете исключительно с индивидуальными показателями волатильности.
Отличие скрипта, приведённого в этой статье, от прямой реализации этой развёрнутой формулы заключается в способе вычисления. Вместо того чтобы вычислять каждый перекрестный член вручную, скрипт напрямую использует матричное выражение wᵀ × Σ × w, которое вычисляется с помощью матричного умножения, ускоренного средствами OpenBLAS. Результат получается тот же, но метод точен, масштабируется на любое количество инструментов и использует ту же высокопроизводительную библиотеку, что и NumPy и SciPy в Python. Это существенное различие. Это основа, на которой строится вся серия статей.
Скрипт вычисляет как наивную, так и истинную оценку на основе текущих рыночных данных из вашего терминала и выводит разницу. Если запустить скрипт для EURUSD, GBPUSD и XAUUSD в период выраженного направленного движения доллара США, этот разрыв часто составляет от двадцати до сорока процентов наивной оценки. Это не погрешность округления — это существенная ошибка в оценке фактического риска портфеля.
OpenBLAS в MetaTrader 5: почему это важно именно здесь
Прежде чем приступить к изучению кода, стоит уделить немного времени тому, что же такое OpenBLAS и почему его наличие в MetaTrader 5 имеет значение для данной серии статей.
OpenBLAS — это реализация спецификации BLAS (Basic Linear Algebra Subprograms) с открытым исходным кодом. BLAS представляет собой набор подпрограмм для выполнения таких операций, как скалярное произведение векторов, умножение матрицы на вектор и умножение матриц, которые составляют вычислительную основу практически всего численного программного обеспечения. Библиотека тщательно оптимизирована на аппаратном уровне с использованием наборов инструкций конкретного процессора, что позволяет выполнять эти операции гораздо быстрее, чем эквивалентные циклы, написанные вручную.
Компания MetaQuotes интегрировала OpenBLAS в платформу MetaTrader 5, благодаря чему методы встроенного типа matrix в MQL5 автоматически используют эти оптимизированные подпрограммы. Когда вы вызываете matrix.Cov() или matrix.MatMul() в своём скрипте, вы не запускаете обычный цикл MQL5, а обращаетесь к библиотеке OpenBLAS, которая на внутреннем уровне передает выполнение ассемблерному коду, оптимизированному для процессора.
В этой статье используются два конкретных вызова, задействующих OpenBLAS:
all_returns.Cov() — вычисляет полную ковариационную матрицу размером 3×3 на основе матрицы данных о доходности за один вызов, используя процедуры BLAS для вычисления соответствующих скалярных произведений.
cov_matrix.MatMul(w) — выполняет умножение матрицы на вектор Σ×w, после чего вычисляется скалярное произведение полученного вектора и w для получения истинной дисперсии портфеля. Это стандартная операция BLAS уровня 2 — умножение матрицы на вектор.
Оба этих вызова явно отмечены в комментариях к коду. Причина, по которой я обращаю на это внимание, заключается не в производительности при работе с портфелем из трёх инструментов — при таких масштабах любой подход будет работать быстро. Причина заключается в корректности и масштабируемости. По мере того как в данной серии мы переходим к разложению по собственным значениям и анализу факторов риска для десяти и более инструментов, матричный подход становится единственным практичным вариантом. В Части 1 с самого начала прививается привычка работать с матрицами.
Создание скрипта: настройка проекта
Прежде чем приступить к написанию логики, в каждом скрипте MQL5 необходимо объявить свойства и входные параметры. Откройте MetaEditor, создайте новый файл скрипта с именем PortfolioRiskAnalyzer.mq5 и начните со следующего кода:
#property copyright "Measuring What Matters Series" #property link "" #property version "1.00" #property script_show_inputs
Первые три строки объявления свойств содержат стандартные метаданные. Четвертая строка, script_show_inputs, указывает MetaTrader 5 отобразить диалоговое окно ввода параметров перед началом выполнения. Без этого скрипт каждый раз запускался бы сразу с использованием значений по умолчанию. Добавление этой строки дает трейдеру возможность настроить символы, таймфрейм и веса до запуска первого расчета — именно это и будет показано на рисунке 1.
Теперь добавьте входные параметры непосредственно под свойствами:
//--- Input parameters input string Symbol1 = "EURUSD"; // First symbol input string Symbol2 = "GBPUSD"; // Second symbol input string Symbol3 = "XAUUSD"; // Third symbol input ENUM_TIMEFRAMES TF = PERIOD_H1; // Timeframe input int LookbackBars = 100; // Lookback period (bars) input double Weight1 = 0.3333; // Portfolio weight for Symbol 1 input double Weight2 = 0.3333; // Portfolio weight for Symbol 2 input double Weight3 = 0.3334; // Portfolio weight for Symbol 3
По умолчанию для трех строковых параметров, задающих символы, используются значения EURUSD, GBPUSD и XAUUSD. Эта комбинация наглядно демонстрирует проблему корреляции: у двух символов есть общий фактор влияния (доллар США), а третий (золото) связан с обоими более сложным образом. Трейдер может заменить любой из этих символов на любой другой, доступный в окне "Обзор рынка" у своего брокера.
Тип ENUM_TIMEFRAMES для TF автоматически отображает в диалоговом окне ввода раскрывающийся список допустимых таймфреймов, что исключает риск ввода недопустимого значения. PERIOD_H1 задает часовые бары, что обеспечивает баланс между достаточным количеством точек данных и выявлением значимых корреляций на интервалах в несколько часов.
Значение 100 у параметра LookbackBars означает, что скрипт использует последние 100 завершенных баров каждого символа. Это объем выборки N в формуле ковариации. При больших значениях оценки статистически более устойчивы, но медленнее реагируют на недавние изменения рыночного режима. При меньших значениях оценки быстрее реагируют на изменения, но содержат больше шума. 100 баров на H1 охватывают примерно четыре торговые недели — это удачное значение по умолчанию.
Три входных параметра Weight задают доли совокупного риска, выделенные каждому инструменту. По умолчанию каждый из них равен одной трети. Если у вас открыта более крупная позиция по XAUUSD, чем по валютным парам, увеличьте значение Weight3 и уменьшите значения остальных параметров. Сумма весов должна равняться 1,0. Скрипт проверяет это во время выполнения и выдает предупреждение, если это не так.

Шаг 1 — Вспомогательная функция PrintDivider
Прежде чем перейти к основной логике, добавьте эту небольшую вспомогательную функцию выше OnStart(). Она не выполняет сложных математических операций, а лишь выводит в журнал строку из повторяющихся символов, чтобы четко отделить разделы итогового отчета друг от друга:
//+------------------------------------------------------------------+ //| Helper: Print a formatted divider line | //+------------------------------------------------------------------+ void PrintDivider(string ch = "-", int len = 55) { string line = ""; for(int i = 0; i < len; i++) line += ch; Print(line); }Функция принимает символьную строку и длину; для обоих параметров предусмотрены значения по умолчанию. Вызов PrintDivider() выводит строку из 55 дефисов. Вызов PrintDivider("=") выводит строку из знаков равенства. Эти строки будут обрамлять разделы итогового отчета, позволяя быстро ориентироваться в журнале, а не видеть сплошную стену одинаково отформатированных строк.
Шаг 2 — Получение данных о ценах и расчет логарифмических доходностей
Первая содержательная задача — сбор истории цен по каждому символу. Встроенный тип vector в MQL5 позволяет удобно решить эту задачу: это одномерный массив чисел типа double со встроенными методами для статистических операций и методом CopyRates(), который за один вызов загружает историю цен непосредственно из кэша данных терминала.
Добавьте функцию ComputeLogReturns выше функции OnStart():
//+------------------------------------------------------------------+ //| Helper: Compute log returns from a close price vector | //+------------------------------------------------------------------+ bool ComputeLogReturns(const string symbol, ENUM_TIMEFRAMES tf, int bars, vector &log_returns) { vector prices; if(!prices.CopyRates(symbol, tf, COPY_RATES_CLOSE, 0, bars + 1)) { PrintFormat("ERROR: Could not copy rates for %s. Error: %d", symbol, GetLastError()); return false; } log_returns.Init(bars); for(int i = 0; i < bars; i++) { if(prices[i] <= 0.0 || prices[i + 1] <= 0.0) { log_returns[i] = 0.0; continue; } log_returns[i] = MathLog(prices[i] / prices[i + 1]); } return true; }
В сигнатуре функции указаны имя символа, таймфрейм, количество баров и выходной вектор, переданный по ссылке: символ & перед log_returns означает, что функция напрямую изменяет вектор вызывающего кода, а не работает с его копией. Это позволяет избежать накладных затрат, связанных с копированием больших структур данных, и является правильным подходом к возврату числовых данных из функций в MQL5.
Внутри функции первый вызов запрашивает bars + 1 значений цен, а не bars. Это связано с тем, что для вычисления N логарифмических доходностей требуется N+1 значений цен: для каждой доходности нужны значения "до" и "после". Флаг COPY_RATES_CLOSE позволяет получать только цены закрытия, которые являются стандартными исходными данными для расчета доходности в финансовой статистике и отражают окончательную согласованную цену для каждого завершенного бара.
Мы используем логарифмическую доходность, а не простую процентную доходность, по конкретной причине: логарифмическая доходность является аддитивной во времени и обладает лучшими статистическими свойствами при расчете ковариации. Кроме того, логарифмические доходности корректно учитывают асимметрию прибылей и убытков: рост на 10% с последующим падением на 10% не возвращает к исходному уровню, но при использовании логарифмических доходностей расчеты симметричны. Это важно для точности дальнейших расчетов ковариации.
Внутри функции OnStart() она вызывается три раза:
//--- 2. Gather Return Vectors vector returns1, returns2, returns3; if(!ComputeLogReturns(Symbol1, TF, LookbackBars, returns1)) return; if(!ComputeLogReturns(Symbol2, TF, LookbackBars, returns2)) return; if(!ComputeLogReturns(Symbol3, TF, LookbackBars, returns3)) return;Если какой-либо вызов завершается с ошибкой из-за отсутствия символа в окне "Обзор рынка" или недоступности истории, скрипт корректно завершает работу, вместо того чтобы продолжать обработку неполных данных и выдавать бессмысленные результаты.
Шаг 3 — Формирование матрицы доходностей
Именно здесь скрипт начинает отличаться от обычной реализации на MQL5 и использовать возможности OpenBLAS по назначению. Вместо того чтобы вычислять ковариации отдельно для каждой пары, скрипт объединяет все три вектора доходности в один объект матрицы и вычисляет полную ковариационную матрицу за один вызов.
//--- 1. Prepare Weights Vector //--- Validate weights sum to approximately 1.0 //--- 3. Construct Matrix (Rows = Assets, Columns = Bars) //--- MQL5's Cov() treats each ROW as a variable (asset), so we lay out //--- assets as rows and bars as columns — opposite of the original layout. vector w = {Weight1, Weight2, Weight3}; double weight_sum = w.Sum(); if(MathAbs(weight_sum - 1.0) > 0.001) PrintFormat("WARNING: Weights sum to %.4f, not 1.0.", weight_sum); matrix all_returns; all_returns.Init(3, LookbackBars); all_returns.Row(returns1, 0); all_returns.Row(returns2, 1); all_returns.Row(returns3, 2);
Матрица построена таким образом, что активы представлены в строках, а бары — в столбцах; это матрица размером 3×LookbackBars. Такая ориентация необходима, поскольку метод .Cov() в MQL5 рассматривает каждую строку как отдельную переменную (актив), а каждый столбец — как наблюдение. Такое расположение строк и столбцов противоположно тому, которое можно было бы интуитивно ожидать, представляя данные как таблицу наблюдений. Очень важно сделать это правильно: если бы матрица была транспонирована, вызов .Cov() вернул бы бессмысленные результаты.
Вектор весов w также инициализируется здесь, а не позже, с использованием синтаксиса инициализации в фигурных скобках языка MQL5. Благодаря этому вектор доступен на этапе умножения матрицы на вектор средствами OpenBLAS, который следует за вычислением ковариационной матрицы. Проверка суммы весов — простая мера предосторожности: она выводит предупреждение, а не останавливает выполнение, поскольку значение, достаточно близкое к 1,0, допустимо, а трейдер мог намеренно оставить небольшую долю средств в денежной форме.
Шаг 4 — Вычисление полной ковариационной матрицы (OpenBLAS)
После формирования матрицы доходности с помощью одной строки вычисляется полная ковариационная матрица размером 3×3:
//--- 4. Compute Full Covariance Matrix (TRIGGERS OPENBLAS) //--- With assets as rows, Cov() now correctly returns a [3x3] matrix //--- where each entry [i,j] is the covariance between asset i and asset j. matrix cov_matrix = all_returns.Cov();
Этот единственный вызов задействует OpenBLAS. Внутри метод .Cov() с помощью подпрограмм BLAS одновременно вычисляет суммы произведений отклонений от среднего для всех пар строк, а затем делит их на N−1, получая выборочную ковариационную матрицу. В результате получается симметричная матрица размером 3×3, в которой каждый диагональный элемент представляет собой дисперсию одного актива, а каждый внедиагональный элемент — ковариацию между двумя активами.
Сразу после этого выполняется проверка размеров матрицы, чтобы убедиться, что результат соответствует ожидаемому:
//--- Guard: must be 3x3 if(cov_matrix.Rows() != 3 || cov_matrix.Cols() != 3) { PrintFormat("ERROR: Unexpected covariance matrix shape [%dx%d]. Expected [3x3].", cov_matrix.Rows(), cov_matrix.Cols()); return; }
Эта проверка практически не влияет на производительность и позволяет выявить любые особые случаи, когда матрица могла быть построена некорректно, например если длина одного из векторов доходности отличалась от длины остальных.
Для извлечения дисперсий из диагонали достаточно одного вызова метода:
//--- Extract individual variances from the diagonal of the covariance matrix vector variances = cov_matrix.Diag(); vector volatilities = MathSqrt(variances) * 100.0;Метод .Diag() возвращает диагональные элементы в виде вектора: Var(EURUSD), Var(GBPUSD) и Var(XAUUSD) именно в таком порядке. Поэлементное извлечение квадратного корня и умножение на 100 преобразуют эти значения в показатели волатильности, выраженные в процентах. Для каждого символа выводятся оба значения:
PrintFormat(" %-10s | Volatility: %.4f%% | Variance: %.8f", Symbol1, volatilities[0], variances[0]); PrintFormat(" %-10s | Volatility: %.4f%% | Variance: %.8f", Symbol2, volatilities[1], variances[1]); PrintFormat(" %-10s | Volatility: %.4f%% | Variance: %.8f", Symbol3, volatilities[2], variances[2]);
И дисперсия, и волатильность выводятся намеренно. Значения дисперсии напрямую учитываются в расчетах портфеля, и их сопоставление с показателями волатильности помогает установить мысленную связь между цифрами, к которым вы привыкли, и исходными данными, которые фактически требуются для формулы.
Шаг 5 — Наивная оценка риска с помощью скалярного произведения (OpenBLAS)
Получив значения дисперсии, вычисляем наивную оценку дисперсии портфеля как скалярное произведение вектора квадратов весов и вектора дисперсий:
//--- 5. Compute Naive Risk (Ignoring correlations) //--- Algebraically: sum(w_i^2 * var_i). Done via OpenBLAS vector dot product. vector w_squared = MathPow(w, 2.0); double naive_variance = w_squared.Dot(variances); // TRIGGERS OPENBLAS double naive_vol = MathSqrt(naive_variance) * 100.0;
Алгебраически это эквивалентно суммированию w₁²Var₁ + w₂²Var₂ + w₃²Var₃, но выражено в виде скалярного произведения векторов, которое является операцией уровня 1 BLAS и выполняется через OpenBLAS. MathPow(w, 2.0) возводит каждый элемент вектора весов в квадрат за один вызов, без использования цикла.
В наивной оценке намеренно исключены все члены, содержащие ковариации. Это не упрощение, сделанное для удобства, — это именно тот расчет, который большинство инструментов для расчета размера позиции выполняют неявно, когда рассчитывают риск по каждому инструменту отдельно и суммируют результаты. Явно выводя это число и называя его "наивным", скрипт делает видимым то, что обычно скрыто в вычислениях.
Шаг 6 — Расчет истинного риска портфеля с помощью умножения матриц (OpenBLAS)
Истинная дисперсия портфеля вычисляется по полной матричной формуле wᵀ × Σ × w в два этапа с ускорением средствами OpenBLAS:
//--- 6. Compute True Portfolio Risk (TRIGGERS OPENBLAS) //--- Matrix formulation: True Variance = w^T * Covariance_Matrix * w //--- MatMul uses OpenBLAS level 2/3 BLAS matrix-vector multiplication gems. vector intermediate = cov_matrix.MatMul(w); // Step A: Σ × w double true_variance = w.Dot(intermediate); // Step B: wᵀ × (Σ × w)
На этапе A вызывается метод .MatMul(w), который выполняет умножение матрицы на вектор — операцию уровня 2 BLAS: ковариационная матрица размером 3×3 умножается на вектор весов из 3 элементов, в результате чего получается промежуточный вектор из 3 элементов. На этапе B вычисляется скалярное произведение исходного вектора весов и полученного промежуточного вектора, что сводит полную квадратичную форму к одному скалярному значению — истинной дисперсии портфеля.
Такое двухэтапное вычисление квадратичной формы — стандартный прием численной линейной алгебры. При реализации через циклы потребовались бы два вложенных цикла; в виде матричных операций код читается почти как математическая запись и выполняется с использованием тех же оптимизированных процедур, на которых основаны библиотеки для количественного анализа финансовых рынков.
Остальные показатели вычисляются непосредственно из истинной дисперсии:
double true_vol = MathSqrt(MathAbs(true_variance)) * 100.0; double cross_terms = true_variance - naive_variance; double hidden_gap = true_vol - naive_vol; double gap_percent = (naive_vol > 0) ? (hidden_gap / naive_vol) * 100.0 : 0.0;
Переменная cross_terms содержит разность между истинной и наивной оценками дисперсии и отражает совокупный вклад всех парных ковариаций в риск портфеля. Когда это значение положительно (а при положительной корреляции между инструментами оно будет положительным), оно показывает, какую дополнительную дисперсию несет ваш портфель, хотя наивная оценка ее не учитывает. Применение MathAbs() к true_variance перед извлечением квадратного корня служит защитой от пограничных случаев вычислений с плавающей запятой при значениях, близких к нулю.
Шаг 7 — Интерпретация результатов
В заключительной части скрипта выводится полное сравнение оценок риска:
//--- 7. Risk Gap Summary PrintFormat(" Naive Risk (correlation ignored): %.4f%%", naive_vol); PrintFormat(" True Risk (covariance included): %.4f%%", true_vol); PrintFormat(" Hidden Risk Gap : %+.4f%%", hidden_gap); PrintFormat(" Gap as %% of naive estimate : %+.2f%%", gap_percent); if(hidden_gap > 0.0001) PrintFormat(" RESULT: Portfolio is RISKIER than naive suggests by %.4f%%.", hidden_gap); else if(hidden_gap < -0.0001) PrintFormat(" RESULT: Diversification benefits found. True risk is %.4f%% LOWER.", MathAbs(hidden_gap)); else Print(" RESULT: Correlations are near zero.");
Спецификатор формата %+.4f с явно заданным знаком плюса позволяет сразу определить направление скрытого разрыва в оценке риска, не вчитываясь в число: положительные значения выводятся со знаком "+" в начале, а отрицательные — со знаком "−".

Как интерпретировать результаты и что они означают
Первое, на что стоит обратить внимание при запуске скрипта, — это строка подтверждения в самом верху журнала: "OpenBLAS threads limit is 3". Платформа MetaTrader 5 автоматически выводит эту строку при инициализации библиотеки OpenBLAS, ещё до того, как начнётся вывод данных из самого скрипта. Тем самым платформа подтверждает, что последующие матричные вычисления будут выполняться средствами оптимизированной библиотеки, а не обычными циклами MQL5.
Волатильность отдельных активов составила 0,0616% за бар для EURUSD, 0,0859% для GBPUSD и 0,3318% для XAUUSD. За этот период ретроспективного анализа волатильность золота в расчете на бар более чем в пять раз превышает волатильность EURUSD, что отражает характерный для золота более широкий диапазон колебаний по сравнению с небольшими движениями основных валютных пар, измеряемыми в пипсах. Если рассматривать эти три числа по отдельности, они показывают лишь то, насколько активно менялась цена каждого инструмента, — и ничего больше.
Наивная оценка волатильности портфеля, полученная путем возведения весов в квадрат и вычисления скалярного произведения полученного вектора и вектора дисперсий без учета всех взаимосвязей между инструментами, составила 0,1161%. Именно такое число вы получите при использовании стандартного инструмента для расчета размера позиции — если рассматривать эти три инструмента так, как будто они существуют в полной изоляции друг от друга.
Вклад взаимных ковариаций показывает совсем другую картину. В результате матричного расчета был получен положительный вклад, равный 0,00000013, что подтверждает: в течение периода ретроспективного анализа инструменты данного портфеля чаще всего демонстрировали синхронную динамику. Если прибавить это значение к истинной дисперсии портфеля и взять квадратный корень, получится истинная волатильность портфеля, равная 0,1215%.
Скрытый разрыв в оценке риска составляет +0,0054%, а в процентах от наивной оценки этот разрыв равен +4,66%. В строке с результатами прямо указано: риск портфеля на 0,0054% выше, чем предполагает наивная оценка.
Разрыв в 4,66% меньше того, который можно наблюдать во время макроэкономических событий, сопровождающихся высокой корреляцией, таких как публикация отчета о занятости в несельскохозяйственном секторе США (NFP) или объявление решения Федеральной резервной системы, но и его нельзя считать незначительным. Это означает, что наивный расчет систематически занижает истинный риск портфеля на каждом баре, пока сохраняется данная структура корреляций. Со временем, и особенно в периоды, когда корреляции становятся ещё более тесными — а это, как правило, происходит именно тогда, когда на рынках наблюдаются резкие колебания, — эта недооценка усугубляется. Расчет размера позиции на основе значения 0,1161%, когда фактическая рисковая экспозиция составляет 0,1215%, — это незаметная, но постоянная ошибка, которая обнаруживается лишь при измерении риска так, как это делает данный скрипт.
Попробуйте изменить период ретроспективного анализа, чтобы охватить другой рыночный режим, и запустите скрипт заново. В периоды выраженного направленного движения доллара или во время важных запланированных событий корреляция между EURUSD и GBPUSD значительно усиливается, а разрыв в оценке риска увеличивается. В более спокойные периоды, когда рынок движется в диапазоне, а макроэкономические факторы в меньшей степени задают направление движения, этот разрыв может сократиться. Показатель 4,66%, который вы видите здесь, представляет собой моментальный срез текущей ситуации с корреляциями, а не постоянную характеристику этих инструментов.
Заключение
Волатильность, рассчитанная отдельно для одного инструмента, отражает лишь часть общей картины. Как только у вас открыто более одной позиции, взаимосвязи между инструментами становятся столь же важными, как и движения каждого из них, однако одно лишь стандартное отклонение совершенно не отражает этих взаимосвязей.
Скрипт PortfolioRiskAnalyzer.mq5, созданный в этой статье, позволяет выявить эти взаимосвязи с помощью матричных операций, ускоренных средствами OpenBLAS. Он объединяет доходности ваших активов в матрицу, вычисляет полную ковариационную структуру за один вызов, а затем оценивает истинную дисперсию портфеля с использованием матричной формулы wᵀ × Σ × w. В результате получаются два числа: оценка риска, не учитывающая корреляцию, и оценка риска, полностью учитывающая её. Разница между этими двумя числами и есть тот разрыв, который определяет, опирается ли ваш расчет размера позиции на реальность или на её чрезмерное упрощение.
Запустите скрипт. Обратите внимание на скрытый разрыв в оценке риска. Измените период ретроспективного анализа и снова запустите скрипт для другого рыночного периода. По мере изменения корреляций эти числа будут меняться, и вы постепенно начнете интуитивно понимать, чем поведение коррелирующих инструментов отличается от того, что можно предположить на основе их индивидуальной волатильности. Это понимание станет основой для всего, что будет разработано в последующих частях серии.
Прилагаемый файл PortfolioRiskAnalyzer.mq5 содержит полный скрипт, готовый к компиляции и запуску в MetaTrader 5. В Части 2 мы возьмём вычисленную здесь ковариационную матрицу и применим к ней разложение по собственным значениям — это первый шаг на пути к полному спектральному анализу, позволяющему определить, где именно сосредоточен риск вашего портфеля.
Прилагаемый файл: PortfolioRiskAnalyzer.mq5
Далее: Часть 2 — Построение ковариационной матрицы: разложение по собственным значениям и анализ факторов риска в MQL5
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/23096
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.