🧑‍💻 Код
August 28

Разработка индикатора ATR

Разработка индикатора ATR: Пограничный случай между простым и сложным

В этой статье я хочу рассказать о разработке индикатора Average True Range (ATR) — индикатора, который заставил меня пересмотреть границы между "простыми" и "сложными" индикаторами в моей библиотеке Taljure.

Почему ATR — особенный индикатор?

ATR измеряет волатильность рынка, но его реализация содержит несколько интересных особенностей:

  1. Требует структурных данных (бары с high, low, close)
  2. Имеет рекуррентную формулу как EMA
  3. Использует вспомогательную функцию (True Range)
  4. Имеет специальный случай для period=1

Архитектурные решения

1. Вспомогательная функция calculate-tr

Первое важное решение — вынесение расчета True Range в отдельную функцию:

(defn- calculate-tr
  "Вычисляет истинный диапазон (True Range) для одного бара"
  [prev-close bar]
  (let [h (:high bar)
        l (:low bar)
        c (:close bar)
        prev-close (or prev-close c) ; Умное использование or для первого бара
        tr1 (- h l)
        tr2 (Math/abs (- h prev-close))
        tr3 (Math/abs (- l prev-close))]
    (max tr1 tr2 tr3)))

Почему это важно:

  • Изоляция сложности — основная функция atr остается чистой
  • Тестируемостьcalculate-tr можно тестировать отдельно
  • Повторное использование — True Range может пригодиться elsewhere
2. Обработка специального случая period=1

Для period=1 ATR превращается в простой диапазон:

(= 1 period) (->> bars
                 (map #(- (:high %) (:low %)))
                 (map #(Math/abs (double %)))
                 last)

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

3. Рекуррентная формула ATR

Основная формула использует рекуррентное вычисление как EMA:

(reduce (fn [prev tr]
         (double 
           (/ (+ (* prev (dec period)) tr) 
           period)))
       initial-atr
       remaining-trs)

Красота: Та же элегантность, что и в EMA, но с другой математикой.

Почему я поместил ATR в "simple"?

Это был сознательный и неочевидный выбор. Вот мои доводы:

Аргументы ЗА размещение в simple:
  1. Математическая простота — в основе лежит макс(3 значения)
  2. Отсутствие сложных зависимостей — не зависит от других индикаторов
  3. Широкая распространенность — базовый индикатор волатильности
  4. Прозрачность алгоритма — логику легко понять и проверить
Аргументы ПРОТИВ:
  1. Требует структурных данных — в отличие от простых числовых последовательностей
  2. Имеет рекуррентную природу — как "advanced" индикаторы
  3. Нужна вспомогательная функция — усложняет архитектуру
Компромиссное решение:

Разместил ATR в simple, но:

  • Сделал calculate-tr private-функцией
  • Добавил тестирование
  • Подробно документировал особенности

Подводные камни реализации

1. Проблема: Обработка первого бара

Как рассчитать TR для первого бара, если нет предыдущего close?

Решение: Умное использование or:

prev-close (or prev-close c) ; Используем текущий close если предыдущего нет
2. Проблема: Производительность на больших данных

Рекуррентный reduce может быть не оптимальным для очень больших временных рядов.

Решение: Я оставил читаемость, но отметил возможность оптимизации:

;; Для оптимизации можно использовать transient и persistent!
;; но оставил reduce для читаемости
3. Проблема: Валидация структуры баров

Что если бар не содержит нужных ключей?

Решение: Добавил проверки в тестах, но в продакшене нужно больше валидации:

;; В идеале добавить:
(when (or (not (every? #(contains? % :high) bars))
          (not (every? #(contains? % :low) bars))
          (not (every? #(contains? % :close) bars)))
  (throw (Exception. "Некорректная структура баров")))

Тестирование

Тесты для ATR особенно важны из-за сложной логики:

Тест граничных случаев
(testing "Период 1 возвращает абсолютную разницу между максимумом и минимумом"
  (let [bars [{:high 100.0 :low 90.0 :close 95.0}]]
    (is (= 10.0 (atr bars 1)))))
Тест недостаточных данных
(testing "Недостаточное количество баров вызывает исключение"
  (let [bars [{:high 100.0 :low 90.0 :close 95.0}]]
    (is (thrown? IllegalArgumentException (atr bars 2)))))
Тест корректности расчета
(testing "Правильный расчет ATR"
  (let [bars [{:high 50.0 :low 40.0 :close 45.0}
              {:high 55.0 :low 45.0 :close 50.0}]]
    (is (number? (atr bars 2)))
    (is (pos? (atr bars 2)))))

Ключевые особенности

  1. Границы между "simple" и "advanced" размыты — классификация субъективна
  2. Вспомогательные функции — мощный инструмент для управления сложностью
  3. Специальные случаи часто упрощают реализацию
  4. Структурные данные требуют особого подхода к валидации
  5. Рекуррентные формулы универсальны для многих индикаторов

Почему ATR все же отправился в simple

В итоге я решил, что ATR уйдет в simple потому что:

  1. Концептуальная простота — измерение диапазона цен
  2. Отсутствие композиции — не строится на других индикаторах
  3. Прозрачность алгоритма — можно понять без глубокой математики

Заключение: Гибкая классификация вместо жестких правил

Разработка ATR научила меня, что классификация индикаторов — это не жесткая система, а гибкий инструмент.

Что определяет "простоту":

  • ✅ Концептуальная понятность
  • ✅ Отсутствие сложных зависимостей
  • ✅ Простота реализации
  • ✅ Широта применения

ATR проходит по всем этим критериям, несмотря на некоторые технические сложности.

Финал: ATR остался в simple, но с пометкой о его "продвинутых" особенностях в документации.

Исходный код доступен на GitFlic.

А как бы вы классифицировали ATR? Simple или advanced?