Собери-будущее

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

Скоростная обработка тиков. Постановка задачи.

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

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

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

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

Обработка коллекций может происходить с использованием трех типичных инструментов:
— стандартные циклы (for, while, do)
— цикл foreach
— linq команды

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

Тестовый скрипт

Скрипт получился не очень большой но покрывающий все наши исходные задачи.

            // чтобы набор чисел всегда был одинаков, используем seed
            var rnd = new Random(100);
            
            // стартовый размер массива данных и делитель
            var arrSize = 100000000;
            var div = 1;

            var currSize = arrSize;
            while (currSize > 10)
            {
                div *= 10;
                currSize = arrSize / div;

                // якобы массив тиков
                var arr = new double[currSize];
                
                for (int i = 0; i < arr.Length; i++)
                    arr[i] = rnd.Next(1, 1000);

                var watch = new Stopwatch();
                watch.Start();
                var count = 0.0;

                // якобы перебор свечек  
                for (var i = 0; i < div; i++)
                {
                    count = 0;
                    // сюда будем вставлять команды работы с тиками
                }


                var msg = string.Format("ArrLength: {0}, Loop: {1}, Time: {2}, count: {3}", currSize, div, watch.Elapsed, count);
                Debug.WriteLine(msg);
            }

Прямой обсчет тиков

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

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

Примеры кода для каждого варианта:

    // linq
    count = arr.Sum(v => v > 500 ? v: 0);

    // for
   for (int k = 0; k < arr.Length; k++)
       count += arr[k] > 500 ? arr[k] : 0;
   
    // foreach
    foreach (var item in arr)
        count += item > 500 ? item : 0;

Результаты следующие:

for ArrLength: 3000000, Loop: 100, Time: 00:00:03.9433100, count: 1122845420
ArrLength: 300000, Loop: 1000, Time: 00:00:03.9302639, count: 112374876
ArrLength: 30000, Loop: 10000, Time: 00:00:03.9892026, count: 11198670
ArrLength: 3000, Loop: 100000, Time: 00:00:03.8094046, count: 1111242
ArrLength: 300, Loop: 1000000, Time: 00:00:03.7044354, count: 116344
ArrLength: 30, Loop: 10000000, Time: 00:00:03.5904666, count: 12634
ArrLength: 3, Loop: 100000000, Time: 00:00:02.6527128, count: 2169
foreach ArrLength: 30000000, Loop: 10, Time: 00:00:04.2863756, count: 11237564998
ArrLength: 3000000, Loop: 100, Time: 00:00:04.2342477, count: 1122845420
ArrLength: 300000, Loop: 1000, Time: 00:00:04.2351377, count: 112374876
ArrLength: 30000, Loop: 10000, Time: 00:00:04.1641656, count: 11198670
ArrLength: 3000, Loop: 100000, Time: 00:00:03.7539018, count: 1111242
ArrLength: 300, Loop: 1000000, Time: 00:00:03.6106301, count: 116344
ArrLength: 30, Loop: 10000000, Time: 00:00:03.3811633, count: 12634
ArrLength: 3, Loop: 100000000, Time: 00:00:02.4263865, count: 2169
linq ArrLength: 30000000, Loop: 10, Time: 00:00:10.2037127, count: 11237564998
ArrLength: 3000000, Loop: 100, Time: 00:00:10.1790545, count: 1122845420
ArrLength: 300000, Loop: 1000, Time: 00:00:10.6790103, count: 112374876
ArrLength: 30000, Loop: 10000, Time: 00:00:10.1260764, count: 11198670
ArrLength: 3000, Loop: 100000, Time: 00:00:10.1368777, count: 1111242
ArrLength: 300, Loop: 1000000, Time: 00:00:09.6797452, count: 116344
ArrLength: 30, Loop: 10000000, Time: 00:00:09.5827869, count: 12634
ArrLength: 3, Loop: 100000000, Time: 00:00:16.0744673, count: 2169

Как видно, linq остает почти в 3 раза от других вариантов цикла. При этом у linq какое-то неадекватное увеличение времени выполнения при размере массива 3. Это не ошибка, результат повторяется стабильно. Видимо накладные расходы на использование linq здесь вылезают по полной программе и время резко увеличивается. Так что, хоть и красиво и удобно, но для обработки тиков linq не подходит при прямой работе с тиками. Увы и ах.
for и foreach идут ноздря в ноздрю и между ними выбирать не стоит. Какой вариант удобнее такой и используйте, но если вы патологический кодер, то используйте всегда базовые циклы, ведь они немного быстрее 🙂 .

Обсчет тиков через промежуточные коллекции.

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

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

Примеры кода для каждого варианта:

    // linq без конвертации в другой вид коллекции
    var result = arr.Where(v => v > 500);
    
    // linq с корвертацией в массив
    var result = arr.Where(v => v > 500).ToArray();

    // foreach с созданием отдельного списка
    var result = new List();
    foreach (var item in arr)
    {
        if (item > 500)
            result.Add(item);
    }

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

    // проводим операцию над коллекцией полученной выше одним из способов
    foreach (var item in result)
       count += item;

Результаты следующие:

foreach ArrLength: 10000000, Loop: 10, Time: 00:00:02.4438366, count: 3746495393
ArrLength: 1000000, Loop: 100, Time: 00:00:02.3367783, count: 374321244
ArrLength: 100000, Loop: 1000, Time: 00:00:02.4252638, count: 37297133
ArrLength: 10000, Loop: 10000, Time: 00:00:02.4478711, count: 3756152
ArrLength: 1000, Loop: 100000, Time: 00:00:01.4827645, count: 355438
ArrLength: 100, Loop: 1000000, Time: 00:00:01.7289573, count: 39551
ArrLength: 10, Loop: 10000000, Time: 00:00:01.7987754, count: 2111
linq ArrLength: 10000000, Loop: 10, Time: 00:00:02.9777427, res.count: 4995004
ArrLength: 1000000, Loop: 100, Time: 00:00:02.9503232, res.count: 499069
ArrLength: 100000, Loop: 1000, Time: 00:00:02.9179539, res.count: 49788
ArrLength: 10000, Loop: 10000, Time: 00:00:02.9169527, res.count: 5013
ArrLength: 1000, Loop: 100000, Time: 00:00:02.7192137, res.count: 478
ArrLength: 100, Loop: 1000000, Time: 00:00:02.5827635, res.count: 50
ArrLength: 10, Loop: 10000000, Time: 00:00:02.8403376, res.count: 3
linq array ArrLength: 10000000, Loop: 10, Time: 00:00:03.8770327, count: 3746495393
ArrLength: 1000000, Loop: 100, Time: 00:00:03.8029816, count: 374321244
ArrLength: 100000, Loop: 1000, Time: 00:00:03.8641673, count: 37297133
ArrLength: 10000, Loop: 10000, Time: 00:00:03.9113998, count: 3756152
ArrLength: 1000, Loop: 100000, Time: 00:00:03.2722251, count: 355438
ArrLength: 100, Loop: 1000000, Time: 00:00:03.9661614, count: 39551
ArrLength: 10, Loop: 10000000, Time: 00:00:04.2493606, count: 2111

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

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

Тяжело в учении – легко в бою

В завершение, хотелось проверить как будет работать скоростная обработка тиков в реальных условиях, то есть, в TSLab. Тиковых данных у меня достаточно, засим переходим в реальные полевые условия (можно накачать тики с http://ftp.moex.ru или достать где-то еще). Тестировать буду на простом примере из библиотеки индикаторов RusAlgo. По умолчанию, там используется linq в виду его красоты и удобства написания. Кубик расчета объема всех сделок на покупку и продажу, подойдет как нельзя кстати. Создадим новую версию кубика, с применением стандартных циклов и будем сравнивать скорость обработки всех баров. Для того чтобы время было ощутимое, в коде скрипта завернем цикл, в котором будем многократно повторять обсчет тиков, что даст нам более адекватные цифры для сравнения.

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

Приводить код тестового скрипта не буду, сразу переходим к итогам:

13:39:49.98 : Direct: for: 00:00:14.5599726
13:40:13.65 : Direct: linq: 00:00:23.6646459

Как видим, linq оказался в 2 раза тормознее (ну не совсем в 2, но разница серьезная). Ранее, мы получали еще большую разницу. Почему? Очевидно, что работа с объектами сделок и запрос свойств объектов, дает затраты и эти затраты одинаковы в обоих случаях, но сильно влияют на общую скорость работы.

Давайте для окончательной проверки, возьмем кубик суммирующий объемы всех сделок на покупку/продажу. Его особенность в том, что linq используется правильно, а именно в виде связки

value = trades.Where(trd => trd.Direction == Direction).Sum(trd => trd.Quantity);

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

13:53:32.71 : Direct: linq: 00:00:24.4801047
13:53:08.23 : Direct: for: 00:00:17.3492000

Разрыв стал меньше, но все равно существенный. Если скрипт работает 17 минут вместо 24-х, это заметно.

Выводы

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

Собственно итоги в куче:
- Везде где можно используем стандартные циклы (for,while,do) или foreach. Ускорение до двух раз.
- Если нужна промежуточная коллекция, лучше создать ее в виде списка или массива чем использовать linq. Памяти сожрет больше, скорость будет тоже больше. Ускорение небольшое, но есть, особенно если запрос к коллекции будет не один а много.
- Скоростная обработка тиков это не миф, а реальность 🙂 .

PS: Всем добра. И не стоит заниматься оптимизацией скорости, если не возникла проблема выполнения скриптов в течение минут десяти и более 🙂 .

Родион Скуратовский - ra81
 
 
 


Вы уже сейчас можете начать изучать Видео курс- роботы в TSLab и научиться самому делать любых роботов!
 
Можно записаться на следующий поток ОнЛайн курса "Создание роботов в TSLab без программирования", информацию по которому можно посмотреть тут->
 
Также можете научиться программировать роботов на нашем Видео курсе "Роботы для QUIK на языке Lua"
 
Если же вам не хочется тратить время на обучение, то вы просто можете выбрать уже готовые роботы из тех, что представлены у нас ДЛЯ TSLab, ДЛЯ QUIK, ДЛЯ MT5, ДЛЯ КРИПТОВАЛЮТЫ!
 
Также можете посмотреть совершенно бесплатные наработки для МТ4, Квика, МТ5. Данный раздел также постоянно пополняется.
 
Не откладывайте свой шанс заработать на бирже уже сегодня!