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.
- optpricing-2.0.0.dist-info/METADATA +105 -0
- optpricing-2.0.0.dist-info/RECORD +92 -0
- optpricing-2.0.0.dist-info/WHEEL +5 -0
- optpricing-2.0.0.dist-info/entry_points.txt +3 -0
- optpricing-2.0.0.dist-info/licenses/LICENSE +9 -0
- optpricing-2.0.0.dist-info/top_level.txt +1 -0
- quantfin/__init__.py +101 -0
- quantfin/atoms/__init__.py +21 -0
- quantfin/atoms/bond.py +28 -0
- quantfin/atoms/option.py +70 -0
- quantfin/atoms/rate.py +46 -0
- quantfin/atoms/stock.py +39 -0
- quantfin/calibration/__init__.py +25 -0
- quantfin/calibration/calibrator.py +155 -0
- quantfin/calibration/fit_jump_parameters.py +66 -0
- quantfin/calibration/fit_market_params.py +120 -0
- quantfin/calibration/iv_surface.py +155 -0
- quantfin/calibration/technique_selector.py +47 -0
- quantfin/calibration/vectorized_bsm_iv.py +93 -0
- quantfin/calibration/vectorized_integration_iv.py +143 -0
- quantfin/cli.py +372 -0
- quantfin/config.py +57 -0
- quantfin/dashboard/__init__.py +14 -0
- quantfin/dashboard/app.py +26 -0
- quantfin/dashboard/pages/1_Calibration.py +57 -0
- quantfin/dashboard/pages/2_Pricer_and_Greeks.py +150 -0
- quantfin/dashboard/pages/3_Financial_Tools.py +69 -0
- quantfin/dashboard/pages/__init__.py +0 -0
- quantfin/dashboard/plots.py +157 -0
- quantfin/dashboard/service.py +176 -0
- quantfin/dashboard/widgets.py +87 -0
- quantfin/data/__init__.py +26 -0
- quantfin/data/historical_manager.py +86 -0
- quantfin/data/market_data_manager.py +236 -0
- quantfin/models/__init__.py +54 -0
- quantfin/models/base/__init__.py +16 -0
- quantfin/models/base/base_model.py +198 -0
- quantfin/models/base/validators.py +95 -0
- quantfin/models/bates.py +192 -0
- quantfin/models/blacks_approx.py +134 -0
- quantfin/models/bsm.py +262 -0
- quantfin/models/cev.py +109 -0
- quantfin/models/cgmy.py +189 -0
- quantfin/models/cir.py +90 -0
- quantfin/models/dupire_local.py +67 -0
- quantfin/models/heston.py +204 -0
- quantfin/models/hyperbolic.py +170 -0
- quantfin/models/kou.py +148 -0
- quantfin/models/merton_jump.py +225 -0
- quantfin/models/nig.py +180 -0
- quantfin/models/perpetual_put.py +115 -0
- quantfin/models/sabr.py +64 -0
- quantfin/models/sabr_jump.py +68 -0
- quantfin/models/vasicek.py +88 -0
- quantfin/models/vg.py +178 -0
- quantfin/parity/__init__.py +13 -0
- quantfin/parity/implied_rate.py +110 -0
- quantfin/parity/parity_model.py +178 -0
- quantfin/techniques/__init__.py +31 -0
- quantfin/techniques/base/__init__.py +21 -0
- quantfin/techniques/base/base_technique.py +54 -0
- quantfin/techniques/base/greek_mixin.py +265 -0
- quantfin/techniques/base/iv_mixin.py +116 -0
- quantfin/techniques/base/lattice_technique.py +129 -0
- quantfin/techniques/base/pricing_result.py +27 -0
- quantfin/techniques/base/random_utils.py +31 -0
- quantfin/techniques/closed_form.py +296 -0
- quantfin/techniques/crr.py +59 -0
- quantfin/techniques/fft.py +178 -0
- quantfin/techniques/integration.py +202 -0
- quantfin/techniques/kernels/__init__.py +37 -0
- quantfin/techniques/kernels/lattice_kernels.py +286 -0
- quantfin/techniques/kernels/mc_kernels.py +337 -0
- quantfin/techniques/leisen_reimer.py +59 -0
- quantfin/techniques/monte_carlo.py +259 -0
- quantfin/techniques/pde.py +155 -0
- quantfin/techniques/topm.py +59 -0
- quantfin/workflows/__init__.py +14 -0
- quantfin/workflows/backtest_workflow.py +133 -0
- quantfin/workflows/configs/__init__.py +51 -0
- quantfin/workflows/configs/bates_config.py +25 -0
- quantfin/workflows/configs/bsm_config.py +9 -0
- quantfin/workflows/configs/cev_config.py +16 -0
- quantfin/workflows/configs/cgmy_config.py +18 -0
- quantfin/workflows/configs/heston_config.py +21 -0
- quantfin/workflows/configs/hyperbolic_config.py +18 -0
- quantfin/workflows/configs/kou_config.py +16 -0
- quantfin/workflows/configs/merton_config.py +18 -0
- quantfin/workflows/configs/nig_config.py +13 -0
- quantfin/workflows/configs/sabr_config.py +19 -0
- quantfin/workflows/configs/vg_config.py +9 -0
- 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
|
+
)
|