Cython для трейдинга – скорость имеет значение

Python замечательный язык, де факто питон стал OpenSource альтернативой множеству дорогостоящих математических пакетов в научной среде, трейдинг здесь не является исплючением. Я считаю, что трейдинг  – это больше наука, чем искусство. Весь прошлый месяц я делал инструмент, который выполняет тонны расчетов, при чем он это делает на питоне. А как любой интерпретируемый язык – питон достаточно медленный, и прожорливый по части памяти, но в умелых руках его скорости могут достигать скоростей C/C++ языков! Поделюсь небольшим опытом оптимизации расчетов на питоне, а также поделюсь single pass алгоритмом для расчета СКО на Питоне. Сразу скажу, для этого конкретного кода мне удалось добиться увеличения производительности кода в 30 (тридцать) раз!

Вот тот самый код, который считает СКО, среднее и количество в online режиме, это позволяет получить большой выигрыш по производительности и по использованию памяти. Если кто не знает, обычный расчет СКО подразумевает 2 прохода по всем данным, сначала рассчитывается средняя, потом среднеквадратическое отклонение от нее. Более того большинство online алгоритмов расчета СКО не проходили проверку на стабильность, я тупо сравнивал с numpy.std(), и мой алгоритм имеет 100% сходимость с numpy. Хотя алгоритм конечно не мой, я откопал его где-то в интернетах и переписал на питон.

class Stats_py:
    k = 0.0
    Mk = 0.0
    Qk = 0.0
    def __init__(self):
        self.k = 0.0
        
    def add(self, x):
        self.k += 1
        if self.k == 1.0:    
            self.Mk = x
            self.Qk = 0.0
        else:
            d = x - self.Mk
            self.Qk += (self.k-1.0)*d*d/self.k
            self.Mk += d / self.k
    
    def std(self):
        return sqrt(self.Qk/self.k)
    def mean(self):
        return self.Mk
    def count(self):
        return self.k

Делаем простую проверку на скорость в IPython:

values = np.random.random(10000)
s = Stats_py()
for x in values:
    s.add(x)  
print np.std(values), s.std()

%timeit s.add(1)
------
0.289223020474 0.289223020474
100000 loops, best of 3: 2.93 µs per loop

Как вы видите цифры бьються до последнего знака, а выполнение одного вызова функции add() занимает 2.93 микросекунды = 2930 наносекунд. С одной стороны это очень мало, с другой стороны когда нужно совершить миллиарды итераций это время начинает уже сильно ощущаться.

Теперь давайте выжмем из этого кода все на что он способен! Для этого нам нужно взять в руки напильник Cython и научиться с ним работать. IPython Notebook предоставляет отличную возможность писать и отлаживать код Cython прямо в браузере! Для этого есть специальный %%cyton magiс, примеры работы с ним можно посмотреть в разделе Cython Magic Functions Extension.

Оптимизация cython

Для начала не будет мудрствовать и просто скопируем код из питона в Cython, переименуем класс в Stats_cy чтобы не запутаться, можно добавить ключ “–annotate” тогда у вас будет дополнительная возможность анализа кода:

%%cython --annotate

import numpy as np
cimport numpy as np
cimport cython
from libc.math cimport sqrt

class Stats_cy:
    k = 0.0
    Mk = 0.0
    Qk = 0.0
    def __init__(self):
        self.k = 0.0
        
    def add(self, x):
        self.k += 1.0        
        if self.k == 1.0:
            self.Mk = x
            self.Qk = 0.0
        else:
            d = x - self.Mk
            self.Qk += (self.k-1.0)*d*d/self.k
            self.Mk += d / self.k
    
    def std(self):
        return sqrt(self.Qk/self.k)
    def mean(self):
        return self.Mk
    def count(self):
        return self.k

Мы увидим примерно следующую картину, желтыми цветом выделен код который является не оптимиальным (т.е. вызовы Python), а если линия выделена белым – то код является чистым Си кодом, который компилируется и работает с такой же скоростью как любая другая программа на Си. Вы также можете щелкнуть на любую линию, чтобы развернуть Си код который сгенерировал и скомпилировал Cython. Он некрасивый, но с другой стороны его никто не читает :)

cython

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

Stats_py time
100000 loops, best of 3: 2.93 µs per loop
Stats_cy time
100000 loops, best of 3: 2.62 µs per loop

Прирост производительности всего 11%, хотя для простого копи-паста уже недурно.

Cython – вторая космическая скорость

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

Смотрим, результат, желтизны как не бывало! Пришлось добавить “cdef” перед именем класса, а также перенести инициацию глобальных переменных класса в метод __init__(). Обратите на листинг кода строки №17, это чистый Си код, теперь без того ужаса что быт тут раньше (см. первый рисунок строка 15)

cython

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

Stats_py time
100000 loops, best of 3: 2.93 µs per loop
Stats_cy time
10000000 loops, best of 3: 94.6 ns per loop

Прирост скорости 30 раз!

Для перфекционистов, чтобы убрать желтый цвет со строк 24 и 25, нужно добавить в шапку функции следующую строку: “@cython.cdivision(True)” она запрещает проверки на деление на ноль и прочее, что делает питон. Сильно это производительность не повысит, это больше для успокоения души, что ваш код, бывший когда-то питоном, стал чистым Cи.

cython

Прирост скорости на уровне статистической погрешности:

Stats_py time
100000 loops, best of 3: 2.98 µs per loop
Stats_cy time
10000000 loops, best of 3: 94.2 ns per loop

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

import numpy as np
cimport numpy as np
cimport cython
from libc.math cimport sqrt

cdef class Stats_cy:
    cdef double k
    cdef double Mk
    cdef double Qk
    
    def __init__(self):
        self.k = 0.0
        self.Mk = 0.0
        self.Qk = 0.0
    
    @cython.cdivision(True)  
    def add(self, double x):
        self.k += 1.0    
        cdef double d
        if self.k == 1.0:
            self.Mk = x
            self.Qk = 0.0
        else:
            d = x - self.Mk
            self.Qk += (self.k-1.0)*d*d/self.k
            self.Mk += d / self.k
    
    def std(self):
        return sqrt(self.Qk/self.k)
    def mean(self):
        return self.Mk
    def count(self):
        return self.k

Cython резюме

Трейдинг – та область, где очень важны математические вычисления, будь это скользящая средняя или формула Блэка-Шоулза, наши алгоритмы выполняют эти вычисления тысячи и миллионы раз за день. А ведь так, хочется чтобы это работало побыстрее. Cython – является прекрасным инструментом, который позволяет увеличить производительность расчетов в разы.

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

 Если Вам понравилась эта статья, поделитесь ей с друзьями, нажмите одну из красивых кнопочек снизу! Или перейдите по ссылке TOP-100 трейдерских блогов, чтобы мой блог поднялся в рейтинге и о нем узнало больше людей. Спасибо, что читаете мой блог :)

  • reply GreenBear ,

    очень грамотный пост.

    • reply Dmitry ,

      Чем Python лучше C или C++ ? Вопрос даже не в скорости питона, можно ускорить код с помощью pycuda т.е. с помощью GPU, а, наверное в количестве уже готовых кодов в интернете для решения различных задач. Трейдингу чаще свойственно решение неформализованных задач, :-), но это очень сложные пути решения… если говорить о трейдинге начинающих и тех кто “застрял” при решении неформализованной задачи. Если рассматривать трейдинг более приближенный к получению каких-то результатов, то важно наличие готовых кодов, низкая стоимость софта (желательно что-бы полностью бесплатно) и “гибкость” кода и платформы. И, вернемся к нашему вопросу – чем-же Питон лучше C/C++ ? Хотелось-бы услышать мнение человека с опытом. Пока-что мне больше нравится C++, Питон мне показался менее популярным и используется повсеместно меньше C++ поэтому и раздобыть где-то нужную информацию тяжелее.

      • reply ubertrader ,

        Для меня Питон лучше тем, что он позволяет добиться решения задач за меньшее количество часов разработки, и меньшим числом строк кода чем C/C++/C#. Т.е. в исследовательских областях (в т.ч. трейдинге) где 95% задач решается, чтобы получить “отрицательный” результат, и выбросить код. С точки зрения экономии времени и эффективности труда разработчика, одна и та же задача решается на питоне быстрее и эффективнее. Там где на С++ изобретается велосипед, на питоне берется готовая библиотека. Если библиотеки нет, то можно сказать, что один и тот же велосипед быстрее сделать на Python чем на C++. По поводу популярности: в России Питон не так популярен – это факт, а вот на западе он давно уже стал де факто стандартом в научной среде, среди обработчиков данных, а также квантов (если не считать HFT). Конечно если мы рассматриваем трейдинг с точки зрения HFT, тот тут у С++ нет конкуренции, в других областях все не так однозначно.

        • reply Dmitry ,

          Может-быть сделаем какое-то сравнение ? Например списки библиотек доступных для Python’a и C++ ? Для списка каких-то задач – построение графиков, solver’s и т.п. ? По квантовой области, для Pythona я нахожу меньше кодов, чем для C++.

          • reply Dmitry ,

            P.s. не заметил что коды solver’ov на C++ такие уж длинные.

            • reply ubertrader ,

              Не знаю насчет solvers, использовал только в excel, в питоне не использовал вообще. Но 90%+ из квантовых задач покрывается набором библиотек SciPy http://www.scipy.org/about.html . Тут более подробный список: http://www.scipy.org/topical-software.html Что касается меня лично, я вполне обхожусь Numpy + Pandas + Matplotlib (для графиков) + своей библиотечкой на C# (так сложилось исторически просто, слишком много кода переписывать нет ни времени ни желания).

              И еще справедливости ради, нужно сказать, что Python понимает модули на C/C++ как родные.

              • Dmitry ,

                Если подумать, то c++ более развит в части библиотек для решения уравнений. Есть поддержка gpu http://headmyshoulder.github.io/odeint-v2/highlights.html и/или “решатели” которые используют при решении gpu напрямую http://code.google.com/p/opencurrent/ . Для Python таких библиотек может еще не быть. Без gpu возможны варианты что и не решить теоретически ничего, т.к. есть задачи, которые и с многими gpu не хватает ресурсов вычислить. :-)

            • reply Dmitry ,

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

            • reply crast ,

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

            • reply Dmitry ,

              P.s. математические задачи на C/C++ решаются достаточно часто, думаю, есть коды для практически любых уравнений.

              • reply Dmitry ,

                Если кому-то известен форум c++ трейдеров дайте ссылку плиз ? А то, на квантквант не регистрируют (и там правила которые мне не очень нравятся,т.е. прежде чем получить нужный статус на форуме нужно выдать всем нормалоную стратегию) на пауке банят. Может создадим c++ трейдинг команду человек из 5-10 ? Откроем командный сайт и т.п.

                • reply ubertrader ,

                  Мне С++ не интересен, возможно имеет смысл кинуть клич на смарт-лабе, там много трейдеров тусуется, в т.ч. много ХФТ.

                  • reply Dmitry ,

                    Ok

                Leave a comment