Python для финансов

Получение данных фондового рынка на Python

В этой статье я расскажу вам о некоторых основах импорта данных из Yahoo Finance API.

Yahoo Finance APIпредлагает доступ к широкому спектру данных, включая котировки акций, исторические цены, информацию о компании и многое другое. Для получения дополнительной информации обратитесь к официальной документации и попробуйте различные функции, чтобы лучше понятьбиблиотеку. Чтобы установить финансовый пакет yahoo, напишите следующее:

!pip install yfinance

Теперь мы импортируем библиотеку, написав следующую команду:

import yfinace as yf

Здесь мы будем импортировать данные за последний 1 год о топ-3 индийских компаниях и рассмотрим только близкую цену этих компаний.

# We add .NS at the end as these companies are listed on the NSE(National Stock Exchange)companies = ['RELIANCE.NS','TCS.NS','HDFCBANK.NS']#Date range
start = '2018-01-01'
end = '2022-10-25'data = yf.download(companies,start=start,end=end)['Adj Close']

Понимание доходности, риска и коэффициента Шарпа

Рентабельность инвестиций (ROI) — это показатель эффективности, используемый для оценки прибыльности инвестиций. ROI пытается напрямую измерить сумму возврата конкретных инвестиций по отношению к стоимости инвестиций.

Здесь мы сравним доходы топ-3 индийских компаний. Обратитесь к последней статье, чтобы лучше понять эту статью.

import yfinance as yf
# We add .NS at the end as these companies are listed on the NSE(National Stock Exchange)companies = ['RELIANCE.NS','TCS.NS','HDFCBANK.NS']#Date range
start = '2018-01-01'
end = '2022-11-1'
data = yf.download(companies,start=start,end=end)['Adj Close']#pct_change() is a built-in method in DataFrame that computes the percent change from one row to another which is exactly what a return isreturns = data.pct_change()
returns = returns.dropna()
# Plotting the returns over the last 30 daysreturns.tail(30).plot(figsize=(10,6))

Годовая доходность— это геометрическое среднее суммы, заработанной инвестициями каждый год в течение определенного периода времени. Он показывает, что инвестор заработал бы в течение определенного периода времени, если бы годовая доходность была усугублена.

Волатильность– это статистическая мерадисперсиидоходности для данного актива; выше волатильность, рискованнее ценная бумага. Он представляет собой то, насколько большая цена актива колеблется вокруг его средней цены. Волатильность рассчитывается с использованиемстандартного отклонения (квадратного корня дисперсии).

volatility = returns.std()

Риск— это вероятность того, что результат или фактическая прибыль инвестиций будут отличаться от ожидаемого результата илидоходности. Это включает в себя возможность потерять часть или все оригинальные инвестиции. В финансах стандартное отклонение является распространенным показателем, связанным с риском. Фундаментальной идеей является взаимосвязь между риском и доходностью. Чем больше риск, который инвестор готов принять, тем больше потенциальная доходность. Высокое стандартное отклонение указывает на большую волатильность стоимости и, следовательно, высокую степень риска.

Коэффициент Шарпа— это мера доходности инвестиций с поправкой на риск, рассчитанная путем сравнения ее доходности с доходностью безрискового актива.

Коэффициент Шарпа может быть использован для оценки эффективности портфеля с поправкой на риск. Это может помочь объяснить, связана ли избыточная доходность портфеля с разумными инвестиционными решениями или просто удачей и риском.

sharpe_ratio  = returns.mean()/returns.std()
#Thre are 252 trading days in a yearannualized_sharpe_ratio = sharpe_ratio*(252**0.5)

Просадки, перекос, куртоз и Jarque-Beta

Просадка— это наибольший потенциал потерь в стоимости инвестиционного объекта, он измеряется как разница между самым высоким взглядом и последующим самым низким минимумом в течение определенного периода времени. Актив с низкой величиной просадки указывает на низкий риск и, следовательно, считается более стабильным.

import yfinance as yf# We add .NS at the end as these companies are listed on the NSE(National Stock Exchange)companies = ['RELIANCE.NS','TCS.NS','HDFCBANK.NS']#Date range
start = '2018-01-01'
end = '2022-11-1'data = yf.download(companies,start=start,end=end)['Adj Close']returns = data.pct_change()
returns = returns.dropna()wealth_index = 1000*(1+returns).cumprod()
wealth_index.plot(figsize=(12,7))
previous_peaks = wealth_index.cummax()
previous_peaks.plot()
drawdown = (wealth_index - previous_peaks)/previous_peaks
drawdown.plot()

Skewness — это способ оценки или измерения формы распределения. Он используется для оценки асимметричного поведения, а не для вычисления распределения частот. Распределение на основе значения асимметрии:

  • Skewness = 0: Затем нормально распределяется.
  • Skewness > 0: Затем больший вес в левом хвосте распределения.
  • Skewness < 0: Затем больший вес в правом хвосте распределения.
import scipy.statsscipy.stats.skew(returns)

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

  • Для распределения, имеющего куртоз < 3: он называется плейкуртическим.
  • Для распределения, имеющего куртоз > 3, он называется лептокуртическим, и это означает, что он пытается производить больше выбросов, а не нормальное распределение.
scipy.stats.kurtosis(returns)
scipy.stats.kurtosis(returns)

Тест Жарке-Бераявляется хорошим тестом на пригодность того, имеют ли данные образца асимметрию и куртоз, соответствующие нормальному распределению.

Тест Жарке-Бера проверяет гипотезу:
H0: Данные нормальны
H1: Данные НЕ нормальны

def is_normal(r, level=0.01):"""
Applies the Jarque-Bera test to determine if a Series is normal or
not. The Test is applied at the 1% level by default.
Returns True if the hypothesis of normality is accepted, False otherwise
"""statistic, p_value = scipy.stats.jarque_bera(r)
return p_value > levelreturns.aggregate(is_normal)

Полудиавиация, VaR, CVaR и модификация Корниша-Фишера

Полудивиация— это метод измерения колебаний ниже среднего, в отличие от дисперсии или стандартного отклонения, он рассматривает только отрицательные колебания цен и используется для оценки риска снижения (риск потери в инвестициях) инвестиций.

import yfinance as yf# We add .NS at the end as these companies are listed on the NSE(National Stock Exchange)companies = ['RELIANCE.NS','TCS.NS','HDFCBANK.NS']#Date range
start = '2018-01-01'
end = '2022-11-1'data = yf.download(companies,start=start,end=end)['Adj Close']#pct_change() is a built-in method in DataFrame that computes the percent change from one row to another which is exactly what a return isreturns = data.pct_change()
returns = returns.dropna()def semideviation(r):
"""
Returns the semideviation of r.
r must be a Series or a DataFrame, else it raises a TypeError
""" is_negative = r < 0
return r[is_negative].std(ddof=0)semideviation(returns)

Value at Risk (VaR) количественно определяет степень потерь в инвестициях за определенный период времени. Он обычно используется для контроля уровня подверженности риску в активах, тесно связанных с сотрудничеством. Мы рассмотрим три различных способа вычисления value at risk

  1. Исторический VaR
  2. Параметрический Гауссов VaR
  3. Модифицированный (Корниш-Фишер) VaR

Чтобы вычислить исторический VaR на определенном уровне, скажем, 5%, все, что нам нужно сделать, это найти такое число, чтобы 5% доходности упали ниже этого числа, а 95% доходности упали выше этого числа, то есть мы хотим 5-процентильную доходность.

import numpy as np 
import pandas as pddef var_historic(r, level=5):
"""
Returns the historic Value at Risk at a specified level
i.e. returns the number such that "level" percent of the returns
fall below that number, and the (100-level) percent are above
"""
if isinstance(r, pd.DataFrame):
return r.aggregate(var_historic, level=level)
elif isinstance(r, pd.Series):
return -np.percentile(r, level)
else:
raise TypeError("Expected r to be a Series or DataFrame")var_historic(returns,level=1)

Мы всегда можем преобразовать процентильную точку в z-оценку (которая представляет собой число стандартных отклонений от среднего значения, которым является число). Поэтому, если мы можем преобразовать уровень VaR (например, 1% или 5%) в z-балл, мы можем рассчитать уровень доходности, где этот процент доходности лежит ниже него.

‘scipy.stat.norm’ содержит функцию ‘ppf()’, которая делает именно это. Он принимает процентиль, такой как 0,05 или 0,01, и дает вам z-балл, соответствующий тому, который находится в нормальном распределении.

# We find the z-score corresponding to percentile level, and then add that many standard deviations to the mean, to obtain the VaR.from scipy.stats import normdef var_gaussian(r, level=5):
"""
Returns the Parametric Gaussian VaR of a Series or DataFrame
"""
# compute the Z score assuming it was Gaussian
z = norm.ppf(level/100)
return -(r.mean() + z*r.std(ddof=0))var_gaussian(returns)

Модификация Cornish-Fisher представляет собой элегантную и простую настройку:

Z-оценка говорит нам, сколько стандартных отклонений от среднего нам нужно пройти, чтобы найти VaR. Если доходность не является нормальной, мы знаем, что z-оценка даст нам неточное число. Основная идея заключается в том, что, поскольку мы можем наблюдать асимметрию и куртоз данных, мы можем скорректировать z-балл вверх или вниз, чтобы придумать измененную z-оценку.

def var_gaussian(r, level=5, modified=False):
"""
Returns the Parametric Gauusian VaR of a Series or DataFrame
If "modified" is True, then the modified VaR is returned,
using the Cornish-Fisher modification
"""
# compute the Z score assuming it was Gaussian
z = norm.ppf(level/100)if modified:
# modify the Z score based on observed skewness and kurtosis
s = skew(r)
k = kurtosis(r)
z = (z + (z**2 - 1)*s/6 + (z**3 -3*z)*(k-3)/24 -(2*z**3 - 5*z)* (s**2)/36)return -(r.mean() + z*r.std(ddof=0))var_gaussian(returns)

Теперь мы сравним различные методы:

var_table = [var_gaussian(returns),
    var_gaussian(returns, modified=True),
    var_historic(returns)]
comparison = pd.concat(var_table, axis=1)
comparison.columns=['Gaussian', 'Cornish-Fisher', 'Historic']
comparison.plot.bar(title="Hedge Fund Indices: VaR at 5%")

Обратите внимание, что если в некоторых случаях VaR корнуэльского рыбака ниже, то есть он оценивает меньшие потери, чем вы могли бы получить из чисто гауссовского предположения. Это может произойти, если наблюдаемый перекос положительный.

Эффективная граница и создание оптимального портфеля

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

Эффективный рубеж — это набор оптимальных портфелей, которые предлагают самую высокую ожидаемую доходность для определенного уровня риска. Он графически представляет портфели, которые максимизируют доходность для предполагаемого риска.

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

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

Инвестор с аппетитом к высокому риску может искать инвестиции, которые попадают на правую сторону границы. Напротив, более консервативный инвестор (аппетит с низким уровнем риска) может искать инвестиции по левую сторону границы.

Здесь мы будем использовать библиотеки yfinance, riskfolio и seaborn. Riskfolio — это библиотека для количественного стратегического распределения активов или оптимизации портфеля. Для того чтобы ознакомиться с yfinance обратитесь к предыдущим статьям этой серии. Мы можем установить их, написав следующие команды:

!pip install yfinance
!pip install riskfolio-lib

Для того, чтобы riskfolio работал, вам необходимо перезапустить ядро вашего ноутбука.

Здесь мы будем использовать некоторые из лучших индийских компаний.

import yfinance as yf
import pandas as pd
import numpy as np
import riskfolio as rp

# We add .NS at the end as these companies are listed on the NSE(National Stock Exchange)
companies = ['RELIANCE.NS','TCS.NS','HDFCBANK.NS','ASIANPAINT.NS','PIDILITIND.NS']

#Date range
start = '2018-01-01'
end = '2022-11-16'

data = yf.download(companies,start=start,end=end)['Adj Close']
data.head()

#pct_change() is a built-in method in DataFrame that computes the percent change from one row to another which is exactly what a return is
returns = data.pct_change().dropna()
returns.head()

Теперь мы вычисляем матрицу корреляции для отображения корреляции между выбранными акциями.

import seaborn as sns

corr_df = data.corr().round(2) # round to 2 decimal places
fig_corr = sns.heatmap(corr_df,annot=True)

Теперь мы рассчитываем оптимальный портфель, состоящий из выбранных акций.

# Building the portfolio object
port = rp.Portfolio(returns=returns)

# Calculating optimal portfolio

# Select method and estimate input parameters:

method_mu='hist' # Method to estimate expected returns based on historical data.
method_cov='hist' # Method to estimate covariance matrix based on historical data.

port.assets_stats(method_mu=method_mu, method_cov=method_cov, d=0.94)

# Estimate optimal portfolio:

model='Classic' # Could be Classic (historical), BL (Black Litterman) or FM (Factor Model)
rm = 'MV' # Risk measure used, this time will be variance
obj = 'Sharpe' # Objective function, could be MinRisk, MaxRet, Utility or Sharpe
hist = True # Use historical scenarios for risk measures that depend on scenarios
rf = 0 # Risk free rate
l = 0 # Risk aversion factor, only useful when obj is 'Utility'

w = port.optimization(model=model, rm=rm, obj=obj, rf=rf, l=l, hist=hist)

display(w.T)

Здесь модель предлагает не инвестировать в HDFC Bank, так как это может быть рискованным вложением.

# Plotting the composition of the portfolio

ax = rp.plot_pie(w=w, title='Sharpe Mean Variance', others=0.05, nrow=25, cmap = "tab20",
height=6, width=10, ax=None)
points = 50 # Number of points of the frontier

frontier = port.efficient_frontier(model=model, rm=rm, points=points, rf=rf, hist=hist)

display(frontier.T.head())
# Plotting the efficient frontier

label = 'Max Risk Adjusted Return Portfolio' # Title of point
mu = port.mu # Expected returns
cov = port.cov # Covariance matrix
returns = port.returns # Returns of the assets

ax = rp.plot_frontier(w_frontier=frontier, mu=mu, cov=cov, returns=returns, rm=rm,
rf=rf, alpha=0.05, cmap='viridis', w=w, label=label,
marker='*', s=16, c='r', height=6, width=10, ax=None)
# Plotting efficient frontier composition

ax = rp.plot_frontier_area(w_frontier=frontier, cmap="tab20", height=6, width=10, ax=None)

Страхование портфеля с постоянной пропорцией (CPPI)

Каковы риски, связанные с вашим текущим портфелем? Что ж, два важных риска, о которых должны беспокоиться инвесторы, — это специфические и систематические риски.

Специфические риски (специфический или несистематический риск): Это тип инвестиционного риска, который является эндемичным (характерным) для отдельного актива (конкретной акции) или группы активов (конкретный сектор) или, в редких случаях, для определенного класса активов (например, обеспеченное ипотечное обязательство). Это относится к неотъемлемым факторам, которые могут негативно повлиять на ценные бумаги. Некоторые ценные бумаги, естественно, имеют более высокий идиосинкразический риск, чем другие. Специфический риск, как правило, может быть смягчен в инвестиционном портфеле путем диверсификации.

Систематический риск: Риск связан с самим рынком и не может быть смягчен диверсификацией. Наиболее популярным решением является хеджирование от таких ситуаций с использованием деривативов с использованием опционов. Хеджирование ограничивает потенциал роста портфеля, потому что он сложен и дорог в реализации.

Систематический и несистематический риск — Управление рисками — YouTube

Не волнуйтесь, есть решение, которое не включает деривативы. Это открывает нам мирстратегий портфельного страхования.

Страхование портфеля с постоянной пропорцией (CPPI) — это тип портфельного страхования, в котором инвесторы устанавливают нижний предел стоимости своего портфеля, а затем структурируют распределение активов вокруг этого решения. Двумя классами активов, используемыми в CPPI, являются рискованные активы (обычно акции) и консервативные активы наличных денег или облигаций. Процент, выделенный каждому из них, зависит от «стоимости подушки», которая представляет собой текущую стоимость портфеля за вычетом коэффициента мультипликатора, где более высокое число обозначает более агрессивную стратегию.

Например, учитывая множитель M= 4 и предполагая, что ваша текущая стоимость актива (AV) составляет 100%, и вы хотите сохранить его значение на уровне не менее 85% (уровень F = 85%). Вы можете инвестировать 4x($100-$85)=4x$15=$60 в свои рискованные активы, а остальные $40 в безопасные активы.

We start with our investment (CPPI) and define your floor (F) as a certain % of our investment. Each investor’s F is different depending on their risk appetite. We then calculate the cushion of our portfolio’s downside as

Cushion (C ) = CPPI — Floor (F)

The risky asset’s (E) exposure is then this cushion multiplied by M. M is calculated as the inverse of the maximum downside that is expected in a given period of time. Think of it as the inverse of the value at risk for your risky asset.

The final CPPI is the sum of the returns of the risky asset (E) and the stable asset (B). This is an iterative calculation done in given intervals (day, week, month, quarter or year) to readjust your portfolio.

Implementation in Python:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import yfinance as yf

#Here RELIANCE is our large cap stable asset and SUZLON is our small cap risky asset
stocks = ['RELIANCE.NS','SUZLON.NS']
prices = pd.DataFrame([])
#Date range
start = '2018-01-01'
end = '2022-11-27'
for i in stocks:
prices[i] = yf.download(i,start=start,end=end)['Adj Close']
prices
#Let's calculate value at risk for our portfolio for this time period (Q1) with confidence level of 99%
def portfolio_var(r,level=5):
return round(-np.percentile(r,level,axis=0)*100,2)


'''Rember that Var at a certain confidence gives the worst return from the remaining
samples after eliminating the lowest % values below the confidence internal
In our case for 99% confidence, the output is the worst return in 99% of the times.
'''
stock_returns = prices.pct_change().dropna()
floor_value = stock_returns.aggregate(portfolio_var)

safe_returns = stock_returns[['RELIANCE.NS']]
risky_returns = stock_returns[['SUZLON.NS']]

#Create a risky and safe allocation of same size as their respective returns using reindex_like() function
risky_alloc = pd.DataFrame().reindex_like(risky_returns)
safe_alloc = pd.DataFrame().reindex_like(safe_returns)
account_history = pd.DataFrame().reindex_like(safe_returns)

#Use Var to define floor of your portfolio
#Absolute worst case considering a drop across all your portfolio stocks in the given time period

print("Worst case drop across portfolio is:", round(floor_value.sum(),2),"% at 95% confidence")

Наихудшее падение портфеля: 7,82% при 95% уверенности

#We can either take this as the floor for our portfolio or define our floor as a certain value above this.
#Let's treat our floor as 20%, i.e. F = 20%

#CPPI basic values definition
investment = 100000
my_floor = investment*0.8
my_M = 5.62

#Traditional CPPI
dates = risky_returns.index
date_index = len(dates) #Calculating the length of date index so you can it

for step in range(date_index):
cushion = (investment - my_floor)/investment #Calculating cushion
risky_w = my_M*cushion #Calculating the weight of the risky asset
risky_w = np.minimum(risky_w,1) #Defining the weight such that it is less than 1 (100%)
risky_w = np.maximum(risky_w,0) #Defining the weight such that it is at least 0 (weight should not go negative)
safe_w = 1 - risky_w #Defining the weight of the stable asset
risky_alloc.iloc[step] = (investment*risky_w)*(1+risky_returns.iloc[step]) #Allocation to the risky asset
safe_alloc.iloc[step] = (investment*safe_w)*(1+safe_returns.iloc[step]) #Allocation to the safe asset
investment = risky_alloc.iloc[step].sum()+safe_alloc.iloc[step].sum() #CPPI which is your investment to the next iteration
account_history.iloc[step] = investment
#Wealth with CPPI
account_history.columns = ['With CPPI']
#Plot the risky and safe allocations
axis1 = risky_alloc.plot(figsize = (16,8))
safe_alloc.plot(ax=axis1)
#Wealth without CPPI
risky_wealth = (10000)*(1+risky_returns).cumprod()
risky_wealth.columns = ['No CPPI']
axis2 = risky_wealth.plot(figsize=(16,8))
account_history.plot(ax=axis2)

CPPIочень ценен, если вы хотите защитить свои инвестиции от падения ниже более низкого порога, чтобы вы могли сократить свои потери и уйти в любое время.

Источник