Оценки стоимости акций с помощью Python

Что такое ценность под риском (VaR)?

Стоимость под риском (VaR) является наиболее распространенным показателем, используемым физическим лицом, фирмами и банками для определения степени потенциальных финансовых потерь за определенный период времени. Финансовые учреждения используют VaR для оценки риска, связанного с инвестициями, чтобы оценить, достаточно ли у них средств для покрытия потенциальных убытков, это также помогает риск-менеджерам переоценить и скорректировать свои инвестиции, чтобы снизить риск более высоких потерь. Типичный вопрос, на который VaR помогает ответить, заключается в том, каков максимальный убыток от инвестиций в размере 100 000 в течение одного года с уверенностью 95%? или Какова вероятность того, что потери составят более 3% в следующем году? Стандартный способ представления этих сценариев — в виде нормального распределения, что значительно упрощает анализ и интерпретацию.

Источник: Investopedia

Есть три важных фактора: доверительный интервал, период времени и сумма инвестиций или ожидаемый процент убытка.

В этом блоге мы рассмотрим следующие темы:

  • Исследовательский анализ данных (EDA)
  • Расчет возвратов
  • Методики расчета VaR
    — Исторический метод
    — Метод Bootstrap
    — Метод фактора распада
    — Метод моделирования методом Монте-Карло

Исследовательский анализ данных (EDA)

Загрузка библиотек

Это стандартные библиотеки для загрузки, анализа и визуализации данных. Ключевым моментом является использование nsepy для получения биржевых данных. Другой альтернативой является использование yahoo finance.

import numpy as np
import pandas as pd
import warnings
import matplotlib.pyplot as plt
from datetime import date
from tabulate import tabulate
from nsepy import get_history as gh

Загрузите данные и начальные настройки

Мы будем использовать данные TATA MOTORS за последний год. Это может быть любое имя, представляющее интерес, например, ICICI или DABUR и т. Д. Это дает дневную стоимость цены акций.

initial_investment = 100000
startdate = date(2022,2,2)
end_date = date(2023,2,2)
stocksymbols = ['TATAMOTORS'] # This can be any stock

def load_stock_data(self):
df = pd.DataFrame()
for i in range(len(self.ticker)):
....
...
return df

OUTPUT:
TATAMOTORS
Date
2022-10-14 396.25
2022-10-17 396.10
2022-10-18 404.25
2022-10-19 399.05
2022-10-20 398.10
....
....
2023-01-27 445.60
2023-01-30 443.65
2023-01-31 452.10
2023-02-01 446.65
2023-02-02 444.80
Источник: Автор

Рассчитайте доходность

Расчет доходности — простой процесс, мы берем процентное изменение между текущим значением и предыдущим значением.

P(t+1) — P(t) / P(t)

Этого можно легко добиться в python с помощью pct_change метода, и давайте визуализируем отдачу.

def stock_returns(self):
df = self.load_stock_data()
df.columns = ['Stock']
returns = df.pct_change()
returns.dropna(inplace=True)
return returns

OUTPUT:
Stock
Date
2022-10-17 -0.000379
2022-10-18 0.020576
2022-10-19 -0.012863
2022-10-20 -0.002381
2022-10-21 -0.000126
Источник: Автор

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

Исторический метод

Это самый простой из всех методов и самый основной, поскольку он не придает значения распределению и хвостам. В этом методе мы берем возвращаемые значения из предыдущего раздела и сортируем значения. В качестве стандарта мы принимаем уровень достоверности 95%, т.е. наш фокус смещается на нижние 5%. Кроме того, количество торговых дней в году составляет 252, поэтому все, что нам нужно сделать, это рассчитать 5% от 252 дней, что составляет 12,6, что означает, что нам придется взять 13-ю самую низкую доходность, которая оказывается -0,039306, как показано ниже.

returns.sort_values('Stock').head(13)

OUTPUT:
2022-02-24 -0.102830
2022-09-26 -0.060506
2022-03-07 -0.055722
2022-02-14 -0.054926
2022-06-16 -0.051075
2022-06-13 -0.049877
2022-11-10 -0.048367
2022-03-04 -0.045413
2022-05-06 -0.041637
2022-05-12 -0.040835
2022-12-23 -0.040816
2022-05-19 -0.039745
2022-10-10 -0.039306

У Python есть более элегантный способ получить это значение с помощью процентильной функции NumPy.

np.percentile(returns['Stock'], 5, interpolation = 'lower')

OUTPUT:
-0.039306077884265433

Вот полный метод, который загружает данные и вычисляет VaR. Конечным результатом является хороший табличный формат.

def var_historical(self):        
returns = obj_loadData.df_returns.copy()
....
....
returns.sort_values('Stock').head(13)
var_hist = np.percentile(returns['Stock'], 5, interpolation = 'lower')

print(tabulate([[self.ticker,avg_rets,avg_std,var_hist]],
headers = ['Mean', 'Standard Deviation', 'VaR %'],
tablefmt = 'fancy_grid',stralign='center',numalign='center',floatfmt=".4f"))
return var_hist

def plot_shade(self, var_returns):
....
plt.text(var_returns, 25, f'VAR {round(var_returns, 4)} @ 5%',
horizontalalignment='right',
size='small',
color='navy')
....
plt.gca().add_patch(rect)

OUTPUT:
╒════════════════╤════════╤══════════════════════╤══════════╕
│ │ Mean │ Standard Deviation │ VaR │
╞════════════════╪════════╪══════════════════════╪══════════╡
│ ['TATAMOTORS'] │ 0.0003 │ 0.0225 │ -0.039306│
╘════════════════╧════════╧══════════════════════╧══════════╛
Источник: Автор

Значение риска -0,039306 указывает на то, что при уровне достоверности 95% максимальный убыток составит 3,9%, либо вероятность того, что потери превысят 3,9%, составляет 5%. В денежном выражении при вложении в 100 000 мы на 95% уверены, что максимальный убыток составит 3 930.

Метод B ootstrap

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

def var_bootstrap(self,iterations: int):

def var_boot(data):
...
...
return np.percentile(dff, 5, interpolation = 'lower')

def bootstrap(data, func):
sample = np.random.choice(data, len(data))
return func(sample)

def generate_sample_data(data, func, size):
bs_replicates = np.empty(size)
....
....
returns = obj_loadData.df_returns.copy()
....
return np.mean(bootstrap_VaR)

Мы передискретизировали данные в течение 500 итераций и вычисляли VaR при каждом запуске, и, поскольку процесс является случайным, ожидается, что значения должны находиться в той же близости, что и у исторического метода, т.е. -0,039306. Давайте построим график этого распределения, чтобы подтвердить наше понимание.

Источник: Автор

Серый прямоугольник представляет собой возвращаемые значения и находится в диапазоне от -0,033 до -0,042, что хорошо. Теперь давайте возьмем среднее значение этих значений, чтобы получить VaR, а также визуализируем значимый уровень, выделив область.

var_bootstrap = np.mean(bootstrap_VaR)
print(f'The Bootstrap VaR measure is {np.mean(bootstrap_VaR)}')
return np.mean(bootstrap_VaR)

OUTPUT:
╒════════════════╤══════════════╤
│ Stock │ Bootstrap │
╞════════════════╪═══════════════
│ ['TATAMOTORS'] │ -0.0369 │
╘════════════════╧══════════════╧
Источник: Автор

Значение риска -0,0369 указывает на то, что при уровне достоверности 95% максимальный убыток составит 3,69%, или существует 5%-ная вероятность того, что потери превысят 3,69%. Это на 0,21% ниже, чем исторический метод, и, возможно, связано со случайностью, введенной в рамках повторной выборки.

Метод фактора D ecay

В обоих предыдущих методах не учитывались максимумы, минимумы или колебания рынка, что означало, что по своей сути существовало большое предположение, что будущий тренд также будет похож на прошлый год. Метод распада решает эту проблему, придавая более высокий вес последним данным. Мы будем использовать коэффициент распада, который будет находиться в диапазоне от o до 1, т.е. присвоим наименьший вес самой дальней точке данных и более высокий вес самой последней точке данных.

decay_factor = 0.5 #we’re picking this arbitrarily
n = len(returns)
wts = [(decay_factor**(i-1) * (1-decay_factor))/(1-decay_factor**n)
for i in range(1, n+1)]

OUTPUT:
0.5,
0.25,
0.125,
0.0625,
0.03125,
....
....
2.210859150104178e-75,
1.105429575052089e-75,
5.527147875260445e-76]
Источник: Автор

Мы создадим фрейм данных, вес которого назначен каждой точке данных.

wts_returns = pd.DataFrame(returns_recent_first['Stock'])
wts_returns['wts'] = wts

OUTPUT:

Stock wts
Date
2023-02-03 0.001461 0.50000
2023-02-02 -0.004142 0.25000
2023-02-01 -0.012055 0.12500
2023-01-31 0.019047 0.06250
2023-01-30 -0.004376 0.03125
....
....
2022-02-07 -0.011986 2.210859e-75
2022-02-04 -0.007730 1.105430e-75
2022-02-03 -0.003752 5.527148e-76

В предыдущих методах мы отсортировали возвращаемые значения в порядке возрастания и взяли 13-е наименьшее возвращаемое значение как VaR. Мы смогли сделать это, потому что каждая из точек данных имела одинаковый вес 1, но в методе коэффициента распада мы назначили разные веса для каждой точки, поэтому мы не можем напрямую взять наименьшее 13-е возвращаемое значение, вместо этого мы будем суммировать веса, пока не достигнем отметки 0,05, которая является значимым уровнем 5%, и чтобы упростить задачу, мы будем использовать кумулятивную сумму.

sort_wts = wts_returns.sort_values(by='Stock')
sort_wts['Cumulative'] = sort_wts.wts.cumsum()
sort_wts

sort_wts = sort_wts.reset_index()
idx = sort_wts[sort_wts.Cumulative <= 0.05].Stock.idxmax()
sort_wts.filter(items = [idx], axis = 0)

OUTPUT:
Date Stock wts Cumulative
63 2022-06-02 -0.012258 6.681912e-52 7.488894e-04
64 2023-02-01 -0.012055 1.250000e-01 1.257489e-01

Мы находим, что кумулятивное значение 0,05 находится между строками 63 и 64. Нам нужно будет интерполировать, чтобы получить значение, которое оказывается равным -0,0121

xp = sort_wts.loc[idx:idx+1, 'Cumulative'].values
fp = sort_wts.loc[idx:idx+1, 'Stock'].values
var_decay = np.interp(0.05, xp, fp)

OUTPUT:
-0.01217808614447785

Ниже приведен полный метод, который загружает данные, генерирует и присваивает веса, интерполирует и вычисляет VaR.

def var_weighted_decay_factor(self):        
returns = obj_loadData.df_returns.copy()
decay_factor = 0.5 #we’re picking this arbitrarily
n = len(returns)
wts = [(decay_factor**(i-1) * (1-decay_factor))/(1-decay_factor**n) for i in range(1, n+1)]
....
....

return var_decay

OUTPUT:
╒════════════════╤══════════════╤
│ Stock │ Decay │
╞════════════════╪═══════════════
│ ['TATAMOTORS'] │ -0.0122 │
╘════════════════╧══════════════╧
Источник: Автор

Метод распада показывает, что при уровне достоверности 95% максимальный убыток составит 1,22%, или существует 5%-ная вероятность того, что потери превысят 1,22%. Это значительно ниже, чем у двух других методов, и это связано с присвоением весов. Скорость распада установлена равной 0,5, мы можем увеличивать или уменьшать скорость распада, чтобы проверить наиболее разумный VaR. Один из подходов состоит в том, чтобы взять диапазон скоростей распада и запустить моделирование, чтобы получить диапазон VaR.

Метод моделирования методом Монте-Карло

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

Мы рассчитаем среднее значение и стандартное распределение.

returns_mean = returns['Stock'].mean()
returns_sd = returns['Stock'].std()

Мы напишем метод для генерации набора значений из распределения со средним значением returns_mean и стандартным распределением returns_sd

iterations = 1000
def simulate_values(mu, sigma, iterations):
try:
result = []
for i in range(iterations):
tmp_val = np.random.normal(mu, sigma, (len(returns)))
var_hist = np.percentile(tmp_val, 5, interpolation = 'lower')
result.append(var_hist)
return result
except Exception as e:
print(f'An exception occurred while generating simulation values: {e}')

Давайте теперь выполним метод и VaR для каждой из 1000 итераций.

sim_val = simulate_values(returns_mean,returns_sd, iterations)

tmp_df = pd.DataFrame(columns=['Iteration', 'VaR'])
tmp_df['Iteration'] = [i for i in range(1,iterations+1)]
tmp_df['VaR'] = sim_val
tmp_df.head(50)

print(f'The mean VaR is {statistics.mean(sim_val)}')

OUTPUT:
Iteration VaR
1 -0.034532
2 -0.035278
3 -0.034831
4 -0.033859
....
....
997 -0.035699
998 -0.038877
999 -0.038362
1000 -0.035165

╒════════════════╤══════════════╤
│ Stock │ Monte Carlo │
╞════════════════╪══════════════╪
│ ['TATAMOTORS'] │ -0.03716 │
╘════════════════╧══════════════╧
Источник: Автор

VaR меньше, чем у исторического метода (-0,0393), и больше, чем у метода распада (-0,0122). Этот результат более или менее совпадает с методом bootstrap (-0,0366).

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

 def show_summary(self):
try:
var_hist = self.var_historical()
var_bs = self.var_bootstrap()
var_decay = self.var_weighted_decay_factor()
var_MC = self.var_monte_carlo()
print(tabulate([[self.ticker,var_hist,var_bs,var_decay, var_MC]],
headers = ['Historical', 'Bootstrap',
'Decay', 'Monte Carlo'],
tablefmt = 'fancy_grid',stralign='center',
numalign='center',floatfmt=".4f"))
except Exception as e:
print(f'An exception occurred while executing show_summary: {e}')


OUTPUT:
╒════════════════╤══════════════╤═════════════╤═════════╤═══════════════╕
│ Stock │ Historical │ Bootstrap │ Decay │ Monte Carlo │
╞════════════════╪══════════════╪═════════════╪═════════╪═══════════════╡
│ ['TATAMOTORS'] │ -0.0393 │ -0.0366 │ -0.0122 │ -0.0373 │
╘════════════════╧══════════════╧═════════════╧═════════╧═══════════════╛

Полный код можно найти на GitHub

Преимущества VaR

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

Недостатки VaR

  • Точность оценки зависит от качества данных и допущений. Например: предполагается, что данные нормально распределены, экономические факторы не учитываются и т. д.
  • VAR дает максимальный потенциальный убыток за данный период с определенным доверительным интервалом, но не говорит нам, насколько большими или маленькими будут потенциальные потери за пределами точки.
  • Использовать VaR для больших портфелей сложно, так как расчет риска должен быть сделан для каждого из активов, а также необходимо учитывать угол корреляции.

Заключение

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

Источник