Разработка индикатора 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: Коварство крайних случаев
Первое же тестирование на реальных данных показало проблему. Что если:
Мое первое "прозрение": Хорошая библиотечная функция должна никогда не падать и возвращать предсказуемый результат даже на некорректных данных.
;; Было: (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, а структурированную информацию для отладки.
Итоговый код: Гармония теории и практики
После всех итераций функция превратилась в произведение искусства:
- Надежная — обрабатывает все крайние случаи
- Документированная — понятная без чтения реализации
- Идиоматичная — использует лучшие практики Clojure
- Тестируемая — предсказуемое поведение
- Расширяемая — легко добавить новые варианты усреднения
Вот что я вынес из этого опыта:
- 📚 Теория ≠ практике — математические формулы нужно адаптировать к реальному миру
- 🧪 Крайние случаи важнее — они определяют надежность библиотеки
- 📝 Документация — это код — хороший docstring экономит часы отладки
- ⚡ Производительность vs. читаемость — всегда выбираю читаемость сначала
- 🎨 Красота в деталях — многоарность, валидация, обработка ошибок
Теперь этот индикатор не просто вычисляет числа — он рассказывает историю о том, как данные превращаются в торговые сигналы.
Исходный код доступен на GitFlic.
А с какими интересными моментами сталкивались вы при реализации математических алгоритмов?