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,143 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from scipy import integrate
8
+
9
+ from quantfin.models import BSMModel
10
+
11
+ if TYPE_CHECKING:
12
+ from quantfin.atoms import Rate
13
+ from quantfin.models import BaseModel
14
+
15
+
16
+ __doc__ = """
17
+ Defines a high-performance, vectorized solver for implied volatility using
18
+ numerical integration of the characteristic function.
19
+ """
20
+
21
+
22
+ class VectorizedIntegrationIVSolver:
23
+ """
24
+ A high-performance, vectorized Secant method solver for implied volatility
25
+ for any model that supports a characteristic function.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ max_iter: int = 20,
31
+ tolerance: float = 1e-7,
32
+ upper_bound: float = 200.0,
33
+ ):
34
+ """
35
+ Initializes the vectorized IV solver.
36
+
37
+ Parameters
38
+ ----------
39
+ max_iter : int, optional
40
+ Maximum number of iterations for the Secant method, by default 20.
41
+ tolerance : float, optional
42
+ Error tolerance for convergence, by default 1e-7.
43
+ upper_bound : float, optional
44
+ The upper limit for the numerical integration, by default 200.0.
45
+ """
46
+ self.max_iter = max_iter
47
+ self.tolerance = tolerance
48
+ self.upper_bound = upper_bound
49
+
50
+ def solve(
51
+ self,
52
+ target_prices: np.ndarray,
53
+ options: pd.DataFrame,
54
+ model: BaseModel,
55
+ rate: Rate,
56
+ ) -> np.ndarray:
57
+ """
58
+ Calculates implied volatility for an array of options and prices.
59
+
60
+ This method uses a vectorized Secant root-finding algorithm. The pricing
61
+ at each step is performed using a vectorized version of the Gil-Pelaez
62
+ inversion formula.
63
+
64
+ Parameters
65
+ ----------
66
+ target_prices : np.ndarray
67
+ An array of market prices for which to find the implied volatility.
68
+ options : pd.DataFrame
69
+ A DataFrame of option contracts.
70
+ model : BaseModel
71
+ The financial model to use for pricing.
72
+ rate : Rate
73
+ The risk-free rate structure.
74
+
75
+ Returns
76
+ -------
77
+ np.ndarray
78
+ An array of calculated implied volatilities.
79
+ """
80
+ iv0 = np.full_like(target_prices, 0.20)
81
+ iv1 = np.full_like(target_prices, 0.25)
82
+
83
+ p0 = self._price_vectorized(iv0, options, model, rate)
84
+ p1 = self._price_vectorized(iv1, options, model, rate)
85
+
86
+ f0 = p0 - target_prices
87
+ f1 = p1 - target_prices
88
+
89
+ for _ in range(self.max_iter):
90
+ if np.all(np.abs(f1) < self.tolerance):
91
+ break
92
+ denom = f1 - f0
93
+ denom[np.abs(denom) < 1e-12] = 1e-12
94
+ iv_next = iv1 - f1 * (iv1 - iv0) / denom
95
+ iv_next = np.clip(iv_next, 1e-4, 5.0)
96
+ iv0, iv1 = iv1, iv_next
97
+ f0 = f1
98
+ p1 = self._price_vectorized(iv1, options, model, rate)
99
+ f1 = p1 - target_prices
100
+
101
+ return iv1
102
+
103
+ def _price_vectorized(
104
+ self,
105
+ iv_vector: np.ndarray,
106
+ options: pd.DataFrame,
107
+ model: BaseModel,
108
+ rate: Rate,
109
+ ) -> np.ndarray:
110
+ """
111
+ Internal method to price a vector of options for a given vector of volatilities.
112
+ """
113
+ bsm_model = BSMModel(params={"sigma": 0.2})
114
+ prices = np.zeros_like(iv_vector)
115
+
116
+ for T, group in options.groupby("maturity"):
117
+ idx = group.index
118
+ S, K = group["spot"].values, group["strike"].values
119
+ q, r = group["dividend"].values, rate.get_rate(T)
120
+ is_call = group["optionType"].values == "call"
121
+
122
+ bsm_model.params["sigma"] = iv_vector[idx]
123
+ phi = bsm_model.cf(t=T, spot=S, r=r, q=q)
124
+ k_log = np.log(K)
125
+
126
+ integrand_p2 = lambda u: (np.exp(-1j * u * k_log) * phi(u)).imag / u
127
+ integrand_p1 = lambda u: (np.exp(-1j * u * k_log) * phi(u - 1j)).imag / u
128
+
129
+ integral_p2, _ = integrate.quad_vec(integrand_p2, 1e-15, self.upper_bound)
130
+
131
+ phi_minus_i = phi(-1j)
132
+ phi_minus_i[np.abs(phi_minus_i) < 1e-12] = 1.0
133
+ integral_p1, _ = integrate.quad_vec(integrand_p1, 1e-15, self.upper_bound)
134
+
135
+ P1 = 0.5 + integral_p1 / (np.pi * np.real(phi_minus_i))
136
+ P2 = 0.5 + integral_p2 / np.pi
137
+
138
+ call_prices = S * np.exp(-q * T) * P1 - K * np.exp(-r * T) * P2
139
+ put_prices = K * np.exp(-r * T) * (1 - P2) - S * np.exp(-q * T) * (1 - P1)
140
+
141
+ prices[idx] = np.where(is_call, call_prices, put_prices)
142
+
143
+ return prices
quantfin/cli.py ADDED
@@ -0,0 +1,372 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import subprocess
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from quantfin.config import PROJECT_ROOT, _config
11
+ from quantfin.data import (
12
+ get_available_snapshot_dates,
13
+ load_market_snapshot,
14
+ save_historical_returns,
15
+ save_market_snapshot,
16
+ )
17
+ from quantfin.workflows import BacktestWorkflow, DailyWorkflow
18
+ from quantfin.workflows.configs import ALL_MODEL_CONFIGS
19
+
20
+ __doc__ = """
21
+ This module provides the command-line interface (CLI) for the quantfin library.
22
+
23
+ It uses the Typer library to create a user-friendly interface for running
24
+ calibrations, backtests, data management tasks, and launching the dashboard.
25
+ """
26
+
27
+ # Create the main Typer application
28
+ app = typer.Typer(
29
+ name="optPricing",
30
+ help="A quantitative finance library for option pricing and analysis.",
31
+ add_completion=False,
32
+ )
33
+
34
+ # Create a subcommand for data-related tasks
35
+ data_app = typer.Typer(name="data", help="Tools for downloading and managing data.")
36
+ app.add_typer(data_app)
37
+
38
+
39
+ # Utility function for logging setup
40
+ def setup_logging(verbose: bool):
41
+ """
42
+ Configures the root logger based on the verbosity flag.
43
+ """
44
+ level = logging.DEBUG if verbose else logging.INFO
45
+ logging.basicConfig(
46
+ level=level,
47
+ format="%(asctime)s - %(levelname)s - %(message)s",
48
+ )
49
+
50
+
51
+ # CLI Commands
52
+
53
+
54
+ @app.command()
55
+ def dashboard():
56
+ """
57
+ Launches the Streamlit dashboard application.
58
+ """
59
+ # Note: We point directly to the source file now.
60
+ app_path = PROJECT_ROOT / "src" / "quantfin" / "dashboard" / "app.py"
61
+ typer.echo(f"Launching Streamlit dashboard from: {app_path}")
62
+
63
+ if not app_path.exists():
64
+ typer.secho(
65
+ f"Error: Dashboard entry point not found at '{app_path}'.",
66
+ fg=typer.colors.RED,
67
+ )
68
+ raise typer.Exit(code=1)
69
+
70
+ try:
71
+ subprocess.run(
72
+ ["streamlit", "run", str(app_path)],
73
+ check=True,
74
+ )
75
+ except FileNotFoundError:
76
+ typer.secho(
77
+ "Error: 'streamlit' command not found; install: "
78
+ "'pip install optPricing[app]'",
79
+ fg=typer.colors.RED,
80
+ )
81
+ raise typer.Exit(code=1)
82
+
83
+
84
+ @data_app.command(name="download")
85
+ def download_data(
86
+ tickers: Annotated[
87
+ list[str] | None,
88
+ typer.Option(
89
+ "--ticker",
90
+ "-t",
91
+ help="Stock ticker to download. Can be used multiple times.",
92
+ ),
93
+ ] = None,
94
+ all_default: Annotated[
95
+ bool,
96
+ typer.Option(
97
+ "--all", help="Download all default tickers specified in config.yaml."
98
+ ),
99
+ ] = False,
100
+ period: Annotated[
101
+ str,
102
+ typer.Option(
103
+ "--period",
104
+ "-p",
105
+ help="Time period for historical data (e.g., '10y', '5y').",
106
+ ),
107
+ ] = "10y",
108
+ ):
109
+ """
110
+ Downloads and saves historical log returns for specified tickers or all defaults.
111
+ """
112
+ if all_default:
113
+ tickers_to_download = _config.get("default_tickers", [])
114
+ if not tickers_to_download:
115
+ typer.secho(
116
+ "Error: --all flag used, but no 'default_tickers' found "
117
+ "in config.yaml.",
118
+ fg=typer.colors.RED,
119
+ )
120
+ raise typer.Exit(code=1)
121
+ typer.echo(f"Downloading all default tickers for period {period}...")
122
+ elif tickers:
123
+ tickers_to_download = tickers
124
+ typer.echo(
125
+ f"Downloading {period} historical data for tickers: "
126
+ f"{', '.join(tickers_to_download)}"
127
+ )
128
+ else:
129
+ typer.secho(
130
+ "Error: Please provide at least one --ticker or use the --all flag.",
131
+ fg=typer.colors.RED,
132
+ )
133
+ raise typer.Exit(code=1)
134
+
135
+ save_historical_returns(tickers_to_download, period=period)
136
+ typer.secho("Download complete.", fg=typer.colors.GREEN)
137
+
138
+
139
+ @app.command()
140
+ def calibrate(
141
+ ticker: Annotated[
142
+ str,
143
+ typer.Option("--ticker", "-t", help="The stock ticker to calibrate against."),
144
+ ],
145
+ model: Annotated[
146
+ list[str],
147
+ typer.Option(
148
+ "--model", "-m", help="Model to calibrate. Can be used multiple times."
149
+ ),
150
+ ],
151
+ date: Annotated[
152
+ str | None,
153
+ typer.Option(
154
+ "--date",
155
+ "-d",
156
+ help="Snapshot date (YYYY-MM-DD). Defaults to latest available.",
157
+ ),
158
+ ] = None,
159
+ fix_param: Annotated[
160
+ list[str] | None,
161
+ typer.Option(
162
+ "--fix",
163
+ help="Fix a parameter (e.g., 'sigma=0.25'). Can be used multiple times.",
164
+ ),
165
+ ] = None,
166
+ verbose: Annotated[
167
+ bool, typer.Option("--verbose", "-v", help="Enable detailed logging.")
168
+ ] = False,
169
+ ):
170
+ """
171
+ Calibrates one or more models to market data for a given ticker and date.
172
+
173
+ Saves the calibrated parameters to the 'artifacts/calibrated_params' directory.
174
+ """
175
+ setup_logging(verbose)
176
+
177
+ # --- 1. Determine the snapshot date to use ---
178
+ if date is None:
179
+ typer.echo(
180
+ f"No date specified for {ticker}. Finding latest available snapshot..."
181
+ )
182
+ available_dates = get_available_snapshot_dates(ticker)
183
+ if not available_dates:
184
+ typer.secho(
185
+ f"Error: No market data snapshots found for ticker '{ticker}'.",
186
+ fg=typer.colors.RED,
187
+ )
188
+ raise typer.Exit(code=1)
189
+ date = available_dates[0]
190
+ typer.echo(f"Using latest date: {date}")
191
+
192
+ # --- 2. Load the market data ---
193
+ market_data = load_market_snapshot(ticker, date)
194
+ if market_data is None:
195
+ typer.secho(
196
+ f"Error: Failed to load market data for {ticker} on {date}.",
197
+ fg=typer.colors.RED,
198
+ )
199
+ raise typer.Exit(code=1)
200
+
201
+ # --- 3. Parse fixed parameters ---
202
+ frozen_params = {}
203
+ if fix_param:
204
+ for p in fix_param:
205
+ try:
206
+ key, value = p.split("=")
207
+ frozen_params[key.strip()] = float(value)
208
+ except ValueError:
209
+ typer.secho(
210
+ f"Invalid format for fixed parameter: '{p}'. Use 'key=value'.",
211
+ fg=typer.colors.RED,
212
+ )
213
+ raise typer.Exit(code=1)
214
+
215
+ # --- 4. Loop through and run workflow for each model ---
216
+ for model_name in model:
217
+ if model_name not in ALL_MODEL_CONFIGS:
218
+ typer.secho(
219
+ f"Warning: Model '{model_name}' not found. Skipping.",
220
+ fg=typer.colors.YELLOW,
221
+ )
222
+ continue
223
+
224
+ config = ALL_MODEL_CONFIGS[model_name].copy()
225
+ config["ticker"] = ticker
226
+ config["frozen"] = {**config.get("frozen", {}), **frozen_params}
227
+
228
+ workflow = DailyWorkflow(market_data, config)
229
+ workflow.run()
230
+
231
+ # --- 5. Display and save results ---
232
+ if workflow.results["Status"] == "Success":
233
+ typer.secho(
234
+ f"\nCalibration for {model_name} on {date} SUCCEEDED.",
235
+ fg=typer.colors.GREEN,
236
+ )
237
+ typer.echo(f" - Final RMSE: {workflow.results['RMSE']:.6f}")
238
+ typer.echo(
239
+ f" - Calibrated Params: {workflow.results['Calibrated Params']}"
240
+ )
241
+
242
+ # Save the parameters
243
+ params_to_save = {
244
+ "model": model_name,
245
+ "ticker": ticker,
246
+ "date": date,
247
+ "params": workflow.results["Calibrated Params"],
248
+ }
249
+ # Example filename: SPY_Heston_2023-01-01.json
250
+ filename = f"{ticker}_{model_name}_{date}.json"
251
+ save_path = PROJECT_ROOT / "artifacts" / "calibrated_params" / filename
252
+ with open(save_path, "w") as f:
253
+ json.dump(params_to_save, f, indent=4)
254
+ typer.echo(f" - Saved parameters to: {save_path}")
255
+
256
+ else:
257
+ typer.secho(
258
+ f"\nCalibration for {model_name} on {date} FAILED.", fg=typer.colors.RED
259
+ )
260
+ typer.echo(f" - Error: {workflow.results.get('Error', 'Unknown error')}")
261
+
262
+
263
+ @app.command()
264
+ def backtest(
265
+ ticker: Annotated[
266
+ str, typer.Option("--ticker", "-t", help="The stock ticker to backtest.")
267
+ ],
268
+ model: Annotated[
269
+ str, typer.Option("--model", "-m", help="The single model to backtest.")
270
+ ],
271
+ # Other options like start/end date, fixed params can be added here
272
+ verbose: Annotated[
273
+ bool, typer.Option("--verbose", "-v", help="Enable detailed logging.")
274
+ ] = False,
275
+ ):
276
+ """
277
+ Runs a historical backtest for a given model and ticker.
278
+ """
279
+ setup_logging(verbose)
280
+
281
+ if model not in ALL_MODEL_CONFIGS:
282
+ typer.secho(
283
+ "Error: Model "
284
+ f"'{model}' not found. Available models: "
285
+ f"{list(ALL_MODEL_CONFIGS.keys())}",
286
+ fg=typer.colors.RED,
287
+ )
288
+ raise typer.Exit(code=1)
289
+
290
+ config = ALL_MODEL_CONFIGS[model].copy()
291
+ config["ticker"] = ticker
292
+
293
+ workflow = BacktestWorkflow(ticker, config)
294
+ workflow.run()
295
+ workflow.save_results()
296
+
297
+
298
+ @app.command()
299
+ def demo(
300
+ model_name: Annotated[
301
+ str | None,
302
+ typer.Argument(help="Run a demo for a specific model name."),
303
+ ] = None,
304
+ ):
305
+ """
306
+ Runs a benchmark demo to showcase model and technique performance.
307
+
308
+ If a model name is provided (e.g., 'BSM'), it runs only that benchmark.
309
+ Otherwise, it runs the full suite.
310
+ """
311
+ benchmark_script_path = PROJECT_ROOT / "demo" / "benchmark.py"
312
+ typer.echo(f"Executing benchmark demo from: {benchmark_script_path}")
313
+
314
+ command_to_run = ["python", str(benchmark_script_path)]
315
+ if model_name:
316
+ command_to_run.append(model_name)
317
+
318
+ try:
319
+ subprocess.run(command_to_run, check=True)
320
+ except FileNotFoundError:
321
+ typer.secho(
322
+ f"Error: Demo script not found at '{benchmark_script_path}'.",
323
+ fg=typer.colors.RED,
324
+ )
325
+ raise typer.Exit(code=1)
326
+
327
+
328
+ @data_app.command(name="snapshot")
329
+ def save_snapshot(
330
+ tickers: Annotated[
331
+ list[str] | None,
332
+ typer.Option(
333
+ "--ticker",
334
+ "-t",
335
+ help="Stock ticker to snapshot. Can be used multiple times.",
336
+ ),
337
+ ] = None,
338
+ all_default: Annotated[
339
+ bool,
340
+ typer.Option(
341
+ "--all", help="Snapshot all default tickers specified in config.yaml."
342
+ ),
343
+ ] = False,
344
+ ):
345
+ """
346
+ Fetches and saves a live market data snapshot for specified tickers.
347
+
348
+ The data provider (yfinance or polygon) is determined by your config.yaml file.
349
+ """
350
+ if all_default:
351
+ tickers_to_download = _config.get("default_tickers", [])
352
+ if not tickers_to_download:
353
+ typer.secho(
354
+ "Error: --all flag used, but no 'default_tickers' in config.yaml.",
355
+ fg=typer.colors.RED,
356
+ )
357
+ raise typer.Exit(code=1)
358
+ typer.echo("Saving live market snapshots for all default tickers...")
359
+ elif tickers:
360
+ tickers_to_download = tickers
361
+ typer.echo(
362
+ f"Saving live market snapshots; tickers: {', '.join(tickers_to_download)}"
363
+ )
364
+ else:
365
+ typer.secho(
366
+ "Error: Please provide at least one --ticker or use the --all flag.",
367
+ fg=typer.colors.RED,
368
+ )
369
+ raise typer.Exit(code=1)
370
+
371
+ save_market_snapshot(tickers_to_download)
372
+ typer.secho("Snapshot complete.", fg=typer.colors.GREEN)
quantfin/config.py ADDED
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ import yaml
7
+
8
+ PROJECT_ROOT = Path(__file__).resolve().parents[2]
9
+ CONFIG_FILE_PATH = PROJECT_ROOT / "config.yaml"
10
+
11
+ if not CONFIG_FILE_PATH.exists():
12
+ print(f"Config file not found. Creating default config.yaml at {CONFIG_FILE_PATH}")
13
+ default_config = {
14
+ "polygon_api_key": "YOUR_POLYGON_API_KEY_HERE",
15
+ "live_data_provider": "yfinance",
16
+ "data_directory": "data",
17
+ "artifacts_directory": "artifacts",
18
+ }
19
+ with open(CONFIG_FILE_PATH, "w") as f:
20
+ yaml.dump(
21
+ default_config,
22
+ f,
23
+ default_flow_style=False,
24
+ sort_keys=False,
25
+ )
26
+
27
+ with open(CONFIG_FILE_PATH) as f:
28
+ _config = yaml.safe_load(f) or {}
29
+
30
+ # API Key Management
31
+ POLYGON_API_KEY = os.environ.get("POLYGON_API_KEY", _config.get("polygon_api_key"))
32
+
33
+ # Directory Management
34
+ DATA_DIR = PROJECT_ROOT / _config.get("data_directory", "data")
35
+ ARTIFACTS_DIR = PROJECT_ROOT / _config.get("artifacts_directory", "artifacts")
36
+
37
+ # Input Data Directories
38
+ MARKET_SNAPSHOT_DIR = DATA_DIR / "market_data_snapshots"
39
+ HISTORICAL_DIR = DATA_DIR / "historical_data"
40
+
41
+ # Output Artifact Directories
42
+ BACKTEST_LOGS_DIR = ARTIFACTS_DIR / "backtest_logs"
43
+ CALIBRATED_PARAMS_DIR = ARTIFACTS_DIR / "calibrated_params"
44
+ CALIBRATION_LOGS_DIR = ARTIFACTS_DIR / "calibration_logs"
45
+
46
+ # Ensure all directories exist
47
+ ALL_DIRS = [
48
+ DATA_DIR,
49
+ ARTIFACTS_DIR,
50
+ MARKET_SNAPSHOT_DIR,
51
+ HISTORICAL_DIR,
52
+ BACKTEST_LOGS_DIR,
53
+ CALIBRATED_PARAMS_DIR,
54
+ CALIBRATION_LOGS_DIR,
55
+ ]
56
+ for path in ALL_DIRS:
57
+ path.mkdir(parents=True, exist_ok=True)
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ __doc__ = """
4
+ This package contains all components for the Streamlit dashboard UI,
5
+ including the main service layer, plotting functions, and UI widgets.
6
+ """
7
+
8
+ from .service import DashboardService
9
+ from .widgets import render_parity_analysis_widget
10
+
11
+ __all__ = [
12
+ "DashboardService",
13
+ "render_parity_analysis_widget",
14
+ ]
@@ -0,0 +1,26 @@
1
+ import streamlit as st
2
+
3
+ st.set_page_config(
4
+ page_title="QuantFin Library Showcase",
5
+ layout="wide",
6
+ )
7
+
8
+ st.title("Welcome to the QuantFin Library Showcase!")
9
+ st.sidebar.success("Select a tool above to begin.")
10
+
11
+ # ruff: noqa: E501
12
+ st.markdown(
13
+ """
14
+ This application is a showcase for a comprehensive, high-performance
15
+ quantitative finance library built in Python.
16
+
17
+ **Select a tool from the sidebar** to see different features in action:
18
+
19
+ - **Calibration:** Calibrate models to market data and visualize the results.
20
+ - **Pricer and Greeks:** An interactive tool to price options with any model/technique and see the effect of changing parameters.
21
+ - **Financial Tools:** Utilities for pricing interest rate derivatives and analyzing put-call parity.
22
+
23
+ This entire application is built on a robust, library with a focus
24
+ on speed, accuracy, and extensibility.
25
+ """
26
+ )
@@ -0,0 +1,57 @@
1
+ import streamlit as st
2
+
3
+ from quantfin.config import _config
4
+ from quantfin.dashboard.service import DashboardService
5
+ from quantfin.data import get_available_snapshot_dates
6
+ from quantfin.workflows.configs import ALL_MODEL_CONFIGS
7
+
8
+ st.set_page_config(layout="wide", page_title="QuantFin | Calibration")
9
+ st.title("Model Calibration & IV Surface Analysis")
10
+ st.caption(
11
+ "Calibrate models to market data and visualize the resulting volatility smiles."
12
+ )
13
+ AVAILABLE_TICKERS = _config.get("default_tickers", ["SPY", "AAPL"])
14
+
15
+ with st.sidebar:
16
+ st.header("Configuration")
17
+ ticker = st.selectbox("Ticker", AVAILABLE_TICKERS)
18
+ data_source_options = get_available_snapshot_dates(ticker)
19
+ snapshot_date = st.selectbox("Snapshot Date", data_source_options)
20
+ model_selection = st.multiselect(
21
+ "Select Models to Calibrate",
22
+ list(ALL_MODEL_CONFIGS.keys()),
23
+ default=["Merton"],
24
+ )
25
+ run_button = st.button("Run Calibration Analysis")
26
+
27
+ if run_button:
28
+ selected_configs = {name: ALL_MODEL_CONFIGS[name] for name in model_selection}
29
+ with st.spinner("Initializing analysis..."):
30
+ service = DashboardService(ticker, snapshot_date, selected_configs)
31
+ try:
32
+ service.run_calibrations()
33
+ smile_fig, surface_fig = service.get_iv_plots()
34
+ st.session_state.service = service
35
+ st.session_state.smile_fig = smile_fig
36
+ st.session_state.surface_fig = surface_fig
37
+ except Exception as e:
38
+ st.error(f"An error occurred during analysis: {e}")
39
+ if "service" in st.session_state:
40
+ del st.session_state.service
41
+
42
+ if "service" in st.session_state:
43
+ service = st.session_state.service
44
+ st.header(f"Analysis for {service.ticker} on {service.snapshot_date}")
45
+ st.subheader("Calibration Summary")
46
+ if service.summary_df is not None:
47
+ st.dataframe(service.summary_df)
48
+ else:
49
+ st.error("Calibration failed for all selected models.")
50
+
51
+ if "smile_fig" in st.session_state:
52
+ st.subheader("Volatility Smile Visualization")
53
+ st.pyplot(st.session_state.smile_fig)
54
+ st.subheader("Implied Volatility Surface (3D)")
55
+ st.plotly_chart(st.session_state.surface_fig, use_container_width=True)
56
+ else:
57
+ st.info("Select parameters and click 'Run Analysis' in the sidebar to begin.")