Можно ли с помощью ИИ автоматизировать набор правил, по которым действуют на бирже профессиональные трейдеры? Команда VK Cloud Solutions перевела статью о том, как это удалось реализовать и что вышло из такой затеи.

Как появилась идея автоматизации

Пару недель назад я болтал с приятелем. Он рассказывал мне, что пытался устроить детокс от брокерского приложения на смартфоне. Я забеспокоился, не разорился ли он, и уточнил значение слова «детокс» в этом контексте. 

Он ответил, что занимается трейдингом. «Если какая-то ценная бумага растет на протяжении часа и я уже заработал более 1%, я ее продаю, — объяснил он. — Это одно из моих персональных правил». Не обращая внимания на псевдонаучный аспект этих правил, я понял, что он имел в виду под детоксом. Чтобы соблюдать такие правила, нужно постоянно заглядывать в смартфон.

И я задумался, можно ли автоматизировать набор правил, по которым действовал мой приятель? Чтобы система занималась трейдингом вместо меня? Вы наверняка уже догадались, что ответ на этот вопрос — «да». Что ж, давайте порассуждаем. 

Но для начала: время — деньги, и я не хочу никого разводить на деньги. Вот что мы сделаем:

  1. Возьмем детализированные данные о ценах на акции в реальном времени, в идеале с интервалом в одну минуту. Чем их больше, тем лучше. Используем для этого Yahoo! Finance, подробнее объясню ниже.

  2. Вместо персонального набора правил добавим в систему ИИ. Раскрою все карты. Я, мягко говоря, не эксперт по анализу временных рядов. Сейчас есть немало руководств по обучению нейронных сетей трейдингу, и мне совершенно не хочется усложнять игрушечную систему вроде этой. Так что давайте стремиться к простоте: пока нам хватит самой базовой модели ARIMA.

  3. У нас есть данные и прогноз, который мы получаем от алгоритма. С его помощью нужно решать, что делать с акциями: покупать, продавать или сохранять. А еще нужно подключиться к брокеру, чтобы выполнять нужные действия. Мы будем использовать RobinHood и Alpaca.

  4. Вот, собственно, и все — система готова. Осталось только где-то деплоить и отслеживать ее работу. Я решил, что система будет отправлять сообщение в телеграм-чат при выполнении какого-либо действия.

Что нам понадобится?

  • Python 3.6 с несколькими библиотеками;

  • учетная запись в облаке с правами администратора для хранения и деплоймента;

  • Node.js, чтобы установить Serverless Framework для деплоймента;

  • аккаунт в телеграме для мониторинга.

Весь написанный код — здесь. Без лишних церемоний перейдем к первой части — сбору данных.

Сбор данных

Собрать данные — дело непростое. Еще несколько лет назад были доступны официальный Yahoo! Finance API и его альтернатива Google Finance. К сожалению, оба сервиса закрылись несколько лет назад. Но остались альтернативы. Я сформулировал следующие требования:

  • Бесплатно. Для рабочей системы я бы, конечно, поменял это требование на «недорого», но для игрушечной системы данные мне нужны бесплатно.

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

  • Данные в реальном времени. Некоторые API выдают данные с небольшой задержкой, скажем, в 15 минут. Мне нужны цены на акции, максимально приближенные к текущим.

  • Простота использования. Это проверка идеи, так что мне нужно самое простое решение.

С учетом своих требований я решил присмотреться к yfinance — неофициальной альтернативе старого доброго Yahoo Finance API. Для рабочей системы я бы выбрал Alpha Vantage API, опираясь на замечательный список Патрика Коллинза. Но пока давайте не усложнять. 

Ран Арусси разработал библиотеку yfinance для доступа к данным Yahoo! Finance, когда официальный API перестал работать. Приведу цитату с GitHub:

«С тех пор как Yahoo! Finance закрыли свой API исторических данных, многие программы, которые использовали этот интерфейс, прекратили работу. yfinance призвана решить проблему — это надежное решение на Python для загрузки исторических данных по рынку из Yahoo! Finance».

Мило, мне подходит. Как это работает? Для начала библиотеку нужно установить:

$ pip install yfinance --user

А потом можно получить доступ к данным через объект Ticker:

import yfinance as yf

google = yf.Ticker(“GOOG”)

Это достаточно быстрый метод (в среднем исполняется чуть дольше 0,005 секунды), который возвращает МАССУ информации об акциях. Например, google.info содержит 123 поля, в том числе:

52WeekChange: 0.3531152
SandP52WeekChange: 0.17859101
address1: 1600 Amphitheatre Parkway
algorithm: None
annualHoldingsTurnover: None
annualReportExpenseRatio: None
ask: 1815
askSize: 1100
…
twoHundredDayAverage: 1553.0764
volume: 1320946
volume24Hr: None
volumeAllCurrencies: None
website: http://www.abc.xyz
yield: None
ytdReturn: None
zip: 94043

 Еще больше данных можно получить с помощью методов dividends, splits, balance_sheet, earnings и других. Большинство из них возвращают данные в виде объекта pandas DataFrame, так что придется немного повозиться, чтобы получить все, что нам нужно. 

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

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

df = google.history(period='1d', interval="1m")print(df.head())
DataFrame — динамика цен на акции Google
DataFrame — динамика цен на акции Google

Видно, как они индексируются по дате и времени. При этом у каждой записи семь характеристик: четыре значения цены на акцию за эту минуту (открытие, максимальная, минимальная, закрытие) а также объем, дивиденды и сплит акций. Я буду использовать только характеристику «минимальная». Соберем необходимые данные:

df = google.history(period='1d', interval="1m")
df = df[['Low']]
df.head()

 

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

df['date'] = pd.to_datetime(df.index).time
df.set_index('date', inplace=True)
df.head()

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

Добавляем ИИ

Не пытайтесь повторять это в домашних условиях. Я подобрал ОЧЕНЬ простую модель ARIMA для прогнозирования следующей цены на акцию, поэтому относитесь к ней как к учебной. Чтобы использовать эти наработки для настоящего трейдинга, советую поискать модель получше и помощнее. Но не теряйте бдительности: если бы это было просто, такие модели были бы у всех.

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

X = df.index.values
y = df['Low'].values
# The split point is the 10% of the dataframe length
offset = int(0.10*len(df))
X_train = X[:-offset]
y_train = y[:-offset]
X_test  = X[-offset:]
y_test  = y[-offset:]

Построим график:

plt.plot(range(0,len(y_train)),y_train, label='Train')
plt.plot(range(len(y_train),len(y)),y_test,label='Test')
plt.legend()
plt.show()

Теперь добавим в модель данные для обучения и получим прогноз. Обратите внимание, что здесь гиперпараметры модели фиксированы, а в реальной жизни для получения оптимальных параметров нужно использовать кросс-валидацию. К вашим услугам замечательное руководство о том, как подбирать гиперпараметры ARIMA по сетке на Python. Я использую конфигурацию 5, 0, 1 и получаю прогноз на момент, который наступает сразу после данных для обучения.

from statsmodels.tsa.arima.model import ARIMAmodel = ARIMA(y_train, order=(5,0,1)).fit()
forecast = model.forecast(steps=1)[0]

Давайте посмотрим, хорошо ли сработала наша учебная модель:

print(f'Real data for time 0: {y_train[len(y_train)-1]}')
print(f'Real data for time 1: {y_test[0]}')
print(f'Pred data for time 1: {forecast}')
—
Real data for time 0: 1776.3199462890625
Real data for time 1: 1776.4000244140625
Pred data for time 1: 1776,392609828666

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

Подключение к брокеру

Как вы, наверное, догадались, многое зависит от выбранного брокера. Я расскажу о подключении к RobinHood и Alpaca. Почему я выбрал именно их?

  • Есть публичный API.

  • Они не берут комиссию за трейдинговые операции.

В зависимости от типа учетной записи действуют те или иные ограничения. Например, у RobinHood можно совершать всего три трейдинговые операции в пять дней, если остаток на счете менее 25 000 долларов. У Alpaca ограничения не такие жесткие, но все же установлен лимит в 200 запросов в минуту на один ключ API.

RobinHood

Есть несколько библиотек, поддерживающих RobinHood API, но ни одна из них не является официальной. Библиотека Sanko была самой большой, с 1,5 тысяч звезд на GitHub, но ее закрыли. Библиотека LichAmnesia приняла эстафету, но пока что набрала только 99 звезд. Я собираюсь использовать robin_stocks, у которой на момент написания статьи чуть более 670 звезд. Давайте ее установим:

$ pip install robin_stocks

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

import pyotp
import robin_stocks as robinhood
RH_USER_EMAIL = <<<YOUR EMAIL GOES HERE>>>
RH_PASSWORD = <<<YOUR PASSWORD GOES HERE>>>
RH_MFA_CODE = <<<THE ALPHANUMERIC CODE GOES HERE>>>
timed_otp = pyotp.TOTP(RH_MFA_CODE).now()
login = rh.login(RH_USER_EMAIL, RH_PASSWORD, mfa_code=totp)

Купить или продать — дело нехитрое:

# Buying 5 shares of Google
rh.order_buy_market('GOOG', 5)
# Selling 5 shares of Google
rh.order_sell_market('GOOG', 5)

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

Alpaca

Для Alpaca мы используем библиотеку alpaca-trade-api, у которой на GitHub более 700 звезд. Устанавливаем:

$ pip install alpaca-trade-api

Войдите в учетную запись и получите API key ID и секретный ключ, они нужны для входа в систему:

import alpaca_trade_api as alpaca
ALPACA_KEY_ID = <<<YOUR KEY ID GOES HERE>>>
ALPACA_SECRET_KEY = <<<YOUR SECRET KEY GOES HERE>>>
# Change to https://api.alpaca.markets for live
BASE_URL = 'https://paper-api.alpaca.markets'
api = alpaca.REST(
    ALPACA_KEY_ID, ALPACA_SECRET_KEY, base_url=BASE_URL)

Отправлять поручения здесь несколько сложнее, чем в RobinHood:

# Buying 5 shares of Google
api.submit_order(
    symbol='GOOG', 
    qty='5',
    side='buy',
    type='market',
    time_in_force='day'
)
# Selling 5 shares of Google
api.submit_order(
    symbol='GOOG', 
    qty='5',
    side='sell',
    type='market',
    time_in_force='day'
)

Готово! Напомню, что оставлять свои учетные данные в виде Plain Text — чрезвычайно плохая идея. Но не переживайте, на следующем этапе мы перейдем к переменным среды, это намного безопаснее. Теперь давайте развернем модель в облаке и настроим мониторинг.

Деплоймент и мониторинг

Мы собираемся задеплоить нашу систему в AWS Lambda. Для работы это не лучший вариант, поскольку в Lambda нет хранилища, а обученную модель пришлось бы где-то хранить, например в S3. 

Но пока обойдемся и этим — запланируем ежедневный запуск Lambda и обучение модели на данных за текущий день. Для мониторинга настроим бот в Telegram, который отправляет сообщение с действием и его результатом. AWS Lambda можно пользоваться бесплатно, если не превышать заданные лимиты; но если вы хотите отправлять очень много сообщений, помните о квотах.

Для начала создадим бота. Я опирался на официальную инструкцию из телеграма:

  • Найдите в телеграме пользователя @BotFather.

  • Используйте команду \newbot, выберите название и имя пользователя для бота.

  • Получите и сохраните в надежном месте токен, он вам скоро понадобится.

Следующий этап — деплоймент. Есть несколько способов деплоймента в Lambda. Я собираюсь использовать фреймворк serverless, так что давайте его установим и создадим шаблон.

$ npm install serverless --global
$ serverless create --template aws-python3 --path ai_trading_system

Мы создали папку scheduled_tg_bot с тремя файлами: .gitignore, serverless.yml, и handler.py. Serverless.yml определяет деплоймент: что, когда и как будет запущено. А файл handler.py содержит запускаемый код.

import telegram
import sys
import os
CHAT_ID = XXXXXXXX
TOKEN = os.environ['TELEGRAM_TOKEN']

# The global variables should follow the structure: 
#       VARIABLE = os.environ['VARIABLE']
# for instance: 
#       RH_USER_EMAIL = os.environ['RH_USER_EMAIL]
def do_everything():
    # The previous code to get the data, train the model
    # and send the order to the broker goes here.
    return 'The action performed'
def send_message(event, context):
    bot = telegram.Bot(token=TOKEN)
    action_performed = do_everything()    bot.sendMessage(chat_id=CHAT_ID, text=action_performed)

Нужно поменять CHAT_ID на ID группы, канала или диалога, с которыми бот должен взаимодействовать. Здесь можно узнать, как получить ID канала, а здесь — ID группы.

Теперь давайте определим, как запускать код. Откройте serverless.yml и напишите:

org: your-organization-name
app: your-app-name
service: ai_trading_system
frameworkVersion: “>=1.2.0 <2.0.0”
provider:
  name: aws
  runtime: python3.6
  environment:
    TELEGRAM_TOKEN: ${env:TELEGRAM_TOKEN}
    # If using RobinHood    
    RH_USER_EMAIL: ${env:RH_USER_EMAIL}
    RH_PASSWORD: ${env:RH_PASSWORD}
    RH_MFA_CODE: ${env:RH_MFA_CODE}
    # If using Alpaca
    ALPACA_KEY_ID: ${env:ALPACA_KEY_ID}
    ALPACA_SECRET_KEY: ${env:ALPACA_SECRET_KEY}
functions:
  cron:
    handler: handler.send_message
    events:
      # Invoke Lambda function at 21:00 UTC every day
      - schedule: cron(00 21 * * ? *)

Этот код сообщает AWS, какая среда выполнения нам нужна, и берет токен телеграма из нашего собственного окружения, чтобы нам не пришлось его развертывать. После этого мы определяем Сron для ежедневного запуска функции в 21:00.

Единственное, что осталось сделать перед деплойментом, это получить учетные данные AWS и установить их как переменные среды вместе с токеном и остальными переменными как переменные среды. Получить учетные данные достаточно просто.

Из консоли AWS:

  • перейдите в My Security Credentials — Users — Add user;

  • выберите имя пользователя и Programmatic access;

  • на следующей странице выберите Attach existing policies directly — AdministratorAccess;

  • скопируйте и сохраните Access Key ID и Secret Access Key;

Вот и все. Теперь давайте экспортируем учетные данные AWS и токен телеграма. Откройте терминал и напишите:

$ export AWS_ACCESS_KEY_ID=[your key goes here]
$ export AWS_SECRET_ACCESS_KEY=[your key goes here]
$ export TELEGRAM_TOKEN=[your token goes here]# 
If using RobinHood
$ export RH_USER_EMAIL=[your mail goes here]
$ export RH_PASSWORD=[your password goes here]
$ export RH_MFA_CODE=[your mfa code goes here]
    
# If using Alpaca
$ export ALPACA_KEY_ID=[your key goes here]
$ export ALPACA_SECRET_KEY=[your key goes here]

Установите необходимые пакеты локально и выполните деплоймент в AWS:

$ pip3 install -r requirements.txt -t . --system
$ serverless deploy

Готово! Бот будет торговать за нас каждый день в 21:00 и отправлять нам сообщения о совершенном действии. Для апробации концепции неплохо. Пожалуй, можно порадовать приятеля: теперь он может расслабиться и заниматься трейдингом, не заглядывая в смартфон сто раз в день :)

Напомню, что у всех ресурсов, которые мы использовали в этом руководстве, есть собственная документация. Вы можете углубиться в любом направлении, которое вас заинтересует, ведь мы опробовали всего-навсего игрушечную систему! И все-таки я думаю, что эта игрушечная система — хорошее начало для многофункционального сложного продукта. Удачи!

Команда VK Cloud Solutions развивает собственные ML-решения. Будем признательны, если вы их протестируете и дадите обратную связь. Для тестирования пользователям при регистрации начисляем 3000 бонусных рублей.

Читать по теме:

 

Комментарии (6)


  1. zxweed
    03.06.2022 16:26
    +4

    Такие статьи надо называть "как с помощью python раздать все свои деньги в рынок"


  1. antonblockchain
    03.06.2022 20:53

    бектеста почему-то нет?
    неспроста.


  1. Adjuster2004
    03.06.2022 20:55

    В казино выигрывает казино.

    На бирже выигрывают большие деньги.

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


    1. turboslon
      04.06.2022 05:35
      +1

      На мой взгляд, Вы переоцениваете коллективные «большие деньги». Так и до масонов с рептилоидами можно докатиться.


      1. Diamos
        05.06.2022 01:52

        Человек не переоценивает, описывает реальность (по крайней мере криптовалютного рынка, на котором я торгую). Я бы добавил, что не просто «большие деньги», а «умные большие деньги». На сленге трейдеров людей/организации/фонды управляющих большими деньгами, называют маркет-мейкерами. Помимо стрижки толпы они приносят и положительное — поддерживают ликвидность торгового инструмента, удерживают ценовой диапазон, воюют между собой, заманивают толпу и т.д. Всё это позволяет зарабатывать и тем простым смертным, которые научились с помощью тех.анализа предугадывать действия нейросетей маркет-мейкеров.


  1. makar_crypt
    04.06.2022 13:28

    А у yfinance архитектура открыта? Меня больше удивил момент как они считают 1d,1w,1m price change в реалтайме. Неужно просто в postgres делают LEAD \ LAG или всеже это сначала бакеты по 1d\1w\**** а потом при ответе JOIN last from 1d JOIN last from 1w , но тогда там столько join'ov что не выглядит производительно.

    Интересный момент