finmat 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: finmat
3
+ Version: 0.1.0
4
+ Summary: A financial math library for options pricing, stochastic processes, and fixed income analytics
5
+ Author-email: Daniel Butler <danielbutler245@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/dbu79/fin-mat-library
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: numpy
11
+ Requires-Dist: scipy
12
+ Requires-Dist: matplotlib
13
+
14
+ # Financial Math Library
15
+
16
+ A Python library for pricing derivatives, simulating stochastic processes, and analyzing fixed income instruments.
17
+
18
+ ## Features
19
+
20
+ - **Options Pricing**
21
+ - Black-Scholes analytical pricing
22
+ - Heston stochastic volatility model
23
+ - Monte Carlo simulation
24
+ - Binomial/trinomial trees
25
+ - Implied volatility calculation
26
+
27
+ - **Stochastic Processes**
28
+ - Arithmetic Brownian Motion (ABM)
29
+ - Geometric Brownian Motion (GBM)
30
+ - Cox-Ingersoll-Ross (CIR)
31
+ - Ornstein-Uhlenbeck (OU)
32
+ - Vasicek
33
+ - Jump diffusion (Merton)
34
+
35
+ - **Fixed Income**
36
+ - Bond pricing and yield curve tools
37
+
38
+ - **Portfolio**
39
+ - Risk and performance metrics
40
+
41
+ ## Project Structure
42
+
43
+ ```
44
+ fixed_income/
45
+ fixed_income.py # Fixed income pricing and analytics
46
+
47
+ options/
48
+ iv.py # Implied volatility solver
49
+ option.py # Option contract definitions
50
+
51
+ portfolio/
52
+ metrics.py # Portfolio risk/performance metrics
53
+
54
+ pricing/
55
+ black_scholes.py # Black-Scholes model
56
+ heston.py # Heston stochastic volatility model
57
+ monte_carlo.py # Monte Carlo pricing engine
58
+ trees.py # Binomial/trinomial tree pricing
59
+
60
+ processes/
61
+ abm.py # Arithmetic Brownian Motion
62
+ cir.py # Cox-Ingersoll-Ross process
63
+ gbm.py # Geometric Brownian Motion
64
+ jump_diffusion.py # Jump diffusion process
65
+ ou.py # Ornstein-Uhlenbeck process
66
+ vasicek.py # Vasicek interest rate model
67
+
68
+ testing.ipynb # Example usage and validation notebook
69
+ ```
70
+
71
+ ## Installation
72
+
73
+ ```bash
74
+ git clone <repo-url>
75
+ cd <repo-name>
76
+ pip install -r requirements.txt
77
+ ```
78
+
79
+ ## Usage
80
+
81
+ See `testing.ipynb` for example workflows, including:
82
+
83
+ - Pricing European options with Black-Scholes and comparing to Monte Carlo
84
+ - Simulating asset paths with GBM, CIR, and jump diffusion processes
85
+ - Calibrating the Heston model to market data
86
+ - Computing implied volatility surfaces
87
+ - Evaluating fixed income instruments and portfolio metrics
88
+
89
+ ## Requirements
90
+
91
+ - Python 3.8+
92
+ - NumPy
93
+ - SciPy
94
+ - matplotlib (for notebook visualizations)
95
+
96
+ ## License
97
+
98
+ This project is licensed under the MIT License - see the LICENSE file for details.
@@ -0,0 +1,23 @@
1
+ fixed_income/__init__.py,sha256=-M03eqOgKmpDdcE76SG2NOOlcv07Muaes_WgEgRSwdk,135
2
+ fixed_income/fixed_income.py,sha256=nmy1c0H-GogKWjTaEcbbVvBdKsnY0Dbcwb4BAN7O5fg,1047
3
+ options/__init__.py,sha256=wgfUg5YBENTIG-caoRvS15_zi9rgt2A6onunFwT2d24,61
4
+ options/iv.py,sha256=UgRGXKrMz4_Bu27dyy-rQJi9P9s7Jn3HYY5AptQmIjM,1143
5
+ options/option.py,sha256=eZZKQn2oqOKi49QtQ5efZXoyDLcoNUx5g9tphc75mJM,845
6
+ portfolio/__init__.py,sha256=6FryqsNsq8vrr9goVaWJ_9P9DbrTC2jnLNYHzGA7pVM,36
7
+ portfolio/metrics.py,sha256=-ppV2CQlml3ejYCwkDAzYzayOSh5wLFZpb4RXsPr2os,1670
8
+ pricing/__init__.py,sha256=A35P97bWugX7mqbDQaiz4CqK2X9I78SovuGd7wmrZNo,166
9
+ pricing/black_scholes.py,sha256=l60HxrasZGn_qzNXE_HvIhYYesJXkTqJIMUA6vZwgNY,3350
10
+ pricing/heston.py,sha256=7OEtU-cu9IpkCw62tuzHugkROsh6gVwkib4isdHYZww,1397
11
+ pricing/monte_carlo.py,sha256=XcbrK9wVpZDUsu2EjecN7kFl2nPPmymJ13BE45_9lm0,1336
12
+ pricing/trees.py,sha256=YIpaBlNyW5EODsP1gWl-byawwn81zN4quYft3o0fSj4,1446
13
+ processes/__init__.py,sha256=dPljFrEtNr4EHmeGWgiP1PEPPiazInuu_ZGzn9R_HM4,231
14
+ processes/abm.py,sha256=XpuqxCrk2MASU6w6xW2lS0SigbB1JzEJx7ZDOA2OZ4w,1511
15
+ processes/cir.py,sha256=4SHR8Y4OpvKHM1BwPDWKfoeugLMcguwDvwbpQYQgk1Y,1623
16
+ processes/gbm.py,sha256=gG0U6P8syxy1iA-FXmvK_sLsXxdvyfWUhq6-PH4KY7Q,1553
17
+ processes/jump_diffusion.py,sha256=s7eJ-dmEB-oHk7AvS0Q7dzQ4uhWcspI8wd1HfAhTIC0,2022
18
+ processes/ou.py,sha256=oKM3XYXRl9ZeD1YiKYjAKZnJfcy18KTun1_juHC5y-4,1638
19
+ processes/vasicek.py,sha256=M6zRIY2KB_cu6DicRHt1-C93_3zoIJEqXHcBPGUE-_0,1792
20
+ finmat-0.1.0.dist-info/METADATA,sha256=j5sC7OQWf20qHh5v009x2HHIinDUdjWBXd2c2xkP1Bs,2798
21
+ finmat-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
22
+ finmat-0.1.0.dist-info/top_level.txt,sha256=O5eTQpngFkm0pXOfoaknqSOt5dQpMYLPLDqFPJCIYrg,49
23
+ finmat-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,5 @@
1
+ fixed_income
2
+ options
3
+ portfolio
4
+ pricing
5
+ processes
@@ -0,0 +1 @@
1
+ from fixed_income import simple_interest, compound_interest, future_value, present_value, annuity_pv, annuity_fv, bond_price, convexity
@@ -0,0 +1,27 @@
1
+ def simple_interest(P: float, r: float, t: float) -> float:
2
+ return P * (1 + r * t)
3
+
4
+ def compound_interest(P: float, r: float, n: int, t: float) -> float:
5
+ return P * (1 + r/n)**(n*t)
6
+
7
+ def future_value(PV: float, r: float, n: int) -> float:
8
+ return PV * (1 + r)**n
9
+
10
+ def present_value(FV: float, r: float, n: int) -> float:
11
+ return FV / (1 + r)**n
12
+
13
+ def annuity_pv(PMT: float, r: float, n: int) -> float:
14
+ return PMT * (1 - (1 + r)**-n) / r
15
+
16
+ def annuity_fv(PMT: float, r: float, n: int) -> float:
17
+ return PMT * ((1 + r)**n - 1) / r
18
+
19
+ def bond_price(PMT: float, FV: float, r: float, n: int) -> float:
20
+ """PMT: coupon per period, FV: face value, r: yield per period, n: number of periods"""
21
+ coupons = sum(PMT / (1 + r)**t for t in range(1, n + 1))
22
+ principal = FV / (1 + r)**n
23
+ return coupons + principal
24
+
25
+ def convexity(P0: float, P_plus: float, P_minus: float, dy: float) -> float:
26
+ """P_plus/P_minus: prices after +dy/-dy yield shock"""
27
+ return (P_plus + P_minus - 2 * P0) / (P0 * dy**2)
options/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .iv import ImpliedVolatility
2
+ from .option import Option
options/iv.py ADDED
@@ -0,0 +1,33 @@
1
+ import numpy as np
2
+ from copy import deepcopy
3
+ from .option import Option
4
+
5
+ class ImpliedVolatility:
6
+ @staticmethod
7
+ def solve(option: Option, market_price, pricer, initial=0.2, tol=1e-8, max_iter=100, h=1e-4, seed=None):
8
+ sigma = initial
9
+ copy_ = deepcopy(option)
10
+ def price_at(sigma_val):
11
+ copy_.sigma = sigma_val
12
+ if seed is not None:
13
+ np.random.seed(seed)
14
+ return pricer(copy_)
15
+
16
+ for _ in range(max_iter):
17
+ model_price = price_at(sigma)
18
+
19
+ if abs(model_price - market_price) < tol:
20
+ return sigma
21
+
22
+ price_up = price_at(sigma + h)
23
+ price_down = price_at(sigma - h)
24
+ vega = (price_up - price_down) / (2 * h)
25
+ if abs(vega) < 1e-10 or not np.isfinite(vega):
26
+ return np.nan
27
+
28
+ sigma_new = sigma - (model_price - market_price) / vega
29
+ if not np.isfinite(sigma_new) or sigma_new <= 0:
30
+ return np.nan
31
+ sigma = sigma_new
32
+
33
+ return np.nan
options/option.py ADDED
@@ -0,0 +1,23 @@
1
+ class Option:
2
+ """European option contract with Black-Scholes-style parameters."""
3
+
4
+ VALID_TYPES = ('call', 'put')
5
+
6
+ def __init__(self, S: float, K: float, T: float, r: float, sigma: float, opt_type: str = 'call'):
7
+ if S <= 0:
8
+ raise ValueError("Spot price must be greater than 0")
9
+ if K <= 0:
10
+ raise ValueError("Strike price must be greater than 0")
11
+ if T < 0:
12
+ raise ValueError("Time to expiry must be non-negative")
13
+ if sigma <= 0:
14
+ raise ValueError("Sigma must be greater than 0")
15
+ if opt_type not in self.VALID_TYPES:
16
+ raise ValueError(f"opt_type must be one of {self.VALID_TYPES}")
17
+
18
+ self.S = S
19
+ self.K = K
20
+ self.T = T
21
+ self.r = r
22
+ self.sigma = sigma
23
+ self.opt_type = opt_type
portfolio/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from metrics import PortfolioMetrics
portfolio/metrics.py ADDED
@@ -0,0 +1,43 @@
1
+ import numpy as np
2
+
3
+
4
+ class PortfolioMetrics:
5
+ """Performance/risk metrics for a daily returns series."""
6
+
7
+ def __init__(self, returns, risk_free_rate: float = 0.0):
8
+ self.returns = np.asarray(returns, dtype=float)
9
+ self.risk_free_rate = risk_free_rate # annualized
10
+
11
+ def annualized_return(self) -> float:
12
+ return np.mean(self.returns) * 252
13
+
14
+ def annualized_volatility(self) -> float:
15
+ return np.std(self.returns, ddof=1) * np.sqrt(252)
16
+
17
+ def sharpe_ratio(self) -> float:
18
+ vol = self.annualized_volatility()
19
+ return (self.annualized_return() - self.risk_free_rate) / vol if vol != 0 else np.inf
20
+
21
+ def downside_deviation(self) -> float:
22
+ daily_rf = self.risk_free_rate / 252
23
+ downside = self.returns[self.returns < daily_rf]
24
+ if len(downside) == 0:
25
+ return 0.0
26
+ return np.sqrt(np.mean((downside - daily_rf)**2)) * np.sqrt(252)
27
+
28
+ def sortino_ratio(self) -> float:
29
+ downside_vol = self.downside_deviation()
30
+ if downside_vol == 0:
31
+ return np.inf
32
+ return (self.annualized_return() - self.risk_free_rate) / downside_vol
33
+
34
+ def max_drawdown(self) -> float:
35
+ """Assumes self.returns is chronologically ordered."""
36
+ cumulative_returns = np.cumprod(1 + self.returns)
37
+ running_max = np.maximum.accumulate(cumulative_returns)
38
+ drawdowns = (cumulative_returns - running_max) / running_max
39
+ return np.min(drawdowns)
40
+
41
+ def calmar_ratio(self) -> float:
42
+ max_dd = self.max_drawdown()
43
+ return self.annualized_return() / abs(max_dd) if max_dd < 0 else np.inf
pricing/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .monte_carlo import MonteCarloPricer
2
+ from .black_scholes import BlackScholesPricer
3
+ from .trees import BinomialTreePricer
4
+ from .heston import HestonPricer
@@ -0,0 +1,107 @@
1
+ from options.option import Option
2
+ import numpy as np
3
+ from scipy.stats import norm
4
+
5
+ class BlackScholesPricer:
6
+ """Black-Scholes pricing and Greeks for European Options."""
7
+ @staticmethod
8
+ def price(option: Option):
9
+ S = option.S
10
+ K = option.K
11
+ T = option.T
12
+ r = option.r
13
+ sigma = option.sigma
14
+ opt_type = option.opt_type
15
+
16
+ d1 = (np.log(S / K) + (r + (sigma**2)/2)*T)/(sigma*np.sqrt(T))
17
+ d2 = d1 - sigma*np.sqrt(T)
18
+
19
+ if opt_type == 'call':
20
+ return S * norm.cdf(d1) - K * np.exp(-r*T) * norm.cdf(d2)
21
+ elif opt_type == 'put':
22
+ return K * np.exp(-r*T) * norm.cdf(-d2) - S * norm.cdf(-d1)
23
+ else:
24
+ raise ValueError("Valid types are: 'call' and 'put'")
25
+
26
+ @staticmethod
27
+ def delta(option: Option) -> float:
28
+ S = option.S
29
+ K = option.K
30
+ T = option.T
31
+ r = option.r
32
+ sigma = option.sigma
33
+ opt_type = option.opt_type
34
+
35
+ d1 = (np.log(S / K) + (r + (sigma**2)/2)*T)/(sigma*np.sqrt(T))
36
+
37
+ if opt_type == 'call':
38
+ return norm.cdf(d1)
39
+ elif opt_type == 'put':
40
+ return norm.cdf(d1) - 1
41
+ else:
42
+ raise ValueError("Valid types are: 'call' and 'put'")
43
+
44
+ @staticmethod
45
+ def gamma(option: Option) -> float:
46
+ S = option.S
47
+ K = option.K
48
+ T = option.T
49
+ r = option.r
50
+ sigma = option.sigma
51
+
52
+ d1 = (np.log(S / K) + (r + (sigma**2)/2)*T)/(sigma*np.sqrt(T))
53
+ return norm.pdf(d1)/(S * sigma * np.sqrt(T))
54
+
55
+ @staticmethod
56
+ def vega(option: Option) -> float:
57
+ """Per 1% change in volatility"""
58
+ S = option.S
59
+ K = option.K
60
+ T = option.T
61
+ r = option.r
62
+ sigma = option.sigma
63
+
64
+ d1 = (np.log(S / K) + (r + (sigma**2)/2)*T)/(sigma*np.sqrt(T))
65
+ return S * norm.pdf(d1) * np.sqrt(T) / 100
66
+
67
+ @staticmethod
68
+ def theta(option: Option) -> float:
69
+ """Per day"""
70
+ S = option.S
71
+ K = option.K
72
+ T = option.T
73
+ r = option.r
74
+ sigma = option.sigma
75
+ opt_type = option.opt_type
76
+
77
+ d1 = (np.log(S / K) + (r + (sigma**2)/2)*T)/(sigma*np.sqrt(T))
78
+ d2 = d1 - sigma*np.sqrt(T)
79
+
80
+ base_theta = -(S * norm.pdf(d1) * sigma) / (2 * np.sqrt(T))
81
+ if opt_type == 'call':
82
+ return (base_theta - (r * K * np.exp(-r * T) * norm.cdf(d2)))/365
83
+ elif opt_type == 'put':
84
+ return (base_theta + (r * K * np.exp(-r * T) * norm.cdf(-d2)))/365
85
+ else:
86
+ raise ValueError("Valid types are 'call' and 'put'")
87
+
88
+ @staticmethod
89
+ def rho(option: Option) -> float:
90
+ """Per 1% change in interest rate"""
91
+ S = option.S
92
+ K = option.K
93
+ T = option.T
94
+ r = option.r
95
+ sigma = option.sigma
96
+ opt_type = option.opt_type
97
+
98
+ d1 = (np.log(S / K) + (r + (sigma**2)/2)*T)/(sigma*np.sqrt(T))
99
+ d2 = d1 - sigma*np.sqrt(T)
100
+
101
+ if opt_type == 'call':
102
+ return K * T * np.exp(-r*T) * norm.cdf(d2) / 100
103
+ elif opt_type == 'put':
104
+ return -K * T * np.exp(-r * T) * norm.cdf(-d2) / 100
105
+ else:
106
+ raise ValueError("Valid types are 'call' and 'put'")
107
+
pricing/heston.py ADDED
@@ -0,0 +1,44 @@
1
+ import numpy as np
2
+ from options.option import Option
3
+
4
+ class HestonPricer:
5
+ """Heston model for option pricing."""
6
+ @staticmethod
7
+ def price(
8
+ option: Option,
9
+ kappa: float,
10
+ theta: float,
11
+ sigma: float,
12
+ rho: float, v0:
13
+ float,
14
+ n_paths: int = 10000,
15
+ n_steps: int = 252) -> float:
16
+ S0 = option.S
17
+ K = option.K
18
+ T = option.T
19
+ r = option.r
20
+ opt_type = option.opt_type
21
+
22
+ dt = T / n_steps
23
+
24
+ dW1 = np.random.normal(0, np.sqrt(dt), (n_paths, n_steps))
25
+ dW2 = rho * dW1 + np.sqrt(1 - rho**2) * np.random.normal(0, np.sqrt(dt), (n_paths, n_steps))
26
+
27
+ S = np.zeros((n_paths, n_steps + 1))
28
+ v = np.zeros((n_paths, n_steps + 1))
29
+
30
+ S[:, 0] = S0
31
+ v[:, 0] = v0
32
+
33
+ for i in range(1, n_steps + 1):
34
+ S[:, i] = S[:, i - 1] * np.exp((r - 0.5 * v[:, i - 1]) * dt + np.sqrt(v[:, i - 1]) * dW1[:, i - 1])
35
+ sqrt_v = np.sqrt(np.maximum(v[:, i-1], 0))
36
+ v_next = (v[:, i-1] + kappa*(theta - v[:, i-1])*dt + sigma*sqrt_v*dW2[:, i-1])
37
+ v[:, i] = np.maximum(v_next, 0)
38
+
39
+ S_t = S[:, -1]
40
+ payoff = np.maximum(S_t - K, 0) if opt_type == 'call' else np.maximum(K - S_t, 0)
41
+ price = np.exp(-r * T) * np.mean(payoff)
42
+ return price
43
+
44
+
pricing/monte_carlo.py ADDED
@@ -0,0 +1,33 @@
1
+ from processes.gbm import GeometricBrownianMotion
2
+ import numpy as np
3
+ from options.option import Option
4
+
5
+ class MonteCarloPricer:
6
+ """Monte Carlo pricing for European and Asian options."""
7
+ @staticmethod
8
+ def price_european(option: Option, n_paths: int = 10000, n_steps: int = 252) -> float:
9
+ gbm = GeometricBrownianMotion(S0=option.S, mu=option.r, sigma=option.sigma)
10
+ paths = gbm.sim_paths(T=option.T, dt=option.T/n_steps, n_paths=n_paths)
11
+ S_t = paths[:, -1]
12
+
13
+ if option.opt_type == 'call':
14
+ payoffs = np.maximum(S_t - option.K, 0)
15
+ else:
16
+ payoffs = np.maximum(option.K - S_t, 0)
17
+
18
+ price = np.exp(-option.r * option.T) * np.mean(payoffs)
19
+ return price
20
+
21
+ @staticmethod
22
+ def price_asian(option: Option, n_paths: int = 10000, n_steps: int = 252) -> float:
23
+ gbm = GeometricBrownianMotion(S0=option.S, mu=option.r, sigma=option.sigma)
24
+ paths = gbm.sim_paths(T=option.T, dt=option.T/n_steps, n_paths=n_paths)
25
+ path_avg = np.mean(paths, axis=1)
26
+
27
+ if option.opt_type == 'call':
28
+ payoffs = np.maximum(path_avg - option.K, 0)
29
+ else:
30
+ payoffs = np.maximum(option.K - path_avg, 0)
31
+
32
+ price = np.exp(-option.r * option.T) * np.mean(payoffs)
33
+ return price
pricing/trees.py ADDED
@@ -0,0 +1,37 @@
1
+ import numpy as np
2
+ from options.option import Option
3
+
4
+ class BinomialTreePricer:
5
+ """Binomial tree pricing for American options."""
6
+ @staticmethod
7
+ def price_american(option: Option, u: float, d: float, n_steps: int = 100) -> float:
8
+ S0 = option.S
9
+ K = option.K
10
+ T = option.T
11
+ r = option.r
12
+ opt_type = option.opt_type
13
+
14
+ dt = T / n_steps
15
+ q = (np.exp(r * dt) - d) / (u - d)
16
+ discount = np.exp(-r * dt)
17
+
18
+ asset_prices = np.zeros(n_steps + 1)
19
+ for i in range(n_steps + 1):
20
+ asset_prices[i] = S0 * (u ** i) * (d ** (n_steps - i))
21
+
22
+ option_values = np.zeros(n_steps + 1)
23
+ for i in range(n_steps + 1):
24
+ if opt_type == 'call':
25
+ option_values[i] = np.maximum(asset_prices[i] - K, 0)
26
+ else:
27
+ option_values[i] = np.maximum(K - asset_prices[i], 0)
28
+
29
+ for i in np.arange(n_steps - 1, -1, -1):
30
+ for j in range(i + 1):
31
+ option_values[j] = discount * (q * option_values[j + 1] + (1 - q) * option_values[j])
32
+ asset_price = S0 * (u ** j) * (d ** (i - j))
33
+ if opt_type == 'call':
34
+ option_values[j] = np.maximum(option_values[j], asset_price - K)
35
+ else:
36
+ option_values[j] = np.maximum(option_values[j], K - asset_price)
37
+ return option_values[0]
processes/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from .gbm import GeometricBrownianMotion
2
+ from .abm import ArithmeticBrownianMotion
3
+ from .jump_diffusion import JumpDiffusion
4
+ from .ou import OrnsteinUhlenbeck
5
+ from .vasicek import VasicekModel
6
+ from .cir import CoxIngersollRoss
processes/abm.py ADDED
@@ -0,0 +1,44 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+
4
+ class ArithmeticBrownianMotion:
5
+ """Arithmetic Brownian Motion (ABM) process."""
6
+ def __init__(self, S0: float, mu: float, sigma: float):
7
+ self.S0 = S0
8
+ self.mu = mu
9
+ self.sigma = sigma
10
+
11
+ def sim_paths(self, dt: float, T: float, n_paths: int) -> np.ndarray:
12
+ n_steps = int(T / dt)
13
+ paths = np.zeros((n_paths, n_steps + 1))
14
+
15
+ dW = np.random.normal(0, 1, (n_paths, n_steps))
16
+ paths[:, 0] = self.S0
17
+
18
+ for i in range(1, n_steps + 1):
19
+ paths[:, i] = paths[:, i-1] + self.mu*dt + self.sigma*np.sqrt(dt)*dW[:, i - 1]
20
+
21
+ return paths
22
+
23
+ def plot_paths(self, paths, title='ABM Simulated Paths', xlabel='Steps', ylabel='Price', average=True, largest=True, smallest=True):
24
+ fig, ax = plt.subplots()
25
+
26
+ for path in paths:
27
+ ax.plot(path, color='#1f77b4', linewidth=0.75, alpha=0.5)
28
+
29
+ average_ = np.mean(paths, axis=0)
30
+ largest_ = np.max(paths, axis=0)
31
+ smallest_ = np.min(paths, axis=0)
32
+ if average:
33
+ ax.plot(average_, color='purple', linewidth=0.8, alpha=0.4)
34
+ if largest:
35
+ ax.plot(largest_, color='green', linewidth=0.8, alpha=0.4)
36
+ if smallest:
37
+ ax.plot(smallest_, color="red", linewidth=0.8, alpha=0.4)
38
+
39
+ ax.set_title(title)
40
+ ax.set_xlabel(xlabel)
41
+ ax.set_ylabel(ylabel)
42
+ plt.show()
43
+
44
+
processes/cir.py ADDED
@@ -0,0 +1,46 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+
4
+ class CoxIngersollRoss:
5
+ """Cox-Ingersoll-Ross (CIR) process."""
6
+ def __init__(self, r0: float, a: float, b: float, sigma: float):
7
+ self.r0 = r0
8
+ self.a = a
9
+ self.b = b
10
+ self.sigma = sigma
11
+
12
+ def sim_paths(self, T: float, dt: float, n_paths: int) -> np.ndarray:
13
+ n_steps = int(T / dt)
14
+ paths = np.zeros((n_paths, n_steps + 1))
15
+ paths[:, 0] = self.r0
16
+
17
+ sqrt_dt = np.sqrt(dt)
18
+ for i in range(1, n_steps + 1):
19
+ r_prev = paths[:, i - 1]
20
+ z = np.random.normal(size=n_paths)
21
+ dr = (self.a * (self.b - r_prev) * dt
22
+ + self.sigma * np.sqrt(r_prev) * sqrt_dt * z)
23
+ paths[:, i] = np.maximum(r_prev + dr, 0)
24
+
25
+ return paths
26
+
27
+ def plot_paths(self, paths, title='CIR Simulated Paths', xlabel='Steps', ylabel='Rate', average=True, largest=True, smallest=True):
28
+ fig, ax = plt.subplots()
29
+
30
+ for path in paths:
31
+ ax.plot(path, color='#1f77b4', linewidth=0.75, alpha=0.5)
32
+
33
+ average_ = np.mean(paths, axis=0)
34
+ largest_ = np.max(paths, axis=0)
35
+ smallest_ = np.min(paths, axis=0)
36
+ if average:
37
+ ax.plot(average_, color='purple', linewidth=0.8, alpha=0.4)
38
+ if largest:
39
+ ax.plot(largest_, color='green', linewidth=0.8, alpha=0.4)
40
+ if smallest:
41
+ ax.plot(smallest_, color="red", linewidth=0.8, alpha=0.4)
42
+
43
+ ax.set_title(title)
44
+ ax.set_xlabel(xlabel)
45
+ ax.set_ylabel(ylabel)
46
+ plt.show()
processes/gbm.py ADDED
@@ -0,0 +1,44 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+
4
+ class GeometricBrownianMotion:
5
+ """Geometric Brownian Motion (GBM) process."""
6
+ def __init__(self, S0: float, mu: float, sigma: float):
7
+ self.S0 = S0
8
+ self.mu = mu
9
+ self.sigma = sigma
10
+
11
+
12
+ def sim_paths(self, T: float, dt: float, n_paths: int) -> np.ndarray:
13
+ n_steps = int(T / dt)
14
+ paths = np.zeros((n_paths, n_steps + 1))
15
+ paths[:, 0] = self.S0
16
+
17
+ dW = np.random.normal(0, np.sqrt(dt), (n_paths, n_steps))
18
+
19
+ for i in range(1, n_steps + 1):
20
+ paths[:, i] = paths[:, i-1] * np.exp(
21
+ (self.mu - 0.5*self.sigma**2)*dt + self.sigma*dW[:, i-1]
22
+ )
23
+ return paths
24
+
25
+ def plot_paths(self, paths, title='GBM Simulated Paths', xlabel='Steps', ylabel='Price', average=True, largest=True, smallest=True):
26
+ fig, ax = plt.subplots()
27
+
28
+ for path in paths:
29
+ ax.plot(path, color='#1f77b4', linewidth=0.75, alpha=0.5)
30
+
31
+ average_ = np.mean(paths, axis=0)
32
+ largest_ = np.max(paths, axis=0)
33
+ smallest_ = np.min(paths, axis=0)
34
+ if average:
35
+ ax.plot(average_, color='purple', linewidth=0.8, alpha=0.4)
36
+ if largest:
37
+ ax.plot(largest_, color='green', linewidth=0.8, alpha=0.4)
38
+ if smallest:
39
+ ax.plot(smallest_, color="red", linewidth=0.8, alpha=0.4)
40
+
41
+ ax.set_title(title)
42
+ ax.set_xlabel(xlabel)
43
+ ax.set_ylabel(ylabel)
44
+ plt.show()
@@ -0,0 +1,54 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+
4
+ class JumpDiffusion:
5
+ """Jump Diffusion process."""
6
+ def __init__(self, S0: float, mu: float, sigma: float, lambda_j: float, mu_j: float, sigma_j: float):
7
+ self.S0 = S0
8
+ self.mu = mu
9
+ self.sigma = sigma
10
+ self.lambda_j = lambda_j
11
+ self.mu_j = mu_j
12
+ self.sigma_j = sigma_j
13
+
14
+ def sim_paths(self, T: float, dt: float, n_paths: int) -> np.ndarray:
15
+ n_steps = int(T / dt)
16
+ paths = np.zeros((n_paths, n_steps + 1))
17
+ paths[:, 0] = self.S0
18
+
19
+ dW = np.random.normal(0, np.sqrt(dt), (n_paths, n_steps))
20
+
21
+ n_jumps = np.random.poisson(self.lambda_j * dt, (n_paths, n_steps))
22
+ jump_mean = n_jumps * self.mu_j
23
+ jump_std = np.sqrt(n_jumps) * self.sigma_j
24
+ log_jumps = np.random.normal(jump_mean, np.where(jump_std > 0, jump_std, 1e-12))
25
+ log_jumps = np.where(n_jumps > 0, log_jumps, 0)
26
+
27
+
28
+
29
+ for i in range(1, n_steps + 1):
30
+ paths[:, i] = paths[:, i-1] * np.exp(
31
+ (self.mu - 0.5*self.sigma**2)*dt + self.sigma*dW[:, i-1] + log_jumps[:, i-1]
32
+ )
33
+ return paths
34
+
35
+ def plot_paths(self, paths, title='Jump Diffusion Simulated Paths', xlabel='Steps', ylabel='Price', average=True, largest=True, smallest=True):
36
+ fig, ax = plt.subplots()
37
+
38
+ for path in paths:
39
+ ax.plot(path, color='#1f77b4', linewidth=0.75, alpha=0.5)
40
+
41
+ average_ = np.mean(paths, axis=0)
42
+ largest_ = np.max(paths, axis=0)
43
+ smallest_ = np.min(paths, axis=0)
44
+ if average:
45
+ ax.plot(average_, color='purple', linewidth=0.8, alpha=0.4)
46
+ if largest:
47
+ ax.plot(largest_, color='green', linewidth=0.8, alpha=0.4)
48
+ if smallest:
49
+ ax.plot(smallest_, color="red", linewidth=0.8, alpha=0.4)
50
+
51
+ ax.set_title(title)
52
+ ax.set_xlabel(xlabel)
53
+ ax.set_ylabel(ylabel)
54
+ plt.show()
processes/ou.py ADDED
@@ -0,0 +1,46 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+
4
+ class OrnsteinUhlenbeck:
5
+ """Ornstein-Uhlenbeck (OU) process."""
6
+ def __init__(self, X0: float, theta: float, mu: float, sigma: float, T: float, dt: float):
7
+ self.X0 = X0
8
+ self.theta = theta
9
+ self.mu = mu
10
+ self.sigma = sigma
11
+ self.T = T
12
+ self.dt = dt
13
+
14
+ def process(self, n_paths: int) -> np.ndarray:
15
+ n_steps = int(self.T / self.dt)
16
+
17
+ paths = np.zeros((n_paths, n_steps + 1))
18
+ paths[:, 0] = self.X0
19
+
20
+ dW = np.random.normal(0, np.sqrt(self.dt), (n_paths, n_steps))
21
+ for i in range(1, n_steps + 1):
22
+ paths[:, i] = paths[:, i-1] + self.theta * (self.mu - paths[:, i-1]) * self.dt + self.sigma * dW[:, i-1]
23
+
24
+
25
+ return paths
26
+
27
+ def plot_paths(self, paths, title='Ornstein-Uhlenbeck Simulated Paths', xlabel='Steps', ylabel='Value', average=True, largest=True, smallest=True):
28
+ fig, ax = plt.subplots()
29
+ for path in paths:
30
+ ax.plot(path, color='#1f77b4', linewidth=0.8, alpha=0.5)
31
+
32
+ average_ = np.mean(paths, axis=0)
33
+ largest_ = np.max(paths, axis=0)
34
+ smallest_ = np.min(paths, axis=0)
35
+ if average:
36
+ ax.plot(average_, color='purple', linewidth=0.8, alpha=0.4)
37
+ if largest:
38
+ ax.plot(largest_, color='green', linewidth=0.8, alpha=0.4)
39
+ if smallest:
40
+ ax.plot(smallest_, color="red", linewidth=0.8, alpha=0.4)
41
+
42
+ ax.set_title(title)
43
+ ax.set_xlabel(xlabel)
44
+ ax.set_ylabel(ylabel)
45
+ plt.show()
46
+
processes/vasicek.py ADDED
@@ -0,0 +1,48 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+
4
+ class VasicekModel:
5
+ """Vasicek model for interest rate simulation."""
6
+ def __init__(self, theta: float, mu: float, sigma: float, r0: float):
7
+ self.theta = theta
8
+ self.mu = mu
9
+ self.sigma = sigma
10
+ self.r0 = r0
11
+
12
+ def sim_paths(self, T: float, dt: float, n_paths: int) -> np.ndarray:
13
+ n_steps = int(T / dt)
14
+ paths = np.zeros((n_paths, n_steps + 1))
15
+ paths[:, 0] = self.r0
16
+
17
+ sqrt_dt = np.sqrt(dt)
18
+ for i in range(1, n_steps + 1):
19
+ r_prev = paths[:, i - 1]
20
+ z = np.random.normal(size=n_paths)
21
+ dr = self.theta * (self.mu - r_prev) * dt + self.sigma * sqrt_dt * z
22
+ paths[:, i] = r_prev + dr
23
+
24
+ return paths
25
+
26
+ def plot_paths(self, paths, T, dt, title='Simulated Vasicek Interest Rate Paths', xlabel='Time', ylabel='Interest Rate', average=True, largest=True, smallest=True):
27
+ n_steps = int(T / dt)
28
+ time_points = np.linspace(0, T, n_steps + 1)
29
+
30
+ fig, ax = plt.subplots()
31
+ for path in paths:
32
+ ax.plot(time_points, path, color='#1f77b4', linewidth=0.75, alpha=0.5)
33
+
34
+ average_ = np.mean(paths, axis=0)
35
+ largest_ = np.max(paths, axis=0)
36
+ smallest_ = np.min(paths, axis=0)
37
+ if average:
38
+ ax.plot(time_points, average_, color='purple', linewidth=0.8, alpha=0.4)
39
+ if largest:
40
+ ax.plot(time_points, largest_, color='green', linewidth=0.8, alpha=0.4)
41
+ if smallest:
42
+ ax.plot(time_points, smallest_, color="red", linewidth=0.8, alpha=0.4)
43
+
44
+ ax.set_title(title)
45
+ ax.set_xlabel(xlabel)
46
+ ax.set_ylabel(ylabel)
47
+ ax.set_xlim(0, T)
48
+ plt.show()