🧑‍💻 Код
August 29

Разработка стохастического осциллятора

Разработка стохастического осциллятора: Функциональный подход к измерению импульса

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

Что делает стохастик особенным?

Стохастический осциллятор — это не просто индикатор, это целая философия:

  • Измеряет импульс — положение цены относительно диапазона
  • Работает в диапазоне 0-100 — идеально для определения перекупленности/перепроданности
  • Имеет две линии — %K (быстрая) и %D (медленная)
  • Визуально интуитивный — легко интерпретировать на графике

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

1. Многоарность для гибкости

Реализовал две версии функции:

;; Версия с периодами по умолчанию
([highs lows closes] (stochastic highs lows closes 14 3))

;; Версия с кастомными периодами
([highs lows closes k-period d-period]
 ;; основная логика
)

Преимущество: Пользователи могут использовать стандартные настройки или кастомизировать под свои стратегии.

2. Локальные функции для ясности

Вместо монолитной функции я использовал локальные функции:

let [; Функция для вычисления %K на позиции i
     calculate-k (fn [i] ...)
     
     ; Функция для вычисления %D (SMA от %K)
     calculate-d (fn [i] ...)]

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

  • Изоляция ответственности — каждая функция делает одну вещь
  • Упрощение тестирования — можно тестировать логику по частям
  • Улучшение читаемости — ясное разделение логики
3. Обработка edge cases

Я предусмотрел специальные случаи:

; Защита от деления на ноль
(when (and window-high window-low (not= window-high window-low))
  (* 100 (/ (- close window-low) (- window-high window-low)))

; Фильтрация nil-значений для %D
k-window (filter some? (subvec k-values start end))

Математическая элегантность

Формула %K поражает простотой и эффективностью:

%K = 100 * (close - lowest_low) / (highest_high - lowest_low)

А %D — это просто сглаживание %K:

%D = SMA(%K, period)

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

1. Проблема: Производительность с большими данными

Использование apply max и apply min на каждом шаге может быть медленным:

window-high (apply max (subvec highs start end))
window-low (apply min (subvec lows start end))

Решение: Для оптимизации можно использовать скользящее окно с обновляемыми экстремумами, но я выбрал читаемость над производительностью для первой версии.

2. Проблема: Обработка идентичных значений

Если high = low в окне, возникает деление на ноль:

; Тест подтверждает обработку этого случая
(let [highs   [100 100 100]
      lows    [100 100 100]
      closes  [100 100 100]
      result  (stochastic highs lows closes)]
  (is (every? nil? (map :k result))) ; Все значения %K = nil
3. Проблема: Синхронизация данных

Три вектора должны быть одинаковой длины и правильно синхронизированы:

; Неявное предположение:
(= (count highs) (count lows) (count closes))

В продакшн-версии нужно добавить явную проверку.

Тестирование: От простого к сложному

Тесты охватывают все аспекты работы индикатора:

Базовый функционал
(testing "Basic stochastic oscillator calculation"
  (let [highs   [100 110 105 120 115]
        lows    [90  95  90  100 105]
        closes  [95  100 98  110 108]
        result  (stochastic highs lows closes)]
    (is (= (count result) 5))
    (is (every? #(contains? % :k) result))
    (is (every? #(contains? % :d) result))))
Граничные случаи
(testing "Vectors with identical values"
  (let [highs   [100 100 100]
        lows    [100 100 100]
        closes  [100 100 100]
        result  (stochastic highs lows closes)]
    (is (every? nil? (map :k result)))
    (is (every? nil? (map :d result)))))
Производительность
(testing "Performance with large dataset"
  (let [large-highs  (vec (repeatedly 1000 #(rand-int 200)))
        large-lows   (vec (repeatedly 1000 #(rand-int 100)))
        large-closes (vec (repeatedly 1000 #(rand-int 150)))
        result       (stochastic large-highs large-lows large-closes)]
    (is (= (count result) 1000))))

Ключевые моменты

  1. Композиция функций — стохастик строится из простых блоков
  2. Обработка edge cases — критически важна для надежности
  3. Гибкость через многоарность — баланс между удобством и настраиваемостью
  4. Локальные функции — мощный инструмент для организации сложной логики
  5. Визуальная природа — некоторые индикаторы лучше воспринимаются графически

Почему стохастик belongs в simple

Несмотря на относительную сложность, стохастик относится к simple потому что:

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

Оптимизации для будущего

Для production-use можно добавить:

  1. Скользящие экстремумы — для O(1) расчета на шаг вместо O(n)
  2. Проверки синхронизации — явные проверки длины векторов
  3. Кеширование результатов — для повторных вычислений
  4. Batch processing — оптимизация для больших исторических данных

Заключение: Элегантность в простоте

Стохастический осциллятор показал мне, что сложные trading-идеи могут иметь простые и элегантные реализации.

Что делает эту реализацию successful:

  • ✅ Чистая функциональная архитектура
  • ✅ Comprehensive обработка edge cases
  • ✅ Гибкий API с разными арностями
  • ✅ Ясное разделение ответственности
  • ✅ Баланс между точностью и производительностью

Финальный вердикт: Стохастик остался в simple как пример того, как сложная концепция может иметь простую и elegant реализацию.

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

А какой ваш любимый осциллятор и почему?