Como o Python ajuda a substituir consultores financeiros

Para continuar o artigo sobre os perigos da diversificação excessiva, criaremos ferramentas úteis de seleção de ações. Depois disso, faremos um reequilíbrio simples e adicionaremos as condições exclusivas dos indicadores técnicos, que costumam faltar nos serviços populares. E então compare os retornos sobre ativos individuais e portfólios diferentes.

Em tudo isso, usamos Pandas e minimizamos o número de ciclos. Agrupe a série temporal e desenhe os gráficos. Vamos nos familiarizar com vários índices e seu comportamento. E tudo isso no Jupyter no Python 3.6.

Se você quiser fazer algo bem, faça você mesmo.
Ferdinand Porsche

A ferramenta descrita permitirá selecionar os ativos ideais para o portfólio e excluir as ferramentas impostas pelos consultores. Mas veremos apenas o quadro geral - sem levar em conta a liquidez, o tempo para o recrutamento de posições, as comissões dos corretores e o custo de uma ação. Em geral, com um reequilíbrio mensal ou anual de grandes corretores, haverá custos insignificantes. No entanto, antes de aplicar, a estratégia escolhida ainda deve ser verificada no backtester orientado a eventos, por exemplo, Quantopian (QP), a fim de eliminar possíveis erros.

Por que não imediatamente no QP? Hora. Lá, o teste mais simples dura cerca de 5 minutos. E a solução atual permitirá verificar centenas de estratégias diferentes com condições únicas em um minuto.

Carregamento de dados brutos


Para carregar os dados, siga o método descrito neste artigo . Eu uso o PostgreSQL para armazenar preços diários, mas agora está cheio de fontes gratuitas a partir das quais você pode criar o DataFrame necessário.

O código para baixar o histórico de preços do banco de dados está disponível no repositório. O link estará no final do artigo.

Estrutura DataFrame


Ao trabalhar com histórico de preços, para um agrupamento conveniente e acesso a todos os dados, a melhor solução é usar um índice múltiplo (MultiIndex) com data e tickers.

df = df.set_index(['dt', 'symbol'], drop=False).sort_index() df.tail(len(df.index.levels[1]) * 2) 


imagem

Usando um índice múltiplo, podemos acessar facilmente todo o histórico de preços de todos os ativos e agrupar a matriz separadamente por data e ativo. Também podemos obter histórico de preços para um ativo.

Aqui está um exemplo de como você pode agrupar facilmente o histórico por semana, mês e ano. E para mostrar tudo isso nos gráficos das forças do Pandas:

 #      agg_rules = { 'dt': 'last', 'symbol': 'last', 'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum', 'adj': 'last' } level_values = df.index.get_level_values #  fig = plt.figure(figsize=(15, 3), facecolor='white') df.groupby([pd.Grouper(freq='W', level=0)] + [level_values(i) for i in [1]]).agg( agg_rules).set_index(['dt', 'symbol'], drop=False ).close.unstack(1).plot(ax=fig.add_subplot(131), title="Weekly") df.groupby([pd.Grouper(freq='M', level=0)] + [level_values(i) for i in [1]]).agg( agg_rules).set_index(['dt', 'symbol'], drop=False ).close.unstack(1).plot(ax=fig.add_subplot(132), title="Monthly") df.groupby([pd.Grouper(freq='Y', level=0)] + [level_values(i) for i in [1]]).agg( agg_rules).set_index(['dt', 'symbol'], drop=False ).close.unstack(1).plot(ax=fig.add_subplot(133), title="Yearly") plt.show() 


imagem

Para exibir corretamente a área com a legenda do gráfico, transferimos o nível do índice com tickers para o segundo nível acima das colunas usando o comando Series (). Unstack (1). Com o DataFrame (), esse número não funcionará, mas a solução está abaixo.

Ao agrupar por períodos padrão, o Pandas usa a data do calendário mais recente do grupo no índice, que geralmente difere das datas reais. Para corrigir isso, atualize o índice.

 monthly = df.groupby([pd.Grouper(freq='M', level=0), level_values(1)]).agg(agg_rules) \ .set_index(['dt', 'symbol'], drop=False) 

Um exemplo de como obter o histórico de preços de um ativo específico (usamos todas as datas, QQQ ticker e todas as colunas):

 monthly.loc[(slice(None), ['QQQ']), :] #    

Volatilidade mensal de ativos


Agora, podemos observar em algumas linhas no gráfico a variação do preço de cada ativo para o período de interesse para nós. Para fazer isso, obtemos a porcentagem de alterações de preço agrupando o quadro de dados por nível de vários índices com um ticker de ativos.

 monthly = df.groupby([pd.Grouper(freq='M', level=0), level_values(1)]).agg( agg_rules).set_index(['dt', 'symbol'], drop=False) #     .   . monthly['pct_close'] = monthly.groupby(level=1)['close'].pct_change().fillna(0) #  ax = monthly.pct_close.unstack(1).plot(title="Monthly", figsize=(15, 4)) ax.axhline(0, color='k', linestyle='--', lw=0.5) plt.show() 

imagem

Comparar retornos de ativos


Agora, usaremos o método da janela Series (). Rolling () e exibiremos o retorno dos ativos por um determinado período:

Código Python
 rolling_prod = lambda x: x.rolling(len(x), min_periods=1).apply(np.prod) #   monthly = df.groupby([pd.Grouper(freq='M', level=0), level_values(1)]).agg( agg_rules).set_index(['dt', 'symbol'], drop=False) #     .   .   1. monthly['pct_close'] = monthly.groupby(level=1)['close'].pct_change().fillna(0) + 1 #  DataFrame    2007  fltr = monthly.dt >= '2007-01-01' test = monthly[fltr].copy().set_index(['dt', 'symbol'], drop=False) #  dataframe    test.loc[test.index.levels[0][0], 'pct_close'] = 1 #    1 #    test['performance'] = test.groupby(level=1)['pct_close'].transform(rolling_prod) - 1 #  ax = test.performance.unstack(1).plot(title="Performance (Monthly) from 2007-01-01", figsize=(15, 4)) ax.axhline(0, color='k', linestyle='--', lw=0.5) plt.show() #       test.tail(len(test.index.levels[1])).sort_values('performance', ascending=False) 


imagem

Métodos de rebalanceamento de portfólio


Então chegamos ao mais delicioso. Nos exemplos, examinaremos os resultados do portfólio na alocação de capital para ações predeterminadas entre vários ativos. E também adicione condições únicas sob as quais abandonaremos alguns ativos no momento da distribuição do capital. Se não houver ativos adequados, assumimos que o broker possui capital no cache.

Para usar os métodos do Pandas para reequilibrar, precisamos armazenar os compartilhamentos de distribuição e as condições de reequilíbrio em um DataFrame com dados agrupados. Agora considere as funções de reequilíbrio que passaremos para o método DataFrame (). Apply ():

Código Python
 def rebalance_simple(x): #     data = x.unstack(1) return (data.pct_close * data['size']).sum() / data['size'].sum() def rebalance_sma(x): #   ,   SMA50 > SMA200 data = x.unstack(1) fltr = data['sma50'] > data['sma200'] if not data[fltr]['size'].sum(): return 1 #   ,    return (data[fltr].pct_close * data[fltr]['size']).sum() / data[fltr]['size'].sum() def rebalance_rsi(x): #   ,   RSI100 > 50 data = x.unstack(1) fltr = data['rsi100'] > 50 if not data[fltr]['size'].sum(): return 1 #   ,    return (data[fltr].pct_close * data[fltr]['size']).sum() / data[fltr]['size'].sum() def rebalance_custom(x, df=None): #         data = x.unstack(1) for s in data.index: if data['dt'][s]: fltr_dt = df['dt'] < data['rebalance_dt'][s] #   values = df[fltr_dt].loc[(slice(None), [s]), 'close'].values data.loc[s, 'custom'] = 0 #    if len(values) > len(values[np.isnan(values)]): #  RSI  100  data.loc[s, 'custom'] = talib.RSI(values, timeperiod=100)[-1] fltr = data['custom'] > 50 if not data[fltr]['size'].sum(): return 1 #   ,    return (data[fltr].pct_close * data[fltr]['size']).sum() / data[fltr]['size'].sum() def drawdown(chg, is_max=False): #    total = len(chg.index) rolling_max = chg.rolling(total, min_periods=1).max() daily_drawdown = chg/rolling_max - 1.0 if is_max: return daily_drawdown.rolling(total, min_periods=1).min() return daily_drawdown 


Em ordem:

  • rebalance_simple é a função mais simples que distribuirá a lucratividade de cada ativo em ações.
  • rebalance_sma é uma função que distribui capital entre ativos cuja média móvel é 50 dias maior que 200 dias no momento do reequilíbrio.
  • rebalance_rsi - uma função que distribui capital entre ativos para os quais o valor do indicador RSI por 100 dias está acima de 50.
  • rebalance_custom é a função mais lenta e universal, onde calcularemos os valores dos indicadores a partir do histórico diário de preços de ativos no momento do reequilíbrio. Aqui você pode usar quaisquer condições e dados. Faça o download sempre de fontes externas. Mas você não pode ficar sem um ciclo.
  • rebaixamento - função auxiliar, mostrando o rebaixamento máximo no portfólio.

Nas funções de reequilíbrio, precisamos de uma matriz de todos os dados da data por ativos. O método DataFrame (). Apply (), pelo qual calcularemos os resultados dos portfólios, transmitirá uma matriz para nossa função, na qual as colunas se tornarão o índice de linha. E se fizermos um multi-índice, onde os tickers estarão no nível zero, um multi-índice chegará até nós. Podemos expandir esse multi-índice em uma matriz bidimensional e obter os dados do ativo correspondente em cada linha.

imagem

Rebalanceamento de portfólio


Agora basta preparar as condições necessárias e fazer um cálculo para cada portfólio do ciclo. Primeiro, calculamos os indicadores no histórico diário de preços:

 #    1  ,      df['sma50'] = df.groupby(level=1)['close'].transform(lambda x: talib.SMA(x.values, timeperiod=50)).shift(1) df['sma200'] = df.groupby(level=1)['close'].transform(lambda x: talib.SMA(x.values, timeperiod=200)).shift(1) df['rsi100'] = df.groupby(level=1)['close'].transform(lambda x: talib.RSI(x.values, timeperiod=100)).shift(1) 

Agora, agruparemos a história para o período de reequilíbrio desejado, usando os métodos descritos acima. Ao mesmo tempo, tomaremos os valores dos indicadores no início do período para excluir o futuro.

Descrevemos a estrutura das carteiras e indicamos o reequilíbrio desejado. Vamos calcular as carteiras em um ciclo, pois precisamos especificar ações e condições exclusivas:

Código Python
 #  :  ,  ,  portfolios = [ {'symbols': [('SPY', 0.8), ('AGG', 0.2)], 'func': rebalance_sma, 'name': 'Portfolio 80/20 SMA50x200'}, {'symbols': [('SPY', 0.8), ('AGG', 0.2)], 'func': rebalance_rsi, 'name': 'Portfolio 80/20 RSI100>50'}, {'symbols': [('SPY', 0.8), ('AGG', 0.2)], 'func': partial(rebalance_custom, df=df), 'name': 'Portfolio 80/20 Custom'}, {'symbols': [('SPY', 0.8), ('AGG', 0.2)], 'func': rebalance_simple, 'name': 'Portfolio 80/20'}, {'symbols': [('SPY', 0.4), ('AGG', 0.6)], 'func': rebalance_simple, 'name': 'Portfolio 40/60'}, {'symbols': [('SPY', 0.2), ('AGG', 0.8)], 'func': rebalance_simple, 'name': 'Portfolio 20/80'}, {'symbols': [('DIA', 0.2), ('QQQ', 0.3), ('SPY', 0.2), ('IWM', 0.2), ('AGG', 0.1)], 'func': rebalance_simple, 'name': 'Portfolio DIA & QQQ & SPY & IWM & AGG'}, ] for p in portfolios: #    rebalance['size'] = 0. for s, pct in p['symbols']: #       rebalance.loc[(slice(None), [s]), 'size'] = pct #            rebalance_perf = rebalance.stack().unstack([1, 2]).apply(p['func'], axis=1) #    p['performance'] = (rebalance_perf.rolling(len(rebalance_perf), min_periods=1).apply(np.prod) - 1) #    p['drawdown'] = drawdown(p['performance'] + 1, is_max=True) 


Desta vez, precisamos fazer um truque com os índices de coluna e linha para obter o multi-índice desejado na função de reequilíbrio. Conseguiremos isso chamando os métodos DataFrame (). Stack (). Unstack ([1, 2]) em sequência. Este código transferirá as colunas para um multi-índice em minúsculas e retornará o multi-índice com tickers e colunas na ordem desejada.

Pastas prontas para gráficos


Agora resta desenhar tudo. Para fazer isso, execute o ciclo do portfólio novamente, que exibe os dados nos gráficos. No final, desenharemos o SPY como uma referência para comparação.

Código Python
 fig = plt.figure(figsize=(15, 4), facecolor='white') ax_perf = fig.add_subplot(121) ax_dd = fig.add_subplot(122) for p in portfolios: p['performance'].rename(p['name']).plot(ax=ax_perf, legend=True, title='Performance') p['drawdown'].rename(p['name']).plot(ax=ax_dd, legend=True, title='Max drawdown') #       print(f"{p['name']}: {p['performance'][-1]*100:.2f}% / {p['drawdown'][-1]*100:.2f}%") # SPY,   rebalance.loc[(slice(None), ['SPY']), :].set_index('dt', drop=False).performance. \ rename('SPY').plot(ax=ax_perf, legend=True) drawdown(rebalance.loc[(slice(None), ['SPY']), :].set_index('dt', drop=False).performance + 1, is_max=True).rename('SPY').plot(ax=ax_dd, legend=True) ax_perf.axhline(0, color='k', linestyle='--', lw=0.5) ax_dd.axhline(0, color='k', linestyle='--', lw=0.5) plt.show() 


imagem

Conclusão


O código considerado permite selecionar várias estruturas de portfólio e condições de reequilíbrio. Com sua ajuda, você pode verificar rapidamente se, por exemplo, vale a pena manter ouro (GLD) ou mercados emergentes (EEM) em um portfólio. Tente você mesmo, adicione suas próprias condições para indicadores ou selecione os parâmetros já descritos. (Mas lembre-se do erro do sobrevivente e que o ajuste a dados passados ​​pode não corresponder às expectativas no futuro.) E depois decida em quem você confia no seu portfólio - Python ou consultores financeiros?

Repositório: rebalance.portfolio

Source: https://habr.com/ru/post/pt419979/


All Articles