С лета этого года МосБиржа позволяет получать данные соотношения лонг и шорт позиций на фьючерсы различных акций (и не только) у физических и юридических лиц по отдельности (раньше Мосбиржа требовала за эту информацию 5К$). Мы хотим проверить, можно ли получить прибыльную стратегию (прибыльнее, чем рынок), торгуя против против физиков.
Сначала требуется получить данные по позициям, а затем по ценам базовых активов (изначально мы хотели торговать фьючерсами, однако данные МосБиржи по позициям указаны за все фьючерсы, а не за какой-то с конкретной датой экспирация, а цены за фьючерсы МосБиржа отображает как цену фьючерса с самым близким к дате временем экспирации, поэтому использовать цены дериватива будет некорректно).
Для получения данных мы используем файлы: Parser_future_fiz_yur.ipynb и Parser_Price_asset.ipynb. Устроены они по одному алгоритму (во втором файле реализовано параллельное скачивание), программа собирает последовательно все дни из предоставленного промежутка (собираем именно день, так как так удобно, вообще можно было бы собирать 3 дня, но тогда бы приходилось бы дополнительно обрабатывать данные, что уменьшало бы производительность), а затем соединяем их в один большой DataFrame и сохраняем в виде .csv файла. В программе предусмотрено, что МосБиржа может в какой-то момент отказать в доступе (например, часто бывала такая ошибка: "превышено время ожидания"), если такое произойдёт, то программа сохранит те данные, что уже есть и при повторном запуске проверяет присутствующие дни и начнёт с последнего сохраненного.
Наши стратегии берут данные по позициям (до того дня, на котором сейчас "находится"). Так как мы имеем данные за каждые 5 минут, а цены только за каждый день, чтобы ребалансировка не происходила часто мы используем скользящие средние с некоторым параметром.
Начинаем мы с того, что стратегия высчитывает вариацию и домножая ее на STD_COUNT_UP в случае верхней границы и STD_COUNT_DOWN в случае нижней, строит диапозон. При пересечении нижней границы диапозона открываем шорт и пересчитываем диапозон. Если пересечём нижнюю границу ещё раз, то просто пересчитаем диапазон. Если верхнюю, то закроем шорт и пересчитаем диапазон. С лонгом аналогично: при пересечении верхней границы открывается лонг. Пересекая верхную ещё раз лонг продолжается, при пересечения нижней - позиция закрывается.
RISK: параметр риска (сколько от нашего баланса мы тратим на открытие позиции). В нашем случае RISK = 1, так как он изначально этот параметр предназначался для построения портфеля акций, но мы это не реализовали.
STD_COUNT_UP, STD_COUNT_DOWN: коэффициенты для расчета границ скользящей средней.
MA_COUNT: ширина окна для построения скользящей средней.
train_data, test_data = clear_data('sr') # clear_data возвращает два датафрейма с данными о активе
strategy = Strategy(train_data, 10000000)
df = pd.DataFrame(strategy.run(1, 1, 1, 500))
Лонг: Траектория ударилась в верхнюю границу. Открылся лонг. Диапазон пересчитался. Дальше она несколько раз бьет верхнюю границу. Там только диапазон пересчитывается. В конце она пересекает нижнюю границу и Лонг закрывается (То есть это жизнь одного Лонга от открытия до закрытия).
Шорт:
Мы хотим оптимизировать параметры, о которых написали выше, для этого мы берем исторические данные и прогоняем наши стратегии с разными начальными параметрами, параметры мы просто задаём через range с некоторым шагом (мы его подбираем самостоятельно).
Например:
std_counts_up = np.arange(0.25, 2, step_std_counts_up)
std_counts_down = np.arange(0.25, 2, step_std_counts_down)
ma_counts = np.arange(500, 3000, step_ma_counts)
Самая главная проблема, с которой нам пришлось столкнуться - переобучения, а именно тот факт, что мы никогда не можем быть уверены, что в реальной жизни наша стратегия поведёт себя таким же образом, что и на бектестах, если банально брать стратегию, которая будет "самая доходная" на train, то вероятнее всего она будет банально переобучена и на реальном рынке бы банально ушла бы в минус, в чем можно убедиться на test данных, так мы плавно переходим к методам нашей борьбы с переобучением.
-
Мосбиржа позволяет получить данные, начиная с мая 2020 года, однако из-за событий февраля 2022 года любое разбиение так или иначе будет показывать не самые корректные результаты, так как модель обучалась на данных одного вида и с одним трендом, а в test получает данные с другими характеристиками, поэтому так или иначе обучение будет некорректно.
-
Положим мы смогли хорошо разбить наши данные (приходится это делать чуть ли не пополам, хотя обычно делят в отношении
$\frac{1}{3}$ или$\frac{4}{5}$ ), однако тогда проблема заключается в поиске наилучшей стратегии, положим мы хотим получить самую доходную стратегию, тогда нам надо её как-то выбирать по результатам train и test, то если наш выбор будет как-то зависеть от результатов на этих двух выборках, то есть разбиение не выполняет своих изначальных целей, так как test данные представляют собой "контрольную группу", а если мы их используем при оптимизации, то они становятся "часть обучения".
Для решения этой задачи мы решили поизучать литературу ("Машинное обучение: алгоритмы для бизнеса" Маркез Лопез) и нашли дилемму bios-variance. А именно наше предположение строится на том, что при малом изменении параметров стратегии доходность очень сильно меняется (дисперсия по соседям велика), то, скорее всего, стратегия будет переобучена на этих данных, то есть конкретно эти начальные данные позволяют пользоваться train данными настолько оптимально, что доходность стратегии очень сильно увеличивается, а значит были обнаружены не общие паттерны (которые мы и хотим найти при обучении), а один конкретный эпизод, который, вероятнее всего, свойственен только этим конкретным train данным.
Что мы предлагаем сделать, задать некоторую
Замечу, что доходности по понятным причинам находятся около 1, а вариация в промежутке от 0 до 0.08
Функцию полезности будем задавать так:
Тогда, если рассчитать функцию полезности на некотором множестве начальных параметров тогда получим такую картину (цвет - значение функции полезности):
Также одна из проблем с которых мы столкнулись - время, затрачиваемое на подбор, для этого мы попытались использовать numby (не получилось), поэтому используем ThreadPoolExecutor. Что помогает ускорить программу примерно в 2 раза.
Если подразумевать, что у нас получилось избежать переобучения полученные данные при помощи функции полезности и она дейсвтительно "показывает" как стратегия будет работать в реальной жизни, то результаты таковы, что наибольшее значение максимизируется при максимизации ma_count, то есть по сути максимизация достигается при условии того, что мы просто хеджируем наши деньги и ничего на них не покупаем. Получается, что в полученном классе стратегий оптимальная будет - не использоваться эту стратегию.
Гипотиза, что торговля против физиков прибыльна - не верна, во всяком случае для приведённого класса стратегий.
- Для подбора хорошей функции полезности можно, например, смотреть насколько получается гладкая доходность на test данных.
- Какое-то хеджирование, то есть Risk доделать.
- Как-то иначе задавать множества начальных данных, к примеру, использоваться optuna для рандомизации выбора.
- Применить не для одной акции, а для портфеля акций.
- Реализовать автоматическую ежедневную загрузку актуальных данных.
Самойлов Роман, Максим Дугин