{"id":14277,"url":"\/distributions\/14277\/click?bit=1&hash=17ce698c744183890278e5e72fb5473eaa8dd0a28fac1d357bd91d8537b18c22","title":"\u041e\u0446\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c \u043b\u0438\u0442\u0440\u044b \u0431\u0435\u043d\u0437\u0438\u043d\u0430 \u0438\u043b\u0438 \u0437\u043e\u043b\u043e\u0442\u044b\u0435 \u0443\u043a\u0440\u0430\u0448\u0435\u043d\u0438\u044f","buttonText":"\u041a\u0430\u043a?","imageUuid":"771ad34a-9f50-5b0b-bc84-204d36a20025"}

Язык Wolfram и результаты конкурса Московской биржи «Лучший Частный Инвестор»

О конкурсе

Каждый год с сентября по декабрь Московская биржа проводит конкурс «Лучший частный инвестор». Принять участие в конкурсе может любой человек. Далее приведено описание с информационной страницы:

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

MOEX

В этой статье мы как раз попытаемся изучить доходности инвесторов с использованием в качестве инструмента для анализа языка Wolfram.

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

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

Формат статьи

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

Ритуал

Для начала как это принято в Wolfram очистим текущий контекст, уменьшим размер истории вычислений и установим рабочую директорию:

ClearAll["`*"]; $HistoryLength = 1; SetDirectory[NotebookDirectory[]];

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

Получение данных

Все результаты конкурса в виде CSV-таблиц располагаются по следующему адресу: ftp://ftp.moex.com/pub/info/stats_contest/2021/. В данном сетевом каталоге есть множество вложенных каталогов, в которых содержатся сделки участников, отсортированные по дате, один общий каталог ftp://ftp.moex.com/pub/info/stats_contest/2021/all/, в котором находятся вообще все сделки всех участников в рамках конкурса и несколько файлов с агрегированными результатами:

  • nomlist.csv — победители
  • result_day.csv — результаты на последний день конкурса
  • trader.csv — список участников

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

URLDownload["ftp://ftp.moex.com/pub/info/stats_contest/2021/result_day.csv", "result_day.csv"] (* Out[] := File[result_day.csv>>]*)

Импорт данных

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

data = <||>;

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

data["all"] = Normal @ Query[All, <| "trader" -> "nik", "broker" -> "diler_name", "type" -> "contype_name", "amount" -> "amount", "volume" -> "vol_rub", "deals" -> "qty", "profit" -> "dohod_rub", "activity" -> "uchastnik_activ" |>] @ Select[Length[#] == 16&] @ ImportString[#, "Dataset", HeaderLines -> 1]& @ StringReplace[#, ";" -> ","]& @ Import["result_day.csv", "Text", CharacterEncoding -> "WindowsCyrillic"]; Dataset[data["all"]]

В данной таблице удалены лишние колонки и для удобства оставлено только имена участников (id удалено).

Результаты

Приступим к статистике. Сначала просто посмотрим сколько всего было уникальных участников без учета типа рынка:

Length @ DeleteDuplicatesBy[#trader&] @ data["all"] (* Out[] := 26053 *)

Соотношение активных и не активных уникальных участников:

Block[{chartData = Map[Length] @ GroupBy[#["activity"]&] @ DeleteDuplicatesBy[#trader&] @ data["all"]}, PieChart[chartData, ChartLabels -> { StringTemplate["Не активные: <*chartData[[1]]*> (<*100*chartData[[1]]/Total[chartData]*>%)"][], StringTemplate["Активные: <*chartData[[2]]*> (<*100*chartData[[2]]/Total[chartData]*>%)"][] }] ]

Теперь посмотрим соотношения по выбранным типам рынков среди активных участников:

Block[{chartData = Map[Length] @ GroupBy[#["type"]&] @ Select[#["activity"] == 1&] @ data["all"]}, PieChart[chartData, ChartLabels -> KeyValueMap[StringTemplate["`1`: `2` (`3`%)"][#1, #2, N[100*#2/Total[chartData], 3]]&] @ chartData] ]

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

data["stock"] = Select[#type == "Фондовый" && #amount > 100000.0 && #activity == 1 && #deals > 0&] @ data["all"]; data["forex"] = Select[#type == "Валютный" && #amount > 100000.0 && #activity == 1 && #deals > 0&] @ data["all"]; data["futures"] = Select[#type == "Срочный" && #amount > 100000.0 && #activity == 1 && #deals > 0&] @ data["all"]; data["active"] = Query[All, <| "trader" -> #[[1, "trader"]], "broker" -> #[[1, "broker"]], "amount" -> #[[1, "amount"]], "volume" -> Total[#[[All, "volume"]]], "deals" -> Total[#[[All, "deals"]]], "profit" -> Total[#[[All, "profit"]]] |>&] @ GroupBy[#trader&] @ Select[#activity == 1 && #amount > 100000.0 && #deals > 0&] @ data["all"]; data["agg"] = Query[All, <| "trader" -> #[[1, "trader"]], "broker" -> #[[1, "broker"]], "amount" -> #[[1, "amount"]], "volume" -> Total[#[[All, "volume"]]], "deals" -> Total[#[[All, "deals"]]], "profit" -> Total[#[[All, "profit"]]] |>&] @ GroupBy[#trader&] @ data["all"];

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

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

Dataset @ Map[Map[EngineeringForm]] @ Map[Map[Quantity[#, "RussianRubles"]&]] @ Query[All, <| "sum" -> Function[Total[#[[All, "amount"]]]], "median" -> Function[Median[#[[All, "amount"]]]], "mean" -> Function[Mean[#[[All, "amount"]]]] |>] @ data

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

Результат по среднему значению уже поинтереснее. Здесь становится хорошо видно, какой большой перевес в свою сторону создает множество неактивных трейдеров и трейдеров со стартовым капиталом меньше 100 000. Для условно «профессионалов», которых мы выделили выше, средний стартовый капитал составляет больше 1 млн. руб. Тогда как с учетом неточных данных он снижается практически в три раза — до 350 тыс. руб. При этом, если бы мы знали точные суммы меньше 100 000 — среднее значение было бы еще ниже.

Интересно так же взглянуть и на распределение стартового капитала между участниками. Построим гистограмму:

Histogram[data[["active", All, "amount"]], {50000}, ImageSize->Large, Frame->True,GridLines->Automatic,PlotTheme->"Detailed" ]

Теперь составим такую же таблицу для объема торгов и рядом же количество сделок:

Dataset @ Map[Map[EngineeringForm]] @ Map[Map[Quantity[#, "RussianRubles"]&]] @ Query[All, <| "sum" -> Function[Total[#[[All, "volume"]]]], "median" -> Function[Median[#[[All, "volume"]]]], "mean" -> Function[Mean[#[[All, "volume"]]]] |>] @ data Dataset @ Query[All, <| "sum" -> Function[Total[#[[All, "deals"]]]], "median" -> Function[Median[#[[All, "deals"]]]], "mean" -> Function[N@Mean[#[[All, "deals"]]]] |>] @ data

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

Length @ Select[#volume == 0&] @ data[["agg"]] (* Out[] := 5256 *)

И теперь самое интересное, суммарные и средние доходности для каждой группы трейдеров:

Dataset @ Map[Map[EngineeringForm]] @ Map[Map[Quantity[#, "RussianRubles"]&]] @ Query[All, <| "sum" -> Function[Total[#[[All, "profit"]]]], "median" -> Function[Median[#[[All, "profit"]]]], "mean" -> Function[Mean[#[[All, "profit"]]]] |>] @ data

Сначала прокомментируем вполне ожидаемые результаты. Средний «доход» по всем активным участникам конкурса составляет -102.5 тыс. руб. Т.е. это около 9% потерь, учитывая что средний стартовый капитал был чуть больше 1.15 млн. руб. А если точнее:

Quantity[100*#profit/#amount, "Percent"]& @ Total @ data[["active", All, {"amount", "profit"}]] (* Out[] := -8.90454% *)

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

А теперь перейдем к доходности на валютном рынке. Там суммарный заработок составил более 20 млн рублей и средний доход более 22 тысяч. Изучим подробнее таблицу с валютными счетами. Сначала отсортируем ее убыванию дохода, а потом по убыванию стартового капитала:

Dataset[#, MaxItems -> 5]& @ SortBy[-#profit&] @ data["forex"] Dataset[#, MaxItems -> 5]& @ SortBy[-#amount&] @ data["forex"]

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

Dataset @ Query[<|"amount" -> "amount", "profit" -> "profit", "percent" -> Function[100*#profit/#amount]|>] @ Total @ Query[ ;; -2, {"profit", "amount"}] @ SortBy[#profit&] @ data["forex"]

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

Эти же выводы можно сделать и по гистограмме, построенной по результатам итоговых доходов (нулевые доходы не учитываются):

Histogram[data[[{"stock", "futures"}, All, "profit"]], {10000}, ImageSize->Large, PlotTheme->"Detailed" ] Histogram[data[["forex", All, "profit"]], {2000}, ImageSize->Large,PlotTheme->"Detailed" ]

По гистограммам выше хорошо видно, что для фондового и срочного рынков есть довольно большая группа участников, которая имеет доход от -80 000 до 30 000 рублей, но большинство участников все же потерпело убытки и располагаются в отрицательной части. На гистограмме для валютного рынка оказалось что есть очень большая группа, которая заработала от -4000 до 6000 рублей. При этом большинство все таки находится по правую сторону, что не может не радовать.
Как показано выше — итоговый доход в процентах для всех активных участников составляет примерно -8.9%. Но нас интересует еще и количество трейдеров, которые по итогам конкурса имели прибыль или убыток. Это говорит не о суммах, используемых на бирже в торгах, а о том насколько успешно применялись торговые стратегии каждым отдельным трейдером. Поэтому сначала вычислим доход в процентах для каждого трейдера, а затем найдем среднее и медианное значение.

Dataset @ Query[All, <| "median" -> Function[Median[100*#[[All, "profit"]]/#[[All, "amount"]]]], "mean" -> Function[Mean[100*#[[All, "profit"]]/#[[All, "amount"]]]] |>] @ data

Здесь все выглядит немного лучше. То есть для активных участников суммарная потеря составляет около 7.7%, а медианная около 8.3%. Но кроме этого давайте также посмотрим на гистограмму с распределением дохода в процентах по участникам:

Histogram[ Query[All, Function[100 * #profit / #amount]] @ data["active"], {5}, Frame -> True, Axes->False, GridLines->Automatic, ImageSize->Large, PlotTheme->"Detailed" ]

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

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

Dataset @ Reverse @ Sort @ Map[Length] @ GroupBy[#broker&] @ data["active"]

Первые три места занимают Тинькофф, ВТБ и Открытие. Непродолжительный поиск показал, что базовые комиссии для первых трех брокеров составляют:

  • Тинькофф Инвестиции — 0.04%
  • ВТБ Капитал Инвестиции — 0.05%
  • АО «Открытие Брокер» — 0.05%

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

Dataset @ Map[Map[EngineeringForm]] @ Map[Map[Quantity[#, "RussianRubles"]&]] @ Query[All, <| "sum" -> Function[Total[#[[All, "profit"]]] - Total[#[[All, "volume"]]] * 0.04/100], "median" -> Function[Median[#[[All, "profit"]]] - Total[#[[All, "volume"]]] * 0.04/100], "mean" -> Function[Mean[#[[All, "profit"]]] - Total[#[[All, "volume"]]] * 0.04/100] |>] @ data

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

Dataset @ Query[All, <| "median" -> Function[Median[100*(#[[All, "profit"]] - #[[All, "volume"]] * 0.04/100)/#[[All, "amount"]]]], "mean" -> Function[Mean[100*(#[[All, "profit"]] - #[[All, "volume"]] * 0.04/100)/#[[All, "amount"]]]] |>] @ data

И распределение:

Histogram[ Query[All, Function[100 * (#profit - #volume * 0.04/100) / #amount]] @ data["active"], {5}, Frame -> True, Axes->False, GridLines->Automatic, ImageSize->Large, PlotTheme->"Detailed" ]

И итоговая диаграмма, на которой отметим тех, кто заработал больше/меньше 0, больше/меньше 5%, и больше/меньше 10%, больше/меньше15%:

PieChart[#, ChartLabels -> Keys[#]]& @ Map[Length] @ GroupBy[Which[ # < -15, "profit < -15%", # < -10, "profit < -10%", # < -5, "profit < -5%", # < 0, "profit < 0%", # < 5, "profit > 0%", # < 10, "profit > 5%", # < 15, "profit > 10%", True, "profit > 15%" ]&] @ Map[Function[100 * (#profit - #volume * 0.04/100) / #amount]] @ data["active"]

На следующем графике показано распределение процента участников с доходом выше некоторого процента. Т.е. по графику ниже например можно понять, что больше 0 заработало только чуть больше 20% участников, больше 10% — только 10% участников, а вот потеряли больше 20% около 40% участников. В общем-то эта кривая может трактовать как функция распределения вероятности для нормального распределения, только в другую сторону. Она показывает какой доход может получить доход в пределах от -100% до 100% на оси x (для конкретного графика), если выбирать случайно любую точку от в диапазоне (0, 100) на оси y:

Block[{values = Map[Function[100 * (#profit - #volume * 0.04/100) / #amount]] @ data["active"]}, ListLinePlot[Table[{p, 100*Length[Select[# > p&] @ values]/Length[values]}, {p, -100, 100}], PlotTheme->"Detailed", ImageSize->Large, PlotStyle->Thick, GridLines->{Range[-100, 100, 10], Range[0, 100, 10]} ] ]

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

Block[{F = Map[Function[100 * (#profit - #volume * 0.04/100) / #amount]] @ data["active"], f}, f = Interpolation @ Table[{p, 100*Length[Select[# > p&] @ F]/Length[F]}, {p, -100, 100, 5}]; Plot[{f[x]/50, -f'[x]}, {x, -100, 100}, PlotTheme->"Detailed", ImageSize->Large, GridLines->{Range[-100, 100, 10], Range[0, 2, 0.1]} ] ]

Заключение

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

0
11 комментариев
Написать комментарий...
Ivan Off
Возможно в статье найдутся ошибки и неточности — обязательно сообщие о них

Букву т потеряли ))

Ответить
Развернуть ветку
Ivan Off

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

Ответить
Развернуть ветку
Kirill Belov
Автор

Что вы имеете ввиду под "эпохой метавселенных" и как это может быть связано с любыми видами инвестиций?

Ответить
Развернуть ветку
Ivan Off

это про Wolfram

Ответить
Развернуть ветку
Kirill Belov
Автор

А могли бы вы развернуть свою мысль? Просто так мне стало еще непонятнее, как вы сравниваете язык программирования с NFT и "метавселенными"? И расскажите пожалуйста что вы имеете ввиду под метавселенными?

Ответить
Развернуть ветку
Ivan Off

Боюсь что трудно объяснить это человеку, перепутавшему Хабр и VC... Ну, типа мета - это тренд для мажоров, а вольф - это тулза для нубов. Как-то так.

Ответить
Развернуть ветку
Kirill Belov
Автор

Хорошо, я вас понял. Я хотел просто чтобы вы привели аргументацию и дали более четкий ответ на мой вопрос, но если не хотите - ничего страшного. Что значит "мета" и почему вы сравниваете тренд и язык программирования мне все еще не ясно и спросил я это с намереньем действительно про это узнать, а не чтобы поспорить.

Ответить
Развернуть ветку
Ivan Off

мета - это

Ответить
Развернуть ветку
Kirill Belov
Автор

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

Ответить
Развернуть ветку
Ivan Off

Когда же вы наконец поймете, что перепутали платформу для публикации. С этой статьей вам надо на Хабр, а не на VC. Уже намекаю-намекаю...

Ответить
Развернуть ветку
Кирилл Васин

иван, вы долбаеб ж)

Ответить
Развернуть ветку
8 комментариев
Раскрывать всегда