Germany | Finland | Saint Petersburg | Drive

Расчет индикаторов методом кофеварки

Опубликовано в QLua

Листая форум сайта quik.ru, узнал о существовании ftp сервера компании ARQA. Среди прочего увидел файл indicators.zip, в котором собраны исходные коды индикаторов, чем-то напоминающих встроенные в терминал quik. Отдавая должное автору, написавшему эту громадную кучу текста, в их использовании было отказано.

 

Причина недостаточной кучерявости этих текстов (с моей точки зрения) - очень много букв и неудобство использования. Попробую поделиться своим подходом к расчету индикаторов.

 

Я недавно видел промышленную кофеварку интересного свойства. Она рассчитана на использование в кофейне, где кофе надо наливать много и часто. У нее нет никаких ручек, кнопок и всего остального, только 4 места под чашки. Ставишь чашку на одно из мест - машина осознает это как предложение поработать, мелет порцию зёрен, варит и наливает в чашку. Чашки можно ставить на любое из 4 мест, можно одновременно несколько чашек, можно одну за другой на то же самое место - как удобно.

Представим эту кофемолку в виде 3 составляющих.

  1. Панель для установки чашек (буфер с результатами)
  2. Печка (алгоритм расчета)
  3. Кофемолка (поставщик данных)

С кофе закончили, переходим к индикаторам.

Итак, делим весь механизм на 3 части : поставщик данных (1) для алгоритма расчета (2), сохраняющего рассчитанные данные в таблице результатов (3).

Поставщик данных

Поскольку мы желаем использовать наш алгоритм всегда и всюду (индикатор lua, скрипт lua, любые структуры данных), то поставщик данных должен быть оформлен отдельной пользовательской функцией с фиксированным интерфейсом. Самый простой и вполне достаточный вариант это функция, принимающая в качестве единственного параметра порядковый номер элемента в выборке (или другими словами номер свечи) и возвращающая значение. В случае, если в качестве исходных данных нам предоставлен массив луа с целочисленными индексами с именем source, то функция будет выглядеть так

function get(indx) return source[indx] end

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

function get(indx) return C(indx) end

или ещё проще

get = C

Аналогично пишутся элементарные функции для получения данных с графиков по индексу или из источников данных.

 

Таблица результатов

Что может быть проще и удобнее в Lua в качестве буфера результатов, чем таблицы с целочисленными индексами? Её и назначим. Каждому целочисленному индексу поставим в соответствие рассчитанное значение.

 

Алгоритм

Теперь самое интересное. Вот формула для расчета экспоненциальной скользящей средней

EMA[i] = (EMA[i-1] * (per-1) + 2*X[i]) / (per+1)

где X[i] - это исходное значение для i-й свечи, per - это период скользящей средней.

Очень хочется добиться 4 принципов: 

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

Ок, приступаем. Привожу упрощенный вариант. Надеюсь, Вы в курсе, что такое метатаблицы языка Lua?

Для всех скользящих средних создаём пространство имен  с именем ma. Помещаем в него первую функцию, вычисляющую экспоненциальную скользящую среднюю.

ma =
{
    -- Exponential Moving Average (EMA)
    -- EMA[i] = (EMA[i]-1*(per-1)+2*X[i]) / (per+1)
    -- Параметры:
    -- period - Период скользящей средней
    -- get - функция с одним параметром (номер в выборке), возвращающая значение выборки
    -- Возвращает массив, при обращению к которому будет рассчитываться только необходимый элемент
    -- При повторном обращении будет возвращено уже рассчитанное значение
    ema =
        function(period,get)
            return setmetatable(
{},                        { __index = function(tbl,indx)
                                          if indx == 1 then
                                                 tbl[indx] = get(1)
                                          else
                                                 tbl[indx] = (tbl[indx-1] * (period-1) + 2 * get(indx)) / (period + 1)
                                          end
                                          return tbl[indx]
                                          end
                        })
       end

Что мы тут видим?

Функция ema ничего не рассчитывает. Все что она делает - это возвращает пустую таблицу (буфер), которой задана метатаблица с единственным методом __index. Именно этот метаметод и занимается расчетом экспоненциальной средней.

Занимается он этим в режиме on-demand («по требованию»). Расчет элемента производится в момент попытки его извлечь. Если значение еще не рассчитывалось, то оно вычисляется. Противном случае сразу отдается уже вычисленное ранее значение. Если присмотреться внимательно, то внутри метаметода можно увидеть рекурсивную обработку (в ней скрывается определённый подвох, связанный с ограниченной глубиной стека вызовов функций в lua)

 

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

local data={1,3,5,7,9,2,4,6,8,0}
local s = ma.ema(3, function(i) return data[i] end)
-- Вводим сразу 7 элемент без обращения к предыдущим print(7," ------------------- " .. tostring(s[7]))

-- А теперь все значения
for i=1,#data do
print(i," ------------------- " .. tostring(s[i]))
end

Результат: 

 

7 ------------------- 4.28125

1 ------------------- 1
2 ------------------- 2
3 ------------------- 3.5
4 ------------------- 5.25
5 ------------------- 7.125
6 ------------------- 4.5625
7 ------------------- 4.28125
8 ------------------- 5.140625
9 ------------------- 6.5703125
10 ------------------- 3.28515625

Распрекрасненько. Дешево и очень сердито.

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

 


Усложним задачку. А как рассчитать DEMA?

Формула для расчета DEMA такая: DEMA[i] = 2 * EMA [i] – EMA(EMA[i]).

Тут у нас EMA вычисляется от EMA. На самом деле все ничуть не сложнее. Добавляем в пространство имен ma функцию для расчета DEMA

-- Double Exponential Moving Average
-- DEMA (i) = 2 * EMA (i) – EMA (EMA (i))
-- Параметры:
-- period - Период скользящей средней
-- get - функция с одним параметром (номер в выборке), возвращающая значение выборки
-- Возвращает массив, при обращению к которому будет рассчитываться только необходимый элемент
-- При повторном обращении будет возвращено уже рассчитанное значение
dema =
     function(period,get)          local ema1 = ma.ema(period,get)
         local ema2 = ma.ema(period,function(indx) return ema1[indx] end)
         return setmetatable(                   {},
                  { __index = function(tbl,indx)
                                      tbl[indx] = 2*ema1[indx] - ema2[indx]
                                      return tbl[indx]
                                   end
                   })
     end

Здесь у нас следующая история. Создаем буфер ema1, который предназначен для расчета EMA по исходным данным. После чего создаём буфер ema2, считающийся по данным буфера ema1. Снабжаем этот буфер поставщиком данных. Ну и как водится, пишем алгоритм расчета алгоритма в методе __index метатаблицы. That's all guys!

 


Ну и еще примерчик, последний.


-- Smoothed Moving Average (SMMA)
-- SMMA[i] = (sum(Pi) - SMMA[i-1] + X[i]) / n
-- Параметры:
-- period - Период скользящей средней
-- get - функция с одним параметром (номер в выборке), возвращающая значение выборки
-- Возвращает массив, при обращению к которому будет рассчитываться только необходимый элемент
-- При повторном обращении будет возвращено уже рассчитанное значение
smma =
    function(period,get)         return setmetatable( {},
              { __index = function(tbl,indx)
                                 if indx < period then
                                        return
                                 end
                                 local sum = 0
                                 for i = indx-period+1, indx do
                                       sum = sum + get(indx)
                                 end
                                 if indx ~= period then
                                      sum = sum - tbl[indx-1] + get(indx)
                                 end
                                 tbl[indx] = sum / period
                                 return tbl[indx]
                               end
            })
    end
Комментарии   
# johncarter 26.06.2017 00:10
это все хорошо, но количество свечей более 150 выдает ошибку "C stack overflow"
Ответить | Ответить с цитатой | Цитировать
# admin 26.06.2017 06:46
Верно.

Я упомянул этот подводный камень в статье: в ней скрывается определённый подвох, связанный с ограниченной глубиной стека вызовов функций в lua

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

Такое происходит из-за рекурсии, когда вам необходимо вычислить элемент с номером 200 и при этом элементы с номерами от 1 до 199 не рассчитаны. Рекурсия будет глубиной 200 и стек луа-машины переполнится.

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


Другой (красивый и правильный) способ - использование концевой рекурсии языка луа. Если правильно организовать концевую рекурсию, то глубина стека никогда не переполнится при любой глубине рекурсивных вызовов.
Ответить | Ответить с цитатой | Цитировать
Добавить комментарий


Архив QLua