Простая стратегия с фундаменталом для Quantopian


3 декабря 2018 Румянцев Александр
Простая стратегия с фундаменталом для Quantopian


Данный алгоритм появился из стороннего примера, найденного на 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.
Добавить фильтр по росту просадок за последний месяц.
(C) Источник

При копировании ссылка обязательна | Не является индивидуальной инвестиционной рекомендацией
Поддержите нас - ссылаясь на материалы и приводя новых читателей
Нашли ошибку: выделите и Ctrl+Enter