Как стать автором
Поиск
Написать публикацию
Обновить

Позиционный трейдинг + ML: от нуля до торговой стратегии за год

Уровень сложностиПростой
Время на прочтение11 мин
Количество просмотров2.1K

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

Начало пути

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

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

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

Первая версия стратегии была довольно простой:

  1. Если цена акции росла какое-то количество дней (параметр Z), то на следующий день её покупаем.

  2. Если после покупки цена акции начинает снижаться, то продаем.

  3. При этом определяются оптимальные уровни Stop Loss, Take Profit и параметр Z.

Для реализации задуманной стратегии я выбрал Python и Jupyter Notebook, потому что был небольшой опыт работы с ними. А так как планировал торговать на Московской бирже, то для получения данных и проведения исследований использовал библиотеку moexalgo:

import pandas as pd  # Для работы с данными в табличной форме
from moexalgo import Ticker  # Для получения данных по конкретным акциям
from moexalgo import session  # Для авторизации в системе MOEX Algo

# Замените на свои учетные данные! 
username = "your_username@example.com"  # Ваша учетная запись MOEX Algo
password = "your_password"  # Ваш пароль MOEX Algo

try:
    # Авторизуемся в системе MOEX Algo
    session.authorize(username, password)

    def import_data_red(names, sdate, edate):
        """
        Функция для получения исторических данных по акциям за указанный период.

        Args:
            names (list): Список тикеров акций (например, ['SBER', 'GAZP']).
            sdate (str): Начальная дата периода в формате 'YYYY-MM-DD' (например, '2023-01-01').
            edate (str): Конечная дата периода в формате 'YYYY-MM-DD' (например, '2023-12-31').

        Returns:
            pandas.DataFrame: DataFrame с историческими данными по акциям.
        """
        data_result = pd.DataFrame()  # Создаем пустой DataFrame для хранения результатов

        for name in names:  # Итерируемся по списку тикеров
            try:
                N = Ticker(name)  # Создаем объект Ticker для текущей акции
                data = N.candles(start=sdate, end=edate, period='1d')  # Получаем свечи (candles) за период с шагом в 1 день
                data['secid'] = name  # Добавляем столбец с тикером акции
                data_result = pd.concat([data_result, data], ignore_index=True)  # Объединяем данные с общим DataFrame
            except Exception as e:
                print(f"Ошибка при получении данных для акции {name}: {e}") # Обрабатываем ошибки при запросе данных

        data_result.rename(columns={'begin': 'tradedate'}, inplace=True)  # Переименовываем столбец 'begin' в 'tradedate'
        data_result = data_result.sort_values('tradedate')  # Сортируем данные по дате
        return data_result  # Возвращаем DataFrame с результатами

После получения данных я приступил к написанию программы. Реализация описанной стратегии оказалась для меня, как для человека без опыта в трейдинге и программировании, достаточно сложной задачей. К счастью, современные чат-боты значительно упростили процесс разработки, особенно в части отладки кода. Ещё существенно упростило разработку это разделение кода на функциональные блоки и хранение их в отдельных Notebook-ах. Все функции и переменные я вынес в отдельный Notebook, а в основном Notebook-е импортировал их. Это существенно облегчило работу. Для импорта функций из другого Notebook-а я использовал следующий скрипт:

import io
import nbconvert
from nbformat import read

# Укажите путь к вашему файлу Jupyter Notebook (с расширением .ipynb)
notebook_filename = 'Notebook.ipynb'

try:
    # Открываем Notebook в режиме чтения
    with io.open(notebook_filename, 'r', encoding='utf-8') as f:
        nb = read(f, 4) # Читаем Notebook, указав версию формата (4)

    # Создаем объект PythonExporter для преобразования Notebook в Python-код
    exporter = nbconvert.PythonExporter()

    # Преобразуем Notebook в строку с Python-кодом
    source, resources = exporter.from_notebook_node(nb)

    # Выполняем сгенерированный Python-код
    exec(source)

    print(f"Код из '{notebook_filename}' успешно выполнен.")

except FileNotFoundError:
    print(f"Ошибка: Файл '{notebook_filename}' не найден.")
except Exception as e:
    print(f"Произошла ошибка при выполнении Notebook: {e}")

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

Прибыль = (sell - buy) / buy * 100

где sell - цена продажи, buy - цена покупки. Затем, для оценки общей динамики доходности, проценты прибыли(или убытка) по всем сделкам последовательно суммировались, формируя накопленный итог (в Python это реализуется функцией cumsum()). Этот накопленный результат отражает изменение общей доходности стратегии на протяжении исследуемого периода.

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

Гипотезы и эксперименты

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

В начале я ввёл коэффициенты, которые так или иначе влияют на доходность. Например, коэффициент KSL, который определяет значение Stop Loss:

Stop Loss = KSL * (open+close)/2

или коэффициент KTP который определяет значение Take Profit:

Take Profit = KTP * (open+close)/2.

В ходе разработки стратегии количество этих коэффициентов менялось, какие-то коэффициенты вводились, а какие-то исключались, например от коэффициента KTP пришлось отказаться. Для оптимизации коэффициентов применялись различные подходы. В некоторых случаях оптимальное значение коэффициента находилось путем простого перебора значений в заданном интервале с определенным шагом. Например для вышеописанного коэффициента KSL, при бычьем рынке, интервал был в диапазоне от 0.9 до 0.99, шаг: 0.01, при медвежьем рынке интервал был в диапазоне от 1.01 до 1.05, шаг: 0.01.

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

  1. Устанавливались минимальное (Amin) и максимальное (Amax) значения коэффициента.

  2. Интервал [Amin, Amax] разбивался на 10 равных частей: f = (Amax - Amin) / 10.

  3. Вычислялась доходность стратегии для 9 равномерно распределенных значений коэффициентов внутри интервала: Amin + f, Amin + 2f, ..., Amin + 9f.

  4. Определялось значение коэффициента, при котором достигалась максимальная доходность и устанавливались новые границы интервала. Например, если наибольшая доходность наблюдалась при Amin + 6*f, то новые границы интервала устанавливались следующим образом:

  • Новый Amin = Amin + 3*f (значение коэффициента на три шага меньше оптимального).

  • Новый Amax = Amin + 9*f (значение коэффициента на три шага больше оптимального).

После сужения интервала процесс повторялся, начиная со второго шага, до тех пор, пока разница между Amax и Amin не становилась меньше заданного порога.

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

Далее я сосредоточиться на стабильности роста доходности. В качестве метрики стабильности выбрал коэффициент вариации (CV) взвешенной по времени доходности (Time-Weighted Return, TWR).  Логика проста: более низкий CV указывает на более равномерный рост стратегии во времени, минимизируя резкие колебания и просадки. Для расчета CV использовался следующий подход:

  1. Определяется доходность каждой сделки (R):

    R = (sell - buy) / buy + 1

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

    TWR = (R.cumprod() - 1) * 100

  3. Рассчитывается коэффициент вариации (CV) изменений TWR. Для этого вычисляется разница между последовательными значениями TWR, чтобы оценить величину изменений доходности во времени. Затем, вычисляется среднее значение и стандартное отклонение этих изменений, и рассчитывается коэффициент вариации как отношение стандартного отклонения к среднему значению. В Python это реализуется следующим образом:

import numpy as np
import pandas as pd

# Предполагаем, что df - это ваш DataFrame, содержащий столбец 'TWR' (Total Wealth Ratio)

# 1. Вычисляем дневные изменения TWR (доходности)
changes = df['TWR'].diff().dropna()  # dropna() убирает первое значение NaN, возникающее из-за diff()

# 2. Рассчитываем среднее значение изменений
mean_change = changes.mean()

# 3. Рассчитываем стандартное отклонение изменений
std_change = changes.std()

# 4. Вычисляем коэффициент вариации (CV)
#    CV показывает относительную степень изменчивости данных.
#    Используем проверку на 0, чтобы избежать деления на ноль и возвращаем NaN в этом случае
cv = std_change / mean_change if mean_change != 0 else np.nan

# чем выше CV, тем выше относительный риск.

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

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

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

Текущая итерация торговой стратегии

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

На подготовительном этапе производится отбор акций из числа ликвидных, для которых разработана стратегия. Для этого выполняется расчет оптимальных коэффициентов на трех временных периодах: 1, 2 и 3 года (параметр Per). Для каждого периода рассчитываются коэффициенты с использованием двух значений параметра Z (5 и 10 дней). Параметр Z определяет число дней по которым рассчитываются коэффициенты и формируются сигналы. Схема определения оптимальных параметров Per и Z показанf на рисунке:

Подготовительный этап
Подготовительный этап

По историческим данным периода Per рассчитываются оптимальные коэффициенты по которым формируются сигналы на конкретную дату (в примере на 01.06.24). Для этого проводятся ежедневные расчеты при различных коэффициентах за последние Z дней. После рассчитываются оптимальные коэффициенты и формируются сигналы на следующую дату (02.06.24) и так до конца года. Когда получены сигналы на каждый день исследуемого периода (01.06.24-31.05.25) по ним определяется доходность за этот период.

Таким образом, для каждой акции получается шесть наборов расчетов. Для каждого набора строится таблица взвешенной по времени доходности (TWR) и определяется коэффициент вариации (CV). Анализ показал, что равномерный рост доходности, как правило, наблюдается при значении CV меньше 2.1. для примера на рисунке показаны графики значений TWR за год для каждого набора для акции аэрофлота и изменение её цены. Видно, что при параметрах Per = 3 года и Z = 5 дней, график доходности показывает устойчивый рост с незначительными, до 5 %, снижениями. При этом коэффициент вариации составил 1.72.

Доходность акции Аэрофлота по разработанной стратегии
Доходность акции Аэрофлота по разработанной стратегии

В результате, на подготовительном этапе, из 40 ликвидных акций на временном интервале (01.06.2024 – 31.05.2025), на "бычьем" рынке отобрано 10 бумаг, а на "медвежьем" – всего 3.

Акции, подходящие для торговли на бычьем рынке согласно данной стратегии:

Инструмент

Z (дней)

Per (лет)

Число сделок за год

% успешных сделок

CV

Доходность TWR за год

AFLT

5

3

16

87,50

1,72

59,40

MTLR

10

2

6

83,33

0,99

18,12

PLZL

10

2

17

82,35

1,59

20,53

MAGN

10

2

5

100,00

0,54

18,18

RTKM

5

3

10

70,00

2,02

22,07

SIBN

10

2

9

77,78

2,00

17,70

ASTR

5

1

6

100,00

0,69

23,40

GAZP

5

2

9

77,78

1,75

27,34

PIKK

5

1

7

71,43

1,72

26,64

TATNP

10

3

8

87,50

1,36

20,42

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

Инструмент

Z (дней)

Per (лет)

Число сделок за год

% успешных сделок

CV

Доходность TWR за год

MOEX

5

2

14

42,86

1,89

11,75

MAGN

5

1

27

44,44

1,95

55,27

WUSH

5

2

13

46,15

1,75

17,69

Этот результат показывает, что не все ликвидные активы соответствуют характеристикам, необходимым для эффективной работы стратегии в текущей конфигурации. Для расширения числа подходящих акций считаю что необходимо увеличить диапазон временных периодов Per (например, добавить 4, 5 и 6 лет) и расширить диапазон параметра Z (например, 6, 7, 8, 9 и 15 дней). Однако, это приведет к значительному увеличению времени расчета. Например оптимальные параметры Per и Z (для вышеописанного примера) рассчитывались около 20 часов.

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

Для отобранных акций с определенными оптимальными значениями параметров Per и Z ежедневно пересчитываются оптимальные коэффициенты. На основе которых генерируются сигналы на покупку или продажу.

Для оперативного реагирования на рыночные изменения и удобства анализа результатов, сгенерированные сигналы ежедневно публикуются в Telegram-канале https://t.me/TrendSignalD. Также ежемесячно в канал планирую выкладывать график доходности (TWR), для отслеживания динамики эффективности стратегии в долгосрочной перспективе. Для отправки сигналов в Telegram-канал я использую следующий код:

import telebot
# Замените на ваш токен и ID канала
API_TOKEN = 
CHAT_ID = 
# Создайте экземпляр бота
bot = telebot.TeleBot(API_TOKEN)   
def send_message_with_retry(chat_id, text, max_retries=5, retry_delay=5):
    """
    Отправляет сообщение в Telegram с автоматическими повторами при сбоях соединения.
    Args:
        chat_id: ID чата (канала или пользователя).
        text: Текст сообщения.
        max_retries: Максимальное количество попыток отправки.
        retry_delay: Задержка между попытками в секундах.
    """
    retries = 0
    while retries < max_retries:
        try:
            bot.send_message(chat_id, text)
            print("Сообщение успешно отправлено!")
            return  # Выходим из цикла, если сообщение отправлено успешно
        except Exception as e:
            print(f"Ошибка при отправке сообщения (попытка {retries + 1}/{max_retries}): {e}")
            if "Connection broken" in str(e) or "Read timed out" in str(e) or "Connection aborted" in str(e) or "Connection forcibly closed" in str(e):  # Проверяем, связана ли ошибка с соединением
                retries += 1
                print(f"Повторная попытка отправки через {retry_delay} секунд...")
                time.sleep(retry_delay)  # Ждем перед повторной попыткой
            else:
                print("Неизвестная ошибка, прекращение попыток.")
                return  # Прекращаем попытки при других типах ошибок

    print(f"Не удалось отправить сообщение после {max_retries} попыток.")

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

Порядок действий для торговли выглядит следующим образом:

  1. В Pydroid 3 открываю терминал и запускаю Jupyter Notebook с помощью команды jupyter notebook.

  2. В открывшемся окне Jupyter Notebook (в браузере) открываю файл с программой.

  3. Чтобы программа не вылетела из-за ограничений операционной системы Android, открываю Pydroid 3 в плавающем окне и отключаю выключение экрана телефона.

  4. Запускаю скрипт в Jupyter Notebook.

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

Заключение

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

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

Теги:
Хабы:
+7
Комментарии4

Публикации

Ближайшие события