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,150 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import streamlit as st
|
|
3
|
+
|
|
4
|
+
from quantfin.atoms import Option, OptionType, Rate, Stock
|
|
5
|
+
from quantfin.models import *
|
|
6
|
+
from quantfin.techniques import *
|
|
7
|
+
|
|
8
|
+
st.set_page_config(layout="wide", page_title="QuantFin | Pricer")
|
|
9
|
+
st.title("On-Demand Pricer & Greek Analysis")
|
|
10
|
+
# ruff: noqa: E501
|
|
11
|
+
st.caption(
|
|
12
|
+
"Price any option with any model and technique. Manually set all parameters to see their effect."
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Model and Technique Selection
|
|
16
|
+
MODEL_MAP = {
|
|
17
|
+
"BSM": BSMModel,
|
|
18
|
+
"Merton": MertonJumpModel,
|
|
19
|
+
"Heston": HestonModel,
|
|
20
|
+
"Bates": BatesModel,
|
|
21
|
+
"Kou": KouModel,
|
|
22
|
+
"NIG": NIGModel,
|
|
23
|
+
"VG": VarianceGammaModel,
|
|
24
|
+
"CGMY": CGMYModel,
|
|
25
|
+
"SABR": SABRModel,
|
|
26
|
+
"CEV": CEVModel,
|
|
27
|
+
}
|
|
28
|
+
TECHNIQUE_MAP = {
|
|
29
|
+
"Analytic/Closed-Form": ClosedFormTechnique,
|
|
30
|
+
"Integration": IntegrationTechnique,
|
|
31
|
+
"FFT": FFTTechnique,
|
|
32
|
+
"Monte Carlo": MonteCarloTechnique,
|
|
33
|
+
"PDE": PDETechnique,
|
|
34
|
+
"Leisen-Reimer": LeisenReimerTechnique,
|
|
35
|
+
"CRR": CRRTechnique,
|
|
36
|
+
"TOPM": TOPMTechnique,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
col1, col2 = st.columns(2)
|
|
40
|
+
with col1:
|
|
41
|
+
model_name = st.selectbox("Select Model", list(MODEL_MAP.keys()))
|
|
42
|
+
|
|
43
|
+
# Get the selected model class and create a dummy instance to check its properties
|
|
44
|
+
model_class = MODEL_MAP[model_name]
|
|
45
|
+
if hasattr(model_class, "default_params"):
|
|
46
|
+
dummy_model_instance = model_class() # Uses default params
|
|
47
|
+
else:
|
|
48
|
+
st.error(f"Model {model_name} is missing 'default_params' attribute.")
|
|
49
|
+
st.stop()
|
|
50
|
+
|
|
51
|
+
# Dynamic Technique Selector
|
|
52
|
+
supported_techs = []
|
|
53
|
+
if dummy_model_instance.has_closed_form:
|
|
54
|
+
supported_techs.append("Analytic/Closed-Form")
|
|
55
|
+
if dummy_model_instance.supports_cf:
|
|
56
|
+
supported_techs.extend(["Integration", "FFT"])
|
|
57
|
+
if (
|
|
58
|
+
dummy_model_instance.supports_sde
|
|
59
|
+
or getattr(dummy_model_instance, "is_pure_levy", False)
|
|
60
|
+
or getattr(dummy_model_instance, "has_exact_sampler", False)
|
|
61
|
+
):
|
|
62
|
+
supported_techs.append("Monte Carlo")
|
|
63
|
+
if dummy_model_instance.supports_pde:
|
|
64
|
+
supported_techs.append("PDE")
|
|
65
|
+
if model_name == "BSM":
|
|
66
|
+
supported_techs.extend(["Leisen-Reimer", "CRR", "TOPM"])
|
|
67
|
+
|
|
68
|
+
with col2:
|
|
69
|
+
# Ensure a default is available if the list of techniques changes
|
|
70
|
+
selected_index = 0
|
|
71
|
+
if st.session_state.get("technique_name") in supported_techs:
|
|
72
|
+
selected_index = supported_techs.index(st.session_state.technique_name)
|
|
73
|
+
technique_name = st.selectbox(
|
|
74
|
+
"Select Technique", supported_techs, index=selected_index, key="technique_name"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Parameter Inputs
|
|
78
|
+
st.subheader("Market Parameters")
|
|
79
|
+
cols = st.columns(4)
|
|
80
|
+
spot = cols[0].number_input("Spot Price", value=100.0, step=1.0)
|
|
81
|
+
strike = cols[1].number_input("Strike Price", value=100.0, step=1.0)
|
|
82
|
+
maturity = cols[2].number_input("Maturity (Years)", value=1.0, min_value=0.01, step=0.1)
|
|
83
|
+
rate_val = cols[3].number_input("Risk-Free Rate", value=0.05, step=0.01, format="%.2f")
|
|
84
|
+
div_val = cols[0].number_input("Dividend Yield", value=0.02, step=0.01, format="%.2f")
|
|
85
|
+
option_type = cols[1].selectbox("Option Type", ("CALL", "PUT"))
|
|
86
|
+
|
|
87
|
+
# Dynamic Model Parameter Inputs
|
|
88
|
+
st.subheader(f"{model_name} Model Parameters")
|
|
89
|
+
params = {}
|
|
90
|
+
if hasattr(dummy_model_instance, "param_defs"):
|
|
91
|
+
param_defs = dummy_model_instance.param_defs
|
|
92
|
+
num_cols = 4
|
|
93
|
+
cols = st.columns(num_cols)
|
|
94
|
+
for i, (p_name, p_def) in enumerate(param_defs.items()):
|
|
95
|
+
params[p_name] = cols[i % num_cols].number_input(
|
|
96
|
+
p_def["label"],
|
|
97
|
+
value=p_def["default"],
|
|
98
|
+
min_value=p_def.get("min"),
|
|
99
|
+
max_value=p_def.get("max"),
|
|
100
|
+
step=p_def.get("step"),
|
|
101
|
+
format="%.4f",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if st.button("Calculate Price & Greeks"):
|
|
105
|
+
# Instantiate Objects
|
|
106
|
+
stock = Stock(spot=spot, dividend=div_val)
|
|
107
|
+
rate = Rate(rate=rate_val)
|
|
108
|
+
option = Option(
|
|
109
|
+
strike=strike, maturity=maturity, option_type=OptionType[option_type]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# merge UI params with non-UI default params
|
|
113
|
+
full_params = model_class.default_params.copy()
|
|
114
|
+
full_params.update(params)
|
|
115
|
+
|
|
116
|
+
model = model_class(params=full_params)
|
|
117
|
+
technique = TECHNIQUE_MAP[technique_name]()
|
|
118
|
+
|
|
119
|
+
# Prepare kwargs for techniques that need extra info (e.g., Heston's v0)
|
|
120
|
+
pricing_kwargs = full_params.copy()
|
|
121
|
+
|
|
122
|
+
# Calculate and Display
|
|
123
|
+
with st.spinner("Calculating..."):
|
|
124
|
+
try:
|
|
125
|
+
results_data = {}
|
|
126
|
+
|
|
127
|
+
results_data["Price"] = technique.price(
|
|
128
|
+
option, stock, model, rate, **pricing_kwargs
|
|
129
|
+
).price
|
|
130
|
+
|
|
131
|
+
results_data["Delta"] = technique.delta(
|
|
132
|
+
option, stock, model, rate, **pricing_kwargs
|
|
133
|
+
)
|
|
134
|
+
results_data["Gamma"] = technique.gamma(
|
|
135
|
+
option, stock, model, rate, **pricing_kwargs
|
|
136
|
+
)
|
|
137
|
+
results_data["Vega"] = technique.vega(
|
|
138
|
+
option, stock, model, rate, **pricing_kwargs
|
|
139
|
+
)
|
|
140
|
+
results_data["Theta"] = technique.theta(
|
|
141
|
+
option, stock, model, rate, **pricing_kwargs
|
|
142
|
+
)
|
|
143
|
+
results_data["Rho"] = technique.rho(
|
|
144
|
+
option, stock, model, rate, **pricing_kwargs
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
st.dataframe(pd.DataFrame([results_data]))
|
|
148
|
+
except Exception as e:
|
|
149
|
+
st.error(f"Calculation failed: {e}")
|
|
150
|
+
st.exception(e)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import streamlit as st
|
|
3
|
+
|
|
4
|
+
from quantfin.atoms import Rate, Stock, ZeroCouponBond
|
|
5
|
+
from quantfin.calibration import fit_jump_params_from_history
|
|
6
|
+
from quantfin.data import load_historical_returns
|
|
7
|
+
from quantfin.models import CIRModel, VasicekModel
|
|
8
|
+
from quantfin.parity import ImpliedRateModel
|
|
9
|
+
from quantfin.techniques import ClosedFormTechnique
|
|
10
|
+
|
|
11
|
+
st.set_page_config(layout="wide", page_title="QuantFin | Tools")
|
|
12
|
+
st.title("Financial Utilities & Tools")
|
|
13
|
+
|
|
14
|
+
# Jump Parameter Fitter
|
|
15
|
+
st.header("Historical Jump Parameter Fitter")
|
|
16
|
+
TICKERS = ["SPY", "AAPL", "META", "GOOGL", "TSLA", "NVDA", "AMD", "MSFT", "AMZN", "JPM"]
|
|
17
|
+
ticker_jump = st.selectbox("Select Ticker for Jump Analysis", TICKERS)
|
|
18
|
+
if st.button("Fit Jump Parameters"):
|
|
19
|
+
with st.spinner(f"Loading 10y returns for {ticker_jump} and fitting..."):
|
|
20
|
+
try:
|
|
21
|
+
returns = load_historical_returns(ticker_jump, period="10y")
|
|
22
|
+
jump_params = fit_jump_params_from_history(returns)
|
|
23
|
+
st.dataframe(pd.DataFrame([jump_params]))
|
|
24
|
+
except Exception as e:
|
|
25
|
+
st.error(f"Could not fit parameters: {e}")
|
|
26
|
+
|
|
27
|
+
# Rate Model Pricer
|
|
28
|
+
st.header("Term Structure Model Pricer")
|
|
29
|
+
col1, col2, col3 = st.columns(3)
|
|
30
|
+
model_name = col1.selectbox("Select Rate Model", ["Vasicek", "CIR"])
|
|
31
|
+
r0 = col2.number_input("Initial Short Rate (r0)", value=0.05, step=0.01)
|
|
32
|
+
bond_maturity = col3.number_input("Bond Maturity (T)", value=1.0, step=0.5)
|
|
33
|
+
params = {}
|
|
34
|
+
if model_name == "Vasicek":
|
|
35
|
+
cols_vasicek = st.columns(3)
|
|
36
|
+
params["kappa"] = cols_vasicek[0].number_input("Mean Reversion (kappa)", value=0.86)
|
|
37
|
+
params["theta"] = cols_vasicek[1].number_input("Long-Term Mean (theta)", value=0.09)
|
|
38
|
+
params["sigma"] = cols_vasicek[2].number_input("Volatility (sigma)", value=0.02)
|
|
39
|
+
model = VasicekModel(params=params)
|
|
40
|
+
else: # CIR
|
|
41
|
+
cols_cir = st.columns(3)
|
|
42
|
+
params["kappa"] = cols_cir[0].number_input("Mean Reversion (kappa)", value=0.86)
|
|
43
|
+
params["theta"] = cols_cir[1].number_input("Long-Term Mean (theta)", value=0.09)
|
|
44
|
+
params["sigma"] = cols_cir[2].number_input("Volatility (sigma)", value=0.02)
|
|
45
|
+
model = CIRModel(params=params)
|
|
46
|
+
if st.button("Price Zero-Coupon Bond"):
|
|
47
|
+
bond = ZeroCouponBond(maturity=bond_maturity)
|
|
48
|
+
r0_stock = Stock(spot=r0)
|
|
49
|
+
dummy_rate = Rate(rate=0.0)
|
|
50
|
+
technique = ClosedFormTechnique()
|
|
51
|
+
price = technique.price(bond, r0_stock, model, dummy_rate).price
|
|
52
|
+
st.metric(label=f"{model_name} ZCB Price", value=f"{price:.6f}")
|
|
53
|
+
|
|
54
|
+
st.header("Put-Call Parity Tools")
|
|
55
|
+
c1, c2, c3, c4, c5 = st.columns(5)
|
|
56
|
+
call_p = c1.number_input("Call Price", value=10.0)
|
|
57
|
+
put_p = c2.number_input("Put Price", value=5.0)
|
|
58
|
+
spot_p = c3.number_input("Spot Price ", value=100.0)
|
|
59
|
+
strike_p = c4.number_input("Strike Price ", value=100.0)
|
|
60
|
+
T_p = c5.number_input("Maturity ", value=1.0)
|
|
61
|
+
|
|
62
|
+
implied_rate_model = ImpliedRateModel(params={"eps": 1e-9, "max_iter": 100})
|
|
63
|
+
try:
|
|
64
|
+
implied_r = implied_rate_model.price_closed_form(
|
|
65
|
+
call_price=call_p, put_price=put_p, spot=spot_p, strike=strike_p, t=T_p, q=0
|
|
66
|
+
)
|
|
67
|
+
st.metric("Implied Risk-Free Rate (r)", f"{implied_r:.4%}")
|
|
68
|
+
except Exception as e:
|
|
69
|
+
st.error(f"Could not calculate implied rate: {e}")
|
|
File without changes
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import plotly.graph_objects as go
|
|
7
|
+
|
|
8
|
+
__doc__ = """
|
|
9
|
+
Contains plotting functions used by the dashboard to visualize
|
|
10
|
+
volatility surfaces and smiles.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def plot_smiles_by_expiry(
|
|
15
|
+
market_surface: pd.DataFrame,
|
|
16
|
+
model_surfaces: dict[str, pd.DataFrame],
|
|
17
|
+
ticker: str,
|
|
18
|
+
snapshot_date: str,
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Generates a matplotlib figure with volatility smiles for key expiries.
|
|
22
|
+
|
|
23
|
+
Compares the market implied volatility to the volatilities implied by
|
|
24
|
+
calibrated models across different strikes for a few selected expiration dates.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
market_surface : pd.DataFrame
|
|
29
|
+
DataFrame containing the market's implied volatility surface.
|
|
30
|
+
Must have 'expiry', 'strike', and 'iv' columns.
|
|
31
|
+
model_surfaces : dict[str, pd.DataFrame]
|
|
32
|
+
A dictionary mapping model names to their implied volatility surfaces.
|
|
33
|
+
ticker : str
|
|
34
|
+
The stock ticker, used for the plot title.
|
|
35
|
+
snapshot_date : str
|
|
36
|
+
The snapshot date, used for the plot title.
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
matplotlib.figure.Figure
|
|
41
|
+
The matplotlib figure object containing the subplots of volatility smiles.
|
|
42
|
+
"""
|
|
43
|
+
expiries = sorted(market_surface["expiry"].unique())
|
|
44
|
+
# Select up to 4 expiries to plot
|
|
45
|
+
plot_indices = np.linspace(0, len(expiries) - 1, min(4, len(expiries)), dtype=int)
|
|
46
|
+
plot_expiries = [expiries[i] for i in plot_indices]
|
|
47
|
+
|
|
48
|
+
fig, axes = plt.subplots(2, 2, figsize=(15, 11), sharey=True)
|
|
49
|
+
fig.suptitle(f"Volatility Smiles for {ticker} on {snapshot_date}", fontsize=18)
|
|
50
|
+
axes = axes.flatten()
|
|
51
|
+
|
|
52
|
+
for i, expiry in enumerate(plot_expiries):
|
|
53
|
+
ax = axes[i]
|
|
54
|
+
market_slice = market_surface[market_surface["expiry"] == expiry]
|
|
55
|
+
|
|
56
|
+
ax.scatter(
|
|
57
|
+
market_slice["strike"],
|
|
58
|
+
market_slice["iv"] * 100,
|
|
59
|
+
label="Market IV",
|
|
60
|
+
alpha=0.7,
|
|
61
|
+
s=20,
|
|
62
|
+
zorder=10,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
for model_name, model_surface in model_surfaces.items():
|
|
66
|
+
model_slice = model_surface[model_surface["expiry"] == expiry].sort_values(
|
|
67
|
+
"strike"
|
|
68
|
+
)
|
|
69
|
+
ax.plot(
|
|
70
|
+
model_slice["strike"],
|
|
71
|
+
model_slice["iv"] * 100,
|
|
72
|
+
label=f"{model_name} IV",
|
|
73
|
+
lw=2.5,
|
|
74
|
+
alpha=0.8,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
expiry_date_str = pd.to_datetime(expiry).strftime("%Y-%m-%d")
|
|
78
|
+
maturity = market_slice["maturity"].iloc[0]
|
|
79
|
+
ax.set_title(f"Expiry: {expiry_date_str} (T={maturity:.2f}y)")
|
|
80
|
+
ax.set_xlabel("Strike")
|
|
81
|
+
ax.set_ylabel("Implied Volatility (%)")
|
|
82
|
+
ax.legend()
|
|
83
|
+
ax.grid(True, linestyle="--", alpha=0.6)
|
|
84
|
+
ax.set_ylim(bottom=max(0, market_slice["iv"].min() * 100 - 5))
|
|
85
|
+
|
|
86
|
+
for j in range(i + 1, len(axes)):
|
|
87
|
+
axes[j].set_visible(False)
|
|
88
|
+
|
|
89
|
+
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
|
|
90
|
+
return fig
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def plot_iv_surface_3d(
|
|
94
|
+
market_surface: pd.DataFrame, model_surfaces: dict[str, pd.DataFrame], ticker: str
|
|
95
|
+
):
|
|
96
|
+
"""
|
|
97
|
+
Creates an interactive 3D plot of the volatility surfaces.
|
|
98
|
+
|
|
99
|
+
Renders the market IV surface as a mesh and overlays the model-implied
|
|
100
|
+
IV surfaces as line plots for comparison.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
market_surface : pd.DataFrame
|
|
105
|
+
DataFrame containing the market's implied volatility surface.
|
|
106
|
+
Must have 'maturity', 'strike', and 'iv' columns.
|
|
107
|
+
model_surfaces : dict[str, pd.DataFrame]
|
|
108
|
+
A dictionary mapping model names to their implied volatility surfaces.
|
|
109
|
+
ticker : str
|
|
110
|
+
The stock ticker, used for the plot title.
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
plotly.graph_objects.Figure
|
|
115
|
+
The Plotly figure object for the 3D surface plot.
|
|
116
|
+
"""
|
|
117
|
+
fig = go.Figure()
|
|
118
|
+
fig.add_trace(
|
|
119
|
+
go.Mesh3d(
|
|
120
|
+
x=market_surface["maturity"],
|
|
121
|
+
y=market_surface["strike"],
|
|
122
|
+
z=market_surface["iv"],
|
|
123
|
+
opacity=0.5,
|
|
124
|
+
color="grey",
|
|
125
|
+
name="Market IV",
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
colors = [
|
|
130
|
+
"#3a7fc1", # Deep ocean blue
|
|
131
|
+
"#ff8c00", # Sunset orange
|
|
132
|
+
"#4aac26", # Fresh sprout green
|
|
133
|
+
"#d03530", # Warning red
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
for i, (name, surface) in enumerate(model_surfaces.items()):
|
|
137
|
+
fig.add_trace(
|
|
138
|
+
go.Scatter3d(
|
|
139
|
+
x=surface["maturity"],
|
|
140
|
+
y=surface["strike"],
|
|
141
|
+
z=surface["iv"],
|
|
142
|
+
mode="lines",
|
|
143
|
+
name=f"{name} IV",
|
|
144
|
+
line=dict(color=colors[i % len(colors)], width=6),
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
fig.update_layout(
|
|
149
|
+
title=f"Implied Volatility Surface for {ticker}",
|
|
150
|
+
scene=dict(
|
|
151
|
+
xaxis_title="Maturity (Years)",
|
|
152
|
+
yaxis_title="Strike",
|
|
153
|
+
zaxis_title="Implied Volatility",
|
|
154
|
+
),
|
|
155
|
+
margin=dict(l=0, r=0, b=0, t=40),
|
|
156
|
+
)
|
|
157
|
+
return fig
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from quantfin.atoms import Rate, Stock
|
|
8
|
+
from quantfin.calibration import VolatilitySurface, fit_rate_and_dividend
|
|
9
|
+
from quantfin.calibration.technique_selector import select_fastest_technique
|
|
10
|
+
from quantfin.dashboard.plots import plot_iv_surface_3d, plot_smiles_by_expiry
|
|
11
|
+
from quantfin.data.market_data_manager import (
|
|
12
|
+
get_live_option_chain,
|
|
13
|
+
load_market_snapshot,
|
|
14
|
+
)
|
|
15
|
+
from quantfin.workflows import DailyWorkflow
|
|
16
|
+
|
|
17
|
+
__doc__ = """
|
|
18
|
+
Defines the main service layer that orchestrates all backend logic for the
|
|
19
|
+
Streamlit dashboard, from data loading to model calibration and plot generation.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DashboardService:
|
|
24
|
+
"""
|
|
25
|
+
Orchestrates all logic for the Streamlit dashboard.
|
|
26
|
+
|
|
27
|
+
This class acts as a stateful service to manage data loading,
|
|
28
|
+
model calibration, and result generation for a given ticker and date.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
ticker : str
|
|
33
|
+
The stock ticker to analyze.
|
|
34
|
+
snapshot_date : str
|
|
35
|
+
The market data snapshot date ('YYYY-MM-DD') or 'Live Data'.
|
|
36
|
+
model_configs : dict[str, Any]
|
|
37
|
+
A dictionary of model configurations to be used for calibration.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
ticker: str,
|
|
43
|
+
snapshot_date: str,
|
|
44
|
+
model_configs: dict[str, Any],
|
|
45
|
+
):
|
|
46
|
+
self.ticker = ticker
|
|
47
|
+
self.snapshot_date = snapshot_date
|
|
48
|
+
self.model_configs = model_configs
|
|
49
|
+
self._market_data: pd.DataFrame | None = None
|
|
50
|
+
self._stock: Stock | None = None
|
|
51
|
+
self._rate: Rate | None = None
|
|
52
|
+
self.calibrated_models: dict[str, Any] = {}
|
|
53
|
+
self.summary_df: pd.DataFrame | None = None
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def market_data(self) -> pd.DataFrame:
|
|
57
|
+
"""
|
|
58
|
+
Lazily loads the market data for the selected ticker and date.
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
pd.DataFrame
|
|
63
|
+
A DataFrame containing the option chain market data.
|
|
64
|
+
"""
|
|
65
|
+
if self._market_data is None:
|
|
66
|
+
if self.snapshot_date == "Live Data":
|
|
67
|
+
self._market_data = get_live_option_chain(self.ticker)
|
|
68
|
+
|
|
69
|
+
else:
|
|
70
|
+
self._market_data = load_market_snapshot(
|
|
71
|
+
self.ticker, self.snapshot_date
|
|
72
|
+
)
|
|
73
|
+
return self._market_data
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def stock(self) -> Stock:
|
|
77
|
+
"""
|
|
78
|
+
Lazily computes and caches the underlying Stock object.
|
|
79
|
+
|
|
80
|
+
The dividend yield is implied from put-call parity.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
Stock
|
|
85
|
+
A Stock instance representing the underlying asset.
|
|
86
|
+
"""
|
|
87
|
+
if self._stock is None:
|
|
88
|
+
spot = self.market_data["spot_price"].iloc[0]
|
|
89
|
+
_, div = fit_rate_and_dividend(
|
|
90
|
+
self.market_data[self.market_data["optionType"] == "call"],
|
|
91
|
+
self.market_data[self.market_data["optionType"] == "put"],
|
|
92
|
+
spot,
|
|
93
|
+
)
|
|
94
|
+
self._stock = Stock(spot=spot, dividend=div)
|
|
95
|
+
|
|
96
|
+
return self._stock
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def rate(self) -> Rate:
|
|
100
|
+
"""
|
|
101
|
+
Lazily computes and caches the risk-free Rate object.
|
|
102
|
+
|
|
103
|
+
The rate is implied from put-call parity.
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
Rate
|
|
108
|
+
A Rate instance representing the risk-free rate.
|
|
109
|
+
"""
|
|
110
|
+
if self._rate is None:
|
|
111
|
+
spot = self.market_data["spot_price"].iloc[0]
|
|
112
|
+
r, _ = fit_rate_and_dividend(
|
|
113
|
+
self.market_data[self.market_data["optionType"] == "call"],
|
|
114
|
+
self.market_data[self.market_data["optionType"] == "put"],
|
|
115
|
+
spot,
|
|
116
|
+
)
|
|
117
|
+
self._rate = Rate(rate=r)
|
|
118
|
+
|
|
119
|
+
return self._rate
|
|
120
|
+
|
|
121
|
+
def run_calibrations(self):
|
|
122
|
+
"""
|
|
123
|
+
Runs the daily calibration workflow for all selected models.
|
|
124
|
+
|
|
125
|
+
This method iterates through the model configurations, runs the
|
|
126
|
+
`DailyWorkflow` for each, and stores the calibrated models and
|
|
127
|
+
a summary of the results.
|
|
128
|
+
"""
|
|
129
|
+
all_results = []
|
|
130
|
+
for model_name, config in self.model_configs.items():
|
|
131
|
+
workflow = DailyWorkflow(market_data=self.market_data, model_config=config)
|
|
132
|
+
workflow.run()
|
|
133
|
+
all_results.append(workflow.results)
|
|
134
|
+
if workflow.results.get("Status") == "Success":
|
|
135
|
+
params = workflow.results.get("Calibrated Params")
|
|
136
|
+
self.calibrated_models[model_name] = config["model_class"](
|
|
137
|
+
params=params
|
|
138
|
+
)
|
|
139
|
+
self.summary_df = pd.DataFrame(all_results)
|
|
140
|
+
|
|
141
|
+
def get_iv_plots(self):
|
|
142
|
+
"""
|
|
143
|
+
Generates and returns the volatility smile and 3D surface plots.
|
|
144
|
+
|
|
145
|
+
It first computes the market IV surface, then computes the IV surface
|
|
146
|
+
for each successfully calibrated model, and finally generates the plots.
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
tuple[matplotlib.figure.Figure, plotly.graph_objects.Figure]
|
|
151
|
+
A tuple containing the volatility smile figure and the 3D surface figure.
|
|
152
|
+
"""
|
|
153
|
+
market_surface = (
|
|
154
|
+
VolatilitySurface(self.market_data)
|
|
155
|
+
.calculate_market_iv(self.stock, self.rate)
|
|
156
|
+
.surface
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
model_surfaces = {}
|
|
160
|
+
for name, model in self.calibrated_models.items():
|
|
161
|
+
technique = select_fastest_technique(model)
|
|
162
|
+
model_surfaces[name] = (
|
|
163
|
+
VolatilitySurface(self.market_data)
|
|
164
|
+
.calculate_model_iv(self.stock, self.rate, model, technique)
|
|
165
|
+
.surface
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
smile_fig = plot_smiles_by_expiry(
|
|
169
|
+
market_surface,
|
|
170
|
+
model_surfaces,
|
|
171
|
+
self.ticker,
|
|
172
|
+
self.snapshot_date,
|
|
173
|
+
)
|
|
174
|
+
surface_fig = plot_iv_surface_3d(market_surface, model_surfaces, self.ticker)
|
|
175
|
+
|
|
176
|
+
return smile_fig, surface_fig
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
7
|
+
from quantfin.atoms import Rate, Stock
|
|
8
|
+
from quantfin.parity import ParityModel
|
|
9
|
+
|
|
10
|
+
__doc__ = """
|
|
11
|
+
Contains reusable Streamlit components (widgets) for displaying
|
|
12
|
+
specific analyses on the dashboard, such as put-call parity checks.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def render_parity_analysis_widget(
|
|
17
|
+
market_data: pd.DataFrame,
|
|
18
|
+
stock: Stock,
|
|
19
|
+
rate: Rate,
|
|
20
|
+
):
|
|
21
|
+
"""
|
|
22
|
+
Creates a Streamlit container to display put-call parity analysis.
|
|
23
|
+
|
|
24
|
+
This function takes market data and computes the arbitrage gap based on
|
|
25
|
+
the put-call parity relationship for a user-selected expiration date.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
market_data : pd.DataFrame
|
|
30
|
+
The full option chain data for the asset.
|
|
31
|
+
stock : Stock
|
|
32
|
+
The underlying Stock object, containing spot price and dividend yield.
|
|
33
|
+
rate : Rate
|
|
34
|
+
The risk-free Rate object.
|
|
35
|
+
"""
|
|
36
|
+
st.subheader("Put-Call Parity Analysis")
|
|
37
|
+
|
|
38
|
+
# TODO: support for ParityModel; needs vecotrizing. Currently do it here.
|
|
39
|
+
parity_model = ParityModel() # noqa: F841
|
|
40
|
+
|
|
41
|
+
expiries = sorted(market_data["expiry"].unique())
|
|
42
|
+
selected_expiry = st.select_slider(
|
|
43
|
+
"Select Expiry to Analyze",
|
|
44
|
+
options=[pd.to_datetime(e).strftime("%Y-%m-%d") for e in expiries],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
expiry_df = market_data[
|
|
48
|
+
market_data["expiry"] == pd.to_datetime(selected_expiry)
|
|
49
|
+
].copy()
|
|
50
|
+
|
|
51
|
+
calls = expiry_df[expiry_df["optionType"] == "call"].set_index("strike")
|
|
52
|
+
puts = expiry_df[expiry_df["optionType"] == "put"].set_index("strike")
|
|
53
|
+
|
|
54
|
+
# Find common strikes
|
|
55
|
+
common_strikes = calls.index.intersection(puts.index)
|
|
56
|
+
if len(common_strikes) == 0:
|
|
57
|
+
st.warning("No common strikes found for this expiry to check parity.")
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
calls = calls.loc[common_strikes]
|
|
61
|
+
puts = puts.loc[common_strikes]
|
|
62
|
+
|
|
63
|
+
# C - P
|
|
64
|
+
price_diff = calls["last_price"] - puts["last_price"]
|
|
65
|
+
|
|
66
|
+
# S*exp(-qT) - K*exp(-rT)
|
|
67
|
+
T = calls["maturity"].iloc[0]
|
|
68
|
+
r = rate.get_rate(T)
|
|
69
|
+
q = stock.dividend
|
|
70
|
+
|
|
71
|
+
parity_diff = stock.spot * np.exp(-q * T) - calls.index.values * np.exp(-r * T)
|
|
72
|
+
|
|
73
|
+
error = price_diff - parity_diff
|
|
74
|
+
|
|
75
|
+
results_df = pd.DataFrame(
|
|
76
|
+
{
|
|
77
|
+
"C - P": price_diff,
|
|
78
|
+
"S*e⁻qT - K*e⁻rT": parity_diff,
|
|
79
|
+
"Arbitrage Gap ($)": error,
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
st.dataframe(results_df)
|
|
84
|
+
st.caption(
|
|
85
|
+
"A non-zero 'Arbitrage Gap' suggests a violation of put-call parity "
|
|
86
|
+
"(or stale data)." # noqa: E501
|
|
87
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
__doc__ = """
|
|
4
|
+
The `data` package provides a clean, unified interface for fetching, loading,
|
|
5
|
+
and saving all market and historical data required by the quantfin library.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .historical_manager import (
|
|
9
|
+
load_historical_returns,
|
|
10
|
+
save_historical_returns,
|
|
11
|
+
)
|
|
12
|
+
from .market_data_manager import (
|
|
13
|
+
get_available_snapshot_dates,
|
|
14
|
+
get_live_option_chain,
|
|
15
|
+
load_market_snapshot,
|
|
16
|
+
save_market_snapshot,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"get_available_snapshot_dates",
|
|
21
|
+
"get_live_option_chain",
|
|
22
|
+
"load_historical_returns",
|
|
23
|
+
"load_market_snapshot",
|
|
24
|
+
"save_historical_returns",
|
|
25
|
+
"save_market_snapshot",
|
|
26
|
+
]
|