Skip to content

Commit 0b964b7

Browse files
authored
Merge pull request #49 from chemardes/feature/implement-monte-carlo-simulations
FEATURE: adding monte carlo simulations
2 parents ae457ba + 5ea016c commit 0b964b7

12 files changed

Lines changed: 240 additions & 12 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
- uses: actions/checkout@v2
1313
- uses: actions/setup-python@v5
1414
with:
15-
python-version: '3.13'
15+
python-version: '3.10'
1616
architecture: 'x64'
1717
- name: Run python tests
1818
run: |

ci/run_python_tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export PYTHONPATH=$(pwd)
66

77
# Set up your Python environment
88
pip install virtualenv
9-
virtualenv -p python3.13 venv
9+
virtualenv -p python3.10 venv
1010
source venv/bin/activate
1111

1212
pip install -r requirements.txt

pdesolvers/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
from .solution import *
33
from .solvers import *
44
from .utils import *
5-
from .enums import *
5+
from .enums import *
6+
from .optionspricing import *

pdesolvers/main.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import numpy as np
2+
import pandas as pd
23
import pdesolvers as pde
34

45
def main():
@@ -14,14 +15,36 @@ def main():
1415
# solver1 = pde.Heat1DCNSolver(equation1)
1516
# solver2 = pde.Heat1DExplicitSolver(equation1)
1617

17-
# testing for bse
18-
equation2 = pde.BlackScholesEquation(pde.OptionType.EUROPEAN_CALL, 300, 1, 0.2, 0.05, 100, 100, 20000)
18+
# testing for monte carlo pricing
19+
20+
ticker = 'AAPL'
21+
22+
# STOCK
23+
historical_data = pde.HistoricalStockData(ticker)
24+
historical_data.fetch_stock_data( "2024-03-21","2025-03-21")
25+
sigma, r = historical_data.estimate_metrics()
26+
current_price = historical_data.get_latest_stock_price()
27+
28+
equation2 = pde.BlackScholesEquation(pde.OptionType.EUROPEAN_CALL, current_price, 1, sigma, r, 100, 100, 20000)
1929

2030
solver1 = pde.BlackScholesCNSolver(equation2)
2131
solver2 = pde.BlackScholesExplicitSolver(equation2)
2232
sol1 = solver1.solve()
23-
sol1.plot_greek(pde.Greeks.GAMMA)
24-
sol1.plot()
33+
# sol1.plot()
34+
35+
# COMPARISON
36+
# look to see the corresponding option price for the expiration date and strike price
37+
pricing_1 = pde.BlackScholesFormula(pde.OptionType.EUROPEAN_CALL, current_price, 100, r, sigma, 1)
38+
pricing_2 = pde.MonteCarloPricing(pde.OptionType.EUROPEAN_CALL, current_price, 100, r, sigma, 1, 365, 10000)
39+
40+
bs_price = pricing_1.get_black_scholes_merton_price()
41+
monte_carlo_price = pricing_2.get_monte_carlo_option_price()
42+
43+
pde_price = sol1.get_result()[-1, 0]
44+
print(f"PDE Price: {pde_price}")
45+
print(f"Black-Scholes Price: {bs_price}")
46+
print(f"Monte-Carlo Price: {monte_carlo_price}")
47+
pricing_2.plot()
2548

2649

2750
if __name__ == "__main__":
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .black_scholes_formula import BlackScholesFormula
2+
from .monte_carlo import MonteCarloPricing
3+
from .market_data import HistoricalStockData
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import numpy as np
2+
from scipy.stats import norm
3+
from pdesolvers.enums.enums import OptionType
4+
5+
class BlackScholesFormula:
6+
def __init__(self, option_type: OptionType, S0, strike_price, r, sigma, expiry):
7+
self.__option_type = option_type
8+
self.__S0 = S0
9+
self.__strike_price = strike_price
10+
self.__r = r
11+
self.__sigma = sigma
12+
self.__expiry = expiry
13+
14+
def get_black_scholes_merton_price(self):
15+
16+
d1 = (np.log(self.__S0/self.__strike_price) + (self.__r + 0.5 * self.__sigma**2) * self.__expiry) / (self.__sigma * np.sqrt(self.__expiry))
17+
d2 = d1 - (self.__sigma * np.sqrt(self.__expiry))
18+
19+
if self.__option_type == OptionType.EUROPEAN_CALL:
20+
payoff = self.__S0 * norm.cdf(d1) - self.__strike_price * np.exp(-self.__r * self.__expiry) * norm.cdf(d2)
21+
elif self.__option_type == OptionType.EUROPEAN_PUT:
22+
payoff = self.__strike_price * np.exp(-self.__r * self.__expiry) * norm.cdf(-d2) - self.__S0 * norm.cdf(-d1)
23+
else:
24+
raise ValueError(f'Unsupported option type: {self.__option_type}')
25+
26+
return payoff
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import numpy as np
2+
import yfinance as yf
3+
4+
class HistoricalStockData:
5+
def __init__(self, ticker):
6+
self.__stock_data = None
7+
self.__options = None
8+
self.__ticker = ticker
9+
10+
def fetch_stock_data(self, start_date, end_date):
11+
self.__stock_data = yf.download(self.__ticker, start=start_date, end=end_date, interval='1d')
12+
return self.__stock_data
13+
14+
def estimate_metrics(self):
15+
16+
if self.__stock_data is None:
17+
raise ValueError(f'No data available - data must be fetched first.')
18+
19+
self.__stock_data["Log Returns"] = np.log(self.__stock_data["Close"] / self.__stock_data["Close"].shift(1))
20+
21+
# takes into account the gaps between trading days
22+
self.__stock_data["Time Diff"] = self.__stock_data.index.to_series().diff().dt.days
23+
self.__stock_data["Z"] = self.__stock_data["Log Returns"] / self.__stock_data["Time Diff"]
24+
25+
print(self.__stock_data)
26+
27+
annualized_sigma = self.__stock_data["Z"].std() * np.sqrt(252)
28+
annualized_mu = self.__stock_data["Z"].mean() * 252
29+
30+
return annualized_sigma, annualized_mu
31+
32+
def get_latest_stock_price(self):
33+
if self.__stock_data is None:
34+
raise ValueError("No data available. Call fetch_data first.")
35+
36+
return self.__stock_data["Close"].iloc[-1].item()
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import numpy as np
2+
import matplotlib.pyplot as plt
3+
4+
from pdesolvers.enums.enums import OptionType
5+
6+
class MonteCarloPricing:
7+
8+
def __init__(self, option_type: OptionType, S0, strike_price, r, sigma, T, time_steps, sim):
9+
"""
10+
Initialize the Geometric Brownian Motion model with the given parameters.
11+
12+
Parameters:
13+
- S0: Initial stock price
14+
- mu: Drift coefficient (expected return)
15+
- sigma: Volatility coefficient (standard deviation of returns)
16+
- T: Time period for the simulation (in years)
17+
- time_steps: Number of time steps in the simulation
18+
- sim: Number of simulations to run
19+
"""
20+
21+
self.__option_type = option_type
22+
self.__S0 = S0
23+
self.__strike_price = strike_price
24+
self.__r = r
25+
self.__sigma = sigma
26+
self.__T = T
27+
self.__time_steps = time_steps
28+
self.__sim = sim
29+
self.__S = None
30+
31+
def get_monte_carlo_option_price(self):
32+
33+
S = self.simulate_gbm()
34+
35+
if self.__option_type == OptionType.EUROPEAN_CALL:
36+
payoff = np.maximum(S[:, -1] - self.__strike_price, 0)
37+
elif self.__option_type == OptionType.EUROPEAN_PUT:
38+
payoff = np.maximum(self.__strike_price - S[:, -1], 0)
39+
else:
40+
raise ValueError(f'Unsupported Option Type: {self.__option_type}')
41+
42+
option_price = np.exp(-self.__r * self.__T) * np.mean(payoff)
43+
44+
return option_price
45+
46+
def simulate_gbm(self):
47+
"""
48+
Simulate the Geometric Brownian Motion for the given parameters.
49+
50+
This method calculates the stock prices at each time step for each simulation.
51+
"""
52+
53+
t = self.__generate_grid()
54+
dt = t[1] - t[0]
55+
56+
B = np.zeros((self.__sim, self.__time_steps))
57+
S = np.zeros((self.__sim, self.__time_steps))
58+
59+
# for all simulations at t = 0
60+
S[:,0] = self.__S0
61+
Z = np.random.normal(0, 1, (self.__sim, self.__time_steps))
62+
63+
for i in range(self.__sim):
64+
for j in range (1, self.__time_steps):
65+
# updates brownian motion
66+
B[i,j] = B[i,j-1] + np.sqrt(dt) * Z[i,j-1]
67+
# calculates stock price based on the incremental difference
68+
S[i,j] = S[i, j-1] * np.exp((self.__r - 0.5*self.__sigma**2)*dt + self.__sigma * (B[i, j] - B[i, j - 1]))
69+
70+
self.__S = S
71+
72+
return self.__S
73+
74+
def __generate_grid(self):
75+
"""
76+
Generate a time grid from 0 to T with `time_steps` intervals.
77+
78+
Returns:
79+
- A numpy array representing the time grid.
80+
"""
81+
82+
return np.linspace(0, self.__T, self.__time_steps)
83+
84+
def plot(self):
85+
"""
86+
Plot the simulated stock prices for all simulations.
87+
"""
88+
89+
t = self.__generate_grid()
90+
91+
fig = plt.figure(figsize=(10,6))
92+
for i in range(np.min([100, self.__sim])):
93+
plt.plot(t, self.__S[i], color='grey', alpha=0.3)
94+
95+
plt.title("Simulated Geometric Brownian Motion")
96+
plt.xlabel("Time (Years)")
97+
plt.ylabel("Stock Price")
98+
plt.show()

pdesolvers/utils/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .utility import RBFInterpolator
1+
from .utility import RBFInterpolator, GPUResults

pdesolvers/utils/utility.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import numpy as np
2+
import pandas as pd
3+
import matplotlib.pyplot as plt
24
from scipy.sparse import csc_matrix
35

46
import pdesolvers.enums.enums as enum
@@ -152,4 +154,42 @@ def interpolate(self, x, y):
152154
interpolated += rbf_weights[3] * self.__z[i_minus + 1, j_minus + 1]
153155
interpolated /= sum_rbf
154156

155-
return interpolated
157+
return interpolated
158+
159+
class GPUResults:
160+
161+
def __init__(self, file_path, s_max, expiry):
162+
self.__file_path = file_path
163+
self.__s_max = s_max
164+
self.__expiry = expiry
165+
self.__grid_data = None
166+
167+
def get_results(self):
168+
169+
# Load data
170+
df = pd.read_csv(self.__file_path, header=None)
171+
print(f"Data shape: {df.shape}")
172+
173+
self.__grid_data = df.values.T
174+
175+
return self.__grid_data
176+
177+
def plot_option_surface(self):
178+
if self.__grid_data is None:
179+
self.get_results()
180+
181+
price_grid = np.linspace(0, self.__s_max, self.__grid_data.shape[0])
182+
time_grid = np.linspace(0, self.__expiry, self.__grid_data.shape[1])
183+
X, Y = np.meshgrid(time_grid, price_grid)
184+
185+
fig = plt.figure(figsize=(10, 6))
186+
ax = fig.add_subplot(111, projection='3d')
187+
surf = ax.plot_surface(X, Y, self.__grid_data, cmap='viridis')
188+
189+
ax.set_xlabel('Time')
190+
ax.set_ylabel('Asset Price')
191+
ax.set_zlabel('Option Value')
192+
ax.set_title('Option Value Surface Plot')
193+
fig.colorbar(surf, shrink=0.5, aspect=5)
194+
195+
plt.show()

0 commit comments

Comments
 (0)