3 декабря 2018 Румянцев Александр
Данный алгоритм появился из стороннего примера, найденного на Quantopian. Я его оптимизировал и сопроводил обильными комментариями на русском. Это не лучшее использование воронок (Pipeline). Но зато использует произвольные факторы (CustomFactor).
Всё это появилось по просьбе автора MindSpace.ru, Оксаны Гафаити. Поехали!
Идея
Торгуем 2000-ми акций с наибольшей капитализацией. В основе три фундаментальных показателя:
P/B — цена к балансовой стоимости. Чем меньше, тем лучше. Ценностные акции.
P/E — цена к прибыли. Чем меньше, тем лучше. Недооценённые акции.
ROA — рентабельность активов. Чем больше, тем лучше.
Все акции упорядочиваем по значениям показателей в зависимости от наших потребностей и получаем три рейтинга. Для каждой акции рассчитываем конечный рейтинг по формуле:
В портфель попадут ТОП 20 акций с положительным моментумом за последние 30 дней.
Произвольный фактор (CustomFactor)
Создать свой фактор легко. В списке inputs перечисляем источники данных. А в методе compute() сохраняем рассчитанное значение в аргумент out. Ниже пример расчёта моментума:
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
Алгоритм
Ребалансируем в первый торговый день месяца на открытии рынка. Стараемся приблизить к реальности, так чтобы ордера выставлялись перед открытием торговой сессии.
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.
Добавить фильтр по росту просадок за последний месяц.
Не является индивидуальной инвестиционной рекомендацией | При копировании ссылка обязательна | Нашли ошибку - выделить и нажать Ctrl+Enter | Жалоба