Жадные алгоритмы: базовые принципы и их применение в количественном анализе

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

Эффективность жадных алгоритмов обусловлена низкой вычислительной сложностью — большинство реализаций работают за O(n log n) или O(n²), что позволяет обрабатывать огромные объемы данных практически в реальном времени.

Принципы работы жадных алгоритмов

Локальная оптимизация решений

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

Ключевые характеристики жадного подхода:

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

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

Условия применимости жадного подхода

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

Признаки задач, решаемых жадным методом:

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

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

Отличия от динамического программирования

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

Сравнение подходов:

Критерий Жадный алгоритм Динамическое программирование
Сложность O(n log n) – O(n²) O(n²) – O(n³)
Память O(n) O(n²)
Гарантия оптимума Только для матроидов Для задач с оптимальной подструктурой
Пересмотр решений Нет Да
Применимость Ограничена Шире

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

Базовые примеры жадных алгоритмов

Задача о выборе активностей

Задача о выборе активностей демонстрирует классический жадный подход.

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

import numpy as np
import pandas as pd

def select_activities(start_times, end_times):
    """
    Жадный алгоритм выбора максимального числа непересекающихся активностей
    
    Parameters:
    start_times: массив времен начала
    end_times: массив времен окончания
    
    Returns:
    selected: индексы выбранных активностей
    """
    n = len(start_times)
    activities = [(start_times[i], end_times[i], i) for i in range(n)]
    
    # Сортировка по времени окончания - ключевой момент жадного подхода
    activities.sort(key=lambda x: x[1])
    
    selected = []
    last_end_time = -np.inf
    
    for start, end, idx in activities:
        if start >= last_end_time:
            selected.append(idx)
            last_end_time = end
    
    return selected

# Пример: торговые сессии на разных рынках
sessions = pd.DataFrame({
    'market': ['Asia', 'Europe', 'US_morning', 'US_afternoon', 'After_hours'],
    'start': [0, 6, 13, 15, 20],
    'end': [8, 14, 16, 21, 24]
})

selected_idx = select_activities(sessions['start'].values, sessions['end'].values)
selected_sessions = sessions.iloc[selected_idx]

print("Выбранные торговые сессии:")
print(selected_sessions[['market', 'start', 'end']])
Выбранные торговые сессии:
        market  start  end
0         Asia      0    8
2   US_morning     13   16
4  After_hours     20   24

Алгоритм работает со скоростью O(n log n) из-за сортировки. После сортировки по времени окончания жадный выбор гарантирует максимальное количество активностей: если выбрать активность с более поздним окончанием, останется меньше времени для последующих выборов.

В торговых системах эта логика применяется при распределении капитала между рынками с разными часовыми поясами. Выбор сессий с ранним закрытием освобождает капитал для последующих интересных возможностей для входа. Алгоритм также адаптируется для задач маршрутизации ордеров между площадками с разными периодами ликвидности.

Задача о рюкзаке с дробными весами

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

Стратегия: сортировать предметы по убыванию удельной ценности (value/weight) и брать в порядке убывания. Ниже пример кода.

import numpy as np
import matplotlib.pyplot as plt

def fractional_knapsack(values, weights, capacity):
    """
    Решение дробной задачи о рюкзаке жадным алгоритмом
    
    Parameters:
    values: ожидаемая доходность активов
    weights: требуемый капитал для каждого актива
    capacity: общий доступный капитал
    
    Returns:
    allocations: доли капитала на каждый актив
    total_value: итоговая ожидаемая доходность
    """
    n = len(values)
    
    # Расчет удельной ценности (доходность на единицу капитала)
    ratios = values / weights
    
    # Сортировка по убыванию удельной ценности
    items = sorted(enumerate(ratios), key=lambda x: x[1], reverse=True)
    
    allocations = np.zeros(n)
    remaining_capacity = capacity
    total_value = 0
    
    for idx, ratio in items:
        if weights[idx] <= remaining_capacity: # Берем актив полностью allocations[idx] = weights[idx] remaining_capacity -= weights[idx] total_value += values[idx] else: # Берем частично allocations[idx] = remaining_capacity total_value += ratio * remaining_capacity break # Нормализация к долям портфеля allocations = allocations / capacity return allocations, total_value # Пример: портфель акций с разной ожидаемой доходностью np.random.seed(42) tickers = ['ASML', 'TSM', 'BABA', 'JD', 'SHOP'] expected_returns = np.array([0.18, 0.22, 0.15, 0.12, 0.25]) # годовая доходность required_capital = np.array([5000, 8000, 3000, 4000, 6000]) # минимальный капитал total_capital = 15000 allocations, portfolio_return = fractional_knapsack( expected_returns * required_capital, required_capital, total_capital ) # Визуализация распределения капитала fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5)) # График 1: Распределение капитала capital_allocated = allocations * total_capital ax1.barh(tickers, capital_allocated, color='#2C3E50') ax1.set_xlabel('Распределенный капитал ($)', fontsize=11) ax1.set_title('Оптимальное распределение капитала', fontsize=12, fontweight='bold') ax1.grid(axis='x', alpha=0.3) # График 2: Удельная доходность vs выбор ratios = expected_returns / (required_capital / 1000) # доходность на $1000 colors = ['#27AE60' if a > 0 else '#95A5A6' for a in allocations]
ax2.scatter(ratios, expected_returns, s=allocations*1000+100, c=colors, alpha=0.6)

for i, ticker in enumerate(tickers):
    ax2.annotate(ticker, (ratios[i], expected_returns[i]), 
                xytext=(5, 5), textcoords='offset points', fontsize=9)

ax2.set_xlabel('Удельная доходность (на $1000)', fontsize=11)
ax2.set_ylabel('Годовая доходность', fontsize=11)
ax2.set_title('Критерий отбора активов', fontsize=12, fontweight='bold')
ax2.grid(True, alpha=0.3)

plt.tight_layout()

print(f"Портфельная доходность: {portfolio_return/total_capital:.2%}")
print("\nРаспределение по активам:")
for ticker, alloc, capital in zip(tickers, allocations, capital_allocated):
    if alloc > 0:
        print(f"{ticker}: {alloc:.1%} (${capital:,.0f})")
Портфельная доходность: 22.93%

Распределение по активам:
ASML: 6.7% ($1,000)
TSM: 53.3% ($8,000)
SHOP: 40.0% ($6,000)

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

Рис. 1: Оптимальное распределение капитала по жадному алгоритму. Левая панель показывает итоговое распределение капитала между активами. Правая панель демонстрирует логику отбора: размер точки пропорционален доле в портфеле, зеленые точки — выбранные активы, серые — отвергнутые. Алгоритм отдает приоритет активам с максимальной удельной доходностью на единицу капитала

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

👉🏻  Основы количественного анализа и моделирования финансовых рынков

Для целочисленной версии (без дробных долей) жадный подход дает приближенное решение. В портфельном менеджменте дробность допустима для крупных счетов, где минимальные лоты незначительны относительно общего капитала. При наличии ограничений на минимальные позиции требуется динамическое программирование или метод branch-and-bound.

Применение в количественном анализе

Оптимизация портфеля: жадный отбор активов

Жадный отбор активов применяется при формировании портфеля с ограничениями на количество позиций. Классическая оптимизация Марковица требует O(n³) операций для n активов, что неприемлемо для рынков из тысяч инструментов. Жадный подход снижает сложность до O(n² k), где k — размер портфеля.

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

import numpy as np
import yfinance as yf
import pandas as pd
from datetime import datetime, timedelta

def greedy_portfolio_selection(returns, n_assets=10, risk_free_rate=0.04):
    """
    Жадный отбор активов для портфеля
    
    Parameters:
    returns: DataFrame с дневными доходностями активов
    n_assets: целевое количество активов в портфеле
    risk_free_rate: безрисковая ставка (годовая)
    
    Returns:
    selected_assets: список выбранных тикеров
    weights: веса активов (равновзвешенные)
    sharpe_history: динамика коэффициента Шарпа при добавлении активов
    """
    all_assets = returns.columns.tolist()
    selected = []
    sharpe_history = []
    
    # Первый актив - с максимальным коэффициентом Шарпа
    initial_sharpes = {}
    for asset in all_assets:
        mean_return = returns[asset].mean() * 252
        std_return = returns[asset].std() * np.sqrt(252)
        sharpe = (mean_return - risk_free_rate) / std_return if std_return > 0 else -np.inf
        initial_sharpes[asset] = sharpe
    
    first_asset = max(initial_sharpes, key=initial_sharpes.get)
    selected.append(first_asset)
    sharpe_history.append(initial_sharpes[first_asset])
    
    # Жадное добавление активов
    remaining = [a for a in all_assets if a not in selected]
    
    for _ in range(n_assets - 1):
        if not remaining:
            break
            
        best_asset = None
        best_sharpe = -np.inf
        
        for candidate in remaining:
            # Пробный портфель с равными весами
            trial_portfolio = selected + [candidate]
            portfolio_returns = returns[trial_portfolio].mean(axis=1)
            
            mean_ret = portfolio_returns.mean() * 252
            std_ret = portfolio_returns.std() * np.sqrt(252)
            sharpe = (mean_ret - risk_free_rate) / std_ret if std_ret > 0 else -np.inf
            
            if sharpe > best_sharpe:
                best_sharpe = sharpe
                best_asset = candidate
        
        if best_asset:
            selected.append(best_asset)
            sharpe_history.append(best_sharpe)
            remaining.remove(best_asset)
    
    # Равновзвешенный портфель
    weights = np.ones(len(selected)) / len(selected)
    
    return selected, weights, sharpe_history

# Загрузка данных для emerging markets акций
tickers = ['TSM', 'ASML', 'NVO', 'BABA', 'SAP', 'TM', 'SNY', 
           'UL', 'SONY', 'SHOP', 'SE', 'MELI', 'PDD', 'GRAB']

end_date = datetime(2025, 9, 30)
start_date = end_date - timedelta(days=730)

data = yf.download(tickers, start=start_date, end=end_date, progress=False)

# Обработка Multiindex
if isinstance(data.columns, pd.MultiIndex):
    prices = data['Close']
else:
    prices = data

# Расчет дневных доходностей
returns = prices.pct_change().dropna()

# Жадный отбор активов
selected_assets, weights, sharpe_history = greedy_portfolio_selection(
    returns, n_assets=8
)

print("Выбранные активы в порядке добавления:")
for i, (asset, sharpe) in enumerate(zip(selected_assets, sharpe_history), 1):
    print(f"{i}. {asset}: Sharpe = {sharpe:.3f}")

print(f"\nВеса портфеля (равновзвешенный):")
for asset, weight in zip(selected_assets, weights):
    print(f"{asset}: {weight:.1%}")

# Сравнение с равновзвешенным портфелем из всех активов
full_portfolio_returns = returns.mean(axis=1)
full_sharpe = (full_portfolio_returns.mean() * 252 - 0.04) / (full_portfolio_returns.std() * np.sqrt(252))

selected_portfolio_returns = returns[selected_assets].mean(axis=1)
selected_sharpe = (selected_portfolio_returns.mean() * 252 - 0.04) / (selected_portfolio_returns.std() * np.sqrt(252))

print(f"\nСравнение результатов:")
print(f"Полный портфель (все {len(tickers)} активов): Sharpe = {full_sharpe:.3f}")
print(f"Жадный портфель ({len(selected_assets)} активов): Sharpe = {selected_sharpe:.3f}")
print(f"Улучшение: {(selected_sharpe/full_sharpe - 1)*100:.1f}%")
Выбранные активы в порядке добавления:
1. SE: Sharpe = 1.655
2. TSM: Sharpe = 1.961
3. SAP: Sharpe = 2.049
4. UL: Sharpe = 2.111
5. BABA: Sharpe = 2.136
6. MELI: Sharpe = 2.149
7. SONY: Sharpe = 2.138
8. SHOP: Sharpe = 2.071

Веса портфеля (равновзвешенный):
SE: 12.5%
TSM: 12.5%
SAP: 12.5%
UL: 12.5%
BABA: 12.5%
MELI: 12.5%
SONY: 12.5%
SHOP: 12.5%

Сравнение результатов:
Полный портфель (все 14 активов): Sharpe = 1.531
Жадный портфель (8 активов): Sharpe = 2.071
Улучшение: 35.3%

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

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

Практическое применение: предварительный скрининг активов для последующей точной оптимизации. Жадный алгоритм сужает пространство поиска с 1000 активов до 20-30, после чего применяется классическая оптимизация Марковица или риск-паритет (risk parity).

👉🏻  Изучаем опционы на Netflix: комплексный анализ и стратегии

Forward Feature Selection для предиктивных моделей

Forward Feature Selection — жадный метод отбора признаков для машинного обучения. Алгоритм начинает с пустого множества фичей и на каждом шаге добавляет признак, максимизирующий метрику качества модели на кросс-валидации. В отличие от рекурсивного исключения признаков, forward selection эффективнее при большом числе фичей.

import numpy as np
import pandas as pd
import yfinance as yf
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import mean_squared_error
from datetime import datetime, timedelta

def forward_feature_selection(X, y, max_features=10, cv_splits=5):
    """
    Жадный forward selection признаков с временной кросс-валидацией
    
    Parameters:
    X: DataFrame с признаками
    y: целевая переменная (доходность)
    max_features: максимальное число отбираемых признаков
    cv_splits: количество фолдов для кросс-валидации
    
    Returns:
    selected_features: список выбранных признаков в порядке важности
    scores_history: динамика RMSE при добавлении признаков
    """
    available_features = list(X.columns)
    selected_features = []
    scores_history = []
    
    tscv = TimeSeriesSplit(n_splits=cv_splits)
    
    for iteration in range(max_features):
        if not available_features:
            break
        
        best_feature = None
        best_score = np.inf
        
        for feature in available_features:
            # Пробный набор признаков
            trial_features = selected_features + [feature]
            X_trial = X[trial_features]
            
            # Кросс-валидация
            cv_scores = []
            for train_idx, val_idx in tscv.split(X_trial):
                X_train, X_val = X_trial.iloc[train_idx], X_trial.iloc[val_idx]
                y_train, y_val = y.iloc[train_idx], y.iloc[val_idx]
                
                model = RandomForestRegressor(
                    n_estimators=100,
                    max_depth=5,
                    random_state=42,
                    n_jobs=-1
                )
                model.fit(X_train, y_train)
                y_pred = model.predict(X_val)
                
                rmse = np.sqrt(mean_squared_error(y_val, y_pred))
                cv_scores.append(rmse)
            
            avg_score = np.mean(cv_scores)
            
            if avg_score < best_score: best_score = avg_score best_feature = feature if best_feature: selected_features.append(best_feature) scores_history.append(best_score) available_features.remove(best_feature) print(f"Итерация {iteration + 1}: добавлен {best_feature}, RMSE = {best_score:.6f}") return selected_features, scores_history # Построение технических индикаторов для предсказания доходности ticker = 'TSM' end_date = datetime(2025, 1, 31) start_date = end_date - timedelta(days=1095) data = yf.download(ticker, start=start_date, end=end_date, progress=False) prices = data['Close'] # Инженерия признаков df = pd.DataFrame(index=prices.index) # Лаги доходностей returns = prices.pct_change() for lag in [1, 2, 3, 5, 10, 20]: df[f'return_lag_{lag}'] = returns.shift(lag) # Скользящие средние for window in [5, 10, 20, 50]: df[f'ma_{window}'] = prices.rolling(window).mean() / prices - 1 # Волатильность for window in [5, 10, 20]: df[f'volatility_{window}'] = returns.rolling(window).std() # RSI (упрощенная версия) def calculate_rsi(prices, period=14): delta = prices.diff() gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
    loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
    rs = gain / loss
    return 100 - (100 / (1 + rs))

df['rsi_14'] = calculate_rsi(prices)

# Объем (нормализованный)
df['volume_norm'] = (data['Volume'] - data['Volume'].rolling(20).mean()) / data['Volume'].rolling(20).std()

# Целевая переменная - доходность через 5 дней
df['target'] = returns.shift(-5)

# Удаление пропусков
df = df.dropna()

# Разделение на признаки и цель
X = df.drop('target', axis=1)
y = df['target']

# Forward feature selection
selected_features, scores_history = forward_feature_selection(
    X, y, max_features=8, cv_splits=5
)

print(f"\nФинальный набор признаков ({len(selected_features)}):")
for i, feature in enumerate(selected_features, 1):
    print(f"{i}. {feature}")

# Оценка модели с выбранными признаками
X_selected = X[selected_features]
tscv = TimeSeriesSplit(n_splits=5)

final_scores = []
for train_idx, test_idx in tscv.split(X_selected):
    X_train, X_test = X_selected.iloc[train_idx], X_selected.iloc[test_idx]
    y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]
    
    model = RandomForestRegressor(n_estimators=200, max_depth=6, random_state=42)
    model.fit(X_train, y_train)
    
    y_pred = model.predict(X_test)
    rmse = np.sqrt(mean_squared_error(y_test, y_pred))
    final_scores.append(rmse)

print(f"\nРезультаты кросс-валидации финальной модели:")
print(f"Средний RMSE: {np.mean(final_scores):.6f}")
print(f"Стандартное отклонение: {np.std(final_scores):.6f}")
Итерация 1: добавлен rsi_14, RMSE = 0.024978
Итерация 2: добавлен return_lag_10, RMSE = 0.024227
Итерация 3: добавлен ma_50, RMSE = 0.024221
Итерация 4: добавлен volatility_5, RMSE = 0.024181
Итерация 5: добавлен return_lag_3, RMSE = 0.024185
Итерация 6: добавлен ma_10, RMSE = 0.024203
Итерация 7: добавлен volatility_10, RMSE = 0.024256
Итерация 8: добавлен volume_norm, RMSE = 0.024246

Финальный набор признаков (8):
1. rsi_14
2. return_lag_10
3. ma_50
4. volatility_5
5. return_lag_3
6. ma_10
7. volatility_10
8. volume_norm

Результаты кросс-валидации финальной модели:
Средний RMSE: 0.024346
Стандартное отклонение: 0.003596

Метод Forward selection работает за O(n² · m · k), где n — число признаков, m — размер выборки, k — сложность обучения модели. Для 100 признаков и 1000 наблюдений алгоритм завершается за 2-5 минут, что приемлемо для регулярного переобучения моделей.

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

  • Рекурсивное исключение признаков (RFE);
  • Регуляризация L1 (Lasso);
  • Алгоритмы на основе важности признаков (Permutation Importance).

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

  1. Использовать forward selection для предварительного сокращения пространства признаков с 500-1000 до 30-50;
  2. Затем применять RFE или Lasso для финального отбора.

Важно также не забывать про временную кросс-валидацию: стандартная k-fold CV плохо применима для финансовых временных рядов, так как создает утечку в будущее (look-ahead bias), завышая оценки качества модели.

Оптимизация исполнения ордеров

Задача: исполнить крупный ордер на покупку N акций, минимизируя транзакционные издержки и маркет-импакт.

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

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

def greedy_order_execution(total_volume, time_slots, available_liquidity, 
                           spreads, max_volume_per_slot=0.1):
    """
    Жадное исполнение крупного ордера с минимизацией маркет-импакта
    
    Parameters:
    total_volume: общий объем для исполнения
    time_slots: массив временных слотов
    available_liquidity: доступный объем в каждом слоте
    spreads: bid-ask spread в каждом слоте (в базисных пунктах)
    max_volume_per_slot: макс. доля ликвидности на слот (антидетекция)
    
    Returns:
    execution_schedule: план исполнения по слотам
    total_cost: общая стоимость маркет-импакта
    """
    n_slots = len(time_slots)
    execution_schedule = np.zeros(n_slots)
    remaining_volume = total_volume
    
    # Расчет стоимости исполнения единицы объема в каждом слоте
    # Учитываем спред и квадратичный маркет-импакт
    unit_costs = spreads * (1 + 0.5 * np.random.uniform(0.8, 1.2, n_slots))
    
    # Жадное распределение объема
    for _ in range(n_slots):
        if remaining_volume <= 0: break # Выбор слота с минимальной стоимостью исполнения eligible_slots = np.where(execution_schedule == 0)[0] if len(eligible_slots) == 0: break # Среди неиспользованных слотов находим лучший best_slot = eligible_slots[np.argmin(unit_costs[eligible_slots])] # Объем для исполнения с учетом ограничений max_allowed = available_liquidity[best_slot] * max_volume_per_slot execute_volume = min(remaining_volume, max_allowed) execution_schedule[best_slot] = execute_volume remaining_volume -= execute_volume # Расчет общей стоимости маркет-импакта total_cost = np.sum(execution_schedule * unit_costs) return execution_schedule, total_cost, unit_costs # Симуляция внутридневной ликвидности np.random.seed(42) n_slots = 24 # торговых часов time_slots = np.arange(9.5, 16, 0.25) # с 9:30 до 16:00, каждые 15 минут n_slots = len(time_slots) # Ликвидность следует U-образной кривой (высокая на открытии/закрытии) base_liquidity = 10000 liquidity_pattern = 1 + 0.5 * (np.cos(np.linspace(0, np.pi, n_slots)) + 1) available_liquidity = base_liquidity * liquidity_pattern * np.random.uniform(0.9, 1.1, n_slots) # Спред обратно коррелирует с ликвидностью spreads = 5 + 10 / (liquidity_pattern + 0.5) + np.random.uniform(-1, 1, n_slots) # Исполнение крупного ордера на 50,000 акций total_volume = 50000 execution_schedule, total_cost, unit_costs = greedy_order_execution( total_volume, time_slots, available_liquidity, spreads, max_volume_per_slot=0.08 ) # Сравнение с равномерным исполнением (TWAP) twap_volume_per_slot = total_volume / n_slots twap_cost = np.sum(twap_volume_per_slot * unit_costs) print(f"Жадное исполнение:") print(f" Общий объем: {execution_schedule.sum():,.0f}") print(f" Стоимость маркет-импакта: ${total_cost:,.2f}") print(f" Средняя стоимость: {total_cost/total_volume:.4f} $/акция") print(f"\nTWAP исполнение:") print(f" Стоимость маркет-импакта: ${twap_cost:,.2f}") print(f" Средняя стоимость: {twap_cost/total_volume:.4f} $/акция") print(f"\nУлучшение: {(1 - total_cost/twap_cost)*100:.1f}%") # Визуализация стратегии исполнения fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(14, 10)) # График 1: Профиль ликвидности и стоимость исполнения ax1_twin = ax1.twinx() ax1.plot(time_slots, available_liquidity/1000, color='#3498DB', linewidth=2, label='Ликвидность') ax1_twin.plot(time_slots, spreads, color='#E74C3C', linewidth=2, label='Спред', linestyle='--') ax1.set_ylabel('Доступная ликвидность (тыс. акций)', color='#3498DB', fontsize=11) ax1_twin.set_ylabel('Bid-Ask спред (bps)', color='#E74C3C', fontsize=11) ax1.set_title('Внутридневная динамика рынка', fontsize=12, fontweight='bold') ax1.tick_params(axis='y', labelcolor='#3498DB') ax1_twin.tick_params(axis='y', labelcolor='#E74C3C') ax1.grid(True, alpha=0.3) ax1.legend(loc='upper left') ax1_twin.legend(loc='upper right') # График 2: План исполнения ордера ax2.bar(time_slots, execution_schedule/1000, width=0.15, color='#27AE60', alpha=0.7, label='Жадное исполнение') ax2.axhline(y=twap_volume_per_slot/1000, color='#95A5A6', linestyle='--', linewidth=2, label='TWAP') ax2.set_ylabel('Объем исполнения (тыс. акций)', fontsize=11) ax2.set_title('Сравнение стратегий исполнения', fontsize=12, fontweight='bold') ax2.legend() ax2.grid(True, alpha=0.3) # График 3: Кумулятивное исполнение cumulative_greedy = np.cumsum(execution_schedule) cumulative_twap = np.cumsum(np.ones(n_slots) * twap_volume_per_slot) ax3.plot(time_slots, cumulative_greedy/1000, color='#27AE60', linewidth=2.5, label='Жадное') ax3.plot(time_slots, cumulative_twap/1000, color='#95A5A6', linewidth=2, linestyle='--', label='TWAP') ax3.fill_between(time_slots, cumulative_greedy/1000, cumulative_twap/1000, alpha=0.2, color='#27AE60') ax3.set_xlabel('Время торгов (часы)', fontsize=11) ax3.set_ylabel('Кумулятивный объем (тыс. акций)', fontsize=11) ax3.set_title('Прогресс исполнения ордера', fontsize=12, fontweight='bold') ax3.legend() ax3.grid(True, alpha=0.3) plt.tight_layout() # Детальный отчет по исполнению executed_slots = np.where(execution_schedule > 0)[0]
print(f"\nДетальный план исполнения ({len(executed_slots)} слотов):")
for slot in executed_slots[:10]:  # первые 10 слотов
    time = time_slots[slot]
    volume = execution_schedule[slot]
    cost = unit_costs[slot]
    print(f"  {time:.2f}: {volume:,.0f} акций @ {cost:.4f} $/акция")
Жадное исполнение:
  Общий объем: 30,979
  Стоимость маркет-импакта: $458,031.63
  Средняя стоимость: 9.1606 $/акция

TWAP исполнение:
  Стоимость маркет-импакта: $757,637.27
  Средняя стоимость: 15.1527 $/акция

Улучшение: 39.5%

Детальный план исполнения (26 слотов):
  9.50: 1,560 акций @ 13.3373 $/акция
  9.75: 1,741 акций @ 14.2656 $/акция
  10.00: 1,661 акций @ 13.9955 $/акция
  10.25: 1,603 акций @ 12.9125 $/акция
  10.50: 1,444 акций @ 13.2081 $/акция
  10.75: 1,419 акций @ 12.2330 $/акция
  11.00: 1,360 акций @ 11.7787 $/акция
  11.25: 1,562 акций @ 14.9591 $/акция
  11.50: 1,443 акций @ 15.2807 $/акция
  11.75: 1,427 акций @ 14.7401 $/акция

Оптимизация исполнения крупного ордера. Верхняя панель показывает внутридневную динамику ликвидности (синяя линия) и спреда (красная пунктирная). Средняя панель сравнивает жадное исполнение (зеленые столбцы) с равномерным TWAP (серая линия). Нижняя панель демонстрирует кумулятивный прогресс: жадный алгоритм концентрирует исполнение в периоды высокой ликвидности и низких спредов, снижая общую стоимость маркет-импакта

Рис. 2: Оптимизация исполнения крупного ордера. Верхняя панель показывает внутридневную динамику ликвидности (синяя линия) и спреда (красная пунктирная). Средняя панель сравнивает жадное исполнение (зеленые столбцы) с равномерным TWAP (серая линия). Нижняя панель демонстрирует кумулятивный прогресс: жадный алгоритм концентрирует исполнение в периоды высокой ликвидности и низких спредов, снижая общую стоимость маркет-импакта

Жадное исполнение ордеров на бирже превосходит TWAP на 20-40% по стоимости маркет-импакта, концентрируя объем в оптимальные моменты. Ограничение max_volume_per_slot затрудняет обнаружение алгоритма участниками рынка. Более сложные версии учитывают прогноз ликвидности на основе исторических паттернов и реагируют на изменения рыночных условий в реальном времени.

👉🏻  Техники энкодинга (encoding) категориальных атрибутов

Альтернативные подходы:

  • VWAP (volume-weighted average price) исполняет ордера пропорционально историческому профилю объемов;
  • POV (percentage of volume) поддерживает фиксированную долю рыночного объема.

Жадный алгоритм адаптивнее, однако требует точного прогноза ликвидности и спредов на горизонте исполнения.

Ребалансировка с минимизацией транзакционных издержек

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

Давайте рассмотрим как можно сделать такую ребалансировку с помощью Python:

import numpy as np
import pandas as pd

def greedy_rebalancing(current_weights, target_weights, trading_costs, 
                       portfolio_value, min_trade_threshold=0.001):
    """
    Жадная ребалансировка портфеля с минимизацией транзакционных издержек
    
    Parameters:
    current_weights: текущие веса активов
    target_weights: целевые веса
    trading_costs: стоимость торговли каждого актива (в % от объема)
    portfolio_value: общая стоимость портфеля
    min_trade_threshold: минимальное отклонение для торговли
    
    Returns:
    trades: объемы торговли для каждого актива
    total_cost: общие транзакционные издержки
    final_weights: веса после ребалансировки
    """
    n_assets = len(current_weights)
    
    # Отклонения от целевых весов
    deviations = target_weights - current_weights
    
    # Стоимость корректировки единицы отклонения
    correction_costs = trading_costs * np.abs(deviations)
    
    # Сортировка активов по приоритету корректировки:
    # большое отклонение + низкая стоимость = высокий приоритет
    priorities = np.abs(deviations) / (correction_costs + 1e-10)
    sorted_indices = np.argsort(-priorities)
    
    trades = np.zeros(n_assets)
    total_cost = 0
    adjusted_weights = current_weights.copy()
    
    # Жадная корректировка позиций
    for idx in sorted_indices:
        deviation = target_weights[idx] - adjusted_weights[idx]
        
        # Игнорируем малые отклонения
        if np.abs(deviation) < min_trade_threshold:
            continue
        
        # Расчет необходимой торговли
        trade_weight = deviation
        trade_value = portfolio_value * np.abs(trade_weight)
        cost = trade_value * trading_costs[idx]
        
        # Исполнение торговли
        trades[idx] = trade_weight * portfolio_value
        adjusted_weights[idx] += trade_weight
        total_cost += cost
    
    return trades, total_cost, adjusted_weights

# Пример: ребалансировка multi-asset портфеля
assets = ['TSM', 'ASML', 'SAP', 'TM', 'SHOP', 'UL', 'SNY', 'SONY', 
          'NVO', 'BABA', 'SE', 'MELI']

# Текущие веса (drift после роста рынка)
np.random.seed(42)
current_weights = np.random.dirichlet(np.ones(len(assets)) * 5)

# Целевые веса (равновзвешенный портфель)
target_weights = np.ones(len(assets)) / len(assets)

# Стоимость торговли варьируется по активам (ликвидность, спреды)
# Более ликвидные активы имеют меньшие издержки
trading_costs = np.array([0.0008, 0.0010, 0.0012, 0.0015, 0.0020, 
                          0.0018, 0.0014, 0.0011, 0.0009, 0.0025,
                          0.0030, 0.0028])

portfolio_value = 1_000_000

# Жадная ребалансировка
trades, total_cost, final_weights = greedy_rebalancing(
    current_weights, target_weights, trading_costs, portfolio_value
)

# Сравнение с полной ребалансировкой (naive approach)
naive_trades = (target_weights - current_weights) * portfolio_value
naive_cost = np.sum(np.abs(naive_trades) * trading_costs)

print("Результаты жадной ребалансировки:\n")
print(f"{'Актив':<8} {'Текущий':<10} {'Целевой':<10} {'Торговля ($)':<15} {'Финальный':<10}")
print("-" * 63)

for i, asset in enumerate(assets):
    print(f"{asset:<8} {current_weights[i]:>8.2%} {target_weights[i]:>9.2%} "
          f"{trades[i]:>13,.0f} {final_weights[i]:>9.2%}")

print(f"\nТранзакционные издержки:")
print(f"  Жадная ребалансировка: ${total_cost:,.2f} ({total_cost/portfolio_value:.3%})")
print(f"  Полная ребалансировка: ${naive_cost:,.2f} ({naive_cost/portfolio_value:.3%})")
print(f"  Экономия: ${naive_cost - total_cost:,.2f} ({(1-total_cost/naive_cost)*100:.1f}%)")

# Оценка качества ребалансировки
tracking_error = np.sqrt(np.sum((final_weights - target_weights)**2))
print(f"\nОтклонение от целевых весов (tracking error): {tracking_error:.4f}")

# Анализ приоритетов корректировки
deviations = np.abs(target_weights - current_weights)
priorities = deviations / (trading_costs * deviations + 1e-10)

print(f"\nТоп-5 активов по приоритету корректировки:")
top_priorities = np.argsort(-priorities)[:5]
for rank, idx in enumerate(top_priorities, 1):
    print(f"{rank}. {assets[idx]}: отклонение {deviations[idx]:.2%}, "
          f"стоимость {trading_costs[idx]:.3%}")
Результаты жадной ребалансировки:

Актив    Текущий    Целевой    Торговля ($)    Финальный 
---------------------------------------------------------------
TSM         9.91%     8.33%       -15,799     8.33%
ASML        7.45%     8.33%         8,877     8.33%
SAP         7.11%     8.33%        12,203     8.33%
TM          7.11%     8.33%        12,203     8.33%
SHOP       15.28%     8.33%       -69,466     8.33%
UL         11.11%     8.33%       -27,791     8.33%
SNY         6.34%     8.33%        19,943     8.33%
SONY       10.11%     8.33%       -17,766     8.33%
NVO         8.87%     8.33%        -5,332     8.33%
BABA        2.78%     8.33%        55,527     8.33%
SE          4.77%     8.33%             0     4.77%
MELI        9.16%     8.33%        -8,223     8.33%

Транзакционные издержки:
  Жадная ребалансировка: $457.53 (0.046%)
  Полная ребалансировка: $564.40 (0.056%)
  Экономия: $106.88 (18.9%)

Отклонение от целевых весов (tracking error): 0.0356

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

👉🏻  Метрики качества ML-моделей: Accuracy, Precision, Recall, F1 Score, ROC-AUC

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

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

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

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

Жадная оптимизация для алгоритмов бустинга

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

import numpy as np
import pandas as pd
import yfinance as yf
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import mean_squared_error, r2_score
import xgboost as xgb
import matplotlib.pyplot as plt
from datetime import datetime, timedelta

# Параметры
ticker = "ASML"
end_date = datetime(2025, 9, 30)
start_date = end_date - timedelta(days=1825)  # 5 лет

# Загрузка данных
data = yf.download(ticker, start=start_date, end=end_date, progress=False, auto_adjust=True)
prices = data['Close']

# Доходности
returns = prices.pct_change().squeeze()
returns.name = 'return'

# Построение признаков
features_df = pd.DataFrame(index=prices.index)

for period in [5, 10, 20, 60]:
    features_df[f'momentum_{period}'] = returns.rolling(period).sum()
for period in [5, 10, 20]:
    features_df[f'volatility_{period}'] = returns.rolling(period).std()
for period in [20, 60]:
    features_df[f'skew_{period}'] = returns.rolling(period).skew()
    features_df[f'kurt_{period}'] = returns.rolling(period).kurt()
for period in [20, 60, 120]:
    rolling_max = prices.rolling(period).max()
    rolling_min = prices.rolling(period).min()
    features_df[f'price_position_{period}'] = (prices - rolling_min) / (rolling_max - rolling_min)
for lag in range(1, 6):
    features_df[f'return_lag_{lag}'] = returns.shift(lag)

# Целевая переменная
target = pd.DataFrame(returns.shift(-5)).rename(columns={'return': 'target'})

# Объединение и очистка
df_model = pd.concat([features_df, target], axis=1).dropna()
X = df_model.drop('target', axis=1)
y = df_model['target']

# Временная кросс-валидация
tscv = TimeSeriesSplit(n_splits=5)
fold_results = []
feature_importance_list = []

params = {
    'objective': 'reg:squarederror',
    'tree_method': 'hist',
    'max_depth': 4,
    'learning_rate': 0.1,
    'subsample': 0.8,
    'colsample_bytree': 0.8,
    'min_child_weight': 1,
    'gamma': 0,
    'reg_alpha': 0,
    'reg_lambda': 1.0
}

for fold, (train_idx, val_idx) in enumerate(tscv.split(X), 1):
    X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]
    y_train, y_val = y.iloc[train_idx], y.iloc[val_idx]

    dtrain = xgb.DMatrix(X_train, label=y_train)
    dval = xgb.DMatrix(X_val, label=y_val)

    model = xgb.train(
        params,
        dtrain,
        num_boost_round=500,
        evals=[(dtrain, 'train'), (dval, 'val')],
        early_stopping_rounds=50,
        verbose_eval=False
    )

    y_pred_train = model.predict(dtrain)
    y_pred_val = model.predict(dval)

    train_rmse = np.sqrt(mean_squared_error(y_train, y_pred_train))
    val_rmse = np.sqrt(mean_squared_error(y_val, y_pred_val))
    val_r2 = r2_score(y_val, y_pred_val)

    fold_results.append({
        'fold': fold,
        'train_rmse': train_rmse,
        'val_rmse': val_rmse,
        'val_r2': val_r2,
        'n_trees': model.best_iteration
    })

    # Важность признаков (gain)
    importance = model.get_score(importance_type='gain')
    feature_importance_list.append(importance)

    print(f"Fold {fold}: Val RMSE = {val_rmse:.6f}, R² = {val_r2:.4f}, Trees = {model.best_iteration}")

# Средние метрики
results_df = pd.DataFrame(fold_results)
print(f"\nСредние метрики по фолдам:")
print(f"  Train RMSE: {results_df['train_rmse'].mean():.6f}")
print(f"  Val RMSE: {results_df['val_rmse'].mean():.6f}")
print(f"  Val R²: {results_df['val_r2'].mean():.4f}")
print(f"  Среднее число деревьев: {results_df['n_trees'].mean():.0f}")

# Агрегированная важность признаков
all_features = X.columns
aggregated_importance = {}
for feature in all_features:
    gains = [imp.get(feature, 0) for imp in feature_importance_list]
    aggregated_importance[feature] = np.mean(gains)

top_features = sorted(aggregated_importance.items(), key=lambda x: x[1], reverse=True)[:10]
print(f"\nТоп-10 признаков по важности (gain):")
for rank, (feature, importance) in enumerate(top_features, 1):
    print(f"{rank:2d}. {feature:<25} {importance:>10.4f}")

# Визуализация
fig, ax = plt.subplots(figsize=(10,6))
features_plot = [f[0] for f in top_features]
importance_plot = [f[1] for f in top_features]
ax.barh(features_plot, importance_plot, color='#2C3E50')
ax.set_xlabel('Feature Importance')
ax.set_title('Топ-10 признаков XGBoost')
ax.invert_yaxis()
plt.show()
Fold 1: Val RMSE = 0.033435, R² = -0.1931, Trees = 0
Fold 2: Val RMSE = 0.028492, R² = -0.1089, Trees = 1
Fold 3: Val RMSE = 0.021034, R² = -0.0780, Trees = 1
Fold 4: Val RMSE = 0.031098, R² = -0.0972, Trees = 1
Fold 5: Val RMSE = 0.027133, R² = -0.0885, Trees = 8

Средние метрики по фолдам:
  Train RMSE: 0.014216
  Val RMSE: 0.028238
  Val R²: -0.1131
  Среднее число деревьев: 2

Топ-10 признаков по важности (gain):
 1. price_position_120            0.0027
 2. return_lag_5                  0.0022
 3. return_lag_3                  0.0022
 4. return_lag_1                  0.0021
 5. momentum_60                   0.0021
 6. skew_20                       0.0020
 7. kurt_60                       0.0019
 8. volatility_10                 0.0019
 9. return_lag_2                  0.0019
10. volatility_20                 0.0018

Топ-10 признаков по важности для обучения модели XGBoost

Рис. 3: Топ-10 признаков по важности для обучения модели XGBoost

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

  1. Сначала создаются признаки на основе исторических цен и доходностей: моменты, волатильность, скошенность и асимметрия доходностей за разные периоды, а также позиция цены относительно локальных максимумов и минимумов;
  2. Затем добавляются лаги доходности, чтобы учесть автокорреляцию;
  3. Признаки подаются в модель. Целевая переменная — будущая доходность через 5 дней;
  4. Модель обучается с использованием xgb.train и ранней остановки, что позволяет жадно добавлять деревья до момента, когда новые деревья перестают улучшать метрику на валидации.
👉🏻  Лаговые переменные и их правильное использование. Избегаем data leakage в финансовых моделях

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

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

Построение торговых расписаний

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

import numpy as np
import pandas as pd
from datetime import datetime, time, timedelta
class TradingScheduler:
"""
Жадный планировщик исполнения торговых стратегий
"""
def __init__(self, strategies, time_slots, max_concurrent=3):
"""
Parameters:
strategies: список словарей с параметрами стратегий
time_slots: доступные временные слоты
max_concurrent: максимум одновременно работающих стратегий
"""
self.strategies = strategies
self.time_slots = time_slots
self.max_concurrent = max_concurrent
self.schedule = {slot: [] for slot in time_slots}
def calculate_priority(self, strategy, slot):
"""
Расчет приоритета размещения стратегии в слоте
Учитывает: ожидаемую доходность, оптимальное время, API лимиты
"""
# Базовый приоритет - ожидаемая доходность
base_priority = strategy['expected_return']
# Штраф за отклонение от оптимального времени
optimal_hour = strategy['optimal_hour']
slot_hour = slot.hour + slot.minute / 60
time_penalty = 1 - min(abs(slot_hour - optimal_hour) / 12, 1) * 0.3
# Бонус за низкое использование API в этом слоте
current_api_usage = sum(s['api_calls'] for s in self.schedule[slot])
api_bonus = 1 + (100 - current_api_usage) / 200
# Штраф за высокую корреляцию с уже размещенными стратегиями
correlation_penalty = 1.0
for placed_strategy in self.schedule[slot]:
corr = strategy['correlation_matrix'].get(placed_strategy['name'], 0)
correlation_penalty *= (1 - abs(corr) * 0.2)
priority = base_priority * time_penalty * api_bonus * correlation_penalty
return priority
def can_place_strategy(self, strategy, slot):
"""
Проверка возможности размещения стратегии в слоте
"""
# Проверка лимита одновременных стратегий
if len(self.schedule[slot]) >= self.max_concurrent:
return False
# Проверка API лимитов
current_api = sum(s['api_calls'] for s in self.schedule[slot])
if current_api + strategy['api_calls'] > 100:
return False
# Проверка вычислительных ресурсов
current_cpu = sum(s['cpu_usage'] for s in self.schedule[slot])
if current_cpu + strategy['cpu_usage'] > 80:
return False
return True
def greedy_schedule(self):
"""
Жадное построение расписания
"""
unscheduled = self.strategies.copy()
while unscheduled:
best_placement = None
best_priority = -np.inf
# Поиск лучшего размещения среди всех комбинаций
for strategy in unscheduled:
for slot in self.time_slots:
if not self.can_place_strategy(strategy, slot):
continue
priority = self.calculate_priority(strategy, slot)
if priority > best_priority:
best_priority = priority
best_placement = (strategy, slot)
# Если не нашли размещение - прерываем
if best_placement is None:
print(f"Не удалось разместить {len(unscheduled)} стратегий")
break
# Размещаем стратегию
strategy, slot = best_placement
self.schedule[slot].append(strategy)
unscheduled.remove(strategy)
return self.schedule
def print_schedule(self):
"""
Вывод расписания
"""
print("\n" + "="*80)
print("ТОРГОВОЕ РАСПИСАНИЕ")
print("="*80)
total_return = 0
for slot in sorted(self.schedule.keys()):
strategies = self.schedule[slot]
if not strategies:
continue
print(f"\n{slot.strftime('%H:%M')} - {(slot + timedelta(minutes=30)).strftime('%H:%M')}")
print("-" * 80)
slot_api = sum(s['api_calls'] for s in strategies)
slot_cpu = sum(s['cpu_usage'] for s in strategies)
slot_return = sum(s['expected_return'] for s in strategies)
for strategy in strategies:
print(f"  • {strategy['name']:<25} " f"Return: {strategy['expected_return']:>6.2%}  "
f"API: {strategy['api_calls']:>3}  "
f"CPU: {strategy['cpu_usage']:>3}%")
print(f"\n  Итого по слоту: Return: {slot_return:>6.2%}, "
f"API: {slot_api}/100, CPU: {slot_cpu}/80%")
total_return += slot_return
print("\n" + "="*80)
print(f"Суммарная ожидаемая доходность: {total_return:.2%}")
print("="*80 + "\n")
# Определение стратегий с параметрами
strategies = [
{
'name': 'Mean_Reversion_Asia',
'expected_return': 0.08,
'optimal_hour': 2,  # Asian session
'api_calls': 25,
'cpu_usage': 15,
'correlation_matrix': {}
},
{
'name': 'Momentum_Europe',
'expected_return': 0.12,
'optimal_hour': 9,  # European open
'api_calls': 30,
'cpu_usage': 20,
'correlation_matrix': {'Mean_Reversion_Asia': -0.3}
},
{
'name': 'Statistical_Arb_US',
'expected_return': 0.15,
'optimal_hour': 14,  # US session
'api_calls': 40,
'cpu_usage': 35,
'correlation_matrix': {'Momentum_Europe': 0.4, 'Mean_Reversion_Asia': -0.2}
},
{
'name': 'Pairs_Trading_Tech',
'expected_return': 0.10,
'optimal_hour': 15,
'api_calls': 35,
'cpu_usage': 25,
'correlation_matrix': {'Statistical_Arb_US': 0.6}
},
{
'name': 'Vol_Arbitrage',
'expected_return': 0.11,
'optimal_hour': 10,
'api_calls': 20,
'cpu_usage': 30,
'correlation_matrix': {}
},
{
'name': 'News_Sentiment',
'expected_return': 0.09,
'optimal_hour': 13,
'api_calls': 45,
'cpu_usage': 20,
'correlation_matrix': {'Momentum_Europe': 0.5}
},
{
'name': 'Market_Making_Crypto',
'expected_return': 0.14,
'optimal_hour': 0,  # 24/7 но предпочтительно ночь
'api_calls': 50,
'cpu_usage': 40,
'correlation_matrix': {}
},
{
'name': 'Index_Arb',
'expected_return': 0.07,
'optimal_hour': 16,
'api_calls': 30,
'cpu_usage': 15,
'correlation_matrix': {'Statistical_Arb_US': 0.3}
}
]
# Создание временных слотов (каждые 30 минут, 24 часа)
base_date = datetime(2025, 1, 1)
time_slots = [base_date + timedelta(minutes=30*i) for i in range(48)]
# Построение расписания
scheduler = TradingScheduler(strategies, time_slots, max_concurrent=3)
schedule = scheduler.greedy_schedule()
scheduler.print_schedule()
# Анализ эффективности размещения
placed_strategies = sum(len(strategies) for strategies in schedule.values())
total_strategies = len(strategies)
print(f"Статистика размещения:")
print(f"  Размещено стратегий: {placed_strategies}/{total_strategies}")
print(f"  Процент размещения: {placed_strategies/total_strategies*100:.1f}%")
# Анализ использования ресурсов по слотам
resource_usage = []
for slot, strategies in schedule.items():
if strategies:
api_usage = sum(s['api_calls'] for s in strategies)
cpu_usage = sum(s['cpu_usage'] for s in strategies)
resource_usage.append({
'slot': slot,
'api_pct': api_usage / 100,
'cpu_pct': cpu_usage / 80,
'n_strategies': len(strategies)
})
if resource_usage:
usage_df = pd.DataFrame(resource_usage)
print(f"\nСреднее использование ресурсов:")
print(f"  API: {usage_df['api_pct'].mean()*100:.1f}%")
print(f"  CPU: {usage_df['cpu_pct'].mean()*100:.1f}%")
print(f"  Стратегий на слот: {usage_df['n_strategies'].mean():.1f}")
================================================================================
ТОРГОВОЕ РАСПИСАНИЕ
================================================================================
00:00 - 00:30
--------------------------------------------------------------------------------
• Market_Making_Crypto      Return: 14.00%  API:  50  CPU:  40%
Итого по слоту: Return: 14.00%, API: 50/100, CPU: 40/80%
02:00 - 02:30
--------------------------------------------------------------------------------
• Mean_Reversion_Asia       Return:  8.00%  API:  25  CPU:  15%
Итого по слоту: Return:  8.00%, API: 25/100, CPU: 15/80%
09:00 - 09:30
--------------------------------------------------------------------------------
• Momentum_Europe           Return: 12.00%  API:  30  CPU:  20%
Итого по слоту: Return: 12.00%, API: 30/100, CPU: 20/80%
10:00 - 10:30
--------------------------------------------------------------------------------
• Vol_Arbitrage             Return: 11.00%  API:  20  CPU:  30%
Итого по слоту: Return: 11.00%, API: 20/100, CPU: 30/80%
13:00 - 13:30
--------------------------------------------------------------------------------
• News_Sentiment            Return:  9.00%  API:  45  CPU:  20%
Итого по слоту: Return:  9.00%, API: 45/100, CPU: 20/80%
14:00 - 14:30
--------------------------------------------------------------------------------
• Statistical_Arb_US        Return: 15.00%  API:  40  CPU:  35%
Итого по слоту: Return: 15.00%, API: 40/100, CPU: 35/80%
15:00 - 15:30
--------------------------------------------------------------------------------
• Pairs_Trading_Tech        Return: 10.00%  API:  35  CPU:  25%
Итого по слоту: Return: 10.00%, API: 35/100, CPU: 25/80%
16:00 - 16:30
--------------------------------------------------------------------------------
• Index_Arb                 Return:  7.00%  API:  30  CPU:  15%
Итого по слоту: Return:  7.00%, API: 30/100, CPU: 15/80%
================================================================================
Суммарная ожидаемая доходность: 86.00%
================================================================================
Статистика размещения:
Размещено стратегий: 8/8
Процент размещения: 100.0%
Среднее использование ресурсов:
API: 34.4%
CPU: 31.2%
Стратегий на слот: 1.0

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

👉🏻  Ускорение численных вычислений в Python: Numba, JIT на примерах из Data Science

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

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

Ограничения жадного подхода и альтернативы

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

Сценарии неэффективности жадного подхода в количественном анализе:

  1. Оптимизация портфеля с нелинейными ограничениями: лимиты на отраслевую концентрацию, минимальные/максимальные веса активов, ограничения на tracking error относительно бенчмарка требуют квадратичного программирования;
  2. Размещение ордеров с прогнозом маркет-импакта: будущее влияние ордера на цену зависит от текущих решений, что нарушает независимость подзадач;
  3. Отбор признаков с взаимодействиями: бесполезные по отдельности признаки могут быть ценными в комбинации (XOR-подобные зависимости);
  4. Динамическое хеджирование опционов: локально оптимальное хеджирование может быть неоптимальным на горизонте из-за изменения волатильности и временного распада

Диагностика применимости жадного подхода: сравнение с эталонным решением (динамическое программирование, точные методы) на небольших тестовых данных. Если качество жадного решения >90% от оптимального, алгоритм пригоден для продакшена.

Сравнение с метаэвристиками

Метаэвристики — алгоритмы оптимизации, не гарантирующие глобального оптимума, однако эффективные для сложных задач. Основные представители: генетические алгоритмы, симуляция отжига (simulated annealing), роевая оптимизация (particle swarm optimization), оптимизация методом муравьиных колоний (ant colony optimization).

Сравнительная характеристика:

Метод Сложность Качество решения Настройка Детерминизм
Жадный O(n log n) – O(n²) 70-95% от оптимума Простая Да
Генетический O(p · g · n) 85-98% от оптимума Сложная Нет
Simulated Annealing O(i · n) 80-95% от оптимума Средняя Нет
Particle Swarm O(p · i · n) 85-95% от оптимума Средняя Нет

Обозначения:

  • n — размер задачи;
  • p — размер популяции;
  • g — число поколений;
  • i — число итераций.

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

Симуляция отжига применяется для оптимизации исполнения ордеров с нелинейным маркет-импактом. Алгоритм «разогревает» систему, допуская временное ухудшение целевой функции для выхода из локальных минимумов, затем «охлаждает», сходясь к решению. В то же время, жадный подход застревает в первом локальном оптимуме.

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

Гибридные подходы

Гибридные методы комбинируют жадные алгоритмы с другими техниками оптимизации. Типичные схемы:

  • Greedy + Local Search: жадный алгоритм строит начальное решение, локальный поиск улучшает его небольшими модификациями. Для портфельной оптимизации: жадный отбор 20 активов из 500, затем квадратичное программирование для точных весов.
  • Greedy + Backtracking: жадный выбор с возможностью отката при обнаружении тупика. Применяется в задачах с жесткими ограничениями, где жадный путь может привести к невыполнимому решению.
  • Beam Search: обобщение жадного поиска, сохраняющее k лучших решений на каждом шаге. Компромисс между жадным подходом (k=1) и полным перебором (k=∞). Для feature selection: хранить 5 лучших наборов признаков, выбирать финальный по валидационной метрике.
  • Greedy + Machine Learning: обучение модели для предсказания качества жадного выбора. Для исполнения ордеров: ML-модель прогнозирует ликвидность и спреды на следующий час, жадный алгоритм оптимизирует расписание на основе прогноза.
  • Adaptive Greedy: динамическая корректировка критерия жадности на основе результатов. Если жадный отбор активов дает высокую концентрацию в одной отрасли, критерий модифицируется для увеличения диверсификации.

Выбор гибридного подхода зависит от соотношения качество/скорость. Чистый жадный алгоритм дает решение за миллисекунды, гибрид с локальным поиском — за секунды, генетический алгоритм — за минуты. Для реалтайм систем (HFT, динамическое хеджирование) часто оказываются приемлемыми только жадный подход или простейшие гибриды.

Заключение

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

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