jason-ash / pyesg

Economic scenario generator for python: simulate stocks, interest rates, and other stochastic processes.
MIT License
128 stars 33 forks source link
actuarial financial-modeling open-source python stochastic-processes

Simulate stocks, interest rates, and other stochastic processes.
pyesg is a lightning fast economic scenario generator for Python.

License Supported python versions Pypi package version Build status Coverage


What is an economic scenario generator?

An economic scenario generator simulates the behavior of unpredictable financial markets like stocks, interest rates, or energy prices. It uses mathematical models called stochastic processes to generate thousands of unique scenarios.

What can you do with an economic scenario generator? Here are a few examples:

Simulate an investment portfolio * Generate 1,000 correlated, daily stock prices * Construct a portfolio from the stocks * Plot the value of the portfolio over time, and calculate metrics like high, low, max drawdown, etc.
Choose a mortgage * Generate 1,000 30-year, monthly scenarios of mortgage rates * Calculate the monthly payment for different loans under each scenario: - 30-year fixed (lock in the rate today for all 30 years) - 15-year fixed (lock in the rate today and pay over 15 years) - 10-year adjustable-rate (fixed rate for 10 years, then variable rate for 20 years - etc. * Determine which loan fits the best with your budget
Plan for retirement * Generate 1,000 30-year, monthly scenarios of stock market data * Generate 1,000 30-year, monthly scenarios of bond fund data * Track your portfolio balance as a mix of investments in stocks and bonds * Each month, simulate a withdrawal from your portfolio and the growth from your investments * Adjust the withdrawal up or down and identify the types of markets that lead to the best and worst outcomes
Protect your business * **Banks**: test whether loan portfolios are resilient to drops in interest rates and higher-than-expected customer refinancing * **Insurers**: test whether assets are sufficient to cover liabilities under a wide range of bond market scenarios * **Investment managers**: test the impact of market declines on revenue from account-based fees * **Small Businesses**: model customer demand correlated with the stock market to plan for potential downturns

Key Features of pyesg

Coming soon

Installing pyesg

pyesg is available on PyPI, and can be installed with pip.

pip install pyesg

Usage

All models in pyesg are created and used nearly identically.

First, create a model with its required parameters.

import pyesg

# create a new model with the required parameters
model = pyesg.GeometricBrownianMotion(mu=0.05, sigma=0.2)

Generate scenarios by calling the <model>.scenarios method. The example below, with 10,000 daily scenarios (2,520,000 values) took just 160 milliseconds to run!

Under the hood: model.scenarios() This method generates scenarios, given the following arguments: - `x0` : the value or array of values each scenario starts with. For example, you might pass `100.0` to model a security with a current value of 100, or `[100.0, 90.0]` to model two scenarios with start values of 100 and 90. - `dt` : the length of each time step in the projection, in years. For example, you might pass `1/12` to model mothly timesteps, `1/52` to model weekly timesteps, `1/252` to model daily (trading day) timesteps, or `1` to model annual timesteps. - `n_scenarios` : the number of scenarios you want to generate. Often you can generate 10,000 scenarios in fractions of a second. - `n_steps` : the number of timesteps for each scenario. In combination with `dt`, this argument determines the total length of time a projection covers. For example, with `dt=1/52` and `n_steps=104`, the projection will cover two years with weekly timesteps. With `dt=1/12` and `n_steps=360`, the projection will cover 30 years with monthly timesteps. - `random_state` : an optional field that may be provided if you want to be able to reproduce pseudo-random numbers for each batch of scenarios. For example, `.scenarios(..., random_state=123)` will generate the exact same batch of scenarios every time.
import pyesg

# instantiate a new model with the required parameters
model = pyesg.GeometricBrownianMotion(mu=0.05, sigma=0.2)

# prepare the arguments to generate scenarios. Here we'll
# generate 10,000 scenarios with 252 daily (trading day)
# time steps, for a one-year projection in total.
x0 = 100.0           # the start value of our process
dt = 1/252           # the length of each timestep in years
n_scenarios = 10000  # the number of scenarios to generate
n_steps = 252        # the number of time steps per scenario
random_state = 123   # optional random_state for reproducibility

# now we generate the scenarios; this outputs a numpy array. It will
# have shape (10000, 253), which represents (scenarios, timesteps).
# There are 253 timesteps because the initial value is included to start
results = model.scenarios(x0, dt, n_scenarios, n_steps, random_state)

# array([[100.        ,  98.65207527,  97.12924873, ..., 111.3500094 ,
#         112.00479028, 113.12444153],
#        [100.        , 101.27637842, 100.8971646 , ...,  61.8709475 ,
#          63.00222064,  62.22126261],
#        [100.        , 100.37636067,  99.32267874, ..., 141.66969149,
#         140.38291993, 138.91659076],
#        ...,
#        [100.        ,  99.42484152,  97.68732205, ..., 139.9306172 ,
#         139.52301459, 139.05345463],
#        [100.        , 100.75304745, 102.09894601, ..., 115.66615197,
#         116.16385992, 118.06267759],
#        [100.        , 101.24269853, 101.73381851, ...,  84.65843473,
#          84.73018762,  85.09768131]])

All models provide methods to evaluate their drift, diffusion, expectation, standard deviation, and more.

import pyesg

# instantiate a new model with the required parameters
model = pyesg.GeometricBrownianMotion(mu=0.05, sigma=0.2)

# drift is the instantaneous drift of the process. For example,
# if we start with x0=100, what is the instantaneous drift?
model.drift(x0=100)
# array([5.])

# similarly we can measure the instantaneous diffusion
model.diffusion(x0=100)
# array([20.])

# the expectation is the expected value of the process after
# a given amount of time, dt. Here we calculate the expected
# value of the process after one year.
model.expectation(x0=100, dt=1.0)
# array([105.])

# the standard deviation is measured at a period of time too
model.standard_deviation(x0=100, dt=1.0)
# array([20.])

Available Models

Equity

Geometric Brownian Motion
Heston Model

Interest Rate

American Academy of Actuaries Interest Rate Generator
Black-Derman-Toy
Black-Karasinski
Cox-Ingersoll-Ross
Ho-Lee
Ornstein-Uhlenbeck Process (Vasicek Model)

General

Wiener Process

Curve Interpolators

We can almost always observe interest rates at key maturities, for example, bonds trading with maturies of 1, 2, 3, 5, 7, or 10 years. If we want to estimate the interest rate for an 8-year bond, we need to interpolate between the observed values. Simple techniques like linear interpolation are possible, but have certain obvious disadvantages - namely that the interest rate curve is non-linear. Instead, better techniques like the Nelson-Siegel and Nelson-Siegel-Svensson interpolators might give better results. Both interpolators are availabe in pyesg.

import numpy as np
from pyesg import NelsonSiegelInterpolator, SvenssonInterpolator
from pyesg.datasets import load_ust_historical

# load a dataset of historical US Treasury rates, contained in pyesg.datasets
# ust is a pandas dataframe of rates for various maturities, indexed by year and month
ust = load_ust_historical()

# we will be interpolating rates from the file:
# y - the observed US Treasury rate for the given maturity for a select observation date
# X - the maturity of the bond measured in years
y = ust.iloc[-10].values
X = np.array([0.25, 0.5, 1, 2, 3, 5, 7, 10, 20, 30])

# create Nelson-Siegel and Nelson-Siegel-Svensson interpolator objects
# then fit both models using the historical data
nelson_siegel = pyesg.NelsonSiegelInterpolator()
svensson = pyesg.SvenssonInterpolator()
nelson_siegel.fit(X, y)
svensson.fit(X, y)

# predict values for each maturity from 1 to 30 years
nelson_siegel.predict(np.arange(1, 31, 1))
# array([0.02033871, 0.02252733, 0.02403659, 0.02510373, 0.02587762,
#        0.02645304, 0.02689131, 0.02723275, 0.02750438, 0.02772458,
#        0.02790617, 0.02805818, 0.02818715, 0.02829786, 0.0283939 ,
#        0.02847798, 0.02855218, 0.02861815, 0.02867718, 0.02873031,
#        0.02877839, 0.02882209, 0.02886199, 0.02889857, 0.02893222,
#        0.02896329, 0.02899205, 0.02901876, 0.02904362, 0.02906683])

License

Open Source and licensed under MIT, Copyright © 2019-2021 Jason Ash