Инвестирование в портфель акций с помощью PyPortfolioOpt

Здравствуйте, читатели. Итак, без лишних слов, давайте начнем. Ранее мы видели, как библиотека используется для торговли портфелем акций, если вы этого не сделали, пожалуйста, прочитайте ее здесь. Этот пост является продолжением предыдущего.pyportfolioopt

Сегодня цель состоит в том, чтобы выбрать портфель акций и инвестировать сумму в эти акции каким-то оптимизированным (взвешенным) способом. Вес будет перебалансироваться каждый месяц, в тот день, когда мы продадим наши предыдущие акции и купим новые с новым распределением суммы. Скажем, в вашем портфеле есть 3 акции, а у вас есть 10000 долларов, поэтому алгоритм решает из 10000, как инвестировать в Apple, Microsoft и Amazon. Это «сколько выделить» решается каждый месяц в тот день, когда мы продаем наш предыдущий холдинг и покупаем новые с новыми суммами для каждой акции.AAPL,MSFTAMZN

Итак, давайте перейдем к кодированию. Ниже мы импортировали все наши необходимые зависимости.

Вы можете установить cloudcraftz libray, если хотите проверить выполненную стратегию. «pip install cloudcraftz«.

Импорт отчетов

from pypfopt.discrete_allocation import DiscreteAllocation, get_latest_prices
from pypfopt import EfficientFrontier
from pypfopt import risk_models
from pypfopt import expected_returns
from pypfopt import objective_functions
from cloudcraftz.utils import financial_summary, 


import numpy as np 
import yfinance as yf
import pandas as pd
import warnings
import glob
import matplotlib.pyplot as plt
warnings.filterwarnings('ignore')

Манипуляции с данными

Ниже мы только что собрали тикеры (символы) различных акций, в которые мы хотели бы инвестировать. Я выбираю некоторые тикеры из индийского фондового рынка (NIFTY 50), вы можете выбрать все, что вам нравится.

# Let's get a set of tickers for our experiment
url = 'https://en.wikipedia.org/wiki/NIFTY_50'
tickers = pd.read_html(url, header=0)[1]
tickers = pd.Series(tickers.Symbol.values).apply(lambda x: x+'.NS')df = pd.DataFrame(columns=['Date', 'High', 'Low', 'Open', 'Close', 'Volume', 'Adj Close'])

for path in tickers:
    data = yf.download(tickers=path, start='2008-01-04', end='2022-06-15')
    data.reset_index(inplace=True)
    
    if data['Date'].loc[0].strftime("%Y-%m-%d")[:4] < '2010':
        data['TIC'] = path.split('/')[-1].replace('.csv', '')
        df = pd.concat([df, data])[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completeddf.reset_index(drop=True, inplace=True)
df.sort_values(['Date', 'TIC'], inplace=True)
df.set_index('Date', inplace=True)df.head()
df.tail()

Итак, мы подготовили наши данные, где у нас есть данные по всем акциям с 2008 по 2022 год.

Поскольку мы заинтересованы в построении портфеля из вышеуказанных акций, мы рассмотрим только вышеперечисленные акции и создадим новый фрейм. (Учитывая, что мы торгуем только по цене закрытия.)Close

Если бы вы просмотрели мой предыдущий пост об этом, вы бы знали, что данные должны быть преобразованы в формат, показанный ниже.

combined_df = df[['Close', 'TIC']]
combined_df.reset_index(inplace=True)
combined_df.loc[:, 'Date'] = pd.to_datetime(combined_df.loc[:, 'Date'])combined_df = combined_df.set_index(['Date', 'TIC']).unstack(level=-1)
combined_df.columns = combined_df.columns.droplevel()
combined_df.columns.name = Nonecombined_df.head()
combined_df.tail()

Ниже мы провели анализ недостающих данных и решили заполнить недостающие значения с помощью метода. (Предполагая, что значения NaN присутствуют, поскольку цена за эти дни была недоступна, поэтому значения предыдущих дней были сохранены). Затем мы использовали данные с 2008 по 2017 год, чтобы первоначально обучить наш алгоритм, рассматривающий данные 2008–2017 годов как исторические данные, доступные нам, и сохранили тестовый набор, который является днем, когда мы фактически начинаем торговать с 2017 года.ffill

combined_df.isnull().sum()ADANIPORTS.NS    2
APOLLOHOSP.NS    2
ASIANPAINT.NS    2
AXISBANK.NS      2
BAJAJ-AUTO.NS    2
BAJAJFINSV.NS    2
BAJFINANCE.NS    2
BHARTIARTL.NS    2
BPCL.NS          2
BRITANNIA.NS     2
CIPLA.NS         2
DIVISLAB.NS      2
DRREDDY.NS       2
EICHERMOT.NS     2
GRASIM.NS        2
HCLTECH.NS       1
HDFC.NS          3
HDFCBANK.NS      2
HEROMOTOCO.NS    2
HINDALCO.NS      2
HINDUNILVR.NS    2
ICICIBANK.NS     2
INDUSINDBK.NS    2
INFY.NS          2
ITC.NS           2
JSWSTEEL.NS      2
KOTAKBANK.NS     2
LT.NS            2
M&M.NS           2
MARUTI.NS        2
NESTLEIND.NS     2
NTPC.NS          2
ONGC.NS          2
POWERGRID.NS     2
RELIANCE.NS      2
SBIN.NS          2
SHREECEM.NS      2
SUNPHARMA.NS     2
TATACONSUM.NS    2
TATAMOTORS.NS    2
TATASTEEL.NS     2
TCS.NS           2
TECHM.NS         2
TITAN.NS         2
ULTRACEMCO.NS    2
UPL.NS           2
WIPRO.NS         2
dtype: int64combined_df.fillna(method='ffill', inplace=True)combined_df.isnull().sum()ADANIPORTS.NS    0
APOLLOHOSP.NS    0
ASIANPAINT.NS    0
AXISBANK.NS      0
BAJAJ-AUTO.NS    0
BAJAJFINSV.NS    0
BAJFINANCE.NS    0
BHARTIARTL.NS    0
BPCL.NS          0
BRITANNIA.NS     0
CIPLA.NS         0
DIVISLAB.NS      0
DRREDDY.NS       0
EICHERMOT.NS     0
GRASIM.NS        0
HCLTECH.NS       0
HDFC.NS          0
HDFCBANK.NS      0
HEROMOTOCO.NS    0
HINDALCO.NS      0
HINDUNILVR.NS    0
ICICIBANK.NS     0
INDUSINDBK.NS    0
INFY.NS          0
ITC.NS           0
JSWSTEEL.NS      0
KOTAKBANK.NS     0
LT.NS            0
M&M.NS           0
MARUTI.NS        0
NESTLEIND.NS     0
NTPC.NS          0
ONGC.NS          0
POWERGRID.NS     0
RELIANCE.NS      0
SBIN.NS          0
SHREECEM.NS      0
SUNPHARMA.NS     0
TATACONSUM.NS    0
TATAMOTORS.NS    0
TATASTEEL.NS     0
TCS.NS           0
TECHM.NS         0
TITAN.NS         0
ULTRACEMCO.NS    0
UPL.NS           0
WIPRO.NS         0
dtype: int64train, test = combined_df[:"2017-06-05"], combined_df["2017-06-05":]

Оптимизация веса сроком на 1 месяц

Теперь наступает реальная часть, где мы фактически начинаем выделять деньги на различные акции в нашем портфеле. Приведенная ниже функция принимает 3 входа, то есть в основном данные за один месяц и которые мы получили после оптимизации на исторических данных.cashbacktest_dfweights

Затем мы запустили цикл for для всего тестового набора, но мы не использовали весь набор сразу, а использовали только данные за 22 дня за раз (так как в месяце всего 22 торговых дня). Используя веса, которые мы получили от оптимизации тренировочных данных, мы выделяем и инвестируем в течение 22 дней, чтобы получить наши результаты, а затем добавляем новые 22-дневные данные, которые использовались для тестирования, в тренировочный набор, что делает наш алгоритм устойчивым к «дрейфу концепции», также этот вид обучения известен как онлайн-обучение, поскольку он продолжает получать новые данные для лучшей оптимизации. Процесс продолжается.

def backtest(cash, backtest_df, weights):
    amount_allocation = {}
    shares, total, balance, total_invested = {}, 0, 0, 0
    prices = backtest_df.iloc[0].to_dict()
    new_prices = backtest_df.iloc[len(backtest_df)-1].to_dict()

    for keys in backtest_df.columns:
        amount_allocation[keys] = weights[keys] * cash

    for keys in backtest_df.columns:
        shares[keys] = (amount_allocation[keys] // prices[keys])

    for keys in backtest_df.columns:
        total_invested = total_invested + (shares[keys] * prices[keys])

    balance = cash - total_invested

    for keys in backtest_df.columns:
        total = total + (shares[keys] * new_prices[keys])

    return total_invested, total, balancecash = 1000000
initial, getch, balances = [], [], []
dates, port_values = [], []# Then we start trading.for i in range(len(test)):
    # Calculate expected returns and sample covariance
    mu = expected_returns.mean_historical_return(train, frequency=22)
    S = risk_models.sample_cov(train, frequency=22)

    # Optimize for maximal Sharpe ratio
    ef = EfficientFrontier(mu, S)
    ef.add_objective(objective_functions.L2_reg, gamma=0.1)
    raw_weights = ef.max_sharpe(risk_free_rate=0)
    cleaned_weights = ef.clean_weights()

    # The weights dictionary
    weights = dict(cleaned_weights)

    if (22*i) < len(test):
        new_test = test[(22*i):(22*i+22)+1]
    else:
        break

    invested, total, balance = backtest(cash, new_test, weights)

    initial.append(invested)
    getch.append(total)
    balances.append(balance)
    dates.append(new_test.index[0].strftime("%Y-%m-%d"))

    portfolio = balance + total
    cash = portfolio
    port_values.append(cash)

    train.append(new_test)fin_df = pd.DataFrame({'date': dates, 'invested': initial, 'fetch': getch, 'balances': balances, 'portfolio': port_values})
fin_df['returns'] = fin_df['portfolio'].pct_change()
fin_df.fillna(0, inplace=True)financial_summary(fin_df, frequency='M')

Затем мы собрали все вложенные и прибыльные суммы, и передали функцию из библиотеки, чтобы посмотреть, как работает наш алгоритм.financial_summarycloudcraftz

Мы сделали 16% годовых, с коэффициентом шарпа 0,9 (приблизительно). Что неплохо!

Источник