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,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
+ ]