optPricing 2.0.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.
Files changed (92) hide show
  1. optpricing-2.0.0.dist-info/METADATA +105 -0
  2. optpricing-2.0.0.dist-info/RECORD +92 -0
  3. optpricing-2.0.0.dist-info/WHEEL +5 -0
  4. optpricing-2.0.0.dist-info/entry_points.txt +3 -0
  5. optpricing-2.0.0.dist-info/licenses/LICENSE +9 -0
  6. optpricing-2.0.0.dist-info/top_level.txt +1 -0
  7. quantfin/__init__.py +101 -0
  8. quantfin/atoms/__init__.py +21 -0
  9. quantfin/atoms/bond.py +28 -0
  10. quantfin/atoms/option.py +70 -0
  11. quantfin/atoms/rate.py +46 -0
  12. quantfin/atoms/stock.py +39 -0
  13. quantfin/calibration/__init__.py +25 -0
  14. quantfin/calibration/calibrator.py +155 -0
  15. quantfin/calibration/fit_jump_parameters.py +66 -0
  16. quantfin/calibration/fit_market_params.py +120 -0
  17. quantfin/calibration/iv_surface.py +155 -0
  18. quantfin/calibration/technique_selector.py +47 -0
  19. quantfin/calibration/vectorized_bsm_iv.py +93 -0
  20. quantfin/calibration/vectorized_integration_iv.py +143 -0
  21. quantfin/cli.py +372 -0
  22. quantfin/config.py +57 -0
  23. quantfin/dashboard/__init__.py +14 -0
  24. quantfin/dashboard/app.py +26 -0
  25. quantfin/dashboard/pages/1_Calibration.py +57 -0
  26. quantfin/dashboard/pages/2_Pricer_and_Greeks.py +150 -0
  27. quantfin/dashboard/pages/3_Financial_Tools.py +69 -0
  28. quantfin/dashboard/pages/__init__.py +0 -0
  29. quantfin/dashboard/plots.py +157 -0
  30. quantfin/dashboard/service.py +176 -0
  31. quantfin/dashboard/widgets.py +87 -0
  32. quantfin/data/__init__.py +26 -0
  33. quantfin/data/historical_manager.py +86 -0
  34. quantfin/data/market_data_manager.py +236 -0
  35. quantfin/models/__init__.py +54 -0
  36. quantfin/models/base/__init__.py +16 -0
  37. quantfin/models/base/base_model.py +198 -0
  38. quantfin/models/base/validators.py +95 -0
  39. quantfin/models/bates.py +192 -0
  40. quantfin/models/blacks_approx.py +134 -0
  41. quantfin/models/bsm.py +262 -0
  42. quantfin/models/cev.py +109 -0
  43. quantfin/models/cgmy.py +189 -0
  44. quantfin/models/cir.py +90 -0
  45. quantfin/models/dupire_local.py +67 -0
  46. quantfin/models/heston.py +204 -0
  47. quantfin/models/hyperbolic.py +170 -0
  48. quantfin/models/kou.py +148 -0
  49. quantfin/models/merton_jump.py +225 -0
  50. quantfin/models/nig.py +180 -0
  51. quantfin/models/perpetual_put.py +115 -0
  52. quantfin/models/sabr.py +64 -0
  53. quantfin/models/sabr_jump.py +68 -0
  54. quantfin/models/vasicek.py +88 -0
  55. quantfin/models/vg.py +178 -0
  56. quantfin/parity/__init__.py +13 -0
  57. quantfin/parity/implied_rate.py +110 -0
  58. quantfin/parity/parity_model.py +178 -0
  59. quantfin/techniques/__init__.py +31 -0
  60. quantfin/techniques/base/__init__.py +21 -0
  61. quantfin/techniques/base/base_technique.py +54 -0
  62. quantfin/techniques/base/greek_mixin.py +265 -0
  63. quantfin/techniques/base/iv_mixin.py +116 -0
  64. quantfin/techniques/base/lattice_technique.py +129 -0
  65. quantfin/techniques/base/pricing_result.py +27 -0
  66. quantfin/techniques/base/random_utils.py +31 -0
  67. quantfin/techniques/closed_form.py +296 -0
  68. quantfin/techniques/crr.py +59 -0
  69. quantfin/techniques/fft.py +178 -0
  70. quantfin/techniques/integration.py +202 -0
  71. quantfin/techniques/kernels/__init__.py +37 -0
  72. quantfin/techniques/kernels/lattice_kernels.py +286 -0
  73. quantfin/techniques/kernels/mc_kernels.py +337 -0
  74. quantfin/techniques/leisen_reimer.py +59 -0
  75. quantfin/techniques/monte_carlo.py +259 -0
  76. quantfin/techniques/pde.py +155 -0
  77. quantfin/techniques/topm.py +59 -0
  78. quantfin/workflows/__init__.py +14 -0
  79. quantfin/workflows/backtest_workflow.py +133 -0
  80. quantfin/workflows/configs/__init__.py +51 -0
  81. quantfin/workflows/configs/bates_config.py +25 -0
  82. quantfin/workflows/configs/bsm_config.py +9 -0
  83. quantfin/workflows/configs/cev_config.py +16 -0
  84. quantfin/workflows/configs/cgmy_config.py +18 -0
  85. quantfin/workflows/configs/heston_config.py +21 -0
  86. quantfin/workflows/configs/hyperbolic_config.py +18 -0
  87. quantfin/workflows/configs/kou_config.py +16 -0
  88. quantfin/workflows/configs/merton_config.py +18 -0
  89. quantfin/workflows/configs/nig_config.py +13 -0
  90. quantfin/workflows/configs/sabr_config.py +19 -0
  91. quantfin/workflows/configs/vg_config.py +9 -0
  92. quantfin/workflows/daily_workflow.py +150 -0
@@ -0,0 +1,155 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ from scipy.optimize import minimize, minimize_scalar
6
+
7
+ from quantfin.atoms import Option, OptionType, Rate, Stock
8
+ from quantfin.models import BaseModel
9
+
10
+ from .technique_selector import select_fastest_technique
11
+
12
+ __doc__ = """
13
+ Defines the main Calibrator class used to fit financial models to market data.
14
+ """
15
+
16
+
17
+ class Calibrator:
18
+ """
19
+ A generic class for calibrating financial models to market data.
20
+
21
+ This class orchestrates the process of finding the model parameters that
22
+ minimize the difference between model prices and observed market prices.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ model: BaseModel,
28
+ market_data: pd.DataFrame,
29
+ stock: Stock,
30
+ rate: Rate,
31
+ ):
32
+ """
33
+ Initializes the Calibrator.
34
+
35
+ Parameters
36
+ ----------
37
+ model : BaseModel
38
+ The financial model to be calibrated (e.g., HestonModel).
39
+ market_data : pd.DataFrame
40
+ A DataFrame containing market prices of options. Must include
41
+ 'strike', 'maturity', 'optionType', and 'marketPrice' columns.
42
+ stock : Stock
43
+ The underlying asset's properties.
44
+ rate : Rate
45
+ The risk-free rate structure.
46
+ """
47
+ self.model = model
48
+ self.market_data = market_data
49
+ self.stock = stock
50
+ self.rate = rate
51
+ self.technique = select_fastest_technique(model)
52
+ _class_name = self.technique.__class__.__name__
53
+ print(f"Calibrator using '{_class_name}' for model '{model.name}'.")
54
+
55
+ def _objective_function(
56
+ self,
57
+ params_to_fit_values: np.ndarray,
58
+ params_to_fit_names: list[str],
59
+ frozen_params: dict[str, float],
60
+ ) -> float:
61
+ """The objective function to be minimized, calculating total squared error."""
62
+ current_params = {
63
+ **frozen_params,
64
+ **dict(zip(params_to_fit_names, params_to_fit_values)),
65
+ }
66
+ print(
67
+ f" Trying params: { {k: f'{v:.4f}' for k, v in current_params.items()} }",
68
+ end="",
69
+ )
70
+ try:
71
+ temp_model = self.model.with_params(**current_params)
72
+ except ValueError as e:
73
+ print(f" -> Invalid params ({e}), returning large error.")
74
+ return 1e12
75
+
76
+ # Vectorized pricing for speed
77
+ # assumes the technique supports vectorized pricing.
78
+ # For now, fall back to iterating, but this is where future optimization lies.
79
+ total_error = 0.0
80
+ for _, row in self.market_data.iterrows():
81
+ option = Option(
82
+ strike=row["strike"],
83
+ maturity=row["maturity"],
84
+ option_type=OptionType.CALL
85
+ if row["optionType"] == "call"
86
+ else OptionType.PUT,
87
+ )
88
+ model_price = self.technique.price(
89
+ option, self.stock, temp_model, self.rate, **current_params
90
+ ).price
91
+ total_error += (model_price - row["marketPrice"]) ** 2
92
+
93
+ print(f" --> Total Error: {total_error:.4f}")
94
+ return total_error
95
+
96
+ def fit(
97
+ self,
98
+ initial_guess: dict[str, float],
99
+ bounds: dict[str, tuple],
100
+ frozen_params: dict[str, float] = None,
101
+ ) -> dict[str, float]:
102
+ """
103
+ Performs the calibration using an optimization algorithm.
104
+
105
+ This method uses `scipy.optimize.minimize` (or `minimize_scalar` for
106
+ a single parameter) to find the optimal set of parameters that
107
+ minimizes the objective function.
108
+
109
+ Parameters
110
+ ----------
111
+ initial_guess : dict[str, float]
112
+ A dictionary of initial guesses for the parameters to be fitted.
113
+ bounds : dict[str, tuple]
114
+ A dictionary mapping parameter names to their (min, max) bounds.
115
+ frozen_params : dict[str, float] | None, optional
116
+ A dictionary of parameters to hold constant during the optimization.
117
+ Defaults to None.
118
+
119
+ Returns
120
+ -------
121
+ dict[str, float]
122
+ A dictionary containing the full set of calibrated and frozen parameters.
123
+ """
124
+ frozen_params = frozen_params or {}
125
+ params_to_fit_names = [p for p in initial_guess if p not in frozen_params]
126
+ print(f"Fitting parameters: {params_to_fit_names}")
127
+ if not params_to_fit_names:
128
+ return frozen_params
129
+
130
+ fit_bounds = [bounds.get(p) for p in params_to_fit_names]
131
+ initial_values = [initial_guess[p] for p in params_to_fit_names]
132
+
133
+ if len(params_to_fit_names) == 1:
134
+ # scalar minimizer for one parameter
135
+ res = minimize_scalar(
136
+ lambda x: self._objective_function(
137
+ np.array([x]), params_to_fit_names, frozen_params
138
+ ),
139
+ bounds=fit_bounds[0],
140
+ method="bounded",
141
+ )
142
+ final_params = {**frozen_params, params_to_fit_names[0]: res.x}
143
+ print(f"Scalar optimization finished. Final loss: {res.fun:.6f}")
144
+ else:
145
+ # gradient-based optimizer for multiple parameters
146
+ res = minimize(
147
+ fun=self._objective_function,
148
+ x0=initial_values,
149
+ args=(params_to_fit_names, frozen_params),
150
+ method="L-BFGS-B",
151
+ bounds=fit_bounds,
152
+ )
153
+ final_params = {**frozen_params, **dict(zip(params_to_fit_names, res.x))}
154
+ print(f"Multivariate optimization finished. Final loss: {res.fun:.6f}")
155
+ return final_params
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+ __doc__ = """
7
+ Provides a utility function to estimate jump parameters from historical data.
8
+ """
9
+
10
+
11
+ def fit_jump_params_from_history(
12
+ log_returns: pd.Series,
13
+ threshold_stds: float = 3.0,
14
+ ) -> dict:
15
+ """
16
+ Estimates jump parameters and diffusion volatility from historical returns.
17
+
18
+ This function separates historical log returns into a "diffusion" component
19
+ (normal daily movements) and a "jump" component (extreme movements) based
20
+ on a standard deviation threshold. It then calculates the annualized
21
+ parameters for a jump-diffusion model like Merton's.
22
+
23
+ Parameters
24
+ ----------
25
+ log_returns : pd.Series
26
+ A pandas Series of daily log returns.
27
+ threshold_stds : float, optional
28
+ The number of standard deviations to use as a threshold for identifying
29
+ jumps, by default 3.0.
30
+
31
+ Returns
32
+ -------
33
+ dict
34
+ A dictionary containing the estimated parameters: 'sigma', 'lambda',
35
+ 'mu_j', and 'sigma_j'.
36
+ """
37
+ print("Fitting jump parameters from historical returns...")
38
+
39
+ std_dev = log_returns.std()
40
+ jump_threshold = threshold_stds * std_dev
41
+
42
+ diffusion_returns = log_returns[abs(log_returns) < jump_threshold]
43
+ jump_returns = log_returns[abs(log_returns) >= jump_threshold]
44
+
45
+ # Annualize daily std dev by multiplying by sqrt(252 trading days)
46
+ sigma_est = diffusion_returns.std() * np.sqrt(252)
47
+
48
+ if len(jump_returns) > 2:
49
+ lambda_est = len(jump_returns) / len(log_returns) * 252
50
+ mu_j_est = jump_returns.mean()
51
+ sigma_j_est = jump_returns.std()
52
+ else:
53
+ lambda_est, mu_j_est, sigma_j_est = 0.1, 0.0, 0.0
54
+ print(" -> Warning: Not enough jumps detected. Using default jump parameters.")
55
+
56
+ # Use the key 'sigma' to match the parameter name in the Merton/Kou models.
57
+ fitted_params = {
58
+ "sigma": sigma_est,
59
+ "lambda": lambda_est,
60
+ "mu_j": mu_j_est,
61
+ "sigma_j": sigma_j_est,
62
+ }
63
+
64
+ formatted = ", ".join(f"{k}: {v:.4f}" for k, v in fitted_params.items())
65
+ print(f" -> Estimated Historical Params: {{{formatted}}}")
66
+ return fitted_params
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ from scipy.optimize import minimize
6
+
7
+ __doc__ = """
8
+ Provides utility functions to fit market-implied parameters like the
9
+ risk-free rate and dividend yield from option prices using put-call parity.
10
+ """
11
+
12
+
13
+ def find_atm_options(
14
+ calls: pd.DataFrame,
15
+ puts: pd.DataFrame,
16
+ spot: float,
17
+ ) -> pd.DataFrame:
18
+ """
19
+ Finds the closest at-the-money (ATM) call-put pair for each expiry.
20
+
21
+ Parameters
22
+ ----------
23
+ calls : pd.DataFrame
24
+ A DataFrame of call options.
25
+ puts : pd.DataFrame
26
+ A DataFrame of put options.
27
+ spot : float
28
+ The current spot price of the underlying.
29
+
30
+ Returns
31
+ -------
32
+ pd.DataFrame
33
+ A DataFrame containing the merged ATM call-put pairs.
34
+ """
35
+ merged = pd.merge(
36
+ calls, puts, on=["strike", "maturity"], suffixes=("_call", "_put"), how="inner"
37
+ )
38
+ if merged.empty:
39
+ return pd.DataFrame()
40
+ merged["moneyness_dist"] = abs(merged["strike"] - spot)
41
+ atm_indices = merged.groupby("maturity")["moneyness_dist"].idxmin()
42
+ return merged.loc[atm_indices]
43
+
44
+
45
+ def fit_rate_and_dividend(
46
+ calls: pd.DataFrame,
47
+ puts: pd.DataFrame,
48
+ spot: float,
49
+ r_fixed: float | None = None,
50
+ q_fixed: float | None = None,
51
+ ) -> tuple[float, float]:
52
+ """
53
+ Fits the risk-free rate (r) and dividend yield (q) from put-call parity.
54
+
55
+ This function uses the prices of at-the-money (ATM) call-put pairs to
56
+ solve for the `r` and `q` that minimize the parity pricing error.
57
+ Parameters can be held fixed or fitted.
58
+
59
+ Parameters
60
+ ----------
61
+ calls : pd.DataFrame
62
+ A DataFrame of call options with 'strike', 'maturity', 'marketPrice'.
63
+ puts : pd.DataFrame
64
+ A DataFrame of put options with 'strike', 'maturity', 'marketPrice'.
65
+ spot : float
66
+ The current spot price of the underlying.
67
+ r_fixed : float | None, optional
68
+ If provided, the risk-free rate is held fixed at this value.
69
+ Defaults to None.
70
+ q_fixed : float | None, optional
71
+ If provided, the dividend yield is held fixed at this value.
72
+ Defaults to None.
73
+
74
+ Returns
75
+ -------
76
+ tuple[float, float]
77
+ A tuple containing the estimated (or fixed) risk-free rate and dividend yield.
78
+ """
79
+ atm_pairs = find_atm_options(calls, puts, spot)
80
+ if atm_pairs.empty:
81
+ print("Warning: No ATM pairs found. Using default r=0.05, q=0.0.")
82
+ return 0.05, 0.0
83
+
84
+ free_param_indices, initial_guess, bounds = [], [], []
85
+ if r_fixed is None:
86
+ free_param_indices.append(0)
87
+ initial_guess.append(0.05)
88
+ bounds.append((0.0, 0.15))
89
+ if q_fixed is None:
90
+ free_param_indices.append(1)
91
+ initial_guess.append(0.01)
92
+ bounds.append((0.0, 0.10))
93
+
94
+ if not free_param_indices:
95
+ return r_fixed, q_fixed
96
+
97
+ def parity_error(x: np.ndarray) -> float:
98
+ r = r_fixed if 0 not in free_param_indices else x[free_param_indices.index(0)]
99
+ q = q_fixed if 1 not in free_param_indices else x[free_param_indices.index(1)]
100
+ parity_rhs = spot * np.exp(-q * atm_pairs["maturity"]) - atm_pairs[
101
+ "strike"
102
+ ] * np.exp(-r * atm_pairs["maturity"])
103
+ parity_lhs = atm_pairs["marketPrice_call"] - atm_pairs["marketPrice_put"]
104
+ return np.sum((parity_lhs - parity_rhs) ** 2)
105
+
106
+ solution = minimize(
107
+ fun=parity_error, x0=initial_guess, bounds=bounds, method="L-BFGS-B"
108
+ )
109
+ r_est = (
110
+ r_fixed
111
+ if 0 not in free_param_indices
112
+ else solution.x[free_param_indices.index(0)]
113
+ )
114
+ q_est = (
115
+ q_fixed
116
+ if 1 not in free_param_indices
117
+ else solution.x[free_param_indices.index(1)]
118
+ )
119
+ print(f"Fitted market params -> r: {r_est:.4f}, q: {q_est:.4f}")
120
+ return float(r_est), float(q_est)
@@ -0,0 +1,155 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+ from quantfin.atoms import Option, OptionType, Rate, Stock
7
+ from quantfin.models import BaseModel
8
+ from quantfin.techniques.base import BaseTechnique
9
+
10
+ from .vectorized_bsm_iv import BSMIVSolver
11
+
12
+ __doc__ = """
13
+ Defines the VolatilitySurface class, used to compute and store implied
14
+ volatility surfaces from both market and model prices.
15
+ """
16
+
17
+
18
+ class VolatilitySurface:
19
+ """
20
+ A class to compute and hold market and model-implied volatility surfaces.
21
+
22
+ This class takes a DataFrame of option market data and provides methods
23
+ to calculate the Black-Scholes implied volatility (IV) for each option,
24
+ either from its market price or from a price generated by a financial model.
25
+ """
26
+
27
+ def __init__(self, option_data: pd.DataFrame):
28
+ """
29
+ Initializes the VolatilitySurface.
30
+
31
+ Parameters
32
+ ----------
33
+ option_data : pd.DataFrame
34
+ A DataFrame containing market prices of options. Must include
35
+ 'strike', 'maturity', 'marketPrice', 'optionType', and 'expiry' columns.
36
+
37
+ Raises
38
+ ------
39
+ ValueError
40
+ If any of the required columns are missing from `option_data`.
41
+ """
42
+ required_cols = ["strike", "maturity", "marketPrice", "optionType", "expiry"]
43
+ if not all(col in option_data.columns for col in required_cols):
44
+ msg = (
45
+ "Input option_data is missing one "
46
+ f"of the required columns: {required_cols}"
47
+ )
48
+ raise ValueError(msg)
49
+
50
+ self.data = option_data[required_cols].copy()
51
+ self.surface: pd.DataFrame | None = None
52
+ self.iv_solver = BSMIVSolver()
53
+
54
+ def _calculate_ivs(
55
+ self,
56
+ stock: Stock,
57
+ rate: Rate,
58
+ prices_to_invert: pd.Series,
59
+ ) -> np.ndarray:
60
+ """Calculates IVs using the fast, vectorized BSM solver."""
61
+ ivs = self.iv_solver.solve(prices_to_invert.values, self.data, stock, rate)
62
+ return ivs
63
+
64
+ def calculate_market_iv(
65
+ self,
66
+ stock: Stock,
67
+ rate: Rate,
68
+ ) -> VolatilitySurface:
69
+ """
70
+ Calculates the market implied volatility surface from market prices.
71
+
72
+ This method inverts the Black-Scholes formula for each option's market
73
+ price to find the corresponding implied volatility. The results are
74
+ stored in the `self.surface` DataFrame.
75
+
76
+ Parameters
77
+ ----------
78
+ stock : Stock
79
+ The underlying asset's properties.
80
+ rate : Rate
81
+ The risk-free rate structure.
82
+
83
+ Returns
84
+ -------
85
+ VolatilitySurface
86
+ The same instance of the class, allowing for method chaining.
87
+ """
88
+ print("Calculating market implied volatility surface...")
89
+ market_ivs = self._calculate_ivs(stock, rate, self.data["marketPrice"])
90
+ self.surface = self.data.copy()
91
+ self.surface["iv"] = market_ivs
92
+ self.surface.dropna(inplace=True)
93
+ self.surface = self.surface[
94
+ (self.surface["iv"] > 1e-4) & (self.surface["iv"] < 2.0)
95
+ ]
96
+ return self
97
+
98
+ def calculate_model_iv(
99
+ self,
100
+ stock: Stock,
101
+ rate: Rate,
102
+ model: BaseModel,
103
+ technique: BaseTechnique,
104
+ ) -> VolatilitySurface:
105
+ """
106
+ Calculates a model's implied volatility surface.
107
+
108
+ This method first prices every option in the dataset using the provided
109
+ model and technique. It then inverts the Black-Scholes formula for each
110
+ of these model prices to generate the model-implied volatility surface.
111
+
112
+ Parameters
113
+ ----------
114
+ stock : Stock
115
+ The underlying asset's properties.
116
+ rate : Rate
117
+ The risk-free rate structure.
118
+ model : BaseModel
119
+ The financial model to generate prices from.
120
+ technique : BaseTechnique
121
+ The pricing technique to use with the model.
122
+
123
+ Returns
124
+ -------
125
+ VolatilitySurface
126
+ The same instance of the class, allowing for method chaining.
127
+ """
128
+ print(f"Calculating {model.name} implied volatility surface...")
129
+
130
+ model_prices = np.array(
131
+ [
132
+ technique.price(
133
+ Option(
134
+ strike=row.strike,
135
+ maturity=row.maturity,
136
+ option_type=OptionType.CALL
137
+ if row.optionType == "call"
138
+ else OptionType.PUT,
139
+ ),
140
+ stock,
141
+ model,
142
+ rate,
143
+ **model.params,
144
+ ).price
145
+ for _, row in self.data.iterrows()
146
+ ]
147
+ )
148
+
149
+ model_ivs = self._calculate_ivs(
150
+ stock, rate, pd.Series(model_prices, index=self.data.index)
151
+ )
152
+ self.surface = self.data.copy()
153
+ self.surface["iv"] = model_ivs
154
+ self.surface.dropna(inplace=True)
155
+ return self
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from quantfin.models.base import BaseModel
4
+ from quantfin.techniques.closed_form import ClosedFormTechnique
5
+ from quantfin.techniques.fft import FFTTechnique
6
+ from quantfin.techniques.monte_carlo import MonteCarloTechnique
7
+
8
+ __doc__ = """
9
+ Provides a utility function to select the most efficient pricing technique
10
+ for a given financial model based on its supported features.
11
+ """
12
+
13
+
14
+ def select_fastest_technique(model: BaseModel):
15
+ """
16
+ Selects the fastest available pricing technique for a given model.
17
+
18
+ The function checks the capabilities of the model in a specific order of
19
+ preference, which generally corresponds to computational speed.
20
+
21
+ The order of preference is:
22
+ 1. Closed-Form
23
+ 2. Fast Fourier Transform (FFT)
24
+ 3. Monte Carlo
25
+
26
+ Parameters
27
+ ----------
28
+ model : BaseModel
29
+ The financial model for which to select a technique.
30
+
31
+ Returns
32
+ -------
33
+ BaseTechnique
34
+ An instance of the fastest suitable pricing technique.
35
+
36
+ Raises
37
+ ------
38
+ TypeError
39
+ If no suitable pricing technique can be found for the model.
40
+ """
41
+ if model.has_closed_form:
42
+ return ClosedFormTechnique()
43
+ if model.supports_cf:
44
+ return FFTTechnique(n=12)
45
+ if model.supports_sde:
46
+ return MonteCarloTechnique(n_paths=5000, n_steps=50, antithetic=True)
47
+ raise TypeError(f"No suitable pricing technique found for model '{model.name}'")
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ from scipy.stats import norm
6
+
7
+ from quantfin.atoms import Rate, Stock
8
+
9
+ __doc__ = """
10
+ Defines a high-performance, vectorized solver for Black-Scholes-Merton
11
+ implied volatility.
12
+ """
13
+
14
+
15
+ class BSMIVSolver:
16
+ """
17
+ High-performance, vectorized Newton-Raphson solver for BSM implied volatility.
18
+
19
+ This solver is designed to calculate the implied volatility for a large
20
+ number of options simultaneously, leveraging NumPy for vectorized operations.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ max_iter: int = 20,
26
+ tolerance: float = 1e-6,
27
+ ):
28
+ """
29
+ Initializes the BSM implied volatility solver.
30
+
31
+ Parameters
32
+ ----------
33
+ max_iter : int, optional
34
+ The maximum number of iterations for the Newton-Raphson method,
35
+ by default 20.
36
+ tolerance : float, optional
37
+ The error tolerance to determine convergence, by default 1e-6.
38
+ """
39
+ self.max_iter = max_iter
40
+ self.tolerance = tolerance
41
+
42
+ def solve(
43
+ self,
44
+ target_prices: np.ndarray,
45
+ options: pd.DataFrame,
46
+ stock: Stock,
47
+ rate: Rate,
48
+ ) -> np.ndarray:
49
+ """
50
+ Calculates implied volatility for an array of options.
51
+
52
+ Parameters
53
+ ----------
54
+ target_prices : np.ndarray
55
+ An array of market prices for which to find the implied volatility.
56
+ options : pd.DataFrame
57
+ A DataFrame of option contracts, must include 'strike', 'maturity',
58
+ and 'optionType' columns.
59
+ stock : Stock
60
+ The underlying asset's properties.
61
+ rate : Rate
62
+ The risk-free rate structure.
63
+
64
+ Returns
65
+ -------
66
+ np.ndarray
67
+ An array of calculated implied volatilities corresponding to the
68
+ target prices.
69
+ """
70
+ S, q = stock.spot, stock.dividend
71
+ K, T = options["strike"].values, options["maturity"].values
72
+ r = rate.get_rate(T) # Use get_rate for term structure
73
+ is_call = options["optionType"].values == "call"
74
+ iv = np.full_like(target_prices, 0.20)
75
+
76
+ for _ in range(self.max_iter):
77
+ with np.errstate(all="ignore"):
78
+ sqrt_T = np.sqrt(T)
79
+ d1 = (np.log(S / K) + (r - q + 0.5 * iv**2) * T) / (iv * sqrt_T)
80
+ d2 = d1 - iv * sqrt_T
81
+ vega = S * np.exp(-q * T) * sqrt_T * norm.pdf(d1)
82
+ call_prices = S * np.exp(-q * T) * norm.cdf(d1) - K * np.exp(
83
+ -r * T
84
+ ) * norm.cdf(d2)
85
+ put_prices = K * np.exp(-r * T) * norm.cdf(-d2) - S * np.exp(
86
+ -q * T
87
+ ) * norm.cdf(-d1)
88
+ model_prices = np.where(is_call, call_prices, put_prices)
89
+ error = model_prices - target_prices
90
+ if np.all(np.abs(error) < self.tolerance):
91
+ break
92
+ iv = iv - error / np.maximum(vega, 1e-8)
93
+ return iv