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

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

Ключевой аспект использования Монте-Карло в финансах — это его способность учитывать и анализировать волатильность и дрейф курсов валют. Для повышения точности моделирования и реалистичности получаемых данных часто применяется ГАРЧ модель (Generalized Autoregressive Conditional Heteroskedasticity). ГАРЧ помогает адекватно оценить и моделировать изменчивость волатильности, что является критичным при анализе финансовых временных рядов.

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

Проект использует следующие библиотеки и инструменты:

  • yfinance для загрузки финансовых данных.

  • numpy и pandas для обработки данных.

  • matplotlib для визуализации данных.

  • scipy и arch для статистического анализа и моделирования.

Импорт и подготовка данных

Для анализа и симуляции курса валют мы используем исторические данные по валютным парам USD/RUB и EUR/RUB, загруженные через библиотеку yfinance. Эти данные обеспечивают основу для нашего стохастического моделирования, начиная с 2013 года и заканчивая апрелем 2023 года. Сей временной промежуток был выбран, для того чтобы при сравнении сгенерированных значений с реальным учитывалось меньше дискретных значений из "линейного" поведения курса и моделировалось наиболее волатильное поведение курса.

Я брал отрезки [2004;2023], [2008;2020], [2010;2023]и т.д, и каждый раз у меня получался схожий временной ряд с временным рядом "спокойного" поведения доллара. Надеюсь, объяснил. Продолжим!

Анализируемые данные включают закрытие дня для каждой валюты, что позволяет вычислить дневные возвраты и волатильность.

Коротко о данных
# Загрузка исторических данных о валютных курсах
data = yf.download("USDRUB=X EURRUB=X", start="2013-01-01", end="2023-04-13")
Сами данные

Из этих данных мы выбираем курс на закрытии

usd_rates = data['Close']['USDRUB=X'].dropna()
eur_rates = data['Close']['EURRUB=X'].dropna()

Описание функций для симуляции

1. Функция simulate_exchange_rates

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

Математическое описание GBM:

S_t = S_0 \exp\left((\mu - \frac{\sigma^2}{2})t + \sigma W_t\right)

где:

  • ( St ) — цена актива в момент времени ( t ),

  • ( S0 ) — начальная цена актива,

  • ( μ ) — ожидаемый дрейф (среднее значение логарифмической доходности),

  • ( σ ) — стандартное отклонение логарифмической доходности (волатильность),

  • ( Wt ) — винеровский процесс (или стандартное броуновское движение).

В контексте симуляции валютных курсов:

  • Дневные возвраты рассчитываются как

( \exp\left((\mu - \frac{\sigma^2}{2}) \frac{1}{\text{days}} + \sigma \sqrt{\frac{1}{\text{days}}} Z\right) )

где ( Z ) — значения из стандартного нормального распределения.

  • Эти возвраты затем умножаются на последнее известное значение курса валюты для получения симулированных курсов на каждый из дней.

Код
def simulate_exchange_rates(historical_rates, days, simulations, drifts, volatilities):
    dt = 1 / days
    simulated_rates = {}
    for currency, rates in historical_rates.items():
        drift = drifts[currency]
        volatility = volatilities[currency]
        daily_returns = np.exp((drift - 0.5 * volatility**2) * dt + 
                               volatility * np.random.normal(0, np.sqrt(dt), (days, simulations)))
        simulation = rates.iloc[-1] * np.cumprod(daily_returns, axis=0)
        simulated_rates[currency] = simulation
    return simulated_rates

2. Функция portfolio_value

Функция portfolio_value рассчитывает стоимость портфеля в рублях, учитывая симулированные курсы валют и количество каждой валюты в портфеле. Стоимость портфеля определяется как:

V = \sum_{i=1}^n x_i \cdot S_i(T)

где:

  • ( xi ) — количество валюты ( i ) в портфеле,

  • ( Si(T) ) — симулированный курс валюты ( i ) на конец периода ( T ),

  • ( n ) — количество различных валют в портфеле.

Код
def portfolio_value(simulated_rates, portfolio):
    simulations = next(iter(simulated_rates.values())).shape[1]
    values = np.full(simulations, portfolio.get('RUB', 0), dtype=float)  
    for currency, amount in portfolio.items():
        if currency != 'RUB':
            values += amount * simulated_rates[currency][-1, :]
    return values

3. Функции calculate_var и calculate_cvar

  • Функция calculate_var оценивает Value at Risk (VaR) портфеля, который представляет собой потенциальную максимальную потерю в стоимости портфеля на заданном уровне доверия (например, 95%). VaR рассчитывается как:

\text{VaR}_\alpha = V_{(\alpha)}

где V (α) — квантиль распределения стоимости портфеля на уровне α.

Код
def calculate_var(portfolio_values, confidence_level=0.95):
    sorted_values = np.sort(portfolio_values)
    index = int((1 - confidence_level) * len(sorted_values))
    return sorted_values[index]

  • Функция calculate_cvar рассчитывает Conditional Value at Risk (CVaR), который представляет среднюю потерю, превышающую VaR:

\text{CVaR}_\alpha = \frac{1}{1-\alpha} \int_\alpha^1 V_u \, du

где ( Vu) — значение портфеля в точке квантиля (u).

Код
def calculate_cvar(portfolio_values, confidence_level=0.95):
    var = calculate_var(portfolio_values, confidence_level)
    cvar = portfolio_values[portfolio_values <= var].mean()
    return cvar

4. Функция calculate_confidence_interval

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

Математическое описание:

Для расчета доверительного интервала используется метод перцентилей. Он основан на следующих шагах:

  • Сначала симулированные данные сортируются по возрастанию.

  • Затем выбираются значения, соответствующие перцентилям, рассчитанным на основе заданного уровня доверия ( α ).

\text{Доверительный интервал} = \left( \text{Перцентиль}_{\left(\frac{1-\alpha}{2}\right) \times 100}, \text{Перцентиль}_{\left(1-\frac{1-\alpha}{2}\right) \times 100} \right)

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

Код
def calculate_confidence_interval(simulated_data, confidence_level=0.95):
    lower_percentile = (1 - confidence_level) / 2 * 100
    upper_percentile = (1 - (1 - confidence_level) / 2) * 100
    lower_bound = np.percentile(simulated_data, lower_percentile)
    upper_bound = np.percentile(simulated_data, upper_percentile)
    return lower_bound, upper_bound

5. Функция calculate_drift_volatility

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

Процесс расчета:

  • Из исторических данных сначала рассчитываются логарифмические доходности:

 ( r_t = \ln\left(\frac{S_t}{S_{t-1}}\right) )
  • Дрейф ( μ ) определяется как среднее значение этих доходностей.

  • Волатильность (σ) — это стандартное отклонение доходностей.

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

Код
def calculate_drift_volatility(rates, lookback_period=252):
    log_returns = np.log(rates / rates.shift(1)).dropna()
    rolling_mean = log_returns.rolling(window=lookback_period).mean()
    rolling_std = log_returns.rolling(window=lookback_period).std()
    drift = rolling_mean.iloc[-1]
    volatility = rolling_std.iloc[-1]
    return drift, volatility

6. Функция fit_garch_model

Функция fit_garch_model применяется для моделирования и оценки волатильности с использованием ГАРЧ модели. ГАРЧ модель особенно полезна для финансовых временных рядов, где волатильность не является постоянной, а изменяется со временем.

Процесс подгонки модели:

  • Входные данные для модели — это логарифмические доходности.

  • Модель ГАРЧ параметризуется значениями ( p ) и ( q ), которые представляют собой порядки модели для условной дисперсии и скользящего среднего соответственно.

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

ГАРЧ модель позволяет более точно оценить риск и управлять им, поскольку принимает во внимание изменчивость волатильности, что является стандартным явлением для финансовых рынков.

Инициализация и выполнение модели Монте-Карло для валютных курсов

После подготовки всех необходимых функций и данных, мы переходим к основному этапу анализа — инициализации и выполнению модели Монте-Карло. Этот процесс включает несколько ключевых шагов:

Расчет дрейфа и волатильности

  1. Извлечение логарифмических доходностей: Используя исторические данные курсов USD и EUR, рассчитываются ежедневные логарифмические доходности. Это первый шаг в процессе определения дрейфа и волатильности, которые понадобятся для симуляций.

    r_t = \ln\left(\frac{S_t}{S_{t-1}}\right)
  2. Расчет скользящих средних и стандартных отклонений: На основе логарифмических доходностей с помощью скользящего окна в 252 дня (примерно количество торговых дней в году) рассчитываются скользящие средние и стандартные отклонения. Эти значения представляют собой дрейф и волатильность каждой валюты.

Секретик

Вообще, функция calculate_confidence_interval была написана мною до того, как я узнал про GARCH, так что я из досады решил оставить инициализацию дрейфа и волатильности двумя способами

Подгонка ГАРЧ модели

  1. Подгонка ГАРЧ модели: Для каждой валюты подгоняется ГАРЧ модель к логарифмическим доходностям. Модель ГАРЧ помогает оценить долгосрочную волатильность и условное среднее, что важно для точности симуляций.

Симуляция валютных курсов

  1. Инициализация симуляций: С использованием полученных параметров дрейфа и волатильности запускаются симуляции курсов валют для заданного количества дней (252 дня в примере) и количества симуляций (1,000,000 в примере). Каждая симуляция представляет возможное будущее состояние курса валюты.

    • Симуляции проводятся по модели геометрического броуновского движения, описанного выше.

Ввод данных пользователем

  1. Получение данных портфеля от пользователя: Запрашивается у пользователя ввод данных о количестве рублей, долларов и евро в его портфеле. Эти данные необходимы для дальнейшего расчета стоимости портфеля.

Весь код
historical_rates = {'USD': usd_rates, 'EUR': eur_rates}
days = 252
simulations = 1000000
# Расчет дрейфа и волатильности для Моделирования 
# через calculate_drift_volatility
drifts = {}
volatilities = {}
for currency, rates in historical_rates.items():
    drifts[currency], volatilities[currency] = calculate_drift_volatility(rates)

# Расчет дрейфа и волатильности для Моделирования via garch
usd_log_returns = np.log(usd_rates / usd_rates.shift(1)).dropna()
eur_log_returns = np.log(eur_rates / eur_rates.shift(1)).dropna()

usd_drift_garch, usd_volatility_garch = fit_garch_model(usd_log_returns)
eur_drift_garch, eur_volatility_garch = fit_garch_model(eur_log_returns)

# Обновление словарей дрейфов и волатильности для использования в симуляции Монте-Карло
drifts['USD'] = usd_drift_garch
drifts['EUR'] = eur_drift_garch
volatilities['USD'] = usd_volatility_garch
volatilities['EUR'] = eur_volatility_garch

# Моделирование курсов валют
simulated_rates = simulate_exchange_rates(historical_rates, days, simulations, drifts, volatilities)

# Ввод данных пользователем
rub_amount = float(input("Введите количество рублей: "))
usd_amount = float(input("Введите количество долларов: "))
eur_amount = float(input("Введите количество евро: "))

# Получение данных пользователя
portfolio = {'RUB': rub_amount, 'USD': usd_amount, 'EUR': eur_amount}
# Расчет стоимости портфеля
portfolio_values = portfolio_value(simulated_rates, portfolio)

После этого через input запрашивается предполагаемый портфель в RUB, EUR, USD

портфель из 1 доллара выбрал для наглядности проверки var и cvar дальше

Расчет стоимости портфеля и рисков

  1. Расчет стоимости портфеля и рисков: Используя симулированные курсы валют и данные о количестве валют в портфеле пользователя, рассчитывается стоимость портфеля и ключевые показатели риска (VaR и CVaR).

Код
cvar = calculate_cvar(portfolio_values)
print(f"Максимальный риск портфеля (95% довер): {cvar}")

Ответ:

Анализ и визуализация результатов

  1. Статистический анализ и визуализация: Проводится сравнение исторических и симулированных данных с помощью статистических тестов, таких как Колмогорова-Смирнова тест и t-тест для независимых выборок. Результаты визуализируются, чтобы наглядно показать реальные и симулированные данные по курсам валют.

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

Код для тестов

Загрузка данных за последний год

data_recent = yf.download("USDRUB=X EURRUB=X", start="2023-04-12", end="2024-04-13")['Close'].dropna()
data_recent_usd = data_recent['USDRUB=X'].dropna()
data_recent_eur = data_recent['EURRUB=X'].dropna()
real_data_last_year = data_recent_usd[-days:]
simulated_data_first_path = simulated_rates['USD'][:, 0]

ks_stat, p_value_ks = ks_2samp(real_data_last_year, simulated_data_first_path)
t_stat, p_value_t = ttest_ind(real_data_last_year, simulated_data_first_path, equal_var=False)

print(f"KS Statistic: {ks_stat}, P-value (KS-test): {p_value_ks}")
print(f"T-statistic: {t_stat}, P-value (T-test): {p_value_t}")

Ответ:

Оба теста вместе показывают, что существует статистически значимое расхождение между реальными и симулированными данными, НО...

Код для доверительных интервалов
usd_confidence_interval = calculate_confidence_interval(simulated_rates['USD'][-1, :])
eur_confidence_interval = calculate_confidence_interval(simulated_rates['EUR'][-1, :])

print(f"Доверительный интервал 95% для курса доллара: {usd_confidence_interval}")
print(f"Доверительный интервал 95% для курса евро: {eur_confidence_interval}")

Ответ:

Визуализация
# Визуализация исторических и симулированных данных
plt.figure(figsize=(14, 7))
plt.plot(usd_rates.index, usd_rates, 
         label='Реальные данные(2013-01-01 - 2023-04-12)')
plt.plot(data_recent_usd.index, data_recent_usd, 
         label='Реальные данные (2023-04-13 - 2024-04-13)', color='green', alpha=0.7)
plt.plot(pd.date_range(start=usd_rates.index[-1], periods=days, freq='B'),
         simulated_rates['USD'][:, 0], label='Смоделированные данные', color='orange', alpha=0.7)
plt.legend()
plt.title('Реальные vs Смоделированные USD/RUB курсы')
plt.xlabel('Дата')
plt.ylabel('Курс')
plt.savefig('RUBUSD.png')
plt.show()
# Визуализация исторических и симулированных данных
plt.figure(figsize=(14, 7))
plt.plot(eur_rates.index, eur_rates, 
         label='Реальные данные(2013-01-01 - 2023-04-12)')
plt.plot(data_recent_eur.index, data_recent_eur, 
         label='Реальные данные (2023-04-13 - 2023-04-13)', color='green', alpha=0.7)
plt.plot(pd.date_range(start=eur_rates.index[-1], periods=days, freq='B'),
         simulated_rates['EUR'][:, 0], label='Смоделированные данные', color='orange', alpha=0.7)
plt.legend()
plt.title('Реальные vs Смоделированные EUR/RUB курсы')
plt.xlabel('Дата')
plt.ylabel('Курс')
plt.savefig('RUBEUR.png')
plt.show()
plt.figure(figsize=(10, 6))
for i in range(15):
    plt.plot(simulated_rates['USD'][:, i], linewidth=1, alpha=0.8)
plt.title('Моделирование траекторий обменного курса доллара к рублю')
plt.xlabel('Дни')
plt.ylabel('Курс')
plt.show()
plt.figure(figsize=(10, 6))
for i in range(15):
    plt.plot(simulated_rates['EUR'][:, i], linewidth=1, alpha=0.8)
plt.title('Моделирование траекторий обменного курса евро к рублю')
plt.xlabel('Дни')
plt.ylabel('Курс')
plt.show()

Бонус

Смоделируем поведение доллара на год вперед (04.2024 - 04.2025]

Прогноз на год вперед
historical_rates_pred = {'USD': data_recent_usd, 'EUR': data_recent_eur}
days_pred= 252
simulations_pred = 1000000
# Расчет дрейфа и волатильности для Моделирования
drifts_pred = {}
volatilities_pred = {}
for currency_pred, rates_pred in historical_rates_pred.items():
    drifts_pred[currency_pred], volatilities_pred[currency_pred] = calculate_drift_volatility(rates_pred)
usd_log_returns_pred = np.log(usd_rates / usd_rates.shift(1)).dropna()
eur_log_returns_pred = np.log(eur_rates / eur_rates.shift(1)).dropna()

usd_drift_garch_pred, usd_volatility_garch_pred = fit_garch_model(usd_log_returns_pred)
eur_drift_garch_pred, eur_volatility_garch_pred = fit_garch_model(eur_log_returns_pred)

# Обновление словарей дрейфов и волатильности для использования в симуляции Монте-Карло
drifts_pred['USD'] = usd_drift_garch_pred
drifts_pred['EUR'] = eur_drift_garch_pred
volatilities_pred['USD'] = usd_volatility_garch_pred
volatilities_pred['EUR'] = eur_volatility_garch_pred
# Моделирование курсов валют
simulated_rates_pred = simulate_exchange_rates(historical_rates_pred, days_pred, simulations_pred, drifts_pred, volatilities_pred)

# Ввод данных пользователем
rub_amount_pred = float(input("Введите количество рублей: "))
usd_amount_pred = float(input("Введите количество долларов: "))
eur_amount_pred = float(input("Введите количество евро: "))

# Получение данных пользователя
portfolio_pred = {'RUB': rub_amount_pred, 'USD': usd_amount_pred, 'EUR': eur_amount_pred}
# Расчет стоимости портфеля
portfolio_values_pred = portfolio_value(simulated_rates_pred, portfolio_pred)

Тут я создал портфель с 1000 USD

cvar_pred = calculate_cvar(portfolio_values_pred)
print(f"Максимальный риск портфеля (95% довер): {cvar_pred}")

Максимальный риск портфеля (95% довер): 71827.22023513853

usd_confidence_interval_pred = calculate_confidence_interval(simulated_rates_pred['USD'][-1, :])
eur_confidence_interval_pred = calculate_confidence_interval(simulated_rates_pred['EUR'][-1, :])
print(f"Доверительный интервал 95% для курса доллара: {usd_confidence_interval_pred }")
print(f"Доверительный интервал 95% для курса евро: {eur_confidence_interval_pred }")

Доверительный интервал 95% для курса доллара: (72.63404780533205, 116.80234915827717)

Доверительный интервал 95% для курса евро: (94.25965585147713, 104.5288904591821)

plt.figure(figsize=(14, 7))
plt.plot(pd.date_range(start=data_recent_usd.index[-1], periods=days, freq='B'),
         simulated_rates_pred['USD'][:, 0], label='Смоделированные данные', color='blue', alpha=0.7)
plt.legend()
plt.title('Смоделированные USD/RUB курс на год')
plt.xlabel('Дата')
plt.ylabel('Курс')
plt.grid()
plt.savefig('RUBUSD_pred.png')
plt.show()

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

Заключение

В данной статье мы провели обширный анализ временных рядов курсов валют с использованием метода Монте-Карло, обогащенного моделями ГАРЧ для уточнения параметров дрейфа и волатильности. Доверять полученным значениям курсов не стоит - определенно, об этом говорит разность графиков, значения к-с и t тестов, но всё же для понимания принципов вышло очень хорошо.

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

Комментарии (3)


  1. Vilos
    24.04.2024 13:31

    Бакс нужно закупать?


    1. Travisw
      24.04.2024 13:31

      судя по графику надо купить в мае, а продать в декабре


  1. S_gray
    24.04.2024 13:31

    Один мой старший знакомый в 90-е годы в попытке "коммерциализации" своей научной работы написал программу СПАД ("статистических процессов анализатор дискретный(?)" - не уверен в последнем слове) и, в частности, попробовал с её помощью прогнозировать колебания курсов валют. В то время это было очень популярной темой и его даже с этим СПАДом взяли на работу в серьезную фирму на приличную (по тем временам, для наёмного работника) зарплату. В общем, он пришёл к выводу, что курсы валют не являются случайными величинами, а посему статистический инструментарий в части прогнозирования неэффективен...