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,86 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ import yfinance as yf
6
+
7
+ from quantfin.config import HISTORICAL_DIR
8
+
9
+
10
+ def save_historical_returns(
11
+ tickers: list[str],
12
+ period: str = "10y",
13
+ ):
14
+ """
15
+ Fetches and saves historical log returns for a list of tickers.
16
+
17
+ This function iterates through a list of stock tickers, fetches historical
18
+ price data from yfinance for the specified period, calculates the daily
19
+ log returns, and saves them to a parquet file in the historical data directory.
20
+
21
+ Parameters
22
+ ----------
23
+ tickers : list[str]
24
+ A list of stock tickers to process, e.g., ['SPY', 'AAPL'].
25
+ period : str, optional
26
+ The time period for which to fetch data, e.g., "10y", "5y", "1mo".
27
+ Defaults to "10y".
28
+ """
29
+ print(f"--- Saving {period} Historical Returns ---")
30
+ for ticker in tickers:
31
+ try:
32
+ print(f"Fetching data for {ticker}...")
33
+ data = yf.Ticker(ticker).history(period=period)
34
+
35
+ if data.empty:
36
+ print(f" -> No data found for {ticker}. Skipping.")
37
+ continue
38
+
39
+ log_returns = np.log(data["Close"] / data["Close"].shift(1)).dropna()
40
+ filename = HISTORICAL_DIR / f"{ticker}_{period}_returns.parquet"
41
+ log_returns.to_frame(name="log_return").to_parquet(filename)
42
+
43
+ print(f" -> Saved to {filename}")
44
+
45
+ except Exception as e:
46
+ print(f" -> FAILED to save data for {ticker}. Error: {e}")
47
+
48
+
49
+ def load_historical_returns(
50
+ ticker: str,
51
+ period: str = "10y",
52
+ ) -> pd.Series:
53
+ """
54
+ Loads historical log returns, fetching and saving them if not found.
55
+
56
+ Checks for a pre-saved parquet file for the given ticker and period. If
57
+ the file does not exist, it calls `save_historical_returns` to download
58
+ and save it first.
59
+
60
+ Parameters
61
+ ----------
62
+ ticker : str
63
+ The stock ticker for which to load returns, e.g., 'SPY'.
64
+ period : str, optional
65
+ The time period for which to fetch data, e.g., "10y", "5y", "1mo".
66
+ Defaults to "10y".
67
+
68
+ Returns
69
+ -------
70
+ pd.Series
71
+ A pandas Series containing the historical log returns.
72
+
73
+ Raises
74
+ ------
75
+ FileNotFoundError
76
+ If the data file cannot be found and also fails to be downloaded.
77
+ """
78
+ filename = HISTORICAL_DIR / f"{ticker}_{period}_returns.parquet"
79
+ if not filename.exists():
80
+ print(f"No historical data found for {ticker}. Fetching and saving now...")
81
+ save_historical_returns([ticker], period)
82
+
83
+ if not filename.exists():
84
+ raise FileNotFoundError(f"Could not find or save historical data for {ticker}.")
85
+
86
+ return pd.read_parquet(filename)["log_return"]
@@ -0,0 +1,236 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date, datetime
4
+
5
+ import pandas as pd
6
+ import yfinance as yf
7
+ from polygon import RESTClient
8
+
9
+ from quantfin.config import MARKET_SNAPSHOT_DIR, POLYGON_API_KEY, _config
10
+
11
+
12
+ def _fetch_from_yfinance(ticker: str) -> pd.DataFrame | None:
13
+ """Fetches a live option chain using yfinance."""
14
+ print(f"Fetching live option chain for {ticker} from yfinance...")
15
+ try:
16
+ ticker_obj = yf.Ticker(ticker)
17
+ expirations = ticker_obj.options
18
+ if not expirations:
19
+ return None
20
+
21
+ all_options = []
22
+ for expiry in expirations:
23
+ opt = ticker_obj.option_chain(expiry)
24
+ if not opt.calls.empty:
25
+ opt.calls["optionType"] = "call"
26
+ opt.calls["expiry"] = pd.to_datetime(expiry)
27
+ all_options.append(opt.calls)
28
+ if not opt.puts.empty:
29
+ opt.puts["optionType"] = "put"
30
+ opt.puts["expiry"] = pd.to_datetime(expiry)
31
+ all_options.append(opt.puts)
32
+
33
+ if not all_options:
34
+ return None
35
+
36
+ chain_df = pd.concat(all_options, ignore_index=True)
37
+ today_date = datetime.combine(date.today(), datetime.min.time())
38
+ chain_df["maturity"] = (chain_df["expiry"] - today_date).dt.days / 365.25
39
+ chain_df["last_price"] = (chain_df["bid"] + chain_df["ask"]) / 2.0
40
+ chain_df["spot_price"] = ticker_obj.fast_info.get(
41
+ "last_price", ticker_obj.history("1d")["Close"].iloc[0]
42
+ )
43
+
44
+ chain_df.dropna(
45
+ subset=["last_price", "strike", "maturity", "impliedVolatility"],
46
+ inplace=True,
47
+ )
48
+ chain_df = chain_df[
49
+ (chain_df["last_price"] > 0.01) & (chain_df["maturity"] > 1 / 365.25)
50
+ ].copy()
51
+ return chain_df
52
+
53
+ except Exception as e:
54
+ print(f" -> FAILED to fetch live yfinance data for {ticker}. Error: {e}")
55
+ return None
56
+
57
+
58
+ def _fetch_from_polygon(ticker: str) -> pd.DataFrame | None:
59
+ """Fetches a live option chain using Polygon.io."""
60
+ print(f"Fetching live option chain for {ticker} from Polygon.io...")
61
+ if not POLYGON_API_KEY or "YOUR_KEY" in POLYGON_API_KEY:
62
+ print("Error: Polygon API key not set in config.yaml or environment variables.")
63
+ return None
64
+
65
+ client = RESTClient(POLYGON_API_KEY)
66
+ try:
67
+ # limited implementation for a free account
68
+ # gets the chain for the next expiration date.
69
+ expirations = list(
70
+ client.list_options_contracts(
71
+ underlying_ticker=ticker, expired=False, limit=1
72
+ )
73
+ )
74
+ if not expirations:
75
+ return None
76
+
77
+ expiry_date = expirations[0].expiration_date
78
+ contracts = list(
79
+ client.list_options_contracts(
80
+ underlying_ticker=ticker, expiration_date=expiry_date, limit=1000
81
+ )
82
+ )
83
+ if not contracts:
84
+ return None
85
+
86
+ # A single snapshot call
87
+ snapshot = client.get_options_chain(
88
+ underlying_ticker=ticker, expiration_date=expiry_date
89
+ )
90
+
91
+ data = []
92
+ for contract_details in snapshot.results:
93
+ greeks = contract_details.greeks
94
+ data.append(
95
+ {
96
+ "ticker": contract_details.ticker,
97
+ "underlying": ticker,
98
+ "strike": contract_details.details.strike_price,
99
+ "expiry": pd.to_datetime(contract_details.details.expiration_date),
100
+ "optionType": "call"
101
+ if contract_details.details.contract_type == "call"
102
+ else "put",
103
+ "spot_price": greeks.underlying_price,
104
+ "last_price": contract_details.last_quote.bid,
105
+ "impliedVolatility": greeks.implied_volatility,
106
+ "volume": contract_details.day.volume,
107
+ "open_interest": contract_details.open_interest,
108
+ }
109
+ )
110
+
111
+ df = pd.DataFrame(data)
112
+ df["maturity"] = (df["expiry"] - pd.Timestamp.now()).dt.days / 365.25
113
+ return df
114
+
115
+ except Exception as e:
116
+ print(f"An error occurred while fetching live Polygon data: {e}")
117
+ return None
118
+
119
+
120
+ def get_live_option_chain(ticker: str) -> pd.DataFrame | None:
121
+ """
122
+ Fetches a live option chain from the configured data provider.
123
+
124
+ The data provider is determined by the `live_data_provider` key in the
125
+ `config.yaml` file. Supported providers are "yfinance" and "polygon".
126
+
127
+ Parameters
128
+ ----------
129
+ ticker : str
130
+ The stock ticker for which to fetch the option chain, e.g., 'SPY'.
131
+
132
+ Returns
133
+ -------
134
+ pd.DataFrame | None
135
+ A DataFrame containing the formatted option chain data, or None if
136
+ the fetch fails or no data is returned.
137
+ """
138
+ provider = _config.get("live_data_provider", "yfinance").lower()
139
+ if provider == "polygon":
140
+ return _fetch_from_polygon(ticker)
141
+
142
+ elif provider == "yfinance":
143
+ return _fetch_from_yfinance(ticker)
144
+
145
+ else:
146
+ print(
147
+ f"Warning: Unknown live_data_provider '{provider}'. Defaulting to yfinance."
148
+ )
149
+ return _fetch_from_yfinance(ticker)
150
+
151
+
152
+ def save_market_snapshot(tickers: list[str]):
153
+ """
154
+ Saves a snapshot of the current market option chain for given tickers.
155
+
156
+ For each ticker, it fetches the live option chain using
157
+ `get_live_option_chain` and saves it to a parquet file named with the
158
+ ticker and the current date.
159
+
160
+ Parameters
161
+ ----------
162
+ tickers : list[str]
163
+ A list of stock tickers to process, e.g., ['SPY', 'AAPL'].
164
+ """
165
+ today_str = date.today().strftime("%Y-%m-%d")
166
+
167
+ print(f"--- Saving Market Data Snapshot for {today_str} ---")
168
+ for ticker in tickers:
169
+ chain_df = get_live_option_chain(ticker)
170
+
171
+ if chain_df is None or chain_df.empty:
172
+ print(f" -> No valid option data found for {ticker}. Skipping.")
173
+ continue
174
+
175
+ filename = MARKET_SNAPSHOT_DIR / f"{ticker}_{today_str}.parquet"
176
+ chain_df.to_parquet(filename)
177
+ print(f" -> Successfully saved {len(chain_df)} options to {filename}")
178
+
179
+
180
+ def load_market_snapshot(ticker: str, snapshot_date: str) -> pd.DataFrame | None:
181
+ """
182
+ Loads a previously saved market data snapshot for a specific date.
183
+
184
+ Parameters
185
+ ----------
186
+ ticker : str
187
+ The stock ticker of the desired snapshot, e.g., 'SPY'.
188
+ snapshot_date : str
189
+ The date of the snapshot in 'YYYY-MM-DD' format.
190
+
191
+ Returns
192
+ -------
193
+ pd.DataFrame | None
194
+ A DataFrame containing the snapshot data, or None if the file
195
+ is not found.
196
+ """
197
+ filename = MARKET_SNAPSHOT_DIR / f"{ticker}_{snapshot_date}.parquet"
198
+ if not filename.exists():
199
+ print(f"Error: Snapshot file not found: {filename}")
200
+ return None
201
+
202
+ print(f"Loading data from {filename}...")
203
+ return pd.read_parquet(filename)
204
+
205
+
206
+ def get_available_snapshot_dates(ticker: str) -> list[str]:
207
+ """
208
+ Lists all available snapshot dates for a given ticker.
209
+
210
+ Scans the market data directory for saved parquet files corresponding
211
+ to the ticker and extracts the date from the filenames.
212
+
213
+ Parameters
214
+ ----------
215
+ ticker : str
216
+ The stock ticker to search for, e.g., 'SPY'.
217
+
218
+ Returns
219
+ -------
220
+ list[str]
221
+ A sorted list of available dates in 'YYYY-MM-DD' format, from
222
+ most recent to oldest.
223
+ """
224
+ try:
225
+ files = [
226
+ f.name
227
+ for f in MARKET_SNAPSHOT_DIR.iterdir()
228
+ if f.name.startswith(f"{ticker}_") and f.name.endswith(".parquet")
229
+ ]
230
+ return sorted(
231
+ [f.replace(f"{ticker}_", "").replace(".parquet", "") for f in files],
232
+ reverse=True,
233
+ )
234
+
235
+ except FileNotFoundError:
236
+ return []
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ __doc__ = """
4
+ The `models` package contains all financial models for option pricing.
5
+
6
+ It provides the abstract `BaseModel` and a suite of concrete implementations,
7
+ from the standard Black-Scholes-Merton to advanced stochastic volatility
8
+ and jump-diffusion models.
9
+ """
10
+
11
+ from .base import CF, BaseModel, ParamValidator, PDECoeffs
12
+ from .bates import BatesModel
13
+ from .blacks_approx import BlacksApproxModel
14
+ from .bsm import BSMModel
15
+ from .cev import CEVModel
16
+ from .cgmy import CGMYModel
17
+ from .cir import CIRModel
18
+ from .dupire_local import DupireLocalVolModel
19
+ from .heston import HestonModel
20
+ from .hyperbolic import HyperbolicModel
21
+ from .kou import KouModel
22
+ from .merton_jump import MertonJumpModel
23
+ from .nig import NIGModel
24
+ from .perpetual_put import PerpetualPutModel
25
+ from .sabr import SABRModel
26
+ from .sabr_jump import SABRJumpModel
27
+ from .vasicek import VasicekModel
28
+ from .vg import VarianceGammaModel
29
+
30
+ __all__ = [
31
+ # Base Components
32
+ "BaseModel",
33
+ "CF",
34
+ "PDECoeffs",
35
+ "ParamValidator",
36
+ # Concrete Models
37
+ "BatesModel",
38
+ "BlacksApproxModel",
39
+ "BSMModel",
40
+ "CEVModel",
41
+ "CGMYModel",
42
+ "CIRModel",
43
+ "DupireLocalVolModel",
44
+ "HestonModel",
45
+ "HyperbolicModel",
46
+ "KouModel",
47
+ "MertonJumpModel",
48
+ "NIGModel",
49
+ "PerpetualPutModel",
50
+ "SABRJumpModel",
51
+ "SABRModel",
52
+ "VarianceGammaModel",
53
+ "VasicekModel",
54
+ ]
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ __doc__ = """
4
+ This module provides the foundational abstract classes and utilities for all
5
+ financial models in the library, including the abstract `BaseModel` and
6
+ parameter validation tools.
7
+ """
8
+ from .base_model import CF, BaseModel, PDECoeffs
9
+ from .validators import ParamValidator
10
+
11
+ __all__ = [
12
+ "BaseModel",
13
+ "CF",
14
+ "PDECoeffs",
15
+ "ParamValidator",
16
+ ]
@@ -0,0 +1,198 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Callable
5
+ from typing import Any
6
+
7
+ import numpy as np
8
+
9
+ __doc__ = """
10
+ Defines the abstract base class for all option pricing models.
11
+ """
12
+
13
+ CF = Callable[[np.ndarray], np.ndarray]
14
+ PDECoeffs = Callable[[np.ndarray, float], tuple[np.ndarray, np.ndarray, np.ndarray]]
15
+
16
+
17
+ class BaseModel(ABC):
18
+ """
19
+ Abstract base class for all financial pricing models.
20
+
21
+ This class defines a common interface for all models, including parameter
22
+ validation, metadata flags for supported features (e.g., characteristic
23
+ function, SDE), and methods for creating modified model instances.
24
+
25
+ Attributes
26
+ ----------
27
+ name : str
28
+ A string identifier for the model (e.g., "Black-Scholes-Merton").
29
+ params : dict[str, float]
30
+ A dictionary holding the model's parameters.
31
+ supports_cf : bool
32
+ Flag indicating if the model implements a characteristic function.
33
+ supports_sde : bool
34
+ Flag indicating if the model implements an SDE simulation path.
35
+ supports_pde : bool
36
+ Flag indicating if the model provides PDE coefficients.
37
+ has_closed_form : bool
38
+ Flag indicating if a closed-form solution is available.
39
+ has_variance_process : bool
40
+ Flag for stochastic volatility models (e.g., Heston, SABR).
41
+ is_pure_levy : bool
42
+ Flag for pure Levy models where the terminal value can be sampled directly.
43
+ has_jumps : bool
44
+ Flag for models that include a jump component.
45
+ """
46
+
47
+ name: str = "BaseModel"
48
+ params: dict[str, float]
49
+ supports_cf: bool = False
50
+ supports_sde: bool = False
51
+ supports_pde: bool = False
52
+ has_closed_form: bool = False
53
+ has_variance_process: bool = False
54
+ is_pure_levy: bool = False
55
+ has_jumps: bool = False
56
+
57
+ # TODO: Rename cf_kwargs to something more specific like `pricing_args`
58
+ # as it's used by more than just the characteristic function.
59
+ cf_kwargs: tuple[str, ...] = ()
60
+
61
+ __slots__ = ("params",)
62
+
63
+ def __init__(self, params: dict[str, float]) -> None:
64
+ """
65
+ Initializes the model and validates its parameters.
66
+
67
+ Parameters
68
+ ----------
69
+ params : dict[str, float]
70
+ A dictionary of parameter names to values for the model.
71
+ """
72
+ self.params = params
73
+ self._validate_params()
74
+
75
+ @abstractmethod
76
+ def _validate_params(self) -> None:
77
+ """
78
+ Abstract method for subclasses to validate their specific parameters.
79
+
80
+ This method should be implemented by all concrete model classes to ensure
81
+ that the provided `self.params` dictionary contains all required keys
82
+ and that their values are valid.
83
+ """
84
+ raise NotImplementedError
85
+
86
+ def cf(self, **kwargs: Any) -> CF:
87
+ """
88
+ Return the characteristic function of the log-price process.
89
+
90
+ Raises
91
+ ------
92
+ NotImplementedError
93
+ If the model does not support a characteristic function.
94
+ """
95
+ if not self.supports_cf:
96
+ raise NotImplementedError(
97
+ f"{self.name} does not support characteristic functions."
98
+ )
99
+ return self._cf_impl(**kwargs)
100
+
101
+ @abstractmethod
102
+ def _cf_impl(self, **kwargs: Any) -> CF:
103
+ """Internal implementation of the characteristic function."""
104
+ ...
105
+
106
+ def get_sde_sampler(self, **kwargs: Any) -> Callable:
107
+ """
108
+ Return a function that can be used to sample paths from the model's SDE.
109
+
110
+ Raises
111
+ ------
112
+ NotImplementedError
113
+ If the model does not support SDE sampling.
114
+ """
115
+ if not self.supports_sde:
116
+ raise NotImplementedError(f"{self.name} does not support SDE sampling.")
117
+ return self._sde_impl(**kwargs)
118
+
119
+ @abstractmethod
120
+ def _sde_impl(self, **kwargs: Any) -> Callable:
121
+ """Internal implementation for returning an SDE sampler."""
122
+ ...
123
+
124
+ def get_pde_coeffs(self, **kwargs: Any) -> PDECoeffs:
125
+ """
126
+ Return the coefficients for the pricing PDE.
127
+
128
+ Raises
129
+ ------
130
+ NotImplementedError
131
+ If the model does not support PDE solving.
132
+ """
133
+ if not self.supports_pde:
134
+ raise NotImplementedError(f"{self.name} does not support PDE solving.")
135
+ return self._pde_impl(**kwargs)
136
+
137
+ @abstractmethod
138
+ def _pde_impl(self, **kwargs: Any) -> PDECoeffs:
139
+ """Internal implementation for returning PDE coefficients."""
140
+ ...
141
+
142
+ def price_closed_form(self, *args, **kwargs) -> float:
143
+ """
144
+ Compute the option price using a closed-form solution, if available.
145
+
146
+ Raises
147
+ ------
148
+ NotImplementedError
149
+ If the model does not have a closed-form solution.
150
+ """
151
+ if not self.has_closed_form:
152
+ raise NotImplementedError(
153
+ f"{self.name} does not have a closed-form solution."
154
+ )
155
+ return self._closed_form_impl(*args, **kwargs)
156
+
157
+ @abstractmethod
158
+ def _closed_form_impl(self, *args, **kwargs) -> float:
159
+ """Internal implementation of the closed-form pricing formula."""
160
+ ...
161
+
162
+ def with_params(self, **updated_params: float) -> BaseModel:
163
+ """
164
+ Create a new model instance with updated parameters.
165
+
166
+ This is useful for calibration and sensitivity analysis.
167
+
168
+ Parameters
169
+ ----------
170
+ **updated_params : float
171
+ Keyword arguments for the parameters to update.
172
+
173
+ Returns
174
+ -------
175
+ BaseModel
176
+ A new instance of the model with the updated parameters.
177
+ """
178
+ new_params = {**self.params, **updated_params}
179
+ return self.__class__(params=new_params)
180
+
181
+ def __repr__(self) -> str:
182
+ """Provide a formal string representation of the model."""
183
+ param_str = ", ".join(f"{k}={v:.4f}" for k, v in self.params.items())
184
+ return f"{self.__class__.__name__}({param_str})"
185
+
186
+ def __eq__(self, other: object) -> bool:
187
+ """
188
+ Check for equality based on class type and parameters.
189
+ """
190
+ if not isinstance(other, self.__class__):
191
+ return NotImplemented
192
+ return self.params == other.params
193
+
194
+ def __hash__(self) -> int:
195
+ """
196
+ Provide a hash for the model, making it usable in sets and dict keys.
197
+ """
198
+ return hash((self.__class__, tuple(sorted(self.params.items()))))
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ __doc__ = """
4
+ Provides a utility class for validating parameters passed to financial models.
5
+ """
6
+
7
+
8
+ class ParamValidator:
9
+ """
10
+ A utility class containing static methods for model parameter validation.
11
+
12
+ This class is not meant to be instantiated. It serves as a namespace
13
+ for common validation logic used by `BaseModel` subclasses.
14
+ """
15
+
16
+ @staticmethod
17
+ def require(params: dict[str, float], required: list[str], *, model: str) -> None:
18
+ """
19
+ Check for the presence of required parameters.
20
+
21
+ Parameters
22
+ ----------
23
+ params : dict[str, float]
24
+ The dictionary of parameters to validate.
25
+ required : list[str]
26
+ A list of parameter names that must be present in `params`.
27
+ model : str
28
+ The name of the model performing the validation, for error messages.
29
+
30
+ Raises
31
+ ------
32
+ ValueError
33
+ If any of the required parameters are missing.
34
+ """
35
+ missing = [k for k in required if k not in params]
36
+ if missing:
37
+ raise ValueError(
38
+ f"{model}: missing required parameters: {', '.join(missing)}"
39
+ )
40
+
41
+ @staticmethod
42
+ def positive(params: dict[str, float], keys: list[str], *, model: str) -> None:
43
+ """
44
+ Check if specified parameters are strictly positive.
45
+
46
+ Parameters
47
+ ----------
48
+ params : dict[str, float]
49
+ The dictionary of parameters to validate.
50
+ keys : list[str]
51
+ A list of parameter names that must be positive.
52
+ model : str
53
+ The name of the model performing the validation, for error messages.
54
+
55
+ Raises
56
+ ------
57
+ ValueError
58
+ If any of the specified parameters are not strictly positive.
59
+ """
60
+ nonpos = [k for k in keys if params.get(k, 0.0) <= 0.0]
61
+ if nonpos:
62
+ raise ValueError(
63
+ f"{model}: parameters must be positive: {', '.join(nonpos)}"
64
+ )
65
+
66
+ @staticmethod
67
+ def bounded(
68
+ params: dict[str, float], key: str, low: float, high: float, *, model: str
69
+ ) -> None:
70
+ """
71
+ Check if a parameter is within a specified inclusive range.
72
+
73
+ Parameters
74
+ ----------
75
+ params : dict[str, float]
76
+ The dictionary of parameters to validate.
77
+ key : str
78
+ The name of the parameter to check.
79
+ low : float
80
+ The lower bound of the valid range.
81
+ high : float
82
+ The upper bound of the valid range.
83
+ model : str
84
+ The name of the model performing the validation, for error messages.
85
+
86
+ Raises
87
+ ------
88
+ ValueError
89
+ If the parameter is outside the [low, high] range.
90
+ """
91
+ val = params.get(key)
92
+ if val is None or not (low <= val <= high):
93
+ raise ValueError(
94
+ f"{model}: parameter '{key}' must be in [{low}, {high}], got {val}"
95
+ )