Разработка индикатора SMA
Разработка индикатора SMA: От наивной реализации к промышленному решению
В этой статье я хочу показать эволюцию разработки, казалось бы, простой функции — Simple Moving Average (SMA). То, что начиналось как несколько строк математики, превратилось в robust промышленное решение с comprehensive тестированием.
Начало: Наивная реализация
Изначально SMA казался элементарным:
(defn naive-sma [prices period] (->> (partition period 1 prices) (map #(/ (apply + %) period))))
20 минут работы — и готово? Как бы не так! Первые же тесты показали, что реальный мир сложнее.
Этап 1: Обнаружение edge cases
Мои первые тесты выявили шокирующие проблемы:
(naive-sma [] 5) ; NullPointerException! (naive-sma [1 nil 3] 2) ; Тот же NPE (naive-sma [1 2] 3) ; Бессмысленный результат
Инсайт: Математическая корректность ≠ промышленная надежность.
Этап 2: Защитное программирование
Добавил валидацию входных данных:
(defn sma [prices period] ;; Проверка, что входные данные являются последовательностью, не пусты и не содержат nil (when (or (not (sequential? prices)) (empty? prices) (some nil? prices)) (throw (IllegalArgumentException. "Некорректный список цен..."))) ;; Проверка периода (when (or (not (pos-int? period)) (> period (count prices))) (throw (IllegalArgumentException. "Некорректный период..."))) ;; Основная логика...
Этап 3: Борьба с особенностями float-арифметики
Новые тесты выявили проблемы с особых значений:
(sma [1 Double/NaN 3] 2) ; Непонятное поведение (sma [1 ##Inf 3] 2) ; Бесконечности ломали расчеты
;; Проверка на наличие NaN, Infinity или -Infinity (when (some #(or (Double/isNaN %) (Double/isInfinite %)) prices) (throw (IllegalArgumentException. "Некорректный список цен: содержит NaN или Infinity")))
Этап 4: Специальный случай period=1
Оказалось, что при period=1 логика ломается:
(sma [1 2 3] 1) ; Ожидалось: [2.0 3.0], получалось: [1.0 2.0 3.0]
Пришлось добавить специальную обработку:
(if (= period 1) (->> prices (rest) (mapv double)) ;; Основная логика )
Этап 5: Тестирование как дизайн-инструмент
Создал юнит-тест, который стал моим главным дизайн-инструментом:
(deftest sma-math-test (testing "Базовые математические расчеты" (is (= (sma [1 2 3 4 5] 2) [1.5 2.5 3.5 4.5])) (is (= (sma [10 20 30 40] 2) [15.0 25.0 35.0])))) (deftest sma-special-cases-test (testing "Специальные случаи" (testing "Период = 1" (is (= (sma [1 2 3] 1) [2.0 3.0])) (is (= (sma [5.5] 1) [])))))
Этап 6: Тонкая настройка числовой точности
Обнаружились проблемы с разными типами чисел:
(sma [1 2.0 3/1 4N] 2) ; Смешанные типы → неожиданные результаты
Решение — явное приведение к double:
(let [prices (vec (map double prices))] ; Преобразуем цены в вектор значений с плавающей точкой ;; дальнейшие вычисления
Итоговая архитектура
Вот что получилось в результате итераций:
(defn sma "Вычисление простой скользящей средней (SMA)." [prices period] ;; Валидация входных данных (3 уровня проверок) ;; Специальный случай period=1 ;; Основная логика с преобразованием типов ;; Обработка числовой точности )
Ключевые инсайты разработки
- Защитное программирование — библиотечные функции должны быть неубиваемыми
- Явное лучше неявного — явные проверки и преобразования типов
- Тестирование граничных случаев — happy path покрывает только 20% реального использования
- Документация исключений — пользователь должен знать, чего ожидать
- Производительность vs. надежность — сначала надежность, потом оптимизация
Что осталось за кадром
В финальной версии я убрал тесты с очень большими числами и списками из-за проблем с стабильностью в CI/CD, но оставил как комментарии для будущих оптимизаций.
Заключение: Простота как сложная цель
SMA научил меня, что простота пользовательского API требует сложности внутренней реализации. То, что выглядит как элементарная математика, превращается в комплексную инженерную задачу при промышленном использовании.
- ✅ 20 строк математической логики
- ✅ 15 строк валидации и проверок
- ✅ 50+ строк тестов
- ✅ 5 специальных случаев
- ✅ 10+ edge cases
Теперь эта функция готова к работе в реальных торговых системах, а не только в учебных примерах.
Исходный код доступен на GitFlic.
С какими подобными моментами сталкивались вы при разработке "простых" математических функций?