![](https://habrastorage.org/getpro/habr/upload_files/616/1a5/799/6161a5799d9d6c20789f9bb75b96bcb4.jpg)
Всем привет. В своей истории я хотел бы поделиться опытом поиска и анализа московской биржи и историей реализации библиотеки для получения данных из нее. И что по итогу вышло.
Все случилось одним летним тёплым днем. Когда меня озарило, или солнечный удар случился, что надо вложиться в деньги. Пошарив по закромам, я даже нашёл небольшую финансовую подушку с остатками моей стипендии. Отлично, начало положено, теперь надо что-то купить, чтобы стать богатым.
Начало
Обчитавшись книгами, статьями, облазив блоги. Было понятно, так не запрыгнуть и не начать грести деньги лопатой. Можно было остановится и купить ETF/Облигации/Фонды, а большую часть денег остановить в подушке безопасности, пить сок и лежать под пальмой.
Но, лёгкий путь решил не выбирать, так ведь совсем неинтересно.
И на меня сошло озарение. Ведь теперь весь смысл моей жизни, это заставлять компьютер делать то что я захочу. Не зря же я получаю высшее образование по специальности угнетатель ЭВМ.
Так почему бы не заставить машину автоматизировать весь процесс, чтобы я нажал на одну кнопку и получил акции/облигации и другие ценные бумаги по своим предпочтениям.
Встреча
Отправной точкой стала Московская биржа и её api, а если точнее и сухо, то информационно-статистический сервер Московской Биржи / ISS MOEX. 5 слов, а звучит как машина по уничтожению человечества.
На главной странице iss moex предоставлен обширный список запросов без регистрации и смс, без ограничений на вызовы, с менее сносным описанием и все это выдаётся в удобном по выбору формате. Я влюбился ????. Пиши обертку не хочу и анализируй все что душе угодно.
Один из моментов, который я усвоил, за свою маленькую карьеру. Всё уже давно за нас написали, а еще аккуратно задокументировали и уложили с любовью в GitHub, хаха, но нет.
![](https://habrastorage.org/getpro/habr/upload_files/616/a21/26f/616a2126fe2d4894e7f4c6ede02b8073.jpg)
Все ясно, топаем в GitHub с запросом iss moex. Пару часов анализа репозиториев, я понял следующее.
На GitHub выделяются пару лагерей по языкам: Python, Java, Go и C#. Python самый активный лагерь, не удивительно, столько инструментов для анализа. Но, увы хотелось бы что-то сносное на C#. Еще одна причина в копилку, почему я начал этот проект, устранить несправедливость, внеся немного своего говно-кода.
Много заброшенных репозиториев и не удивительно. На странице api биржи, можно подсчитать количество запросов, а именно - 141 и это без учета скрытых запросов, по типу bondization. Для каждого нужно написать класс с запросом, потом еще класс с ответом. Короче говоря, тихий ужас, постараемся так не делать и найти альтернативное решение.
Конструктор путей
Для проблемы, которую описал выше, я нашел/украл решение в виде fluent/dot notation конструктора путей, через методы расширения. Приведу пример. Нужно получить спецификацию инструмента, вызываем по цепочки нужные пути.
Securities().Security("MOEX");
Выделив только уникальные пути, сократим количество запросов до 89. Возвращая только собственный путь, изолируя итоговый URL. Благодаря чему можно реализовать путь любой сложности, а гибкость повышается в разы.
При появлении других запросов, просто добавляем только новые уникальные пути. Самое вкусно то, что не надо запоминать существует ли уже запрос, придумывать имена новым классам и методам, копипастить схожие запросы. Монотонной работы должно быть минимум.
Звучит все сказочно, но у всего есть цена. Тут как с LINQ, большие запросы трудно читать, благо нет вложенности. Если глаз уже пристрелен, такая проблема не значительная.
Объект для всех запросов
Отлично, с путями обошлись малой кровью. Как быть с ответами, все-таки придется писать для каждого запроса свой класс ответа? Мы же не знаем заранее какой у нас путь получиться, это надо еще итоговый путь проверять? Паника.
![](https://habrastorage.org/getpro/habr/upload_files/c95/228/f37/c95228f3761980fe3526404e7d37c41e.jpg)
Без паники, снова топаем на страницу с апи, вызываем парочку запросов и любуемся на ответ в виде сырой html страницы. Уже понимаете куда я веду? Все ответы по структуре одинаковы и имеют вид таблицы. Структура следующая:
![В именах колонки указан так же тип данных В именах колонки указан так же тип данных](https://habrastorage.org/getpro/habr/upload_files/2e5/5bb/127/2e55bb127c73d0036a511eebdbd8e740.jpg)
В структуру, которую описал выше, вписывается любой ответ от московской биржи. Так зачем писать множество подобных ответов, придумывать сотни схожих имен для класса ответа? Честно, я не знаю.
А еще в C# существует один козырь, с помощью которого, подобную структуру будет еще проще описать, но его я оставил на десерт.
База
Ну, наконец-то, душные главы прошли. Одна болтовня и сладкие речи, а результата нет. Пора писать код и оценивать результат.
Начнем. Нужен класс, который будет оперировать с URL. В URL есть 3 части которые будут динамически изменяться. Это пути, параметры и расширение.
Для путей и запросов добавим коллекции
List<string> Paths
IDictionary<string, string> Queries
Создадим методы, которые добавляют путь, параметр в собственные коллекции.
public void AddPath(string path) => Paths.Add(path)
public void AddQuery(KeyValuePair<string, string> query) => Queries.Add(query)
Отлично. Теперь создадим статическую обертку-расширение, в которой будет вызывать предыдущие методы.
public static IIssRequst Path(this IIssRequst request, string path)
public static IIssRequst Query(
this IIssRequst request, KeyValuePair<string, string> query)
public static IIssRequst Extension(this IIssRequst request, Extension extension)
Теперь можем делать вот такое непотребство.
var request = new IssRequest().Path("engines");
Кручу верчу
Общая концепция понятна, пора перейти к реализации конструктора путей. Делаем так.
public static IIssRequst Securities(this IIssRequst request)
{
request.AddPath("securities");
return request;
}
и повторяем 88 раз ????.
![](https://habrastorage.org/getpro/habr/upload_files/7da/526/d2f/7da526d2f8e8379ad05ef1b68f396835.jpg)
Так, а теперь серьезно. Еще раз себе напомним, ЭВМ на нас работает, а не мы на нее.
На странице со списком всех запросов смотрим внимательно. Каждый запрос обернут в </dt>
. Остается спарсить все запросы, разбить полные пути на массив и из них выделить только уникальные. Берем в руки HtmlAgilityPack и идем в бой.
.SelectNodes("//dt").SelectMany(node => node.InnerText.Split("/")).Distinct();
Чистим от мусора список с путями, генерируем методы, реализацию оставлю за кадром. За минут 5 у нас готовы все запросы и не надо больше годами писать в issue добавить недостающий запрос. Слава автоматизации ????. Теперь можно реализовать любой запрос из списка на iss moex.
Теперь можем делать вот такие выкрутасы.
var request = new IssRequest().Engines().Engine(Engine.Stock).Markets();
var longRequst = new IssRequest().Statistics()
.Engines().Engine(Engine.Stock)
.Markets().Market(StockMarket.Bonds)
.Path("bondization", "SU25084RMFS3");
Ключ от всех запросов
Пора браться за парсинг ответа от московской биржи. Создадим класс, который описывает табличную структуру ответа.
[JsonConverter(typeof(IssResponseJsonConverter))]
public record IssResponse(IDictionary<string, Table> Tables);
public record Table(IEnumerable<Column> Columns, IEnumerable<Row> Rows);
public record Row(IDictionary<string, object> Values);
public record Column(string Name);
Структура та же как я и описывал пару глав назад. Займемся парсингом. Создадим класс для десериализации ответа и унаследуем его от JsonConverter
.
Погорим немного о структуре и парсинге ответа в формате json. Первый объект таблица с уникальным именем. Внутри него 3 объекта metadata, columns, data
. Columns - это массив с именами колонок, Data - массив с данными с привязкой к номеру колонки.
![metadata, прости, но тебя проигнорирую metadata, прости, но тебя проигнорирую](https://habrastorage.org/getpro/habr/upload_files/54b/7a9/3d3/54b7a93d3171d91330e657192587bd6f.jpg)
Структура парсинга следующая: Переходим к таблице, читаем columns, data
, превращаем их в Column
и Row
добавляем в Table
и повторяем так пока не прочитаем все таблицы.
Читаем детей
var metadataJtoken = JToken.Load(reader);
var columnsJtoken = JToken.Load(reader);
var dataJtoken = JToken.Load(reader);
Читаем название колонок.
var columns = JArray.Load(columnsJtoken.First.CreateReader())
.ToObject<IEnumerable<string>>()
.Select(item => new Column(item.ToPascalCase()));
Читаем данные из строк.
var data = JArray.Load(dataJtoken.First.CreateReader())
.ToObject<IEnumerable<IEnumerable<object>>>();
После, связываем название колонки и данные в ней.
var rows = data.Select(data => data
.Zip(columns, (value, column) => new { value, column })
.ToDictionary(item => item.column.Name, item => item.value))
.Select(dic => new Row(dic));
Пропускаем все лишнее и возвращаемся к чтению следующей таблицы.
Теперь при десериализации ответа указываем IssResponse
и IssResponseJsonConverter
сам подтянется к конвертации. Теперь можно читать любой ответ.
var tables = new IssRequest().Engines().Fetch().ToResponse();
var engine = tables["Engines"].Rows[0].Values["Name"];
Это будет работать с любым запросом. Не надо создавать сотни однотипных классов со схожими именами, полями и не нужно их придумывать для каждого ????.
Красота, но можно ли сделать еще лучше? Конечно же! Пристегнитесь мы начинаем погружение.
![](https://habrastorage.org/getpro/habr/upload_files/d20/290/b7f/d20290b7f6a722bdbfd9beccfa7bade6.jpg)
Хочу то, не знаю что
Для тех кто знал, забыл или не знал. В C# есть тип dynamic
. Он на самом деле object
, который умеет обходить проверку типа во время компиляции, а значит можно с ним работать как с любым типом, но вовремя выполнения тип все равно будет проверен.
А значит, можно делать так.
dynamic a = 1;
var sum = a + 3;
С другой стороны есть класс ExpandoObject
- который предоставляет возможности динамически добавлять члены экземпляров.
dynamic @object = new ExpandoObject();
@object.number = 10;
var sum = @object.number + 3;
Самое интересное, что ExpandoObject
наследует IDictionary<string,object>
. Значит нам ничего не запрещает сделать так.
var @object = new ExpandoObject() as IDictionary<string, object>
@object["key"] = "value"
Следите за руками. В одной руке возможность динамически добавлять члены экземпляров, а в другой, объект у которого можно динамически вызывать эти параметры. Сделаем следующую магию.
dynamic @object = new ExpandoObject();
var dictionary = @object as IDictionary<string, object>;
dictionary["key"] = "value";
var value = @object.key;
И это работает, то есть на ходу можно строит класс, который на самом деле словарь ключ/объект. Но пусть это останется магией и волшебством . А еще к ExpandoObject можно привязывать методы.
Ладно, простите, отвлекся. Возвращаемся и вспоминаем, что наш ответ с таблицами - словарь, а еще строки с данными - словарь типа ключ/объект. Так что мешает это все запихнуть в ExpandoObject?
Получаем IssResponse
и для каждой таблицы делаем следующее.
dynamic @object = new ExpandoObject();
@object.Columns = table.Columns;
@object.Rows = table.Rows.Select(row =>
{
dynamic values = new ExpandoObject();
var prop = values as IDictionary<string, object>;
row.Values.ToLookup(item => prop[item.Key] = item.Value);
return values;
}).ToList();
Теперь можно вытащить кролика из шляпы ????.
var response = new IssRequest()
.Engines().Engine(Engine.Stock)
.Markets().Fetch().ToDynamic();
var market = response.Markets.Rows[0].Name;
Хочу все и сразу
Вот и подходим к заключительному этапу. К чему один ответ, если их тысячи? Давайте получим их всех!
Хм, прям всю тысячу сразу? А как?
Брать все сразу записи самоубийство. Было бы здорово, берем ответ по одному, делаем махинации и переходим к следующему ответу. И такой механизм - IEnumerable
. Он предоставляет возможность, одним за другим получать элементы из массива.
С другой стороны, yield с помощью которого сообщаем, что данный блок кода итерируемый. Сохраняем это у себя в голове и держим до реализации итератора.
Теперь давайте создадим фундамент. Для этого вернемся к документации iss moex и узнаем, что есть блок данных который называется Сursor, но на самом деле это своеобразная пагинация.
Где INDEX - текущая страница, но в виде полученного объема данных. Если INDEX равен 500 значит мы уже получили 500 элементов. TOTAL - общее количество данных, а PAGESIZE - количество данных на страницу. Добавляем все 3 поля в класс Сursor.
![](https://habrastorage.org/getpro/habr/upload_files/f70/569/8e3/f705698e3ccfdf69f10a05fd58d4123e.jpg)
Чтобы наш итератор заработал, нам нужны два метода. Метод который который получает данные из текущей страницы. Метод который переходит на следующую страницу с учетом следующего ограничения: (INDEX + PAGESIZE) < TOTAL.
public bool TryNext()
{
if (index + pageSize >= total) return false;
index += pageSize;
return true;
}
public IDictionary<string, Table> Current()
{
var query = new KeyValuePair<string, string>("start", index);
Request.AddQuery(query);
return Request.Fetch().ToResponse();
}
Теперь достаем знания, которые запомнили и делаем следующее.
public IEnumerable<IDictionary<string, Table>> Next()
{
do
{
yield return Current();
} while (TryNext());
}
Добавим ключевое слово yield и вернем IEnumerable. Сообщая то, что метод является итерируемым и его можно перебирать в цикле.
var responses = new IssRequest()
.Path("history/engines/stock/markets/shares/securities/MOEX")
.Fetch()
.ToCursor().Iterator;
foreach (var response in responses)
{
var table = response["History"].Rows.ElementAt(0).Values["Open"];
}
Теперь мы готовы к парсингу Московской Биржи и к базовому анализу. Но это уже другая история.
Прощание
Фух, вроде рассказал самое нужно. Спасибо что прочитали мою эпопею до конца, простите за столь длинное полотно, ужимал материал как мог.
В данной статье была лишь краткая выжимка основных моментов, для более подробного изучения прошу ко мне в GitHub.
Буду рад оказанной помощи, критике и комментариям. На этом прошу откланиваться, спасибо тебе читатель за то, что уделил мне время.
Комментарии (12)
jerd3007
15.07.2022 08:53Спасибо за труд. Легко читается, интересный подход к решению. У джуна горят глаза, значит будут свернуты горы)
Earthsea
15.07.2022 09:46+2Рубль достиг своего максимума к евро за 7 лет, и покупать привязанные к нему активы на московской бирже мягко говоря ну очень нежелательно, получается что покупаем на пике, продаем на дне. Если еще и кредитное плечо использовать, то получится полный набор начинающего инвестора.
Зарубежные активы покупать опасно, если где-то и осталась такая возможность, то очередные санкции или антисанкции перекроют к ним доступ и можно все потерять.
Для себя я решил, что покупка наличных евро - самое то. Пусть даже по 75 рублей при официальном курсе 58. Разбогатеть и приумножить не получится, но если я купил 1500 евро, то я знаю что у меня лежат 1500 евро. Сколько это в рублях сейчас или через год-два-три-пять мне фиолетово, если рубль еще укрепится то я просто куплю больше евро на ту же сумму в рублях.
Почему евро а не доллар? Во-первых, я переезжать именно в США не собираюсь. Если лично вы хотите туда, можно брать доллары, промежуточные конвертации в будущем это лишние потери. Если планируете переезжать в страны еврозоны - берите евро. Другие валюты не так доступны в наших банках, да и планы через несколько лет могут поменяться, поэтому если не в страны еврозоны, то все равно евро, но не доллар. Потому что, во-вторых, евро к доллару сильно упал (точнее доллар вырос, но в нашем контексте это неважно), и выгодно покупать как раз евро - через какое-то время он может начать к доллару расти.
zlat_zlat
15.07.2022 09:50+1Полностью с вами согласен. Плохо одно - помимо рисков физического ограбления, стали ненулевыми риски запрета хранения наличных валют, в результате которого пересечь границу с наличными может оказаться непросто. Недаром для переводов на зарубежные счета лимит подняли аж до миллиона долларов, а наличный вывоз по-прежнему ограничен десятью тысячами.
litos
А потом в мире что-то меняется и весь индекс падает на 30% вместо роста …
urvanov
Или цена на бездумно выбранный ETF FXRB становится равной 0. Всякое бывает. Биржа же.
kuza2000
... и заработали те, кто в шортах)
mbobka
Не-а. Как ныли так и продолжают ныть. Ничего не меняется.