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,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.")
|