Русский
Русский
English
Статистика
Реклама

Блог компании технологический центр дойче банка

Векторные языки параллельный мир

01.12.2020 16:10:27 | Автор: admin

Векторные языки мало известны широкому кругу программистов и занимают узкую нишу обработки данных в финансах, статистике и прикладной математике. Хотя сам векторный подход (или, точнее, программирование с помощью массивов) распространен гораздо шире, чем может показаться. Он реализован в известных библиотеках (NumPy), популярном языке статистиков R, математических пакетах (MATLAB), даже в современных языках программирования (Julia). Однако возможность умножить матрицу на вектор простым выражением (A*v) это всего лишь вершина айсберга возможностей, которыми обладают полноценные векторные языки. При том, что эти языки не так сильно отличаются от обычных, как может показаться на первый взгляд, они заставляют программиста мыслить совершенно в других категориях и реализовывать алгоритмы способами, которые никогда не придут в голову человеку, привыкшему к Java или даже Haskell. Их характерной чертой, например, является выворачивание наизнанку циклов вместо того, чтобы спускаться по вложенным циклам вниз к простым значениям и там использовать их в функциях, вы оперируете сложными объектами целиком, давая указания языку, какие именно части этих объектов и как именно вы хотите использовать и так много раз в одном выражении. В этой статье я хочу познакомить вас с этим оригинальным подходом к реализации алгоритмов.


Введение

Для начала определим место векторных языков в мире языков программирования. Исторически первый такой язык (APL) был создан Кеннетом Иверсоном, вдохновленным математической записью. До сих пор эта "математичность" видна невооруженным взглядом и определяет область использования векторных языков финансы, статистика, математические вычисления. Векторные языки обычно выделяют в отдельную ветвь развития, что создает впечатление об их исключительности. Однако по своей сути это типичные функциональные языки, идеологически очень близкие к Lisp. Как Lisp они обладают минимальным синтаксисом, опираются на один основной тип данных массив (он же список). Программы пишутся в основном в функциональном стиле, но, в отличие от более "чистых" языков типа ML, в них нет никаких препятствий для использования элементов императивного программирования типа деструктивного присваивания. Без присваивания было бы не обойтись в любом случае, поскольку само слово векторный означает, что меняются большие массивы данных, которые было бы накладно копировать при каждом изменении.

У языков программирования есть, как правило, излюбленные области, где особенно ярко проявляются их сильные стороны. На С, например, легко работать с железом. На ML легко писать компиляторы. Lisp известен тем, что на нем легко написать интерпретатор Lisp. Аналогично на векторном языке сравнительно легко можно написать интерпретатор векторного языка. Но главная сила векторных языков, разумеется, в их векторности. Соответственно на них легко написать математическую библиотеку, что и было проделано много раз. Также вектор значений это фактически колонка в таблице в базе данных, т.е. на векторном языке должно быть легко написать базу данных, и это действительно так. В этой статье я опишу векторные языки в целом, а в следующей я планирую показать, как в пару десятков строк кода можно реализовать простой SQL интерпретатор, начиная с лексера и заканчивая джойнами.

Язык "Вектор"

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

Назовем этот язык "Вектор". Для начала определим простые (атомарные) типы данных в этом языке:

// для комментариев будем использовать обозначение как в C10; -20      // integer, ; разделяет выражения1.2; -1.3e10 // float"c"          // char

Я не буду заводить отдельный булев тип. Будем считать, что true - это обычное число 1, а false - 0.

Главный составной тип - это массив, он же список. В векторных языках для определения массивов можно использовать краткую запись:

1 -2 3              // это массив из трех элементов, достаточно перечислить элементы через пробел1.2 10 -3           // чтобы создать массив float, достаточно записать только одно число как float"string"            // для строк (массив символов) используется традиционная запись(1;"a";1.2 1.3 1.4) // смешанный массив, явная запись с помощью (;;;)

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

Еще один составной тип, который нам понадобится, - это словарь. Он же запись или объект в других языках программирования. Словарь - это два списка одинаковой длины. В одном хранятся ключи, а во втором - значения. В качестве ключей и значений можно использовать любой тип данных:

a=1 2 3 dict 3 2 1 // int -> inta[1]               // доступ через ключ - аналогично индексированиюkey a; value a     // доступ к составным частям словаря

Выше я использовал оператор присваивания "=". Главная особенность присваивания - оно действует как обычный оператор и возвращает присваиваемое значение, т.е. можно написать:

b=1+a=3*2 // b равно 7

В векторных языках традиционно уделяется особое внимание функциям от одной (нуля) или двух переменных - монадам и диадам. J доходит до того, что там абсолютно все функции имеют два и меньше аргументов. Мы не будем себя ограничивать, в "Векторе" для функций мы определим следующие правила:

{x}; {x;y}; {x;y;z} // функции от 1/2/3 переменных, определенных неявно как x,y,z{[a;b;c;d] }        // явное определение аргументовf[x;y;z]            // вызов функции с помощью скобокf x                 // монадный вызовy f x               // диадный вызов, т.е. f[x;y]{self[]}            // ссылка на саму функцию для рекурсивного вызова

Нам не понадобится присваивать значения глобальным переменным в функциях, поэтому все присваивания внутри функций локальные.

Определим несколько базовых функций:

+ - * /         // арифметические функции== < > <= >= <> // функции сравнения~               // функция эквивалентности. Если 1 == x~y, то x и y неотличимы.list            // создать массив(список) из аргументов: list[1;2],               // конкатенация списков и атомов (чисел и символов)

Функция "," - центральная во всех векторных языках, она соединяет два объекта в один. Если ее аргументы - словари, то результатом будет словарь (значения с одинаковыми ключами будут взяты из правого аргумента), в остальных случаях результатом будет список:

1,2 -> (1;2)1,2 3 -> (1;2;3)"ab",1 2 -> ("a";"b";1;2)

Этих сведений нам пока достаточно, остальные функции определим по ходу дела.

3 кита векторных языков

Все векторные языки основаны на нескольких базовых принципах, которые сильно выделяют их из общей массы языков:

  1. Порядок выполнения операций.

  2. Особые модификаторы функций.

  3. Опора на индексирование и структурно-полиморфные базовые функции.

Я не включаю в этот список специальные знаки для обозначения базовых операций, хотя именно они являются первым, что бросается в глаза при знакомстве с векторным языком. Дело в том, что только в APL они действительно к месту, в остальные языки они перекочевали по наследству и вместо ясности, наоборот, добавляют тумана и затрудняют чтение программы. Как вы убедитесь, можно все (почти) особые обозначения заменить на обычные слова и ничего не потерять. Даже длина выражений возрастет всего раза в два.

Первые два пункта и индексирование подробно объясняются в следующих трех разделах. Под структурно-полиморфными функциями я понимаю функции, которым безразличен не только тип аргумента, но и его конкретный вид. Например, функция first возвращает первый элемент чего бы то ни было атома (простого типа данных), массива, словаря. Точно также , соединяет два любых объекта, которые можно соединить. Некоторые из этих функций я определю по ходу дела, а часть подробнее рассмотрю в специальном разделе ниже.

Порядок выполнения

В векторных языках вычисления традиционно производятся справа налево, а все встроенные функции и операторы имеют одинаковый приоритет. Т.е. выражение типа

2*3+1

следует читать как

2*(3+1)

Порядок выполнения (справа налево) нет необходимости обосновывать, потому что, если фиксировать один порядок для всех выражений, то у нас есть всего два возможных варианта. При этом, если присваивание - это такая же операция, как и остальные, то порядок справа налево получается наиболее логичным. Что касается приоритета операций, то достаточно взглянуть на любую программу на APL или J, чтобы понять, что его просто не может быть в принципе - там слишком много различных операций и базовых функций, чтобы можно было приписать им какие-то разумные приоритеты.

На самом деле, вопрос стоило бы поставить иначе - зачем вообще нужен приоритет операций. Пользы от него почти никакой, зато он порождает двусмысленности и необходимость запоминать лишнюю информацию. Например, в С++ существует 17 уровней приоритета операций, некоторые из которых право ассоциативные, а некоторые лево ассоциативные. Порядок вычислений не определен, но при этом есть больше двух десятков правил, определяющих этот порядок в частных случаях. Есть даже особый класс задач для собеседований, нужный чтобы подловить кандидатов на незнании этих правил. В векторных языках, напротив, основное правило одно - вычисления производятся справа налево, логичным исключением из этого правила являются скобки и управляющие конструкции типа if-then-else.

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

Модификаторы функций

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

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

модификатор действия + опциональные аргументы + название + опциональный индикатор монадности

Где модификатор действия это "/" или "\" (об этом ниже), а индикатор монадности - ":". Индикатор монадности нужен для тех случаев, когда мы хотим вызвать функцию с одним аргументом, но из контекста это неясно. Т.е. в общем случае функция с суффиксом выглядит так:

fn/[args]suffix:

map

Рассмотрим самые важные суффиксы, и тогда их смысл станет понятнее. Например, суффикс map:

f/map[a1;..;an]; x f/map y; f/map x

Это классическая функция, которая в наше время есть во всех языках. Смысл ее в том, чтобы вызвать f последовательно для всех элементов списков одинаковой длины a1,..,an с одинаковыми индексами. Однако суффикс map имеет важное отличие - любой аргумент может быть атомом. Если все аргументы атомы, то f/map эквивалентно просто f. Примеры:

1 +/map 1 2 3 -> 2 3 41 2 3 */map 2 3 4 -> 2 6 121 {x+y}/map 1 -> 2

APL и J допускают для map дополнительный аргумент rank (дальнейшее является переносом этой идеи в наш язык, а не буквальным описанием). Просто map имеет бесконечный ранг, что значит, что функция применяется непосредственно к элементам списка. 0-й ранг значит, что функцию необходимо применить к атомарным значениям, 1-й - к векторам, 2-й к матрицам и т.д. Можно допустить и отрицательные значения, чтобы указывать ранг с другой стороны, т.е. количество map в сложном суффиксе "/map/map/.../map". Это бывает удобно, когда мы хотим применить функцию к элементам сложной структуры. Например:

// хотим применить f к элементам матрицыf/[0]map matrix ~ f/map/map matrix ~ f/[-2]map matrix

Также понятие ранга полезно для понимания действия базовых операторов и функций в векторном языке. Многие из них по умолчанию имеют суффикс "/[0]map". Например, "+" на самом деле "+/[0]map", т.е. он принимает в качестве аргументов любые структуры, которые можно сложить. Действие map определяется в таком случае рекурсивно - к атомам применяется сама функция, в остальных случаях применяется рекурсивно f/[0]map. Например:

(1;1 2;1 1) + (10 20;10;1 1) -> (11 21;11 12;2 2)  // аргументы конформны, т.е. имеют совместимую форму1 2 + 1 2 3 -> exception                          // а так нельзя, длина спиcков должна быть одинакова

fold

Следующий суффикс - это, конечно, reduce, он же fold или свертка:

f/fold[a1;..;an] или f\fold[a1;..;an]

Для одного аргумента вызов fold эквивалентен подставлению f между элементами массива:

a[n-1] f ... f a[1] f a[0]

Классический пример - это суммирование элементов списка:

+/fold 1 2 3 -> 6

fold легко расширяется на произвольное число аргументов. Любой аргумент, как и в случае map, может быть атомом. Есть два варианта fold: /fold и \fold. Разница в том, что мы хотим получить в конце - только финальный результат или также все промежуточные. Сравните, например:

+/fold 1 2 3 4 -> 10+\fold 1 2 3 4 -> 1 3 6 10

Иногда бывает полезно передать в fold начальное значение, используем для этого параметр суффикса:

+/[100]fold 1 2 3 -> 106

left, right

Также очень полезны суффиксы left и right. Это разновидности map для двух аргументов:

x f/right y ~ f[x]/map yx f/left y ~ f[;y]/map x // запись f[;y] означает, что один аргумент пропущен.

Эти суффиксы нужны в ситуациях типа:

// строка  это массив, поэтому просто map использовать нельзя"Mr. " ,/right ("John";"Bill") ~ ("Mr. John";"Mr. Bill")  

Поскольку left и right - это разновидности map, то логично добавить в них поддержку рангов. В первую очередь потому, что некоторые базовые функции неявно определяются с рангом 0. Например, функция поиска в массиве:

a ? b ~ a ?/[0]right b // возвращает индекс в "a" для каждого атома в "b"1 2 3?(1 2;2 3) ~ (0 1;1 2)

Обобщенное индексирование и присваивание

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

Вполне логично, что в векторных языках в качестве индексов можно использовать не только числа, но и массивы. Для определения семантики такой операции воспользуемся самим языком:

// обычное индексирование, i - числоa i ~ a idx i ~ a[i]                                       // обобщенное индексирование, один уровень, ii - вектор или атом чиселa ii ~ a[ii] ~ a idx/right ii// индексирование вглубь, рекурсивное определение. i1,i2,.. - атомы или вектора чиселa[i1;i2;...] ~ a didx (i1;i2;...) ~ a[i1] didx/left (i2;..)

Отметим разницу между idx и didx. Первый - это поверхностный (shallow) индекс, второй - индекс вглубь. На idx логично навесить неявный суффикс "/[0]right", чтобы можно было индексировать с помощью любых структур.

Например, если "a" - это изображение, то вычислить все конволюции (свертки, основная операция в сверточных нейронных сетях - CNN) с помощью функции "f" можно таким выражением:

b = (0 1 2;0 1 2)                  // квадрат 3x3a[0 1 2;0 1 2] ~ a didx b          // подматрица 3x3 в ac = til count[a]-2                 // допустимые индексы, til n ~ 0 .. n-1c {f a didx b+(x;y)}/right/left c  // конструкция /right/left  это декартово произведение, результат которого  матрица всех пар из c

Для такой полезной функции как didx неплохо бы иметь свой собственный суффикс. Назовем его set:

// f\[i1;..;in]set[a;b;..] определим какv=a; v[i1;..;in] = f/[neg n]map[a[i1;...;in];b;..]; v

Выражение выглядит запутанно, но суть его проста. Мы выбираем подмножество "a" с помощью глубокого индекса i1..in, после чего вызываем функцию f спустившись на глубину индекса, т.е. f вызывается отдельно для каждого элемента, который мы индексируем, а остальные аргументы разлагаются на части согласно правилам map (т.е. они должны быть конформны форме индекса). set возвращает копию "a", где проиндексированные элементы заменены результатом функции. set является обобщением присваивания, поскольку позволяет менять не только переменные, но и любые значения:

// присвоить центральному квадрату матрицы значение 1(0 0 0 0;0 0 0 0;0 0 0 0;0 0 0 0) =\[1 2;1 2]set 1

Часто при присваивании мы хотим также вызвать какую-нибудь функцию:

a[1 3] += 10 // прибавить 10 к элементам с индексами 1 и 3

Такой синтаксис позволяет вызывать некоторые встроенные функции, но не произвольные. set решает эту проблему, если мы сделаем одно допущение:

a +\[1 3]set 10 // присваиваниеa +/[1 3]set 10 // просто выражениеneg/[1 3]set a  // унарный вариант присваивания, изменить знак элементов 1 3

Т.е. добавим правило, что если слэш указывает на переменную, то изменение происходит в самой переменной (in place), если нет, то в функцию передается ее копия. С помощью такого set можно коротким выражением описать программу, которая займет не один десяток строк кода на обычном языке.

Например, пусть у нас есть массив чисел "a", и есть список апдейтов. Каждый апдейт это функция + индексы в "a", к которым ее нужно применить, + значения. Разделим апдейты на три части - массив функций "fn", массив соответствующих им индексов "ii" и массив значений "v". С помощью set все апдейты можно применить одной строкой:

{a x\[y]set z}/fold[fn;ii;v]

Практический пример

Того, что мы уже знаем о векторных языках достаточно, чтобы решить вполне практическую задачу. Представим, что у нас есть котировки неких товаров (например, акций) - price - и объем предложения для каждой цены - volume. Нам поступает заявка от клиента - order - купить столько-то товара по самой выгодной цене:

price = (100 102 103;200 201;300 310 320 330)volume = (10 9 12;5 6;30 25 20 22)order = (8;20;45)

Необходимо посчитать: а) сколько реально каждого товара будет продано б) сколько придется заплатить с) убрать проданное из цен и объема. Остановитесь и подумайте минуту, как бы вы реализовали этот алгоритм на своем любимом языке. Так вы четче почувствуете контраст с векторным подходом.

Нам понадобится функция where, которая вычисляет индексы ненулевых элементов:

where 0 1 1 -> 1 2a where a=1        // она используется в основном для фильтрации массивовa filter a=1       // в APL/J используется ее бинарный аналог, который сразу фильтрует аргумент

Итак, программа на нашем игрушечном языке займет меньше места, чем потребовалось на описание задачи:

total = +/fold ,/fold: price * v=0 min volume + 0 max order -\fold/map volumesold = +/fold/map vvolume = volume app/map ii= where 0 < volume= volume - v // x app y ~ x[y]price = price app/map ii

Самая сложная часть, пожалуй, это вычисление переменной v, поэтому я покажу, что происходит для одного заказа по шагам:

// выполняем заказ, когда числа становятся отрицательными, он выполнен45 -\fold 30 23 20 22 -> 15 -10 -30 -52// убираем те предложения, которые полностью исчерпаны0 max <val> -> 0 -10 -30 -52// теперь положительные числа - это то, что попало в заказvolume + <val> -> 30 15 -10 -30// убираем отрицательный мусор и получаем количество реально проданного для каждого предложенияv=0 min <val> -> 30 15 0 0// теперь, чтобы определить сколько было продано товара типа 2, достаточно сложить полученные числа+/fold v -> 45// чтобы получить сумму, нужно умножить на цены и сложить+/fold price[2] * v -> 13650

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

Для простоты я взял order той же длины, что и price/volume. На практике order будет значительно короче, как же поменяется программа:

iorder=0 2; order=8 45 // каким-то образом нам заданы индексы ordertotal = +/fold ,/fold: price[iorder] * v=0 min vo + 0 max order -\fold/map vo=volume iordersold = +/fold/map vvolume[iorder] = vo app/map ii= where 0 < vo= vo - vprice app\[iorder]set ii // для разнообразия используем didx

Как видите, изменений почти нет. Для полноты я приведу реальную программу на Q:

total:(+/) (,/) price[iorder]*v:0|vo + 0&order -\' vo:volume iordersold: (+/') vvolume[iorder]: vo @' ii:where each 0 < vo:vo - v@[`price;iorder;@;ii]

Другие суффиксы

map/fold/set и их вариации являются самыми важными и часто используемыми суффиксами, однако есть и другие, без которых не может обойтись ни один векторный язык. В первую очередь это итерационные суффиксы, аналоги циклов while/for:

// вызвать f[a1;..;an] N раз, если аргументов больше одного, то f должна возвращать списокf/[N]for[a1;..;an]// predicate - функция с тем же количеством аргументов, что и f. Вызывать f до тех пор пока predicate возвращает значение не равное 0f/[predicate]while[a1;..;an]

Простые примеры использования:

last {(y;x+y)}/[10]for[0;1]      // вычислить 12-е число Фибоначчиf/[0<count a]for a               // аналог "if condition then f[a] else a", используется в JprocessSomeA/[{count x}]while a  // processSomeA обрабатывает a пока там что-то есть

Еще один более экзотический суффикс - это over:

// f вызывается до тех пор, пока не вернет либо первоначальные аргументы, либо значение с предыдущего шагаf/over[a1;..;an]

Примеры:

rotate[1]\over 0 0 0 0 1       // получить матрицу с единицами на второй главной диагоналиsqrt={{0.5*y+x/y}[x]/over x/2} // вычисление квадратного корня sqrt(a) ~ x=0.5*x+a/x

Поскольку все итерационные суффиксы производят промежуточные результаты, то с ними можно использовать модификатор "\" (вернуть промежуточные результаты).

Крайне полезен также суффикс prior:

// вызвать f для всех пар значений в векторе v -> (f[v 0;val];f[v 1;v 0];f[v 2;v 1];...)f/[val]prior v 

Он используется в функциях типа:

differ = {not ~/prior x}  // вернуть для x массив, где 1 помечает элементы отличающиеся от предыдущегоdeltas = -/prior          // вернуть дельты соседних значений

Дальше я кратко перечислю более экзотические суффиксы. На самом деле их можно придумать достаточно много.

Инвертирование действия:

f/inv // вызвать функцию "обратную" к f

Аналог этого суффикса есть в J. Нужно, чтобы в языке была возможность определить функцию обратную к f. sqrt vs x^2; sin vs arcsin; zip vs unzip и т.п.

Поменять аргументы местами:

x f/swp y ~ y f x; f/swp x ~ x f x

Этот суффикс тоже из J, крайне полезен для устранения лишних скобок.

Суффикс memo:

f/[size]memo // запоминать результаты для ускорения вычислений в кеше определенного размера

Суффикс trap:

f/[value]trap // в случае исключения вернуть valuef/[fn]trap    // или вызвать exception handler

Асинхронность:

f/async // аналог async f(..) для вызова асинхронных функций

Также можно пофантазировать о пользе метасуффиксов, т.е. суффиксов, модифицирующих поведение других суффиксов. Пара таких уже имеется - "\" и "/". Полезны были бы в том числе метасуффиксы, изменяющие направление вычислений - reverse и flip. Они были бы полезны в паре с fold - делать все тоже самое, но с конца аргументов в начало, и делать все тоже самое, но поперек массива т.е. вдоль колонок, а не строк.

Еще один метасуффикс parallel. Его можно было бы применять в паре с map/right/left и т.п., чтобы распараллелить вычисления. В Q версии 4.0+ многие базовые операции снабжены этим суффиксом в неявном виде, что позволяет существенно ускорить обработку больших массивов.

Структурно полиморфные функции

Это функции, которые некоторым образом трансформируют структуру данных. В отличие от обычных языков, в векторных такие функции являются центральными, потому что операции, как правило, производятся с массивами, матрицами и другими сложными структурами данных. Выше мы уже применяли некоторые такие функции на практике (where), а индексирование само по себе является такой операцией. Дальше я опишу некоторые типичные функции подобного плана.

Функции til/take/drop/cut

Это центральные функции для любого векторного языка, они необходимы для создания и изменения формы массивов.

til N               // til 5 -> 0 1 2 3 4, создать последовательность чисел от 0 до N-1N take M            // 10 take 1; 10 take 0 1 2; -2 take til 5  создать массив из N элементов, элементы брать последовательно из источника M (при отрицательном N с конца)(n1;;nk) take M    // создать k-мерную матрицу (логичное обобщение)N drop M            // 2 drop 1 2 3; -2 drop 1 2 3  убрать N элементов из начала/конца M(n1;n2;..;nk) cut M // разрезать M на части по индексам n1nk

Некоторые примеры:

5 5 take 1 0 0 0 0 0            // матрица 5x5 с 1 на главной диагонали1 drop/map (where a==@) cut a // разрезать a на части, начинающиеся на @, убрать @

Разбить текст на абзацы:

txt 2*til (count txt=(where 0 =/prior 0<count/map txt) cut txt) div 2

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

Выделить из текста даты в формате DDDDXDDXDD, где X это одно из -/., привести X к стандартной форме /:

extract={{x =/set[;4 7] /} x where "0000-00-00"~/right (m dict "---",10 take "0") x=x (where 1<-2 -/prior w) cut w=where x in m="-/.0123456789"}// -/prior вычисляет дельты соседних элементов, т.е. 1<deltas idx вычисляет места, где индекс не возрастает на 1 - места разрыва// -2 в prior, чтобы первая разность была больше 1, а следовательно индекс 0 попал в результат// словарь (m dict "---",10 take "0") по сути является функцией, которая отобразит числа в 0, разделители в -, а остальные символы в символ по умолчанию (в Q это пробел)extract "On 2010.10.10 and 2020-12-13 at 10:00:00.0" -> ("2010/10/10";"2020/12/13")

Заметьте, как часто используется индексирование в том числе с помощью словаря. Этот прием уникален для векторных языков и позволяет реализовывать алгоритмы необычным образом.

Функции split/join

Эти функции, обычно, есть во всех языках. В векторных они сильно перегружены по первому аргументу и выполняют все возможные операции по слиянию и разбиению массивов. Некоторые из них:

\n split str; \n join str // самый очевидный способ использования10 split num; 10 join 1 2 3 4 // разобрать/собрать число в 10-ной системе счисления (или любой другой)24 60 60 1000 split 36938567  // для каждого разряда можно указать свой модуль (в данном случае разбираем на части время)

Перепишем разбиение на абзацы с помощью split (пусть текст нам задан одной строкой):

v where 0<count/map v=\n\n split txt

Сортировка

В векторном языке предпочтение отдается функции, которая не сортирует массив, а возвращает индексы для его сортировки iasc:

sort={x iasc x} // сама функция сортировки легко определяется через iasc

iasc намного полезнее просто sort. Например, с ее помощью можно легко реализовать сортировку по нескольким колонкам:

// сортируем 0..n-1 по каждой колонке с концаmsort={x app\left til[count x 0] {x iasc y}/fold reverse x} 

Отсортировать, согласно какой-нибудь функции:

x iasc x mod 10 // например, по последней цифре

Выражение iasc iasc x вернет нам для каждого элемента x его место в отсортированном массиве. Cоответственно, если мы применим этот индекс к уже отсортированному другому массиву y, то перемешаем его точно таким же образом, как x:

// фактически обратная функция к sort, если y=sort a, то a~a unsort y    unsort={y iasc iasc x} 

Или более обще мы можем перемешать любой массив y согласно массиву x. Например, если мы хотим сделать что-то с подгруппами массива, но не менять при этом порядок элементов (т.е. аналог update group by ):

a=11 2 6 15 6 18 19// разделим на две группы (a>10), посчитаем +\fold для каждойg=+\fold/map (where differ g i) cut a i=iasc g=a>10 // теперь, чтобы восстановить первоначальный порядок, достаточно второй раз применить iasc(,/fold g)iasc i -> 11 2 8 26 14 44 63

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

Группировка

Функция group возвращает словарь, где ключи уникальные элементы массива, а значения - их индексы в нем. Схематично ее можно определить через наши примитивы так:

group={u dict where x ==/right u=distinct x}group 0 1 2 1 0 1 -> 0 1 2 dict (0 4;1 3 5;list 2)

С помощью group выражение выше можно записать более просто:

// все fold map и т.д. при аргументе словаре работают со списком его значений// f/fold d ~ f/fold value d; f/map d ~ (key d) dict f/map value d(,/fold +\fold/map a g) iasc ,/fold g= group a>10 

Функция group почти эквивалентна group by в SQL, но она очень удобна и сама по себе. Например, пусть есть очередь задач (q) от разных пользователей (u). Мы хотим упорядочить ее так, чтобы все пользователи были равны:

q (,/fold g) iasc ,/fold til/map count/map g=group u

Сгруппируем по имени пользователя, каждой задаче назначим приоритет в виде числа (til + count), отсортируем все задачи по приоритету и индексами применим этот порядок к q.

Практические примеры

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

Сортировка на J

Часто приводится следующий пример сортировки на J, выглядящий как бессмысленный набор знаков:

qsort=: (($:@(<#[), (=#[), $:@(>#[)) ({~ ?@#)) ^: (1<#)

На самом деле точно такой же алгоритм часто приводят для демонстрации возможностей других языков (Haskell, например). Если переписать его буквально на наш язык, то получим:

qsort={{(x y filter y<x),(y filter y=v),x y filter y>v:y rand count y}[self]/[1<count x]for x}

Или более по-человечески:

qsort={if 1<c:count x then (self y filter y<v),(y filter y=v),self y filter y>v:y rand count c else x end}

Нельзя даже сказать, что он как-то сильно подчеркивает достоинства J, потому что на Haskell он выглядит практически также. На K эта функция выглядит так:

qsort:{$[1<#x;(f x<e),(x@&:x=e),(f:.z.s x@&:)x>e:*1?x;x]}

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

Игра Жизнь на APL

В примерах для APL приводится следующая программа для вычисления одного шага в игре жизнь:

life{1 .3 4=+/,1 0 1.1 0 1.}

Кратко напомню правила. Есть прямоугольное поле из живых и мертвых клеток (1 vs 0). На каждом шаге: a) в пустой клетке, у которой ровно 3 соседа, зарождается жизнь б) живые клетки, у которых меньше двух или больше 3 соседей, погибают от одиночества или перенаселенности. Соседи - это 8 клеток вокруг заданной клетки, плюс сама клетка.

Обратите внимание, что знаки APL хоть и необычны, но выглядят гораздо лучше ASCI мешанины в J. Увы, но изящность нотации APL нельзя повторить с помощью стандартного набора символов.

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

a (f . g) b ~ f/fold a g/map b

Для матриц:

a (f . g) b ~ a {f/fold x g/map y}/right/left flip b

Т.е. мы можем легко выразить его нашими примитивами. Итак, на нашем языке программа будет выглядеть так:

life = {(x min a==4) max 3==a=+/fold ,/fold: -1 0 1 rotate/left/right -1 0 1 rotate/right/left x}

Программа, почти один в один эквивалентна APL программе - поворачиваем матрицу 3 раза по строкам, затем 3 раза каждую из этих трех по столбцам, выравниваем массив, т.е. получаем список из 9 матриц, суммируем их, получая число соседей, включая статус самих клеток и потом проверяем правила.

Для сравнения тот же алгоритм на Q:

{(x&a=4)|3=a:sum raze -1 0 1 rotate\:/:-1 0 1 rotate/:\: x}
Подробнее..

Опционы пут-колл парити, броуновское движение. Ликбез для гика, ч. 7

10.09.2020 18:21:56 | Автор: admin
Это вторая часть рассказа про опционы, где мы разберемся с пут-колл парити, условием безарбитражности рынка, познакомимся с идеями хеджирования и репликации и поговорим про то, что такое броуновское движение и как оно связано с моделированием поведения курса финансового актива во времени.

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




Данный пост расшифровка моих видеолекций Пут-колл парити и условие отсутствия арбитража, Броуновское движение, созданных в рамках курса Finmath for Fintech.

Пут-колл парити. Пример использования условия отсутствия арбитража для анализа цены портфеля инструментов


Итак, из предыдущей части мы знаем, как выглядят выплаты для put и call опциона на expiry (момент времени, когда правом, предоставляемым опционом, можно воспользоваться), но мы также хотели бы знать, как рассчитывать опцион и на другие промежутки времени. Для этого нам необходимо построить математическую модель, используя более сложный математический аппарат. Однако перед тем, как мы это сделаем, давайте рассмотрим соотношение пут-колл парити, которое не требует сложных вычислений и в тоже время очень полезно на практике.

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

Базовым активом может быть акция или курс валют. Рыночный курс на базовый актив называется спот, и в формулах значение спота на момент времени $t$ обозначается как $S_t$.

Опцион, дающий право на покупку базового актива, называется колл-опционом (call option). Право на продажу это пут-опцион (put option). Цена, по которой опцион дает право заключить сделку в будущем, называется страйк (strike), обозначается $K$.

Заранее оговоренное в контракте время, в которое опционом можно будет воспользоваться, это время экспирации опциона (expiry) $T$. Значение курса базового актива на момент expiry обозначается $S_T$.

Построим графики выплат на expiry. У нас есть некий базовый актив его цена на expiry: $S_T$, а также выплата $P$, которую мы получаем. Графики выплат будут в этих координатах $S_T, P$. Зададим $K$ уровень страйк на оси $S_T$.

Первый опцион, который мы нарисуем, колл-опцион. Мы купили колл-опцион.



Это также называется long call option, позиция со знаком плюс по этому опциону. Но мы можем опционы еще и продавать, это называется short .

Второй опцион, который мы нарисуем, будет short put.



На графике мы видим, что, когда мы сложили две выплаты, мы получили простую линейную функцию, которая определяется как ($S_T-K$). Тот же самый результат можно получить аналитически. У нас есть позиция колл-опциона со знаком плюс и пут-опцион со знаком минус:

$C-P$

Воспользуемся аналитическими формулами, которые мы уже знаем:

$max(0, S_T-K)-max(0, K-S_T)$.

Чтобы раскрыть скобки, мы должны рассмотреть два отдельных случая, когда $S_T>K$ и $S_T<K$.

Имеем следующую систему:

$ \begin{cases} S_T-K-0, S_T>K \\ 0-K+S_T, S_TK \end{cases} $



В обоих случаях получается одна и та же простая формула: $S_T-K$.

Таким образом, выплаты в любом случае описываются одной и той же формулой, независимо от того, какая цена базового актива реализовалась на момент expiry. Опять-таки напоминаю, что те выплаты, которые мы нарисовали, это выплаты (значит, и стоимость) опционов на момент expiry. В случае цен опционов на какой-то другой момент времени, они описываются какими-то другими, более сложными функциями. Я их пока нарисую условно.



Мы знаем, что для этой комбинации на момент expiry выплата определяется формулой $S_T-K$, для любого значения $S_T$. Если мы найдем какую-то другую комбинацию инструментов, которая будет давать на момент expiry такую же выплату, то можно утверждать, что стоимость такой комбинации инструментов и комбинации $C-P$ должна быть одинаковой.

Если бы это было не так, то можно сегодня купить более дешевую из этих комбинаций инструментов и продать более дорогую, получив тем самым прибыль. А так как эти две комбинации дают одинаковую выплату на expiry, и мы их взяли с обратными знаками, то суммарная выплата гарантированно будет равна нулю. Такая сделка, которая дает гарантированный доход без риска, просто за счет несбалансированности цен инструментов на рынке, называется арбитражем. Математические теории для расчета цен инструментов обычно включают в себя предположение о безарбитражности рынка. Это предположение достаточно хорошо соответствует действительности. Арбитражные возможности на рынке если и возникают, то живут очень недолго. Найти и воспользоваться ими непросто. Так что в норме это предположение работает хорошо.

Из условия безарбитражности рынка следует то, что комбинация $C-P$ будет в любой момент времени $tT$ (а не только $T$) стоить столько же, сколько и любая комбинация инструментов, выплата по которой в момент времени $T$ будет равна $S_T-K$. Такую комбинацию легко составить, купив базовый актив $S$ и взяв в долг денег в таком количестве, что на момент expiry нужно будет вернуть сумму, равную $K$. При работе с финансовыми инструментами такой долг эквивалентен продаже бескупонной облигации (бонда), который дает выплату $K$ в момент времени $T$. Подробнее про облигации и проценты можно прочитать в предыдущих постах из этой серии (Стоимость денег, типы процентов, дисконтирование и форвардные ставки. Ликбез для гика, ч. 1 и Облигации: купонные и бескупонные, расчет доходности. Ликбез для гика, ч. 2).

Итак, портфель из колл-опциона и портфель из пут-опциона равен комбинации long по базовому активу и short бонду, который бы давал выплату один на expiry с номиналом $K$.

$C_t-P_t=S_t-B_TK$

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

Итак, мы получили соотношение для портфеля, составленного из колл- и пут-опционов. Мы составили портфель, рассмотрели, какая у него будет выплата на момент expiry, выяснили, что выплата описывается одним линейным уравнением. В отличие от функции выплаты для колл- и пут-опционов, в каждой из которых есть два участка, больше и меньше $S_T$. Это позволяет составить портфель из более простых инструментов, который даст такую же выплату на expiry в любой ситуации. Цена этих двух портфелей будет равна в любой момент времени, а не только в момент expiry. Это гарантируется нам условием отсутствия арбитража на рынке. Если же на рынке есть арбитраж и это равенство не выполняется, то мы, соответственно, можем купить один из этих портфелей, продать другой и получить гарантированный выигрыш. Это соотношение не зависит от каких-то математических моделей, которые мы могли бы построить, например, для цены базового актива. Это соотношение обязано выполняться в любой модели.

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

$C_t-P_t-S_t=-B_tK$

Такой способ избавления от риска называется хеджированием. Мы составляем портфель из нескольких инструментов, в которых заложен какой-то одинаковый риск, но подбираем их в таких соотношениях, что эти риски взаимно уравновешивает друг друга и мы от него избавляемся. Такая идея используется и в других, более сложных стратегиях хеджирования. Рассматриваемый случай очень простой, он позволяет работать только с определенной комбинацией опционов.

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

В данном случае у нас очень простая ситуация, и для того, чтобы полностью хеджировать риск или реплицировать выплату, нам достаточно один раз составить портфель, и потом мы уже ждем до момента expiry, выплата нам уже гарантирована. Это называется статическая репликация (статическое хеджирование). Это редкий случай, и обычно так не получается сделать. Для того чтобы добиться такого эффекта в более общем виде, нужно будет прибегать к динамическим стратегиям хеджирования. То есть мы составим один раз портфель, но потом нам постоянно надо будет что-то в него добавлять или что-то там изменять, чтобы выплата на момент expiry получилась именно такой, как мы захотим.

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

Нам бы хотелось построить такую модель, которая бы давала не только соотношение между колл- и пут-опционами, но и цену опциона как функцию от наблюдаемых на рынке величин. Это потребует более сложной математической теории.

Что такое броуновское движение и кто такой Роберт Браун. Как моделировать броуновское движение на компьютере. Что такое геометрическое броуновское движение


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

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

Думаю, что у многих термин броуновское движение ассоциируется со школьной программой физики. Многие считают, что человек, который ввел это понятие в научный оборот, был физиком по фамилии Броун и, судя по фамилии, являлся англичанином. Интересно, что все эти предположения неверны. Во-первых, звали этого ученого Robert Brown, что по-русски следует читать как Роберт Браун. Хотя это могло быть неочевидно для образованного человека XVIIIXIX веков, у которого первый иностранный язык был французский, а второй немецкий. Во-вторых, он не был англичанином он был шотландцем, что, как мы понимаем, совсем не одно и то же. Ну а самое интересное, он не был физиком он был ботаником. Когда он провел и описал свой знаменитый эксперимент, он занимался изучением частиц пыльцы под микроскопом. Препарат на предметном стекле был подготовлен в виде капли жидкости, в которой помещались частицы пыльцы для того, чтобы пыльца не улетала от каждого сквозняка и ее можно было спокойно рассматривать.

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



Если мы изобразим ее движение, это будет некоторая случайная траектория.



Какое это имеет отношение к нашей предметной области? На самом деле аналогия прямая. Мы рассматриваем курс финансового актива во времени. На него, как и на ту частицу, в каждый момент времени действует очень много случайных факторов. Мы их не видим, как не видел в микроскоп отдельные молекулы Роберт Браун.

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



Формальная математическая модель процесса, который мы будем использовать, связана с именем другого ученого американского математика Норберта Винера. Она выглядит следующим образом. Мы рассматриваем функцию непрерывного времени. Поскольку $t$ непрерывно, то и функция $W(t)$ непрерывная.

В нее заложена случайная составляющая, которая математически определяется следующим образом:

$\triangle W_{\triangle t}$ независимы при условии, что приращения по времени не пересекаются.

Приращение функции от момента времени $t$ до момента времени $t+s$ распределено нормально с параметрами 0 и $s$ (длина временного промежутка).

$W_{t+s}-W_tN(0,s)$

В дальнейшем мы увидим, что очень важно уметь генерировать такие пути на компьютере это необходимо для многих вычислительных методов. Как бы мы могли это сделать? Время, которое в теоретической математической модели непрерывно, мы разбиваем на компьютере на какие-то приращения, обычно с фиксированным шагом. Создаем некую начальную точку, из которой стартует наш процесс, с координатами $W_0;t_0$. Далее для каждого последующего шага по времени генерируем случайную величину с таким распределением, сдвигаемся на шаг. Так делаем в каждой точке. Получилась ломаная линия.



Где-то приращение получилось со знаком плюс, где-то со знаком минус. В результате в каждой конкретной точке значение всего процесса определяется кумулятивной суммой всех этих случайных величин. Для того чтобы иметь возможность масштабировать среднее смещение за единицу времени, мы можем ввести также дополнительный параметр, обычно обозначаемый буквой $\sigma$ (как и для нормального распределения). Мы можем рассматривать функцию $\sigma W_t$, где $W_t$ стандартное броуновское движение, а $W_t$ имеет дисперсию шире или уже, в зависимости от того, что нам нужно.

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

Если бы мы решали задачу для начисления процентов на некоторую сумму $P$ в непрерывном времени, то для небольшого шага по времени у нас было бы верно соотношение $\triangle P=rP\triangle t$ или

$\frac{\triangle P}{P}=r\triangle t$,

где $r$ это риск-нейтральная процентная ставка. И, перейдя к пределу $t d_t$, получим дифференциальное уравнение

$\frac{dP}{P}=rdt$.

Из него получаем уже знакомую нам формулу для дисконтирования в непрерывном времени $P=P_0e^{rt}$, где $P_0$ начальное значение.

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

$inline$\frac{S}{S}=t+W$inline$

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

$\frac{dS}{S}=dt+dW$

Но есть техническая проблема. Дело в том, броуновское движение (винеровский процесс), как мы его определили, является непрерывной функцией времени, но она не является дифференцируемой в смысле классического матанализа. Это можно формально доказать (доказательство опустим).

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

Я напишу для справки те результаты, которые нам понадобятся относительно этого математического аппарата. Дифференциал Ито подчиняется таким правилам.

Если

$dx=a(x,t)dt+b(x,t)dW_t$,

то для $f(x,t)$:

$inline$df=\frac{f}{t}dt+\frac{f}{x}dx+\frac{1}{2}\frac{^2f}{x^2}dx^2$inline$.

Это правило отличается от того, как мы дифференцируем функцию двух переменных в обычном матанализе. Если у нас есть две независимые переменные, в обычном матанализе мы берем частные производные и останавливаемся на первых двух членах разложения. Третий компонент разложения дифференциала функции в формуле Ито появляется именно благодаря тому, что мы работаем не с обычными функциями, а со случайным, стохастическим процессом. Этот результат мы берем готовым, не доказывая.

Нужно еще кое-что сказать о $dx^2$ в последнем уравнении. По условию $dx=a(x,t)dt+b(x,t)dW_t$, если мы возведем это в квадрат, то возникнут слагаемые с множителями $dt dW_t$, $dt^2$, $dW^2_t$. Для применения формулы Ито нужно принять:

$dt dW_t=0$; $dt^2=0$; $dW^2_t=dt$.

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

И теперь мы можем преодолеть нашу техническую сложность, так как мы знаем, как оперировать с объектом $dW$.

В качестве переменной $x$ у нас выступает курс базового актива $S$, мы можем его выразить:

$dS=Sdt+SdW_t$.

Далее мы знаем, как записать дифференциал функции, где есть $S$ и $t$. Давайте посмотрим, чему равен дифференциал от функции $f(S, t)=ln(S)$.

$inline$dln S = df = \frac{f}{t}dt+\frac{f}{S}dS+\frac{1}{2}\frac{d^2f}{dS^2}dS^2=$inline$

$=0 dt+\frac{1}{S}(Sdt+SdW_t)-\frac{1}{2}\frac{1}{S^2}\sigma^2 S^2 dt$

Теперь, собрав слагаемые, мы получим выражение для логарифма $S$.

$dS=(-\frac{\sigma^2}{2})dt+dW$

Теперь мы знаем, чему равен $S$ (заметим, он имеет нормальное распределение). Нас интересует непосредственно выражение для $S$.

$S=S_0 e^{t}exp(W_t-\frac{\sigma^2}{2}t)$

Записанное выше выражение описывает геометрическое броуновское движение. Оно представляет собой некоторый экспоненциальный рост с параметром $\mu$, который изначально начинается в точке $S_0$, и вокруг этой экспоненты накладывается шум согласно выражению $exp(\sigma W_t-\frac{\sigma^2}{2}t)$. Это уже можно считать на компьютере, мы можем генерировать пути броуновского движения. Мы получим некоторые возможные реализации нашего пути для курса базового актива. В этом уравнении есть два параметра: $\sigma$ дисперсия и $\mu$ дрифт. Они соответствуют дисперсии нормального распределения и смещения нормального распределения для $S$. Как я уже сказал, теперь можно выполнять моделирование на компьютере, однако есть еще один компонент теории, который нам необходимо ввести, чтобы при помощи этого процесса мы могли просчитать цену опционов. Дальше мы поговорим про риск-нейтральную меру.



Все статьи этой серии

Стоимость денег, типы процентов, дисконтирование и форвардные ставки. Ликбез для гика, ч. 1
Облигации: купонные и бескупонные, расчет доходности. Ликбез для гика, ч. 2
Облигации: оценка рисков и примеры использования. Ликбез для гика, ч. 3
Как банки берут друг у друга в долг. Плавающие ставки, процентные свопы. Ликбез для гика, ч. 4
Построение кривой дисконтирования. Ликбез для гика, ч. 5
Что такое опционы и кому это нужно. Ликбез для гика, ч. 6
Подробнее..

Индексное инвестирование, часть 1. Рациональные инвесторы. Риск и доходность. Диверсификация. Портфельная оптимизация

03.12.2020 16:09:25 | Автор: admin
Эдвард Мэтью Ворд. Пузырь Компании Южных морей. 1847 г. Галерея Тейт, Лондон.

Часть 1. Рациональные инвесторы. Риск и доходность. Диверсификация. Портфельная оптимизация
Часть 2. Модель CAPM. Систематический и идиосинкратический риск. Рыночная премия за риск
Часть 3. Анализ доходности фондов. Факторные модели. Арбитражная теория ценообразования
Часть 4. Биржевые фонды. Эффективность рынка. Личный опыт и сбережения на пенсию

В какие ценные бумаги вкладывать деньги? Как накопить на пенсию? Кто такие ETFы и почему все с ними носятся? Зачем покупать акции, если рынок может упасть? Такие вопросы я слышу от студентов и коллег, когда читаю лекции о деривативах. В принципе, неудивительно. Деривативы это что-то далёкое из мира больших банков, а личные инвестиции намного ближе к телу.

Можно было бы ответить коротко: Покупайте индексные фонды, это хорошо! К сожалению, такой ответ не объясняет, почему это хорошо. Если бы я услышал его 15 лет назад, когда ещё не интересовался финансами, то он не нашёл бы отклика в моём сердце. Пришлось прослушать не один курс лекций, чтобы осознать, какая экономическая теория стоит за этим советом, и начать применять его на практике.

Собственно, моя статья не столько инвестиционный совет (хотя я и расскажу о личном опыте и даже посчитаю свою альфу), сколько обзорный курс по теории инвестиций. Полезно знать, какие модели придумали предыдущие поколения, и в каких терминах можно думать об инвестициях. Если из теории следует, что имеет смысл покупать индексные ETFы, чтобы копить на пенсию так и быть, расскажу и об этом.

Не секрет, что в финансах много математики. Я постарался соблюсти баланс. Я считаю, что интуитивное понимание главных экономических идей важнее, чем конкретная формула. Даже если вы пропустите вообще все формулы, то вы всё равно поймёте суть и получите полезные знания. С другой стороны, если вы хотели бы размять мозги не ахти какой сложной математикой, то у вас будет такая возможность.

Рациональные инвесторы и избегание риска


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

Кому-то может показаться, что избегание риска (risk aversion) это нерациональное поведение слабых духом homo sapiens. На деле же рациональный до мозга костей homo economicus тоже будет избегать ненужного риска, если мы сделаем несколько предположений о том, как он принимает решения [BKM14, ch. 6.1].

Предположим, что рациональный инвестор максимизирует функцию полезности (utility function). Это означает, что все-все-все альтернативы, которые он рассматривает, подаются на вход некоторой функции u(x), которая присваивает каждой альтернативе число полезность (utility). Из множества доступных альтернатив рациональный индивид всегда выбирает ту, которая даёт наибольшую ожидаемую полезность.

Допустим, что функция полезности рационального инвестора десятичный логарифм количества долларов на счету. Каждый новый доллар на счету увеличивает полезность (уровень счастья), потому что логарифм возрастающая функция. Кроме того, каждый следующий доллар приносит меньше счастья, чем предыдущий, потому что логарифм выпуклая вверх (concave) функция. Никакие другие параметры помимо суммы на счету нашего инвестора не интересуют.

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

Итак, рассмотрим инвестора с логарифмической полезностью. Сейчас у него на счету $100000, которые дают полезность lg 100000 = 5.0 условных единиц счастья.

Посмотрите на таблицу 1.1. Инвестор должен вложить всё своё состояние в один из двух инструментов: либо в абсолютно надёжные облигации, либо в рискованные акции. Что бы ни произошло в будущем, облигации совершенно точно вырастут на $5000, и инвестор через год будет иметь $105000. Акции либо с вероятностью 50% вырастут на $25000 и будут стоить $125000, либо с вероятностью 50% упадут на $15000 и будут стоить $85000. Математическое ожидание вложения в акции равно 0.5 $85000 + 0.5 $125000 = $105000, то есть совпадает с тем, что обещают безрисковые облигации.

Таблица 1.1: Капитал и полезность инвестора в случае инвестиций в облигации или в акции.

Давайте теперь посчитаем полезность. В результате вложения в облигации инвестор получит полезность lg 105000 = 5.021 условных единиц счастья. Если он вложится в акции, то с вероятностью 50% акции вырастут, и полезность составит lg 125000 = 5.097. Однако с вероятностью 50% акции упадут, и полезность будет равна lg 85000 = 4.929. Средняя ожидаемая полезность от инвестиции в акции, таким образом, равна 0.5 5.097 + 0.5 4.929 = 5.013.

Из-за формы функции полезности радость от добавочных $20000 по сравнению с облигациями в хорошем сценарии (5.097 5.021 = 0.076) по модулю меньше, чем расстройство от упущенных $20000 в плохом сценарии (4.929 5.021 = 0.092). Потерянные с вероятностью 50% $20000 ценнее, чем заработанные с вероятностью 50% $20000.

Так какую же из двух альтернатив выберет наш рациональный инвестор: облигации с ожидаемой полезностью 5.021 или акции с ожидаемой полезностью 5.013? Ответ очевиден: 5.021 больше, чем 5.013, поэтому инвестор выберет облигации. При одинаковой ожидаемой доходности (в обоих случаях ожидаемый капитал составляет $105000) рациональный инвестор выберет менее рискованную альтернативу, то есть проявит то же самое избегание риска, что и реальные биологические инвесторы.

Как изменить условие задачи, чтобы инвестор хотя бы воспринимал две альтернативы безразлично? Можно, например, пообещать ему более высокую доходность акций в хорошем случае. Если акции будут приносить не $125000, а $129706, то, как показано в таблице 1.2, ожидаемые полезности двух альтернатив совпадут.

Таблица 1.2: Капитал и полезность инвестора в случае инвестиций в облигации или в акции. Акции имеют более высокую доходность по сравнению с таблицей 1.1.

Чтобы уравнять ожидаемые полезности, нам пришлось улучшить математическое ожидание дохода от акций. Раньше акции давали в среднем $105000, а теперь $107353, на $2353 больше. Эти $2353 дополнительной ожидаемой доходности премия за риск (risk premium), которую требует инвестор, чтобы рассмотреть возможность покупки акций. Премия за риск уравнивает прибавку полезности в хорошем случае (5.113 5.021 = 0.092) и снижение полезности в плохом случае (5.021 4.929 = 0.092). Если накинуть к доходности акций ещё доллар, то инвестор предпочтёт их облигациям.

Эти рассуждения верны не только для инвесторов с логарифмической полезностью. Достаточно, чтобы функция полезности была возрастающей и выпуклой вверх. Тогда инвесторы будут избегать риска и требовать премию (добавочную доходность) от рискованных инвестиций. Запомним эту мысль. Она пригодится, когда мы будем говорить о теории CAPM.

Корреляция с рынком


Рассмотрим ещё один модельный пример, основанный на идее из лекции профессора Джона Кохрэйна (John Cochrane) [Coc13].

Есть две акции, A и B, каждая из которых может принести в будущем либо $1000, либо $500 с вероятностью 50/50. Акции устроены так, что когда акция A приносит $1000, акция B приносит $500. И наоборот, когда A приносит $500, B приносит $1000. Математическое ожидание дохода от каждой акции равно $750. При прочих равных, какой акцией вы хотели бы владеть? Забудем о цене и предположим, что акцию вы получите в подарок.

На первый взгляд, акции совершенно симметричны. Нет никаких рациональных аргументов, чтобы предпочесть одну акцию другой. Вы могли бы подбросить монетку, положиться на случай и не прогадать. Верно? Не совсем. Что, если я уточню, в каких именно сценариях акция A приносит $1000, а в каких $500?

Предположим, что в будущем возможны два сценария. С вероятностью 50% вы потеряете работу или другой источник дохода, и в этом же сценарии акция A будет стоить $1000, а акция B будет стоить $500. С вероятностью 50% вы не только не потеряете работу, а даже получите премию $10000, и в этом же сценарии акция A будет стоить $500, а акция B будет стоить $1000. Эти альтернативы перечислены в таблице 1.3.

Таблица 1.3: Две акции дают одинаковый ожидаемый доход, но приносят большую пользу в разных состояниях мира.

Когда на лекции я провожу голосование среди студентов (живых людей, а не рациональных роботов), все в один голос заявляют, что предпочли бы владеть акцией A. Это соответствует простой житейской мудрости. Акция A принесёт дополнительные деньги именно в плохом сценарии, когда каждый доллар на счету. Акция A похожа на страховку от потери работы, и поэтому люди её ценят.

Примечательно, что рациональные логарифмические инвесторы из нашей теории будут вести себя точно так же. Поскольку функция полезности выпукла вверх, они будут больше ценить акцию A. Акция A приносит больший доход в плохом сценарии, когда каждый дополнительный доллар более ценен. С другой стороны, акция B приносит $1000 в хорошем сценарии, когда у инвестора и так прибавится $10000, и добавочная полезность от $1000 будет не столь велика.

Сделаем следующий шаг. Предположим, что в нашей экономике не один рациональный инвестор, а множество. Каждый из них предпочтёт ту акцию, которая защитит его от потери работы. Что, если риск потери работы одним инвестором связан (скоррелирован) с потерей работы остальными? Это вполне разумное предположение. Согласитесь, что для большинства людей шансы потерять работу в кризис выше, чем в хорошие времена. Кризис потому и кризис, что плохо становится сразу многим компаниям, и многие люди теряют работу одновременно.

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

Что это означает для доходности инвестиций в акцию A и акцию B? Для начала давайте договоримся о формальном определении, что такое доходность. Допустим, что вы купили актив (акцию, облигацию, квартиру) в момент времени t по цене Pt, а в момент времени t+1 актив стал стоить Pt+1. Кроме того, вы получили от актива денежную выплату (дивиденды, купон, арендную плату) Dt+1. Тогда ваша полная доходность за период времени между t и t+1 составила

$ R_{t+1} = \dfrac{P_{t+1} + D_{t+1}}{P_t} - 1 \quad (1.1) $

Например, предположим, что инвестор купил акцию B за Pt = $600. Реализовался хороший сценарий, и акция стала стоить Pt+1 = $1000 и не заплатила никаких дивидендов (Dt+1 = $0). Тогда инвестор заработал $1000 $600 1 66.7%.

Если считать, что будущая цена Pt+1 и будущие дивиденды Dt+1 случайные величины, то будущая доходность Rt+1 тоже случайная величина. Поэтому формулу (1.1) можно записать и для математических ожиданий:

$ \mathbb{E}(R_{t+1}) = \dfrac{\mathbb{E}(P_{t+1}) + \mathbb{E}(D_{t+1})}{P_t} - 1 \quad (1.2) $

Выглядит как урок арифметики для старшей группы детского садика, но он показывает нам важную деталь. Текущая цена Pt стоит в формулах (1.1) и (1.2) в знаменателе, поэтому при той же будущей цене и будущих дивидендах более низкая цена сегодня означает большую ожидаемую доходность в будущем.

Как мы выяснили, инвесторы будут предпочитать акцию A акции B. Если инвесторы не получают акции в подарок, а покупают их на рынке, то спрос на акцию A окажется выше, чем на акцию B. Следовательно, в равновесии цена акции B должна быть ниже, а ожидаемая доходность выше! Например, если рынок оценит акцию A в $700, а акцию B в $650, то по формуле (1.2) ожидаемая доходность акции A составит $750 $700 1 7.1%, а акции B $750 $650 1 15.3%.

Таким образом, инвесторы будут зарабатывать более высокую доходность (большую премию за риск) на активах, похожих на акцию B. Это те активы, которые сильнее связаны с общим состоянием экономики, то есть растут в хорошие времена и падают в плохие. Поставим крестик, чтобы вернуться к этой идее позже, когда будем изучать CAPM.

Вспоминаем теорию вероятностей


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

Я предполагаю, что все более-менее интуитивно понимают, что такое математическое ожидание случайной величины. Для наших целей совершенно нет необходимости знать, что это интеграл Лебега. Достаточно простой интуиции, что мат. ожидание это среднее значение случайной величины. Я буду обозначать мат. ожидание случайной величины X как E(x).

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

$ \mathbb{E}(X) = \dfrac{1}{6}\cdot 1 + \dfrac{1}{6}\cdot 2 + ... + \dfrac{1}{6}\cdot 6 = 3.5 $

Важное свойство мат. ожидания линейность. Например, если я бросаю не один кубик, а четыре, и складываю выпавшие очки, то мат. ожидание суммы будет равно 4 3.5 = 14. Формально это можно записать так ( и константы, X и Y случайные величины):

$ \mathbb{E}(\alpha X + \beta Y) = \alpha\mathbb{E}(X) + \beta\mathbb{E}(Y) $

Дисперсия случайной величины показывает, насколько велик разброс значений вокруг среднего. Чем больше разброс (например, чем дальше друг от друга минимум и максимум), тем больше дисперсия. Стандартное отклонение это квадратный корень из дисперсии. Я буду обозначать дисперсию случайной величины X как Var(X), а её стандартное отклонение как X.

$ Var(X) = \sigma_X^2 = \mathbb{E}\left[(X - \mathbb{E}(X))^2 \right] $

В примере с игральным кубиком дисперсия будет равна

$ Var(X) = \dfrac{(1 - 3.5)^2 + (2 - 3.5)^2 + ... + (6 - 3.5)^2}{6} \approx 2.92 $

Предположим, что у нас есть две случайные величины X и Y. Резонно задать вопрос: а есть ли связь между X и Y? Например, верно ли, что большие значения X чаще выпадают одновременно с большими значениями Y? Ответ на этот вопрос дают ковариация, которую я буду обозначать Cov(X,Y), и коэффициент корреляции, который я обозначу X,Y.

$ Cov(X,Y) = \rho_{X,Y}\sigma_X\sigma_Y = \mathbb{E}\left[(X - \mathbb{E}(X))(Y - \mathbb{E}(Y)) \right] $

Чтобы визуализировать идею корреляции, я четыре раза попросил компьютер сгенерировать по 250 случайных реализаций стандартных нормальных величин X и Y. Четыре эксперимента отличаются только корреляциями между X и Y. Результаты представлены на рисунке 1.1. Как видите, чем ближе корреляция к 1, тем очевиднее линейная связь между X и Y.

Рис. 1.1: Реализации случайных величин X и Y в зависимости от корреляции между ними.

Нам понадобится правило для вычисления дисперсии суммы случайных величин. Дисперсия суммы зависит как от дисперсии слагаемых, так и от ковариации (или от корреляции) между ними:

$ \begin{align*} Var(\alpha X + \beta Y) &= \alpha^2 Var(X) + \beta^2 Var(Y) + 2 \alpha \beta Cov(X, Y) = \nonumber \\ &= \alpha^2\sigma_X^2 + \beta^2 \sigma_Y^2 + 2\alpha\beta\rho_{X,Y}\sigma_X\sigma_Y \quad \quad (1.3) \end{align*} $

Диверсификация, или о пользе корреляций


Если бы я мог дать вам всего один совет касательно инвестиций, то я бы сказал: Диверсифицируйтесь! Или, следуя народной мудрости, не кладите все яйца в одну корзину.

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

Рассмотрим пример. Пусть у нас есть всего две акции, X и Y. Мы знаем, что они имеют одинаковую ожидаемую доходность = 5% и одинаковое стандартное отклонение = 10%. Кроме того, они связаны друг с другом корреляцией X,Y = 0.4. Вы должны вложить долю w своего капитала в акции X, а долю (1 w) в акции Y.

Зависит ли ожидаемая доходность ваших инвестиций от выбора w? Нет, не зависит. Из линейности мат. ожидания следует, что при любом выборе w вы всегда получите одну и ту же ожидаемую доходность 5%:

$ \mathbb{E}\left[ wX + (1-w)Y\right] = w\mathbb{E}(X) + (1-w)\mathbb{E}(Y) = w\mu + (1-w)\mu = \mu = 5\% $

А что с риском? Из формулы (1.3) следует, что дисперсия и стандартное отклонение зависят не только от стандартного отклонения каждой акции, но и от корреляции между ними:

$ \begin{align*} Var(wX + (1-w)Y) &= w^2Var(X) + (1-w)^2Var(Y) + 2w(1-w)Cov(X,Y) = \\ &= w^2\sigma^2 + (1-w)^2\sigma^2 + 2w(1-w)\rho_{X,Y}\sigma^2 = \\ &= \sigma^2\left(w^2 + (1-w)^2 + 2w(1-w)\rho_{X,Y} \right) \end{align*} $

Невооружённым глазом видно, что дисперсия портфеля (то есть риск) есть квадратичная функция от w. Стало быть, при каком-то w она должна достигать минимума. Как показано на рисунке 1.2, этот минимум действительно достигается при w = 0.5, то есть если вы инвестируете половину капитала в акции X и половину в акции Y. Стандартное отклонение доходности вашего портфеля составит 8.37%. С другой стороны, если бы вы инвестировали все деньги только в акцию X (или наоборот, только в акцию Y), то вам бы пришлось смириться со стандартным отклонением целых 10%.

Рис. 1.2: Зависимость стандартного отклонения доходности портфеля от доли инвестиций в акцию X.

Вывод: вам не нужно искать активы с отрицательной корреляцией, чтобы воспользоваться плодами диверсификации! Вполне достаточно, чтобы корреляция была отлична от 1.0. Диверсификация может снизить риск ваших инвестиций при той же ожидаемой доходности или дать большую доходность при неизменном уровне риска.

Портфельная оптимизация


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

Допустим, что нам известны ожидаемые доходности четырёх классов активов: акций, облигаций, инвестиционных фондов недвижимости (real estate investment trust, REIT) и золота. Также мы знаем стандартные отклонения и корреляции между активами. Эти значения приведены в таблице 1.4.

Таблица 1.4: Средние годовые доходности, стандартные отклонения и корреляции между классами активов. 19942020. Данные: [EF20].

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

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

Разумно задать следующий вопрос: если вы хотите получить ожидаемую доходность, скажем 10%, то какой портфель (какая пропорция акций, облигаций, недвижимости и золота) обеспечат такую доходность? А если таких возможных портфелей несколько (что вполне возможно), то который из них будет наименее рискованным (дисперсия и стандартное отклонение будут меньше, чем у остальных портфелей с такой доходностью)?

Рисунок 1.3 отвечает на этот вопрос. Каждая точка на графике это гипотетический портфель, который характеризуется стандартным отклонением (ось x) и ожидаемой доходностью (ось y). Для каждого уровня желаемой доходности я рассчитал (как расскажу позже) оптимальный портфель, то есть портфель с наименьшим стандартным отклонением из всех портфелей с данной доходностью.

Рис. 1.3: Граница эффективности для портфелей, составленных из акций, облигаций, недвижимости и золота.

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

Например, совершенно нет смысла выбирать портфель C, состоящий на 30% из облигаций и на 70% из золота. Этот портфель имеет ожидаемую доходность 6.5% при стандартном отклонении 12%. Однако раз уж вы согласны принять на себя риск в 12%, то за этот риск вы можете получить более высокую доходность почти 10% в портфеле B (63.2% в акциях). С другой стороны, если вы готовы удовлетвориться ожидаемой доходностью 6.5%, то лучше выбрать менее рискованный портфель A (74.6% в облигациях), который имеет стандартное отклонение 4.4%.

Другими словами, вы всегда стремитесь выбрать портфель, который лежит выше (больше доходность) и левее (меньше риск). В какой-то момент вы упрётесь в границу эффективности и не сможете двигаться дальше не получится заработать 15% при стандартном отклонении 4%. Очутившись на границе эффективности, вы можете гулять по ней либо вправо-вверх (больше риск, больше доходность), либо влево-вниз (ниже риск, ниже доходность). То, на каком из оптимальных портфелей на границе эффективности остановитесь именно вы, зависит от вашей личной чувствительности к риску.

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

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

Рис. 1.4: Состав портфелей на границе эффективности.

В подтверждение тезиса о диверсификации, единственный оптимальный портфель, который состоит только из одного актива (акций) это портфель с максимальной доходностью и максимальным риском. Это ожидаемо, потому что в условии задачи именно акции имеют максимальную доходность 10.9%. Если хоть немного разбавить акции другим активом с меньшей доходностью, то общая доходность портфеля упадёт. Так что инвестор, который желает выжать максимально возможную доходность, будет вынужден составить портфель только из акций.

Квадратичное программирование


В предыдущем разделе я обещал рассказать, как именно я посчитал оптимальные портфели. В этом разделе я выполню обещание и объясню, как формально поставить задачу выбора оптимального портфеля на языке математики. Если вам не очень интересны математические подробности, то вы можете смело перейти к следующему разделу. Незнание этих выкладок не помешает вам продолжить читать статью и извлечь из неё пользу.

Если вы всё ещё со мной, то, как говорил известный сатирик, наберите воздуха в грудь.

Итак, у нас есть n активов. Будущая доходность i-го актива это случайная величина со средним i и стандартным отклонением i. Доходности i-го и j-го актива связаны корреляцией i,j.

Нас просят распределить единичный капитал между активами. Более формально, i-му активу нужно присвоить вес (долю в портфеле) xi. Потребуем, чтобы все веса xi были положительными (нельзя продать актив, которого у нас нет), а их сумма равнялась единице (мы должны распределить весь капитал без остатка).

Для начала введём несколько удобных матричных обозначений, то есть запишем параметры задачи в аккуратные прямоугольные таблицы.

$ x = \begin{bmatrix}x_1 \\ x_2 \\ \vdots \\ x_n\end{bmatrix} \qquad \mu = \begin{bmatrix}\mu_1 \\ \mu_2 \\ \vdots \\ \mu_n\end{bmatrix} \qquad S = \begin{bmatrix} \sigma_1^2 & \rho_{1,2}\sigma_1\sigma_2 & \cdots & \rho_{1,n}\sigma_1\sigma_n \\ \rho_{2,1}\sigma_2\sigma_1 & \sigma_2^2 & \cdots & \rho_{2,n}\sigma_2\sigma_n \\ \vdots & \vdots & \ddots & \vdots \\ \rho_{n,1}\sigma_n\sigma_1 & \rho_{n,2}\sigma_n\sigma_2 & \cdots & \sigma_n^2 \end{bmatrix} \qquad e = \begin{bmatrix}1 \\ 1 \\ \vdots \\ 1\end{bmatrix} $

Утверждается, что вопрос какой портфель с ожидаемой доходностью r имеет наименьшую дисперсию? можно формально записать в виде следующей задачи минимизации:

$ \begin{cases} x^TSx \to \min \\ \mu^Tx = r \\ e^Tx = 1 \\ x \ge 0 \end{cases} \quad (1.4) $

Вид формул (1.4) вызывает у разных людей противоположные эмоции. Например, человек, знакомый с теорией математической оптимизации, воскликнет: Батюшки, да это же банальный QP! Стоило ли ради этого писать столько текста?

Действительно, хорошая новость заключается в том, что человечество уже давно научилось решать задачи такого вида и даже дало им специальное название квадратичное программирование (quadratic programming, QP) [CT06, ch. 78]. Почти для любого языка программирования, от C до Питона, найдётся готовая библиотека для решения этой задачи. Достаточно ничего не напутать и правильно составить матрицы , S и e, а дальше библиотека сама найдёт оптимальное решение. Совершенно не обязательно разбираться в том, какой алгоритм поиска решения крутится под капотом.

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

Что такое xTSx? Это компактная запись дисперсии портфеля, составленного с весами xi. Для наглядности можно расписать это выражение для случая двух активов (n = 2) и перемножить матрицы как нас учили на первом курсе, строка на столбец. Получится уже знакомая нам формула (1.3), связывающая дисперсию суммы с ковариацией:

$ \begin{align*} x^TSx &= \begin{bmatrix}x_1 & x_2\end{bmatrix} \begin{bmatrix} \sigma_1^2 & \rho_{1,2}\sigma_1\sigma_2 \\ \rho_{1,2}\sigma_1\sigma_2 & \sigma_2^2 \end{bmatrix} \begin{bmatrix} x_1 \\ x_2 \end{bmatrix} = \begin{bmatrix}x_1 & x_2\end{bmatrix} \begin{bmatrix} x_1\sigma_1^2 + x_2\rho_{1,2}\sigma_1\sigma_2 \\ x_1\rho_{1,2}\sigma_1\sigma_2 + x_2\sigma_2^2 \end{bmatrix} = \\ &= x_1^2\sigma_1^2 + 2x_1x_2\rho_{1,2}\sigma_1\sigma_2 + x_2^2\sigma_2^2 \end{align*} $

Следовательно, если мы попросим алгоритм минимизировать значение xTSx, то он постарается найти портфель (набор весов xi) с минимальной дисперсией.

Если не задать алгоритму оптимизации никаких ограничений, то он довольно быстро скажет вам, что портфель с минимальной дисперсией это портфель с нулевыми весами. Действительно, если ничего не инвестировать, то и риска никакого не будет. Это не совсем то, что мы хотим, поэтому нам нужно добавить в задачу дополнительные ограничения (constraints).

Первое ограничение это Tx = r. По-русски, мы просим алгоритм рассматривать только те портфели, которые имеют ожидаемую доходность r. В самом деле, если расписать матричное умножение, то получится сумма 1x1 + + nxn, то есть ожидаемая доходность портфеля.

Второе ограничение eTx = 1 можно записать как x1 + + xn = 1. Мы говорим алгоритму, что корректное решение (набор весов xi) это когда весь единичный капитал распределён между активами и ни один рубль не остался неинвестированным.

Наконец, третье ограничение x 0 просто говорит, что все веса xi должны быть неотрицательными (можно только покупать активы в портфель, но нельзя продавать).

Всё, заканчиваю стращать вас математикой. В сухом остатке, мы всегда можем найти оптимальный портфель, то есть портфель с минимальной дисперсией, для заданной ожидаемой доходности r. Для этого нужно составить несколько матриц и скормить их алгоритму квадратичного программирования.

В качестве исторической справки отмечу, что идея сформулировать задачу выбора портфеля как задачу поиска баланса между дисперсией и ожидаемой доходностью принадлежит Гарри Марковицу (Harry Markowitz) [Mar52]. Поэтому иногда эту задачу называют оптимизацией по Марковицу. Ещё одно название, которое вы можете встретить современная портфельная теория (modern portfolio theory, MPT).

Отказ от ответственности


Мнение автора статьи может не совпадать с официальной позицией Deutsche Bank AG. Статья не является предложением или рекламой какой-либо услуги. Упоминание третьих сторон не предполагает одобрения или неодобрения. Автор и Deutsche Bank напоминают, что торговля на финансовых рынках сопряжена с риском, и не несут ответственности за возможные негативные последствия ваших личных инвестиционных решений.

Список литературы


[BKM14] Zvi Bodie, Alex Kane, and Alan J Marcus. Investments. 4th ed. McGraw-Hill Education, 2014. ISBN: 978-0-07-786167-4.
[Coc13] John H Cochrane. Consumption and Risk Premiums. University of Chicago. 2013.
[CT06] Gerard Cornuejols and Reha Ttnc. Optimization methods in finance. Cambridge University Press, 2006. ISBN: 9780511258183.
[EF20] Silicon Cloud Technologies LLC. Portfolio Visualizer Efficient Frontier. 2020.
[Mar52] Harry Markowitz. Portfolio Selection. In: The Journal of Finance 7.1 (1952), pp. 7791.
Подробнее..

Опционы расчет одношаговой биномиальной модели. Ликбез для гика, ч. 8

21.01.2021 16:23:47 | Автор: admin

Это третья часть рассказа про опционы, где мы поговорим про биномиальную модель, риск-нейтральную меру и разберёмся, как рассчитать цену опциона.

Если вы пропустили наш подробный рассказ про опционы, вот ссылки на предыдущие части:
Что такое опционы и кому это нужно. Ликбез для гика, ч. 6
Опционы: пут-колл парити, броуновское движение. Ликбез для гика, ч. 7

Данный пост основан на расшифровке моих видеолекций Одношаговая биномиальная модель и Расчет опциона, созданных в рамках курса Finmath for Fintech.

Одношаговая биномиальная модель. Можно ли считать цену опциона как математическое ожидание дисконтируемой выплаты? Что может пойти не так?

Следующее понятие, которое необходимо обсудить, риск-нейтральная мера. Это понятие в общем виде требует не-школьной математики, поэтому, как и ранее, мы не будем что-то доказывать в общем виде. Вместо этого начнем с очень простого примера (даже может показаться, что он слишком простой и вообще не имеет никакого отношения к реальным моделям для нашей темы). На этом примере мы увидим, как работают термины, а все результаты, которые мы получим, будут верны и в более общем случае.

Мы рассматриваем биномиальную модель с дискретным временем. У нас есть два момента времени, в которых наблюдается рынок: t0 и t1, есть некий рисковый актив, т.е. он содержит в себе риск. Его цена в момент t0 равна 50. И есть два варианта развития события в будущем (поэтому модель и называется биномиальной): цена может увеличиться до 100 или упасть до 25. Это наш рисковый актив, в котором есть некоторая неопределенность. Также нам в нашей модели нужен некоторый безрисковый актив, аналог банковского счета в надёжном банке. Предположим, у нас риск-нейтральная ставка 20% и, значит, деньги, положенные в момент времени t0 на депозит в количестве 50 в момент времени t1 дадут выплату 60.

Рис. 1Рис. 1

В нашей модели есть некоторые вероятности. Обозначим их: p вероятность того, что актив увеличится в цене, и вероятность того, что актив в цене уменьшится: 1-p.

Если бы мы работали с активом, в котором нет никакой неопределенности, то цена такого актива в момент времени t0 была бы просто дисконтированной выплатой в момент t1, как это есть для безрискового актива. Но у нас есть два варианта с некоторой неопределенностью. Логично выглядит предположение о том, что, используя эти вероятности, мы бы могли просчитать математическое ожидание от дисконтированной выплаты какого-то производного продукта. Если мы рассмотрим колл-опцион на такой риск базовый актив, то его цена, наверное, будет равна математическому ожиданию дисконтированной выплаты в момент времени t1:

Это наше предположение.

Тут D1 коэффициент дисконтирования из момента t1 в момент t0. Посчитаем, чему будет равно записанное выше выражение. Для определенности давайте скажем, что страйк этого опциона K равен 70. Тогда у нас есть все данные, чтобы посчитать мат. ожидание.

Проверим, работает ли наше предположение. Мы сделаем так: составим некоторый портфель из опциона, базового актива в момент t0 и рассмотрим его выплату в момент t1. Составим портфель следующим образом. Мы продаем 15 колл-опционов и покупаем 6 штук базового актива. Берем в долг 125 единиц денег. Такие вот магические цифры, чуть попозже мы увидим, откуда они берутся, как их можно посчитать, но пока это просто выбранные константы. В момент времени t0 изменение нашего баланса будет выглядеть следующим образом:

Рис. 2Рис. 2

Мы получаем прибыль за 15 опционов: +15С0; платим за шесть единиц базового актива по цене 50: -6*50; получаем: +125 денег.

Далее следует момент времени t1 и два возможных варианта: когда цена актива (S1) стала 25 и когда цена актива стала 100. В первом случае, когда цена стала 25 при страйке 70, опцион ничего не стоит. Когда цена стала 100, и мы продали 15 опционов, нам нужно заплатить премию -15*(100-70). Приобретенные шесть единиц базового актива у нас на балансе, мы можем их продать и получить деньги по текущему курсу 6*25 или 6*100 соответственно тому, какая цена реализовалась. Наш долг увеличивается согласно процентной ставке, и мы получаем выплату, в обоих случаях одинаковую, которая не зависит от цены актива: -150. Теперь сложим все числа, которые у нас получились на момент времени t1. Как видим, в обеих колонках получаем ноль. Это связано с тем, что изначально цифры были специально подобраны.

Рис. 3Рис. 3

В момент времени t1 независимо от того, какой сценарий реализуется, портфель, составленный таким образом, стоит ноль. Следовательно, в момент времени t0 он тоже должен стоить столько же ноль. Мы опять используем условие отсутствие арбитража на рынке.

Приравняв эту сумму к нулю, мы получаем цену опциона:

Мы видим, что результат 11,6 не совпадает с тем, что получили ранее: 12,5. Этот, казалось бы, интуитивно верный результат не сработал. Составив специальный портфель, мы увидели, что, если бы цена на колл-опцион была 12,5, это бы как раз и означало наличие арбитража на рынке. То есть при такой цене на колл-опцион можно было бы зарабатывать деньги без риска. Давайте разберемся, почему же так получилось.

Одношаговая биномиальная модель. Расчет опциона. Риск-нейтральные вероятности

Чтобы проанализировать полученный результат, давайте немного обобщим модель и будем работать уже не с фиксированными числами, а с какими-то параметрами. Обозначим текущий курс рискового актива как S, введем параметр d и параметр u. У нас к моменту времени цена t1 рискового актива может пойти либо вниз в d раз, либо вверх в u относительно текущего уровня. Цена безрискового актива по-прежнему определяется некоторым дискаунт-фактором.

Рис. 4Рис. 4

Заметим, что по построению параметры модели заданы так, что d<D1-1<u (см. рис. 4).

По поводу составления портфеля следует заметить, что мы сразу можем поделить весь портфель на номинал нашей позиции в опционах. Это, возможно, даст нам дробные номиналы в каких-то других позициях, но мы разрешаем себе с этим работать в рамках нашего математического анализа. Мы будем работать с любыми действительными числами как с положительными, так и с отрицательными. В рамках практики рынка отрицательные позиции соответствуют short selling, и могут быть определенные ограничения на такие позиции, но мы этим в нашем тоже анализе пренебрегаем.

Так как мы можем поделить все на номинал нашей позиции в опционах, то мы просто будем рассматривать один опцион. И для составления портфеля остается только два параметра: размеры позиций в рисковом и безрисковом активах.

Купим какое-то количество x базового актива S и возьмем в долг какое-то количество денег, такое, чтобы получить выплату ровно y. Мы его дисконтируем на момент t0 и получаем D1y. Мы берем в долг D1y, а выплатить в момент t1 нам нужно будет y. Т.е. для баланса это будет сумма -y.

В случае с базовым активом мы покупаем на сумму xS. У нас возможны два случая: когда цена пошла вниз S1=d*S и когда цена пошла вверх S1=u*S. Соответственно, наша позиция будет стоить x*d*S или x*u*S. И мы продаем один колл-опцион, в начальный момент времени мы за него получаем премию C0. В момент времени t1 мы обязаны сделать выплату по этому опциону, так как взяли на себя это обязательство: -Cd и -Cu соответственно.

Рис. 5Рис. 5

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

Эта система из двух уравнений с двумя неизвестными имеет решение. Вычтем одно уравнение из другого, чтобы сразу получить значение x.

Подставляя значение x, можно найти y.

По построению этих уравнений такие значения x и y дают нам в обоих возможных вариантах цену портфеля, равной нулю. Рассуждая точно так же, как и в прошлый раз, портфель на expiry в момент времени t1 стоит ровно ноль, независимо от того, какая цена базового актива реализуется на рынке. Следовательно, этот портфель должен стоить ноль и в момент t0, по условию отсутствия арбитража. Таким образом, мы получаем уравнение для цены колл-опциона в начальный момент времени. Запишем выражение для цены колл-опциона C0 в момент времени t0, используя наше решение.

Где q новый параметр, который выражается через известные нам коэффициенты:

По построению, так как мы задали параметры u, d и дискаунт-фактор D1-1 так, как показано на рис. 4, то значение q лежит в диапазоне от нуля до единицы. То есть в диапазонах, доступных для значений вероятности.

В слагаемом с Cu мы используем q, а для выплаты Cd при движении вниз мы используем (1-q). То есть формулу можно переписать как вычисление математического ожидания, но используя уже не вероятность p, которая соответствует реальной вероятности того, что произойдет на рынке, а некоторую синтетическую вероятность, которая определяется формулой для q. Иначе говоря, мы получили математическое ожидание дисконтированной выплаты, но используя уже некоторые другие вероятности. Это и является вычислением математического ожидания в риск-нейтральной мере.

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

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

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

Многошаговая биномиальная модель

Подход, описанный здесь для рассуждения про риск-нейтральные вероятности, стал популярен благодаря статье Cox, J. C.; Ross, S. A.; Rubinstein, M. (1979). Option pricing: A simplified approach. Такой подход помогает решить сразу несколько важных проблем.

Во-первых, после публикаций статей Блэка, Шоульца и Мертона в 1973 году появилось успешная, быстро и широко принятая теория прайсинга опционов. Эту теорию нужно было преподавать в университетах. В том числе MBA-студентам и студентам экономических факультетов. Но разделы математики, которые используются для рассуждений, в общем виде в стандартную программу не входят. Даже сегодня и даже для студентов технических специальностей. А преподавать как-то нужно. Ведь MBA-программа это не только красивый диплом для студентов, но и дополнительный заработок для преподавателей математики.

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

Если у нас есть фиксированный отрезок времени до expiry опциона, то можно попробовать построить математическую модель, в которой мы разделим этот отрезок на N отрезков, и сказать, что на каждом шаге возможно два исхода. Тогда мы получим решетку возможным исходов (см. рис.6). Тут, как и раньше, голубыми цветом нарисованы возможные значения рискового актива, малиновым цветом нарисован рост безрискового актива. Для рискового актива для каждого значения спота есть два варианта развития ситуации на следующем шаге по времени. Линии между голубыми точками показывают, как они связаны друг с другом.

Рис. 6Рис. 6

Далее можно математически проанализировать предельный случай при N . Что и было сделано авторами статьи. Если правильно выбрать параметры u и d, тов пределе решётка будет приближаться к логнормальному распределению. Т.е. в пределе получаем модель Блэка-Шоульца.

Математический анализ такого предельного случая рассматривать не будем, просто приведём некоторую визуализацию. На рис. 7 и 8 нарисованы решетки при больших значениях N. В отличии от рис. 6 нарисованы только точки, показывающие возможные значения, линий между ними нет, чтобы можно было разглядеть детали.

Рис. 7Рис. 7Рис. 8Рис. 8

Все статьи этой серии:

Стоимость денег, типы процентов, дисконтирование и форвардные ставки. Ликбез для гика, ч. 1
Облигации: купонные и бескупонные, расчет доходности. Ликбез для гика, ч. 2
Облигации: оценка рисков и примеры использования. Ликбез для гика, ч. 3
Как банки берут друг у друга в долг. Плавающие ставки, процентные свопы. Ликбез для гика, ч. 4
Построение кривой дисконтирования. Ликбез для гика, ч. 5
Что такое опционы и кому это нужно. Ликбез для гика, ч. 6
Опционы: пут-колл парити, броуновское движение. Ликбез для гика, ч. 7

Подробнее..

Уравнение теории ценообразования. Ликбез для гика, ч. 9

05.02.2021 16:09:35 | Автор: admin

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

В основу этой статьи легли четыре мои видеолекции из курса Finmath for Fintech, которые можно найти на YouTube: Основное уравнение теории ценообразования, Стохастический коэффициент дисконтирования, Связь цен и корреляции рисков и Избыточная доходность. Риски.

Основное уравнение теории ценообразования

Для начала рассмотрим некоторую игрушечную задачу. Начнем с воображаемого инвестора, который думает, сколько ему потратить сегодня, сколько ему вложить, чтобы получить какую-то прибыль завтра. У него есть горизонт планирования, состоящий из двух дней: сегодня t и завтра t+1. Предположим, поначалу наш инвестор был ленив и потреблял все, что он получал. Назовем это ситуацией потребителя П. Потребитель получает какую-то зарплату et (какой-то доход earnings) и всю ее потребляет сt (consumption). Аналогично выглядит ситуация завтра:

ct+1 = et + 1

Теперь, предположим, наш потребитель задумался о том, чтобы вложить часть своей зарплаты в какой-то актив (A), одна единица которого стоит pt и который завтра принесет инвестору выручку в размере xt+1. Нужно объяснить, почему мы обозначили цену сегодня и выручку завтра разными буквами. Дело в том, что цена сегодня это фиксированное число, она известна. В то время как xt+1 (та выручка, которую принесет актив завтра) это случайная величина, не гарантированная: может быть больше, может меньше. Это рисковый инструмент. Предположим, что наш инвестор решил стать настоящим инвестором (И) и купить N штук этого актива. Тогда его потребление сегодня уменьшается:

сt = et - Npt

Но завтра его потребление увеличится, потому что он получит

сt + 1 = et + 1 + Nxt+1

Возникает естественный вопрос: лучше ли поведение инвестора (И), чем поведение простого потребителя (П)? Уточним вопрос. Какое значение N наиболее оптимально?

Чтобы ответить на это, нужно понять, что именно означает оптимально. То есть мы должны выяснить, какую функцию мы здесь оптимизируем. Мы можем смоделировать поведение инвестора, введя некоторую функцию полезности. Функция полезности или функция удовольствия зависит от уровня потребления. Запишем это. Если сегодня уровень потребления сt, то сегодняшнее значение уровня полезности u(сt). Соответственно, для завтрашнего уровня потребления u(сt+1).

В нашей временной модели, состоящей из двух временных периодов сегодня и завтра, мы бы хотели, чтобы ответ на вопрос какое N является оптимальным? зависел от этих двух величин (u(сt) и u(сt+1)). Поэтому мы сейчас их объединим в одну общую функцию полезности. Наша общая функция полезности будет иметь следующий вид: потребление сегодня плюс бета, помноженная на математическое ожидание потребления завтра.

U = u(сt) + E[u(сt+1)]

Сегодняшнее потребление нам известно, потому что про сегодня у нас есть полная информация. Завтрашнее потребление зависит от того, какова будет величина сt+1. А она включает в себя выручку от того же актива, в который мы вложились. В нашу общую функцию полезности входит ожидаемое удовольствие от потребления завтра. Случайная величина сt+1 включает в себя доход от нашего актива, в который мы вложились, именно поэтому здесь стоит знак математического ожидания. Более того, мы дисконтируем это ожидаемое удовольствие на некоторый коэффициент , который можно назвать коэффициентом нетерпения (нам всегда хочется здесь и сейчас, чем в какой-то абстрактный момент времени завтра).

Про саму функцию u мы сделаем два вполне естественных предположения. Первое предположение: функция u(сt) является возрастающей, то есть чем больше мы потребляем, тем больше удовольствие. Второе предположение: функция U является вогнутой. Иными словами, чем больше мы потребляем, тем меньше получаем дополнительное удовольствие от каждого следующего употребленного кусочка.

График функции U может выглядеть следующим образом:

Типичным вариантом будет функция такого вида:

Итак, наш инвестор хочет максимизировать общую функцию полезности, найти оптимальный уровень вложений N:

Umax

Условием первого порядка для решения этой оптимизационной задачи является условие равенства нулю производной.

Распишем выражение:

Давайте теперь посмотрим на это уравнение, на условие первого порядка, и решим его относительно цены актива сегодня pt. Величины u'(ct) и не являются случайными, они определены. Поэтому мы их можем внести под знак математического ожидания. Получим следующую формулу:

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

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

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

Стохастический коэффициент дисконтирования

Итак, у нас есть выражение:

Обозначим весь коэффициент перед иксом как mt+1 и назовем его стохастический коэффициент дисконтирования. После этого наше уравнение установления цен примет более компактный вид:

Название стохастический коэффициент дисконтирования обобщает ситуацию с обычным коэффициентом дисконтирования. Давайте, например, рассмотрим ситуацию, где нет никакой неопределенности и где величина выручки завтра известна заранее. Это ситуация, когда риск отсутствует. В этом случае цена сегодня и выручка завтра связаны следующим образом:

Где Rf базовая процентная ставка (предполагаем, что базовая процентная ставка положительная, поэтому коэффициент дисконтирования 1/Rf меньше единицы и наша цена сегодня, естественно, чуть меньше, чем выручка, которую мы получим завтра).

Если убрать зависимость от индекса t, то можно записать уравнение установления цен в максимально компактном виде:

Если есть какие-то рисковые инструменты, тогда стоимость будет следующей:

Если рисковый инструмент имеет такую же ожидаемую доходность, что и безрисковый инструмент, разумно предположить, что, поскольку вложения в него связаны с некоторой неопределенностью, с определенным риском, его стоимость сегодня будет меньше. Полученное нами выше общее уравнение ценообразования можно рассматривать как обобщение этих ситуаций. Оно говорит, что все дополнительные поправки, связанные с риском, имеют своим источником один стохастический коэффициент дисконтирования, который является одним и тем же для всех рисковых и безрисковых инструментов. Для объекта m есть несколько разных названий: стохастический коэффициент дисконтирования, ядро ценообразования. Если математическое ожидание представить себе в виде интеграла, то тогда функция m выступает там в качестве ядра интегрирования. Все теории ценообразования так или иначе занимаются тем, что пытаются связать этот m с данными. То есть, если у вас есть какие-то данные, вы строите модель, как из них получить этот коэффициент m.

Мы сказали, что рассмотренная нами ситуация универсальна. На практике могут быть разные случаи. Давайте запишем некоторые примеры тех активов, о которых мы говорим. Мы говорили, что у актива есть цена сегодня и есть выручка завтра.

Простейший случай это вклад. У вас есть 1 рубль сегодня, и вы ожидаете какую-то доходность завтра Rt+1. Это может быть какая-то акция, у которой есть сегодняшняя цена pt. Завтрашняя цена может быть какая-то иная, случайная величина pt+1, плюс дополнительно владельцу этой акции могут быть выплачены какие-то дивиденды dt+1 (цена и дивиденды, конечно, связаны). Это может быть какой-то бонд, у которого, наоборот, купон фиксированный 1, и тогда речь идет о том, какая у него цена сегодня pt. Это может быть опцион, например колл-опцион. Вы покупаете его за некоторую сумму сегодня Ct, и он дает вам выручку, которая связана со страйк-ценой, с ценой актива по какой-то такой формуле из теории опционов: max(St - K, O). Во всех этих и многих других ситуациях (наш список можно продолжить) уравнение установления цен, полученное нами, применимо. С практической точки зрения вопрос заключается только в том, чтобы объяснить или предложить модель построения этого стохастического коэффициента дисконтирования m.

Связь цен и корреляции рисков

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

cov(m,x) = E[mx] - E[m]E[x]

Выражение E[mx] и присутствует в уравнении установления цен. Перепишем наше уравнение установления цен, выразив этот компонент через два остальных члена этого равенства:

pt = E[m]E[x] + cov(m,x)

Само математическое ожидание стохастического коэффициента дисконтирования E[m] можно представить себе следующим образом. Если у нас есть безрисковый вклад, то есть мы вложили единицу, то тогда мы должны получить:

Поскольку Rf не является случайной величиной, ее можно вынести за знак математического ожидания, и наше уравнение принимает вид:

Теперь заменим математическое ожидание стохастического коэффициента дисконтирования E[m] на величину, обратную базовой процентной ставке:

Итак, что же у нас получилось? Сегодняшняя цена актива имеет две составляющие. Первая составляющая зависит только от самого актива (это, можно сказать, приведенное, с помощью базовой процентной ставки, к сегодняшнему дню математическое ожидание выручки нашего актива). Вторая составляющая, которая добавляется, связывает наш актив и коэффициент дисконтирования. Назовем вторую часть формулы cov(m,x)платой за риск. Она является поправкой к приведенной текущей стоимости, к первой части формулы. Давайте подумаем, какой может быть поправка. Она может быть как отрицательной, так и положительной. Это зависит от того, как наш актив скоррелирован со стохастическим коэффициентом дисконтирования. Давайте вспомним формулу для m и напишем это выражение чуть более подробно:

Величина u't(ct) не является случайной.

Чем больше потребление, тем меньше становится производная функции u, то есть с увеличением потребления u't+1уменьшается. Если же наш актив ведет себя следующим образом: когда потребление увеличивается, наш актив тоже увеличивается (то есть это актив, который хорошо работает в ситуации, когда экономика и благосостояние растут), то тогда такой актив имеет отрицательную ковариацию cov(m,x). Наш актив становится дешевле. Когда наш актив является чем-то вроде страховки и, наоборот, хорошо работает, когда потребление падает, в тех ситуациях он приносит нам доход. А когда употребление растет, он не так хорош, его поведение, поведение этой случайной величины и поведение производной функции u' одинаковы эта величина положительная, и мы платим дополнительную величину.

Функция u вогнутая, чем больше растет потребление (consumption), тем производная этой функции меньше. Если величина x с ростом потребления увеличивается, то есть ведет себя как актив, который приносит хорошую прибыль, когда экономика растет, он скоррелирован отрицательно с величиной u', и тогда добавка, плата за риск, отрицательна. Какой в этом смысл? Если вы дополнительно инвестируете в такой актив,то ваш уровень потребления как пользователя становится более волатильным и для вас это приемлемо только в том случае, если цена этого актива будет с каким-то дисконтом (cov(m,x) будет снижать цену по сравнению с базовой стоимостью). И наоборот, когда актив это что-то вроде страховки (когда потребление падает) и ведет себя хорошо, то тогда он положительно скоррелирован с производной функцией u, и тогда у вас появляется дополнительная премия, которую вы платите, чтобы инвестировать в этот актив.

Избыточная доходность. Системный и идиосинкратический риск

Посмотрим, какие выкладки получаются для вкладов. Пусть у нас сегодня есть один рубль, который мы вложили в какой-то рисковый инструмент. Согласно нашему уравнению ценообразования мы должны получить для доходности нашего инструмента Ri. Имеем:

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

То, что стоит в левой части равенства, доходность рискового инструмента сверх базовой ставки, дополнительная доходность. То есть слева мы получаем разницу между доходностью этого рискового инструмента и базовой ставкой. Эту разницу можно назвать избыточной доходностью (рискового инструмента). Данная формула говорит о том, что избыточную доходность нам дают те инструменты, у которых ковариация отрицательна, то есть те инструменты, которые положительно коррелируют с уровнем потребления. Такие инструменты должны обещать большую избыточную доходность. Наоборот, инструменты типа страховки дадут положительное значение ковариации, и доходность будет отрицательной (но они страхуют нас, когда потребление падает).

Посмотрим, что нам еще дает уравнение установления цен, записанное в таком виде:

Рисковый актив x обладает некоторой волатильностью. Заметим, что в наше уравнение установления цен входит именно ковариация m и x, а не вариация случайной величины x. Например, если x и m скоррелированы слабо, значение ковариации практически равно нулю, то, несмотря на то что x может иметь очень большую или очень маленькую волатильность, цена p, согласно этому уравнению, будет той же самой и определяться компонентой формулы E[x]/Rf, фактически от E[x]. Из этого можно сделать вывод: важна волатильность случайной величины x сама по себе, а именно ее проекция на m. Можно записать, что x является суммой двух величин проекции величины x на m и всего остального:

Для проекции можно записать явную формулу:

Эта формула похожа на формулу, которая пишется для проекции одного вектора на другой в векторном пространстве. Что же касается ортогональной части , то ее ковариация с m равна нулю:

Случайная величина x, ожидаемая доходность актива, может иметь некоторую волатильность, то есть некоторый риск. При этом часть этого риска влияет на цену (является систематическим риском та часть, которая является проекцией на m). Другая же часть, хоть и является случайной, никак не влияет на цену и называется идиосинкратической. Риск какого-то инструмента, какого-то актива, всегда распадается на систематическую и идиосинкратическую части.

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

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

Одной из первых попыток построить работающую модель была попытка под названием CAPM (Capital Asset Pricing Model), которая утверждает, что наш стохастический коэффициент дисконтирования есть просто линейная комбинация, в которую входит Rw доходность портфеля благосостояния.

Доходность этого портфеля благосостояния можно померить, если в качестве ориентира взять какой-нибудь большой индекс, например Standard & Poor's 500, и посмотреть доходность этого индекса. Эту модель называют моделью с одним фактором. Более сложные модели, такие как APT (Arbitrage pricing theory), говорят, что m представляет собой линейную комбинацию нескольких факторов.

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

Все статьи этой серии

Подробнее..

Векторные языки SQL интерпретатор в 100 строк

10.06.2021 16:07:13 | Автор: admin

В предыдущей статье я описал векторные языки и их ключевые отличия от обычных языков. На коротких примерах я постарался показать, как эти особенности позволяют реализовывать алгоритмы необычным образом, кратко и с высоким уровнем абстракции. В силу своей векторной природы такие языки идеально присоблены для обработки больших данных, и в качестве доказательства в этой статье я полностью реализую на векторном языке простой SQL интерпретатор. А чтобы продемонстрировать, что векторный подход можно использовать на любом языке, я реализую тот же самый интерпретатор на Rust. Преимущества векторного подхода столь велики, что даже интерпретатор в интерпретаторе сможет обработать select с группировкой из таблицы в 100 миллионов строк за 20 с небольшим секунд.

Общий план.

Конечная цель - реализовать интепретатор, способный выполнять выражения типа:

select * from (select sym,size,count(*),avg(price) into r  from bt where price>10.0 and sym='fb'  group by sym,size)  as t1 join ref on t1.sym=ref.sym, t1.size = ref.size

Т.е. он должен поддерживать основные функции типа сложения и сравнения, позволять where и group by выражения, а также - inner join по нескольким колонкам.

В качестве векторного языка я возьму Q, так как его специализация - базы данных.

Интерпретатор будет состоять из лексера, парсера и собственно интерпретатора. Для экономии места я буду приводить только ключевые места, а весь код можно найти здесь. Так же для краткости я реализую лишь часть функциональности, но так, чтобы все важное было на месте: join, where, group by, 3 типа данных, агрегирующие функции и т.п.

На примере лексера вы сможете познакомиться, пожалуй, с одним из самых красивых приемов программирования, который встречается в векторных языках, - использование данных как функций.

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

Наконец, сам интерпретатор. На Q он имеет весьма скромные размеры - строк 25. На Rust, конечно, гораздо больше, но написать его проще, чем может показаться. Нужно просто по шагам повторять все операции Q, адаптируя их к специфике Rust.

Это моя первая программа на Rust и сразу хочу сказать, что слухи о его сложности сильно преувеличены. Если писать в функциональном стиле (read only), то проблем нет никаких. После того, как Rust несколько раз забраковал мои идеи, я понял, чего он хочет и уже не сталкивался с необходимостью все переделывать из-за контроллера ссылок, а явные времена жизни понадобились только один раз и по делу. Зато взамен мы получаем программу, которую можно распараллелить по щелчку пальцев. Что мы и сделаем, чтобы добиться просто феноменальной производительности для столь примитивной программы.

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

Лексер

Векторные языки идеальны для написания лексеров. Пусть у нас есть функция fsa, которая принимает на вход текущее состояние лексера и входной символ и возвращает новое состояние:

fsa[state;char] -> state

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

Т.е. есть следующие этапы:

  • Кодирование. Входные символы отображаются в группы (my.var -> aa.aaa, 12.01 -> 00.00, "str 1" -> "sss 1" и т.д.).

  • Трансформация. Закодированные символы пропускаются через fsa (aa.aaa -> aAAAAA, 00.00 -> 0IFFF, "sss 1" -> "SSSSSR).

  • Разбиваем начальную строку на части по начальным состояниям (a, 0, " и т.д.). Для удобства все не начальные состояния обозначены большими буквами.

Все три этапа - это векторные операции, поэтому на Q эта идея реализуется одной строкой (все состояния закодированы так, что начальные меньше limit):

(where fsa\[start;input]<limit)cut input

Это в сущности весь лексер. Правда еще необходимо определить fsa. В подавляющем большинстве случаев в качестве fsa можно взять матрицу - таблицу переходов конечного автомата. Простой автомат можно задать и руками, но можно реализовать небольшой DSL. Отображение в группы можно организовать через небольшой массив (ограничимся ASCII символами для простоты):

cgrp: ("\t \r\n";"0..9";"a..zA..Z"),"\\\"=<>.'";c2grp: 128#0; // массив [0;128]// Q позволяет присваивать значения по индексу любой формы.// В данном случае массиву массивов. В Rust необходимы два явных цикла:// cgrp.iter().enumerate().for_each(|(i,&s)| s.iter()//   .for_each(|&s| c2grp[s as usize] = i + 1));c2grp[`int$cgrp]: 1+til count cgrp;

Для краткости я не привожу все цифры и буквы. Нас интересуют пробельные символы, цифры, буквы, а также несколько специальных символов. Мы закодируем эти группы числами 1, 2 и т.д., все остальные символы поместим в группу 0. Чтобы закодировать входную строку, достаточно взять индекс в массиве c2grp:

c2grp `int$string

Автомат задается правилами (текущее состояние(я);группа(ы) символов) -> новое состояние. Для обозначения групп и начальных состояний токенов удобно использовать первые символы соответствующих групп (для группы 0..9 - 0, например). Для обозначения промежуточных состояний - большие буквы. Например, правило для имен можно записать так:

aA А a0.

т.е. если автомат находится в состояниях "a" (начало имени) или "A" (внутри имени), и на вход поступают символы из групп [a,0,.], то автомат остается в состоянии "A". В начальное состояние "a" автомат попадет автоматически, когда встретит букву (это правило действует по умолчанию). После этого, если дальше он встретит букву, цифру или точку, то перейдет во внутреннее состояние "A" и будет там оставаться до тех пор, пока не встретит какой-то другой символ. Я запишу все правила без лишних комментариев (Rust):

let rules: [&[u8];21] =  [b"aA A a0.",                         // имена   b"0I I 0",b"0I F .",b"F F 0",        // int/float   b"= E =",b"> E =",b"< E =",b"< E >", // <>, >= и т.п.   b"\" S *",b"S S *",b"\"S R \"",      // "str"   b"' U *",b"U U *",b"'U V '",         // 'str'   b"\tW W \t"];                        // пробельные символы

Числа и строки заданы в упрощенном формате. "*" в качестве входного символа имеет специальное значение - все возможные символы. Большие буквы - это состояния внутри токенов. Такая договоренность дает нам возможность легко определить начало токена - это все состояния, которые не большие буквы.

Матрица fsa из этих правил генерируется элементарно. Схематично это выглядит так:

fsa[*;y] = y (по умолчанию для всех состояний)"aA A a0." -> "aA","A","a0."; fsa[enc["aA"];enc["a0."]] = enc["A"]...

Необходимо закодировать символы с помощью вектора states:

states: distinct " ",(first each cgrp),raze fsa[;1];limit: 2+count cgrp;enc:states?; // в Q encode - это поиск индекса элемента в векторе

Вперед поместим все начальные состояния (пробел для учета группы 0), чтобы можно было легко определить limit.

Код генерации fsa я опускаю - он следует схеме выше.

Матрица переходов у нас есть, осталось определить саму функцию lex. В парсере нам понадобится знать тип токена, поэтому вычислим и его тоже. На Rust лексер выглядит так:

let s2n = move |v| ["ID","NUM","STR","STR","WS","OTHER"][find1(&stn,&v)];move |s| {    if s.len()==0 {return Vec::<Token>::new()};    let mut sti = 0usize;    let st: Vec<usize> = s.as_bytes().iter().map(|b| { // st:fsa\[0;c2grp x]        sti = fsa[sti][c2grp[std::cmp::min(*b as usize,127)]];        sti}).collect();    let mut ix: Vec<usize> = st.iter().enumerate() // ix:where st<sta        .filter_map(|(i,v)| if *v<sta {Some(i)} else {None}).collect();    ix.push(st.len());    (0..ix.len()-1).into_iter()        .filter_map(|i|            match s2n(st[ix[i]]) {                 "WS" => None,                  kind => Some(Token{ str:&s[ix[i]..ix[i+1]], kind})             }).collect()

На Q получится значительно более кратко:

s2n:(states?"a0\"'\t")!("ID";"NUM";"STR";"STR";"WS");lex:{  i:where (st:fsa\[0;c2grp x])<limit;  {x[;where not "WS"~/: x 0]} (s2n st i;i cut x)};

Если мы запустим лексер, то получим:

lex "10 + a0" -> (("NUM";"";"ID");("10";"+";"a0"))

Интерпретатор

Интерпретатор можно разделить на две части - выполнение выражений и выполнение select. Первая часть тривиальна на Q, но требует большого количества кода на Rust. Я приведу основные структуры данных, чтобы было понятно, как в целом работает интерпретатор. В основе лежит enum Val:

type RVal=Arc<Val>;enum Val {       I(i64),    D(f64),    II(Vec<i64>),    DD(Vec<f64>),    S(Arc<String>),    SS(Vec<Arc<String>>),    TBL(Dict<RVal>),    ERR(String),}

Есть три типа данных - строки, целые и нецелые, две формы их представления - атомарная и вектор. Также есть таблицы и ошибки. Dict - это пара Vec<String> и Vec<T> одинаковой длины. В случае таблицы T = Vec<RVal>, где каждый Val - это II, DD или SS. Rust позволяет в легкую распаралелливать программу, но нужно, чтобы типы данных позволяли передавать свои значения между потоками. Для этого я обернул все разделяемые значения в асинхронный счетчик ссылок Arc. Считается, что атомарные операции более медленные, однако в программе, которая работает с большими данными, это не имеет большого значения.

Интерпретатор работает с выражениями:

enum Expr {    Empty,    F1(fn (RVal) -> RRVal, Box<Expr>), // f(x)    F2(fn (RVal,RVal) -> RRVal, Box<Expr>, Box<Expr>), // f(x,y)    ELst(Vec<Expr>),    ID(String),  // variable/column    Val(Val),    // simple value - 10, "str"    Set(String,Box<Expr>), // 'set var expr' - assignment    Sel(Sel), // select    Tbl(Vec<String>,Vec<Expr>), // [c1 expr1, c2 expr2] - create table }

ELst и Empty используются только парсером. Expr (ссылки на себя) необходимо хранить в куче (Box). Выполняются выражения функцией eval в некотором контексте, где заданы переменные (Set), а также могут быть определены колонки таблицы:

struct ECtx {    ctx: HashMap<String,Arc<Val>>,   // variables}struct SCtx {    tbl: Arc<Table>,                // within select    idx: Option<Vec<usize>>,        // idx into tbl    grp: Arc<Vec<String>>,          // group by cols}

eval сравнительно проста (self = ECtx):

type RRVal=Result<Arc<Val>,String>;fn top_eval(&mut self, e: Expr) -> RRVal {    match e {        Expr::Set(id,e) => {            let v = self.eval(*e, None)?;            self.ctx.insert(id,v.clone()); Ok(v)},        Expr::Sel(s) => self.do_sel(s),        _ => self.eval(e, None)    }}fn eval(&self, e: Expr, sctx:Option<&SCtx>) -> RRVal {    match e {        Expr::ID(id) => self.resolve_name(sctx,&id),        Expr::Val(v) => Ok(v.into()),        Expr::F1(f,e) => Ok(f(self.eval(*e,sctx)?)?),        Expr::F2(f,e1,e2) => Ok(f(self.eval(*e1,sctx)?,self.eval(*e2,sctx)?)?),        Expr::Tbl(n,e) => { self.eval_table(None,n,e) }        e => Err(format!("unexpected expr {:?}",e))    }}

Set и Sel нужен модифицируемый контекст, а его нельзя будет передать просто так в другой поток. Поэтому eval разбит на две части. Задача resolve_name - найти переменную или колонку и при необходимости применить where индекс. eval_table - собрать таблицу из частей и проверить, что с ней все в порядке (колонки одной длины и т.п.). Функции F1 (max, count ...) и F2 (+, >=, ...) сводятся к огромным match блокам, где для каждого типа прописывается нужная операция. Макросы позволяют уменьшить количество кода. Например, для арифметических операций часть match выглядит так:

(Val::D(i1),Val::I(i2)) => Ok(Val::D($op(*i1,*i2 as f64)).into()),(Val::D(i1),Val::D(i2)) => Ok(Val::D($op(*i1,*i2)).into()),(Val::I(i1),Val::II(i2)) => Ok(Val::II(i2.par_iter()    .map(|v| $op(*i1,*v)).collect()).into()),

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

Выполнение select гораздо интереснее и сложнее. Разберем его на Q, потому что код на Rust многословно повторяет код на Q, который и сам по себе непростой.

Select состоит из подвыражений (join, where, group, select, distinct, into), каждое из которых выполняется отдельно. Самое сложное из них - join. В его основе лежит функция rename, задача которой присвоить колонкам уникальные имена, чтобы не возникло конфликта при join:

// если x это name -> найти, select -> выполнитьsget:{[x] $[10=type x;get x;sel1 x]};// в грамматике таблица определяется как '(ID|sel) ("as" ID)?'// так что x это список из 2 элементов: (ID из as или имя таблицы;ID/select)// y - уникальный префиксrename:{[x;y]  t:sget x 1; // получить таблицу: names!vals  k:(k!v),(n,/:k)!v:(y,n:x[0],"."),/:k:key t; // k - оригинальные имена,        // v - уникальные, n - с префиксом (table.name)  (k;v!value t)};

Все эти манипуляции сводятся к построению двух словарей - отображения из настоящих имен колонок и расширенных (table.name) в уникальные и из уникальных имен в сами колонки таблицы. Уникальные имена позволяют иметь в одной join таблице колонки с одинаковыми именами из разных таблиц и обращаться к ним в выражениях через нотацию с точкой.

В основе join следующая функция:

// x - текущая таблица в формате rename// y - следующая таблица в этом формате// z - join выражение, список (колонка в x;и в y)// условие join: x[z[i;0]]==y[z[i;1]] для всех ijoin_core:{[x;y;z]  // m - отображение имен в уникальные для новой таблицы x+y  // имена из x имеют приоритет  // c - переименовываем join колонки в уникальные имена  c:(m:y[0],x 0)z;  // после join z[;0] и z[;1] колонки будут одинаковыми  // поэтому колонки из y перенаправим на x  m[z[;1]]:c[;0];  // x[1]c[;0] - просто join колонки из таблицы x (подтаблица)  // y[1]c[;1] - симметрично из y  // sij[xval;yval] -> (idx1;idx2) найти индексы join в обеих таблицах  // sidx[(i1;i2);x;y без join колонок] -  //  собрать новую таблицу из x и y и индексов  (m;sidx[sij[x[1]c[;0];y[1]c[;1]];x 1;c[;1]_ y 1])}// sidx просто применяет индексы ко всем колонкам и объединяет y и z// y z - это словари, но поскольку традиционно векторные функции имеют// максимально широкую область определения, не нужно обращаться явно к value sidx:{(y@\:x 0),z@\:x 1};

Вся работа выполняется в функции sij, все остальное - это манипуляции именами, колонками и индексами. Если бы мы захотели добавить другой тип индекса, достаточно было бы написать еще одну sij. Конечно, функция выглядит непросто, но учитывая, что она покрывает 80% select, ей можно это простить.

Функция sij сводится к поиску строк таблицы x в таблице y. В Rust для этих целей можно использовать HashMap с быстрой hash функцией FNV - поместить в Map одну таблицу и потом искать в ней строки второй. В Q, судя по времени выполнения, скорее всего используется что-то подобное. В целом в Q у нас есть два варианта - использовать векторные примитивы или воспользоваться встроенными возможностями связанными с таблицами. В первом варианте все по-честному:

// x и y - списки колонокsij:{j:where count[y 0]>i:$[1=count x;y[0]?x 0;flip[y]?flip x]; (j;i j)};// или на псевдокоде// i=find_idx[tblY;tblX]; j=i where not null i; return (j,i[j])

используем функцию поиска значения в векторе (?) и транспозиции матрицы (flip). Этот вариант не такой медленный как может показаться - всего в 2.5 раза медленнее, чем оптимизированный поиск сразу по таблице (который выглядит ровно также - x?y, только x и y - таблицы, а не списки векторов). Это показывает в очередной раз силу векторных примитивов.

Наконец сам join - это просто цикл свертки по всем таблицам (fold):

// "/" это fold, rename' это map(rename)sjoin:{[v] join_core/[rename[v 0;"@"];rename'[v 1;string til count v 1];v 2]};

Остальные части select гораздо проще. where:

swhere:{[t;w] i:til count value[t 1]0;  // все строки по умолчанию  $[count w;where 0<>seval[t;i;();w];i]}; // выбрать те, которые не 0// seval такой же как eval в Rust, т.е. его сигнатура:// seval[table,index;group by cols;expr], ECtx - это сам Q

Основная функция select:

sel2:{[p] // p ~ словарь с элементами select (`j, `s, `g  и т.п.)  i:swhere[tbl:sjoin p`j;p`w]; // сходу делаем join и where  if[0=count p`s; // в случае select * надо найти подходящие имена колонкам    rmap:v[vi]!key[tbl 0] vi:v?distinct v:value tbl 0;    p[`s]:{nfix[x]!x} rmap key tbl 1];  if[count p`g; // group by    // из group колонок нужен только первый элемент, нужно знать их имена    gn:nfix {$[10=type x;x;""]} each p`g;    // sgrp вернет список индексов (idx1;idx2;..) для каждой группы    // затем нужно выполнить seval[tbl;idxN;gn;exprM] для всех idx+expr    // т.е. двойной цикл, который в Q скрыт за двумя "each"    g:sgrp[tbl;i;p`g]];    :key[p`s]!flip {x[z] each y}[seval[tbl;;gn];value p`s] each g;  // если group нет, то все элементарно - просто seval для всех select выражений  (),/:seval[tbl;i;()] each p`s };

Функция sgrp в основе group by - это просто векторный примитив group, возвращающий словарь, где ключи - уникальные значения, а значения - их индексы во входном значении:

sgrp:{[t;i;g] i value group flip seval[t;i;()] each g};

Я опускаю distinct и into части, поскольку они малоинтересны. В целом - это весь select на Q. В краткой записи он занимает всего 25 строк. Можно ли ждать хоть какой-то производительности от столь скромной программы? Да, потому что она написана на векторном языке!

Производительность

Напомню, что этот игрушечный интерпретатор может выполнять выражения типа

select * from (select sym,size,count(*),avg(price) into r  from bt where price>10.0 and sym='fb'  group by sym,size)  as t1 join ref on t1.sym=ref.sym, t1.size = ref.size

и при этом справляться с таблицами в сотни миллионов строк. В частности таблица bt в выражении выше сгенерирована выражением:

// в интерпретаторе на Rust// s = ("apple";"msft";"ibm";"bp";"gazp";"google";"fb";"abc")// i/f - i64/f64 интервалы [0-100)set bt [sym rand('s',100000000), size rand('i', 100000000),    price rand('f', 100000000)]

Т.е. содержит 100 миллионов строк. Поначалу базовый select с group by (получается 800 групп по ~125000 элементов)

select sym,size,count(*),avg(price) into r from bt group by sym,size

работал 44 секунды в программе на Rust, что совсем неплохо и само по себе. Однако после некоторых оптимизаций, а главное распаралелливания ключевых операций, мне удалось добиться скорости порядка 6 секунд. На мой взгляд весьма хороший результат для подобной таблицы.

Самое главное, программа на Rust, несмотря на свой внушительный вид, - это почти 1 в 1 программа на Q. Поэтому больших интеллектуальных усилий и даже отладки она не потребовала. Также благодаря векторности изначального языка ее ускорение путем распараллеливания не потребовало почти никаких усилий - если все операции изначально над массивами, то все что нужно - это вставить там и тут par_iter вместо iter.

Интерпретатор на Q не столь быстр, но вышеуказанный select всего на 50% медленнее аналогичного запроса на самом Q. Это значит, что по сути Q работает на больших данных почти также быстро, как и программа на компилируемом языке.

Хочу также отметить то, насколько великолепным языком проявил себя Rust. За все время разработки и отладки я не получил ни одного segfault и даже panic увидел всего несколько раз, и почти все это были простые ошибки выхода за пределы массива. Также поражает, насколько легко и безопасно в нем можно распараллелить задачу.

Парсер

Я отложил парсер на конец, поскольку он довольно объемен и не имеет прямого отношения к теме статьи. Тем не менее, может вам будет интересно ознакомиться с тем, как легко можно написать весьма серьезный парсер в функциональном стиле на Rust.

Это рекурсивный нисходящий парсер без заглядывания вперед. Из-за этого он не может предсказать следующий шаг и вынужден обходить все варианты. Такой парсер, конечно, медленный и не годится для серьезных задач, но для SQL выражений больше и не надо. Какая разница, парсится выражение 1 микросекунду или 10, если сам запрос займет минимум сотни микросекунд.

Такие парсеры часто пишут руками, и выглядят они примерно так:

parse_expr1(..) {   if(success(parse_expr2(..)) {    if (success(parse_str("+") || success(parse_str("-")) {      if(success(parse_expr1(..)) {         return <expr operation expr>      }      return Fail    }    return <expr>  }  return Fail;}

Главная идея предлагаемого парсера в том, что нет смысла писать это все руками, можно написать генератор подобных парсеров из BNF-подобной формы. Для всех сущностей BNF пишем по функции, затем генерируем из описания грамматики в виде строк набор парсящих функций, и все готово. В Rust, как строго типизированном языке, есть нюансы. В первую очередь определим типы для парсящих и post process функций:

type ParseFn = Box<dyn Fn(&PCtx,&[Token],usize) -> Option<(Expr,usize)>>;type PPFn = fn(Vec<Expr>) -> Expr;

ParseFn будет захватывать правила грамматики, поэтому она должна быть замыканием (closure) и лежать в куче. PCtx содержит другие ParseFn для рекурсивных вызовов и PPFn для постобработки дерева. Если парсинг не удался, она возвращает None, иначе Some с выражением и новым индексом в массив токенов. PPFn обрабатывает узел дерева, поэтому принимает безликий список выражений и превращает его в нужное выражение.

Для понимания процесса приведу часть грамматики, касающуюся выражений:

("expr", "expr1 ('or' expr {lst})? {f2}"),("expr1","'not' expr1 {f1} | expr2 ('and' expr1 {lst})? {f2}"),("expr2","expr3 (('='|'<>'|'<='|'>='|'>'|'<') expr2 {lst})? {f2}"),("expr3","expr4 (('+'|'-') expr3 {lst})? {f2}"),("expr4","vexpr (('*'|'/') expr4 {lst})? {f2}"),("vexpr","'(' expr ')' {2} | '-' expr {f1} | call | ID | STR | NUM |  '[' (telst (',' telst)* {conc}) ']' {tblv}"),("call", "('sum'|'avg'|'count'|'min'|'max') '(' expr ')' {call} |  'count' '(' '*' ')' {cnt} | 'rand' '(' STR ',' NUM ')' {rand}"),

Тут видны ключевые части - имя правила, само правило и PP функции в фигурных скобках. Каждая продукция правила должна заканчиваться на PP функцию, поскольку правило возвращает Expr, а не Vec<Expr>. PP функция по умолчанию возвращает последний элемент вектора, поэтому кое-где PP функций нет. ID, NUM и т.п. должны обрабатываться ParseFn функцией с соответствующим именем.

Генерируется наш парсер с помощью следующей функции:

let parse = |str| {    let t = l(str);  // add ({}) depth map    let mut lvl = 0;    pp_or(&t.into_iter().map(|v| {      match v.str.as_bytes()[0] {        b'(' | b'{' => lvl+=1,        b')' | b'}' => lvl-=1,        _ => ()};      (v,std::cmp::max(0,lvl))}).collect::<Vec<(Token,i32)>>()    , 0)};

l - это лексер, я переиспользую для этого лексер SQL. Нужно добавить карту глубины вложенности скобок, чтобы было удобно выделять BNF подвыражения. Поскольку это парсер для внутренних потребностей, то нет необходимости проверять правильность скобок, беспокоиться о глубине рекурсии и т.п.

Далее наше правило поступает в парсер BNF. Нужно реализовать следующие компоненты:

  • or правило - A | B

  • and правило - A B C

  • const правило - "(", "select".

  • token правило - NUM, STR.

  • subrule правило - expr1, call.

  • optional правило - A?

  • 0+ правило - A*

  • 1+ правило - A+

  • PP правило - {ppfn}

Это работа, требующая тщательности, но проделать ее нужно один раз. Например, or правило:

fn pp_or(t: &[(Token,i32)], lvl:i32) -> ParseFn {    if t.len() == 0 {return Box::new(|_,_,i| Some((Expr::Empty,i)))};    let mut r: Vec<ParseFn> = t      .split(|(v,i)| *i == lvl && v.str.as_bytes()[0] == b'|' )      .map(|v| pp_and(v,lvl)).collect();    if 1 == r.len() {        r.pop().unwrap()    } else {        Box::new(move |ctx,toks,idx|          r.iter().find_map(|f| f(ctx,toks,idx)))    }}

Функция должна вернуть ParseFn замыкание. В общем случае, когда pp_and вернула несколько ParseFn, нужно организовать цикл и выполнять подфункции, пока одна из них не вернет Some.

pp_and работает аналогично, только все ее подфункции должны вернуть Some. Также в случае успеха она должна вызвать нужную PPFn для обработки результата.

fn pp_and(t: &[(Token,i32)], lvl:i32) -> ParseFn {    if t.len() == 0 {return Box::new(|_,_,i| Some((Expr::Empty,i)))};    let (rules,usr) = pp_val(Vec::<ParseFn>::new(),t,lvl);    Box::new(move |ctx,toks,i| {        let mut j = i;        let mut v = Vec::<Expr>::with_capacity(rules.len());        for r0 in &rules {        if let Some((v0,j0)) = r0(ctx,toks,j) {            j = j0; v.push(v0)            } else {return None} };        Some((ctx.ppfns[&usr](v),j))    })}

pp_val рекурсивно обрабатывает круглые скобки и все базовые выражения. Вот некоторые примеры из нее:

// Token - if ok call rules[Token]move |ctx,tok,i| if i<tok.len() && tok[i].kind == s   {ctx.rules[&s](ctx,tok,i)} else {None}// Subrulemove |ctx,tok,i| ctx.rules[&s](ctx,tok,i))}// rule?move |ctx,tok,i| Some(rule(ctx,tok,i).unwrap_or((Expr::Empty,i)))// rule+move |ctx,tok,i| {    let (e,i) = plst(&rule,ctx,tok,i);    if 0<e.len() {Some((Expr::ELst(e),i))} else {None}}// где plstlet mut j = i; let mut v:Vec<Expr> = Vec::new();while let Some((e,i)) = rule(ctx,tok,j) {j=i; v.push(e)};(v,j)

Это весь код, необходимый для создания парсера. Чтобы его сгенерировать, нужно вызвать parse и положить правило в map:

let mut map = HashMap::new();map.insert("expr".to_string(), parse("expr1 ('or' expr {lst})? {f2}"));...  

Также необходимо определить PP функции. В большинстве случаев они сравнительно просты:

let mut pfn: HashMap<String,PPFn> = HashMap::new();// default rulepfn.insert("default".to_string(),|mut e| e.pop().unwrap());// set name expr выражениеpfn.insert("set".to_string(),|mut e| Expr::Set(e.swap_remove(1).as_id(),  e.pop().unwrap().into()) );

В Rust нельзя просто взять элемент из массива, поэтому необходимы функции типа swap_remove, которые делают это безопасно.

Наконец, положим правила в специальную структуру и определим для нее функцию parse:

PCtx { rules:map, ppfns:pfn}...impl PCtx {    fn parse(&self, t:&[Token]) -> Expr {        if let Some((e,i)) = self.rules["top"](&self,t,0) {            if i == t.len() {e}              else {Val::ERR("parse error".into()).into()}        } else {Val::ERR("parse error".into()).into()}    }}

Все, парсер готов. Он не быстр, но зато очень прост. Плюс он полностью динамический - можно менять правила во время выполнения программы, например, отключить какие-то возможности.

Подробнее..

Построение кривой дисконтирования. Ликбез для гика, Ч. 5

16.07.2020 16:05:50 | Автор: admin
Давайте научимся строить кривую дисконтирования. Скажу сразу: очень важно иметь актуальные рыночные данные. Если вы будете искать interest rate свопы на доллар, евро или какую-нибудь другую валюту, то в интернете не так много открытой информации. Большинство данных доступны либо через торговые терминалы, либо у специальных компаний провайдеров рыночных данных. Я нашел на сайте одной крупной скандинавской банковской группы данные по датской, шведской и норвежской кронам, а также данные для евро и доллара.



Далее мы попробуем построить кривую дисконтирования для шведской кроны.


Данный пост адаптированная версия моей третьей видеолекции Построение кривой дисконтирования в рамках курса Finmath for Fintech.

Итак, наша кривая дисконтирования для кроны будет состоять из ставок овернайт на каждый день. Первое, что нам известно, так называемый base rate ставка на короткие депозиты (кредиты). Далее, известны свопы, начиная с одного года до тридцати лет. Для иллюстрации метода мы построим кривую до десяти лет. Текущие значения рыночных данных можно увидеть на этом изображении:



Чтобы начать строить кривую, нам нужно сделать несколько предположений.

Давайте для упрощения считать, что наши свопы это fix floating свопы с периодичностью выплат раз в шесть месяцев. Ниже приведена схема для одногодичного свопа. Мы знаем, что в начале base rate равен нулю процентов. Чтобы посчитать честную цену свопа, нам нужно знать значение дискаунт-фактора шести месяцев и дискаунт-фактора 12 месяцев. Что у нас будет в качестве плавающей ноги? Предположим, что в качестве нее мы будем платить среднее значение овернайтов за каждый из диапазонов. То есть значение плавающей ноги до шести месяцев это будет среднее значение овернайтов за 180 дней. Плавающая нога для точки 12 месяцев будет то же самое, только здесь будет суммирование начиная со 181 дня до 360-го.



Данный способ усреднения широко известен. Он называется overnight index swap и очень часто используется в рыночных продуктах. Плавающая нога тут определяется как среднее за период.

Мы знаем значение базовой ставки и стоимость свопа. Очевидно, что если мы запишем формулу честной цены в лоб, то у нас будет слишком много неизвестных. У нас неизвестен дискаунт-фактор для 6 месяцев, дискаунт-фактор для 12 месяцев и неизвестны значения процентных ставок кроме одного самого первого. Слишком много неизвестных, и всего лишь одно уравнение.



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



В точке 6 месяцев это будет X/2 (среднее арифметическое между нулем и X), и мы можем найти значение процентной ставки в любой произвольный день. И нет ничего сложного в том, чтобы посчитать нашу плавающую процентную ставку в точках 6 и 12 месяцев:

$\begin{eqnarray} L_1&=& \frac{X}{4} \end{eqnarray}$



$\begin{eqnarray} L_1&=& \frac{3X}{4} \end{eqnarray}$



Теперь перейдем к дискаунт-факторам. Мы используем кривую дисконтирования на основе овернайт рэйта. Поэтому дискаунт-фактор в точке шесть месяцев произведение 180 дискаунт-факторов в каждой точке, и это будет, очевидно, являться какой-то функцией от X.



Дискаунт-фактор в точке 12 месяцев строится аналогичным образом с той лишь разницей, что мне нужно больше множителей. Это тоже будет какая-то функция от X.



Итак, дискаунт-факторы выражены через X, также есть первое и второе значения плавающей ставки. Перейдем к записи уравнения. Значение цены свопа нам известно, допустим, оно равно P. Вспомним уравнение для честной цены. Нам надо P умножить на дисконт-фактор в точке двенадцати месяцев и приравнять к следующей сумме:



Напомню, дискаунт-фактор для одного дня будет определяться следующей формулой:

$\begin{eqnarray} DF_i&=& \frac{1}{(1+\frac{r_i}{360})} \end{eqnarray}$



где ri значение процентной ставки. Число 360 я использую из предположения, что в году 360 дней (это вполне распространенное соглашение о календарях). В каждой конкретной точке мы знаем, как выразить дискаунт-фактор, ri выражается через X, с исползованием линейной интерполяции. Наше уравнение оказывается всего с одним неизвестным, и его можно решить, используя численные методы. Как это делается смотрите в коде на Python.

Итак, мы знаем, как найти значение нашей ставки в точке 1 год. Используя предположение о линейной интерполяции и исходя из значения свопа, которое нам известно с рынка, мы найдем наше значение X. Вот мы построили первый участок нашей кривой:



Теперь, чтобы посчитать цену свопа на два года, нам нужно значение в точке 6 месяцев, 12 месяцев, 18 месяцев и 2 года. Мы будем использовать точно такое же предположение, как в прошлый раз. Назовем значение искомой ставки Y и также будем использовать предположение о линии интерполяции, восстановив второй участок кривой. Таким образом, шаг за шагом мы дойдем до конца до точки 10 лет.



Такой метод называется bootstrap. Он не является идеальным или единственно верным, но достаточно прост для реализации и понимания в качестве стартового уровня метод bootstrap отлично подходит.



Мы нашли дискаунт-кривую. Что она нам дает? Если говорить формально, то это значения овернайт-ставки в любой точке в будущем до десяти лет. Вы, наверное, спросите: А кому это надо?. Действительно, трудно себе представить сценарий, когда к вам придет клиент и скажет: Я хочу открыть однодневный депозит, который начнется через 567 дней. Это довольно непонятная ситуация, и в таком непосредственном виде воспринимать построенную кривую не стоит.

Представим себе, что у нас есть какой-то платеж в будущем, скажем семь с половиной лет. Вопрос: как мы узнаем его текущую стоимость?

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



Ту модель, которую я использовал, а именно какие я брал плавающие ставки, как я интерполировал промежуточные значения, и вообще тот факт, что я выбрал интерполяцию, очень сильно было определено тем, какие именно данные у меня есть. У меня было очень мало данных только одна базовая ставка и значения свопов. Если мне доступно больше данных, либо они другие, то, скорее всего, я изменю модель. Но метод bootstrap (когда вы строите кривую сначала на коротком участке, а затем строите все дальше и дальше, опираясь на полученные ранее значения) все равно применим.

Теперь вспомним, что помимо дискаунт-кривых нам нужны кривые LIBOR (TIBOR, EURIBOR и пр.). Разница будет в том, какие инструменты мы добавляем в нашу модель для расчета. Мы будем искать контракты, содержащие LIBOR, и похожим способом, используя метод bootstrap, восстановим LIBOR-кривую.

Если вам придется строить настоящую LIBOR-кривую, очень внимательно отнеситесь к тому, какие инструменты вы используете для ее построения, внимательно оцените ту модель, которую вы будете применять. В данном случае я использовал дисконтирование овернайт, но для построения LIBOR-кривой нужен другой способ. Скорее всего, дисконтирование будет каждые три месяца или шесть, в зависимости от инструмента. Если у вас есть достаточно данных, вы можете построить LIBOR-кривую, EURIBOR-кривую, TIBOR-кривую и любую другую.

Если к вам приходит клиент со словами: Я хочу процентный своп не на десять лет, а на 134 месяца, в которые буду платить каждые 25 дней плавающий LIBOR это не проблема. У нас есть LIBOR-кривая, мы используем предположение об интерполяции, мы можем восстановить значение LIBOR в любой точке. Мы знаем значение дискаунт-кривой в каждой точке, мы тоже можем подсчитать все платежи и найти ту самую цену фиксированной ноги, которая уравновешивает эти плавающие платежи. Тем самым вы можете найти значения честной цены для совершенно любого инструмента, построив несколько кривых.

Итак, давайте еще раз пробежимся по основным моментам. Я взял доступные данные и сформулировал несколько допущений. Во-первых, расписание платежей: как часто, с какой периодичностью каждая сторона платит фиксированную ногу и плавающую ногу. Во-вторых, как я буду считать ставку на плавающей ноге. Третье предположение о линейной интерполяции ставок. Используя все эти три предположения, я сформулировал несколько нелинейных уравнений, которые решил численно. Jupyter notebook вы найдете по ссылке. Последовательно, начиная с самого короткого отрезка в один год, далее два года, три и т.д., я восстановил кривую на промежутке до 10 лет. Это моя дискаунт-кривая, которую я могу использовать для оценки любых инструментов. Такой метод называется bootstrap: отрезок кривой, который я посчитал в самом начале, использую на втором шаге, а то, что получил на втором шаге, я использую для третьего шага и так далее, пока кривая не сформируется полностью.

Надеюсь, теперь вы больше не плаваете в теме плавающих процентных ставок и среди interest rate свопов сможете найти ванильный. А еще сможете построить любую кривую методом bootstrap.

Все статьи этой серии:
Стоимость денег, типы процентов, дисконтирование и форвардные ставки. Ликбез для гика, Ч.1
Облигации: купонные и бескупонные, расчет доходности. Ликбез для гика, Ч.2
Облигации: оценка рисков и примеры использования. Ликбез для гика, Ч.3
Как банки берут друг у друга в долг. Плавающие ставки, процентные свопы. Ликбез для гика, Ч. 4
Построение кривой дисконтирования. Ликбез для гика, Ч. 5
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru