Применение Python для среднесрочной торговли на Forex: разработка скринера трендов

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

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

Цель данной работы — создать автоматизированный скринер трендов, основанный на методе сингулярного спектрального анализа (SSA), который используется в институциональном трейдинге для выделения устойчивых трендовых компонент из рыночного шума. Система должна обеспечивать объективную оценку текущих трендов, расчет потенциальных рисков и доходности, а также визуализацию результатов в удобном для принятия торговых решений формате.

Сингулярный спектральный анализ (SSA) в трейдинге

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

  • тренд;
  • циклические составляющие;
  • шум.

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

Метод работает в несколько этапов:

  1. Сначала исходный временной ряд длины N преобразуется в траекторную матрицу размера L×K, где L — длина окна (window length), а K = N-L+1;
  2. Затем к этой матрице применяется сингулярное разложение (SVD), которое выделяет главные компоненты, упорядоченные по убыванию вклада в общую дисперсию ряда. Первые несколько компонент обычно соответствуют основному тренду, следующие могут отражать циклические паттерны, а последние представляют собой шум.

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

Практическое применение SSA в трейдинге требует тщательного выбора параметров. Длина окна L определяет масштаб анализируемых паттернов: слишком короткое окно приведет к избыточной чувствительности к шуму, слишком длинное — к запаздыванию сигналов. Количество используемых компонент для реконструкции тренда также влияет на баланс между гладкостью и реактивностью получаемого сигнала.

Typical Price как основа анализа

Качество торговых сигналов сильно зависит от таймфрейма и анализируемых котировок. Большинство трейдеров по умолчанию использует цены закрытия, однако этот подход игнорирует значительную часть информации о внутридневной динамике. Typical Price, рассчитываемый как среднее арифметическое максимума, минимума и цены закрытия (High + Low + Close)/3, предоставляет более сбалансированное представление о ценовом действии за период.

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

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

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

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

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

  1. Загрузка и предобработка данных;
  2. Анализ трендов с помощью SSA;
  3. Визуализация результатов.

Такое разделение позволяет легко модифицировать отдельные компоненты без влияния на остальную систему.

Выбор анализируемых инструментов основан на ликвидности и важности для глобального валютного рынка. В скринер включены 29 основных валютных пар, покрывающих все major и наиболее торгуемые minor пары: EUR/USD, GBP/USD, USD/JPY, AUD/USD, USD/CAD, USD/CHF, NZD/USD, а также кроссы между основными валютами. Дополнительно включены золото (XAU/USD) и Bitcoin как альтернативные активы, часто демонстрирующие корреляцию с валютными движениями.

Временные параметры системы оптимизированы для среднесрочного анализа:

  • Использование 12-месячной истории обеспечивает достаточный объем данных для стабильной работы SSA-алгоритма, при этом не включая слишком старые данные, которые могут быть нерелевантными в текущих рыночных условиях;
  • Гранулярность в 2 торговых дня позволяет сгладить внутридневный шум, сохраняя при этом достаточную детализацию для выявления среднесрочных трендов.
👉🏻  Этапы разработки биржевых торговых стратегий

Техническая реализация опирается на экосистему Python для научных вычислений:

  • Yahoo Finance API обеспечивает надежный источник исторических данных и притом совершенно бесплатно;
  • NumPy и SciPy предоставляют высокопроизводительные функции для математических операций, включая сингулярное разложение матриц;
  • Matplotlib и Seaborn используются для создания профессиональных графиков с возможностью тонкой настройки внешнего вида.

Реализация скринера динамики валют на Python

Загрузка и предобработка данных

Первый модуль системы отвечает за получение и предварительную обработку ценовых данных. Функция load_market_data() инкапсулирует всю логику загрузки, ресемплирования и расчета Typical Price, предоставляя чистый интерфейс для остальных компонентов системы.

import yfinance as yf
import pandas as pd
pd.set_option('display.expand_frame_repr', False)
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import signal
from datetime import datetime, timedelta
import warnings
warnings.filterwarnings('ignore')

sns.set(style='darkgrid')

def load_market_data(tickers_dict, start_date, end_date, granularity=2):
    """
    Загрузка данных с Yahoo Finance для множественных тикеров
    
    Parameters:
    tickers_dict (dict): Словарь {название: тикер}
    start_date (str): Дата начала в формате 'YYYY-MM-DD'
    end_date (str): Дата окончания в формате 'YYYY-MM-DD'
    granularity (int): Гранулярность в днях (по умолчанию 2)
    
    Returns:
    dict: Словарь с данными для каждого тикера
    """
    market_data = {}
    
    print(f"Загружаем данные с {start_date} по {end_date}")
    print(f"Гранулярность: {granularity} дня(ей)")
    print(f"Тикеров для загрузки: {len(tickers_dict)}")
    print("="*50)
    
    for name, ticker in tickers_dict.items():
        try:
            # Загружаем дневные данные
            stock = yf.Ticker(ticker)
            daily_data = stock.history(start=start_date, end=end_date)
            
            if len(daily_data) == 0:
                print(f"Нет данных для {name} ({ticker})")
                continue
            
            if granularity == 1:
                market_data[name] = daily_data
            else:
                # Ресемплируем данные по заданной гранулярности
                market_data[name] = daily_data.resample(f'{granularity}D').agg({
                    'Open': 'first',
                    'High': 'max',
                    'Low': 'min',
                    'Close': 'last',
                    'Volume': 'sum'
                }).dropna()
            
            # Добавляем Typical Price
            market_data[name]['Typical_Price'] = (
                market_data[name]['High'] + 
                market_data[name]['Low'] + 
                market_data[name]['Close']
            ) / 3
            
            print(f"{name}: {len(market_data[name])} записей")
            
        except Exception as e:
            print(f"Ошибка загрузки {name} ({ticker}): {e}")
    
    print("="*50)
    print(f"Успешно загружено {len(market_data)} тикеров")
    
    return market_data

# Настройки для загрузки данных
start_date = "2024-09-26"
end_date = "2025-09-26"
granularity = 2  #1=таймфрейм 1D, 2=таймфрейм 2D итд 

# Определяем тикеры для анализа
tickers = {
    "AUD/CAD": "AUDCAD=X",
    "AUD/CHF": "AUDCHF=X",
    "AUD/JPY": "AUDJPY=X",
    "AUD/NZD": "AUDNZD=X",
    "CAD/CHF": "CADCHF=X",
    "CAD/JPY": "CADJPY=X",
    "CHF/JPY": "CHFJPY=X",
    "EUR/USD": "EURUSD=X",
    "EUR/AUD": "EURAUD=X",
    "EUR/CAD": "EURCAD=X",
    "EUR/CHF": "EURCHF=X",
    "EUR/GBP": "EURGBP=X",
    "EUR/JPY": "EURJPY=X",
    "EUR/NZD": "EURNZD=X",
    "GBP/USD": "GBPUSD=X",
    "GBP/AUD": "GBPAUD=X",
    "GBP/CAD": "GBPCAD=X",
    "GBP/CHF": "GBPCHF=X",
    "GBP/JPY": "GBPJPY=X",
    "GBP/NZD": "GBPNZD=X",
    "NZD/USD": "NZDUSD=X",
    "NZD/CAD": "NZDCAD=X",
    "NZD/CHF": "NZDCHF=X",
    "NZD/JPY": "NZDJPY=X",
    "USD/CAD": "USDCAD=X",
    "USD/CHF": "USDCHF=X",
    "USD/JPY": "USDJPY=X",
    "XAU/USD": "GC=F",
    "BTC/USD": "BTC-USD"
}

# Загружаем данные
market_data = load_market_data(tickers, start_date, end_date, granularity)
Загружаем данные с 2024-09-26 по 2025-09-26
Гранулярность: 2 дня(ей)
Тикеров для загрузки: 29
==================================================
AUD/CAD: 155 записей
AUD/CHF: 155 записей
AUD/JPY: 155 записей
AUD/NZD: 155 записей
CAD/CHF: 155 записей
CAD/JPY: 155 записей
CHF/JPY: 155 записей
EUR/USD: 155 записей
EUR/AUD: 155 записей
EUR/CAD: 155 записей
EUR/CHF: 155 записей
EUR/GBP: 155 записей
EUR/JPY: 155 записей
EUR/NZD: 155 записей
GBP/USD: 155 записей
GBP/AUD: 155 записей
GBP/CAD: 155 записей
GBP/CHF: 155 записей
GBP/JPY: 155 записей
GBP/NZD: 155 записей
NZD/USD: 155 записей
NZD/CAD: 155 записей
NZD/CHF: 155 записей
NZD/JPY: 155 записей
USD/CAD: 155 записей
USD/CHF: 155 записей
USD/JPY: 155 записей
XAU/USD: 156 записей
BTC/USD: 183 записей
==================================================
Успешно загружено 29 тикеров

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

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

SSA-анализ и автоматическое определение трендов

Ядро системы представлено классом MarketSSAAnalyzer, который реализует полный цикл анализа: от сингулярного разложения временных рядов до классификации трендов и расчета риск-метрик. Архитектура класса позволяет гибко настраивать параметры анализа под различные торговые стратегии.

class MarketSSAAnalyzer:
"""
Анализатор рынка с SSA методом для Typical Price
"""
def __init__(self, market_data):
self.data = market_data.copy()
self.trend_signals = {}
self.reconstructed_trends = {}
def ssa_decomposition(self, series, window_length):
"""Выполняет декомпозицию временного ряда методом SSA"""
N = len(series)
K = N - window_length + 1
# Создание траекторной матрицы
X = np.zeros((window_length, K))
for i in range(K):
X[:, i] = series[i:i + window_length]
# Сингулярное разложение (SVD)
U, S, Vt = np.linalg.svd(X, full_matrices=False)
return U, S, Vt
def ssa_reconstruction(self, U, S, Vt, components, series_length):
"""Реконструирует временной ряд из выбранных SSA компонент"""
window_length = U.shape[0]
K = Vt.shape[1]
# Реконструируем только выбранные компоненты
X_reconstructed = np.zeros((window_length, K))
for i in components:
if i < len(S):
X_reconstructed += S[i] * np.outer(U[:, i], Vt[i, :])
# Диагональное усреднение для получения временного ряда
reconstructed = np.zeros(series_length)
for k in range(series_length):
i_start = max(0, k - K + 1)
i_end = min(window_length - 1, k)
count = 0
total = 0
for i in range(i_start, i_end + 1):
j = k - i
if 0 <= j < K:
total += X_reconstructed[i, j]
count += 1
if count > 0:
reconstructed[k] = total / count
return reconstructed
def ssa_trend_analysis_all(self, window_length=15, trend_components=[0, 1, 2],
threshold_percentile=70, sensitivity_boost=1.5):
"""Анализ трендов с использованием SSA для всех тикеров"""
for name in self.data.keys():
# Используем Typical Price
prices = self.data[name]['Typical_Price'].values
series_length = len(prices)
# Корректируем window_length если он слишком большой
current_window = min(window_length, series_length // 3)
# Выполняем SSA декомпозицию
U, S, Vt = self.ssa_decomposition(prices, current_window)
# Реконструируем тренд из выбранных компонент
trend_reconstructed = self.ssa_reconstruction(
U, S, Vt, trend_components, series_length
)
self.reconstructed_trends[name] = trend_reconstructed
# Анализируем производную тренда для определения направления
trend_derivative = np.gradient(trend_reconstructed)
# Определяем пороги для классификации трендов
threshold = np.percentile(np.abs(trend_derivative), threshold_percentile)
threshold = threshold / sensitivity_boost
# Создаем сигналы трендов
trend_signal = np.zeros(len(prices))
trend_signal[trend_derivative > threshold] = 1   # Восходящий тренд
trend_signal[trend_derivative < -threshold] = -1  # Нисходящий тренд
# Легкое сглаживание
kernel = np.ones(3) / 3
trend_signal = np.convolve(trend_signal, kernel, mode='same')
# Финальная классификация
final_trend = np.zeros(len(prices))
classification_threshold = 0.3 / sensitivity_boost
final_trend[trend_signal > classification_threshold] = 1
final_trend[trend_signal < -classification_threshold] = -1
# Сохраняем результаты
self.data[name]['SSA_Trend'] = trend_reconstructed
self.data[name]['Trend_Signal'] = final_trend
self.trend_signals[name] = final_trend
def calculate_trend_statistics(self, name):
"""Рассчитывает статистику по трендам для конкретного тикера"""
if name not in self.data or name not in self.trend_signals:
return {}
trend_signal = self.trend_signals[name]
prices = self.data[name]['Typical_Price'].values
# Находим отдельные тренды
trend_changes = np.diff(np.concatenate([[0], trend_signal]))
up_trends = []
down_trends = []
current_trend = None
trend_start = 0
all_trend_durations = []
for i in range(len(trend_signal)):
if i < len(trend_changes) and trend_changes[i] != 0:
# Завершаем предыдущий тренд
if current_trend is not None:
trend_end = i - 1
trend_length = trend_end - trend_start + 1
trend_duration_days = trend_length * 2  # для подсчета статистики длительности трендов с granularity = 2
start_price = prices[trend_start]
end_price = prices[trend_end]
trend_return = ((end_price / start_price) - 1) * 100
# Рассчитываем Worst Case Drawdown для завершенного тренда
trend_prices = prices[trend_start:trend_end+1]
max_price_idx = np.argmax(trend_prices)
max_price = trend_prices[max_price_idx]
# Ищем минимум после максимума
if max_price_idx < len(trend_prices) - 1:
min_after_max = np.min(trend_prices[max_price_idx:])
worst_drawdown_pct = ((min_after_max - max_price) / max_price) * 100
worst_drawdown_abs = abs(10000 * (worst_drawdown_pct / 100))
else:
worst_drawdown_pct = 0
worst_drawdown_abs = 0
all_trend_durations.append(trend_duration_days)
trend_data = {
'length': trend_length,
'return': trend_return,
'duration_days': trend_duration_days,
'worst_drawdown_pct': worst_drawdown_pct,
'worst_drawdown_abs': worst_drawdown_abs
}
if current_trend == 1:
up_trends.append(trend_data)
elif current_trend == -1:
down_trends.append(trend_data)
# Начинаем новый тренд
current_trend = trend_signal[i]
trend_start = i
# Завершаем последний тренд
if current_trend is not None:
trend_end = len(trend_signal) - 1
trend_length = trend_end - trend_start + 1
trend_duration_days = trend_length * 2  # granularity = 2
start_price = prices[trend_start]
end_price = prices[trend_end]
trend_return = ((end_price / start_price) - 1) * 100
# Рассчитываем Worst Case Drawdown для последнего тренда
trend_prices = prices[trend_start:trend_end+1]
max_price_idx = np.argmax(trend_prices)
max_price = trend_prices[max_price_idx]
# Ищем минимум после максимума
if max_price_idx < len(trend_prices) - 1:
min_after_max = np.min(trend_prices[max_price_idx:])
worst_drawdown_pct = ((min_after_max - max_price) / max_price) * 100
worst_drawdown_abs = abs(10000 * (worst_drawdown_pct / 100))
else:
worst_drawdown_pct = 0
worst_drawdown_abs = 0
all_trend_durations.append(trend_duration_days)
trend_data = {
'length': trend_length,
'return': trend_return,
'duration_days': trend_duration_days,
'worst_drawdown_pct': worst_drawdown_pct,
'worst_drawdown_abs': worst_drawdown_abs
}
if current_trend == 1:
up_trends.append(trend_data)
elif current_trend == -1:
down_trends.append(trend_data)
# Рассчитываем статистики
avg_trend_duration_days = np.mean(all_trend_durations) if all_trend_durations else 0
avg_worst_drawdown_pct = np.mean([abs(t['worst_drawdown_pct']) for t in up_trends + down_trends]) if (up_trends + down_trends) else 0
avg_worst_drawdown_abs = np.mean([t['worst_drawdown_abs'] for t in up_trends + down_trends]) if (up_trends + down_trends) else 0
avg_worst_drawdown_x30 = avg_worst_drawdown_abs * 30 if avg_worst_drawdown_abs > 0 else 0
avg_historical_abs = np.mean([abs(t['return']) * 100 for t in up_trends + down_trends]) if (up_trends + down_trends) else 0
avg_historical_x30 = avg_historical_abs * 30 if avg_historical_abs > 0 else 0
stats = {
'up_trends_count': len(up_trends),
'down_trends_count': len(down_trends),
'avg_up_return': np.mean([t['return'] for t in up_trends]) if up_trends else 0,
'avg_down_return': np.mean([t['return'] for t in down_trends]) if down_trends else 0,
'avg_trend_duration_days': avg_trend_duration_days,
'avg_worst_drawdown_pct': avg_worst_drawdown_pct,
'avg_worst_drawdown_abs': avg_worst_drawdown_abs,
'avg_worst_drawdown_x30': avg_worst_drawdown_x30,
'avg_historical_abs': avg_historical_abs,
'avg_historical_x30': avg_historical_x30
}
return stats, up_trends, down_trends
def get_current_trend_info(self, name):
"""Получает информацию о текущем тренде"""
if name not in self.data or name not in self.trend_signals:
return {}
trend_signal = self.trend_signals[name]
prices = self.data[name]['Typical_Price'].values
# Текущий тренд (последнее значение)
current_trend_value = trend_signal[-1]
if current_trend_value == 1:
current_trend = "Bull"
elif current_trend_value == -1:
current_trend = "Bear"
else:
current_trend = "Flat"
# Находим начало текущего тренда и его продолжительность
trend_start_price = None
trend_return = 0
current_trend_days = 0
if current_trend != "Flat":
# Ищем начало текущего тренда (идем назад)
trend_start_idx = None
for i in range(len(trend_signal) - 1, -1, -1):
if i == 0 or trend_signal[i] != trend_signal[i-1]:
trend_start_price = prices[i]
trend_start_idx = i
break
if trend_start_price is not None and trend_start_idx is not None:
trend_return = ((prices[-1] / trend_start_price) - 1) * 100
current_trend_days = (len(trend_signal) - trend_start_idx) * 2  # для granularity = 2
return {
'current_trend': current_trend,
'trend_return': trend_return,
'current_trend_days': current_trend_days
}
def calculate_trend_progress(self, name):
"""Рассчитывает прогресс текущего тренда относительно исторических значений"""
if name not in self.data:
return {}
current_trend_info = self.get_current_trend_info(name)
current_trend = current_trend_info['current_trend']
current_return = current_trend_info['trend_return']
if current_trend == "Flat":
return {
'progress_pct': None,
'progress_bar': "Нет тренда",
'progress_vs_avg': None
}
stats, up_trends, down_trends = self.calculate_trend_statistics(name)
if current_trend == "Bull":
historical_returns = [t['return'] for t in up_trends[:-1]] if len(up_trends) > 1 else []
avg_return = stats['avg_up_return']
else:  # Bear
historical_returns = [abs(t['return']) for t in down_trends[:-1]] if len(down_trends) > 1 else []
avg_return = abs(stats['avg_down_return'])
current_return = abs(current_return)
if not historical_returns:
return {
'progress_pct': None,
'progress_bar': "Недостаточно данных",
'progress_vs_avg': None
}
# Прогресс относительно среднего исторического значения
if avg_return > 0:
progress_vs_avg = (current_return / avg_return) * 100
else:
progress_vs_avg = 0
# Прогресс-бар на основе процента от среднего (ограничиваем до 100%)
progress_for_bar = min(progress_vs_avg, 100)  # Максимум 100% для визуализации
# Создаем прогресс-бар
bar_length = 20
filled = int((progress_for_bar / 100) * bar_length)  # 100% = полная шкала
bar = "█" * filled + "░" * (bar_length - filled)
# Для диапазона [min, max]
min_return = min(historical_returns)
max_return = max(historical_returns)
if max_return == min_return:
progress_range = 50  # Средняя позиция если нет диапазона
else:
progress_range = ((current_return - min_return) / (max_return - min_return)) * 100
progress_range = max(0, min(100, progress_range))
progress_info = {
'progress_pct': progress_vs_avg,   # Процент от среднего
'progress_range': progress_range,  # Позиция в диапазоне
'progress_bar': f"|{bar}| {progress_vs_avg:.1f}%",
'current_vs_avg': current_return - avg_return,
'min_historical': min_return,
'max_historical': max_return,
'avg_historical': avg_return
}
return progress_info
def plot_charts(self, tickers_to_plot=None):
"""Визуализация графиков с SSA трендами"""
if tickers_to_plot is None:
tickers_to_plot = list(self.data.keys())
for ticker_name in tickers_to_plot:
if ticker_name not in self.data:
continue
ticker_data = self.data[ticker_name]
trend_signal = self.trend_signals[ticker_name]
fig, ax = plt.subplots(1, 1, figsize=(15, 6))
# Добавляем бары High-Low
ax.vlines(ticker_data.index,
ticker_data['Low'], ticker_data['High'],
colors='black', linewidth=1, alpha=0.8)
# SSA тренд
ax.plot(ticker_data.index, ticker_data['SSA_Trend'],
linewidth=2, color='RoyalBlue', alpha=0.9)
# Цветные области для трендов
i = 0
while i < len(trend_signal):
if trend_signal[i] != 0:
# Находим конец текущего тренда
j = i
while j < len(trend_signal) and trend_signal[j] == trend_signal[i]:
j += 1
# Создаем область
start_idx = i
end_idx = min(j, len(ticker_data.index) - 1)
color = 'green' if trend_signal[i] == 1 else 'red'
ax.axvspan(ticker_data.index[start_idx], ticker_data.index[end_idx],
alpha=0.2, color=color)
i = j
else:
i += 1
# Добавляем информацию о последней цене и времени
last_close = ticker_data['Close'].iloc[-1]
last_date = ticker_data.index[-1]
# Текст с информацией
info_text = f"Last Close: {last_close:.5f}\nLast Update: {last_date.strftime('%Y-%m-%d %H:%M')}"
ax.text(0.02, 0.98, info_text, transform=ax.transAxes, fontsize=10,
verticalalignment='top', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8))
ax.set_title(f'{ticker_name} Trends (tf: 2D)',
fontsize=16, fontweight='bold')
ax.set_ylabel('Price', fontsize=14)
ax.set_xlabel('Date', fontsize=14)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
def get_market_summary(self):
"""Получить полную сводку анализа"""
summary = {}
trending_summary = {}
for name in self.data.keys():
# Базовая статистика
stats, _, _ = self.calculate_trend_statistics(name)
current_trend_info = self.get_current_trend_info(name)
progress_info = self.calculate_trend_progress(name)
# Объединяем все статистики
ticker_summary = {**stats, **current_trend_info, **progress_info}
summary[name] = ticker_summary
# Отдельная сводка только для валют, которые сейчас в тренде
if current_trend_info['current_trend'] in ['Bull', 'Bear']:
trending_summary[name] = ticker_summary
return summary, trending_summary
# Параметры SSA анализа
window_length = 17             # =34 торговых дней. Чем больше, тем более длинные тренды ищет алгоритм, но меньше сигналов
trend_components = [0, 1, 2]   # Ключевые компоненты SSA. Чем больше число компонент, тем больше шума захватываем
threshold_percentile = 70      # Порог чувствительности SSA (50-99)
sensitivity_boost = 1.25       # Тонкая настройка чувствительности начала трендов
print("SSA АНАЛИЗ РЫНКА")
print("="*50)
# Создаем анализатор и запускаем анализ
analyzer = MarketSSAAnalyzer(market_data)
analyzer.ssa_trend_analysis_all(
window_length=window_length,
trend_components=trend_components,
threshold_percentile=threshold_percentile,
sensitivity_boost=sensitivity_boost
)
# Строим графики
analyzer.plot_charts()
# Получаем сводки
full_summary, trending_summary = analyzer.get_market_summary()
print("\n" + "="*70)
print("ВАЛЮТЫ В ТРЕНДЕ")
print("="*70)
for ticker_name, ticker_summary in trending_summary.items():
print(f"\n{ticker_name.upper()}")
print("-" * 50)
current_trend = ticker_summary['current_trend']
trend_return = ticker_summary['trend_return']
progress_bar = ticker_summary.get('progress_bar', 'N/A')
print(f"Текущий тренд: {current_trend}")
print(f"% изменения с начала тренда: {trend_return:.2f}%")
if ticker_summary.get('progress_pct') is not None:
progress_pct = ticker_summary.get('progress_pct', 0)
progress_range = ticker_summary.get('progress_range', 0)
progress_bar = ticker_summary.get('progress_bar', 'N/A')
avg_historical = ticker_summary.get('avg_historical', 0)
current_vs_avg = ticker_summary.get('current_vs_avg', 0)
min_hist = ticker_summary.get('min_historical', 0)
max_hist = ticker_summary.get('max_historical', 0)
print(f"Прогресс от среднего: {progress_pct:.1f}% (текущий/средний)")
print(f"Прогресс-бар: {progress_bar}")
print(f"Средний исторический: {avg_historical:.2f}%")
print(f"Отклонение от среднего: {current_vs_avg:+.2f}%")
print(f"Диапазон: [{min_hist:.2f}% - {max_hist:.2f}%]")
else:
progress_bar = ticker_summary.get('progress_bar', 'N/A')
print(f"Прогресс тренда: {progress_bar}")
print("\n" + "="*70)

График котировок AUD-CAD (бары, таймфрейм 2 дня, цены Typical Close за 2 дня), тренды SSA (синяя линия), подсветка зон растущих трендов (зеленый) и падающих (красный)

Рис. 1: График котировок AUD-CAD (бары, таймфрейм 2 дня, цены Typical Close за 2 дня), тренды SSA (синяя линия), подсветка зон растущих трендов (зеленый) и падающих (красный)

График валютной пары AUD-CHF и построенных трендов

Рис. 2: График валютной пары AUD-CHF и построенных трендов

График валютной пары CAD-CHF и построенных трендов

Рис. 3: График валютной пары CAD-CHF и построенных трендов

Метод ssa_decomposition() выполняет сингулярное разложение траекторной матрицы с помощью NumPy. Траекторная матрица строится путем сдвига окна фиксированной длины вдоль временного ряда, что создает координатное представление динамической системы. SVD разложение выделяет главные компоненты, упорядоченные по убыванию объясняемой дисперсии.

👉🏻  Линейная алгебра: векторы и матрицы в финансовой математике

График валютной пары CAD-JPY и построенных трендов

Рис. 4: График валютной пары CAD-JPY и построенных трендов

График валютной пары EUR-USD и построенных трендов

Рис. 5: График валютной пары EUR-USD и построенных трендов

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

График валютной пары EUR-AUD и построенных трендов

Рис. 6: График валютной пары EUR-AUD и построенных трендов

График валютной пары GBP-USD и построенных трендов

Рис. 7: График валютной пары GBP-USD и построенных трендов

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

График валютной пары GBP-CAD и построенных трендов

Рис. 8: График валютной пары GBP-CAD и построенных трендов

График цен на золото (XAU-USD) и построенных трендов

Рис. 9: График цен на золото (XAU-USD) и построенных трендов

График цен на Биткоин (BTC-USD) и построенных трендов

Рис. 10: График цен на Биткоин (BTC-USD) и построенных трендов

======================================================================
ВАЛЮТЫ В ТРЕНДЕ
======================================================================
AUD/CAD
--------------------------------------------------
Текущий тренд: Bear
% изменения с начала тренда: 0.19%
Прогресс от среднего: 11.1% (текущий/средний)
Прогресс-бар: |██░░░░░░░░░░░░░░░░░░| 11.1%
Средний исторический: 1.74%
Отклонение от среднего: -1.54%
Диапазон: [0.77% - 6.50%]
AUD/CHF
--------------------------------------------------
Текущий тренд: Bear
% изменения с начала тренда: -0.30%
Прогресс от среднего: 11.2% (текущий/средний)
Прогресс-бар: |██░░░░░░░░░░░░░░░░░░| 11.2%
Средний исторический: 2.72%
Отклонение от среднего: -2.42%
Диапазон: [1.18% - 7.12%]
AUD/NZD
--------------------------------------------------
Текущий тренд: Bull
% изменения с начала тренда: 3.42%
Прогресс от среднего: 173.2% (текущий/средний)
Прогресс-бар: |████████████████████| 173.2%
Средний исторический: 1.97%
Отклонение от среднего: +1.45%
Диапазон: [1.03% - 1.89%]
EUR/AUD
--------------------------------------------------
Текущий тренд: Bull
% изменения с начала тренда: 0.38%
Прогресс от среднего: 13.0% (текущий/средний)
Прогресс-бар: |██░░░░░░░░░░░░░░░░░░| 13.0%
Средний исторический: 2.90%
Отклонение от среднего: -2.52%
Диапазон: [1.60% - 5.60%]
EUR/GBP
--------------------------------------------------
Текущий тренд: Bull
% изменения с начала тренда: 0.40%
Прогресс от среднего: 20.0% (текущий/средний)
Прогресс-бар: |████░░░░░░░░░░░░░░░░| 20.0%
Средний исторический: 2.00%
Отклонение от среднего: -1.60%
Диапазон: [1.63% - 3.19%]
EUR/NZD
--------------------------------------------------
Текущий тренд: Bull
% изменения с начала тренда: 2.83%
Прогресс от среднего: 92.9% (текущий/средний)
Прогресс-бар: |██████████████████░░| 92.9%
Средний исторический: 3.05%
Отклонение от среднего: -0.22%
Диапазон: [0.86% - 4.75%]
GBP/USD
--------------------------------------------------
Текущий тренд: Bear
% изменения с начала тренда: -0.88%
Прогресс от среднего: 38.5% (текущий/средний)
Прогресс-бар: |███████░░░░░░░░░░░░░| 38.5%
Средний исторический: 2.28%
Отклонение от среднего: -1.40%
Диапазон: [1.29% - 3.79%]
GBP/CHF
--------------------------------------------------
Текущий тренд: Bear
% изменения с начала тренда: -0.32%
Прогресс от среднего: 17.6% (текущий/средний)
Прогресс-бар: |███░░░░░░░░░░░░░░░░░| 17.6%
Средний исторический: 1.79%
Отклонение от среднего: -1.48%
Диапазон: [0.38% - 5.74%]
GBP/NZD
--------------------------------------------------
Текущий тренд: Bull
% изменения с начала тренда: 1.55%
Прогресс от среднего: 81.5% (текущий/средний)
Прогресс-бар: |████████████████░░░░| 81.5%
Средний исторический: 1.90%
Отклонение от среднего: -0.35%
Диапазон: [1.18% - 2.97%]
NZD/USD
--------------------------------------------------
Текущий тренд: Bear
% изменения с начала тренда: -2.52%
Прогресс от среднего: 107.5% (текущий/средний)
Прогресс-бар: |████████████████████| 107.5%
Средний исторический: 2.34%
Отклонение от среднего: +0.18%
Диапазон: [0.14% - 4.87%]
NZD/CAD
--------------------------------------------------
Текущий тренд: Bear
% изменения с начала тренда: -2.10%
Прогресс от среднего: 148.2% (текущий/средний)
Прогресс-бар: |████████████████████| 148.2%
Средний исторический: 1.42%
Отклонение от среднего: +0.68%
Диапазон: [0.14% - 2.73%]
NZD/CHF
--------------------------------------------------
Текущий тренд: Bear
% изменения с начала тренда: -2.46%
Прогресс от среднего: 79.9% (текущий/средний)
Прогресс-бар: |███████████████░░░░░| 79.9%
Средний исторический: 3.08%
Отклонение от среднего: -0.62%
Диапазон: [2.05% - 7.28%]
NZD/JPY
--------------------------------------------------
Текущий тренд: Bear
% изменения с начала тренда: -0.92%
Прогресс от среднего: 33.7% (текущий/средний)
Прогресс-бар: |██████░░░░░░░░░░░░░░| 33.7%
Средний исторический: 2.73%
Отклонение от среднего: -1.81%
Диапазон: [1.73% - 4.64%]
USD/CAD
--------------------------------------------------
Текущий тренд: Bull
% изменения с начала тренда: 0.97%
Прогресс от среднего: 62.4% (текущий/средний)
Прогресс-бар: |████████████░░░░░░░░| 62.4%
Средний исторический: 1.55%
Отклонение от среднего: -0.58%
Диапазон: [1.04% - 2.75%]
BTC/USD
--------------------------------------------------
Текущий тренд: Bear
% изменения с начала тренда: -2.75%
Прогресс от среднего: 36.5% (текущий/средний)
Прогресс-бар: |███████░░░░░░░░░░░░░| 36.5%
Средний исторический: 7.55%
Отклонение от среднего: -4.79%
Диапазон: [4.27% - 15.22%]

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

👉🏻  23 способа визуализации котировок с помощью Mplfinance

Аналитическая панель

Модуль визуализации результатов представляет собой интерактивную pandas-таблицу с расширенной системой метрик для оценки торговых возможностей.

def create_summary_table(full_summary, trending_summary):
"""
Создает pandas таблицу с результатами анализа
"""
# Создаем полную таблицу
full_data = []
for ticker, stats in full_summary.items():
# Рассчитываем абсолютные приросты капитала
trend_return_pct = abs(stats.get('trend_return', 0))  # По модулю для текущего тренда
trend_return_abs = abs(10000 * (stats.get('trend_return', 0) / 100))  # Модуль для абсолютных значений
trend_return_leveraged = abs(10000 * (stats.get('trend_return', 0) / 100) * 30)  # Модуль с брокерским плечом 1:30
# Рассчитываем прогресс относительно минимума и максимума
current_return = abs(stats.get('trend_return', 0))  # Всегда по модулю
min_hist = stats.get('min_historical', 0)
max_hist = stats.get('max_historical', 0)
avg_hist = abs(stats.get('avg_historical', 0))  # По модулю для средних исторических
# Проверяем флет для исторических показателей
is_flat = stats.get('current_trend', 'Flat') == 'Flat'
# Прогресс от минимума
if min_hist > 0 and not is_flat:
progress_vs_min = min((current_return / min_hist) * 100, 100)
min_filled = int((progress_vs_min / 100) * 10)
progress_min_bar = f"|{'█' * min_filled}{'░' * (10 - min_filled)}| {progress_vs_min:.0f}%"
else:
progress_min_bar = ""
# Прогресс от максимума  
if max_hist > 0 and not is_flat:
progress_vs_max = min((current_return / max_hist) * 100, 100)
max_filled = int((progress_vs_max / 100) * 10)
progress_max_bar = f"|{'█' * max_filled}{'░' * (10 - max_filled)}| {progress_vs_max:.0f}%"
else:
progress_max_bar = ""
# Прогресс-бар от среднего (выравниваем ширину с другими столбцами прогресс-баров)
if not is_flat and stats.get('progress_pct') is not None:
progress_pct = stats.get('progress_pct', 0)
progress_for_bar = min(progress_pct, 100)
filled = int((progress_for_bar / 100) * 10)
progress_bar = f"|{'█' * filled}{'░' * (10 - filled)}| {progress_pct:.0f}%"
else:
progress_bar = ""
# Заменяем нулевые значения на пустые строки (с округлением до 1 знака)
trend_return_str = f"{trend_return_pct:.1f}%" if trend_return_pct != 0 else ""
trend_return_abs_str = f"${trend_return_abs:.0f}" if trend_return_abs != 0 else ""
trend_return_leveraged_str = f"${trend_return_leveraged:.0f}" if trend_return_leveraged != 0 else ""
avg_historical_str = f"{avg_hist:.1f}%" if avg_hist != 0 and not is_flat else ""
# Новые метрики
current_days = stats.get('current_trend_days', 0)
current_days_str = f"{current_days:.0f}" if current_days != 0 else ""
# Исторические показатели (пустые для флета)
avg_duration_str = f"{stats.get('avg_trend_duration_days', 0):.0f}" if stats.get('avg_trend_duration_days', 0) != 0 and not is_flat else ""
worst_dd_pct_str = f"{stats.get('avg_worst_drawdown_pct', 0):.1f}%" if stats.get('avg_worst_drawdown_pct', 0) != 0 and not is_flat else ""
worst_dd_abs_str = f"-${stats.get('avg_worst_drawdown_abs', 0):.0f}" if stats.get('avg_worst_drawdown_abs', 0) != 0 and not is_flat else ""
worst_dd_x30_str = f"-${stats.get('avg_worst_drawdown_x30', 0):.0f}" if stats.get('avg_worst_drawdown_x30', 0) != 0 and not is_flat else ""
avg_historical_abs = stats.get('avg_historical_abs', 0)
avg_historical_abs_str = f"${avg_historical_abs:.0f}" if avg_historical_abs != 0 and not is_flat else ""
avg_historical_x30 = stats.get('avg_historical_x30', 0)
avg_historical_x30_str = f"${avg_historical_x30:.0f}" if avg_historical_x30 != 0 and not is_flat else ""
row = {
'Ticker': ticker,
'Current_Trend': stats.get('current_trend', 'Flat'),
'vs_Average': progress_bar,
'vs_Min': progress_min_bar,
'vs_Max': progress_max_bar,
'Current_Days': current_days_str,
'HistAverage_Days': avg_duration_str,
'Current_$': trend_return_abs_str,
'HistAverage_$': avg_historical_abs_str,
'Current_x30': trend_return_leveraged_str,
'HistAverage_x30': avg_historical_x30_str,
'Current_%': trend_return_str,
'HistAverage_%': avg_historical_str,
'HistWorstDD_%': worst_dd_pct_str,
'HistWorstDD_$': worst_dd_abs_str,
'HistWorstDD_x30': worst_dd_x30_str
}
full_data.append(row)
full_df = pd.DataFrame(full_data)
return full_df
def display_styled_table(full_df):
"""
Отображает таблицу с красивым форматированием
"""
# Функция для цветовой кодировки трендов
def color_trends(val):
if val == 'Bull':
return 'background-color: darkgreen; color: white'
elif val == 'Bear':
return 'background-color: darkred; color: white'
else:  # Flat
return 'background-color: darkgray; color: white'
# Стилизация таблицы
styled_df = full_df.style.applymap(color_trends, subset=['Current_Trend'])
display(styled_df)
return full_df
def export_to_excel(full_df, filename="market_analysis.xlsx"):
"""
Экспортирует таблицу в Excel файл
"""
try:
full_df.to_excel(filename, index=False)
print(f"\n Данные экспортированы в файл: {filename}")
except Exception as e:
print(f"\n Ошибка экспорта в Excel: {e}")
print("Убедитесь, что установлен openpyxl: pip install openpyxl")
# Создаем таблицу из результатов анализа
full_df = create_summary_table(full_summary, trending_summary)
# Отображаем стилизованную таблицу
display_styled_table(full_df)
# Экспортируем в Excel (опционально)
# export_to_excel(full_df)

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

  • Прогресс-бары визуализируют текущее состояние тренда относительно исторических паттернов. Индикатор vs_Average показывает, насколько текущее движение соответствует типичным трендам для данного инструмента. Значения близкие к 100% указывают на зрелость тренда, что может сигнализировать о приближающемся развороте;
  • Индикаторы vs_Min и vs_Max помещают текущее движение в контекст исторического диапазона, помогая оценить потенциал дальнейшего развития.
👉🏻  Расчет показателей доходности и риска биржевой торговли на Python

Таблица очень широкая, поэтому для цели читаемости этой статьи я буду показывать ее по частям - левой, центральной и правой:

Левая часть таблицы для скрининга трендов на валютном рынке Forex. Беглого взгляда достаточно чтобы оценить что сейчас растет, что падает, где флэт / боковик и какой потенциал тренда относительно исторически средней, минимальной и максимальной длительности трендов.

Рис. 11: Левая часть таблицы для скрининга трендов на валютном рынке Forex. Беглого взгляда достаточно чтобы оценить что сейчас растет, что падает, где флэт / боковик и какой потенциал тренда относительно исторически средней, минимальной и максимальной длительности трендов.

Расчет потенциальной прибыли представлен в трех вариантах: процентная доходность, абсолютная прибыль с капитала 10,000 долларов без использования плеча, и аналогичный расчет с плечом 1:30. Такое представление позволяет быстро оценить масштаб возможностей и соответствующие риски. Все расчеты выполняются по модулю, что упрощает сравнение инструментов независимо от направления тренда.

Таблица скрининга трендов на Forex: центральная часть таблицы с указанием текущей доходности в тренде, средней по всем трендам за 12 месяцев, текущей с плечом 1:30, исторической средней с плечом 1:30

Рис. 12: Таблица скрининга трендов на Forex: центральная часть таблицы с указанием текущей доходности в тренде, средней по всем трендам за 12 месяцев, текущей с плечом 1:30, исторической средней с плечом 1:30

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

Правая часть таблицы скрининга трендов на Forex. Здесь уже представлена статистика по текущей и исторически средней доходности при торговле по трендам в процентах движения цены. Также представлена статистика максимальных исторических просадок, которые произошли при открытии позиций по тренду с плечом (x30) и без него.

Рис. 13: Правая часть таблицы скрининга трендов на Forex. Здесь уже представлена статистика по текущей и исторически средней доходности при торговле по трендам в процентах движения цены. Также представлена статистика максимальных исторических просадок, которые произошли при открытии позиций по тренду с плечом (x30) и без него.

Интерпретация трендовых сигналов

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

👉🏻  Показатели ликвидности акций и методы их расчета

Прогресс-бары не являются предсказательными индикаторами в прямом смысле, а скорее показывают текущее состояние тренда в контексте исторических данных. Высокие значения vs_Average (80-100%) указывают на то, что текущее движение уже реализовало значительную часть потенциала, характерного для данного инструмента.

Также интерпретация требует учета корреляционной структуры валютного рынка. Одновременные сигналы на коррелированных парах (например, EUR/USD и GBP/USD) могут указывать на системные движения, связанные с изменением глобального стремления к риску. В таких случаях следует отдавать предпочтение разнонаправленным инструментам или рассматривать возможность хеджирования позиций.

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

Временные характеристики трендов помогают планировать горизонт удержания позиций. Средняя продолжительность трендов для конкретного инструмента дает ориентир для установки целевых уровней прибыли и stop-loss ордеров. Инструменты с короткими трендами требуют более активного управления позициями и более узких уровней риска.

Дисклеймер: представленная стратегия не является инвестиционной рекомендацией. И предлагается как демонстрация возможностей Python и авторского подхода к разработке алгоритмов. Это не софт для реальной торговли, данная стратегия не тестировалась на реальных торгах!

Настройка параметров под торговые задачи

Оптимизация параметров SSA-анализа должна основываться на целевом горизонте торговли и характеристиках используемой стратегии:

  • window_length. Для среднесрочных позиций продолжительностью 2-6 недель оптимальная длина окна составляет 12-18 периодов при двухдневной гранулярности. Такие настройки обеспечивают хороший поиск паттернов длиной 24-36 торговых дней, что соответствует типичным циклам среднесрочных движений на рынке Форекс, с учетом периода накоплений перед началом тренда.
  • Параметр threshold_percentile контролирует чувствительность системы к изменениям тренда. Значения 65-75% обеспечивают разумный баланс между количеством сигналов и их качеством. Снижение порога увеличивает количество выявляемых трендов, но может привести к избыточному числу ложных сигналов. Повышение порога делает систему более консервативной, что подходит для стратегий с низкой частотой торгов.
  • Параметр sensitivity_boost предоставляет дополнительный инструмент тонкой настройки после выбора основного порога. Значения меньше 1.0 снижают чувствительность системы, что полезно для фильтрации слабых трендов в периоды высокой волатильности. Значения больше 1.0 повышают чувствительность, что может быть полезно в спокойные периоды для выявления зарождающихся движений.
👉🏻  ИИ Инвестирование (AI Investing): Что это? Преимущества и недостатки подхода

Количество используемых компонент (trend_components) для реконструкции тренда влияет на гладкость получаемого сигнала. Использование только первых двух компонент [0, 1] создает очень гладкий тренд, подходящий для долгосрочных стратегий. Добавление третьей компоненты [0, 1, 2] увеличивает реактивность системы за счет некоторого увеличения шума.

Балансировка между количеством и качеством сигналов требует итеративного подхода с бэктестингом торговой системы на исторических данных. Оптимальные параметры зависят от рыночного режима: в трендовые периоды предпочтительна высокая чувствительность, в боковых рынках — низкая. Как вариант улучшения - адаптивная система, которая будет автоматически корректировать параметры на основе измеряемой волатильности, скорости трендов или других рыночных индикаторов.

Практические рекомендации

  1. Как уже было сказано выше: данный скринер НЕ является готовым софтом для реальной торговли. Он еще не тестировался на реальных счетах. Поэтому, если вас заинтересовала данная стратегия, тестируйте ее обязательно на демо-счете.
  2. Для торговли лучше выбирать валютные пары со свежими трендовыми сигналами и умеренными значениями заполнения прогресс-баров, небольшого количества дней в текущем тренде относительно средних исторических. В этом случае минимизируются риски "попасть в последний вагон" (но вероятность такая есть всегда, это рынок), к тому же выше потенциал для дохода.
  3. Рекомендуется комбинировать сигналы трендов SSA с фундаментальным анализом, в контексте предстоящих макроэкономических событий, заседаний центральных банков и публикаций важной статистики. Технические сигналы против фундаментального направления требуют особой осторожности и сокращенных размеров позиций.
  4. Управление рисками должно основываться на исторических метриках просадок. Максимальный размер позиции рассчитывается как отношение допустимого риска портфеля к Worst Case Drawdown инструмента. Например, при готовности рисковать 2% портфеля и историческом максимуме просадки 5%, размер позиции не должен превышать 40% от доступного капитала.
  5. Диверсификация позиций требует учета корреляционной структуры валютного рынка. Одновременное удержание позиций по EUR/USD, GBP/USD и AUD/USD может создать концентрированную экспозицию к долларовому движению. Предпочтительно комбинировать некоррелированные инструменты или использовать hedge-стратегии для снижения системного риска.

Потенциальные проблемы стратегии

Эта стратегия - не грааль, предсказывающий рынок. SSA декомпозиция временных рядов неизбежно рисует тренды с запаздыванием. Математическая природа метода требует накопления достаточного количества данных для выявления изменения тренда, что приводит к запаздыванию в 3-6 периодов. При двухдневной гранулярности это соответствует 6-12 торговым дням, что может существенно снизить прибыльность стратегии на коротких движениях.

Историческая статистика по длине трендов, их доходности и максимальным просадкам не гарантирует повторения в будущем. Рыночные режимы могут кардинально изменяться под влиянием макроэкономических шоков, изменений в монетарной политике центральных банков или структурных сдвигов в глобальной экономике. То, что GBP/USD исторически демонстрировал тренды средней длиной 15 дней с доходностью 2.3%, не означает, что будущие тренды будут следовать этому паттерну. Период низкой волатильности может смениться эпохой высокой волатильности, и наоборот.

Еще одна потенциальная проблема - переоптимизация параметров. Соблазн подстроить параметры window_length, threshold_percentile и sensitivity_boost под исторические данные может привести к созданию системы, которая отлично работала в прошлом, но провалится в будущем. Этот эффект усиливается при использовании ограниченного исторического периода для тестирования.

Проскальзывание и транзакционные издержки могут существенно снизить реальную прибыльность, особенно для малопопулярных валютных пар с низкой ликвидностью. Спреды bid-ask, комиссии брокера и проскальзывание при исполнении ордеров не учитываются в расчетах потенциальной доходности системы. Для некоторых экзотических пар суммарные транзакционные издержки могут достигать 10-20 пипсов на сделку.

Заключение

Разработанный скринер трендов демонстрирует практическую применимость сингулярного спектрального анализа для автоматизации торговых решений на валютном рынке. Использование Typical Price и адаптивного порогового механизма обеспечивает более стабильные сигналы по сравнению с традиционными техническими индикаторами. Комплексная система риск-метрик позволяет количественно оценить потенциальные возможности и просадки для торговли по каждому инструменту.

SSA-скринер следует рассматривать как один из инструментов в арсенале трейдера, который дополняет, но не заменяет фундаментальный анализ и здравый смысл. Практическая ценность системы заключается в способности одновременного мониторинга 29 валютных пар с объективной оценкой их торгового потенциала. Исторические метрики просадок и доходностей предоставляют надежную основу для расчета размеров позиций и планирования торговых стратегий.

Полный код скринера мною специально выложен в открытый доступ, чтобы каждый желающий мог поэкспериментировать с методом и адаптировать его под собственные задачи. Модульная архитектура системы позволяет легко интегрировать дополнительные алгоритмы машинного обучения - от gradient boosting для улучшения классификации трендов до рекуррентных нейросетей для прогнозирования продолжительности движений. Комбинирование SSA с современными ML-подходами может открыть новые возможности для создания более точных и адаптивных торговых систем.