Оценка финансовых рисков с помощью Apache Spark

В статье рассмотрены преимущества фреймворка Spark при расчете статистических показателей, требующих больших вычислительных ресурсов. В частности, описана методика расчета показателя VaR с помощью метода Монте-Карло.

Какой объем финансовых потерь вы можете ожидать при определенных условиях? Финансовый статистический показатель VaR (value at risk, стоимостная мера риска) предназначен для того, чтобы дать ответ на этот вопрос. Разработанный на Уолл-стрит вскоре после краха фондового рынка в 1987 году, показатель VaR нашел широкое применение во всей отрасли финансовых услуг. Некоторые компании вычисляют его по предписанию нормативных документов, другие – чтобы лучше понять характеристики рисков для больших портфелей, а третьи – перед осуществлением сделок, чтобы принять своевременные и взвешенные решения.

По некоторым причинам, которые мы обсудим позже, процесс расчета точного показателя VaR может оказаться очень ресурсоемким. Передовым подходом является моделирование с помощью метода Монте-Карло – класса алгоритмов, предназначенных для вычисления величин с помощью многочисленных случайных выборок. В тех случаях, когда невозможно получить распределение вероятностей в аналитическом виде, существует возможность оценить его форму посредством многократного моделирования более простых процессов, входящих в его состав, и последующего обобщения результатов.

В этой статье изложены теоретические сведения о показателе VaR и рассмотрены методы его расчета. Также представлена реализация вычисления этого показателя посредством метода Монте-Карло с помощью Spark, который обеспечивает эффективное параллельное моделирование.

Пример кода и небольшой набор данных находятся здесь.

VaR. Теория

VaR – это простая мера инвестиционного риска. Данный показатель представляет собой размер вероятного максимального убытка за определенный период времени. При расчете VaR учитывается заданный доверительный интервал (confidence level). Например, VaR равный $1 000 000 с доверительным интервалом 95% означает, что вероятность убытков более $1 000 000 за данный период времени составляет всего 5%.

Для расчета VaR применяется несколько различных методов:

  • Ковариационный метод (variance-covariance). Наиболее простой и наименее требовательный к вычислительным ресурсам. Позволяет получить решение аналитически на основе упрощающих допущений о распределениях вероятностей.
  • Историческое моделирование (historical simulation). Этот метод экстраполирует риск на основе данных за предыдущие периоды. Недостаток заключается в том, что исторические данные часто ограничены и не учитывают различные возможные ситуации. В доступных нам исторических данных могут отсутствовать рыночные коллапсы, при этом, возможно, нам как раз и необходимо смоделировать подобную ситуацию, чтобы узнать, что в этом случае произойдет с нашим портфелем. Существуют способы, позволяющие устранить этот недостаток, например, путем добавления в данные так называемых «потрясений», но в данной статье мы не будет затрагивать эту тему.
  • Метод Монте-Карло. Данный метод позволяет отказаться от некоторых допущений, используемых в описанных выше подходах. В своей наиболее общей форме, он реализует следующее:
    • Устанавливает взаимосвязь между рыночными условиями и стоимостью каждого инструмента.
    • Проводит «испытания» с помощью случайных рыночных условий.
    • Вычисляет стоимость портфеля в рамках каждого испытания, а затем использует агрегированные данные испытаний для создания профиля рисковых характеристик портфеля.

Безусловно, следует отметить, что метод Монте-Карло не является идеальным. Чтобы генерировать рыночные условия для испытаний и с помощью них определять стоимость инструментов портфеля, модели должны применять некоторые упрощающие допущения. Соответственно, точность полученного распределения имеет определенные ограничения.

Модель

Модель для оценки риска на основе метода Монте-Карло обычно выражает стоимость каждого инструмента с помощью набора рыночных факторов, таких как, например, значение индекса S&P 500, ВВП США или валютный курс. В нашем случае мы будем использовать простую линейную модель: стоимость инструмента рассчитывается, как взвешенная сумма значений рыночных факторов. Мы выбираем различные веса для каждого инструмента и для каждого рыночного фактора. Мы можем обучить модель для работы с каждым инструментом с помощью регрессии, используя исторические данные. Кроме того, мы обеспечим нашим инструментам возможность параметризации – для каждого из них можно будет задать минимальную и максимальную стоимость. Таким образом мы добавим элемент нелинейности, которую не может обеспечить ковариационный метод.

Также следует сказать, что мы могли бы выбрать более сложную модель, применяющую, например, определенные знания из предметной области. Spark прекрасно справляется с задачей обучения модели, но ради краткости мы не будем рассматривать здесь этот процесс.

Теперь, когда у нас есть модель для расчета стоимости инструментов на основе рыночных факторов, нам необходим процесс, моделирующий поведение рыночных факторов. Простым допущением является то, что каждый рыночный фактор имеет нормальное распределение. Чтобы учесть корреляцию рыночных факторов, которая часто имеет место (когда NASDAQ падает, Dow, скорее всего, также не в лучшей форме), мы можем использовать многомерное нормальное распределение (multivariate normal distribution) с недиагональной ковариационной матрицей (non-diagonal covariance matrix). Здесь мы также могли бы выбрать более сложный метод моделирования рынка или применить различные распределения для каждого рыночного фактора, например, с более «тяжелыми» хвостами.

Рыночные факторы для испытаний получены на основе многомерного нормального распределения:

1

Стоимость данного инструмента в данном испытании представляет собой скалярное произведение рыночных факторов и их весов wi, ограниченное минимальным и максимальным значением, ni и xi соответственно:

equation21

Стоимость портфеля в данном испытании – это сумма стоимостей всех инструментов в данном испытании:

equation31

Выполнение с помощью Spark

Недостатком метода Монте-Карло является требовательность к вычислительным ресурсам. Чтобы получить точные результаты для крупного портфеля, может потребоваться большое количество испытаний, а моделирование каждого испытания – это сложные вычисления. Здесь в игру вступает Spark.

Spark позволяет реализовать параллельные вычисления на множестве машин посредством простых операторов. Задание Spark состоит из набора преобразований параллельных коллекций (parallel collection). Мы просто передаем функции, написанные на Scala (или на Java, или на Python), а Spark распределяет вычисления по кластеру. Spark отказоустойчив, поэтому, если во время вычислений произойдет сбой какой-либо машины или процесса, нам не придется начинать все с начала.

Общая схема вычислений:

  1. Отправляем данные по инструментам на каждый узел кластера. Крупный портфель может состоять из миллионов инструментов, при этом он занимает не более нескольких десятков гигабайт и легко помещается в основной памяти современных машин.
  2. Создаем параллельную коллекцию инициализаторов (seed) для генераторов случайных чисел.
  3. Создаем новую параллельную коллекцию, содержащую стоимости портфеля при случайных условиях испытаний, с помощью функции, которая на основе каждого инициализатора генерирует набор случайных условий, вычисляет стоимость каждого инструмента при данных условиях, а затем суммирует стоимости всех инструментов.
  4. Находим граничную стоимость портфеля – значение, отделяющее нижние 5% смоделированных стоимостей портфеля от остальной части.
  5. Вычитаем граничную стоимость из текущей стоимости портфеля, чтобы найти VaR.

Представленный ниже код на Scala реализует вычисление стоимостей портфеля. Функция trialValues принимает в качестве параметров все инструменты и количество испытаний, которое необходимо выполнить. Результатом работы этой функции является массив, содержащий стоимости портфеля в каждом испытании.

def trialValues(seed: Long, numTrials: Int, instruments: Seq[Instrument],
      factorMeans: Array[Double], factorCovariances: Array[Array[Double]]): Seq[Double] = {
    val rand = new MersenneTwister(seed)
    val multivariateNormal = new MultivariateNormalDistribution(rand, factorMeans,
      factorCovariances)
 
    val trialValues = new Array[Double](numTrials)
    for (i <- 0 until numTrials) {
      val trial = multivariateNormal.sample()
      trialValues(i) = trialValue(trial, instruments)
    }
    trialValues
  }
 
  def trialValue(trial: Array[Double], instruments: Seq[Instrument]): Double = {
    var totalValue = 0.0
    for (instrument <- instruments) {
      totalValue += instrumentTrialValue(instrument, trial)
    }
    totalValue
  }
 
  def instrumentTrialValue(instrument: Instrument, trial: Array[Double]): Double = {
    var instrumentTrialValue = 0.0
    var i = 0
    while (i < trial.length) {
      instrumentTrialValue += trial(i) * instrument.factorWeights(i)
      i += 1
    }
    Math.min(Math.max(instrumentTrialValue, instrument.minValue), instrument.maxValue)
  }

Мы также могли бы написать этот код на Java или на Python.

Следующий Spark-код выполняет описанный выше Scala-код на кластере:

val broadcastInstruments = sc.broadcast(instruments)
    val seeds = (seed until seed + parallelism)
 
    val seedRdd = sc.parallelize(seeds, parallelism)
    val trialsRdd = seedRdd.flatMap(trialValues(_, numTrials / parallelism,
      broadcastInstruments.value, factorMeans, factorCovariances))

Теперь у нас есть набор стоимостей портфеля, смоделированных во множестве испытаний. Чтобы рассчитать VaR, нам необходима стоимость портфеля на границе нижних 5%, которую мы можем получить следующим образом:

val varFivePercent = trialsRdd.takeOrdered(numTrials / 20).last

Безусловно, мы не должны останавливаться на расчете VaR, потому что смоделированные данные содержат намного больше информации о рисковых характеристиках портфеля. Например, мы можем обратить внимание на те испытания, где наш портфель показал худшие результаты, и определить, какие инструменты и рыночные факторы оказали наибольшее негативное влияние. Spark также очень полезен при расчете подобных агрегаций. Кроме того, мы можем использовать ядерную оценку плотности распределения (kernel density estimation), чтобы визуализировать распределение вероятностей для смоделированных стоимостей портфеля. В репозитории размещена простая реализация этого подхода, которая скоро войдет в состав встроенной в Spark библиотеки машинного обучения MLLib.

Используя экспериментальные данные (четыре инструмента, три рыночных фактора и миллион испытаний), мы получаем следующий профиль риска (график создан на основе плотности распределения с помощью электронной таблицы):

portvalue

Другие возможности

Интерактивный анализ

До сих пор мы представляли вычисления в виде пакетного задания, однако Spark также поддерживает интерактивный режим. Благодаря этому режиму аналитик или трейдер может увидеть, что произойдет при изменении параметров модели, при выделении набора инструментов на основе определенных критериев или при учете планируемой сделки. После «трансляции» данных, Spark сохранит набор инструментов в памяти каждой машины кластера, благодаря чему они смогут обслуживать интерактивные запросы. Если регулярно выполняется фильтрация инструментов на основе определенных атрибутов, имеет смысл хранить инструменты в виде отображения, проиндексированного с помощью этих атрибутов.

Особо крупные портфели

Портфели редко бывают настолько большим, чтобы не поместиться целиком в памяти одной машины. Однако в случае особо крупных портфелей, состоящих из таких инструментов, как кредиты для малого бизнеса, может потребоваться разделение данных портфеля и размещение их на нескольких машинах.

Spark отлично справляется и с этой задачей. Вот общая схема:

val sc = new SparkContext(...)
  val instrumentsRdd = sc.parallelize(instruments, numPartitions)
  val fragmentedTrialsRdd = instrumentsRdd.mapPartitions(trialReturns(seed, _,
    numTrials, modelParameters))
  val trialsRdd = fragmentedTrialsRdd.sumByKey()
 
  def trialReturns(seed: Long, instruments: Iterator[Instrument], numTrials: Int,
      modelParameters: YourModelParameters): Iterator[(Int, Double)] = {
    // Compute the value of the given subset of instruments for all trials,
    // and emit tuples of (trialId, value)
  }

По сути, мы инвертируем параллелизм таким образом, что каждая задача вычисляет стоимость подмножества инструментов во всех испытаниях, вместо расчета стоимости всех инструментов в подмножестве испытаний.

Другие фреймворки для распределенных вычислений

До сих пор мы рассматривали Spark, не сравнивая его с другими фреймворками для распределенных вычислений. Например, почему бы не применить MPI (message passing interface, интерфейс передачи сообщений) и не воспользоваться скоростью C++? Основным преимуществом Spark является баланс между гибкостью и простотой использования. Его модель программирования позволяет описывать параллельные вычисления посредством значительно меньшего количества строк, а интерактивные оболочки для Scala и Python обеспечивают возможность экспериментировать с моделями в интерактивном режиме. Spark сочетает в себе потенциал быстрого прототипирования и высокую производительность виртуальной машины Java (JVM), что имеет существенное значение, когда приходит время ввести модель в эксплуатацию. Кроме того, для более глубокого анализа обычно необходимо работать с матрицей, содержащей данные по всем инструментам и испытаниям, которая в случае крупных портфелей может достигать в размере нескольких терабайт. Spark благодаря своей отказоустойчивости и эффективной реализации параллельных вычислений способен успешно обрабатывать данные такого объема.

Следует отметить, что только оптимизированный код позволяет максимально использовать вычислительные ресурсы. Большинство расчетов сводится к операциям над матрицами и векторами. Основная часть вычислений модели, описанной в этой статье, представляет собой скалярные произведения. При использовании Java и Python, такие операции выполняются с помощью соответствующих библиотек этих языков JBlas и NumPy, в которых операции линейной алгебры реализованы на оптимизированном Fortran. Доступ к C++ библиотекам можно получить посредством JNI (java native interface, нативный интерфейс Java).

В этой статье мы не рассматривали подобную оптимизацию, стараясь не усложнять изложение, но ей следует уделить внимание, когда речь идет о промышленном приложении.

Заключение

В этой статье мы рассмотрели, как с помощью Spark и метода Монте-Карло можно решать задачи финансовой аналитики. Показатель VaR является первым шагом, позволяющим получить ответы на ряд важных вопросов. Какова форма распределения вероятностей для стоимостей портфеля? Насколько чувствителен портфель? Какие рыночные факторы оказывают наибольшее влияние на его стоимость? Spark позволяет использовать преимущества кластеров, чтобы получить ответы на эти вопросы.

По материалам: Cloudera

Добавить комментарий

Ваш e-mail не будет опубликован.

закрыть

Поделиться

Отправить на почту
закрыть

Вход

закрыть

Регистрация

+ =