Тренды временных рядов: Как вычислить их направление и силу?

Выявление тренда временного ряда является must have задачей для любого биржевого аналитика. Однако зачастую это непростая задача — многие временные ряды настолько нелинейны и/или зашумлены, что в них не то что нельзя понять направление и силу тренда, но в принципе невозможно обнаружить какую-либо тенденцию.

В этой статье я подробно разберу современные методы выявления трендов, рассмотрю их математические основы и продемонстрирую практическое применение на реальных данных. Мы не будем глубоко рассматривать традиционные методы определения тренда, такие как технический анализ и индикаторы (потому что это работает плохо), и сфокусируемся больше на продвинутых техниках, которые используются профессионалами в крупных аналитических компаниях и хедж-фондах.

Что такое тренд временного ряда и почему его важно определять

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

Правильное определение тренда критически важно по нескольким причинам:

  1. Тренд позволяет понять фундаментальное направление развития исследуемого процесса;
  2. Знание тренда помогает отфильтровать шум и краткосрочные колебания, фокусируясь на долгосрочной перспективе;
  3. Измерение силы тренда дает возможность оценить вероятность его продолжения в будущем;
  4. Комбинирование анализа тренда с другими инструментами позволяет создавать более точные прогностические модели.

Типы трендов на финансовых рынках: восходящий, нисходящий, боковой

Рис. 1: Типы трендов на финансовых рынках: восходящий, нисходящий, боковой

На графике выше довольно простые примеры. Достаточно беглого взгляда чтобы понять с каким трендом мы имеем дело. Однако в реальности такие явные тренды происходят нечасто, а если и происходят, то увидеть их можно уже только уже когда тренд сформировался, «задним числом». Если мы говорим о биржевой торговле, ценность такой информации близка к нулю.

Когда я только начинал работать с временными рядами, я часто недооценивал важность детального анализа тренда, полагаясь на визуальную оценку графиков. Однако опыт показал, что количественная оценка этого компонента может значительно улучшить качество прогнозов и аналитических выводов.

Классификация трендов временных рядов

Прежде чем погружаться в методы определения направления и силы трендов, важно понимать их основные типы и характеристики. Это создаст необходимую теоретическую базу для дальнейшего анализа.

Тренды временных рядов можно классифицировать по нескольким ключевым параметрам. По направлению тренды делятся на:

  • Восходящий (бычий) тренд — характеризуется последовательным повышением значений временного ряда. Математически можно выразить как положительный наклон линии тренда;
  • Нисходящий (медвежий) тренд — обозначает последовательное снижение значений ряда. Соответствует отрицательному наклону линии тренда;
  • Боковой (флэт) тренд — предполагает отсутствие выраженного направления движения, когда значения колеблются вокруг некоторого уровня. Наклон линии тренда близок к нулю.

По характеру изменения тренды можно разделить на:

  • Линейный тренд — изменение ряда происходит с постоянной скоростью. Математически описывается уравнением вида y = ax + b, где a определяет скорость изменения.
  • Экспоненциальный тренд — скорость изменения ряда пропорциональна его текущему значению. Описывается функцией вида y = ae^(bx).
  • Логарифмический тренд — скорость изменения уменьшается с течением времени. Выражается уравнением y = a + b·ln(x).
  • Полиномиальный тренд — описывает более сложную динамику с несколькими точками перегиба. Представляется полиномом степени n: y = a₀ + a₁x + a₂x² + … + aₙxⁿ.
  • Кусочный тренд — состоит из нескольких участков с различными характеристиками, соединенных в точках структурных изменений.

По степени выраженности тренды обычно разделяют на:

  • Явные (сильные) тренды. Выраженная направленная динамика, минимум боковых колебаний, легко распознается на графике.
  • Скрытые (слабо выраженные) тренды. Неявное направление, так как тенденция скрыта за множественными колебаниями данных, сложно отличить от случайного блуждания.

На практике временные ряды редко демонстрируют чистую форму тренда — обычно мы имеем дело с комбинацией различных типов и дополнительными компонентами, такими как сезонность и случайные колебания.

Также тренды принято разделять по временному горизонту. Если мы говорим о финансовых рынках:

  • Краткосрочные тренды (intraday trends). Внутридневные тренды с горизонтом от нескольких минут до нескольких часов. Как правило их используют в торговых стратегиях высокочастотной торговли, скальпинга и дейтрейдинга;
  • Среднесрочные тренды. Это тенденции с горизонтом от нескольких дней до нескольких недель. Как правило, мониторятся в swing-trading стратегиях;
  • Долгосрочные тренды. Тенденции с горизонтом от нескольких месяцев. Внутри таких трендов могут быть значительные колебания и отклонения. Поэтому интерес к ним, в основном, проявляют buy-and-hold инвесторы, позиционные трейдеры и инвест-фонды.

Устойчивость тренда и режимы временных рядов

Важной характеристикой тренда является его устойчивость — способность сохранять направление и силу на протяжении длительного времени. На практике я выделяю следующие режимы устойчивости:

  • Стабильный тренд — сохраняет свои характеристики на протяжении длительного периода времени;
  • Переходный режим — происходит смена направления или силы тренда;
  • Смешанный режим — тренд присутствует, но его характеристики неустойчивы;
  • Неопределенный режим — тренд отсутствует или его невозможно достоверно выделить.

Критически важно не только определить текущее направление тренда, но и оценить его устойчивость, что позволяет судить о вероятности сохранения выявленной тенденции в будущем.

Традиционные методы определения тренда

Начнем с рассмотрения классических подходов к определению тренда. Хотя многие из них имеют очевидные ограничения, понимание их принципов и недостатков создает фундамент для освоения более продвинутых техник.

Визуальный анализ графиков

Самым базовым методом определения тренда является визуальный анализ графика временного ряда. Он включает:

  1. Построение линейного графика временного ряда;
  2. Выявление общего направления движения данных;
  3. Определение локальных максимумов и минимумов;
  4. Оценку наклона воображаемой линии, соединяющей эти экстремумы.

Преимущества этого метода — простота и интуитивность. Недостатки — высокая субъективность и невозможность количественной оценки силы тренда.

Метод скользящих средних

Скользящие средние помогают сгладить краткосрочные колебания и выделить долгосрочный тренд. Принцип их работы заключается в вычислении среднего значения за определенный период, который «скользит» по временному ряду.

Простая скользящая средняя (Simple Moving Average, SMA) для окна размером m вычисляется как:

Формула расчета простой скользящей средней (SMA)

  • где P(t−k) — цена (или значение временного ряда) в моменты времени t,t−1,…,t−m+1 ;
  • m — размер окна (например, 10 дней).

Для выявления тренда часто используют следующие подходы:

  • Анализ наклона самой скользящей средней;
  • Пересечение нескольких скользящих средних с различными периодами (например, пересечение 50-дневной и 200-дневной SMA).

Давайте рассмотрим пример реализации на Python:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
import yfinance as yf

# Загрузим данные акций Tesla за последние 5 лет
ticker = yf.Ticker("TSLA")
data = ticker.history(period="5y")

# Рассчитаем скользящие средние
data['SMA50'] = data['Close'].rolling(window=50).mean()
data['SMA200'] = data['Close'].rolling(window=200).mean()

# Определим сигналы пересечения скользящих средних
data['Signal'] = 0
data.loc[data['SMA50'] > data['SMA200'], 'Signal'] = 1
data.loc[data['SMA50'] < data['SMA200'], 'Signal'] = -1 

# Построим график 
plt.figure(figsize=(14, 7)) 
plt.plot(data['Close'], label='TSLA', alpha=0.5) 
plt.plot(data['SMA50'], label='SMA 50', linewidth=1) 
plt.plot(data['SMA200'], label='SMA 200', linewidth=1.5) 

# Добавим точки пересечения (смены тренда) 
plt.scatter(data[data['Signal'].diff() != 0].index, data[data['Signal'].diff() != 0]['Close'], color='red', s=50, marker='^', label='Смена тренда') 
plt.title('Определение тренда с помощью пересечения скользящих средних') 
plt.xlabel('Дата') 
plt.ylabel('Цена ($)') 
plt.legend() 
plt.grid(True, alpha=0.3) plt.show() 

# Рассчитаем наклон 200-дневной SMA для количественной оценки тренда 
data['SMA200_slope'] = np.nan 
window = 30 # окно для расчета наклона 

for i in range(window, len(data)): 
   x = np.arange(window) 
   y = data['SMA200'].iloc[i-window:i].values 
   slope, _, _, _, _ = stats.linregress(x, y) 
   data.iloc[i, data.columns.get_loc('SMA200_slope')] = slope 

# Нормализуем наклон для удобства интерпретации 
max_abs_slope = data['SMA200_slope'].abs().max()
data['normalized_slope'] = data['SMA200_slope'] / max_abs_slope 

# Вывод статистики по наклону за последние 30 дней 
recent_slope = data['normalized_slope'].iloc[-30:].mean() 
print(f"Средний нормализованный наклон за последние 30 дней: {recent_slope:.4f}") 
print(f"Интерпретация: {'Сильный восходящий тренд' if recent_slope > 0.7 else 'Умеренный восходящий тренд' if recent_slope > 0.3 else 'Слабый восходящий тренд' if recent_slope > 0 else 'Слабый нисходящий тренд' if recent_slope > -0.3 else 'Умеренный нисходящий тренд' if recent_slope > -0.7 else 'Сильный нисходящий тренд'}")

Определение тренда акций Tesla с помощью пересечения скользящих средних

Рис. 2: Определение тренда акций Tesla с помощью пересечения скользящих средних

Средний нормализованный наклон за последние 30 дней: 0.3378
Интерпретация: Умеренный восходящий тренд

Читайте также:  Применение NumPy для финансового анализа

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

Линейная регрессия и метод наименьших квадратов

Линейная регрессия — классический метод для выявления и измерения линейного тренда. Метод наименьших квадратов позволяет найти оптимальную прямую, которая минимизирует сумму квадратов отклонений фактических значений от модельных.

Для оценки статистической значимости выявленного тренда используются p-значения, доверительные интервалы и коэффициент детерминации R².

Рассмотрим пример реализации линейной регрессии для анализа тренда:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
import statsmodels.api as sm
import yfinance as yf

# Загрузим данные индекса S&P 500
sp500 = yf.Ticker("^GSPC")
data = sp500.history(period="2y")

# Подготовка данных для регрессии
data = data.reset_index()
data['t'] = np.arange(len(data))  # временной индекс

# Построение линейной регрессии
X = sm.add_constant(data['t'])  # добавляем константу для свободного члена
model = sm.OLS(data['Close'], X).fit()

# Выводим результаты регрессии
print(model.summary())

# Рассчитаем прогнозные значения
data['trend'] = model.predict(X)

# Визуализация результатов
plt.figure(figsize=(14, 7))
plt.plot(data['Date'], data['Close'], label='S&P 500', alpha=0.7)
plt.plot(data['Date'], data['trend'], label='Линейный тренд', color='red', linewidth=2)

# Добавим доверительные интервалы
pred_conf = model.get_prediction(X).conf_int(alpha=0.05)
plt.fill_between(data['Date'], pred_conf[:, 0], pred_conf[:, 1], color='red', alpha=0.1, label='95% доверительный интервал')

plt.title('Определение тренда S&P 500 с помощью линейной регрессии')
plt.xlabel('Дата')
plt.ylabel('Значение индекса')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# Анализ силы тренда
slope = model.params[1]
p_value = model.pvalues[1]
r_squared = model.rsquared

print(f"Коэффициент наклона (сила тренда): {slope:.4f}")
print(f"p-значение: {p_value:.4f}")
print(f"Коэффициент детерминации (R²): {r_squared:.4f}")

# Интерпретация результатов
print("\nИнтерпретация:")
if p_value < 0.05:
    print("Тренд статистически значим (p < 0.05)") if slope > 0:
        strength = abs(slope) / (data['Close'].max() - data['Close'].min()) * len(data) * 100
        print(f"Восходящий тренд. Относительная сила: {strength:.2f}%")
    else:
        strength = abs(slope) / (data['Close'].max() - data['Close'].min()) * len(data) * 100
        print(f"Нисходящий тренд. Относительная сила: {strength:.2f}%")
else:
    print("Тренд статистически незначим (p >= 0.05)")

print(f"Качество модели: {'Высокое' if r_squared > 0.7 else 'Среднее' if r_squared > 0.3 else 'Низкое'}")

Определение тренда индекса SP500 с помощью линейной регрессии

Рис. 3: Определение тренда индекса SP500 с помощью линейной регрессии

                            OLS Regression Results                            
==============================================================================
Dep. Variable:                  Close   R-squared:                       0.854
Model:                            OLS   Adj. R-squared:                  0.854
Method:                 Least Squares   F-statistic:                     2927.
Date:                Fri, 02 May 2025   Prob (F-statistic):          3.94e-211
Time:                        08:24:45   Log-Likelihood:                -3450.1
No. Observations:                 502   AIC:                             6904.
Df Residuals:                     500   BIC:                             6913.
Df Model:                           1                                         
Covariance Type:            nonrobust                                         
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
const       4163.0124     20.868    199.495      0.000    4122.013    4204.012
t              3.9013      0.072     54.103      0.000       3.760       4.043
==============================================================================
Omnibus:                      147.731   Durbin-Watson:                   0.051
Prob(Omnibus):                  0.000   Jarque-Bera (JB):              363.019
Skew:                          -1.492   Prob(JB):                     1.48e-79
Kurtosis:                       5.907   Cond. No.                         578.
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

Коэффициент наклона (сила тренда): 3.9013
p-значение: 0.0000
Коэффициент детерминации (R²): 0.8541

Интерпретация:
Тренд статистически значим (p < 0.05)
Восходящий тренд. Относительная сила: 94.02%
Качество модели: Высокое

Этот пример демонстрирует не только выявление направления тренда (знак коэффициента наклона), но и оценку его силы и статистической значимости. Относительная сила тренда рассчитывается как отношение абсолютного изменения, вызванного трендом, к общему диапазону колебаний ряда.

Продвинутые методы определения тренда

Традиционные методы имеют серьезные ограничения, особенно при работе с нелинейными трендами и рядами с высокой волатильностью. Рассмотрим более продвинутые техники, которые позволяют преодолеть эти ограничения.

Робастная регрессия (LOWESS, LOESS)

Локально взвешенная регрессия (LOWESS — Locally Weighted Scatterplot Smoothing) — непараметрический метод регрессии, который комбинирует несколько регрессионных моделей для создания гладкой кривой через точки данных. Этот метод устойчив к выбросам и не требует предположений о глобальной форме тренда.

Принцип работы LOWESS:

  1. Для каждой точки данных определяется локальное окно наблюдений;
  2. Точкам в окне назначаются веса в зависимости от их расстояния до целевой точки;
  3. Строится взвешенная регрессия для определения значения тренда в целевой точке.

Реализация LOWESS для анализа тренда в Python:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.nonparametric.smoothers_lowess import lowess
import yfinance as yf

# Загрузим данные биткоина
btc = yf.Ticker("BTC-USD")
data = btc.history(period="2y")

# Применим LOWESS для выделения тренда
# frac определяет размер окна как долю от общего количества наблюдений
frac_values = [0.1, 0.2, 0.5]
plt.figure(figsize=(14, 10))

# Основной график
plt.subplot(2, 1, 1)
plt.plot(data.index, data['Close'], label='Bitcoin', alpha=0.7)

for frac in frac_values:
    # Вычисляем тренд с помощью LOWESS
    filtered = lowess(data['Close'], np.arange(len(data)), frac=frac, it=3, return_sorted=False)
    plt.plot(data.index, filtered, label=f'LOWESS (frac={frac})', linewidth=2)

plt.title('Определение тренда Bitcoin с помощью LOWESS')
plt.ylabel('Цена ($)')
plt.legend()
plt.grid(True, alpha=0.3)

# График производной тренда для оценки силы
plt.subplot(2, 1, 2)

for frac in frac_values:
    # Вычисляем тренд с помощью LOWESS
    filtered = lowess(data['Close'], np.arange(len(data)), frac=frac, it=3, return_sorted=False)
    
    # Вычисляем приближенную производную (скорость изменения тренда)
    derivative = np.gradient(filtered, 5)  # 5 - шаг для сглаживания производной
    
    # Нормализуем для сравнения
    norm_derivative = derivative / np.max(np.abs(derivative))
    
    plt.plot(data.index, norm_derivative, label=f'Производная тренда (frac={frac})')

plt.axhline(y=0, color='black', linestyle='--', alpha=0.3)
plt.title('Сила тренда (нормализованная производная)')
plt.ylabel('Нормализованная скорость изменения')
plt.xlabel('Дата')
plt.legend()
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# Анализ текущего тренда
current_frac = 0.2  # выбираем оптимальное значение frac
filtered = lowess(data['Close'], np.arange(len(data)), frac=current_frac, it=3, return_sorted=False)
derivative = np.gradient(filtered, 5)

# Оценка последних 30 дней
recent_derivative = derivative[-30:]
avg_derivative = np.mean(recent_derivative)
std_derivative = np.std(recent_derivative)

print(f"Средняя скорость изменения тренда за последние 30 дней: {avg_derivative:.2f}")
print(f"Волатильность скорости тренда: {std_derivative:.2f}")

# Интерпретация
if avg_derivative > 3 * std_derivative:
    trend_strength = "Сильный восходящий"
elif avg_derivative > std_derivative:
    trend_strength = "Умеренный восходящий"
elif avg_derivative > -std_derivative:
    trend_strength = "Нейтральный (флэт)"
elif avg_derivative > -3 * std_derivative:
    trend_strength = "Умеренный нисходящий"
else:
    trend_strength = "Сильный нисходящий"

print(f"Текущий тренд: {trend_strength}")

Определение трендов в котировках Bitcoin методом LOWESS

Рис. 4: Определение трендов в котировках Bitcoin методом LOWESS

Средняя скорость изменения тренда за последние 30 дней: -39.61
Волатильность скорости тренда: 0.48
Текущий тренд: Сильный нисходящий

Преимущество LOWESS перед простыми скользящими средними и линейной регрессией заключается в способности адаптироваться к локальной структуре данных и выявлять нелинейные тренды. Кроме того, метод устойчив к выбросам и не требует спецификации параметрической формы тренда.

Фильтр Ходрика-Прескотта

Фильтр Ходрика-Прескотта (HP) — это метод декомпозиции временного ряда, который отделяет долгосрочный тренд от циклической компоненты, содержащей краткосрочные колебания и шум. Метод основан на минимизации следующей целевой функции:

Формула фильтра Ходрика-Прескотта

или более кратко:

Формула фильтра Ходрика-Прескотта

где:

  • y(t) — исходный временной ряд в момент времени t(i)
  • y(тренд, t) — оцененное значение тренда для момента t(i)
  • lambda — параметр сглаживания, управляющий балансом между точностью аппроксимации исходного ряда и гладкостью получаемого тренда.

Чем выше значение lambda, тем более гладким будет тренд. Типичные значения lambda:

  • 100 для годовых данных;
  • 1600 для квартальных данных;
  • 14400 для месячных данных;
  • 129600 для недельных данных;
  • 6553600 для дневных данных.

Вот как можно реализовать в Python фильтр Ходрика-Прескотта для анализа тренда:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.filters.hp_filter import hpfilter
import yfinance as yf

# Загрузим данные нефти Brent
oil = yf.Ticker("BZ=F")
data = oil.history(period="5y")

# Используем фильтр Ходрика-Прескотта для выделения тренда
cycle, trend = hpfilter(data['Close'], lamb=6553600)  # lambda для дневных данных

# Добавим результаты в датафрейм
data['trend'] = trend
data['cycle'] = cycle

# Рассчитаем первую и вторую производные тренда
data['trend_slope'] = np.gradient(data['trend'], 5)  # Первая производная (скорость)
data['trend_accel'] = np.gradient(data['trend_slope'], 5)  # Вторая производная (ускорение)

# Визуализация результатов
fig, axs = plt.subplots(3, 1, figsize=(14, 15), sharex=True)

# Исходные данные и тренд
axs[0].plot(data.index, data['Close'], label='Brent', alpha=0.6)
axs[0].plot(data.index, data['trend'], label='HP тренд', linewidth=2, color='red')
axs[0].set_title('Цена нефти Brent и её тренд (Фильтр Ходрика-Прескотта)')
axs[0].set_ylabel('Цена ($)')
axs[0].legend()
axs[0].grid(True, alpha=0.3)

# Скорость изменения тренда (первая производная)
axs[1].plot(data.index, data['trend_slope'], label='Скорость тренда', color='green')
axs[1].axhline(y=0, color='black', linestyle='--', alpha=0.5)
axs[1].set_title('Скорость изменения тренда (первая производная)')
axs[1].set_ylabel('Изменение в день ($)')
axs[1].legend()
axs[1].grid(True, alpha=0.3)

# Ускорение тренда (вторая производная)
axs[2].plot(data.index, data['trend_accel'], label='Ускорение тренда', color='purple')
axs[2].axhline(y=0, color='black', linestyle='--', alpha=0.5)
axs[2].set_title('Ускорение тренда (вторая производная)')
axs[2].set_ylabel('Изменение скорости')
axs[2].set_xlabel('Дата')
axs[2].legend()
axs[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# Анализ текущего тренда
recent_data = data.iloc[-60:]  # последние 60 дней
avg_slope = recent_data['trend_slope'].mean()
avg_accel = recent_data['trend_accel'].mean()

print(f"Средняя скорость тренда за последние 60 дней: {avg_slope:.4f} $ в день")
print(f"Среднее ускорение тренда: {avg_accel:.6f} $ в день²")

# Определение силы и направления тренда
slope_std = data['trend_slope'].std()
slope_ratio = avg_slope / slope_std

print("\nАнализ тренда:")
if abs(slope_ratio) < 0.5: trend_type = "Боковой (флэт)" strength = "Слабый" else: if avg_slope > 0:
        trend_type = "Восходящий"
    else:
        trend_type = "Нисходящий"
        
    if abs(slope_ratio) > 2.0:
        strength = "Сильный"
    elif abs(slope_ratio) > 1.0:
        strength = "Умеренный"
    else:
        strength = "Слабый"

print(f"Тип тренда: {trend_type}")
print(f"Сила тренда: {strength} (соотношение скорости к стандартному отклонению: {abs(slope_ratio):.2f})")

# Прогноз направления
if avg_accel > 0.0001:
    direction = "Тренд, вероятно, ускоряется"
elif avg_accel < -0.0001:
    direction = "Тренд, вероятно, замедляется"
else:
    direction = "Тренд движется с постоянной скоростью"

print(f"Прогноз направления: {direction}")

Цена нефти Brent, ее тренд, скорость его изменения и ускорения через фильтр Ходрика-Прескотта

Рис. 5: Цена нефти Brent, ее тренд, скорость его изменения и ускорения через фильтр Ходрика-Прескотта

Средняя скорость тренда за последние 60 дней: -0.0142 $ в день
Среднее ускорение тренда: -0.000008 $ в день²

Анализ тренда:
Тип тренда: Нисходящий
Сила тренда: Слабый (соотношение скорости к стандартному отклонению: 0.69)
Прогноз направления: Тренд движется с постоянной скоростью

Фильтр Ходрика-Прескотта особенно полезен для макроэкономических и финансовых временных рядов, где необходимо отделить долгосрочный тренд от циклических колебаний. Анализ первой и второй производных тренда дает представление о текущей силе тренда и его потенциальных изменениях.

Читайте также:  Основы диверсификации биржевых портфелей

Вейвлет-анализ для выявления трендов

Вейвлет-анализ представляет собой мощный инструмент для обработки сигналов, который позволяет анализировать временные ряды одновременно во временной и частотной областях. В отличие от преобразования Фурье, вейвлеты способны локализовать изменения сигнала во времени, что делает их особенно полезными для выявления трендов в нестационарных временных рядах.

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

Существует несколько типов вейвлет-преобразований:

  1. Непрерывное вейвлет-преобразование (CWT) — позволяет получить высокодетализированную информацию о временном ряде;
  2. Дискретное вейвлет-преобразование (DWT) — более эффективно с вычислительной точки зрения;
  3. Максимальное перекрытие дискретного вейвлет-преобразования (MODWT) — улучшенная версия DWT, инвариантная к сдвигу.

Рассмотрим применение вейвлет-анализа для выявления тренда на реальных данных:

!pip install PyWavelets
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import pywt
import yfinance as yf

# Загрузим данные индекса Nasdaq
nasdaq = yf.Ticker("^IXIC")
data = nasdaq.history(period="5y")['Close']

# Применим вейвлет-декомпозицию
# Выбираем вейвлет 'sym4' (симлет 4-го порядка) и уровень разложения 8
wavelet = 'sym4'
level = 8

# Выполняем многоуровневую вейвлет-декомпозицию
coeffs = pywt.wavedec(data, wavelet, level=level)

# Извлекаем аппроксимирующие коэффициенты (тренд) и детализирующие коэффициенты (шум)
cA = coeffs[0]  # аппроксимирующие коэффициенты (тренд)
cD = coeffs[1:]  # детализирующие коэффициенты (шум различных масштабов)

# Реконструируем сигнал, оставляя только тренд (обнуляем все детализирующие коэффициенты)
trend_coeffs = [cA] + [np.zeros_like(cd) for cd in cD]
trend = pywt.waverec(trend_coeffs, wavelet)

# Обрезаем реконструированный сигнал до длины исходного (может быть немного длиннее)
trend = trend[:len(data)]

# Визуализация результатов
plt.figure(figsize=(14, 10))

# Исходные данные и тренд
plt.subplot(2, 1, 1)
plt.plot(data.index, data, label='Nasdaq', alpha=0.7)
plt.plot(data.index, trend, label=f'Вейвлет-тренд ({wavelet}, уровень {level})', 
         linewidth=2, color='red')
plt.title('Индекс Nasdaq и его тренд (вейвлет-анализ)')
plt.ylabel('Значение индекса')
plt.legend()
plt.grid(True, alpha=0.3)

# Производная тренда для оценки силы
plt.subplot(2, 1, 2)
trend_slope = np.gradient(trend, 20)  # Шаг 20 для сглаживания

plt.plot(data.index, trend_slope, label='Скорость изменения тренда', color='green')
plt.axhline(y=0, color='black', linestyle='--', alpha=0.5)
plt.title('Скорость изменения тренда (производная)')
plt.ylabel('Изменение в день')
plt.xlabel('Дата')
plt.legend()
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# Анализ текущего тренда
window = 60  # Последние 60 дней
recent_slope = trend_slope[-window:]
avg_slope = np.mean(recent_slope)
slope_std = np.std(trend_slope)

# Нормализация силы тренда
normalized_strength = avg_slope / slope_std

print(f"Средняя скорость изменения тренда за последние {window} дней: {avg_slope:.2f}")
print(f"Нормализованная сила тренда: {normalized_strength:.2f}")

# Определение направления и силы тренда
print("\nАнализ текущего тренда:")
if abs(normalized_strength) < 0.5: print("Направление: Боковой (флэт)") print("Сила: Несущественная") else: if normalized_strength > 0:
        print("Направление: Восходящий")
    else:
        print("Направление: Нисходящий")
    
    if abs(normalized_strength) > 2.0:
        print("Сила: Очень сильный")
    elif abs(normalized_strength) > 1.5:
        print("Сила: Сильный")
    elif abs(normalized_strength) > 1.0:
        print("Сила: Умеренный")
    else:
        print("Сила: Слабый")

# Дополнительный анализ: выявление точек разворота тренда
zero_crossings = np.where(np.diff(np.signbit(trend_slope)))[0]
recent_reversals = [data.index[i] for i in zero_crossings if i >= len(data) - 252]  # За последний год

print("\nПоследние развороты тренда:")
for date in recent_reversals[-5:]:  # Последние 5 разворотов
    print(f"- {date.strftime('%Y-%m-%d')}")

Определение тренда и его скорости в котировках индекса Nasdaq с помощью вейвлетов

Рис. 6: Определение тренда и его скорости в котировках индекса Nasdaq с помощью вейвлетов

Средняя скорость изменения тренда за последние 60 дней: -0.14
Нормализованная сила тренда: -0.19

Анализ текущего тренда:
Направление: Боковой (флэт)
Сила: Несущественная

Последние развороты тренда:
- 2024-10-17

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

Эмпирическая модовая декомпозиция (EMD)

Эмпирическая модовая декомпозиция (EMD) — адаптивный метод анализа сигналов, разработанный для работы с нелинейными и нестационарными данными. В отличие от вейвлет-анализа и преобразования Фурье, EMD не требует предварительного выбора базисных функций, что делает его более гибким.

EMD разлагает временной ряд на набор функций внутренних мод (Intrinsic Mode Functions, IMF) и остаточный тренд. Каждая IMF представляет колебания определенного масштаба, а остаток (residual) может рассматриваться как тренд сигнала.

Для анализа трендов с помощью EMD применяется следующий алгоритм:

  1. Разложение временного ряда на IMF и остаток;
  2. Удаление высокочастотных IMF для выделения тренда;
  3. Анализ остатка и низкочастотных IMF для определения направления и силы тренда.

Рассмотрим реализацию на Python с использованием библиотеки PyEMD:

!pip install EMD-signal
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from PyEMD import EMD
import yfinance as yf

# Загрузим данные золота
gold = yf.Ticker("GC=F")
data = gold.history(period="5y")['Close']

# Применим EMD для разложения временного ряда
emd = EMD()
imfs = emd(data.values)
n_imfs = imfs.shape[0]

# Последняя IMF - это тренд (остаток)
trend = imfs[-1]

# Можно также включить предпоследнюю IMF для более детального тренда
# trend = imfs[-2] + imfs[-1]

# Визуализация результатов
plt.figure(figsize=(16, 18))

# Исходный временной ряд
plt.subplot(n_imfs+1, 1, 1)
plt.plot(data.index, data, 'k')
plt.title('Исходные данные цены золота')
plt.ylabel('Цена ($)')

# Все IMF и тренд
for i in range(n_imfs):
    plt.subplot(n_imfs+1, 1, i+2)
    plt.plot(data.index, imfs[i], 'g')
    if i == n_imfs-1:
        plt.title('Тренд (остаток)')
    else:
        plt.title(f'IMF {i+1}')
    plt.ylabel('Амплитуда')

plt.tight_layout()
plt.show()

# Отдельный график сравнения исходных данных с трендом
plt.figure(figsize=(14, 7))
plt.plot(data.index, data, label='Золото', alpha=0.7)
plt.plot(data.index, trend, label='EMD тренд', linewidth=2, color='red')
plt.title('Цена золота и её тренд (EMD)')
plt.xlabel('Дата')
plt.ylabel('Цена ($)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

# Анализ силы и направления тренда
window = 60  # Последние 60 дней
trend_slope = np.gradient(trend, 10)  # 10 - шаг для сглаживания производной
recent_slope = trend_slope[-window:]
avg_slope = np.mean(recent_slope)
slope_std = np.std(trend_slope)

# Нормализация силы тренда
normalized_strength = avg_slope / slope_std

print(f"Средняя скорость изменения тренда за последние {window} дней: {avg_slope:.4f}")
print(f"Нормализованная сила тренда: {normalized_strength:.2f}")

# Определение направления и силы тренда
print("\nАнализ текущего тренда:")
if abs(normalized_strength) < 0.5: print("Направление: Боковой (флэт)") print("Сила: Несущественная") else: if normalized_strength > 0:
        print("Направление: Восходящий")
    else:
        print("Направление: Нисходящий")
    
    if abs(normalized_strength) > 2.0:
        print("Сила: Очень сильный")
    elif abs(normalized_strength) > 1.5:
        print("Сила: Сильный")
    elif abs(normalized_strength) > 1.0:
        print("Сила: Умеренный")
    else:
        print("Сила: Слабый")

# Дополнительный анализ: изменение силы тренда во времени
plt.figure(figsize=(14, 7))
plt.plot(data.index, trend_slope, label='Скорость изменения тренда')
plt.axhline(y=0, color='black', linestyle='--', alpha=0.5)
plt.title('Изменение силы тренда со временем (EMD)')
plt.xlabel('Дата')
plt.ylabel('Скорость изменения')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

Эмпирическая модовая декомпозиция (EMD) цен на золото

Рис. 7: Эмпирическая модовая декомпозиция (EMD) цен на золото

Определение тренда в котировках на золото с помощью метода EMD

Рис. 8: Определение тренда в котировках на золото с помощью метода EMD

Средняя скорость изменения тренда за последние 60 дней: 0.0940
Нормализованная сила тренда: 1.79

Анализ текущего тренда:
Направление: Восходящий
Сила: Сильный

График изменения силы тренда, вычисляемый методом EMD

Рис. 9: График изменения силы тренда, вычисляемый методом EMD

Преимущество EMD заключается в его способности адаптивно разлагать нестационарные сигналы без необходимости выбора базисных функций. Это делает метод особенно полезным для сложных финансовых временных рядов, где структура тренда может меняться со временем.

Машинное обучение для выявления трендов

Современные методы машинного обучения предоставляют мощные инструменты для анализа временных рядов и выявления трендов. Рассмотрим некоторые из наиболее эффективных подходов.

Рекуррентные нейронные сети (LSTM, GRU) для обнаружения тренда

Рекуррентные нейронные сети (RNN), особенно их разновидности LSTM (Long Short-Term Memory) и GRU (Gated Recurrent Unit), способны эффективно моделировать временные зависимости в данных. Эти архитектуры обладают «памятью», что позволяет им учитывать долгосрочные зависимости во временных рядах.

Для выявления тренда с помощью RNN можно использовать следующий подход:

  1. Обучить сеть на историческом временном ряде;
  2. Использовать обученную модель для предсказания будущих значений;
  3. Сгладить предсказания для получения тренда;
  4. Анализировать наклон полученной кривой для определения направления и силы тренда.
Читайте также:  Ad hoc анализ трафика сайтов с помощью SQL и Python

Реализация с использованием LSTM:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
from sklearn.preprocessing import MinMaxScaler
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, LSTM, Dropout
from tensorflow.keras.callbacks import EarlyStopping

# Загрузим данные индекса Dow Jones
dji = yf.Ticker("^DJI")
data = dji.history(period="5y")['Close']

# Масштабирование данных
scaler = MinMaxScaler(feature_range=(0, 1))
scaled_data = scaler.fit_transform(data.values.reshape(-1, 1))

# Подготовка данных для LSTM
def create_dataset(data, time_step=1):
    X, Y = [], []
    for i in range(len(data) - time_step - 1):
        a = data[i:(i + time_step), 0]
        X.append(a)
        Y.append(data[i + time_step, 0])
    return np.array(X), np.array(Y)

time_step = 60  # Используем 60 дней для предсказания следующего
X, y = create_dataset(scaled_data, time_step)

# Разделение на обучающую и тестовую выборки
train_size = int(len(X) * 0.8)
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]

# Подготовка данных для LSTM (требуется 3D формат)
X_train = X_train.reshape(X_train.shape[0], X_train.shape[1], 1)
X_test = X_test.reshape(X_test.shape[0], X_test.shape[1], 1)

# Создание модели LSTM
model = Sequential()
model.add(LSTM(units=50, return_sequences=True, input_shape=(time_step, 1)))
model.add(Dropout(0.2))
model.add(LSTM(units=50, return_sequences=False))
model.add(Dropout(0.2))
model.add(Dense(units=25))
model.add(Dense(units=1))

model.compile(optimizer='adam', loss='mean_squared_error')

# Обучение модели с ранней остановкой
early_stop = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True)
history = model.fit(X_train, y_train, epochs=100, batch_size=32, 
                    validation_data=(X_test, y_test), callbacks=[early_stop], verbose=1)

# Предсказание на всем наборе данных
X_full = X.reshape(X.shape[0], X.shape[1], 1)
predictions = model.predict(X_full)
predictions = scaler.inverse_transform(predictions)

# Сдвиг предсказаний для соответствия исходным датам
train_predictions = np.empty_like(data)
train_predictions[:] = np.nan
train_predictions[time_step:len(predictions)+time_step] = predictions.flatten()

# Сглаживание предсказаний для получения тренда
window_size = 30
trend = pd.Series(train_predictions).rolling(window=window_size, center=True).mean()

# Визуализация результатов
plt.figure(figsize=(14, 7))
plt.plot(data.index, data, label='Dow Jones', alpha=0.6)
plt.plot(data.index, trend, label='LSTM тренд', linewidth=2, color='red')
plt.title('Индекс Dow Jones и его тренд (LSTM)')
plt.xlabel('Дата')
plt.ylabel('Значение индекса')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

# Анализ направления и силы текущего тренда
valid_trend = trend.dropna()
trend_slope = np.gradient(valid_trend.values, 5)  # Шаг 5 для сглаживания производной
recent_window = 60  # Последние 60 дней с данными
recent_slope = trend_slope[-recent_window:]
avg_slope = np.mean(recent_slope)
slope_std = np.std(trend_slope)

# Нормализация силы тренда
normalized_strength = avg_slope / slope_std

print(f"Средняя скорость изменения тренда за последние {recent_window} дней: {avg_slope:.2f}")
print(f"Нормализованная сила тренда: {normalized_strength:.2f}")

# Определение направления и силы тренда
print("\nАнализ текущего тренда:")
if abs(normalized_strength) < 0.5: print("Направление: Боковой (флэт)") print("Сила: Несущественная") else: if normalized_strength > 0:
        print("Направление: Восходящий")
    else:
        print("Направление: Нисходящий")
    
    if abs(normalized_strength) > 2.0:
        print("Сила: Очень сильный")
    elif abs(normalized_strength) > 1.5:
        print("Сила: Сильный")
    elif abs(normalized_strength) > 1.0:
        print("Сила: Умеренный")
    else:
        print("Сила: Слабый")

# Визуализация истории обучения
plt.figure(figsize=(14, 5))
plt.plot(history.history['loss'], label='Обучающая выборка')
plt.plot(history.history['val_loss'], label='Тестовая выборка')
plt.title('История обучения модели')
plt.xlabel('Эпохи')
plt.ylabel('Потери (MSE)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

Определение тренда индекса Dow Jones нейронной сетью LSTM

Рис. 10: Определение тренда индекса Dow Jones нейронной сетью LSTM

Средняя скорость изменения тренда за последние 60 дней: -9.01
Нормализованная сила тренда: -0.89

Анализ текущего тренда:
Направление: Нисходящий
Сила: Слабый

История обучения модели построения трендов

Рис. 11: История обучения модели построения трендов

RNN, в частности LSTM, могут улавливать сложные паттерны во временных рядах, которые трудно выявить традиционными методами. Однако этот подход требует значительных вычислительных ресурсов и большого объема данных для обучения.

Стохастический градиентный спуск (SGD) и авторегрессионные модели

Стохастический градиентный спуск (SGD) в сочетании с онлайн-обучением может быть эффективным инструментом для отслеживания изменяющихся трендов во временных рядах. Эта техника особенно полезна в сценариях, где тренд может меняться со временем.

Авторегрессионные модели, такие как AR (AutoRegressive), MA (Moving Average), ARMA (AutoRegressive Moving Average), могут быть использованы для анализа трендов, особенно если их адаптировать для онлайн-обучения с использованием SGD.

Реализация онлайн-обучения авторегрессионной модели с SGD:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
from sklearn.linear_model import SGDRegressor
from sklearn.preprocessing import StandardScaler

# Загрузим данные индекса S&P 500
sp500 = yf.Ticker("^GSPC")
data = sp500.history(period="5y")['Close']

# Функция для создания признаков для AR модели
def create_features(ts, lags=5):
    df = pd.DataFrame(ts)
    df.columns = ['y']
    
    for lag in range(1, lags + 1):
        df[f'lag_{lag}'] = df['y'].shift(lag)
    
    return df.dropna()

# Создаем признаки (лаги) для модели
lags = 10
features_df = create_features(data, lags=lags)

# Разделение на целевую переменную и признаки
y = features_df['y']
X = features_df.drop('y', axis=1)

# Масштабирование данных
scaler_X = StandardScaler()
scaler_y = StandardScaler()
X_scaled = scaler_X.fit_transform(X)
y_scaled = scaler_y.fit_transform(y.values.reshape(-1, 1)).flatten()

# Онлайн-обучение с SGD
window_size = 252  # Размер окна для обучения (примерно 1 год торговых дней)
trend = np.full(len(data), np.nan)

for i in range(window_size + lags, len(data)):
    # Выбираем окно данных для обучения
    start_idx = i - window_size
    end_idx = i
    
    # Ограничиваем индексы диапазоном данных
    train_start = max(0, start_idx - lags)
    train_end = min(end_idx - lags, len(X_scaled) - 1)

    if train_end <= train_start: continue # Пропускаем, если данные для обучения недоступны X_window = X_scaled[train_start:train_end] y_window = y_scaled[train_start:train_end] # Обучаем модель sgd_model.fit(X_window, y_window) # Горизонт прогнозирования horizon = 30 pred_start = end_idx - lags pred_end = end_idx + horizon - lags # Проверяем, чтобы индексы были в допустимых пределах if pred_start >= len(X_scaled):
        continue

    pred_end = min(pred_end, len(X_scaled))
    if pred_start >= pred_end:
        continue

    X_pred = X_scaled[pred_start:pred_end]

    if X_pred.shape[0] == 0:
        continue  # Пропускаем, если нет данных для предсказания

    # Предсказание
    y_pred = sgd_model.predict(X_pred)

    if len(y_pred) < 2:
        continue  # Нужно минимум 2 точки для вычисления наклона

    # Обратное преобразование масштаба
    y_pred = scaler_y.inverse_transform(y_pred.reshape(-1, 1)).flatten()

    # Вычисляем тренд как линейный наклон предсказаний
    x = np.arange(len(y_pred))
    try:
        slope, intercept = np.polyfit(x, y_pred, 1)
        trend[i] = slope
    except np.linalg.LinAlgError:
        continue  # Игнорируем ошибки polyfit

# Нормализуем тренд для лучшей интерпретации
trend_series = pd.Series(trend, index=data.index)
trend_normalized = trend_series / trend_series.rolling(window=252).std()

# Визуализация результатов
fig, ax1 = plt.subplots(figsize=(14, 7))

# Цена на первой оси
color = 'black'
ax1.set_xlabel('Дата')
ax1.set_ylabel('Цена', color=color)
ax1.plot(data.index, data, color=color, alpha=0.7, label='S&P 500')
ax1.tick_params(axis='y', labelcolor=color)

# Тренд на второй оси
ax2 = ax1.twinx()
color = 'tab:green'
ax2.set_ylabel('Нормализованная сила тренда', color=color)
ax2.plot(trend_normalized.index, trend_normalized, color=color, label='Сила тренда')
ax2.tick_params(axis='y', labelcolor=color)
ax2.axhline(y=0, color='black', linestyle='--', alpha=0.5)

# Пороговые уровни
ax2.axhline(y=2.5, color='green', linestyle='--', alpha=0.5, label='Сильный тренд')
ax2.axhline(y=-2.5, color='green', linestyle='--', alpha=0.5)

# Заголовок и легенда
plt.title('S&P 500 и сила тренда (SGD)')
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')

plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# Анализ текущего тренда
recent_trend = trend_normalized.dropna()[-60:].mean()  # Среднее за последние 60 дней

print(f"Средняя нормализованная сила тренда за последние 60 дней: {recent_trend:.2f}")

# Определение направления и силы тренда
print("\nАнализ текущего тренда:")
if abs(recent_trend) < 0.5: print("Направление: Боковой (флэт)") print("Сила: Несущественная") else: if recent_trend > 0:
        print("Направление: Восходящий")
    else:
        print("Направление: Нисходящий")
    
    if abs(recent_trend) > 2.0:
        print("Сила: Очень сильный")
    elif abs(recent_trend) > 1.5:
        print("Сила: Сильный")
    elif abs(recent_trend) > 1.0:
        print("Сила: Умеренный")
    else:
        print("Сила: Слабый")

# Дополнительная статистика: частота смены тренда
trend_changes = (np.diff(np.sign(trend_normalized.dropna())) != 0).sum()
total_days = len(trend_normalized.dropna())
avg_trend_duration = total_days / (trend_changes + 1)  # +1 чтобы избежать деления на ноль

print(f"\nЧастота смены тренда: {trend_changes} раз за {total_days} дней")
print(f"Средняя продолжительность тренда: {avg_trend_duration:.1f} дней")

Определение силы тренда через SGD

Рис. 12: Определение силы тренда через SGD

Преимущество этого подхода заключается в его способности адаптироваться к изменениям в данных, что особенно важно для финансовых временных рядов, где характеристики тренда могут меняться со временем.

Заключение

В данной статье мы рассмотрели различные методы определения направления и силы тренда во временных рядах — от классических подходов, таких как визуальный анализ и скользящие средние, до продвинутых техник, включая фильтр Ходрика-Прескотта, вейвлет-анализ, эмпирическую модовую декомпозицию и нейронные сети.

Основные выводы, которые можно сделать из нашего исследования:

  1. Не существует универсального метода определения тренда — выбор подхода зависит от характеристик временного ряда, требуемой точности, вычислительных возможностей и конкретной задачи;
  2. Комбинирование различных методов часто дает лучшие результаты — объединение сигналов от нескольких алгоритмов позволяет повысить точность и снизить количество ложных сигналов;
  3. Важно оценивать не только направление, но и силу тренда — это позволяет более точно определить момент входа в рынок и вероятность продолжения тенденции;
  4. Адаптивные методы демонстрируют лучшую эффективность — алгоритмы, способные подстраиваться под изменения в структуре данных, показывают лучшие результаты на нестационарных временных рядах;
  5. Машинное обучение открывает новые возможности — современные методы искусственного интеллекта позволяют выявлять сложные паттерны, которые невозможно обнаружить классическими статистическими подходами.

Стоит помнить, что даже самые продвинутые методы не гарантируют 100% точности в определении тренда. Рынки и другие системы, генерирующие временные ряды, постоянно эволюционируют, адаптируются и подвержены влиянию множества труднопредсказуемых факторов.

В заключение отмечу, что определение тренда — это не самоцель, а инструмент для принятия обоснованных решений. Истинная ценность анализа тренда раскрывается только когда он интегрирован в целостную стратегию управления рисками и принятия решений. Не полагайтесь только на один метод определения тренда. Создавайте комплексные системы, объединяющие различные подходы, и всегда учитывайте фундаментальные факторы, влияющие на изучаемый процесс.