Pull to refresh

Вливаемся в Московскую биржу

Reading time 8 min
Views 21K

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

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

Начало

Обчитавшись книгами, статьями, облазив блоги. Было понятно, так не запрыгнуть и не начать грести деньги лопатой. Можно было остановится и купить ETF/Облигации/Фонды, а большую часть денег остановить в подушке безопасности, пить сок и лежать под пальмой.

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

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

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

Встреча

Отправной точкой стала Московская биржа и её api, а если точнее и сухо, то информационно-статистический сервер Московской Биржи / ISS MOEX. 5 слов, а звучит как машина по уничтожению человечества.

На главной странице iss moex предоставлен обширный список запросов без регистрации и смс, без ограничений на вызовы, с менее сносным описанием и все это выдаётся в удобном по выбору формате. Я влюбился 🔥. Пиши обертку не хочу и анализируй все что душе угодно.

Один из моментов, который я усвоил, за свою маленькую карьеру. Всё уже давно за нас написали, а еще аккуратно задокументировали и уложили с любовью в GitHub, хаха, но нет.

Все ясно, топаем в GitHub с запросом iss moex. Пару часов анализа репозиториев, я понял следующее.

На GitHub выделяются пару лагерей по языкам: Python, Java, Go и C#. Python самый активный лагерь, не удивительно, столько инструментов для анализа. Но, увы хотелось бы что-то сносное на C#. Еще одна причина в копилку, почему я начал этот проект, устранить несправедливость, внеся немного своего говно-кода.

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

Конструктор путей

Для проблемы, которую описал выше, я нашел/украл решение в виде fluent/dot notation конструктора путей, через методы расширения. Приведу пример. Нужно получить спецификацию инструмента, вызываем по цепочки нужные пути.

Securities().Security("MOEX");

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

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

Звучит все сказочно, но у всего есть цена. Тут как с LINQ, большие запросы трудно читать, благо нет вложенности. Если глаз уже пристрелен, такая проблема не значительная.

Объект для всех запросов

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

Без паники, снова топаем на страницу с апи, вызываем парочку запросов и любуемся на ответ в виде сырой html страницы. Уже понимаете куда я веду? Все ответы по структуре одинаковы и имеют вид таблицы. Структура следующая:

В именах колонки указан так же тип данных
В именах колонки указан так же тип данных

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

А еще в 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 раз 😬.

Так, а теперь серьезно. Еще раз себе напомним, ЭВМ на нас работает, а не мы на нее.

На странице со списком всех запросов смотрим внимательно. Каждый запрос обернут в </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, прости, но тебя проигнорирую

Структура парсинга следующая: Переходим к таблице, читаем 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"];

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

Красота, но можно ли сделать еще лучше? Конечно же! Пристегнитесь мы начинаем погружение.

Хочу то, не знаю что

Для тех кто знал, забыл или не знал. В 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.

Чтобы наш итератор заработал, нам нужны два метода. Метод который который получает данные из текущей страницы. Метод который переходит на следующую страницу с учетом следующего ограничения: (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.

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

Tags:
Hubs:
+10
Comments 13
Comments Comments 13

Articles