kameshcodes commited on
Commit
7bb0af0
·
0 Parent(s):

feat: initial implementation of VaR engine

Browse files

Portfolio Value-at-Risk computation engine with Historical and
Parametric (variance-covariance) methods, stressed VaR/ES, live
market data via yfinance, and audit-ready Excel export with
embedded formulas.

Includes:
- Gradio web UI with live-updating inputs
- Historical & Parametric VaR/ES calculations
- Stressed metrics over configurable stress windows
- Excel reports with formula-driven cells (no hardcoded values)
- Jupyter notebooks for exploration
- Project scaffolding (pyproject.toml, uv lockfile)

.gitattributes ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ *.png filter=lfs diff=lfs merge=lfs -text
2
+ *.jpg filter=lfs diff=lfs merge=lfs -text
3
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
4
+ *.gif filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ .gradio
12
+ log/
13
+ output/
README.md ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # VaR Engine
2
+
3
+ An interactive web app to compute **Value at Risk (VaR)** and **Expected Shortfall (ES)** for equities, featuring an interactive Gradio UI and **audit-ready Excel reports with embedded formulas**.
4
+
5
+
6
+ ## Features
7
+
8
+ - **Interactive UI**: Sleek Gradio interface that dynamically updates on input changes.
9
+ - **Live Market Data**: Fetches historical prices via `yfinance`
10
+ - **Historical & Parametric VaR**: Supports both Historical and Parametric (variance-covariance) methods
11
+ - **Stressed VaR/ES**: Computes stressed metrics over a configurable stress window (e.g. GFC 2008)
12
+ - **Audit-Ready Excel**: Exports reports with **formula-driven calculations (no hardcoded outputs)**
13
+
14
+ ## Getting Started
15
+
16
+ ### Option 1: Using `uv` (Recommended)
17
+
18
+ 1. **Sync the environment**:
19
+ ```bash
20
+ uv sync
21
+ ```
22
+ 2. **Run the application**:
23
+ ```bash
24
+ gradio app.py
25
+ ```
26
+
27
+ ### Option 2: Manual Setup (Pip)
28
+
29
+ 1. **Create and activate a virtual environment**:
30
+ - **MacOS / Linux**:
31
+ ```bash
32
+ python3 -m venv .venv
33
+ source .venv/bin/activate
34
+ ```
35
+ - **Windows**:
36
+ ```powershell
37
+ python -m venv .venv
38
+ .venv\Scripts\activate
39
+ ```
40
+
41
+ 2. **Install dependencies**:
42
+ ```bash
43
+ pip install -r requirements.txt
44
+ ```
45
+
46
+ 3. **Run the application**:
47
+ ```bash
48
+ gradio app.py
49
+ ```
50
+
51
+ The server will launch locally (typically at `http://127.0.0.1:7860` or `http://localhost:7860`). Open this address in your browser to access the VaR Engine.
52
+
53
+ ## Project Structure
54
+
55
+ - **`app.py`**: The thin Gradio presentation/UI layer.
56
+ - **`src/`**: Core VaR calculation engine, data processing, and plotting logic.
57
+ - **`config.yaml`**: Application settings (tickers, lookback window, stress period).
58
+ - **`notebooks/`**: Jupyter notebooks for exploratory analysis (historical, parametric, Monte Carlo).
59
+ - **`log/`**: Persistent application logs managed by `loguru`.
60
+ - **`output/`**: Directory for exported Excel report files.
61
+
62
+
63
+ ## Future Work
64
+
65
+ - **Multi-Asset Portfolio Support (In Progress)**: Extending from single equities to portfolios with configurable weights and aggregation of VaR/ES
66
+ - **Asset Class Expansion (In Progress)**: Adding support for indices, ETFs, bonds, and other instruments
app.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ app.py -- Gradio UI for Value at Risk Analysis.
3
+ """
4
+
5
+ import pandas as pd
6
+ import gradio as gr
7
+ from src.logger import logger
8
+ from src.config import TICKERS, LOOKBACK_DAYS, STRESS_START_DATE, STRESS_END_DATE, STRESS_LABEL
9
+ from src.historical import historical_var_es_pipeline
10
+ from src.parametric import parametric_var_es_pipeline
11
+
12
+
13
+ def calculate_var_analysis(
14
+ ticker: str,
15
+ end_date_str: str,
16
+ portfolio_value: float,
17
+ n_days: int,
18
+ var_confidence_label: str,
19
+ es_confidence_label: str,
20
+ method: str,
21
+ ):
22
+ """Calculate Value at Risk analysis based on Gradio inputs and delegate to the analysis pipeline."""
23
+ logger.info(
24
+ f"Analysis requested: {ticker} | VaR={var_confidence_label} ES={es_confidence_label} | {method} | N={n_days} | Date={end_date_str} | PV=${portfolio_value:,.0f}"
25
+ )
26
+
27
+ var_confidence = float(var_confidence_label.strip().replace("%", "")) / 100.0
28
+ es_confidence = float(es_confidence_label.strip().replace("%", "")) / 100.0
29
+ var_conf_pct = var_confidence_label.strip()
30
+ es_conf_pct = es_confidence_label.strip()
31
+
32
+ today = pd.Timestamp.today().normalize()
33
+
34
+ try:
35
+ end_date = pd.to_datetime(end_date_str, errors="raise").normalize()
36
+ except Exception:
37
+ gr.Warning("Invalid date selection. Please try again.")
38
+ return reset_analysis_results(n_days, var_confidence_label, es_confidence_label, method)
39
+
40
+ if end_date >= today:
41
+ gr.Warning(
42
+ "Invalid date selection. VaR estimation requires historical data, so please choose a date prior to today."
43
+ )
44
+ return reset_analysis_results(n_days, var_confidence_label, es_confidence_label, method)
45
+
46
+ fy_start = pd.Timestamp(year=today.year - 1, month=4, day=1)
47
+
48
+ if end_date < fy_start:
49
+ gr.Warning(f"VaR Date must be after {fy_start.strftime('%Y-%m-%d')}")
50
+ return reset_analysis_results(n_days, var_confidence_label, es_confidence_label, method)
51
+
52
+ if portfolio_value <= 0:
53
+ gr.Warning("Portfolio value must be positive.")
54
+ return reset_analysis_results(n_days, var_confidence_label, es_confidence_label, method)
55
+
56
+ pipeline = historical_var_es_pipeline if method == "Historical VaR" else parametric_var_es_pipeline
57
+ result = pipeline(
58
+ ticker,
59
+ var_confidence,
60
+ es_confidence,
61
+ LOOKBACK_DAYS,
62
+ int(n_days),
63
+ portfolio_value,
64
+ end_date,
65
+ stress_start=STRESS_START_DATE,
66
+ stress_end=STRESS_END_DATE,
67
+ stress_label=STRESS_LABEL,
68
+ )
69
+
70
+ logger.success(
71
+ f"Analysis complete: VaR=${result['var_nd']:,.2f}, ES=${result['es_nd']:,.2f}, Excel={result['excel_path']}\n"
72
+ )
73
+
74
+ return (
75
+ gr.update(
76
+ label=f"{int(n_days)}-day {var_conf_pct} VaR",
77
+ value=f"${result['var_nd']:,.2f}",
78
+ ),
79
+ gr.update(
80
+ label=f"{int(n_days)}-day {es_conf_pct} ES",
81
+ value=f"${result['es_nd']:,.2f}",
82
+ ),
83
+ gr.update(
84
+ label=f"{int(n_days)}-day {var_conf_pct} Stressed VaR",
85
+ value=f"${result['stressed_var_nd']:,.2f}",
86
+ ),
87
+ gr.update(
88
+ label=f"{int(n_days)}-day {es_conf_pct} Stressed ES",
89
+ value=f"${result['stressed_es_nd']:,.2f}",
90
+ ),
91
+ gr.update(value=result["fig_dist"], visible=True),
92
+ gr.update(value=result["excel_path"], visible=True),
93
+ )
94
+
95
+
96
+ def reset_analysis_results(n_days: float, var_confidence_label: str, es_confidence_label: str, method: str):
97
+ """Reset and hide analysis results when input parameters are modified."""
98
+ var_conf_pct = var_confidence_label.strip()
99
+ es_conf_pct = es_confidence_label.strip()
100
+ method_short = "Historical" if method == "Historical VaR" else "Parametric"
101
+
102
+ return (
103
+ gr.update(value="", label=f"{int(n_days)}-day {var_conf_pct} VaR"),
104
+ gr.update(value="", label=f"{int(n_days)}-day {es_conf_pct} ES"),
105
+ gr.update(value="", label=f"{int(n_days)}-day {var_conf_pct} Stressed VaR"),
106
+ gr.update(value="", label=f"{int(n_days)}-day {es_conf_pct} Stressed ES"),
107
+ gr.update(value=None, visible=False),
108
+ gr.update(visible=False),
109
+ )
110
+
111
+
112
+ def enable_run_button_for_method(method: str):
113
+ """Enable the Run Analysis button only for fully implemented VaR methods."""
114
+ return gr.update(interactive=(method in ("Historical VaR", "Parametric VaR")))
115
+
116
+
117
+ # ------------------------------------------------------------------
118
+ # UI builder
119
+ # ------------------------------------------------------------------
120
+
121
+
122
+ def build_app() -> gr.Blocks:
123
+ """Construct and return the Gradio Blocks application."""
124
+
125
+ with gr.Blocks(
126
+ title="VaR Engine",
127
+ ) as app:
128
+ with gr.Row():
129
+ with gr.Column(scale=3):
130
+ gr.Markdown("# VaR Engine")
131
+ with gr.Column(scale=1, min_width=260):
132
+ download_file = gr.DownloadButton(
133
+ "Download Excel Report",
134
+ variant="primary",
135
+ visible=False,
136
+ elem_id="excel-btn",
137
+ )
138
+
139
+ with gr.Row():
140
+ # Sidebar
141
+ with gr.Column(scale=1, min_width=260):
142
+ gr.Markdown("### Inputs")
143
+ with gr.Group():
144
+ ticker_dd = gr.Dropdown(
145
+ choices=TICKERS,
146
+ value=TICKERS[0],
147
+ label="Ticker",
148
+ )
149
+ end_date_input = gr.DateTime(
150
+ include_time=False,
151
+ type="datetime",
152
+ label="VaR Date",
153
+ )
154
+ portfolio_val_input = gr.Number(
155
+ value=1000000,
156
+ label="Portfolio Value ($)",
157
+ )
158
+ n_days_slider = gr.Slider(
159
+ minimum=1,
160
+ maximum=15,
161
+ step=1,
162
+ value=10,
163
+ label="N Days for VaR",
164
+ )
165
+ with gr.Row():
166
+ confidence_dd = gr.Dropdown(
167
+ choices=["99%", "97.5%", "95%"],
168
+ value="99%",
169
+ label="VaR Confidence",
170
+ min_width=100,
171
+ )
172
+ es_confidence_dd = gr.Dropdown(
173
+ choices=["99%", "97.5%", "95%"],
174
+ value="99%",
175
+ label="ES Confidence",
176
+ min_width=100,
177
+ )
178
+ method_radio = gr.Radio(
179
+ choices=[
180
+ "Historical VaR",
181
+ "Parametric VaR",
182
+ ],
183
+ value="Historical VaR",
184
+ label="Method",
185
+ )
186
+ run_btn = gr.Button("Run Analysis", variant="primary")
187
+
188
+ # Enable/disable run button based on method availability
189
+ method_radio.change(
190
+ fn=enable_run_button_for_method,
191
+ inputs=method_radio,
192
+ outputs=run_btn,
193
+ )
194
+ with gr.Column(scale=3):
195
+ gr.Markdown("### Results")
196
+ with gr.Row():
197
+ with gr.Column(scale=1):
198
+ var_box = gr.Textbox(
199
+ label="10-day 99% VaR",
200
+ interactive=False,
201
+ )
202
+ with gr.Column(scale=1):
203
+ es_box = gr.Textbox(
204
+ label="10-day 99% ES",
205
+ interactive=False,
206
+ )
207
+ with gr.Row():
208
+ with gr.Column(scale=1):
209
+ stressed_var_box = gr.Textbox(
210
+ label="10-day 99% Stressed VaR",
211
+ interactive=False,
212
+ )
213
+ with gr.Column(scale=1):
214
+ stressed_es_box = gr.Textbox(
215
+ label="10-day 99% Stressed ES",
216
+ interactive=False,
217
+ )
218
+
219
+ plot_dist = gr.Plot(show_label=False, visible=False)
220
+
221
+ # Wiring
222
+ all_outputs = [var_box, es_box, stressed_var_box, stressed_es_box, plot_dist, download_file]
223
+
224
+ run_btn.click(
225
+ fn=calculate_var_analysis,
226
+ inputs=[
227
+ ticker_dd,
228
+ end_date_input,
229
+ portfolio_val_input,
230
+ n_days_slider,
231
+ confidence_dd,
232
+ es_confidence_dd,
233
+ method_radio,
234
+ ],
235
+ outputs=all_outputs,
236
+ )
237
+
238
+ all_inputs = [
239
+ ticker_dd,
240
+ end_date_input,
241
+ portfolio_val_input,
242
+ n_days_slider,
243
+ confidence_dd,
244
+ es_confidence_dd,
245
+ method_radio,
246
+ ]
247
+
248
+ label_inputs = [n_days_slider, confidence_dd, es_confidence_dd, method_radio]
249
+
250
+ for comp in all_inputs:
251
+ if comp is n_days_slider:
252
+ comp.release(
253
+ fn=reset_analysis_results,
254
+ inputs=label_inputs,
255
+ outputs=all_outputs,
256
+ )
257
+ else:
258
+ comp.change(
259
+ fn=reset_analysis_results,
260
+ inputs=label_inputs,
261
+ outputs=all_outputs,
262
+ )
263
+
264
+ method_radio.change(
265
+ fn=enable_run_button_for_method, inputs=method_radio, outputs=run_btn
266
+ )
267
+
268
+ return app
269
+
270
+
271
+ # ------------------------------------------------------------------
272
+ # Entry point
273
+ # ------------------------------------------------------------------
274
+
275
+ if __name__ == "__main__":
276
+ custom_css = """
277
+ .form { border: none !important; box-shadow: none !important; gap: 0 !important; }
278
+ .form .block, .form .row, .form > * { border: none !important; box-shadow: none !important; }
279
+ #excel-btn, #excel-btn.primary {
280
+ background: #ea580c !important;
281
+ background-color: #ea580c !important;
282
+ color: white !important;
283
+ border-color: #7c2d12 !important;
284
+ border: 1px solid #7c2d12 !important;
285
+ }
286
+ #excel-btn:hover, #excel-btn.primary:hover {
287
+ background: #c2410c !important;
288
+ background-color: #c2410c !important;
289
+ border-color: #451a03 !important;
290
+ border: 1px solid #451a03 !important;
291
+ }
292
+ """
293
+
294
+ application = build_app()
295
+ application.launch(share=True, theme=gr.themes.Base(), css=custom_css)
config.yaml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ------------------------------------------------------------------
2
+ # VaR Engine – Application Settings
3
+ # ------------------------------------------------------------------
4
+
5
+ tickers:
6
+ - AAPL
7
+ - MSFT
8
+ - GOOG
9
+ - AMZN
10
+ - NVDA
11
+ - JPM
12
+ - BCS
13
+
14
+ lookback_days: 251
15
+
16
+ # Stress window used for Stressed VaR/ES
17
+ stressed_period_label: "Global Financial Crisis (2008)"
18
+ stressed_period_start_date: "2008-01-01"
19
+ stressed_period_end_date: "2008-12-31"
notebooks/historical.ipynb ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "title",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Historical VaR"
9
+ ]
10
+ },
11
+ {
12
+ "cell_type": "markdown",
13
+ "id": "e210b117",
14
+ "metadata": {},
15
+ "source": [
16
+ "## 1. Imports"
17
+ ]
18
+ },
19
+ {
20
+ "cell_type": "code",
21
+ "execution_count": null,
22
+ "id": "imports",
23
+ "metadata": {},
24
+ "outputs": [],
25
+ "source": "import numpy as np\nimport pandas as pd\nimport plotly.graph_objects as go\nimport yfinance as yf\nfrom datetime import datetime\nimport warnings\nwarnings.filterwarnings('ignore')"
26
+ },
27
+ {
28
+ "cell_type": "code",
29
+ "execution_count": 2,
30
+ "id": "c0717aab",
31
+ "metadata": {},
32
+ "outputs": [
33
+ {
34
+ "name": "stdout",
35
+ "output_type": "stream",
36
+ "text": [
37
+ "Analyzing GOOG with 99% confidence\n",
38
+ "Lookback period: 251 days, Horizon: 10 days\n",
39
+ "Portfolio value: $1,000,000\n"
40
+ ]
41
+ }
42
+ ],
43
+ "source": [
44
+ "# Set up parameters\n",
45
+ "TICKER = 'GOOG'\n",
46
+ "CONFIDENCE_LEVEL = 0.99\n",
47
+ "LOOKBACK_DAYS = 251 # ~1 year of trading days\n",
48
+ "HORIZON_DAYS = 10\n",
49
+ "PORTFOLIO_VALUE = 1_000_000\n",
50
+ "\n",
51
+ "print(f\"Analyzing {TICKER} with {CONFIDENCE_LEVEL:.0%} confidence\")\n",
52
+ "print(f\"Lookback period: {LOOKBACK_DAYS} days, Horizon: {HORIZON_DAYS} days\")\n",
53
+ "print(f\"Portfolio value: ${PORTFOLIO_VALUE:,}\")"
54
+ ]
55
+ },
56
+ {
57
+ "cell_type": "markdown",
58
+ "id": "bc738b1a",
59
+ "metadata": {},
60
+ "source": [
61
+ "## 1. Fetch Prices "
62
+ ]
63
+ },
64
+ {
65
+ "cell_type": "code",
66
+ "execution_count": 3,
67
+ "id": "fetch_data",
68
+ "metadata": {},
69
+ "outputs": [],
70
+ "source": [
71
+ "def fetch_prices(ticker, lookback, var_date=None):\n",
72
+ " \"\"\"Fetch daily close prices for a ticker.\n",
73
+ "\n",
74
+ " Gets the last `lookback` trading days of data up to the day before `var_date`.\n",
75
+ " If `var_date` is not given, it uses the last business day.\n",
76
+ " \"\"\"\n",
77
+ " # if the var date is none fetch the last business day\n",
78
+ " if var_date is None:\n",
79
+ " var_date = (pd.Timestamp.today() - pd.offsets.BDay()).date()\n",
80
+ " \n",
81
+ " calendar_days = int(lookback * 1.6)\n",
82
+ " start = var_date - pd.Timedelta(days=calendar_days)\n",
83
+ "\n",
84
+ " df = yf.download(\n",
85
+ " ticker,\n",
86
+ " start=start.strftime(\"%Y-%m-%d\"),\n",
87
+ " end=var_date.strftime(\"%Y-%m-%d\"),\n",
88
+ " progress=False,\n",
89
+ " interval=\"1d\",\n",
90
+ " auto_adjust=True\n",
91
+ " )\n",
92
+ "\n",
93
+ " if df.empty:\n",
94
+ " raise ValueError(f\"No data returned for ticker '{ticker}'.\")\n",
95
+ " \n",
96
+ " prices = df[\"Close\"].squeeze()\n",
97
+ " prices.name = ticker\n",
98
+ " result = prices.tail(lookback)\n",
99
+ " return result"
100
+ ]
101
+ },
102
+ {
103
+ "cell_type": "code",
104
+ "execution_count": 4,
105
+ "id": "ff696443",
106
+ "metadata": {},
107
+ "outputs": [
108
+ {
109
+ "name": "stdout",
110
+ "output_type": "stream",
111
+ "text": [
112
+ "Shape: (251,)\n",
113
+ "Start date: 2025-03-26\n",
114
+ "End date: 2026-03-25\n"
115
+ ]
116
+ }
117
+ ],
118
+ "source": [
119
+ "prices = fetch_prices(TICKER, LOOKBACK_DAYS)\n",
120
+ "print(f\"Shape: {prices.shape}\")\n",
121
+ "print(f\"Start date: {prices.index.min().date()}\")\n",
122
+ "print(f\"End date: {prices.index.max().date()}\")"
123
+ ]
124
+ },
125
+ {
126
+ "cell_type": "markdown",
127
+ "id": "614eb489",
128
+ "metadata": {},
129
+ "source": [
130
+ "## 2. Return Calculation\n",
131
+ "\n",
132
+ "Calculate daily returns from the price data."
133
+ ]
134
+ },
135
+ {
136
+ "cell_type": "code",
137
+ "execution_count": null,
138
+ "id": "a2d126c7",
139
+ "metadata": {},
140
+ "outputs": [],
141
+ "source": "def compute_returns(prices, kind=\"arithmetic\"):\n \"\"\"Compute daily returns from a price series.\n\n kind : \"arithmetic\" or \"log\"\n arithmetic -> (P_t - P_{t-1}) / P_{t-1}\n log -> log(P_t) - log(P_{t-1})\n \"\"\"\n if kind == \"log\":\n returns = np.log(prices) - np.log(prices.shift(1))\n returns.name = \"Daily Log Return\"\n else:\n returns = (prices - prices.shift(1)) / prices.shift(1)\n returns.name = \"Daily Return\"\n return returns.dropna()"
142
+ },
143
+ {
144
+ "cell_type": "code",
145
+ "execution_count": null,
146
+ "id": "ba2e12a3",
147
+ "metadata": {},
148
+ "outputs": [],
149
+ "source": "# Calculate returns\ndaily_returns = compute_returns(prices, kind=\"arithmetic\")\n\nprint(\"Daily return series:\")\nprint(\"Shape: \", daily_returns.shape)\nprint(\"\\n \", daily_returns.head())"
150
+ },
151
+ {
152
+ "cell_type": "markdown",
153
+ "id": "3aba51de",
154
+ "metadata": {},
155
+ "source": [
156
+ "## 3. Calculate VaR and ES"
157
+ ]
158
+ },
159
+ {
160
+ "cell_type": "code",
161
+ "execution_count": null,
162
+ "id": "calculate_var_es",
163
+ "metadata": {},
164
+ "outputs": [],
165
+ "source": "def calculate_historical_var(returns, confidence):\n \"\"\"Compute VaR from returns using the percentile method.\n\n VaR is the (1 - confidence) percentile of returns, negated to express as loss.\n Returns VaR as a positive loss fraction.\n \"\"\"\n vals = returns.values\n return -float(np.percentile(np.asarray(vals), (1.0 - confidence) * 100))\n\n\ndef calculate_historical_es(returns, confidence):\n \"\"\"Compute ES from returns using the percentile method.\n\n ES = E[loss | loss > VaR], the mean of losses exceeding VaR.\n Returns ES as a positive loss fraction.\n \"\"\"\n var = calculate_historical_var(returns, confidence)\n losses = -np.asarray(returns.values)\n tail = losses[losses > var]\n return float(np.mean(tail)) if len(tail) > 0 else var"
166
+ },
167
+ {
168
+ "cell_type": "code",
169
+ "execution_count": null,
170
+ "id": "0368a7bb",
171
+ "metadata": {},
172
+ "outputs": [],
173
+ "source": "var_pct = calculate_historical_var(daily_returns, CONFIDENCE_LEVEL)\nes_pct = calculate_historical_es(daily_returns, CONFIDENCE_LEVEL)\nprint(f\"VaR: {var_pct:.4f} ({var_pct*100:.2f}%)\")\nprint(f\"ES: {es_pct:.4f} ({es_pct*100:.2f}%)\")"
174
+ },
175
+ {
176
+ "cell_type": "markdown",
177
+ "id": "analysis_section",
178
+ "metadata": {},
179
+ "source": [
180
+ "## 5. Orchestration of the workflow"
181
+ ]
182
+ },
183
+ {
184
+ "cell_type": "code",
185
+ "execution_count": null,
186
+ "id": "12a3fbb2",
187
+ "metadata": {},
188
+ "outputs": [],
189
+ "source": "def historical_var_es_pipeline(ticker, confidence, lookback, n_days, portfolio_value, end_date=None):\n \"\"\"Run the full historical VaR workflow.\n\n Fetches data, computes 1-day VaR/ES, and scales results to n-day horizon.\n Returns dollar VaR/ES along with the underlying data.\n \"\"\"\n # 1. Fetch data and compute returns\n prices = fetch_prices(ticker, lookback, end_date)\n daily_returns = compute_returns(prices, kind=\"arithmetic\")\n\n # 2. Calculate 1-day VaR and ES\n var_1d_pct = calculate_historical_var(daily_returns, confidence)\n es_1d_pct = calculate_historical_es(daily_returns, confidence)\n var_1d = var_1d_pct * portfolio_value\n es_1d = es_1d_pct * portfolio_value\n\n # 3. Scale to N-day horizon\n scaling_factor = np.sqrt(n_days)\n var_nd = var_1d * scaling_factor\n es_nd = es_1d * scaling_factor\n\n return {\n \"var_1d\": var_1d,\n \"var_nd\": var_nd,\n \"es_1d\": es_1d,\n \"es_nd\": es_nd,\n \"prices\": prices,\n \"daily_returns\": daily_returns,\n }"
190
+ },
191
+ {
192
+ "cell_type": "code",
193
+ "execution_count": null,
194
+ "id": "2609e970",
195
+ "metadata": {},
196
+ "outputs": [],
197
+ "source": "results = historical_var_es_pipeline(\n ticker=TICKER,\n confidence=CONFIDENCE_LEVEL,\n lookback=LOOKBACK_DAYS,\n n_days=HORIZON_DAYS,\n portfolio_value=PORTFOLIO_VALUE)"
198
+ },
199
+ {
200
+ "cell_type": "code",
201
+ "execution_count": null,
202
+ "id": "summary",
203
+ "metadata": {},
204
+ "outputs": [],
205
+ "source": "print(\"=\" * 60)\nprint(f\"HISTORICAL VaR ANALYSIS SUMMARY - {TICKER}\")\nprint(\"=\" * 60)\nprint(f\"VaR Date: {datetime.now().strftime('%Y-%m-%d')}\")\nprint(f\"Portfolio Value: ${PORTFOLIO_VALUE:,}\")\nprint(f\"Confidence Level: {CONFIDENCE_LEVEL:.0%}\")\nprint(f\"Time Horizon: {HORIZON_DAYS} days\")\nprint(f\"Historical Period: {LOOKBACK_DAYS} trading days\")\nprint()\nprint(\"VaR METRICS:\")\nprint('-'*60)\nprint(f\" {HORIZON_DAYS}-Day VaR: ${results['var_nd']:,.2f} ({results['var_nd']/PORTFOLIO_VALUE*100:.2f}%)\")\nprint(f\" {HORIZON_DAYS}-Day ES: ${results['es_nd']:,.2f} ({results['es_nd']/PORTFOLIO_VALUE*100:.2f}%)\")\nprint()"
206
+ }
207
+ ],
208
+ "metadata": {
209
+ "kernelspec": {
210
+ "display_name": ".venv",
211
+ "language": "python",
212
+ "name": "python3"
213
+ },
214
+ "language_info": {
215
+ "codemirror_mode": {
216
+ "name": "ipython",
217
+ "version": 3
218
+ },
219
+ "file_extension": ".py",
220
+ "mimetype": "text/x-python",
221
+ "name": "python",
222
+ "nbconvert_exporter": "python",
223
+ "pygments_lexer": "ipython3",
224
+ "version": "3.13.5"
225
+ }
226
+ },
227
+ "nbformat": 4,
228
+ "nbformat_minor": 5
229
+ }
notebooks/monte-carlo-simulation.ipynb ADDED
@@ -0,0 +1,474 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "title",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Monte Carlo VaR — Step by Step"
9
+ ]
10
+ },
11
+ {
12
+ "cell_type": "code",
13
+ "execution_count": null,
14
+ "id": "c1",
15
+ "metadata": {},
16
+ "outputs": [],
17
+ "source": [
18
+ "import numpy as np\n",
19
+ "import matplotlib.pyplot as plt\n",
20
+ "import yfinance as yf\n",
21
+ "import warnings\n",
22
+ "warnings.filterwarnings('ignore')"
23
+ ]
24
+ },
25
+ {
26
+ "cell_type": "code",
27
+ "execution_count": null,
28
+ "id": "c2",
29
+ "metadata": {},
30
+ "outputs": [
31
+ {
32
+ "data": {
33
+ "text/plain": [
34
+ "array([0.4 , 0.35, 0.25])"
35
+ ]
36
+ },
37
+ "execution_count": 26,
38
+ "metadata": {},
39
+ "output_type": "execute_result"
40
+ }
41
+ ],
42
+ "source": [
43
+ "TICKERS = ['AAPL', 'GOOGL', 'MSFT']\n",
44
+ "WEIGHTS = np.array([0.40, 0.35, 0.25])\n",
45
+ "PORT_VALUE = 1_000_000\n",
46
+ "np.random.seed(42)\n",
47
+ "\n",
48
+ "WEIGHTS"
49
+ ]
50
+ },
51
+ {
52
+ "cell_type": "code",
53
+ "execution_count": 27,
54
+ "id": "c3",
55
+ "metadata": {},
56
+ "outputs": [
57
+ {
58
+ "name": "stderr",
59
+ "output_type": "stream",
60
+ "text": [
61
+ "[*********************100%***********************] 3 of 3 completed\n"
62
+ ]
63
+ },
64
+ {
65
+ "data": {
66
+ "text/html": [
67
+ "<div>\n",
68
+ "<style scoped>\n",
69
+ " .dataframe tbody tr th:only-of-type {\n",
70
+ " vertical-align: middle;\n",
71
+ " }\n",
72
+ "\n",
73
+ " .dataframe tbody tr th {\n",
74
+ " vertical-align: top;\n",
75
+ " }\n",
76
+ "\n",
77
+ " .dataframe thead th {\n",
78
+ " text-align: right;\n",
79
+ " }\n",
80
+ "</style>\n",
81
+ "<table border=\"1\" class=\"dataframe\">\n",
82
+ " <thead>\n",
83
+ " <tr style=\"text-align: right;\">\n",
84
+ " <th>Ticker</th>\n",
85
+ " <th>AAPL</th>\n",
86
+ " <th>GOOGL</th>\n",
87
+ " <th>MSFT</th>\n",
88
+ " </tr>\n",
89
+ " <tr>\n",
90
+ " <th>Date</th>\n",
91
+ " <th></th>\n",
92
+ " <th></th>\n",
93
+ " <th></th>\n",
94
+ " </tr>\n",
95
+ " </thead>\n",
96
+ " <tbody>\n",
97
+ " <tr>\n",
98
+ " <th>2025-12-24</th>\n",
99
+ " <td>273.554016</td>\n",
100
+ " <td>314.089996</td>\n",
101
+ " <td>486.908630</td>\n",
102
+ " </tr>\n",
103
+ " <tr>\n",
104
+ " <th>2025-12-26</th>\n",
105
+ " <td>273.144409</td>\n",
106
+ " <td>313.510010</td>\n",
107
+ " <td>486.599365</td>\n",
108
+ " </tr>\n",
109
+ " <tr>\n",
110
+ " <th>2025-12-29</th>\n",
111
+ " <td>273.504089</td>\n",
112
+ " <td>313.559998</td>\n",
113
+ " <td>485.990753</td>\n",
114
+ " </tr>\n",
115
+ " <tr>\n",
116
+ " <th>2025-12-30</th>\n",
117
+ " <td>272.824707</td>\n",
118
+ " <td>313.850006</td>\n",
119
+ " <td>486.369904</td>\n",
120
+ " </tr>\n",
121
+ " <tr>\n",
122
+ " <th>2025-12-31</th>\n",
123
+ " <td>271.605835</td>\n",
124
+ " <td>313.000000</td>\n",
125
+ " <td>482.518677</td>\n",
126
+ " </tr>\n",
127
+ " </tbody>\n",
128
+ "</table>\n",
129
+ "</div>"
130
+ ],
131
+ "text/plain": [
132
+ "Ticker AAPL GOOGL MSFT\n",
133
+ "Date \n",
134
+ "2025-12-24 273.554016 314.089996 486.908630\n",
135
+ "2025-12-26 273.144409 313.510010 486.599365\n",
136
+ "2025-12-29 273.504089 313.559998 485.990753\n",
137
+ "2025-12-30 272.824707 313.850006 486.369904\n",
138
+ "2025-12-31 271.605835 313.000000 482.518677"
139
+ ]
140
+ },
141
+ "execution_count": 27,
142
+ "metadata": {},
143
+ "output_type": "execute_result"
144
+ }
145
+ ],
146
+ "source": [
147
+ "prices = yf.download(TICKERS, start='2023-01-01', end='2026-01-01', auto_adjust=True)['Close'][TICKERS].dropna()\n",
148
+ "prices.tail()"
149
+ ]
150
+ },
151
+ {
152
+ "cell_type": "code",
153
+ "execution_count": 28,
154
+ "id": "c4",
155
+ "metadata": {},
156
+ "outputs": [
157
+ {
158
+ "data": {
159
+ "text/html": [
160
+ "<div>\n",
161
+ "<style scoped>\n",
162
+ " .dataframe tbody tr th:only-of-type {\n",
163
+ " vertical-align: middle;\n",
164
+ " }\n",
165
+ "\n",
166
+ " .dataframe tbody tr th {\n",
167
+ " vertical-align: top;\n",
168
+ " }\n",
169
+ "\n",
170
+ " .dataframe thead th {\n",
171
+ " text-align: right;\n",
172
+ " }\n",
173
+ "</style>\n",
174
+ "<table border=\"1\" class=\"dataframe\">\n",
175
+ " <thead>\n",
176
+ " <tr style=\"text-align: right;\">\n",
177
+ " <th>Ticker</th>\n",
178
+ " <th>AAPL</th>\n",
179
+ " <th>GOOGL</th>\n",
180
+ " <th>MSFT</th>\n",
181
+ " </tr>\n",
182
+ " </thead>\n",
183
+ " <tbody>\n",
184
+ " <tr>\n",
185
+ " <th>count</th>\n",
186
+ " <td>751.0000</td>\n",
187
+ " <td>751.0000</td>\n",
188
+ " <td>751.0000</td>\n",
189
+ " </tr>\n",
190
+ " <tr>\n",
191
+ " <th>mean</th>\n",
192
+ " <td>0.0011</td>\n",
193
+ " <td>0.0017</td>\n",
194
+ " <td>0.0010</td>\n",
195
+ " </tr>\n",
196
+ " <tr>\n",
197
+ " <th>std</th>\n",
198
+ " <td>0.0160</td>\n",
199
+ " <td>0.0190</td>\n",
200
+ " <td>0.0146</td>\n",
201
+ " </tr>\n",
202
+ " <tr>\n",
203
+ " <th>min</th>\n",
204
+ " <td>-0.0970</td>\n",
205
+ " <td>-0.0999</td>\n",
206
+ " <td>-0.0638</td>\n",
207
+ " </tr>\n",
208
+ " <tr>\n",
209
+ " <th>25%</th>\n",
210
+ " <td>-0.0067</td>\n",
211
+ " <td>-0.0087</td>\n",
212
+ " <td>-0.0068</td>\n",
213
+ " </tr>\n",
214
+ " <tr>\n",
215
+ " <th>50%</th>\n",
216
+ " <td>0.0013</td>\n",
217
+ " <td>0.0023</td>\n",
218
+ " <td>0.0012</td>\n",
219
+ " </tr>\n",
220
+ " <tr>\n",
221
+ " <th>75%</th>\n",
222
+ " <td>0.0087</td>\n",
223
+ " <td>0.0116</td>\n",
224
+ " <td>0.0091</td>\n",
225
+ " </tr>\n",
226
+ " <tr>\n",
227
+ " <th>max</th>\n",
228
+ " <td>0.1426</td>\n",
229
+ " <td>0.0973</td>\n",
230
+ " <td>0.0965</td>\n",
231
+ " </tr>\n",
232
+ " </tbody>\n",
233
+ "</table>\n",
234
+ "</div>"
235
+ ],
236
+ "text/plain": [
237
+ "Ticker AAPL GOOGL MSFT\n",
238
+ "count 751.0000 751.0000 751.0000\n",
239
+ "mean 0.0011 0.0017 0.0010\n",
240
+ "std 0.0160 0.0190 0.0146\n",
241
+ "min -0.0970 -0.0999 -0.0638\n",
242
+ "25% -0.0067 -0.0087 -0.0068\n",
243
+ "50% 0.0013 0.0023 0.0012\n",
244
+ "75% 0.0087 0.0116 0.0091\n",
245
+ "max 0.1426 0.0973 0.0965"
246
+ ]
247
+ },
248
+ "execution_count": 28,
249
+ "metadata": {},
250
+ "output_type": "execute_result"
251
+ }
252
+ ],
253
+ "source": [
254
+ "log_returns = np.log(prices / prices.shift(1)).dropna()\n",
255
+ "log_returns.describe().round(4)"
256
+ ]
257
+ },
258
+ {
259
+ "cell_type": "code",
260
+ "execution_count": 29,
261
+ "id": "c5",
262
+ "metadata": {},
263
+ "outputs": [
264
+ {
265
+ "data": {
266
+ "text/plain": [
267
+ "array([0.00105378, 0.00168275, 0.00096676])"
268
+ ]
269
+ },
270
+ "execution_count": 29,
271
+ "metadata": {},
272
+ "output_type": "execute_result"
273
+ }
274
+ ],
275
+ "source": [
276
+ "mu = log_returns.mean().values\n",
277
+ "mu"
278
+ ]
279
+ },
280
+ {
281
+ "cell_type": "code",
282
+ "execution_count": 30,
283
+ "id": "c6",
284
+ "metadata": {},
285
+ "outputs": [
286
+ {
287
+ "data": {
288
+ "text/plain": [
289
+ "array([[0.00025692, 0.00014008, 0.00011358],\n",
290
+ " [0.00014008, 0.00036247, 0.00013643],\n",
291
+ " [0.00011358, 0.00013643, 0.00021204]])"
292
+ ]
293
+ },
294
+ "execution_count": 30,
295
+ "metadata": {},
296
+ "output_type": "execute_result"
297
+ }
298
+ ],
299
+ "source": [
300
+ "cov = log_returns.cov().values\n",
301
+ "cov"
302
+ ]
303
+ },
304
+ {
305
+ "cell_type": "code",
306
+ "execution_count": 35,
307
+ "id": "c7",
308
+ "metadata": {},
309
+ "outputs": [
310
+ {
311
+ "data": {
312
+ "text/plain": [
313
+ "array([1472.72241061, 1118.21086262, 518.11465968])"
314
+ ]
315
+ },
316
+ "execution_count": 35,
317
+ "metadata": {},
318
+ "output_type": "execute_result"
319
+ }
320
+ ],
321
+ "source": [
322
+ "S0 = prices.iloc[-1].values\n",
323
+ "shares = (WEIGHTS * PORT_VALUE) / S0\n",
324
+ "shares"
325
+ ]
326
+ },
327
+ {
328
+ "cell_type": "code",
329
+ "execution_count": 36,
330
+ "id": "c8",
331
+ "metadata": {},
332
+ "outputs": [
333
+ {
334
+ "data": {
335
+ "text/plain": [
336
+ "(10000, 3)"
337
+ ]
338
+ },
339
+ "execution_count": 36,
340
+ "metadata": {},
341
+ "output_type": "execute_result"
342
+ }
343
+ ],
344
+ "source": [
345
+ "sim_returns = np.random.multivariate_normal(mu, cov, size=10_000)\n",
346
+ "sim_returns.shape"
347
+ ]
348
+ },
349
+ {
350
+ "cell_type": "code",
351
+ "execution_count": 37,
352
+ "id": "c9",
353
+ "metadata": {},
354
+ "outputs": [
355
+ {
356
+ "data": {
357
+ "text/plain": [
358
+ "array([[268.98623474, 310.87375941, 483.01010853],\n",
359
+ " [266.65990125, 306.33240488, 473.68121623],\n",
360
+ " [269.20598714, 303.56541957, 473.94807854]])"
361
+ ]
362
+ },
363
+ "execution_count": 37,
364
+ "metadata": {},
365
+ "output_type": "execute_result"
366
+ }
367
+ ],
368
+ "source": [
369
+ "sim_prices = S0 * np.exp(sim_returns)\n",
370
+ "sim_prices[:3]"
371
+ ]
372
+ },
373
+ {
374
+ "cell_type": "code",
375
+ "execution_count": 38,
376
+ "id": "c10",
377
+ "metadata": {},
378
+ "outputs": [
379
+ {
380
+ "name": "stdout",
381
+ "output_type": "stream",
382
+ "text": [
383
+ "Mean ΔP: $+1,474 | Std: $13,661\n"
384
+ ]
385
+ }
386
+ ],
387
+ "source": [
388
+ "delta_P = sim_prices @ shares - PORT_VALUE\n",
389
+ "print(f'Mean ΔP: ${np.mean(delta_P):+,.0f} | Std: ${np.std(delta_P):,.0f}')"
390
+ ]
391
+ },
392
+ {
393
+ "cell_type": "code",
394
+ "execution_count": 39,
395
+ "id": "c11",
396
+ "metadata": {},
397
+ "outputs": [
398
+ {
399
+ "name": "stdout",
400
+ "output_type": "stream",
401
+ "text": [
402
+ "95% VaR = $21,007 (2.10% of portfolio)\n"
403
+ ]
404
+ }
405
+ ],
406
+ "source": [
407
+ "var95 = -np.percentile(delta_P, 5)\n",
408
+ "print(f'95% VaR = ${var95:,.0f} ({var95/PORT_VALUE:.2%} of portfolio)')"
409
+ ]
410
+ },
411
+ {
412
+ "cell_type": "code",
413
+ "execution_count": 40,
414
+ "id": "c12",
415
+ "metadata": {},
416
+ "outputs": [
417
+ {
418
+ "name": "stdout",
419
+ "output_type": "stream",
420
+ "text": [
421
+ "99% VaR = $29,626 (2.96% of portfolio)\n"
422
+ ]
423
+ }
424
+ ],
425
+ "source": [
426
+ "var99 = -np.percentile(delta_P, 1)\n",
427
+ "print(f'99% VaR = ${var99:,.0f} ({var99/PORT_VALUE:.2%} of portfolio)')"
428
+ ]
429
+ },
430
+ {
431
+ "cell_type": "code",
432
+ "execution_count": 43,
433
+ "id": "c14",
434
+ "metadata": {},
435
+ "outputs": [
436
+ {
437
+ "data": {
438
+ "image/png": "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",
439
+ "text/plain": [
440
+ "<Figure size 1000x400 with 1 Axes>"
441
+ ]
442
+ },
443
+ "metadata": {},
444
+ "output_type": "display_data"
445
+ }
446
+ ],
447
+ "source": [
448
+ "plt.figure(figsize=(10, 4))\n",
449
+ "plt.hist(delta_P, bins=80, density=True, alpha=0.7, color='steelblue', edgecolor='none')\n",
450
+ "plt.axvline(-var95, color='orange', lw=2, label=f'95% VaR ${var95:,.0f}')\n",
451
+ "plt.axvline(-var99, color='red', lw=2, label=f'99% VaR ${var99:,.0f}')\n",
452
+ "plt.xlabel('P&L ($)')\n",
453
+ "plt.ylabel('Density')\n",
454
+ "plt.title('1-Day P&L Distribution')\n",
455
+ "plt.legend()\n",
456
+ "plt.tight_layout()\n",
457
+ "plt.show()"
458
+ ]
459
+ }
460
+ ],
461
+ "metadata": {
462
+ "kernelspec": {
463
+ "display_name": "Python 3",
464
+ "language": "python",
465
+ "name": "python3"
466
+ },
467
+ "language_info": {
468
+ "name": "python",
469
+ "version": "3.9.6"
470
+ }
471
+ },
472
+ "nbformat": 4,
473
+ "nbformat_minor": 5
474
+ }
notebooks/parametric.ipynb ADDED
@@ -0,0 +1,605 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "title",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Parametric VaR\n",
9
+ "\n",
10
+ "The Parametric (Variance-Covariance) approach assumes that portfolio returns are\n",
11
+ "**normally distributed**. Instead of sorting historical returns, we estimate the\n",
12
+ "mean and standard deviation of the return distribution and use the **z-score**\n",
13
+ "at the desired confidence level to compute VaR analytically."
14
+ ]
15
+ },
16
+ {
17
+ "cell_type": "markdown",
18
+ "id": "imports_header",
19
+ "metadata": {},
20
+ "source": [
21
+ "## 1. Imports"
22
+ ]
23
+ },
24
+ {
25
+ "cell_type": "code",
26
+ "execution_count": 1,
27
+ "id": "imports",
28
+ "metadata": {},
29
+ "outputs": [],
30
+ "source": [
31
+ "import numpy as np\n",
32
+ "import pandas as pd\n",
33
+ "import yfinance as yf\n",
34
+ "from scipy import stats\n",
35
+ "from datetime import datetime\n",
36
+ "import warnings\n",
37
+ "warnings.filterwarnings('ignore')"
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "code",
42
+ "execution_count": 2,
43
+ "id": "parameters",
44
+ "metadata": {},
45
+ "outputs": [
46
+ {
47
+ "name": "stdout",
48
+ "output_type": "stream",
49
+ "text": [
50
+ "Analyzing GOOG with 99% confidence\n",
51
+ "Lookback period: 251 days, Horizon: 10 days\n",
52
+ "Portfolio value: $1,000,000\n"
53
+ ]
54
+ }
55
+ ],
56
+ "source": [
57
+ "# Set up parameters\n",
58
+ "TICKER = 'GOOG'\n",
59
+ "CONFIDENCE_LEVEL = 0.99\n",
60
+ "LOOKBACK_DAYS = 251 # ~1 year of trading days\n",
61
+ "HORIZON_DAYS = 10\n",
62
+ "PORTFOLIO_VALUE = 1_000_000\n",
63
+ "\n",
64
+ "print(f\"Analyzing {TICKER} with {CONFIDENCE_LEVEL:.0%} confidence\")\n",
65
+ "print(f\"Lookback period: {LOOKBACK_DAYS} days, Horizon: {HORIZON_DAYS} days\")\n",
66
+ "print(f\"Portfolio value: ${PORTFOLIO_VALUE:,}\")"
67
+ ]
68
+ },
69
+ {
70
+ "cell_type": "markdown",
71
+ "id": "fetch_header",
72
+ "metadata": {},
73
+ "source": [
74
+ "## 2. Fetch Prices"
75
+ ]
76
+ },
77
+ {
78
+ "cell_type": "code",
79
+ "execution_count": 3,
80
+ "id": "fetch_data",
81
+ "metadata": {},
82
+ "outputs": [],
83
+ "source": [
84
+ "def fetch_prices(ticker, lookback, var_date=None):\n",
85
+ " \"\"\"Fetch daily close prices for a ticker.\n",
86
+ "\n",
87
+ " Gets the last `lookback` trading days of data up to the day before `var_date`.\n",
88
+ " If `var_date` is not given, it uses the last business day.\n",
89
+ " \"\"\"\n",
90
+ " if var_date is None:\n",
91
+ " var_date = (pd.Timestamp.today() - pd.offsets.BDay()).date()\n",
92
+ "\n",
93
+ " calendar_days = int(lookback * 1.6)\n",
94
+ " start = var_date - pd.Timedelta(days=calendar_days)\n",
95
+ "\n",
96
+ " df = yf.download(\n",
97
+ " ticker,\n",
98
+ " start=start.strftime(\"%Y-%m-%d\"),\n",
99
+ " end=var_date.strftime(\"%Y-%m-%d\"),\n",
100
+ " progress=False,\n",
101
+ " interval=\"1d\",\n",
102
+ " auto_adjust=True\n",
103
+ " )\n",
104
+ "\n",
105
+ " if df.empty:\n",
106
+ " raise ValueError(f\"No data returned for ticker '{ticker}'.\")\n",
107
+ "\n",
108
+ " prices = df[\"Close\"].squeeze()\n",
109
+ " prices.name = ticker\n",
110
+ " result = prices.tail(lookback)\n",
111
+ " return result"
112
+ ]
113
+ },
114
+ {
115
+ "cell_type": "code",
116
+ "execution_count": 4,
117
+ "id": "run_fetch",
118
+ "metadata": {},
119
+ "outputs": [
120
+ {
121
+ "name": "stdout",
122
+ "output_type": "stream",
123
+ "text": [
124
+ "Shape: (251,)\n",
125
+ "Start date: 2025-03-27\n",
126
+ "End date: 2026-03-26\n"
127
+ ]
128
+ }
129
+ ],
130
+ "source": [
131
+ "prices = fetch_prices(TICKER, LOOKBACK_DAYS)\n",
132
+ "print(f\"Shape: {prices.shape}\")\n",
133
+ "print(f\"Start date: {prices.index.min().date()}\")\n",
134
+ "print(f\"End date: {prices.index.max().date()}\")"
135
+ ]
136
+ },
137
+ {
138
+ "cell_type": "markdown",
139
+ "id": "returns_header",
140
+ "metadata": {},
141
+ "source": [
142
+ "## 3. Return Calculation\n",
143
+ "\n",
144
+ "Calculate daily returns from the price data."
145
+ ]
146
+ },
147
+ {
148
+ "cell_type": "code",
149
+ "execution_count": 5,
150
+ "id": "compute_returns",
151
+ "metadata": {},
152
+ "outputs": [],
153
+ "source": [
154
+ "def compute_returns(prices, kind=\"arithmetic\"):\n",
155
+ " \"\"\"Compute daily returns from a price series.\n",
156
+ "\n",
157
+ " kind : \"arithmetic\" or \"log\"\n",
158
+ " arithmetic -> (P_t - P_{t-1}) / P_{t-1}\n",
159
+ " log -> log(P_t) - log(P_{t-1})\n",
160
+ " \"\"\"\n",
161
+ " if kind == \"log\":\n",
162
+ " returns = np.log(prices) - np.log(prices.shift(1))\n",
163
+ " returns.name = \"Daily Log Return\"\n",
164
+ " else:\n",
165
+ " returns = (prices - prices.shift(1)) / prices.shift(1)\n",
166
+ " returns.name = \"Daily Return\"\n",
167
+ " return returns.dropna()"
168
+ ]
169
+ },
170
+ {
171
+ "cell_type": "code",
172
+ "execution_count": 6,
173
+ "id": "run_returns",
174
+ "metadata": {},
175
+ "outputs": [
176
+ {
177
+ "name": "stdout",
178
+ "output_type": "stream",
179
+ "text": [
180
+ "Daily log return series:\n",
181
+ "Shape: (250,)\n",
182
+ "\n",
183
+ " Date\n",
184
+ "2025-03-28 -0.050114\n",
185
+ "2025-03-31 0.001089\n",
186
+ "2025-04-01 0.016820\n",
187
+ "2025-04-02 -0.000126\n",
188
+ "2025-04-03 -0.040007\n",
189
+ "Name: Daily Log Return, dtype: float64\n"
190
+ ]
191
+ }
192
+ ],
193
+ "source": [
194
+ "daily_returns = compute_returns(prices, kind=\"log\")\n",
195
+ "\n",
196
+ "print(\"Daily log return series:\")\n",
197
+ "print(\"Shape: \", daily_returns.shape)\n",
198
+ "print(\"\\n \", daily_returns.head())"
199
+ ]
200
+ },
201
+ {
202
+ "cell_type": "markdown",
203
+ "id": "distribution_header",
204
+ "metadata": {},
205
+ "source": [
206
+ "## 4. Estimate Distribution Parameters\n",
207
+ "\n",
208
+ "The parametric method models daily returns as $R \\sim \\mathcal{N}(\\mu, \\sigma^2)$. \n",
209
+ "We estimate the **mean** ($\\mu$) and **standard deviation** ($\\sigma$) from the\n",
210
+ "historical sample, then derive VaR from the normal quantile."
211
+ ]
212
+ },
213
+ {
214
+ "cell_type": "code",
215
+ "execution_count": 7,
216
+ "id": "estimate_params",
217
+ "metadata": {},
218
+ "outputs": [],
219
+ "source": [
220
+ "def estimate_distribution(returns):\n",
221
+ " \"\"\"Estimate mean and standard deviation of daily returns.\"\"\"\n",
222
+ " mu = returns.mean()\n",
223
+ " # ddof=1 ensures we divide by (N-1), giving an unbiased estimate of volatility from sample data\n",
224
+ " sigma = returns.std(ddof=1)\n",
225
+ " return mu, sigma"
226
+ ]
227
+ },
228
+ {
229
+ "cell_type": "code",
230
+ "execution_count": 8,
231
+ "id": "run_estimate",
232
+ "metadata": {},
233
+ "outputs": [
234
+ {
235
+ "name": "stdout",
236
+ "output_type": "stream",
237
+ "text": [
238
+ "Mean daily return (mu): 0.002162 (0.2162%)\n",
239
+ "Std dev daily return (sigma): 0.018841 (1.8841%)\n"
240
+ ]
241
+ }
242
+ ],
243
+ "source": [
244
+ "mu, sigma = estimate_distribution(daily_returns)\n",
245
+ "\n",
246
+ "print(f\"Mean daily return (mu): {mu:.6f} ({mu*100:.4f}%)\")\n",
247
+ "print(f\"Std dev daily return (sigma): {sigma:.6f} ({sigma*100:.4f}%)\")"
248
+ ]
249
+ },
250
+ {
251
+ "cell_type": "markdown",
252
+ "id": "var_header",
253
+ "metadata": {},
254
+ "source": [
255
+ "## 5. Calculate VaR and ES\n",
256
+ "\n",
257
+ "**Parametric VaR** at confidence level $c$ is:\n",
258
+ "\n",
259
+ "$$\\text{VaR}_{1\\text{-day}} = -(\\mu + z_{\\alpha}\\,\\sigma)$$\n",
260
+ "\n",
261
+ "where $z_{\\alpha} = \\Phi^{-1}(1 - c)$ is the z-score for the lower tail.\n",
262
+ "\n",
263
+ "**Expected Shortfall (ES)** under the normal assumption is:\n",
264
+ "\n",
265
+ "$$\\text{ES}_{1\\text{-day}} = -(\\mu - \\sigma \\cdot \\frac{\\phi(z_{\\alpha})}{1-c})$$\n",
266
+ "\n",
267
+ "where $\\phi$ is the standard normal PDF."
268
+ ]
269
+ },
270
+ {
271
+ "cell_type": "code",
272
+ "execution_count": 9,
273
+ "id": "calculate_var_es",
274
+ "metadata": {},
275
+ "outputs": [],
276
+ "source": [
277
+ "def calculate_parametric_var(returns, confidence):\n",
278
+ " \"\"\"Compute parametric VaR assuming normally distributed returns.\n",
279
+ "\n",
280
+ " VaR = -(mu - z * sigma) where z = norm.ppf(confidence).\n",
281
+ " Returns VaR as a positive loss fraction.\n",
282
+ " \"\"\"\n",
283
+ " mu, sigma = estimate_distribution(returns)\n",
284
+ " z = float(stats.norm.ppf(confidence))\n",
285
+ " return -(mu - z * sigma)\n",
286
+ "\n",
287
+ "\n",
288
+ "def calculate_parametric_es(returns, confidence):\n",
289
+ " \"\"\"Compute parametric ES assuming normally distributed returns.\n",
290
+ "\n",
291
+ " ES = -(mu - sigma * phi(z) / (1 - confidence)).\n",
292
+ " Returns ES as a positive loss fraction.\n",
293
+ " \"\"\"\n",
294
+ " mu, sigma = estimate_distribution(returns)\n",
295
+ " z = float(stats.norm.ppf(confidence))\n",
296
+ " alpha = 1.0 - confidence\n",
297
+ " return -(mu - sigma * float(stats.norm.pdf(z)) / alpha)"
298
+ ]
299
+ },
300
+ {
301
+ "cell_type": "code",
302
+ "execution_count": 10,
303
+ "id": "run_var",
304
+ "metadata": {},
305
+ "outputs": [
306
+ {
307
+ "name": "stdout",
308
+ "output_type": "stream",
309
+ "text": [
310
+ "1-Day VaR: 0.0417 (4.17%)\n",
311
+ "1-Day ES: 0.0481 (4.81%)\n"
312
+ ]
313
+ }
314
+ ],
315
+ "source": [
316
+ "var_pct = calculate_parametric_var(daily_returns, CONFIDENCE_LEVEL)\n",
317
+ "es_pct = calculate_parametric_es(daily_returns, CONFIDENCE_LEVEL)\n",
318
+ "print(f\"1-Day VaR: {var_pct:.4f} ({var_pct*100:.2f}%)\")\n",
319
+ "print(f\"1-Day ES: {es_pct:.4f} ({es_pct*100:.2f}%)\")"
320
+ ]
321
+ },
322
+ {
323
+ "cell_type": "markdown",
324
+ "id": "orchestration_header",
325
+ "metadata": {},
326
+ "source": [
327
+ "## 6. Orchestration of the Workflow"
328
+ ]
329
+ },
330
+ {
331
+ "cell_type": "code",
332
+ "execution_count": 11,
333
+ "id": "calculate_parametric_var",
334
+ "metadata": {},
335
+ "outputs": [],
336
+ "source": [
337
+ "def parametric_var_es_pipeline(ticker, confidence, lookback, n_days, portfolio_value, end_date=None):\n",
338
+ " \"\"\"Run the full parametric VaR workflow.\n",
339
+ "\n",
340
+ " Fetches data, estimates distribution parameters, computes 1-day VaR/ES\n",
341
+ " using the normal model, and scales results to the n-day horizon.\n",
342
+ " Returns dollar VaR/ES along with the underlying data.\n",
343
+ " \"\"\"\n",
344
+ " # 1. Fetch data and compute returns\n",
345
+ " prices = fetch_prices(ticker, lookback, end_date)\n",
346
+ " daily_returns = compute_returns(prices, kind=\"log\")\n",
347
+ " mu, sigma = estimate_distribution(daily_returns)\n",
348
+ "\n",
349
+ " # 2. Calculate 1-day VaR and ES\n",
350
+ " var_1d_pct = calculate_parametric_var(daily_returns, confidence)\n",
351
+ " es_1d_pct = calculate_parametric_es(daily_returns, confidence)\n",
352
+ " var_1d = var_1d_pct * portfolio_value\n",
353
+ " es_1d = es_1d_pct * portfolio_value\n",
354
+ "\n",
355
+ " # 3. Scale to N-day horizon\n",
356
+ " scaling_factor = np.sqrt(n_days)\n",
357
+ " var_nd = var_1d * scaling_factor\n",
358
+ " es_nd = es_1d * scaling_factor\n",
359
+ "\n",
360
+ " return {\n",
361
+ " \"var_1d\": var_1d,\n",
362
+ " \"var_nd\": var_nd,\n",
363
+ " \"es_1d\": es_1d,\n",
364
+ " \"es_nd\": es_nd,\n",
365
+ " \"prices\": prices,\n",
366
+ " \"daily_returns\": daily_returns,\n",
367
+ " \"mu\": mu,\n",
368
+ " \"sigma\": sigma,\n",
369
+ " }"
370
+ ]
371
+ },
372
+ {
373
+ "cell_type": "code",
374
+ "execution_count": 12,
375
+ "id": "run_analysis",
376
+ "metadata": {},
377
+ "outputs": [],
378
+ "source": [
379
+ "results = parametric_var_es_pipeline(\n",
380
+ " ticker=TICKER,\n",
381
+ " confidence=CONFIDENCE_LEVEL,\n",
382
+ " lookback=LOOKBACK_DAYS,\n",
383
+ " n_days=HORIZON_DAYS,\n",
384
+ " portfolio_value=PORTFOLIO_VALUE)"
385
+ ]
386
+ },
387
+ {
388
+ "cell_type": "code",
389
+ "execution_count": 13,
390
+ "id": "summary",
391
+ "metadata": {},
392
+ "outputs": [
393
+ {
394
+ "name": "stdout",
395
+ "output_type": "stream",
396
+ "text": [
397
+ "============================================================\n",
398
+ "PARAMETRIC VaR ANALYSIS SUMMARY - GOOG\n",
399
+ "============================================================\n",
400
+ "VaR Date: 2026-03-28\n",
401
+ "Portfolio Value: $1,000,000\n",
402
+ "Confidence Level: 99%\n",
403
+ "Time Horizon: 10 days\n",
404
+ "Historical Period: 251 trading days\n",
405
+ "\n",
406
+ "DISTRIBUTION PARAMETERS:\n",
407
+ "------------------------------------------------------------\n",
408
+ " Mean daily return (mu): 0.002162\n",
409
+ " Std dev (sigma): 0.018841\n",
410
+ "\n",
411
+ "VaR METRICS:\n",
412
+ "------------------------------------------------------------\n",
413
+ " 10-Day VaR: $131,766.21 (13.18%)\n",
414
+ " 10-Day ES: $151,955.80 (15.20%)\n",
415
+ "\n"
416
+ ]
417
+ }
418
+ ],
419
+ "source": [
420
+ "print(\"=\" * 60)\n",
421
+ "print(f\"PARAMETRIC VaR ANALYSIS SUMMARY - {TICKER}\")\n",
422
+ "print(\"=\" * 60)\n",
423
+ "print(f\"VaR Date: {datetime.now().strftime('%Y-%m-%d')}\")\n",
424
+ "print(f\"Portfolio Value: ${PORTFOLIO_VALUE:,}\")\n",
425
+ "print(f\"Confidence Level: {CONFIDENCE_LEVEL:.0%}\")\n",
426
+ "print(f\"Time Horizon: {HORIZON_DAYS} days\")\n",
427
+ "print(f\"Historical Period: {LOOKBACK_DAYS} trading days\")\n",
428
+ "print()\n",
429
+ "print(\"DISTRIBUTION PARAMETERS:\")\n",
430
+ "print('-'*60)\n",
431
+ "print(f\" Mean daily return (mu): {results['mu']:.6f}\")\n",
432
+ "print(f\" Std dev (sigma): {results['sigma']:.6f}\")\n",
433
+ "print()\n",
434
+ "print(\"VaR METRICS:\")\n",
435
+ "print('-'*60)\n",
436
+ "print(f\" {HORIZON_DAYS}-Day VaR: ${results['var_nd']:,.2f} ({results['var_nd']/PORTFOLIO_VALUE*100:.2f}%)\")\n",
437
+ "print(f\" {HORIZON_DAYS}-Day ES: ${results['es_nd']:,.2f} ({results['es_nd']/PORTFOLIO_VALUE*100:.2f}%)\")\n",
438
+ "print()"
439
+ ]
440
+ },
441
+ {
442
+ "cell_type": "markdown",
443
+ "id": "tests_header",
444
+ "metadata": {},
445
+ "source": [
446
+ "## 7. Assumption Tests\n",
447
+ "\n",
448
+ "Parametric VaR rests on specific assumptions. The tests below verify that:\n",
449
+ "1. Returns are assumed to be **normally distributed**.\n",
450
+ "2. VaR is computed from **mean and standard deviation**.\n",
451
+ "3. **Tail risk is driven purely by volatility**."
452
+ ]
453
+ },
454
+ {
455
+ "cell_type": "markdown",
456
+ "id": "test1_intro",
457
+ "metadata": {},
458
+ "source": [
459
+ "### Test 1 – Normality of Returns\n",
460
+ "\n",
461
+ "Parametric VaR assumes that daily returns follow a normal distribution. \n",
462
+ "We apply two statistical tests to evaluate this assumption:\n",
463
+ "\n",
464
+ "| Test | Purpose | Best for |\n",
465
+ "| --- | --- | --- |\n",
466
+ "| **Shapiro-Wilk** | General normality test (primary) | Small-to-medium samples (~250) |\n",
467
+ "| **Jarque-Bera** | Tests skewness & kurtosis specifically | Diagnosing *how* normality fails |\n",
468
+ "\n",
469
+ "A **p-value > 0.05** means we cannot reject the null hypothesis of normality\n",
470
+ "at the 5% significance level. The Shapiro-Wilk result drives the overall\n",
471
+ "conclusion; Jarque-Bera provides supporting detail."
472
+ ]
473
+ },
474
+ {
475
+ "cell_type": "code",
476
+ "execution_count": 14,
477
+ "id": "test_normality_fn",
478
+ "metadata": {},
479
+ "outputs": [],
480
+ "source": [
481
+ "def test_normality(returns):\n",
482
+ " \"\"\"Test whether returns follow a normal distribution.\n",
483
+ "\n",
484
+ " Runs both the Shapiro-Wilk test (primary, suitable for n ~ 250)\n",
485
+ " and the Jarque-Bera test (supporting, focuses on skewness and\n",
486
+ " kurtosis). Returns a dict with all test statistics.\n",
487
+ " \"\"\"\n",
488
+ " # Shapiro-Wilk (primary)\n",
489
+ " sw_stat, sw_p = stats.shapiro(returns)\n",
490
+ "\n",
491
+ " # Jarque-Bera (supporting diagnostic)\n",
492
+ " jb_stat, jb_p = stats.jarque_bera(returns)\n",
493
+ "\n",
494
+ " # Descriptive shape statistics\n",
495
+ " skewness = returns.skew()\n",
496
+ " excess_kurtosis = returns.kurtosis() # normal = 0\n",
497
+ "\n",
498
+ " return {\n",
499
+ " \"sw_stat\": sw_stat,\n",
500
+ " \"sw_pvalue\": sw_p,\n",
501
+ " \"jb_stat\": jb_stat,\n",
502
+ " \"jb_pvalue\": jb_p,\n",
503
+ " \"skewness\": skewness,\n",
504
+ " \"excess_kurtosis\": excess_kurtosis,\n",
505
+ " \"n_obs\": len(returns),\n",
506
+ " \"is_normal\": sw_p > 0.05\n",
507
+ " }"
508
+ ]
509
+ },
510
+ {
511
+ "cell_type": "code",
512
+ "execution_count": 15,
513
+ "id": "run_test_normality",
514
+ "metadata": {},
515
+ "outputs": [
516
+ {
517
+ "name": "stdout",
518
+ "output_type": "stream",
519
+ "text": [
520
+ "============================================================\n",
521
+ "TEST 1: Normality of Returns\n",
522
+ "============================================================\n",
523
+ " Sample size: 250\n",
524
+ "\n",
525
+ "SHAPE STATISTICS:\n",
526
+ "------------------------------------------------------------\n",
527
+ " Skewness: 0.5195 (normal ~ 0)\n",
528
+ " Excess kurtosis: 4.2984 (normal ~ 0)\n",
529
+ "\n",
530
+ "PRIMARY TEST – Shapiro-Wilk:\n",
531
+ "------------------------------------------------------------\n",
532
+ " Statistic: 0.949339\n",
533
+ " p-value: 0.000000\n",
534
+ "\n",
535
+ "SUPPORTING TEST – Jarque-Bera:\n",
536
+ "------------------------------------------------------------\n",
537
+ " Statistic: 193.9030\n",
538
+ " p-value: 0.000000\n",
539
+ "\n",
540
+ "CONCLUSION (based on Shapiro-Wilk):\n",
541
+ "============================================================\n",
542
+ " WARNING - Normality rejected (Shapiro-Wilk p < 0.05).\n",
543
+ " Returns may exhibit fat tails or skew; Parametric VaR could underestimate true tail risk.\n",
544
+ " Data is NOT normal as per Shapiro-Wilk test.\n"
545
+ ]
546
+ }
547
+ ],
548
+ "source": [
549
+ "normality = test_normality(daily_returns)\n",
550
+ "\n",
551
+ "print(\"=\" * 60)\n",
552
+ "print(\"TEST 1: Normality of Returns\")\n",
553
+ "print(\"=\" * 60)\n",
554
+ "print(f\" Sample size: {normality['n_obs']}\")\n",
555
+ "print()\n",
556
+ "print(\"SHAPE STATISTICS:\")\n",
557
+ "print(\"-\" * 60)\n",
558
+ "print(f\" Skewness: {normality['skewness']:.4f} (normal ~ 0)\")\n",
559
+ "print(f\" Excess kurtosis: {normality['excess_kurtosis']:.4f} (normal ~ 0)\")\n",
560
+ "print()\n",
561
+ "print(\"PRIMARY TEST – Shapiro-Wilk:\")\n",
562
+ "print(\"-\" * 60)\n",
563
+ "print(f\" Statistic: {normality['sw_stat']:.6f}\")\n",
564
+ "print(f\" p-value: {normality['sw_pvalue']:.6f}\")\n",
565
+ "print()\n",
566
+ "print(\"SUPPORTING TEST – Jarque-Bera:\")\n",
567
+ "print(\"-\" * 60)\n",
568
+ "print(f\" Statistic: {normality['jb_stat']:.4f}\")\n",
569
+ "print(f\" p-value: {normality['jb_pvalue']:.6f}\")\n",
570
+ "print()\n",
571
+ "print(\"CONCLUSION (based on Shapiro-Wilk):\")\n",
572
+ "print(\"=\" * 60)\n",
573
+ "if normality[\"is_normal\"]:\n",
574
+ " print(\" PASS - Cannot reject normality at 5% significance.\")\n",
575
+ " print(\" The normal-distribution assumption is reasonable for this sample.\")\n",
576
+ " print(\" Data is considered normal as per Shapiro-Wilk test.\")\n",
577
+ "else:\n",
578
+ " print(\" WARNING - Normality rejected (Shapiro-Wilk p < 0.05).\")\n",
579
+ " print(\" Returns may exhibit fat tails or skew; Parametric VaR could underestimate true tail risk.\")\n",
580
+ " print(\" Data is NOT normal as per Shapiro-Wilk test.\")"
581
+ ]
582
+ }
583
+ ],
584
+ "metadata": {
585
+ "kernelspec": {
586
+ "display_name": "value-at-risk (3.13.5)",
587
+ "language": "python",
588
+ "name": "python3"
589
+ },
590
+ "language_info": {
591
+ "codemirror_mode": {
592
+ "name": "ipython",
593
+ "version": 3
594
+ },
595
+ "file_extension": ".py",
596
+ "mimetype": "text/x-python",
597
+ "name": "python",
598
+ "nbconvert_exporter": "python",
599
+ "pygments_lexer": "ipython3",
600
+ "version": "3.13.5"
601
+ }
602
+ },
603
+ "nbformat": 4,
604
+ "nbformat_minor": 5
605
+ }
pyproject.toml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "value-at-risk"
3
+ version = "0.1.0"
4
+ description = "Portfolio Value at Risk (VaR) Analysis Engine"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "gradio>=6.9.0",
9
+ "loguru>=0.7.3",
10
+ "matplotlib>=3.10.8",
11
+ "numpy>=2.4.3",
12
+ "openpyxl>=3.1.5",
13
+ "pandas>=3.0.1",
14
+ "plotly>=6.6.0",
15
+ "kaleido>=0.2.1",
16
+ "yfinance>=1.2.0",
17
+ "scipy>=1.17.1",
18
+ ]
19
+
20
+ [tool.pyright]
21
+ include = ["."]
22
+ extraPaths = ["."]
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=6.9.0
2
+ loguru>=0.7.3
3
+ matplotlib>=3.10.8
4
+ numpy>=2.4.3
5
+ openpyxl>=3.1.5
6
+ pandas>=3.0.1
7
+ plotly>=6.6.0
8
+ kaleido>=0.2.1
9
+ yfinance>=1.2.0
10
+ scipy>=1.17.1
src/__init__.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """VaR business logic."""
2
+
3
+ from src.logger import logger
4
+
5
+ from src.utils import (
6
+ fetch_prices,
7
+ compute_returns,
8
+ plot_distribution,
9
+ )
10
+ from src.historical import (
11
+ calculate_historical_var,
12
+ calculate_historical_es,
13
+ compute_historical_var_es,
14
+ compute_stressed_historical_var_es,
15
+ historical_var_es_pipeline,
16
+ )
17
+ from src.parametric import (
18
+ estimate_distribution,
19
+ calculate_parametric_var,
20
+ calculate_parametric_es,
21
+ compute_parametric_var_es,
22
+ compute_stressed_parametric_var_es,
23
+ parametric_var_es_pipeline,
24
+ )
25
+ from src.excel_export import export_historical_var_report, export_parametric_var_report
26
+ from src.config import (
27
+ TICKERS,
28
+ LOOKBACK_DAYS,
29
+ STRESS_LABEL,
30
+ STRESS_START_DATE,
31
+ STRESS_END_DATE,
32
+ )
33
+
34
+ __all__ = [
35
+ "logger",
36
+ "fetch_prices",
37
+ "compute_returns",
38
+ "plot_distribution",
39
+ "calculate_historical_var",
40
+ "calculate_historical_es",
41
+ "compute_historical_var_es",
42
+ "compute_stressed_historical_var_es",
43
+ "historical_var_es_pipeline",
44
+ "estimate_distribution",
45
+ "calculate_parametric_var",
46
+ "calculate_parametric_es",
47
+ "compute_parametric_var_es",
48
+ "compute_stressed_parametric_var_es",
49
+ "parametric_var_es_pipeline",
50
+ "export_historical_var_report",
51
+ "export_parametric_var_report",
52
+ "TICKERS",
53
+ "LOOKBACK_DAYS",
54
+ "STRESS_LABEL",
55
+ "STRESS_START_DATE",
56
+ "STRESS_END_DATE",
57
+ ]
src/config.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ config.py -- Load application configuration from config.yaml.
3
+
4
+ Usage:
5
+ from src.config import TICKERS, LOOKBACK_DAYS, STRESS_LABEL, STRESS_START_DATE, STRESS_END_DATE
6
+ """
7
+
8
+ from pathlib import Path
9
+
10
+ import yaml
11
+
12
+ _CONFIG_PATH = Path(__file__).resolve().parent.parent / "config.yaml"
13
+
14
+ with open(_CONFIG_PATH) as _f:
15
+ _cfg = yaml.safe_load(_f)
16
+
17
+ TICKERS: list[str] = _cfg["tickers"]
18
+ LOOKBACK_DAYS: int = _cfg["lookback_days"]
19
+ STRESS_LABEL: str = _cfg["stressed_period_label"]
20
+ STRESS_START_DATE: str = _cfg["stressed_period_start_date"]
21
+ STRESS_END_DATE: str = _cfg["stressed_period_end_date"]
src/excel_export.py ADDED
@@ -0,0 +1,540 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ excel_export.py -- Excel workbook creation with natively calculated VaR/ES formulas.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import math
8
+ import os
9
+ from datetime import date
10
+ import openpyxl
11
+ from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
12
+ from openpyxl.worksheet.worksheet import Worksheet
13
+ import pandas as pd
14
+ from loguru import logger
15
+
16
+ # Cell values can be str, int, float, None, etc.
17
+ CellValue = str | int | float | None
18
+ SummaryRow = list[CellValue]
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Shared styles
23
+ # ---------------------------------------------------------------------------
24
+
25
+ HEADER_FILL = PatternFill(start_color="1F497D", end_color="1F497D", fill_type="solid")
26
+ HEADER_FONT = Font(color="FFFFFF", bold=True)
27
+ VAR_95_FILL = PatternFill(start_color="FFD966", end_color="FFD966", fill_type="solid") # orangish-yellow
28
+ VAR_99_FILL = PatternFill(start_color="F6C96C", end_color="F6C96C", fill_type="solid") # light amber
29
+ THIN_BORDER = Border(
30
+ left=Side(style="thin"),
31
+ right=Side(style="thin"),
32
+ top=Side(style="thin"),
33
+ bottom=Side(style="thin"),
34
+ )
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Public helpers
39
+ # ---------------------------------------------------------------------------
40
+
41
+
42
+ def make_output_dir() -> str:
43
+ """Create and return the directory ``output/{YYYY-MM-DD}/``."""
44
+ dir_path = os.path.join("output", date.today().strftime("%Y-%m-%d"))
45
+ os.makedirs(dir_path, exist_ok=True)
46
+ logger.debug(f"Output directory: {dir_path}")
47
+ return dir_path
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Data columns (A-D) -- shared between Historical and Parametric
52
+ # ---------------------------------------------------------------------------
53
+
54
+
55
+ def _write_data_columns(
56
+ worksheet: Worksheet,
57
+ prices: pd.Series,
58
+ max_data_row: int,
59
+ method: str,
60
+ ) -> None:
61
+ """Write date/price headers and rows into columns A-D.
62
+
63
+ Column C:
64
+ - Historical: -(P_t - P_{t-1}) / P_{t-1} (arithmetic loss, positive = loss)
65
+ - Parametric: LN(P_t / P_{t-1}) (log return, negative = loss)
66
+ Column D:
67
+ - Historical: LARGE() -- losses descending (worst first)
68
+ - Parametric: SMALL() -- returns ascending (worst first)
69
+ """
70
+ if method == "Historical":
71
+ col_c_header = "Daily Arithmetic Return"
72
+ col_d_header = "Sorted Return"
73
+ else:
74
+ col_c_header = "Daily Log Return"
75
+ col_d_header = "Sorted Return"
76
+ center = Alignment(horizontal="right")
77
+
78
+ headers = ["Date", "Close Price", col_c_header, col_d_header]
79
+ for col_idx, header in enumerate(headers, start=1):
80
+ cell = worksheet.cell(row=1, column=col_idx, value=header)
81
+ cell.fill = HEADER_FILL
82
+ cell.font = HEADER_FONT
83
+ cell.border = THIN_BORDER
84
+ if col_idx in (2, 3, 4):
85
+ cell.alignment = center
86
+
87
+ dates = pd.DatetimeIndex(prices.index)
88
+ price_values = prices.values
89
+
90
+ for i in range(len(prices)):
91
+ row = i + 2
92
+ worksheet.cell(row=row, column=1, value=dates[i].strftime("%Y-%m-%d"))
93
+ price_cell = worksheet.cell(row=row, column=2, value=float(price_values[i]))
94
+ price_cell.alignment = center
95
+
96
+ if row > 2:
97
+ if method == "Historical":
98
+ col_c_formula = f"=(B{row}-B{row - 1})/B{row - 1}"
99
+ col_d_formula = f"=SMALL(C$3:C${max_data_row}, ROW()-2)"
100
+ else:
101
+ col_c_formula = f"=LN(B{row}/B{row - 1})"
102
+ col_d_formula = f"=SMALL(C$3:C${max_data_row}, ROW()-2)"
103
+ return_cell = worksheet.cell(row=row, column=3, value=col_c_formula)
104
+ return_cell.number_format = "0.0000%"
105
+ return_cell.alignment = center
106
+
107
+ sorted_cell = worksheet.cell(row=row, column=4, value=col_d_formula)
108
+ sorted_cell.number_format = "0.0000%"
109
+ sorted_cell.alignment = center
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # VaR / ES formulas -- method-specific
114
+ # ---------------------------------------------------------------------------
115
+
116
+
117
+ def _var_dollar_formula(method: str, max_data_row: int, alpha: float, pv_ref: str) -> str:
118
+ """Return the Excel formula for 1-Day VaR ($) — positive = loss.
119
+
120
+ Historical: PERCENTILE(losses, confidence) * V
121
+ Parametric: -V * (mu - z_alpha * sigma) where column C = LN returns
122
+ """
123
+ confidence = 1.0 - alpha
124
+ rng = f"C$3:C${max_data_row}"
125
+ if method == "Historical":
126
+ return f"=-PERCENTILE({rng},{alpha})*{pv_ref}"
127
+ else:
128
+ return (
129
+ f"=-{pv_ref}*(AVERAGE({rng})"
130
+ f"-_xlfn.NORM.S.INV({confidence})*_xlfn.STDEV.S({rng}))"
131
+ )
132
+
133
+
134
+ def _es_dollar_formula(method: str, max_data_row: int, alpha: float, pv_ref: str) -> str:
135
+ """Return the Excel formula for 1-Day ES ($) — positive = loss.
136
+
137
+ Historical: ES = E[loss | loss > VaR] where loss = -return
138
+ -AVERAGEIF(returns < VaR_threshold) * V
139
+ Parametric: -V * (mu - sigma * phi(z) / alpha)
140
+ """
141
+ confidence = 1.0 - alpha
142
+ rng = f"C$3:C${max_data_row}"
143
+ if method == "Historical":
144
+ var_threshold = f"PERCENTILE({rng},{alpha})"
145
+ return f'=-AVERAGEIF({rng},"<"&{var_threshold})*{pv_ref}'
146
+ else:
147
+ return (
148
+ f"=-{pv_ref}*(AVERAGE({rng})"
149
+ f"-_xlfn.STDEV.S({rng})"
150
+ f"*_xlfn.NORM.DIST(_xlfn.NORM.S.INV({confidence}),0,1,FALSE)/{alpha})"
151
+ )
152
+
153
+
154
+ # ---------------------------------------------------------------------------
155
+ # Core export (shared between Historical and Parametric)
156
+ # ---------------------------------------------------------------------------
157
+
158
+
159
+ def _export_sheet(
160
+ method: str,
161
+ path: str,
162
+ prices: pd.Series,
163
+ ticker: str,
164
+ n_days: int,
165
+ portfolio_value: float,
166
+ var_date: pd.Timestamp | None,
167
+ stressed: bool,
168
+ lookback: int | None,
169
+ stress_start: str,
170
+ stress_end: str,
171
+ stress_label: str,
172
+ var_confidence: float = 0.99,
173
+ es_confidence: float = 0.975,
174
+ ) -> str:
175
+ """Create or append a VaR/ES sheet to an Excel workbook.
176
+
177
+ ``method`` must be ``"Historical"`` or ``"Parametric"``.
178
+ """
179
+
180
+ # ---- 1. Workbook / sheet setup -----------------------------------------
181
+
182
+ sheet_title = "VaR and ES"
183
+ stressed_sheet_title = "Stressed VaR and ES"
184
+
185
+ if stressed:
186
+ workbook = openpyxl.load_workbook(path)
187
+ worksheet = workbook.create_sheet(title=stressed_sheet_title)
188
+ else:
189
+ workbook = openpyxl.Workbook()
190
+ worksheet = workbook.active
191
+ assert worksheet is not None
192
+ worksheet.title = sheet_title
193
+
194
+ max_data_row = len(prices) + 1
195
+
196
+ # ---- 2. Write data columns A-D -----------------------------------------
197
+
198
+ _write_data_columns(worksheet, prices, max_data_row, method)
199
+
200
+ # ---- 2b. Highlight sorted returns at 95% and 99% VaR positions ---------
201
+
202
+ n_returns = max_data_row - 2 # returns start at row 3
203
+ for alpha, fill in [(0.05, VAR_95_FILL), (0.01, VAR_99_FILL)]:
204
+ pos = alpha * n_returns
205
+ lo = math.floor(pos)
206
+ hi = math.ceil(pos)
207
+ for k in {lo, hi}:
208
+ if 1 <= k <= n_returns:
209
+ worksheet.cell(row=k + 2, column=4).fill = fill # column D
210
+
211
+ # ---- 3. Build parameter entries ----------------------------------------
212
+
213
+ date_str = var_date.strftime("%Y-%m-%d") if var_date is not None else ""
214
+
215
+ if stressed:
216
+ param_entries: list[SummaryRow] = [
217
+ ["Method", method, ""],
218
+ ["Ticker", ticker, ""],
219
+ ["VaR Date", date_str, ""],
220
+ ["Portfolio Value ($)", portfolio_value, ""],
221
+ ["N-Day Horizon", n_days, ""],
222
+ ["Stress Period Start Date", stress_start, ""],
223
+ ["Stress Period End Date", stress_end, ""],
224
+ ["Stress Period", stress_label, ""],
225
+ ]
226
+ else:
227
+ param_entries = [
228
+ ["Method", method, ""],
229
+ ["Ticker", ticker, ""],
230
+ ["VaR Date", date_str, ""],
231
+ ["Portfolio Value ($)", portfolio_value, ""],
232
+ ["N-Day Horizon", n_days, ""],
233
+ ["Return Observations", lookback - 1 if lookback else "", ""],
234
+ ]
235
+
236
+ # ---- 4. Compute row layout ---------------------------------------------
237
+ #
238
+ # Layout (0-based indices into summary_data):
239
+ # [0] Parameter header
240
+ # [1..N] param_entries (N = len(param_entries))
241
+ # [N+1] separator
242
+ # [N+2] Standard header (Risk Metric | 99% VaR ($) | 97.5% ES ($))
243
+ # [N+3] Standard 1-Day
244
+ # [N+4] Standard 10-Day Scaled
245
+ # [N+5] separator
246
+ # [N+6] Custom header (Risk Metric | VaR X% | ES Y%)
247
+ # [N+7] Custom 1-Day
248
+ # [N+8] Custom 10-Day Scaled
249
+ # [N+9] Custom n-Day Scaled (only when n_days != 10)
250
+
251
+ summary_start_row = 2
252
+ summary_start_col = 7 # Column G
253
+
254
+ N_params = len(param_entries)
255
+ # Portfolio Value is always param_entries[3]; entries start at absolute index 1
256
+ portfolio_value_abs_idx = 1 + 3 # = 4
257
+ pv_ref = f"$H${summary_start_row + portfolio_value_abs_idx}"
258
+
259
+ param_header_idx = 0
260
+ param_separator_idx = N_params + 1
261
+
262
+ std_header_idx = param_separator_idx + 1
263
+ std_1day_idx = std_header_idx + 1
264
+ std_10day_idx = std_1day_idx + 1
265
+ std_separator_idx = std_10day_idx + 1
266
+
267
+ custom_header_idx = std_separator_idx + 1
268
+ custom_1day_idx = custom_header_idx + 1
269
+ custom_10day_idx = custom_1day_idx + 1
270
+ custom_nday_idx: int | None = None
271
+ if n_days != 10:
272
+ custom_nday_idx = custom_10day_idx + 1
273
+
274
+ # ---- 5. Build standard table (fixed 99% VaR / 97.5% ES) ----------------
275
+
276
+ # Only show custom table when the selected levels differ from the standard (99%/97.5%)
277
+ show_custom = not (var_confidence == 0.99 and es_confidence == 0.975)
278
+
279
+ std_1day_h = f"H{summary_start_row + std_1day_idx}"
280
+ std_1day_i = f"I{summary_start_row + std_1day_idx}"
281
+
282
+ std_header_label = "Standard Stressed Risk Summary" if stressed else "Standard Risk Summary"
283
+
284
+ std_rows: list[SummaryRow] = [
285
+ [std_header_label, "99% VaR ($)", "97.5% ES ($)"],
286
+ ["1-Day",
287
+ _var_dollar_formula(method, max_data_row, 0.01, pv_ref),
288
+ _es_dollar_formula(method, max_data_row, 0.025, pv_ref)],
289
+ ["10-Day Scaled",
290
+ f"={std_1day_h} * SQRT(10)",
291
+ f"={std_1day_i} * SQRT(10)"],
292
+ ]
293
+ std_nday_idx: int | None = None
294
+ if n_days != 10 and not show_custom:
295
+ std_nday_idx = std_10day_idx + 1
296
+ std_rows.append([
297
+ f"{n_days}-Day Scaled",
298
+ f"={std_1day_h} * SQRT({n_days})",
299
+ f"={std_1day_i} * SQRT({n_days})",
300
+ ])
301
+
302
+ # ---- 6. Build custom table (user-selected confidence levels) ------------
303
+
304
+ var_alpha = 1.0 - var_confidence
305
+ es_alpha = 1.0 - es_confidence
306
+ var_conf_label = f"{var_confidence * 100:g}%"
307
+ es_conf_label = f"{es_confidence * 100:g}%"
308
+
309
+ custom_1day_h = f"H{summary_start_row + custom_1day_idx}"
310
+ custom_1day_i = f"I{summary_start_row + custom_1day_idx}"
311
+
312
+ custom_header_label = "Stressed Risk Summary" if stressed else "Risk Summary"
313
+
314
+ custom_rows: list[SummaryRow] = [
315
+ [custom_header_label, f"{var_conf_label} VaR ($)", f"{es_conf_label} ES ($)"],
316
+ ["1-Day",
317
+ _var_dollar_formula(method, max_data_row, var_alpha, pv_ref),
318
+ _es_dollar_formula(method, max_data_row, es_alpha, pv_ref)],
319
+ ["10-Day Scaled",
320
+ f"={custom_1day_h} * SQRT(10)",
321
+ f"={custom_1day_i} * SQRT(10)"],
322
+ ]
323
+ if n_days != 10:
324
+ custom_rows.append([
325
+ f"{n_days}-Day Scaled",
326
+ f"={custom_1day_h} * SQRT({n_days})",
327
+ f"={custom_1day_i} * SQRT({n_days})",
328
+ ])
329
+
330
+ # ---- 7. Assemble full summary ------------------------------------------
331
+
332
+ empty_row: SummaryRow = ["", "", ""]
333
+ summary_data: list[SummaryRow] = []
334
+ summary_data.append(["Parameter", "Value", ""]) # index 0
335
+ summary_data.extend(param_entries) # indices 1..N
336
+ summary_data.append(empty_row) # index N+1
337
+ summary_data.extend(std_rows) # indices N+2..N+4
338
+ if show_custom:
339
+ summary_data.append(empty_row) # index N+5
340
+ summary_data.extend(custom_rows) # indices N+6..
341
+
342
+ # ---- 9. Write and style the summary ------------------------------------
343
+
344
+ # Indices of param-section rows (skip col I for these)
345
+ param_all_indices = {param_header_idx} | set(range(1, 1 + N_params))
346
+
347
+ # Which rows to right-align value (col H) — param entries except Portfolio Value
348
+ right_align_indices = set(range(1, 1 + N_params)) - {portfolio_value_abs_idx}
349
+
350
+ # Which rows get money ($) formatting
351
+ money_format_indices: set[int] = {
352
+ portfolio_value_abs_idx,
353
+ std_1day_idx, std_10day_idx,
354
+ }
355
+ if std_nday_idx is not None:
356
+ money_format_indices.add(std_nday_idx)
357
+ if show_custom:
358
+ money_format_indices |= {custom_1day_idx, custom_10day_idx}
359
+ if custom_nday_idx is not None:
360
+ money_format_indices.add(custom_nday_idx)
361
+
362
+ # Which rows get dark-blue header styling
363
+ section_header_indices = {param_header_idx, std_header_idx}
364
+ if show_custom:
365
+ section_header_indices.add(custom_header_idx)
366
+
367
+ # Which rows skip all styling (separators)
368
+ unstyled_indices = {param_separator_idx}
369
+ if show_custom:
370
+ unstyled_indices.add(std_separator_idx)
371
+
372
+ for data_index, row_data in enumerate(summary_data):
373
+ sheet_row = summary_start_row + data_index
374
+ for col_offset, value in enumerate(row_data):
375
+ # Third column (col I) is unused for parameter rows
376
+ if data_index in param_all_indices and col_offset == 2:
377
+ continue
378
+
379
+ col = summary_start_col + col_offset
380
+ cell = worksheet.cell(row=sheet_row, column=col, value=value) # type: ignore[arg-type]
381
+
382
+ # Skip styling for empty separators
383
+ if data_index in unstyled_indices:
384
+ continue
385
+
386
+ cell.border = THIN_BORDER
387
+
388
+ if data_index in section_header_indices:
389
+ cell.fill = HEADER_FILL
390
+ cell.font = HEADER_FONT
391
+ if col == 8:
392
+ cell.alignment = Alignment(horizontal="right")
393
+ elif col == 9 and data_index != param_header_idx:
394
+ cell.alignment = Alignment(horizontal="right")
395
+
396
+ if data_index in right_align_indices and col_offset == 1:
397
+ cell.alignment = Alignment(horizontal="right")
398
+
399
+ if col in (8, 9): # Columns H and I
400
+ if data_index in money_format_indices:
401
+ cell.number_format = '"$"#,##0.00'
402
+
403
+ # ---- 10. Column widths -------------------------------------------------
404
+
405
+ column_widths = {
406
+ "A": 12, "B": 15, "C": 20, "D": 15,
407
+ "E": 5, "F": 5,
408
+ "G": 27, "H": 24, "I": 24,
409
+ }
410
+ for column_letter, width in column_widths.items():
411
+ worksheet.column_dimensions[column_letter].width = width
412
+
413
+ # ---- 11. Save ----------------------------------------------------------
414
+
415
+ workbook.save(path)
416
+ action = "sheet added" if stressed else "report saved"
417
+ logger.info(f"{method} VaR ES {action}: {path}")
418
+ return path
419
+
420
+
421
+ # ---------------------------------------------------------------------------
422
+ # API -- thin wrappers around _export_sheet
423
+ # ---------------------------------------------------------------------------
424
+
425
+
426
+ def export_historical_var_sheet(
427
+ path: str,
428
+ prices: pd.Series,
429
+ ticker: str,
430
+ n_days: int,
431
+ portfolio_value: float,
432
+ var_date: pd.Timestamp | None = None,
433
+ stressed: bool = False,
434
+ lookback: int | None = None,
435
+ stress_start: str = "",
436
+ stress_end: str = "",
437
+ stress_label: str = "",
438
+ var_confidence: float = 0.99,
439
+ es_confidence: float = 0.975,
440
+ ) -> str:
441
+ """Create or append a Historical VaR/ES sheet to an Excel workbook."""
442
+ return _export_sheet(
443
+ "Historical", path, prices, ticker, n_days, portfolio_value,
444
+ var_date, stressed, lookback, stress_start, stress_end, stress_label,
445
+ var_confidence, es_confidence,
446
+ )
447
+
448
+
449
+ def export_parametric_var_sheet(
450
+ path: str,
451
+ prices: pd.Series,
452
+ ticker: str,
453
+ n_days: int,
454
+ portfolio_value: float,
455
+ var_date: pd.Timestamp | None = None,
456
+ stressed: bool = False,
457
+ lookback: int | None = None,
458
+ stress_start: str = "",
459
+ stress_end: str = "",
460
+ stress_label: str = "",
461
+ var_confidence: float = 0.99,
462
+ es_confidence: float = 0.975,
463
+ ) -> str:
464
+ """Create or append a Parametric VaR/ES sheet to an Excel workbook."""
465
+ return _export_sheet(
466
+ "Parametric", path, prices, ticker, n_days, portfolio_value,
467
+ var_date, stressed, lookback, stress_start, stress_end, stress_label,
468
+ var_confidence, es_confidence,
469
+ )
470
+
471
+
472
+ # ---------------------------------------------------------------------------
473
+ # Report-level exports (output dir + both normal & stressed sheets)
474
+ # ---------------------------------------------------------------------------
475
+
476
+
477
+ def export_historical_var_report(
478
+ prices: pd.Series,
479
+ ticker: str,
480
+ n_days: int,
481
+ portfolio_value: float,
482
+ var_date: pd.Timestamp | None,
483
+ lookback: int,
484
+ stressed_prices: pd.Series,
485
+ stress_start: str,
486
+ stress_end: str,
487
+ stress_label: str,
488
+ var_confidence: float = 0.99,
489
+ es_confidence: float = 0.975,
490
+ ) -> str:
491
+ """Generate a full Historical VaR Excel report (normal + stressed sheets)."""
492
+ output_dir = make_output_dir()
493
+ date_str = var_date.strftime("%Y-%m-%d") if var_date else ""
494
+ excel_path = os.path.join(output_dir, f"{ticker}_{date_str}_Historical_VaR.xlsx")
495
+
496
+ export_historical_var_sheet(
497
+ path=excel_path, prices=prices, ticker=ticker, n_days=n_days,
498
+ portfolio_value=portfolio_value, var_date=var_date, stressed=False,
499
+ lookback=lookback, var_confidence=var_confidence, es_confidence=es_confidence,
500
+ )
501
+ export_historical_var_sheet(
502
+ path=excel_path, prices=stressed_prices, ticker=ticker, n_days=n_days,
503
+ portfolio_value=portfolio_value, var_date=var_date, stressed=True,
504
+ stress_start=stress_start, stress_end=stress_end, stress_label=stress_label,
505
+ var_confidence=var_confidence, es_confidence=es_confidence,
506
+ )
507
+ return excel_path
508
+
509
+
510
+ def export_parametric_var_report(
511
+ prices: pd.Series,
512
+ ticker: str,
513
+ n_days: int,
514
+ portfolio_value: float,
515
+ var_date: pd.Timestamp | None,
516
+ lookback: int,
517
+ stressed_prices: pd.Series,
518
+ stress_start: str,
519
+ stress_end: str,
520
+ stress_label: str,
521
+ var_confidence: float = 0.99,
522
+ es_confidence: float = 0.975,
523
+ ) -> str:
524
+ """Generate a full Parametric VaR Excel report (normal + stressed sheets)."""
525
+ output_dir = make_output_dir()
526
+ date_str = var_date.strftime("%Y-%m-%d") if var_date else ""
527
+ excel_path = os.path.join(output_dir, f"{ticker}_{date_str}_Parametric_VaR.xlsx")
528
+
529
+ export_parametric_var_sheet(
530
+ path=excel_path, prices=prices, ticker=ticker, n_days=n_days,
531
+ portfolio_value=portfolio_value, var_date=var_date, stressed=False,
532
+ lookback=lookback, var_confidence=var_confidence, es_confidence=es_confidence,
533
+ )
534
+ export_parametric_var_sheet(
535
+ path=excel_path, prices=stressed_prices, ticker=ticker, n_days=n_days,
536
+ portfolio_value=portfolio_value, var_date=var_date, stressed=True,
537
+ stress_start=stress_start, stress_end=stress_end, stress_label=stress_label,
538
+ var_confidence=var_confidence, es_confidence=es_confidence,
539
+ )
540
+ return excel_path
src/historical.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ historical_var.py -- Historical Value at Risk calculation and analysis pipeline.
3
+ """
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from loguru import logger
8
+ from src.utils import fetch_prices, compute_returns, plot_distribution
9
+ from src.excel_export import export_historical_var_report
10
+
11
+
12
+ def calculate_historical_var(returns: pd.Series, confidence: float) -> float:
13
+ """Return VaR as a positive loss value.
14
+
15
+ VaR = (1 - confidence) percentile of returns, negated to express as loss.
16
+ """
17
+ vals = returns.values
18
+ return -float(np.percentile(np.asarray(vals), (1.0 - confidence) * 100))
19
+
20
+
21
+ def calculate_historical_es(returns: pd.Series, confidence: float) -> float:
22
+ """Return ES as a positive loss value.
23
+
24
+ ES = E[loss | loss > VaR], the mean of losses exceeding VaR.
25
+ """
26
+ var = calculate_historical_var(returns, confidence)
27
+ losses = -np.asarray(returns.values)
28
+ tail = losses[losses > var]
29
+ return float(np.mean(tail)) if len(tail) > 0 else var
30
+
31
+
32
+
33
+ def compute_historical_var_es(
34
+ returns: pd.Series,
35
+ var_confidence: float,
36
+ es_confidence: float,
37
+ n_days: int,
38
+ portfolio_value: float,
39
+ ) -> dict:
40
+ """Compute VaR and ES from returns and scale to n-day horizon.
41
+
42
+ Returns a dict with 1-day and n-day dollar VaR and ES.
43
+ """
44
+ var_1d_pct = calculate_historical_var(returns, var_confidence)
45
+ es_1d_pct = calculate_historical_es(returns, es_confidence)
46
+ var_1d = var_1d_pct * portfolio_value
47
+ es_1d = es_1d_pct * portfolio_value
48
+
49
+ scaling_factor = np.sqrt(n_days)
50
+ var_nd = var_1d * scaling_factor
51
+ es_nd = es_1d * scaling_factor
52
+
53
+ return {
54
+ "var_1d": var_1d,
55
+ "var_nd": var_nd,
56
+ "es_1d": es_1d,
57
+ "es_nd": es_nd,
58
+ }
59
+
60
+
61
+ def compute_stressed_historical_var_es(
62
+ ticker: str,
63
+ var_confidence: float,
64
+ es_confidence: float,
65
+ n_days: int,
66
+ portfolio_value: float,
67
+ stress_start: str,
68
+ stress_end: str,
69
+ stress_label: str,
70
+ ) -> dict:
71
+ """Compute Stressed Historical VaR and ES over a defined stress window."""
72
+ prices = fetch_prices(ticker, start_date=stress_start, end_date=stress_end)
73
+ daily_returns = compute_returns(prices, kind="arithmetic")
74
+
75
+ result = compute_historical_var_es(daily_returns, var_confidence, es_confidence, n_days, portfolio_value)
76
+
77
+ logger.info(
78
+ f"Stressed VaR: 1d=${result['var_1d']:,.2f}, {n_days}d=${result['var_nd']:,.2f} | "
79
+ f"Stressed ES: 1d=${result['es_1d']:,.2f}, {n_days}d=${result['es_nd']:,.2f}"
80
+ )
81
+
82
+ return {
83
+ **result,
84
+ "stress_start": stress_start,
85
+ "stress_end": stress_end,
86
+ "stress_label": stress_label,
87
+ "prices": prices,
88
+ }
89
+
90
+
91
+ def historical_var_es_pipeline(
92
+ ticker: str,
93
+ var_confidence: float,
94
+ es_confidence: float,
95
+ lookback: int,
96
+ n_days: int,
97
+ portfolio_value: float,
98
+ end_date: pd.Timestamp | None = None,
99
+ stress_start: str = "2008-01-01",
100
+ stress_end: str = "2008-12-31",
101
+ stress_label: str = "Global Financial Crisis (2008)",
102
+ ):
103
+ """Execute the full Historical VaR pipeline.
104
+
105
+ Returns a dict with all computed results.
106
+ PnL and VaR values are expressed in dollars based on *portfolio_value*.
107
+ If end_date is None, defaults to current date.
108
+ """
109
+ # 1. Fetch data and compute returns
110
+ prices = fetch_prices(ticker, lookback, end_date)
111
+ daily_returns = compute_returns(prices, kind="arithmetic")
112
+
113
+ # 2. Compute normal VaR and ES
114
+ normal = compute_historical_var_es(
115
+ daily_returns, var_confidence, es_confidence, n_days, portfolio_value,
116
+ )
117
+
118
+ # 3. Compute Stressed VaR/ES
119
+ stressed = compute_stressed_historical_var_es(
120
+ ticker, var_confidence, es_confidence, n_days, portfolio_value,
121
+ stress_start, stress_end, stress_label,
122
+ )
123
+
124
+ # 4. Generate Excel report (normal + stressed sheets)
125
+ excel_path = export_historical_var_report(
126
+ prices=prices,
127
+ ticker=ticker,
128
+ n_days=n_days,
129
+ portfolio_value=portfolio_value,
130
+ var_date=end_date,
131
+ lookback=lookback,
132
+ stressed_prices=stressed["prices"],
133
+ stress_start=stressed["stress_start"],
134
+ stress_end=stressed["stress_end"],
135
+ stress_label=stressed["stress_label"],
136
+ var_confidence=var_confidence,
137
+ es_confidence=es_confidence,
138
+ )
139
+
140
+ # 5. Generate distribution plot
141
+ var_date_str = end_date.strftime("%Y-%m-%d") if end_date else ""
142
+ var_conf_pct = f"{var_confidence * 100:g}"
143
+ es_conf_pct = f"{es_confidence * 100:g}"
144
+ fig_dist = plot_distribution(
145
+ returns=daily_returns * portfolio_value,
146
+ var_cutoff=-normal["var_nd"],
147
+ var_label=f"VaR ({var_conf_pct}%, {n_days}d)",
148
+ es_cutoff=-normal["es_nd"],
149
+ es_label=f"ES ({es_conf_pct}%, {n_days}d)",
150
+ var_date=var_date_str,
151
+ method="Historical",
152
+ ticker=ticker,
153
+ )
154
+
155
+ logger.info(
156
+ f"VaR: 1d=${normal['var_1d']:,.2f}, {n_days}d=${normal['var_nd']:,.2f} | "
157
+ f"ES: 1d=${normal['es_1d']:,.2f}, {n_days}d=${normal['es_nd']:,.2f}"
158
+ )
159
+
160
+ return {
161
+ **normal,
162
+ "stressed_var_nd": stressed["var_nd"],
163
+ "stressed_es_nd": stressed["es_nd"],
164
+ "prices": prices,
165
+ "daily_returns": daily_returns,
166
+ "excel_path": excel_path,
167
+ "fig_dist": fig_dist,
168
+ }
src/logger.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ logger.py -- Centralized loguru configuration.
3
+
4
+ Import this module once (in app.py) to activate console + file sinks.
5
+ All other modules just do `from loguru import logger` directly.
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from loguru import logger
12
+
13
+ # Project root = parent of src/
14
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
15
+ LOG_DIR = PROJECT_ROOT / "log"
16
+ LOG_FILE = LOG_DIR / "var_engine.log"
17
+
18
+ # Remove default stderr handler
19
+ logger.remove()
20
+
21
+ # Colored console output (INFO+)
22
+ logger.add(
23
+ sys.stderr,
24
+ level="INFO",
25
+ colorize=True,
26
+ format="<green>{time:HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan> - <level>{message}</level>",
27
+ )
28
+
29
+ # Single rotating file (DEBUG+)
30
+ LOG_DIR.mkdir(parents=True, exist_ok=True)
31
+ logger.add(
32
+ str(LOG_FILE),
33
+ level="INFO",
34
+ rotation="10 MB",
35
+ retention="30 days",
36
+ format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
37
+ )
src/parametric.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ parametric_var.py -- Parametric (Variance-Covariance) Value at Risk calculation and analysis pipeline.
3
+ """
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from scipy import stats
8
+ from loguru import logger
9
+ from src.utils import fetch_prices, compute_returns, plot_distribution
10
+ from src.excel_export import export_parametric_var_report
11
+
12
+
13
+ def estimate_distribution(returns: pd.Series) -> tuple[float, float]:
14
+ """Estimate mean and standard deviation of daily returns.
15
+
16
+ Uses an unbiased sample standard deviation (ddof=1).
17
+ Returns (mu, sigma).
18
+ """
19
+ mu = float(returns.mean())
20
+ sigma = float(returns.std(ddof=1))
21
+ return mu, sigma
22
+
23
+
24
+ def calculate_parametric_var(returns: pd.Series, confidence: float) -> float:
25
+ """Return VaR as a positive loss value using the normal model.
26
+
27
+ VaR = -(mu - z * sigma) where z = norm.ppf(confidence).
28
+ """
29
+ mu, sigma = estimate_distribution(returns)
30
+ z = float(stats.norm.ppf(confidence))
31
+ return -(mu - z * sigma)
32
+
33
+
34
+ def calculate_parametric_es(returns: pd.Series, confidence: float) -> float:
35
+ """Return ES as a positive loss value using the normal model.
36
+
37
+ ES = -(mu - sigma * phi(z) / (1 - confidence)).
38
+ """
39
+ mu, sigma = estimate_distribution(returns)
40
+ z = float(stats.norm.ppf(confidence))
41
+ alpha = 1.0 - confidence
42
+ return -(mu - sigma * float(stats.norm.pdf(z)) / alpha)
43
+
44
+
45
+
46
+ def compute_parametric_var_es(
47
+ returns: pd.Series,
48
+ var_confidence: float,
49
+ es_confidence: float,
50
+ n_days: int,
51
+ portfolio_value: float,
52
+ ) -> dict:
53
+ """Compute VaR and ES from returns and scale to n-day horizon.
54
+
55
+ Returns a dict with 1-day and n-day dollar VaR and ES.
56
+ """
57
+ var_1d_pct = calculate_parametric_var(returns, var_confidence)
58
+ es_1d_pct = calculate_parametric_es(returns, es_confidence)
59
+ var_1d = var_1d_pct * portfolio_value
60
+ es_1d = es_1d_pct * portfolio_value
61
+
62
+ scaling_factor = np.sqrt(n_days)
63
+ var_nd = var_1d * scaling_factor
64
+ es_nd = es_1d * scaling_factor
65
+
66
+ return {
67
+ "var_1d": var_1d,
68
+ "var_nd": var_nd,
69
+ "es_1d": es_1d,
70
+ "es_nd": es_nd,
71
+ }
72
+
73
+
74
+ def compute_stressed_parametric_var_es(
75
+ ticker: str,
76
+ var_confidence: float,
77
+ es_confidence: float,
78
+ n_days: int,
79
+ portfolio_value: float,
80
+ stress_start: str,
81
+ stress_end: str,
82
+ stress_label: str,
83
+ ) -> dict:
84
+ """Compute Stressed Parametric VaR and ES over a defined stress window."""
85
+ prices = fetch_prices(ticker, start_date=stress_start, end_date=stress_end)
86
+ daily_returns = compute_returns(prices, kind="log")
87
+ result = compute_parametric_var_es(daily_returns, var_confidence, es_confidence, n_days, portfolio_value)
88
+
89
+ logger.info(
90
+ f"Stressed Parametric VaR: 1d=${result['var_1d']:,.2f}, {n_days}d=${result['var_nd']:,.2f} | "
91
+ f"Stressed ES: 1d=${result['es_1d']:,.2f}, {n_days}d=${result['es_nd']:,.2f}"
92
+ )
93
+
94
+ return {
95
+ **result,
96
+ "stress_start": stress_start,
97
+ "stress_end": stress_end,
98
+ "stress_label": stress_label,
99
+ "prices": prices,
100
+ }
101
+
102
+
103
+ def parametric_var_es_pipeline(
104
+ ticker: str,
105
+ var_confidence: float,
106
+ es_confidence: float,
107
+ lookback: int,
108
+ n_days: int,
109
+ portfolio_value: float,
110
+ end_date: pd.Timestamp | None = None,
111
+ stress_start: str = "2008-01-01",
112
+ stress_end: str = "2008-12-31",
113
+ stress_label: str = "Global Financial Crisis (2008)",
114
+ ):
115
+ """Execute the full Parametric VaR pipeline.
116
+
117
+ Returns a dict with all computed results.
118
+ VaR and ES values are expressed as positive dollar losses based on *portfolio_value*.
119
+ If end_date is None, defaults to the last business day.
120
+ """
121
+ # 1. Fetch data and compute returns
122
+ prices = fetch_prices(ticker, lookback, end_date)
123
+ daily_returns = compute_returns(prices, kind="log")
124
+ mu, sigma = estimate_distribution(daily_returns)
125
+
126
+ # 2. Compute normal VaR and ES
127
+ normal = compute_parametric_var_es(
128
+ daily_returns, var_confidence, es_confidence, n_days, portfolio_value,
129
+ )
130
+
131
+ # 3. Compute Stressed VaR/ES
132
+ stressed = compute_stressed_parametric_var_es(
133
+ ticker, var_confidence, es_confidence, n_days, portfolio_value,
134
+ stress_start, stress_end, stress_label,
135
+ )
136
+
137
+ # 4. Generate Excel report (normal + stressed sheets)
138
+ excel_path = export_parametric_var_report(
139
+ prices=prices,
140
+ ticker=ticker,
141
+ n_days=n_days,
142
+ portfolio_value=portfolio_value,
143
+ var_date=end_date,
144
+ lookback=lookback,
145
+ stressed_prices=stressed["prices"],
146
+ stress_start=stressed["stress_start"],
147
+ stress_end=stressed["stress_end"],
148
+ stress_label=stressed["stress_label"],
149
+ var_confidence=var_confidence,
150
+ es_confidence=es_confidence,
151
+ )
152
+
153
+ # 5. Generate distribution plot
154
+ var_date_str = end_date.strftime("%Y-%m-%d") if end_date else ""
155
+ var_conf_pct = f"{var_confidence * 100:g}"
156
+ es_conf_pct = f"{es_confidence * 100:g}"
157
+ fig_dist = plot_distribution(
158
+ returns=daily_returns * portfolio_value,
159
+ var_cutoff=-normal["var_nd"],
160
+ var_label=f"VaR ({var_conf_pct}%, {n_days}d)",
161
+ es_cutoff=-normal["es_nd"],
162
+ es_label=f"ES ({es_conf_pct}%, {n_days}d)",
163
+ var_date=var_date_str,
164
+ method="Parametric",
165
+ ticker=ticker,
166
+ )
167
+
168
+ logger.info(
169
+ f"Parametric VaR: 1d=${normal['var_1d']:,.2f}, {n_days}d=${normal['var_nd']:,.2f} | "
170
+ f"ES: 1d=${normal['es_1d']:,.2f}, {n_days}d=${normal['es_nd']:,.2f}"
171
+ )
172
+
173
+ return {
174
+ **normal,
175
+ "stressed_var_nd": stressed["var_nd"],
176
+ "stressed_es_nd": stressed["es_nd"],
177
+ "prices": prices,
178
+ "daily_returns": daily_returns,
179
+ "mu": mu,
180
+ "sigma": sigma,
181
+ "excel_path": excel_path,
182
+ "fig_dist": fig_dist,
183
+ }
src/utils.py ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ utils.py -- Shared utilities: data fetching, return computation, and plotting.
3
+ """
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ import plotly.graph_objects as go
8
+ import yfinance as yf
9
+ from loguru import logger
10
+
11
+
12
+ def fetch_prices(
13
+ ticker: str,
14
+ lookback: int | None = None,
15
+ var_date: pd.Timestamp | None = None,
16
+ start_date: str | None = None,
17
+ end_date: str | None = None,
18
+ ) -> pd.Series:
19
+ """Download close prices for *ticker*.
20
+
21
+ Two modes of operation:
22
+
23
+ **Lookback mode** (default): Supply *lookback* and optionally *var_date*.
24
+ Fetches the last *lookback* trading days ending before *var_date*.
25
+
26
+ **Date-range mode**: Supply *start_date* and *end_date* (YYYY-MM-DD strings).
27
+ Fetches all trading days in that window, plus one prior day so the
28
+ first daily return falls on or near *start_date*.
29
+ """
30
+ if start_date and end_date:
31
+ # Date-range mode (stress periods)
32
+ start = pd.to_datetime(start_date) - pd.Timedelta(days=10)
33
+ end = pd.to_datetime(end_date) + pd.Timedelta(days=1) # yfinance 'end' is exclusive
34
+
35
+ logger.debug(
36
+ f"Fetching {ticker}: {start.strftime('%Y-%m-%d')} to {end_date}"
37
+ )
38
+
39
+ try:
40
+ df = yf.download(
41
+ ticker,
42
+ start=start.strftime("%Y-%m-%d"),
43
+ end=end.strftime("%Y-%m-%d"),
44
+ progress=False,
45
+ interval="1d",
46
+ auto_adjust=True,
47
+ )
48
+ except Exception:
49
+ raise ValueError(
50
+ f"No data returned for ticker '{ticker}' ({start_date} to {end_date})."
51
+ )
52
+ if not isinstance(df, pd.DataFrame) or df.empty:
53
+ raise ValueError(
54
+ f"No data returned for ticker '{ticker}' ({start_date} to {end_date})."
55
+ )
56
+
57
+ prices = pd.Series(df["Close"].squeeze())
58
+ prices.name = ticker
59
+
60
+ # Trim to one trading day before start_date through end_date
61
+ start_ts = pd.to_datetime(start_date)
62
+ start_idx = prices.index.searchsorted(start_ts)
63
+ start_idx = max(0, start_idx - 1)
64
+ prices = prices.iloc[start_idx:]
65
+ prices = prices.loc[:end_date]
66
+
67
+ logger.info(
68
+ f"Fetched {len(prices)} trading days for {ticker} "
69
+ f"({prices.index[0].strftime('%Y-%m-%d')} to {prices.index[-1].strftime('%Y-%m-%d')})"
70
+ )
71
+ return prices
72
+
73
+ # Lookback mode (historical VaR)
74
+ if var_date is None:
75
+ var_date = pd.Timestamp((pd.Timestamp.today() - pd.offsets.BDay()).date())
76
+
77
+ if lookback is None:
78
+ raise ValueError("lookback is required when start_date/end_date are not provided.")
79
+ calendar_days = int(lookback * 1.6)
80
+ # yfinance 'end' is exclusive, so passing var_date fetches up to the day before
81
+ start = var_date - pd.Timedelta(days=calendar_days)
82
+ logger.debug(
83
+ f"Fetching {ticker}: {start.strftime('%Y-%m-%d')} to {var_date.strftime('%Y-%m-%d')} (lookback={lookback})"
84
+ )
85
+
86
+ try:
87
+ df = yf.download(
88
+ ticker,
89
+ start=start.strftime("%Y-%m-%d"),
90
+ end=var_date.strftime("%Y-%m-%d"),
91
+ progress=False,
92
+ interval="1d",
93
+ auto_adjust=True
94
+ )
95
+ except Exception:
96
+ raise ValueError(f"No data returned for ticker '{ticker}'.")
97
+ if not isinstance(df, pd.DataFrame) or df.empty:
98
+ raise ValueError(f"No data returned for ticker '{ticker}'.")
99
+
100
+ prices = pd.Series(df["Close"].squeeze())
101
+ prices.name = ticker
102
+ result = prices.tail(lookback)
103
+ logger.info(
104
+ f"Fetched {len(result)} trading days for {ticker} (last date: {result.index[-1].strftime('%Y-%m-%d')})"
105
+ )
106
+ return result
107
+
108
+
109
+ # ------------------------------------------------------------------
110
+ # Return computation
111
+ # ------------------------------------------------------------------
112
+
113
+
114
+ def compute_returns(prices: pd.Series, kind: str = "arithmetic") -> pd.Series:
115
+ """Compute daily returns from a price series.
116
+
117
+ Parameters
118
+ ----------
119
+ kind : "arithmetic" or "log"
120
+ arithmetic -> (P_t - P_{t-1}) / P_{t-1}
121
+ log -> log(P_t) - log(P_{t-1})
122
+ """
123
+ if kind == "log":
124
+ log_prices = pd.Series(np.log(prices))
125
+ returns = log_prices - log_prices.shift(1)
126
+ name = "Daily Log Return"
127
+ else:
128
+ returns = (prices - prices.shift(1)) / prices.shift(1)
129
+ name = "Daily Return"
130
+ returns = pd.Series(returns, name=name)
131
+ return returns.dropna()
132
+
133
+
134
+
135
+ # ------------------------------------------------------------------
136
+ # Plotting (Plotly)
137
+ # ------------------------------------------------------------------
138
+
139
+
140
+ def plot_distribution(
141
+ returns: pd.Series,
142
+ var_cutoff: float,
143
+ var_label: str = "VaR",
144
+ es_cutoff: float | None = None,
145
+ es_label: str = "ES",
146
+ var_date: str = "",
147
+ method: str = "",
148
+ ticker: str = "",
149
+ ) -> go.Figure:
150
+ """Return a histogram of the daily P&L distribution highlighting VaR and ES tail risk."""
151
+ fig = go.Figure()
152
+
153
+ # Split the distribution at the VaR cutoff (P&L below VaR are in the left tail)
154
+ normal_returns = returns[returns >= var_cutoff]
155
+ tail_returns = returns[returns < var_cutoff]
156
+
157
+ fig.add_trace(
158
+ go.Histogram(
159
+ x=normal_returns.values,
160
+ marker_color="steelblue",
161
+ opacity=0.8,
162
+ )
163
+ )
164
+ fig.add_trace(
165
+ go.Histogram(
166
+ x=tail_returns.values,
167
+ marker_color="darkorange",
168
+ opacity=0.8,
169
+ )
170
+ )
171
+
172
+ if var_cutoff is not None:
173
+ fig.add_vline(x=var_cutoff, line_width=1.5, line_dash="dot", line_color="black")
174
+ fig.add_annotation(
175
+ x=var_cutoff, xref="x",
176
+ y=0.5, yref="paper",
177
+ text=f"{var_label}<br>= ${abs(var_cutoff):,.2f}",
178
+ xanchor="left", yanchor="middle",
179
+ xshift=6,
180
+ showarrow=False,
181
+ font=dict(size=9, color="#444444"),
182
+ )
183
+
184
+ if es_cutoff is not None:
185
+ fig.add_vline(x=es_cutoff, line_width=1.5, line_dash="dash", line_color="darkred")
186
+ fig.add_annotation(
187
+ x=es_cutoff, xref="x",
188
+ y=0.5, yref="paper",
189
+ text=f"{es_label}<br>= ${abs(es_cutoff):,.2f}",
190
+ xanchor="right", yanchor="middle",
191
+ xshift=-6,
192
+ showarrow=False,
193
+ font=dict(size=9, color="darkred"),
194
+ )
195
+
196
+ title = "Daily Portfolio P&L Distribution with VaR & ES Thresholds"
197
+
198
+ fig.update_layout(
199
+ title=dict(text=title, font=dict(size=14)),
200
+ xaxis_title=dict(text="P&L ($)", font=dict(size=12)),
201
+ yaxis_title=dict(text="Frequency", font=dict(size=12)),
202
+ barmode="stack",
203
+ template="plotly_white",
204
+ yaxis=dict(showgrid=False),
205
+ margin=dict(t=80, b=40),
206
+ height=392.5,
207
+ showlegend=False,
208
+ )
209
+
210
+ if var_date:
211
+ fig.add_annotation(
212
+ text=f"VaR Date: {var_date}",
213
+ xref="paper", yref="paper",
214
+ x=1.08, y=1.22,
215
+ xanchor="right", yanchor="top",
216
+ showarrow=False,
217
+ font=dict(size=9, color="#444444"),
218
+ )
219
+
220
+ if method:
221
+ fig.add_annotation(
222
+ text=f"Method: {method}",
223
+ xref="paper", yref="paper",
224
+ x=1.08, y=1.16,
225
+ xanchor="right", yanchor="top",
226
+ showarrow=False,
227
+ font=dict(size=9, color="#444444"),
228
+ )
229
+
230
+ if ticker:
231
+ fig.add_annotation(
232
+ text=f"Ticker: {ticker}",
233
+ xref="paper", yref="paper",
234
+ x=1.08, y=1.10,
235
+ xanchor="right", yanchor="top",
236
+ showarrow=False,
237
+ font=dict(size=9, color="#444444"),
238
+ )
239
+
240
+ return fig
uv.lock ADDED
The diff for this file is too large to render. See raw diff