🧑‍💻 Код
August 25

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

Разработка индикатора RSI на Clojure: Подводные камни и прозрения

В сегодняшней статье хочу поделиться опытом разработки функции rsi — индикатора относительной силы. Это был один из самых интересных и поучительных этапов создания библиотеки Taljure, где теория алготрейдинга встретилась с практикой функционального программирования.

Замысел: Элегантная математика в функциональном стиле

Изначально концепция казалась простой. Формула RSI выглядит элегантно:

RSI = 100 - (100 / (1 + RS))
где RS = (средний рост) / (среднее падение)

Я представлял себе что-то вроде:

(defn simple-rsi [prices]
  (let [diffs (map - (rest prices) (butlast prices))
        gains (filter pos? diffs)
        losses (filter neg? diffs)
        rs (/ (mean gains) (mean losses))]
    (- 100 (/ 100 (inc rs)))))

Наивное предположение: "Это же просто арифметика! Закончу за 15 минут".

Реальность оказалась куда интереснее.

Подводный камень №1: Коварство крайних случаев

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

  • Все цены одинаковые?
  • Цены только растут?
  • Цены только падают?
  • В данных меньше 15 элементов?

Мое первое "прозрение": Хорошая библиотечная функция должна никогда не падать и возвращать предсказуемый результат даже на некорректных данных.

;; Было: 
(defn rsi [prices] ...) ; Могла упасть с ArithmeticException

;; Стало:
(defn rsi [prices period]
  (try
    (when (< (count prices) (inc period)) 
      (throw (ex-info "Недостаточно данных" {...})))
    ;; ... сложная логика с проверками
    (catch Exception e nil))) ; Возвращаем nil при ошибках

Подводный камень №2: Разные интерпретации "среднего" значения

Изначально я использовал простое среднее арифметическое для подсчета среднего роста и падения. Но в процессе тестирования обнаружил, что:

  • Традиционный RSI использует сглаженное среднее (smoothed average)
  • Некоторые реализации используют экспоненциальное скользящее среднее (EMA)
  • Другие подходы используют простое среднее арифметическое

Решение: Я выбрал наиболее распространенный подход с простым средним, но документировал это поведение в коде:

;; Вспомогательная функция для скользящего среднего
moving-avg (fn [coll]
            (when (seq coll) ; Важно: проверка на пустую коллекцию!
              (/ (reduce + 0.0 coll) (count coll)))) ; 0.0 для float-арифметики

Подводный камень №3: Точность вычислений и float-арифметика

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

  • Целочисленное деление в Clojure по умолчанию
  • Накопление ошибок округления при последовательных вычислениях

Фикс: Ясное указание типа чисел:

;; Было:
(/ (reduce + coll) (count coll)) ; Могло возвращать fractions

;; Стало:
(/ (reduce + 0.0 coll) (count coll)) ; Гарантированно float

Подводный камень №4: Производительность на больших данных

Изначальная версия использовала несколько проходов по данным:

(let [diffs (map - (rest prices) prices)
      gains (map #(max % 0) diffs)    ; Первый проход
      losses (map #(max (- %) 0) diffs) ; Второй проход
      avg-gain (mean (take-last period gains)) ; Третий проход
      avg-loss (mean (take-last period losses))] ; Четвертый проход

Оптимизация: сократил до одного прохода для вычисления разниц, но осознанно оставил читаемость важнее микрооптимизации.

Крутейший опыт №1: Многоарность функций (arity)

Обнаружил красоту clojure-подхода к арности:

;; Версия с периодом по умолчанию (14)
([prices] (rsi prices 14))

;; Основная версия
([prices period] ...)

Это позволило сохранить обратную совместимость и удобство использования.

Крутейший опыт №2: Полноценная документация как код

Я впервые так глубоко использовал docstrings для описания:

  • Математической формулы
  • Граничных условий
  • Примеров использования
  • Поведения в особых случаях
"Вычисляет Relative Strength Index (RSI) - индекс относительной силы.
RSI - это индикатор технического анализа, измеряющий силу ценовых движений
и определяющий условия перекупленности/перепроданности.
...
Примеры:
  (rsi [45.0 45.5 46.0 45.75 46.25] 5) => ~72.5
  (rsi (range 10 25)) => 100.0"

Крутейший опыт №3: Изящная обработка исключений

Комбинация try/catch с ex-info дала мощный инструмент:

(catch Exception e
  (println "Ошибка в RSI:" (ex-message e) "\nДанные:" (ex-data e))
  nil)

Теперь при ошибках я получаю не просто stacktrace, а структурированную информацию для отладки.

Итоговый код: Гармония теории и практики

После всех итераций функция превратилась в произведение искусства:

  1. Надежная — обрабатывает все крайние случаи
  2. Документированная — понятная без чтения реализации
  3. Идиоматичная — использует лучшие практики Clojure
  4. Тестируемая — предсказуемое поведение
  5. Расширяемая — легко добавить новые варианты усреднения

Вот что я вынес из этого опыта:

  • 📚 Теория ≠ практике — математические формулы нужно адаптировать к реальному миру
  • 🧪 Крайние случаи важнее — они определяют надежность библиотеки
  • 📝 Документация — это код — хороший docstring экономит часы отладки
  • Производительность vs. читаемость — всегда выбираю читаемость сначала
  • 🎨 Красота в деталях — многоарность, валидация, обработка ошибок

Теперь этот индикатор не просто вычисляет числа — он рассказывает историю о том, как данные превращаются в торговые сигналы.

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

А с какими интересными моментами сталкивались вы при реализации математических алгоритмов?