# Как построить надежную алгостратегию на Python и обойти индекс

Источник: https://www.youtube.com/watch?v=9Y3yaoi9rUQ
Канал: freeCodeCamp.org
Опубликовано: 26.10.2023

---

Хотя Python считается слишком медленным языком для высокочастотного трейдинга, он остается главным инструментом квантов для исследования рынка и бэктестинга стратегий. Создание прибыльного алгоритма — это постоянная борьба с ловушками вроде заглядывания в будущее, ошибки выжившего и хаотичного шума в соцсетях. Практическое руководство по алготрейдингу показывает, как объединить кластеризацию K-Means, модель волатильности GARCH(1,3) и теорию Марковица в устойчивую систему, способную стабильно обходить рыночные бенчмарки.

## 📈 Введение в алгоритмическую торговлю: от основ Python до машинного обучения
[[JUMP:02:37]]

### Основы алготрейдинга и роль Python: почему язык программирования решает всё
[[JUMP:02:37]]

Алгоритмическая торговля представляет собой процесс исполнения сделок на основе заранее определенных правил, объединенных в единую систему или стратегию [2:37]. Эти правила описываются с помощью языка программирования и исполняются компьютером без необходимости постоянного ручного контроля [2:49]. На практике алготрейдинг разделяют на два ключевых подхода:

*   Мануальный (полуавтоматический) режим: когда система генерирует скринеры акций или отправляет алерты при выполнении условий, но финальное решение и ручное исполнение ордера остаются за человеком [3:03].

*   Полностью автоматический режим: когда сложный комплекс алгоритмов самостоятельно рассчитывает размер позиции, определяет точки входа и выхода, после чего автоматически отправляет заявки брокеру [3:15].

Python прочно закрепил за собой статус ключевого инструмента в количественных финансах и анализе данных [3:29]. Его популярность обусловлена огромной экосистемой библиотек и простотой синтаксиса, значительно ускоряющей проведение исследований [3:43]. В количественных финансах Python применяется для построения конвейеров данных, тестирования гипотез (бэктестинга) и автоматизации несложных систем [3:43]. Тем не менее, у языка есть ограничение — низкая скорость выполнения кода [3:56]. Для высокочастотных стратегий (HFT) или ультрасложных инфраструктур, где критически важна микросекундная задержка, разработчики выбирают C++ или Java [3:56].

Индустрия количественных финансов предлагает колоссальные карьерные возможности в хедж-фондах, банках и проп-трейдинговых фирмах [4:10]. По последним оценкам, средняя базовая зарплата количественного исследователя (Quant Researcher) составляет около 173 000 долларов в год, не включая ежегодные бонусы [4:23]. Для успешного старта в этой сфере специалисту необходимо глубоко понимать Python, уметь воспроизводить академические финансовые работы на практике, проводить точный бэктестинг и владеть методами машинного обучения [4:37].

### Машинное обучение в количественных стратегиях: возможности и суровые реалии рынка
[[JUMP:04:49]]

Интеграция машинного обучения (ML) открывает новые горизонты для создания торговых стратегий. В рамках обучения с учителем (Supervised Learning) ML-модели эффективно применяются для генерации сигналов, например, прогнозирования направления движения цены или знака доходности актива [4:49, 5:03]. Также модели полезны в риск-менеджменте для точного определения размера позиций, весов активов в портфеле или расчета уровней стоп-лосса [5:16]. Обучение без учителя (Unsupervised Learning) помогает извлекать скрытые закономерности, выявлять рыночные режимы, проводить кластеризацию и оптимизировать структуру портфелей [5:30].

Однако применение ML на финансовых рынках сопряжено с серьезными барьерами, главным из которых является теоретический парадокс — «петля обратной связи рефлексивности» [5:57]. Суть явления в том, что рынок адаптируется к действиям участников. Если модель находит устойчивый паттерн — например, что определенная акция регулярно растет по пятницам [6:11] — трейдер начинает покупать ее в четверг, чтобы зафиксировать прибыль в пятницу [6:11]. По мере того как другие участники рынка обнаруживают эту же закономерность и начинают действовать аналогично, точка роста смещается на четверг, а изначальная рыночная неэффективность полностью арбитражируется и исчезает [6:23].

Сложность прогнозирования различных величин на рынке также неоднородна. Труднее всего предсказывать абсолютные цены и доходности активов [6:47]. Немного проще, но все еще крайне сложно прогнозировать направление движения цены (знак доходности) [6:47] или макроэкономические показатели вроде еженедельных заявок на пособие по безработице [7:02]. В то же время прогнозирование волатильности является относительно простой и реализуемой задачей [7:17].

К техническим вызовам quant-инженеры относят переобучение (overfitting), когда модель отлично работает на исторических данных, но проваливается на тестовых [7:29], проблему обобщающей способности (generalization) [7:29], нестационарность данных и смену рыночных режимов [7:41]. Наконец, сложные модели (например, нейросети) часто представляют собой «черный ящик», что затрудняет их интерпретацию [7:54]. Стандартный рабочий процесс включает в себя сбор данных, формулирование гипотезы, написание кода, обучение модели и обязательное историческое тестирование [7:54].

### Ошибка выжившего в бэктестинге и практическая подготовка данных
[[JUMP:11:16]]

При проведении бэктестинга критически важно избегать методологических ошибок, главной из которых является ошибка выжившего (survivorship bias) [11:16, 11:29]. Она возникает, когда тестирование проводится на актуальном (сегодняшнем) списке компонентов индекса, например, S&P 500 [11:29].

Суть этой проблемы заключается в следующем: компании, которые обанкротились, показали худшие результаты или были поглощены, исключаются из индекса [11:43]. Например, если в конце прошлого года некоторая компания демонстрировала затяжное падение и в декабре была исключена из S&P 500 [11:55], то при оптимизации портфеля в ноябре реальный трейдер мог бы купить ее акции и понести убытки [12:08]. Однако, используя современный список компонентов индекса, мы просто не увидим эту компанию в исторических данных, что искусственно завысит историческую доходность стратегии на бэктесте [12:08, 12:22]. В рамках данного учебного проекта автор использует актуальный список S&P 500 и обращает внимание на то, что это допущение является сознательным ограничением бэктеста [12:22].

Практическая часть проекта начинается с загрузки списка акций S&P 500 с помощью парсинга таблиц Wikipedia через функцию `pandas.read_html` [16:55]. Чтобы избежать сбоев при последующей загрузке данных из Yahoo Finance, тикеры проходят предварительную очистку: точки в названиях заменяются на дефисы [18:03]. Для симуляции берется временной интервал в 8 лет, отсчитываемый назад от 27 сентября 2023 года [18:46, 18:59].

Загруженный датафрейм преобразуется методом `.stack()`, переводящим данные в удобный формат мультииндекса с уровнями «Дата» и «Тикер» [20:32]. Это позволяет эффективно структурировать почти миллион строк информации для 500 компаний [20:59]. В завершение этапа подготовки названия колонок приводятся к нижнему регистру [21:25], подготавливая базу для расчета первых количественных характеристик.

Далее в курсе авторы переходят к расчету и масштабированию технических индикаторов, отбору наиболее ликвидных акций и расчету факторных бет Фама-Френча [22:04, 23:44], однако эти темы подробно разбираются в следующей главе.

## 🛠️ Масштабирование признаков, фильтрация ликвидности и интеграция факторов Фама-Френча
[[JUMP:25:04]]

### Расчет и нормализация технических индикаторов
[[JUMP:25:18]]

Подготовка сырых рыночных данных к использованию в моделях машинного обучения требует тщательного расчета технических индикаторов и их последующего масштабирования. Первым шагом является расчет линий Боллинджера (Bollinger Bands). Для этого используется библиотека `pandas_ta` [25:18]. Расчет производится на основе логарифма скорректированной цены закрытия (`np.log1p`) с окном в 20 дней [25:59, 26:14]. Поскольку функция `ta.bbands` возвращает сразу пять колонок, разработчику необходимо извлечь три ключевые линии: нижнюю (Lower Band), среднюю (Middle Band) и верхнюю (Upper Band) [25:32, 25:46]. Это реализуется через группировку по тикерам и применение метода `transform` с индексацией соответствующих колонок через `.iloc` [25:59, 26:41].

```python
# Пример расчета полос Боллинджера с логарифмированием
df['BBL'] = df.groupby(level=1)['adj close'].transform(lambda x: ta.bbands(np.log1p(x), length=20).iloc[:, 0])
```

Для расчета среднего истинного диапазона (ATR) требуется более сложный подход. Функция ATR из библиотеки `pandas_ta` требует на вход три колонки: максимальную (High), минимальную (Low) и цену закрытия (Close) [27:25, 28:03]. Метод `transform` в pandas оптимизирован для работы с одиночными сериями, поэтому для многоколоночного ввода создается пользовательская функция `compute_atr` [27:37, 28:28]. Внутри нее применяется группировка по тикерам с параметром `group_keys=False`, чтобы избежать нежелательного дублирования временных индексов при объединении данных [29:10, 29:23]. 

Важнейшим этапом является нормализация признаков. ATR и схождение-расхождение скользящих средних (MACD, рассчитываемый по цене закрытия с окном 20) подвергаются стандартному масштабированию: из каждого значения вычитается среднее, после чего результат делится на стандартное отклонение [28:55, 30:02, 30:29]. Такая нормализация критически важна, поскольку в дальнейшем эти признаки будут переданы в модель машинного обучения (в частности, для кластеризации, о чем подробнее пойдет речь в следующей главе) [30:29, 31:25]. Единственным индикатором, который намеренно не подвергается нормализации на этом этапе, остается индекс относительной силы (RSI) [31:25]. 

В завершение первого этапа рассчитывается показатель долларового объема торгов (Dollar Volume), определяемый как произведение скорректированной цены закрытия на объем торгов [31:39]. Для удобства масштабирования и интерпретации полученное значение делится на 1 000 000 [32:05].

---

### Агрегация данных и отбор 150 наиболее ликвидных акций
[[JUMP:32:46]]

Для сокращения времени обучения будущих моделей машинного обучения и ускорения экспериментов с торговыми стратегиями применяется фильтрация акций по ликвидности [32:59]. Процесс начинается с агрегации ежедневных данных до месячного уровня [32:46]. При этом для технических индикаторов и цен закрытия берется последнее значение месяца, а для долларового объема вычисляется среднее арифметическое за весь месяц [33:13].

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

1. Исходный DataFrame «разворачивается» по тикерам с помощью метода `unstack` [33:26].
2. Для расчета среднего долларового объема применяется ресемплирование по месячному индексу `resample('M').mean()`, после чего данные «сворачиваются» обратно через `stack` [33:38, 33:52].
3. Для остальных признаков и цен закрытия применяется аналогичная схема, но с методом `.last()` [35:56, 36:11].
4. Результаты объединяются с помощью `pd.concat` по оси `axis=1` [36:28].

Чтобы отсеять неликвидные активы и избежать рыночного шума, рассчитывается 5-летнее скользящее среднее месячного долларового объема для каждой акции [37:09]. Размер скользящего окна составляет 60 месяцев (5 лет * 12 месяцев) [37:39, 37:51]. На основе этого показателя каждый месяц проводится кросс-секционное ранжирование акций от наибольшего объема к наименьшему с помощью метода `groupby(level=0).rank(ascending=False)` [38:17, 38:30]. В итоговый набор данных проходят только те акции, чей ранг строго меньше или равен 150 [38:59, 39:14].

Дополнительно для захвата ценового импульса (моментум-эффекта) рассчитываются исторические доходности акций за различные временные горизонты: 1, 2, 3, 6, 9 и 12 месяцев [39:55, 40:34]. Чтобы минимизировать влияние аномальных рыночных выбросов на будущую модель, доходности подвергаются винзорированию (клиппингу) на уровнях 0.5% и 99.5% квантилей [41:15, 41:29]. Расчет выполняется с помощью кастомной функции `calculate_returns` и группировки по тикерам [43:21].

---

### Интеграция и расчет факторных бет Фама-Френча
[[JUMP:44:43]]

Чтобы оценить чувствительность выбранных акций к общим систематическим факторам риска, в модель интегрируется классическая пятифакторная модель Фама-Френча [44:43, 45:13]. Эти пять факторов — рыночный риск (Market Risk), размер (Size/SMB), стоимость (Value/HML), рентабельность (Profitability/RMW) и инвестиционная активность (Investment/CMA) — исторически объясняют значительную часть доходности акций и широко применяются в индустрии управления активами [45:25, 45:38].

Данные факторов загружаются напрямую из библиотеки Кеннета Френча (Ken French Data Library) с помощью пакета `pandas_datareader` (импортированного как `web`) [45:52, 46:05]. Используется ежемесячный набор данных `'F-F_Research_Data_5_Factors_2x3'` начиная с 2010 года [46:44].

```python
# Загрузка 5 факторов Фама-Френча через pandas_datareader
ff_data = web.DataReader('F-F_Research_Data_5_Factors_2x3', 'famafrench', start='2010')[0]
```

Перед проведением расчетов данные факторов проходят предобработку:

* Колонка безрисковой ставки (`RF`) удаляется, так как она не используется в рамках текущего исследования [47:12].
* Индекс преобразуется в формат даты и сдвигается к концу месяца для точного совмещения со скользящими датами основного датафрейма акций [47:41, 48:21].
* Все значения факторов делятся на 100, так как в исходном источнике они представлены в процентах [48:21].

Подготовленные факторы объединяются с одномесячной доходностью акций [49:00]. Это позволяет сопоставить доходность акции на конец месяца с факторами, известными на начало этого же месяца [49:15]. 

Факторные беты (чувствительности) рассчитываются путем оценки параметров линейной регрессии методом наименьших квадратов (OLS) [45:13]. Для этого применяется скользящее окно регрессии (Rolling OLS) из библиотеки `statsmodels` [45:13]. Данный подход позволяет получить динамические во времени коэффициенты чувствительности для каждой из 150 наиболее ликвидных акций, что существенно обогащает итоговый набор признаков перед этапом кластеризации [49:15, 50:08].

---
CHAPTER-META---
{"summary": "В главе подробно описан процесс технического анализа и предобработки данных: вычисление и нормализация индикаторов (Bollinger Bands, ATR, MACD), фильтрация 150 самых ликвидных акций на основе 5-летнего долларового объема, а также подготовка к расчету факторных бет Фама-Френча через интеграцию внешних данных и регрессионный анализ.", "quotes": [{"text": "...we would like to get the average dollar volume for the whole month for each stock... we do that to reduce training time for any potential machine learning model.", "speaker": "Instructor", "time": "32:59"}, {"text": "We are normalizing the data because we are going to use it into a machine learning model. We are going to cluster the data we want to do that straight away.", "speaker": "Instructor", "time": "30:29"}], "key_facts": ["Нормализация признаков (ATR, MACD) выполняется делением разности с математическим ожиданием на стандартное отклонение.", "RSI намеренно остается ненормализованным для последующего этапа кластеризации.", "Фильтрация топ-150 акций проводится по 5-летнему скользящему среднему долларовому объему с ежемесячным пересчетом рангов.", "Факторы Фама-Френча скачиваются через pandas_datareader, делятся на 100 и выравниваются по датам конца месяца для регрессионного анализа."], "covered_topics": ["Расчет и масштабирование технических индикаторов", "Отбор наиболее ликвидных акций", "Расчет факторных бет Фама-Френча"]}

## 📈 Борьба с look-ahead bias и кластеризация активов по RSI
[[JUMP:50:22]]

### Устранение эффекта заглядывания в будущее при расчете факторных бет
[[JUMP:58:34]]

Прежде чем приступать к интеграции машинного обучения в торговую систему, необходимо убедиться в корректности и чистоте подготовленных данных. На этапе предварительной обработки автор выявляет критическую ошибку в индексации дат, из-за которой в датасете осталось всего 23 месяца наблюдений вместо ожидаемых 71 [51:21]. После исправления фильтрации по долларовому объему (ранее в разговоре обсуждался отбор наиболее ликвидных акций) история успешно восстанавливается до полного периода с октября 2017 года [52:01].

Для стабильной работы регрессионных моделей крайне важно исключить из выборки активы с недостаточным объемом исторических данных. Автор вводит фильтр, отсекающий любые акции, имеющие менее 10 месяцев наблюдений [52:30]. Это предотвращает сбои при расчете скользящих окон [50:37]. Расчет факторных бет (ранее в разговоре они рассчитывали факторные беты Фама-Френча) производится с помощью скользящей OLS-регрессии с окном в 24 месяца [56:15]. Если у акции накоплено меньше 24 месяцев истории, но больше установленного лимита в 10 месяцев, размер окна динамически сужается до доступного количества строк [57:26].

Главная методологическая опасность на этом этапе — эффект заглядывания в будущее (look-ahead bias) [58:34]. Поскольку для расчета текущей беты используются значения факторов на начало месяца и доходность на конец месяца, итоговое значение коэффициента становится известным инвестору только в самом конце расчетного периода [58:48]. Прямое объединение этих бет с признаками текущего месяца приведет к неявной передаче информации из будущего в обучающую выборку [59:01].

Для устранения этой уязвимости автор применяет сдвиг данных на один шаг вперед [59:17]. Однако стандартный метод сдвига DataFrame `.shift(1)` использовать нельзя: он сдвинет строки глобально, из-за чего данные одного тикера (например, Verizon) перетекут в строки другого [59:42]. Решением становится группировка по тикерам перед сдвигом:

`data.groupby(level='ticker').shift(1)` [1:00:08].

После сдвига в данных неизбежно возникают пропуски (NaN). Их заполнение (импутация) выполняется путем группировки по каждому отдельному тикеру и вычисления среднего значения для каждого конкретного фактора [1:01:49]. Наконец, из итогового набора данных удаляется колонка скорректированной цены закрытия (`adjusted close`), оставляя чистую матрицу из 18 признаков, готовую для передачи в модели машинного обучения [1:02:46].

### Кластеризация K-Means для формирования портфеля
[[JUMP:1:04:09]]

Сформировав качественный массив признаков, команда переходит к этапу генерации торговых сигналов [1:03:00]. Вместо построения сложных прогнозных моделей с учителем (supervised learning) для предсказания точной доходности или весов активов, автор предлагает использовать методы обучения без учителя (unsupervised learning) [1:04:09]. Задача состоит в том, чтобы ежемесячно группировать 150 самых ликвидных акций на рынке [1:03:15] в однородные кластеры по их характеристикам [1:04:50].

Для решения этой задачи применяется алгоритм кластеризации K-Means [1:04:24]. На основе предварительных исследований автор устанавливает оптимальное количество кластеров, равное 4 [1:05:03]. Этот параметр позволяет сбалансированно разделять активы без избыточного дробления выборки [1:05:17].

Процесс кластеризации реализуется через создание функции `get_clusters` [1:06:40]. Внутри нее инициализируется объект класса `KMeans` из библиотеки `scikit-learn` [1:06:26] со следующими параметрами:

*   `n_clusters=4` — целевое число групп [1:07:05];

*   `random_state=0` — фиксация генератора случайных чисел для воспроизводимости результатов между запусками [1:07:18];

*   `init='random'` — базовая случайная инициализация центроидов на первом этапе тестирования модели [1:07:31].

Алгоритм применяется ежемесячно с помощью конструкции группировки по датам:

`data.groupby(level='date').apply(get_clusters)` [1:08:29].

В результате работы модели каждая акция в датасете получает метку кластера от 0 до 3 для каждого конкретного месяца [1:08:53].

### Проблема случайной инициализации и кастомные центроиды по RSI
[[JUMP:1:09:05]]

Для оценки качества работы алгоритма автор visualizes полученные группы [1:09:05]. Поскольку работать одновременно с 18 признаками на плоскости невозможно, для наглядности строятся двумерные диаграммы рассеяния (scatter plots), где по оси абсцисс откладывается индикатор ATR, а по оси ординат — ненормализованные значения индекса относительной силы (RSI) [1:10:45].

Визуализация подтверждает высокую эффективность группировки: во все месяцы акции четко разделяются на горизонтальные зоны в зависимости от уровня их перекупленности или перепроданности [1:11:28]. В частности, стабильно выделяются:

*   группа акций с высоким восходящим импульсом (RSI в районе 60–70) [1:11:41];

*   две промежуточные группы (RSI 50–60 и 40–50) [1:11:54];

*   группа аутсайдеров с низким RSI (около 35) [1:11:54].

Однако при детальном анализе обнаруживается серьезная проблема: из-за случайной инициализации центроидов метки кластеров хаотично меняются местами от месяца к месяцу [1:12:06]. Если в октябре кластер с меткой `0` соответствовал акциям с RSI 35, то в ноябре под меткой `0` может оказаться группа с RSI 50 [1:12:21].

Для импульсной (momentum) торговой стратегии, цель которой — регулярно покупать исключительно активы с сильным восходящим трендом (RSI от 65 до 75), такая нестабильность фатальна [1:12:51]. Торговому роботу необходимо точно знать, какой именно номер кластера соответствует зоне максимального импульса в текущем месяце [1:13:06].

Чтобы решить эту проблему, автор предлагает отказаться от стандартной случайной инициализации `init='random'` и передавать в K-Means предопределенные начальные координаты центроидов через параметр `init` [1:13:41]. В качестве опорных точек для инициализации выбираются целевые значения RSI: 30, 45, 55 и 70 [1:15:00]. Программа создает массив нулевых векторов размерностью `(4, 18)` (по числу кластеров и признаков) [1:15:15] и заполняет в них только координаты, соответствующие индексу признака RSI. Это вынуждает алгоритм K-Means всегда привязывать метку кластера к строго определенному диапазону RSI, обеспечивая стабильную сортировку активов для дальнейшего отбора в портфель.

## 📈 Оптимизация портфеля по максимуму Шарпа и динамическое ребалансирование
[[JUMP:1:18:32]]

### Отбор акций на основе momentum-эффекта и подготовка временной структуры
[[JUMP:1:18:32]]

Ранее в рамках разработки торговой системы авторы реализовали алгоритм кластеризации K-Means для группировки активов по техническим индикаторам [1:15:30]. Теперь, когда кластеры стабильно идентифицируются из месяца в месяц и за каждым из них закрепляется определенное рыночное состояние [1:17:52], наступает этап практического применения полученных данных. Согласно принятой momentum-гипотезе, акции с высоким показателем RSI (находящимся в районе 70, что стабильно соответствует кластеру под номером 3) [1:18:04] демонстрируют сильный ценовой импульс. Предполагается, что этот импульс продолжит работать и позволит бумагам опережать рынок в течение следующего месяца [1:19:11].

Для реализации этой стратегии на стыке месяцев (например, 31 октября) отбирается список подходящих тикеров [1:20:08]. Процесс подготовки данных в Pandas включает сброс первого уровня мультииндекса [1:20:49] и смещение дат на один день вперед с использованием `DateOffset` [1:21:15]. Это необходимо для того, чтобы перенести точку принятия решений на точную дату начала инвестиционного периода — первое число следующего месяца [1:20:21]. 

Итогом этого этапа становится формирование словаря `fixed_dates` [1:22:26]. Ключами в данном словаре выступают строковые даты начала торговых периодов, а значениями — списки тикеров, приведенные к стандартному формату Python-list [1:23:06]. Этот словарь служит мостом между модулем машинного обучения и блоком портфельной оптимизации [1:23:36].

### Математическое ядро стратегии: PyPortfolioOpt и диверсификационные ограничения
[[JUMP:1:23:48]]

Переходя от простого отбора акций к математическому распределению весов капитала, авторы обращаются к современной портфельной теории (MPT) Гарри Марковица [1:23:48]. Для этого используется специализированная библиотека `PyPortfolioOpt`, из которой импортируются класс `EfficientFrontier`, а также модули `risk_models` и `expected_returns` [1:24:18]. 

Основная задача оптимизатора — максимизация коэффициента Шарпа, то есть поиск такого соотношения активов, которое обеспечивает наибольшую избыточную доходность на единицу риска. Написанная авторами функция `optimize_weights` принимает на вход массив исторических цен [1:25:10]. Внутри функции рассчитывается среднегодовая историческая доходность через метод `mean_historical_return` [1:25:36], а также ковариационная матрица с помощью `sample_cov` [1:26:14]. В обоих случаях задается рабочая частота в 252 торговых дня в году [1:26:02].

Особое внимание авторы уделяют ограничениям весов (weight bounds) для минимизации портфельных рисков. Стандартные настройки класса `EfficientFrontier` допускают распределение долей от 0 до 100% на один актив [1:28:57]. Чтобы избежать опасной концентрации капитала в одной-двух бумагах, вводится жесткое верхнее ограничение: доля любого отдельного актива в портфеле не может превышать 10% (0.1) [1:29:25]. 

Нижняя граница веса (lower bound) настраивается динамически [1:29:40]. Авторы предлагают привязывать ее к числу вошедших в портфель активов в конкретном месяце. Например, если в портфель отобрано 20 акций, то при равном распределении доля каждой составит 5% [1:29:52]. Минимальный порог в таком случае устанавливается как половина от равного веса (то есть 2.5%) [1:30:04]. 

После настройки ограничений вызывается метод оптимизации `ef.max_sharpe()`, а итоговые веса обрабатываются функцией `clean_weights()`, которая округляет незначительные значения и отсекает нулевые доли [1:28:02]. Такой подход гарантирует получение хорошо диверсифицированного и сбалансированного портфеля [1:30:16].

### Реализация контура ребалансировки и скользящего окна оптимизации
[[JUMP:1:30:55]]

Чтобы протестировать стратегию на исторических данных, требуется загрузить ежедневные цены для всего пула из 150 наиболее ликвидных акций исходного датасета [1:31:08]. Для этого используется библиотека `yfinance` [1:30:55]. Поскольку для первой оптимизации на дату 1 ноября 2017 года необходим как минимум один год предшествующей ценовой истории, стартовая дата загрузки динамически сдвигается на 12 месяцев назад с помощью вычитания временного смещения [1:31:46].

После успешного скачивания цен рассчитываются ежедневные логарифмические доходности активов [1:34:05]. Далее запускается основной цикл ребалансировки по ключам словаря `fixed_dates` [1:34:34]. На каждой итерации (в начале очередного месяца) определяются границы текущего инвестиционного окна: дата входа и дата выхода, рассчитываемая через смещение конца месяца `month end` [1:35:32].

Для корректного расчёта весов на текущий месяц из общего массива данных извлекается строго определенное скользящее окно исторических цен [1:36:55]. Данное окно начинается ровно за 12 месяцев до даты ребалансировки [1:38:04] и заканчивается за один день до нее [1:38:19]. Срез цен по выбранным для портфеля тикерам [1:40:20] передается в оптимизатор, который на основе данных прошедшего года генерирует оптимальные веса для удержания активов в течение следующего месяца.

## 5. Отказоустойчивость бэктеста и переход к сентимент-анализу соцсетей
[[JUMP:1:40:20]]

### Резервные стратегии при сбое оптимизатора: Реализация отказоустойчивости бэктеста
[[JUMP:1:52:06]]

В количественном трейдинге переход от теоретической оптимизации портфеля к автоматизированному бэктесту на исторических данных часто выявляет краевые сценарии, способные полностью остановить работу скрипта. В процессе ежемесячного пересчета весов алгоритм определяет окно оптимизации — например, с 1 ноября 2016 года по 30 октября 2017 года [1:41:15]. Далее накладываются ограничения: максимальный вес одного актива не должен превышать 10% [1:41:40], а динамический нижний предел устанавливается как половина от веса в гипотетическом портфеле с равными долями [1:41:52]. При наличии 10 акций в выбранном месяце равное распределение дало бы по 10% на каждую бумагу; соответственно, минимальный лимит на акцию фиксируется на уровне 5% (или 0.05) [1:42:08], который затем округляется для чистоты расчетов [1:42:33].

Однако при итеративном запуске этой процедуры на протяжении нескольких лет математический оптимизатор (такой как CVXPY) периодически сталкивается с вычислительными трудностями. Из-за экстремальных рыночных фаз или аномально высокой корреляции активов алгоритм не может найти корректное решение, удовлетворяющее всем жестким ограничениям при максимизации коэффициента Шарпа. В этот момент оптимизатор выдает ошибку смены статуса на `Infeasible` (недопустимое решение) [1:53:28]. В стандартном бэктестере это привело бы к тому, что портфель остался бы без весов [1:53:42], а инвестор полностью пропустил бы этот месяц на рынке, зафиксировав нулевую доходность [1:53:55].

Для создания отказоустойчивой системы промышленного уровня в код внедряется блок обработки исключений `try-except` [1:52:18]. Основная резервная стратегия проста: если оптимизация максимального коэффициента Шарпа дает сбой, портфель автоматически распределяется в равных долях [1:54:22]. Логика отслеживается с помощью булевой переменной `success`, которая изначально имеет значение `False` [1:55:16]. Если оптимизатор успешно находит веса, флаг меняется на `True` [1:55:16]. В случае сбоя срабатывает блок `except`, на экран выводится предупреждение `"Max Sharpe optimization failed... continuing with equal weights"` [1:54:48], а бэктестер генерирует Pandas DataFrame с равным распределением долей (1, деленное на число доступных активов) для всех бумаг текущего месяца [1:55:58 - 1:56:41]. Такой подход гарантирует непрерывность тестирования на всем историческом интервале [1:57:39].

### Оценка результатов: Сравнение стратегии с бенчмарком S&P 500
[[JUMP:1:58:20]]

Когда доходность портфеля рассчитана без пропусков по всей временной шкале, наступает этап оценки эффективности. Для этого система загружает исторические данные по ETF SPY, который отслеживает индекс S&P 500 [1:58:33]. Загрузка охватывает период с 1 января 2015 года по текущую дату с помощью API библиотеки Yahoo Finance [1:58:46 - 1:59:03].

Для корректного сопоставления результатов цены закрытия SPY преобразуются в логарифмическую доходность с применением функций `.diff()` и `.dropna()` [1:59:16]. Полученный столбец переименовывается в `"SPY Buy and Hold"`, становясь эталоном для сравнения [1:59:40]. Данные бенчмарка объединяются с результатами нашей стратегии методом `merge` по датам [2:00:06].

Накопленная доходность как для торговой стратегии, так и для индекса S&P 500 рассчитывается через расширяющееся окно экспоненциальной суммы логарифмических доходностей [2:01:18]. Результаты визуализируются в стиле `ggplot` библиотеки Matplotlib [2:01:03] с размером графика 16x6 [2:02:15]. Для придания графику профессионального вида шкала оси Y форматируется в процентах [2:03:00]. Для этого импортируется модуль `matplotlib.ticker` под псевдонимом `MTI` [2:03:28], после чего к оси применяется соответствующий метод форматирования [2:03:41]. Полученная диаграмма наглядно демонстрирует, как портфель, сформированный на базе машинного обучения, показывает себя на фоне стандартного пассивного инвестирования в индекс [2:04:22].

### Анализ альтернативных данных и сентимента акций NASDAQ 100
[[JUMP:2:05:05]]

Завершив построение портфеля на основе машинного обучения, курс переходит к изучению другого направления современного количественного анализа — торговле на основе альтернативных данных [2:05:05]. Вместо того чтобы ограничиваться стандартными ценовыми графиками и финансовыми отчетами компаний, во втором проекте создается торговая стратегия на базе анализа настроений (сентимента) в Твиттере [2:05:05].

Входящий конвейер данных считывает специализированный датасет, содержащий информацию о сентименте пользователей Твиттера по отношению к акциям, входящим в технологический индекс NASDAQ 100 [2:05:19 - 2:05:32]. Несмотря на то что социальные сети выступают мощным источником рыночных сигналов (альфы), они содержат колоссальный объем информационного мусора. Розничные инвесторы, спам-аккаунты и специализированные боты регулярно пытаются манипулировать объемами упоминаний для искусственного разгона котировок отдельных тикеров.

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

Внезапный всплеск упоминаний от свежесозданных профилей с нулевым числом лайков или ретвитов классифицируется алгоритмом как бот-активность и отсекается. Напротив, публикации от авторитетных или вовлеченных участников обсуждения, вызывающие живую дискуссию, получают высокий приоритет. Подобная фильтрация позволяет отсеивать шум и использовать только реальные, весомые сигналы для принятия инвестиционных решений и последующего формирования портфеля [2:05:32].

## 📊 Бэктестинг портфельной стратегии на основе сентимента из Twitter
[[JUMP:2:05:45]]

### Очистка данных и вычисление коэффициента вовлеченности (Engagement Ratio)
[[JUMP:2:05:45]]

Для реализации стратегии, основанной на альтернативных данных, сначала необходимо подготовить рабочую среду и импортировать ключевые библиотеки: `pandas`, `numpy`, `matplotlib`, `datetime`, `yfinance` и системный модуль `os` [2:06:13]. В качестве стилизации графиков традиционно выбирается стиль `ggplot` [2:06:25]. Исходные данные загружаются из CSV-файла `sentiment_data.csv` [2:06:51]. Этот датасет содержит информацию о дате, тикере (символе) акции, количестве постов в Twitter, комментариях, лайках, показах (impressions) и итоговом показателе сентимента, рассчитанном провайдером данных [2:07:05]. 

Первым шагом предобработки является конвертация колонки дат в формат `datetime` и создание мультииндекса, состоящего из даты и тикера [2:07:33]. Главная проблема сырых данных из соцсетей заключается в обилии ботов, которые сильно искажают реальную картину популярности активов [2:08:28]. Чтобы отфильтровать этот шум, вместо использования абсолютного сентимента или количества просмотров вводится производная метрика — коэффициент вовлеченности (Engagement Ratio) [2:08:16]. Он рассчитывается как отношение количества комментариев к количеству лайков под постами об акции [2:08:54]:

$$\text{Engagement Ratio} = \frac{\text{Twitter Comments}}{\text{Twitter Likes}}$$

Если у компании много лайков, но совсем нет комментариев, это с высокой долей вероятности указывает на накрутку ботами [2:09:07]. Высокий коэффициент вовлеченности, напротив, свидетельствует о живом интересе со стороны реальных инвесторов [2:09:07]. Для исключения случайных выбросов применяется жесткий фильтр: в инвестиционную вселенную попадают только те акции, которые за день набрали более 20 лайков и более 10 комментариев [2:10:01].

### Агрегация, кросс-секционное ранжирование и выбор топ-акций
[[JUMP:2:10:57]]

После первичной фильтрации необходимо рассчитать среднемесячный коэффициент вовлеченности для каждой акции [2:11:09]. Для этого индекс по тикерам временно сбрасывается, и данные группируются по двум уровням — месяцу и символу акции [2:11:24]. На выходе формируется агрегированный датафрейм со средними значениями Engagement Ratio за каждый месяц [2:12:05].

Следующий ключевой шаг — кросс-секционное ранжирование активов [2:12:17]. Процедура выполняется ежемесячно с помощью группировки по дате и применения функции ранжирования в порядке убывания (`ascending=False`) [2:12:46]. Акции с наибольшим уровнем вовлеченности получают наивысший ранг [2:13:13]. 

Из полученного списка для каждого месяца отбираются топ-5 акций с рангом меньше или равным 5 [2:13:25]. Для формирования портфеля на следующий месяц к индексу даты добавляется один день с помощью `pandas.DateOffset` [2:14:44]. Это позволяет получить точную дату начала инвестиционного периода (первый день следующего месяца) и список активов для покупки [2:14:58]. 

Из уникального списка всех отобранных акций [2:16:48] загружаются исторические котировки через API Yahoo Finance за период с 1 января 2021 года по 1 марта 2023 года [2:17:27].

### Симуляция равновзвешенного портфеля и сравнение с бенчмарком QQQ
[[JUMP:2:18:24]]

На основе скорректированных цен закрытия рассчитываются ежедневные логарифмические доходности для каждой акции [2:18:09]. Моделирование портфеля происходит в цикле по ключевым датам [2:18:38]. Для каждого месяца определяется дата его окончания с использованием смещения `month end` [2:19:06]. Внутри каждого временного интервала выбираются топ-5 акций, актуальных для этого месяца, и вычисляется их средняя ежедневная доходность [2:20:27]. Таким образом формируется равновзвешенный портфель с ежемесячным ребалансированием [2:21:13].

Здесь важно сделать оговорку, о которой авторы упоминали ранее в курсе: используемый набор акций не очищен от ошибки выжившего (survivorship bias) [2:21:52], что может несколько завышать итоговые результаты бэктеста.

В качестве бенчмарка для сравнения выбирается ETF на индекс NASDAQ — QQQ [2:22:19]. Для него загружаются аналогичные исторические цены и рассчитывается ежедневная логарифмическая доходность [2:22:48]. Объединив доходности портфеля и QQQ в один датафрейм [2:23:01], рассчитывается их накопленная (кумулятивная) доходность с использованием расширяющегося окна (expanding window) и суммы накопленных доходностей [2:24:07].

Результаты визуализируются на графике с процентным форматом оси Y [2:25:08]. Бэктест показывает следующие результаты:

*   При ранжировании акций исключительно по количеству лайков стратегия значительно уступает бенчмарку QQQ [2:26:40].
*   Ранжирование только по комментариям дает показатели чуть выше бенчмарка [2:26:52].
*   Использование комплексного коэффициента вовлеченности (Engagement Ratio) позволяет стабильно обходить индекс NASDAQ на исследуемом промежутке времени [2:27:35].

Это наглядно доказывает, что правильная обработка альтернативных данных и создание производных признаков (derivative features) способны генерировать реальную альфу для торговых стратегий [2:27:06]. В дальнейшем авторы переходят к разбору интрадей-стратегий с использованием моделей прогнозирования волатильности [2:27:49].

## 7. Прогнозирование волатильности с GARCH и мультивременные торговые сигналы
[[JUMP:2:31:03]]

### Настройка и импорт данных для интрадей-стратегии
[[JUMP:2:31:03]]

Для построения продвинутой торговой стратегии на стыке дневных и внутридневных интервалов необходимо правильно подготовить и синхронизировать исторические данные. Процесс начинается с загрузки дневных котировок, на которых рассчитывается логарифмическая доходность с использованием метода `.diff()` [2:31:03]. Параллельно в систему загружается датасет с симулированными 5-минутными барами через стандартный метод `pd.read_csv` библиотеки Pandas [2:31:16]. 

В полученном пятиминутном датафрейме критически важно привести индекс к временному формату. Для этого колонка времени преобразуется с помощью функции `pd.to_datetime` [2:31:40] и назначается в качестве индекса таблицы [2:31:56]. На основе обновленного временного индекса генерируется дополнительное поле `date` без указания внутридневного времени [2:32:10]. Эта колонка в дальнейшем послужит ключевым связующим звеном для объединения (merge) дневных сигналов с интрадей-сеткой [2:32:35].

### Моделирование волатильности с помощью GARCH(1,3)
[[JUMP:2:32:49]]

Центральным математическим элементом прогнозирования дневной волатильности актива выступает модель GARCH — обобщенная авторегрессионная условная гетероскедастичность [2:32:49]. Прогнозирование дисперсии на один день вперед осуществляется в скользящем окне глубиной 6 месяцев (или 180 торговых дней) [2:33:01]. 

Для спецификации модели GARCH исследователю необходимо определить порядок авторегрессии (обозначаемый как $p$) и порядок скользящего среднего ($q$) [2:33:01]. В рамках рассматриваемого курса автор применил метод полного перебора (Brute Force), обучив 16 различных конфигураций моделей, где параметры $p$ и $q$ варьировались в диапазоне от 1 до 4 [2:33:13]. Оценка моделей производилась по двум ключевым показателям: среднеквадратичной ошибке (MSE) и Байесовскому информационному критерию (BIC) [2:33:26]. В результате лучшей моделью, минимизирующей критерий BIC на используемых данных, была признана архитектура GARCH(1,3), где $p = 1$, а $q = 3$ [2:33:38].

Непосредственно перед запуском прогнозов выполняются следующие шаги:

*   На исторических дневных данных рассчитывается скользящая полугодовая дисперсия логарифмической доходности [2:33:54].
*   Датасет фильтруется для ускорения расчетов, оставляя исторический интервал с начала 2020 года [2:34:51].
*   Описывается функция прогнозирования `predict_volatility`, которая инициализирует объект `arch_model` с фиксированными параметрами авторегрессии $p=1$ и $q=3$ [2:35:31], [2:35:43].

Внутри функции модель обучается с параметром частоты обновления логов `update_freq=5` при отключенном выводе технической информации на экран [2:36:10]. Прогноз строится ровно на один шаг вперед (`Horizon=1`) [2:36:22], после чего из результатов работы модели извлекается прогнозное значение дисперсии для последней доступной даты [2:36:37]. Запуск этой функции в скользящем окне через конструкцию `apply` и лямбда-выражение производит пошаговый расчет для всего тестового набора данных [2:37:16].

### Расчет премии за волатильность и генерация дневного сигнала
[[JUMP:2:38:31]]

Полученные точечные прогнозы дисперсии используются для вычисления премии за прогнозируемую волатильность (Prediction Premium) [2:38:31]. Формула этой метрики выглядит следующим образом:

$$\text{Prediction Premium} = \frac{\text{Predictions} - \text{Variance}}{\text{Variance}}$$

Из прогнозного значения дисперсии вычитается фактическая скользящая историческая дисперсия, а полученная разность нормируется (делится) на ту же фактическую историческую дисперсию [2:39:49]. Далее рассчитывается скользящее стандартное отклонение полученной премии за 6 месяцев (180 дней) [2:40:03].

На основе этих расчетов формируется ежедневный бинарный сигнал:

*   Значение `1` (сигнал к покупке/лонгу на день) генерируется, если значение премии за волатильность превышает свое скользящее стандартное отклонение более чем в 1.5 раза [2:40:43].
*   Значение `-1` (сигнал к продаже/шорту) генерируется, если премия падает ниже уровня минус 1.5 стандартных отклонений [2:41:13].
*   Значение `NaN` присваивается во всех прочих случаях, когда премия колеблется в рамках стандартного диапазона [2:41:29].

Для оценки распределения сигналов строится гистограмма [2:42:24]. Она наглядно демонстрирует, что за исследуемый период стратегия генерирует около 50 дневных сигналов на покупку и порядка 35 сигналов на продажу [2:42:40]. Чтобы избежать классической ошибки заглядывания в будущее (которая подробно разбиралась ранее в курсе), полученный дневной сигнал сдвигается ровно на один торговый день вперед методом `.shift(1)` [2:43:18]. Это обусловлено тем, что дневной сигнал, сформированный на закрытии дня $T$, может быть практически применен только на следующий торговый день $T+1$ [2:43:31]. После сдвига дневной датафрейм объединяется с 5-минутными внутридневными данными с помощью функции `merge()` по дате [2:43:59].

### Интрадей-фильтры и совмещение сигналов для входа в позицию
[[JUMP:2:45:27]]

После завершения слияния таблиц и очистки итогового датафрейма от дублирующих временных колонок [2:45:27] импортируется специализированная библиотека технического анализа `pandas_ta` [2:45:43]. Для интрадей-данных рассчитываются два классических индикатора: 20-периодный индекс относительной силы (RSI) [2:45:57] и линии Боллинджера (Bollinger Bands) [2:46:22]. Из многоколоночного вывода линий Боллинджера отбираются крайние границы: нижняя линия (первый столбец данных) [2:46:22] и верхняя линия (третий столбец) [2:46:49].

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

*   Покупка (`1`) фиксируется, если индикатор RSI поднимается выше уровня 70, а цена закрытия 5-минутного бара пробивает верхнюю границу полосы Боллинджера [2:47:31].
*   Продажа (`-1`) фиксируется при падении RSI ниже 30 и закрытии бара под нижней полосой Боллинджера [2:47:57].

Финальное торговое решение принимается на пересечении дневного прогноза GARCH и интрадей-импульса. Логика входа строится на гипотезе о возврате к среднему (mean reversion): если дневной сигнал указывает на аномально высокую прогнозную волатильность (`signal_daily == 1`), а интрадей-рынок при этом перекуплен (`signal_intraday == 1`), то открывается короткая позиция (short) [2:50:21]. Напротив, если оба сигнала принимают значение `-1` (дневная волатильность низкая, а интрадей-рынок экстремально перепродан), инициируется длинная позиция (long) [2:51:17].

Правило исполнения предполагает вход в сделку при первом появлении совмещенного сигнала в течение дня с последующим удержанием позиции до конца торговой сессии [2:52:30]. Любые повторные сигналы внутри того же дня игнорируются [2:52:43]. В программном коде это реализуется группировкой данных по дням через `pd.Grouper` [2:54:35] и применением метода `transform` с лямбда-функцией `forward fill` (`ffill`) [2:54:49]. Этот прием распределяет («протягивает») знак первой открытой позиции на все последующие 5-минутные бары до момента автоматического закрытия в конце дня [2:55:18].

## 📈 Расчет доходности и визуализация результатов интрадей-стратегии
[[JUMP:2:55:47]]

### Векторизованный расчет пятиминутных доходностей без избыточных группировок
[[JUMP:2:56:02]]

При оценке эффективности торговых стратегий на исторических данных критически важно правильно выстроить процесс векторизованных вычислений, чтобы избежать ресурсоемких циклов. На этапе подготовки пятиминутных данных нет необходимости применять ресурсоемкую группировку `groupby` по дням для каждого шага вычислений [2:56:02]. Поскольку анализируемый датасет представляет собой непрерывный поток пятиминутных интервалов от начала и до конца каждого дня, включая выходные, мы можем проводить расчеты напрямую по всему массиву [2:56:15]. 

Достаточно рассчитать пятиминутную доходность [2:56:15] и сдвинуть полученный вектор на одну строку назад с помощью метода `shift(-1)` [2:56:15]. Этот сдвиг необходим для того, чтобы сопоставить сигнал, сгенерированный в текущий момент времени, с доходностью, которая будет получена на следующем шаге. Векторизованный расчет доходности самой стратегии реализуется путем простого перемножения будущей доходности (`forward_return`) на знак сгенерированного торгового сигнала [2:56:29]. В результате такой операции мы получаем вектор доходности на пятиминутном уровне [2:56:41]. Использование векторизации в библиотеке Pandas позволяет мгновенно обрабатывать даже многолетние массивы интрадей-данных без зависания интерпретатора Python.

### Агрегация результатов на дневной уровень и расчет кумулятивной доходности
[[JUMP:2:56:41]]

Получив вектор доходности на уровне пятиминутных интервалов, необходимо перейти к более репрезентативному временному масштабу для оценки общей динамики торговой системы [2:56:41]. Переход осуществляется с помощью метода группировки `groupby` по датам, где для каждого отдельного дня выбирается колонка доходности стратегии, а затем к ней применяется функция суммирования [2:56:55]. Таким образом, все интрадей-результаты внутри дня складываются, формируя единый временной ряд дневных доходностей [2:56:55].

После успешного получения дневного датафрейма можно переходить к финальному шагу бэктеста — расчету кумулятивной доходности всей стратегии [2:57:09]. Для корректного отражения сложного процента при использовании логарифмических доходностей применяется следующая математическая последовательность:

1. Расчет кумулятивной суммы дневных доходностей с помощью метода `cumsum()` [2:57:23].

2. Применение экспоненциальной функции NumPy (`np.exp`) к полученному вектору кумулятивных логарифмических доходностей для перевода их в линейный масштаб [2:57:23].

3. Вычитание единицы из конечного результата для корректного отображения чистого прироста капитала в процентном отношении [2:57:38].

Этот подход позволяет точно смоделировать рост или падение условного торгового депозита на протяжении всего исторического периода тестирования [2:57:38], минимизируя вычислительные погрешности и округления.

### Визуализация эквити и анализ периодов бездействия
[[JUMP:2:57:52]]

Финальным штрихом оценки интрадей-стратегии является построение графика изменения капитала (эквити). Для визуализации кумулятивной доходности используется библиотека Matplotlib, где графику присваивается понятный заголовок — "Intraday Strategy Returns" [2:57:52]. Чтобы сделать график профессиональным и читаемым, шкала оси Y форматируется в процентах с помощью специального форматтера [2:58:04]. Подпись оси Y задается как "Return" для однозначной интерпретации результатов [2:58:21].

Анализируя получившуюся кривую доходности, можно заметить характерную особенность интрадей-стратегии: она может находиться в режиме ожидания в течение длительного времени. Например, на графике отчетливо виден плоский участок продолжительностью почти шесть месяцев, когда стратегия не совершала никаких сделок [2:58:33]. Такое поведение абсолютно нормально, так как вход в позицию происходит исключительно тогда, когда совпадают дневной и интрадей-сигналы [2:58:47]. 

Ранее в разговоре авторы касались прогнозирования волатильности с моделью GARCH и симулированных интрадей-данных, комбинация которых как раз и формирует данные условия для входа [2:58:47]. Когда оба фильтра срабатывают одновременно, стратегия открывает позиции и генерирует прибыль [2:58:47]. На этом разбор третьего практического проекта курса успешно завершается [2:58:59]. Все необходимые Jupyter-ноутбуки и исходные данные доступны по ссылкам под видео, а для желающих углубиться в тему количественного трейдинга автор предлагает посетить свой специализированный веб-сайт [2:59:12].