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

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

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

Обратите внимание: если вы новичок в RL, вам, вероятно, будет полезно сначала прочитать мою предыдущую статью об обучении Deep-Q. Этот немного отличается, но он также начинается с более высокого уровня с более неявными знаниями.

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

Постановка проблемы

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

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

Чтобы все было под контролем, я решил работать только с парой предположительно несколько коррелированных акций: AAPL и MSFT. Изначально я хотел, чтобы бот мог выбирать, сколько акций он покупает или продает на каждом временном шаге, и это привело меня в кроличью нору обучения с подкреплением в «пространстве непрерывного действия». В дискретном пространстве бот может получить представление о ценности каждого из своих дискретных действий с учетом текущего состояния. Чем сложнее пространство, тем труднее тренироваться; в непрерывном пространстве спектр действий увеличивается в геометрической прогрессии. Для простоты я сократил свои амбиции на данный момент, чтобы ИИ мог покупать или продавать только одну акцию за временной шаг.

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

Перед тем, как продолжить, вот совет от Captain Obvious: тестируйте все по ходу! Попытка отладить среду и ИИ одновременно («или мне просто нужно потренировать его дольше или настроить гиперпараметры ?!») может стать настоящим кошмаром.

Подготовка данных

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

msf = quandl.get('WIKI/MSFT', start_date="2014-01-01", end_date="2018-08-20")

Я не буду выполнять все утомительные шаги по подготовке данных, вы можете следить за ними в моей записной книжке здесь. Но я хочу указать на ценность того, чтобы не пропускать хотя бы беглый EDA (исследовательский анализ данных) - я сначала не обнаружил, что выбранный мной диапазон дат имеет большой разрыв для AAPL.

Выяснилось, что 9 июня 2014 года акции Apple разделились в соотношении 1: 7. Поэтому я просто разделил все до этой даты на 7, чтобы все было согласовано.

Еще одна важная вещь - удалить тенденции из данных. Сначала я этого не делал, и мой ИИ просто научился максимизировать свою прибыль, покупая акции на раннем этапе и удерживая их до конца игры, потому что была общая восходящая тенденция. Не интересно! Кроме того, у AAPL и MSFT были очень разные средства и стандартные средства разработки, так что это было все равно, что просить ИИ научиться торговать яблоками (каламбур!) И апельсинами.

По-видимому, есть несколько разных способов убрать подобные тенденции; Я выбрал один из модуля обработки сигналов SciPy. Он подбирает линейный аппроксиматор к данным, а затем вычитает оценочные данные из реальных данных. Например, MSFT был преобразован из:

To:

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

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

Давайте пока оставим в стороне вопрос о разделении «поезд / тест»; Вы поймете почему, когда я вернусь к этому позже.

Актер-Критик RL

Я отказался от Q-обучения для реализации бота трейдера по двум причинам:

  • Все говорят, что актер-критик лучше; а также
  • На самом деле это более интуитивно понятно. Забудьте об уравнении Беллмана, просто используйте другую нейронную сеть для вычисления значений состояния и оптимизируйте ее так же, как вы оптимизируете нейронную сеть, выбирающую основное действие (также известную как политика, актер).

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

Код довольно простой, в таком псевдокоде:

For 1..n episodes:
  Until environment says "done":
    Run the current state through the network
    That yields:
      a set of "probabilities" for each action
      a value for how good the state is
    Sample from those probabilities to pick an action
    Act upon the environment with that action
    Save the state, action, reward from environment to a buffer
  Adjust each saved reward by the value of the terminal state
  discounted by gamma each timestep
  Loss for the episode is the sum over all steps of:
    The log probability of the action we took * its reward
    The value of that action compared to what was expected
  Propagate the loss back and adjust network parameters

Вы можете видеть, что A-C не пытается оптимизировать выбор действия - мы никогда не знаем, каким было бы объективно правильное действие, - а скорее:

  • Степень уверенности в каждом действии с учетом его итоговой награды.
  • Степень неожиданности каждой награды с учетом состояния

По мере того, как сеть становится более уверенной в данном действии (p - ›1), потери уменьшаются (ln (p) -› 0), и она медленнее обучается. И, с другой стороны, маловероятные действия, которые были отобраны и неожиданно привели к большим вознаграждениям, приводят к гораздо большим потерям. Что при обратном распространении / градиентном спуске заставляет оптимизатор увеличивать вероятность этого маловероятного действия в будущем.

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

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

Сеть, на которой я в конечном итоге остановился, не была сложной, за исключением того, что, вероятно, для меня было ошибкой использовать повторяющийся слой по причинам, к которым мы придем:

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

Состояние и некоторые грубые ошибки

В конечном итоге я выбрал состояние, состоящее из вектора, состоящего из:

[AAPL holdings, 
MSFT holdings, 
cash, 
current timestep's AAPL opening price, 
current timestep's MSFT opening price, 
current portfolio value, 
past 5 day average AAPL opening price, 
past 5 day average MSFT opening price]

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

Видите ли, я хотел, чтобы ИИ изучал основы сигналов ПОКУПКА и ПРОДАЖИ, а не просто учился отрыгивать временные ряды. Итак, каждый раз, когда среда сбрасывалась, я устанавливал ее на запуск с другого случайного временного шага и использовал другой случайный шаг по данным, и определял «готово» как другое случайное количество шагов в будущем.

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

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

Полученные результаты

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

(Начальная стоимость портфеля составляла в среднем около 3000, поэтому 4000 вознаграждений представляют собой рост на 36%.) Бот тоже усвоил правила: он не обанкротился и не пытался продать больше акций, чем у него было, за исключением одного раза. Вот как это выглядело на разделе обучающих данных:

Мастерим, чтобы добиться лучших результатов

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

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

Я давал агенту небольшую положительную награду за действия и небольшую отрицательную награду за бездействие. Таким образом, действие типа «купить AAPL» может принести +0,1 награды в любой момент в течение последовательности. Но в конечном итоге мы хотим, чтобы бот продолжал работать до конца последовательности, не разоряя себя, что я поощрял, добавляя элемент времени, а именно вычитая количество оставшихся шагов из вознаграждения. «Покупка AAPL» за 50 шагов может дать вознаграждение в -49,9; при 49 шагах награда будет -48,9 (лучше) и так далее.

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

Пара других эмпирических выводов:

  • Компенсируйте редкость хороших наград, значительно увеличивая их по сравнению со всеми отрицательными наградами.
  • При расчете вознаграждения нет необходимости вычитать начальную стоимость портфеля из конечной стоимости. Вы получаете это бесплатно, так как бот всегда пытается оптимизировать выше.
  • Как только бот достигает максимума, он начинает колебаться; в этот момент вам лучше прекратить тренировки.
  • Идеальное место для тренировок, кажется, находится «на грани» между достижением конечного состояния в половине случаев и неудачей в половине случаев.

Разделение на тренировку / тест

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

Что я сделал дальше, так это вернуться раньше во времени к 2012 году и получить данные об акциях с того момента и до настоящего времени; Затем я разделил его примерно на 2/3 поезда и оставил последнюю 1/3 для тестирования.

К сожалению, после учета тренда обе акции испытали сползание в период 2012–2016 / 17 годов, и, если вы помните, тогда рынок считался довольно волатильным. Из-за этого боту было действительно сложно научиться получать прибыль. (Исходя из исходных данных за 2014–2018 гг., Которые я использовал, простая покупка акций и удержание их было довольно хорошей стратегией).

Чтобы помочь боту, я установил шаг на 1 и длину последовательности равную длине обучающих данных, чтобы у него было как можно больше последовательных данных для тренировки. Теперь было обработано достаточно пар «действие-вознаграждение», поэтому процесс был слишком медленным для продолжения работы ЦП, поэтому, добавив 5 операторов .cuda(), я смог перенести большую часть работы на графический процессор.

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

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

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

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

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

Вот одна из его «торговых стратегий», изображенная на графике. Красный означает покупку, зеленый означает продажу, а слева - AAPL, а справа - MSFT. Желтые крестики - это временные интервалы, на которых бот решил не предпринимать никаких действий.

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

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

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