Работая с большими массивами данных и разрабатывая торговые системы, я неоднократно убеждался, что правильно подобранный алгоритм способен кардинально изменить результаты. Однако важно понимать: не все алгоритмические решения одинаково полезны в контексте финансовых рынков. Существует огромное количество подходов, от примитивных до чрезвычайно сложных, и ваша задача — выбрать те, которые действительно работают в условиях высокой неопределенности.
В этой статье я детально рассмотрю, какие алгоритмы программирования действительно имеют значение для эффективной работы на финансовых рынках и почему их понимание может стать вашим конкурентным преимуществом.
Почему алгоритмический подход критически важен
Финансовые рынки генерируют колоссальные объемы данных каждую секунду. Без автоматизации и алгоритмического анализа обработать такие объемы информации практически невозможно. Однако дело не только в количестве. Рыночные данные обладают рядом специфических особенностей:
- Высокая зашумленность — полезный сигнал часто составляет лишь малую часть всей информации;
- Нестационарность — статистические свойства данных меняются со временем;
- Сложные взаимосвязи между различными факторами;
- Отсутствие линейных зависимостей во многих процессах.
В таких условиях классические подходы к анализу данных часто оказываются малоэффективными. Банальные индикаторы вроде скользящих средних или стохастических осцилляторов, которые так любят рекомендовать в популярных финансовых СМИ, как правило, дают результаты не лучше случайных. Настоящее преимущество дают современные алгоритмические техники, которыми пользуются профессионалы из квантовых хедж-фондов и инвестиционных банков.
Эволюция алгоритмических подходов в трейдинге
Исторически развитие алгоритмических методов в трейдинге происходило параллельно с эволюцией вычислительных мощностей и математических моделей. Если в 1970-80-х годах основной упор делался на относительно простые статистические методы, то сегодня мы наблюдаем настоящий расцвет сложных вычислительных подходов:
- 1970-е годы: первые попытки использования компьютеров для анализа рыночных данных, простейшие технические индикаторы;
- 1980-е годы: развитие статистических методов анализа временных рядов (ARIMA, GARCH);
- 1990-е годы: появление первых, примитивных нейронных сетей и генетических алгоритмов в трейдинге;
- 2000-е годы: развитие алгоритмического и высокочастотного трейдинга;
- 2010-е годы: внедрение методов машинного обучения и глубоких нейронных сетей;
- 2020-е годы: применение продвинутых методов глубокого обучения, графовых нейронных сетей, комбинации RL с каузальными моделями
Современный этап характеризуется использованием сложных ансамблевых моделей, учитывающих непрерывно меняющуюся природу рынков и интегрирующих различные источники данных — от традиционной рыночной информации до новостных потоков и альтернативных данных.
Фундаментальные алгоритмы для работы с рыночными данными
Прежде чем погружаться в специализированные алгоритмы для трейдинга, важно освоить базовые алгоритмические конструкции, которые служат фундаментом для более сложных решений. Эти алгоритмы помогают эффективно обрабатывать, структурировать и анализировать данные.
Алгоритмы сортировки и поиска
Несмотря на кажущуюся простоту, алгоритмы сортировки и поиска остаются критически важными для обработки финансовых данных. Они используются для:
- Упорядочивания ценовых данных;
- Определения экстремумов (максимумы и минимумы);
- Быстрого поиска информации в больших датасетах;
- Идентификации паттернов.
Для финансовых задач особенно важна временная эффективность этих алгоритмов. Рассмотрим пример реализации алгоритма быстрой сортировки (QuickSort) на языке программирования Python, который часто применяется для работы с большими массивами данных:
!pip install ccxt
import ccxt
import pandas as pd
import numpy as np
# Загружаем котировки Bitcoin с биржи Kucoin за последние 252 дня
exchange = ccxt.kucoin()
ohlcv = exchange.fetch_ohlcv(
'BTC/USDT',
timeframe='1d',
limit=252
)
# Сохраняем их в датафрейм
df = pd.DataFrame(ohlcv, columns=['time', 'open', 'high', 'low', 'close', 'volume'])
df['time'] = pd.to_datetime(df['time'], unit='ms') + pd.Timedelta(hours=3)
df.set_index('time', inplace=True)
def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort(left) + middle + quick_sort(right)
# Сортируем цены закрытия
closing_prices = df['close'].values
sorted_prices = quick_sort(closing_prices)
# Находим квантили распределения цен для оценки волатильности
q25 = sorted_prices[int(len(sorted_prices) * 0.25)]
q50 = sorted_prices[int(len(sorted_prices) * 0.50)] # медиана
q75 = sorted_prices[int(len(sorted_prices) * 0.75)]
print(f"Медиана цены BTC/USDT: {q50}")
print(f"Межквартильный размах: {q75 - q25}")
Медиана цены BTC/USDT: 87279.5
Межквартильный размах: 28138.600000000006
В коде выше мы загрузили котировки Bitcoin за последние 252 дня, отсортировали их алгоритмом быстрой сортировки, вычислили квантили для оценки волатильности и межквартильный размах.
Давайте теперь рассмотрим как можно написать алгоритм поиска для этих данных. Пример ниже — алгоритм поиска ближайшего значения (Nearest Neighbor Search), который может быть крайне полезен если мы не знаем точное значение цены, а хотим найти ближайшее к искомому:
def find_nearest(arr, target):
idx = np.abs(arr - target).argmin()
return idx, arr[idx]
prices = df['close'].values
target_price = 94500
index, nearest_price = find_nearest(prices, target_price)
print(f"Ближайшая цена к {target_price}: {nearest_price} на индексе {index}")
Ближайшая цена к 94500: 94525.7 на индексе 141
Еще один полезный алгоритм — поиск дубликатов (повторяющихся уровней). Он может быть полезен для анализа возможных уровней отскока цены: поддержки / сопротивления.
def find_duplicate_prices(arr, tolerance=1.0):
seen = {}
duplicates = []
for i, price in enumerate(arr):
found = False
for key in seen:
if abs(key - price) < tolerance:
seen[key].append(i)
found = True
break
if not found:
seen[price] = [i]
for key, indices in seen.items():
if len(indices) > 1:
duplicates.append((key, indices))
return duplicates
duplicate_prices = find_duplicate_prices(prices, tolerance=2.0)
print("\n Повторяющиеся уровни:")
for price, indices in duplicate_prices:
print(f"Цена ~{price:.2f} встречается в строках: {indices}")
Повторяющиеся уровни:
Цена ~63205.50 встречается в строках: [26, 48]
Цена ~63331.40 встречается в строках: [29, 36]
Цена ~69376.80 встречается в строках: [69, 72]
Цена ~94285.00 встречается в строках: [87, 124]
Цена ~97458.00 встречается в строках: [96, 116]
Цена ~84337.80 встречается в строках: [187, 202]
Сами алгоритмы могут быть реализованы по-разному: через классические циклы, синтаксический сахар, функции и т. д. На практике для задач сортировки я обычно использую оптимизированные функции из Numpy, Numba, либо специализированные библиотеки. Тем не менее, понимание принципов работы базовых алгоритмов критически важно для разработки более сложных систем.
Алгоритмы работы с графами
Графовые алгоритмы становятся все более важными в финансовом анализе, особенно при моделировании сложных взаимосвязей между различными активами и факторами. Ключевые алгоритмы включают:
- Поиск в ширину (BFS) и поиск в глубину (DFS) — для исследования структуры взаимосвязей;
- Алгоритм Дейкстры — для нахождения кратчайших путей в графе взаимодействий;
- Минимальное остовное дерево (MST) — для идентификации ключевых связей между активами.
Эти алгоритмы особенно полезны при построении сетей корреляций между активами или при анализе структуры рынка.
Вот пример построения и анализа графа корреляций между самыми ликвидными криптовалютами (Bitcoin, Ethereum, Ripple, Avalanche, Dogecoin, Solana, Polkadot, Chainlink):
import ccxt
import pandas as pd
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
import seaborn as sns
from tabulate import tabulate # Для красивой печати таблиц
# Список пар
symbols = ['BTC/USDT', 'ETH/USDT', 'XRP/USDT', 'AVA/USDT',
'DOGE/USDT', 'SOL/USDT', 'DOT/USDT', 'LINK/USDT']
exchange = ccxt.kucoin()
# Функция загрузки цен закрытия, берем данные по дням за последние 252 торговых дня
def fetch_crypto_prices(symbol, exchange, timeframe='1d', limit=252):
ohlcv = exchange.fetch_ohlcv(symbol, timeframe=timeframe, limit=limit)
df = pd.DataFrame(ohlcv, columns=['time', 'open', 'high', 'low', 'close', 'volume'])
df['time'] = pd.to_datetime(df['time'], unit='ms') + pd.Timedelta(hours=3)
df.set_index('time', inplace=True)
return df['close']
# Загружаем цены
price_data = {}
for symbol in symbols:
try:
price_data[symbol] = fetch_crypto_prices(symbol, exchange)
print(f"Загружены данные для {symbol}")
except Exception as e:
print(f"Ошибка при загрузке {symbol}: {e}")
# Объединяем в один DataFrame
data = pd.DataFrame(price_data).dropna()
# Расчёт логдоходностей и корреляций
returns = np.log(data / data.shift(1)).dropna()
corr_matrix = returns.corr()
# Создание графа
G = nx.Graph()
for ticker in data.columns:
G.add_node(ticker)
threshold = 0.5
for i, ticker_i in enumerate(data.columns):
for j, ticker_j in enumerate(data.columns):
if i < j: corr_value = corr_matrix.iloc[i, j] if abs(corr_value) > threshold:
G.add_edge(ticker_i, ticker_j, weight=abs(corr_value))
# Определение сообществ
communities = nx.community.louvain_communities(G)
# Вывод: найденные сообщества
print("\n Найденные сообщества:")
for i, community in enumerate(communities):
print(f"Кластер {i+1}: {community}")
print("-" * 60)
# Вывод: средняя корреляция внутри каждого кластера
print("\n Средняя корреляция внутри кластеров:")
for idx, community in enumerate(communities):
cluster_assets = list(community)
if len(cluster_assets) > 1:
cluster_corr = corr_matrix.loc[cluster_assets, cluster_assets].values
mean_corr = np.mean([cluster_corr[i][j] for i in range(len(cluster_corr))
for j in range(len(cluster_corr)) if i != j])
print(f"Кластер {idx+1} ({len(cluster_assets)} активов): средняя корреляция = {mean_corr:.2f}")
else:
print(f"Кластер {idx+1}: одиночный актив")
print("-" * 60)
# Вывод: топ-коррелирующие и слабые пары
corr_pairs = []
for i in range(len(corr_matrix.columns)):
for j in range(i + 1, len(corr_matrix.columns)):
ticker_i = corr_matrix.columns[i]
ticker_j = corr_matrix.columns[j]
corr_value = corr_matrix.iloc[i, j]
corr_pairs.append((ticker_i, ticker_j, corr_value))
# Сортировка по модулю корреляции
corr_pairs.sort(key=lambda x: abs(x[2]), reverse=True)
# Таблица с топ-парами
top_pairs = corr_pairs[:5]
low_pairs = corr_pairs[-5:]
table = [["Пара", "Корреляция"]] + \
[[f"{p[0]} ↔ {p[1]}", f"{p[2]:.2f}"] for p in top_pairs]
low_table = [["Пара", "Корреляция"]] + \
[[f"{p[0]} ↔ {p[1]}", f"{p[2]:.2f}"] for p in low_pairs]
print("\n Самые высокие корреляции:")
print(tabulate(table, headers="keys", tablefmt="psql"))
print("\n Наименее скоррелированные пары:")
print(tabulate(low_table, headers="keys", tablefmt="psql"))
print("-" * 60)
# Последние цены и доходности
latest_prices = data.iloc[-1]
daily_returns = returns.iloc[-1]
summary_df = pd.DataFrame({
'Последняя цена': latest_prices,
'Дневная доходность (%)': daily_returns * 100
}).round({'Дневная доходность (%)': 2})
print("\n Последние данные по активам:")
print(summary_df.to_string())
print("-" * 60)
# График
pos = nx.spring_layout(G, seed=42)
plt.figure(figsize=(12, 8))
# Цвета для сообществ
for i, community in enumerate(communities):
nx.draw_networkx_nodes(G, pos, nodelist=list(community),
node_color=f"C{i}", node_size=500, alpha=0.8)
# Ребра
edge_weights = [G[u][v]['weight'] * 3 for u, v in G.edges()]
nx.draw_networkx_edges(G, pos, width=edge_weights, alpha=0.5)
# Метки
nx.draw_networkx_labels(G, pos, font_size=12)
# Отображение
plt.title("Граф корреляций криптоактивов с выделенными кластерами")
plt.axis('off')
plt.tight_layout()
plt.show()
Найденные сообщества:
Кластер 1: {'DOT/USDT', 'XRP/USDT', 'ETH/USDT', 'DOGE/USDT', 'BTC/USDT', 'SOL/USDT'}
Кластер 2: {'AVA/USDT', 'LINK/USDT'}
------------------------------------------------------------
Средняя корреляция внутри кластеров:
Кластер 1 (6 активов): средняя корреляция = 0.68
Кластер 2 (2 активов): средняя корреляция = 0.53
------------------------------------------------------------
Самые высокие корреляции:
+----------------------+------------+
| 0 | 1 |
|----------------------+------------|
| Пара | Корреляция |
| BTC/USDT ↔ DOGE/USDT | 0.82 |
| BTC/USDT ↔ ETH/USDT | 0.82 |
| BTC/USDT ↔ SOL/USDT | 0.77 |
| ETH/USDT ↔ LINK/USDT | 0.76 |
| DOT/USDT ↔ LINK/USDT | 0.75 |
+----------------------+------------+
Наименее скоррелированные пары:
+----------------------+------------+
| 0 | 1 |
|----------------------+------------|
| Пара | Корреляция |
| ETH/USDT ↔ AVA/USDT | 0.38 |
| AVA/USDT ↔ SOL/USDT | 0.31 |
| AVA/USDT ↔ DOGE/USDT | 0.30 |
| BTC/USDT ↔ AVA/USDT | 0.29 |
| XRP/USDT ↔ AVA/USDT | 0.24 |
+----------------------+------------+
------------------------------------------------------------
Последние данные по активам:
Последняя цена Дневная доходность (%)
BTC/USDT 96253.80000 -0.66
ETH/USDT 1840.98000 -0.07
XRP/USDT 2.19247 -0.74
AVA/USDT 0.68150 5.16
DOGE/USDT 0.17655 -2.76
SOL/USDT 147.63700 -0.26
DOT/USDT 4.05630 -2.55
LINK/USDT 14.33990 -2.20
------------------------------------------------------------
Рис. 1: График корреляций топовых криптовалют с выделенными кластерами
Такой анализ позволяет выявить естественные кластеры активов, которые движутся синхронно, что критически важно для построения диверсифицированных портфелей и управления риском.
Динамическое программирование
Динамическое программирование представляет собой мощный метод для решения сложных задач оптимизации, разбивая их на более простые подзадачи. В контексте финансовых рынков этот подход применяется для:
- Оптимизации портфеля;
- Динамического хеджирования;
- Определения оптимальных моментов входа и выхода из позиций;
- Структурирования сложных торговых стратегий.
Рассмотрим классическую задачу оптимизации портфеля с учетом транзакционных издержек, которая может быть эффективно решена с помощью динамического программирования:
import numpy as np
import pandas as pd
def optimize_portfolio_with_transaction_costs(expected_returns, covariance, risk_aversion,
initial_weights, transaction_costs):
"""
Оптимизация портфеля с учетом транзакционных издержек с помощью динамического программирования
Parameters:
-----------
expected_returns : array
Ожидаемая доходность активов
covariance : 2D array
Ковариационная матрица активов
risk_aversion : float
Коэффициент неприятия риска
initial_weights : array
Начальные веса портфеля
transaction_costs : float
Коэффициент транзакционных издержек
Returns:
--------
array
Оптимальные веса портфеля
"""
n = len(expected_returns)
# Определяем функцию полезности с учетом транзакционных издержек
def utility(weights, prev_weights):
# Ожидаемая доходность
returns = np.sum(weights * expected_returns)
# Риск (дисперсия портфеля)
risk = np.dot(weights, np.dot(covariance, weights))
# Транзакционные издержки
costs = transaction_costs * np.sum(np.abs(weights - prev_weights))
# Полезность: доходность - риск - издержки
return returns - risk_aversion * risk - costs
# Начальные параметры
best_weights = initial_weights
best_utility = utility(best_weights, initial_weights)
# Шаг изменения весов
delta = 0.05
# Максимально допустимое изменение для каждого актива
max_change = 0.2
# Итеративно улучшаем решение (подход динамического программирования)
for iteration in range(10):
improved = False
for asset in range(n):
for direction in [-1, 1]: # Уменьшение или увеличение веса актива
temp_weights = best_weights.copy()
# Ограничиваем изменение весом
change = min(delta, max_change)
change = min(change, temp_weights[asset] if direction < 0 else 1 - temp_weights[asset])
# Изменяем вес выбранного актива
temp_weights[asset] += change * direction
# Нормализуем веса, чтобы сумма была равна 1
if temp_weights[asset] < 0: temp_weights[asset] = 0 temp_weights = temp_weights / np.sum(temp_weights) # Вычисляем новую полезность temp_utility = utility(temp_weights, initial_weights) # Если улучшение, обновляем лучший вариант if temp_utility > best_utility:
best_weights = temp_weights
best_utility = temp_utility
improved = True
# Если нет улучшений, уменьшаем шаг
if not improved:
delta /= 2
# Если шаг стал слишком маленьким, завершаем
if delta < 1e-5:
break
return best_weights
# Пример использования
n_assets = 5
expected_returns = np.array([0.08, 0.10, 0.12, 0.07, 0.11])
covariance = np.array([
[0.10, 0.03, 0.04, 0.02, 0.01],
[0.03, 0.12, 0.05, 0.03, 0.02],
[0.04, 0.05, 0.15, 0.04, 0.03],
[0.02, 0.03, 0.04, 0.08, 0.02],
[0.01, 0.02, 0.03, 0.02, 0.09]
])
risk_aversion = 2.0
initial_weights = np.array([0.2, 0.2, 0.2, 0.2, 0.2])
transaction_costs = 0.01
optimal_weights = optimize_portfolio_with_transaction_costs(
expected_returns, covariance, risk_aversion, initial_weights, transaction_costs
)
print("Оптимальные веса портфеля с учетом транзакционных издержек:")
for i, weight in enumerate(optimal_weights):
print(f"Актив {i+1}: {weight:.4f}")
Оптимальные веса портфеля с учетом транзакционных издержек:
Актив 1: 0.1994
Актив 2: 0.1770
Актив 3: 0.0999
Актив 4: 0.1994
Актив 5: 0.3242
Этот алгоритм находит компромисс между потенциальной доходностью, риском и транзакционными издержками, что намного ближе к реальным условиям, чем классическая теория портфелей Марковица, не учитывающая издержки.
Продвинутые алгоритмы и структуры данных для трейдинга
После освоения базовых алгоритмов имеет смысл перейти к более продвинутым структурам данных и алгоритмам, которые помогут создавать более эффективные торговые системы. Эти инструменты позволяют решать сложные задачи оптимизации, обработки сигналов и управления рисками.
Алгоритмы оптимизации
Задачи оптимизации возникают повсеместно при разработке торговых стратегий — от подбора оптимальных параметров до балансировки портфеля. Классические алгоритмы оптимизации часто не справляются с многомерными пространствами параметров и нелинейными целевыми функциями, типичными для финансовых рынков.
Для решения таких задач более эффективны следующие подходы:
- Генетические алгоритмы;
- Метод имитации отжига (Simulated Annealing);
- Дифференциальная эволюция;
- Байесовская оптимизация.
Рассмотрим пример реализации метода имитации отжига для оптимизации параметров простой торговой стратегии на скользящих средних (EMA):
import ccxt
import numpy as np
import pandas as pd
import random
import math
import matplotlib.pyplot as plt
# Загрузка данных
exchange = ccxt.kucoin()
ohlcv = exchange.fetch_ohlcv('BTC/USDT', timeframe='1d', limit=500)
df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms') + pd.Timedelta(hours=3)
df.set_index('timestamp', inplace=True)
# Целевая функция на основе EMA
def objective(individual):
short_window, long_window = int(individual[0]), int(individual[1])
if short_window >= long_window or short_window < 2 or long_window > 200:
return -1000 # Штраф за недопустимые значения
try:
df['short_ma'] = df['close'].ewm(span=short_window, adjust=False).mean()
df['long_ma'] = df['close'].ewm(span=long_window, adjust=False).mean()
df['signal'] = np.where(df['short_ma'] > df['long_ma'], 1, 0)
df['position'] = df['signal'].diff()
df['returns'] = df['close'].pct_change()
df['strategy_returns'] = df['signal'].shift(1) * df['returns'].dropna()
total_return = (df['strategy_returns'].cumprod().iloc[-1] - 1) * 100
sharpe = df['strategy_returns'].mean() / df['strategy_returns'].std() * np.sqrt(252)
drawdown = (df['strategy_returns'].cumprod() /
df['strategy_returns'].cumprod().cummax() - 1).min() * 100
fitness = total_return * 0.4 + sharpe * 0.4 - abs(drawdown) * 0.2
return fitness if not np.isnan(fitness) else -1000
except:
return -1000
# Simulated Annealing
def simulated_annealing(obj_func, bounds, n_iterations, temp_start, cooling_rate):
best = [random.randint(bounds[i][0], bounds[i][1]) for i in range(len(bounds))]
best_score = obj_func(best)
current, current_score = best, best_score
scores = []
for i in range(n_iterations):
candidate = [
max(bounds[0][0], min(bounds[0][1], int(current[0] + random.gauss(0, 3)))),
max(bounds[1][0], min(bounds[1][1], int(current[1] + random.gauss(0, 5))))
]
candidate_score = obj_func(candidate)
if candidate_score > current_score:
current, current_score = candidate, candidate_score
else:
diff = candidate_score - current_score
metropolis = math.exp(diff / temp_start) if temp_start > 0 and diff < 0 else 0
if random.random() < metropolis: current, current_score = candidate, candidate_score if current_score > best_score:
best, best_score = current, current_score
scores.append(best_score)
temp_start *= cooling_rate
if i % 20 == 0:
print(f"Iteration {i}, Temp: {temp_start:.2f}, Best score: {best_score:.2f}, Params: {best}")
return best, best_score, scores
# Настройки оптимизации
bounds = [(2, 40), (41, 100)]
n_iterations = 100
temp_start = 100
cooling_rate = 0.95
# Запуск алгоритма
best_params, best_fitness, history = simulated_annealing(objective, bounds, n_iterations, temp_start, cooling_rate)
print("\nЛучшие параметры:")
print(f"Short EMA: {best_params[0]}, Long EMA: {best_params[1]}")
print(f"Fitness: {best_fitness:.2f}")
# Применяем лучшие параметры к данным
short_window, long_window = best_params
df['short_ma'] = df['close'].ewm(span=short_window, adjust=False).mean()
df['long_ma'] = df['close'].ewm(span=long_window, adjust=False).mean()
df['signal'] = np.where(df['short_ma'] > df['long_ma'], 1, 0)
df['position'] = df['signal'].diff()
df['returns'] = df['close'].pct_change()
df['strategy_returns'] = df['signal'].shift(1) * df['returns']
# График прогресса оптимизации
plt.figure(figsize=(10, 4))
if len(history) > 0 and any(x > -999 for x in history):
plt.plot(history, label="Best Fitness", color="blue")
else:
plt.text(0.5, 0.5, "Нет данных для отображения", ha='center', va='center')
plt.title("Прогресс оптимизации (Simulated Annealing)")
plt.xlabel("Итерация")
plt.ylabel("Fitness")
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.show()
# График цены и сигналов
plt.figure(figsize=(14, 8))
# Цена и EMA
plt.subplot(2, 1, 1)
plt.plot(df['close'], label='BTC/USDT', alpha=0.7)
plt.plot(df['short_ma'], label=f'Short EMA ({short_window})')
plt.plot(df['long_ma'], label=f'Long EMA ({long_window})')
buy_signals = df[df['position'] == 1].index
sell_signals = df[df['position'] == -1].index
plt.scatter(buy_signals, df.loc[buy_signals, 'close'], marker='^', color='g', s=100, label='Buy')
plt.scatter(sell_signals, df.loc[sell_signals, 'close'], marker='v', color='r', s=100, label='Sell')
plt.legend()
plt.title("Цена и экспоненциальные скользящие средние")
# Доходность
plt.subplot(2, 1, 2)
plt.plot((1 + df['returns']).cumprod(), label='Buy & Hold')
plt.plot((1 + df['strategy_returns']).cumprod(), label='EMA Strategy')
plt.legend()
plt.title("Сравнение доходности")
plt.tight_layout()
plt.show()
Лучшие параметры:
Short EMA: 12, Long EMA: 59
Рис. 2: График доходности торговой стратегии на 12-EMA и 59-EMA для Bitcoin рассчитанный методом отжига
Я скептически отношусь к классическому техническому анализу и торговым индикаторам. И выше один из примеров почему. Даже сложные вычисления оптимальных характеристик скользящих средних не дают никаких бонусов в торговле. Эти индикаторы постоянно запаздывают и безнадежно устарели.
Пожалуй, наибольшие перспективы есть у генетических алгоритмов, но построенных не на классических индикаторах, а на прогнозировании волатильности. Многие из таких сегодня широко используются в финансовой сфере, поскольку они могут эффективно исследовать сложные ландшафты параметров без застревания в локальных оптимумах, что часто происходит с градиентными методами оптимизации.
Деревья решений и «лесные» структуры
Деревья принятия решений и их ансамбли (случайный лес, градиентный бустинг) — мощные инструменты для анализа финансовых данных. Они успешно обрабатывают нелинейные зависимости и взаимодействия между признаками, что делает их особенно ценными для прогнозирования на финансовых рынках.
Основные преимущества деревьев и их ансамблей:
- Способность работать с нелинейными зависимостями;
- Устойчивость к шуму в данных;
- Автоматический отбор значимых признаков;
- Интерпретируемость результатов.
Ниже пример использования градиентного бустинга для анализа важности различных факторов при прогнозировании направления движения цены Bitcoin:
import ccxt
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score
# Загрузка данных
exchange = ccxt.kucoin()
ohlcv = exchange.fetch_ohlcv('BTC/USDT', timeframe='1d', limit=500)
df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms') + pd.Timedelta(hours=3)
df.set_index('timestamp', inplace=True)
# Функция для создания признаков
def create_features(df):
# Расчет различных индикаторов
# Доходности
df['returns'] = df['close'].pct_change()
df['direction'] = np.where(df['returns'] > 0, 1, 0)
# Лаговые переменные
for lag in range(1, 6):
df[f'returns_lag_{lag}'] = df['returns'].shift(lag)
# Волатильность
df['volatility_5d'] = df['returns'].rolling(5).std()
df['volatility_10d'] = df['returns'].rolling(10).std()
df['volatility_15d'] = df['returns'].rolling(15).std()
df['volatility_20d'] = df['returns'].rolling(20).std()
# Тренды (скользящие средние)
df['ema_9'] = df['close'].ewm(span=9, adjust=False).mean()
df['ema_15'] = df['close'].ewm(span=15, adjust=False).mean()
df['ema_21'] = df['close'].ewm(span=21, adjust=False).mean()
df['ema_30'] = df['close'].ewm(span=30, adjust=False).mean()
# Расстояние до скользящих средних
df['dist_ema_9'] = (df['close'] / df['ema_9'] - 1)
df['dist_ema_15'] = (df['close'] / df['ema_15'] - 1)
df['dist_ema_21'] = (df['close'] / df['ema_21'] - 1)
df['dist_ema_30'] = (df['close'] / df['ema_30'] - 1)
# Объемные индикаторы
df['volume_change'] = df['volume'].pct_change()
df['volume_ma_5'] = df['volume'].rolling(5).mean()
df['rel_volume'] = df['volume'] / df['volume_ma_5']
# Дневные диапазоны
df['day_range'] = (df['high'] - df['low']) / df['close']
df['body_ratio'] = abs(df['close'] - df['open']) / (df['high'] - df['low'])
return df
# Создание признаков
df_processed = create_features(df).dropna()
# Подготовка данных для модели
X = df_processed.drop(['open', 'high', 'low', 'close', 'volume', 'returns', 'direction', 'ema_9', 'ema_15', 'ema_21', 'ema_30'], axis=1)
y = df_processed['direction']
# Разделение на обучающую и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, shuffle=False)
# Обучение модели градиентного бустинга
model = GradientBoostingClassifier(n_estimators=500, learning_rate=0.01, max_depth=7, random_state=42)
model.fit(X_train, y_train)
# Оценка модели
y_pred = model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
report = classification_report(y_test, y_pred)
print(f"Точность модели: {accuracy:.4f}")
print("\nОтчет о классификации:")
print(report)
# Важность признаков
feature_importances = pd.DataFrame({
'feature': X.columns,
'importance': model.feature_importances_
}).sort_values('importance', ascending=False)
# Построение графика важности признаков
plt.figure(figsize=(12, 8))
sns.barplot(x='importance', y='feature', data=feature_importances)
plt.title('Важность признаков при прогнозировании направления цены Bitcoin')
plt.tight_layout()
plt.show()
print("\nВажность признаков:")
print(feature_importances.to_string())
Точность модели: 0.7083
Отчет о классификации:
precision recall f1-score support
0 0.69 0.75 0.72 72
1 0.73 0.67 0.70 72
accuracy 0.71 144
macro avg 0.71 0.71 0.71 144
weighted avg 0.71 0.71 0.71 144
Рис. 3: Диаграмма важности признаков при прогнозировании направления цены Bitcoin
В этом примере мы использовали градиентный бустинг для прогнозирования направления цены Bitcoin на основе различных технических индикаторов. Интересно отметить, что наиболее важными признаками оказались отклонение от скользящей средней EMA-9, дневной диапазон цен (body ratio) и изменение объема (volume change).
Точность модели составила около 71%, что лучше случайного угадывания, но все еще далеко от идеала. Это типичная ситуация для финансовых рынков — даже сложные алгоритмы могут дать лишь небольшое преимущество из-за высокой зашумленности и нестационарности данных.
Помимо того, стоит помнить что градиентные бустинги (как и любые другие модели машинного обучения, основанные на деревьях решений) склонны к переобучению. Вот почему крайне важно получив интересный результат обязательно протестировать его на множестве различных отрезков временных рядов и протестировать через кросс-валидацию.
Заключение
Алгоритмическое мышление становится все более важным навыком для современных трейдеров и инвесторов. В эпоху больших данных и сложных вычислительных систем понимание фундаментальных алгоритмов и умение их применять дает серьезное конкурентное преимущество на финансовых рынках.
Ключевые выводы:
- Базовые алгоритмические знания критичны. Даже простые алгоритмы сортировки и поиска могут значительно ускорить процесс анализа данных и принятия решений;
- Не все алгоритмы одинаково полезны. Традиционные индикаторы технического анализа часто дают результаты не лучше случайных, в то время как современные методы машинного обучения способны выявлять более сложные закономерности;
- Важна устойчивость к переобучению. Финансовые рынки генерируют много шума, поэтому предпочтение стоит отдавать робастным алгоритмам, способным обобщать закономерности, а не запоминать специфические паттерны исторических данных;
- Алгоритмический подход должен быть систематическим. Успешные стратегии основаны на строгой методологии и дисциплине, а не на интуитивных решениях;
- Постоянное обучение и адаптация необходимы. Финансовые рынки эволюционируют, и эффективные сегодня алгоритмы могут потерять свою силу завтра.
В завершение стоит отметить, что алгоритмы — это всего лишь инструменты, и их эффективность в конечном счете зависит от умения аналитика или трейдера грамотно их применять, учитывая специфику финансовых рынков и особенности конкретных торговых задач. Даже самые продвинутые алгоритмы машинного обучения не заменят понимания фундаментальных экономических процессов и рыночной психологии.