Данный алгоритм появился из стороннего примера, найденного на Quantopian. Я его оптимизировал и сопроводил обильными комментариями на русском. Это не лучшее использование воронок (Pipeline). Но зато использует произвольные факторы (CustomFactor).
Всё это появилось по просьбе автора MindSpace.ru, Оксаны Гафаити. Поехали!
💡 Идея
Торгуем 2000-ми акций с наибольшей капитализацией. В основе три фундаментальных показателя:
- P/B — цена к балансовой стоимости. Чем меньше, тем лучше. Ценностные акции.
- P/E — цена к прибыли. Чем меньше, тем лучше. Недооценённые акции.
- ROA — рентабельность активов. Чем больше, тем лучше.
Все акции упорядочиваем по значениям показателей в зависимости от наших потребностей и получаем три рейтинга. Для каждой акции рассчитываем конечный рейтинг по формуле:
В портфель попадут ТОП 20 акций с положительным моментумом за последние 30 дней.
🛠 Произвольный фактор (CustomFactor)
Создать свой фактор легко. В списке inputs
перечисляем источники данных. А в методе compute()
сохраняем рассчитанное значение в аргумент out
. Ниже пример расчёта моментума:
1 2 3 4 5 6 7 8 9 10 11 12 |
class Momentum(CustomFactor): """ Получаем моментум за 30 дней. """ # Получаем цены закрытия акций, торгующихся в США за последние 30 дней inputs = [USEquityPricing.close] window_length = 30 # количество дней # Получаем изменение цены за 30 дней def compute(self, today, assets, out, close): # [:] чтобы записывать во входящий аргумент out и не создавать новую переменную out[:] = close[-1] / close[0] - 1 |
📈 Алгоритм
Ребалансируем в первый торговый день месяца на открытии рынка. Стараемся приблизить к реальности, так чтобы ордера выставлялись перед открытием торговой сессии.
Код алгоритма:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
""" Source: https://www.quantopian.com/posts/alghoritm-dlia-kursa-investment-management """ from quantopian.algorithm import attach_pipeline, pipeline_output from quantopian.pipeline import Pipeline from quantopian.pipeline import CustomFactor from quantopian.pipeline.data.builtin import USEquityPricing from quantopian.pipeline.data import morningstar import pandas as pd import numpy as np class Momentum(CustomFactor): """ Получаем моментум за 30 дней. """ # Получаем цены закрытия акций, торгующихся в США за последние 30 дней inputs = [USEquityPricing.close] window_length = 30 # количество дней # Получаем изменение цены за 30 дней def compute(self, today, assets, out, close): # [:] чтобы записывать во входящий аргумент out и не создавать новую переменную out[:] = close[-1] / close[0] - 1 class Pricetobook(CustomFactor): """ Получаем P/B (цена к балансовой стоимости) """ # Получаем P/B от Morningstar за последний день inputs = [morningstar.valuation_ratios.pb_ratio] window_length = 1 # количество значений def compute(self, today, assets, out, pb): table = pd.DataFrame(index=assets) table["pb"] = pb[-1] # Если значение P/B для акции отсутствует, тогда заполняем недостающие # значения максимальным. Чтобы исключить эти акции, так как алгоритм # будет выбирать акции с наименьшим значением P/B. Акции ценности. out[:] = table.fillna(table.max()).mean(axis=1) class Pricetoearnings(CustomFactor): """ Получаем P/E (цена к прибыли) """ # Получаем P/E от Morningstar за последний день inputs = [morningstar.valuation_ratios.pe_ratio] window_length = 1 def compute(self, today, assets, out, pe): table = pd.DataFrame(index=assets) table["pe"] = pe[-1] # Если значение P/E для акции отсутствует, тогда заполняем недостающие # значения максимальным. Чтобы исключить эти акции, так как алгоритм # будет выбирать акции с наименьшим значением P/E. Недооценённые акции. out[:] = table.fillna(table.max()).mean(axis=1) class Roa(CustomFactor): """ Получаем RoA (рентабельность активов) FIXME Не используется """ # Получаем RoA от Morningstar за последний день inputs = [morningstar.operation_ratios.roa] window_length = 1 def compute(self, today, assets, out, roa): table = pd.DataFrame(index=assets) table["roa"] = roa[-1] # Если значение RoA для акции отсутствует, тогда заполняем недостающие # значения минимальным. Чтобы исключить эти акции, так как алгоритм # будет выбирать акции с наибольшим значением RoA. out[:] = table.fillna(table.min()).mean(axis=1) class Roe(CustomFactor): """ Получаем RoE (рентабельность собственного капитала) """ # Получаем RoE от Morningstar за последний день inputs = [morningstar.operation_ratios.roe] window_length = 1 def compute(self, today, assets, out, roe): table = pd.DataFrame(index=assets) table["roe"] = roe[-1] # Если значение RoE для акции отсутствует, тогда заполняем недостающие # значения минимальным. Чтобы исключить эти акции, так как алгоритм # будет выбирать акции с наибольшим значением RoE. out[:] = table.fillna(table.min()).mean(axis=1) class MarketCap(CustomFactor): """ Создание своего признака для расчета рыночной капитализации на основании последней цены закрытия и кол-ва акций """ # Получаем цену закрытия и кол-во акций от Morningstar за последний день inputs = [USEquityPricing.close, morningstar.valuation.shares_outstanding] window_length = 1 def compute(self, today, assets, out, close, shares): # Умножим цену на кол-во акций out[:] = close[-1] * shares[-1] def initialize(context): """ Подготовка алгоритма Создадим воронку (pipeline) для отбора акций, в которые будем инвестировать. """ # создадим пустую воронку и прикрепим к алгоритму pipe = Pipeline() attach_pipeline(pipe, 'ranked_2000') # 1. ТОП 2000 АКЦИЙ по КАПИТАЛИЗАЦИИ # выберем 2000 акций с наибольшей капитализацией, которые будут обновляться каждый день mkt_cap = MarketCap() top_2000 = mkt_cap.top(2000) # 2. ФОРМУЛА ОТБОРА АКЦИЙ # создадим рейтинг бумаг, упорядоченных от наименьшего к наибольшему (0 -> 9) по P/B pb = Pricetobook() pb_rank = pb.rank(mask=top_2000, ascending=True) # создадим рейтинг бумаг, упорядоченных от наименьшего к наибольшему (0 -> 9) по P/E pe = Pricetoearnings() pe_rank = pe.rank(mask=top_2000, ascending=True) # создадим рейтинг бумаг, упорядоченных от наибольшего к наименьшему (9 -> 0) по RoE roe = Roe() roe_rank = roe.rank(mask=top_2000, ascending=False) # создадим новые порядковые номера по среднему значению суммы рейтингов # P/B+P/E+RoE с равными весами combo_raw = (1*pb_rank + 1*pe_rank + 1*roe_rank) / 3 # добавим в воронку рейтинг акций по нашей формуле от наименьшего к наибольшему (0 -> 9) pipe.add(combo_raw.rank(mask=top_2000), 'combo_rank') # 3. ФИЛЬТР ПО ПОЛОЖИТЕЛЬНОМУ МОМЕНТУМУ # среди акций оставим только те, которые показали рост в последние 30 дней momentum = Momentum() pipe.set_screen(top_2000 & (momentum > 0)) # ребалансируем в первый торговый день каждого месяца на открытии # (эмулируем установку ордеров перед открытием рынка) schedule_function(func=rebalance, date_rule=date_rules.month_start(days_offset=0), time_rule=time_rules.market_open(), half_days=True) # ежедневно на закрытии рынка собираем статистику schedule_function(func=record_vars, date_rule=date_rules.every_day(), time_rule=time_rules.market_close(), half_days=True) # для исключения торговли с плечом ограничим использованием только доступного капитала context.long_leverage = 0.9 def before_trading_start(context, data): """ Выбор акций перед ребалансировкой """ # перед ребалансировкой подготовим наши акции context.output = pipeline_output('ranked_2000') # используем только 20 верхних акций по нашему рейтингу context.long_list = context.output.sort_values(['combo_rank'], ascending=True).iloc[:20] def record_vars(context, data): """ Собираем статистику использования капитала """ record(leverage=context.account.leverage, positions=len(context.portfolio.positions)) def rebalance(context,data): """ Ребалансировка """ try: # покупать доступные акции будем равными частями long_weight = context.long_leverage / float(len(context.long_list)) except ZeroDivisionError: # если акций нет, long_weight = 0 # ограничиваем максимальный вес до 5%, в случае доступности малого кол-ва бумаг if long_weight > 0.054: long_weight = 0.05 # закрываем позиции, которых нет на открытие for stock in context.portfolio.positions.iterkeys(): if stock not in context.long_list.index: order_target(stock, 0) # открываем новые позиции или ребалансируем for long_stock in context.long_list.index: order_target_percent(long_stock, long_weight) |
🏁 Заключение
Алгоритм показывает хорошие результаты по доходности и опережает рынок в период с 2002 до 2018 гг. Но присутствует очень высокая просадка в -63%. Она не позволяет использовать его для торговли. Идеи улучшения:
- Добавить анализ роста волатильности.
- Добавить фильтр SMA 50 и SMA 200.
- Добавить фильтр по росту просадок за последний месяц.
🛠 Вы можете заказать улучшение данной стратегии и подключение её к Interactive Brokers, написав мне на почту.
💬В комментариях задавайте вопросы и напишите, как можно уменьшить просадку.
Автор Quantrum.me
Telegram-канал📣: @quantiki
Подбор и тестирование портфелей. Подключение стратегий к IB.