Skip to main content

risk-analysis

Risk measurement and stress testing — VaR/CVaR/max drawdown calculation, Monte Carlo simulation, extreme-value tail-risk analysis, and historical scenario stress testing.

الانتقال إلى التثبيت

معلومات المصدر

المستودع
HKUDS/Vibe-Trading
آخر نشاط في المصدر
٢٣ أغسطس ٢٠٢٦ في ١٤:٥٠
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٣٣٬٧٩٥
التفرعات
٥٬٥١٠

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
risk-analysis
description
Risk measurement and stress testing — VaR/CVaR/max drawdown calculation, Monte Carlo simulation, extreme-value tail-risk analysis, and historical scenario stress testing.
category
analysis
# Risk Measurement and Stress Testing ## Overview Systematic risk-measurement methodology covering VaR/CVaR calculation, Monte Carlo simulation, stress-test design, and tail-risk analysis. It provides risk evaluation for backtest results and risk-control constraints for asset allocation. The measures below are implemented once, with tests, in `src/quantlib/risk.py`. Call them; do not retype the formulas, because a hand-retyped VaR is where the sign convention silently flips. ```python from src.quantlib.risk import ( historical_var, parametric_var, historical_cvar, max_drawdown_analysis, monte_carlo_gbm, analyze_mc_results, fit_gpd_tail, ) ``` ### Sign convention **A loss is a positive number**, uniformly, across every function in the module: | Value | Reads as | |------|------| | `historical_var(...) == 0.028` | a 2.8% loss | | `historical_cvar(...) == 0.042` | a 4.2% average loss in the tail | | `max_drawdown_analysis(...)["max_drawdown"] == 0.325` | a 32.5% peak-to-trough decline | | `analyze_mc_results(...)["var"] == 0.224` | a 22.4% loss | Quantities that are *returns* rather than *losses* keep their natural sign and are named `*_return` (`mean_return`, `worst_5pct_return`, `best_5pct_return`), so a bad outcome there is negative. Report VaR to the user with the sign the user expects, but never re-derive it — flip it at the presentation layer only. `cvar >= var` holds by construction whenever both come from the same sample at the same confidence level. If you ever compute a CVaR below its VaR, the tail mask is wrong. This is *not* `cvar >= var >= 0`. The magnitudes are never clipped, so a sample whose tail contains no actual loss reports a **negative** loss — a gain. That is deliberate and informative; do not assert non-negativity on a VaR and do not clip it, or you destroy the distinction between "small loss" and "no loss at all". ## Risk Measurement Methods ### 1. VaR (Value at Risk) **Definition**: the maximum expected loss over a given horizon at a specified confidence level. #### Three Calculation Methods | Method | Formula / Steps | Advantages | Disadvantages | |------|----------|------|------| | Historical simulation | Sort historical returns and take the quantile | No distribution assumption | Depends on historical samples | | Parametric (normal) | `VaR = μ - z_α × σ` | Easy to compute | Assumes a normal distribution | | Monte Carlo | Simulate N paths and take the quantile | Flexible | Computationally intensive | #### Historical Simulation Reads the loss straight off the sorted sample, so it inherits whatever fat tails the history actually had. `horizon` scales by the square-root-of-time rule, which is only valid under i.i.d. returns. ```python historical_var(returns, confidence=0.95) # 1-day 95% VaR historical_var(returns, confidence=0.99, horizon=10) # 10-day 99% VaR ``` The quantile is a *non-interpolating lower order statistic*: element `ceil((1 - confidence) * n) - 1` of the ascending-sorted returns, negated. The result is therefore always a return that was actually observed, never a blend of two neighbours. #### Parametric (normal) ```python parametric_var(returns, confidence=0.95) ``` Fits `mu` and the sample `sigma` (ddof=1) and returns `-(mu + z*sigma)` with `z = norm.ppf(1 - confidence)`. Needs at least 2 observations. **Do not assume the parametric figure is the lower one.** The direction of the gap depends on the confidence level. A fat tail inflates the fitted `sigma`, which pushes the normal quantile *outward* at moderate confidence, where the empirical quantile is still sitting in the well-behaved body. Measured over 300 t(4) samples of 750 daily returns: | Confidence | Parametric reads **above** historical | |---|---| | 90% | 100% of samples | | 95% | 92.7% | | 97.5% | 40.3% | | 99% | 5.3% | So the familiar "parametric understates risk" result only appears at 99% and deeper. At the 95% default it is normally the *higher* of the two, and that is not a sign your code is wrong. Quote both at 99% when the point is to expose the tail. ### 2. CVaR / ES (Conditional VaR / Expected Shortfall) **Definition**: the average loss beyond the VaR threshold, more conservative than VaR. ```python historical_cvar(returns, confidence=0.95) historical_cvar(returns, confidence=0.99, horizon=10) ``` Averages the VaR order statistic together with everything worse than it (inclusive), which is the standard expected shortfall and is what makes `cvar >= var` structural rather than incidental. **VaR vs CVaR comparison**: | Metric | VaR(95%) | CVaR(95%) | Meaning | |------|----------|-----------|------| | Typical value | 2.1% | 3.4% | CVaR is usually 1.3-1.8x VaR | | Subadditivity | Not satisfied | Satisfied | CVaR can be used for portfolio risk decomposition | | Regulation | Basel II | Basel III | Regulatory trend is shifting toward CVaR | ### 3. Maximum Drawdown Analysis ```python dd = max_drawdown_analysis(equity) # equity = a strictly positive net-value Series dd["max_drawdown"] # 0.325 -> fell 32.5% below its running peak (POSITIVE) dd["peak_date"], dd["trough_date"], dd["recovery_date"] dd["recovered"] # False when the series ends still underwater ``` Full return keys: `max_drawdown`, `peak_date`, `trough_date`, `recovery_date`, `recovered`, `peak_to_trough_periods`, `trough_to_recovery_periods`, `underwater_days`, `recovery_days`. - Recovery means reaching the **peak** value again, not merely bouncing off the trough; `recovery_date` is None and `recovered` is False if it never happens. - `underwater_days` / `recovery_days` are calendar days and require a `DatetimeIndex`; on any other index they come back None and you should use the `*_periods` counts, which are always populated. - Non-positive equity raises — a drawdown *ratio* is undefined at or below zero. Rebase a signed PnL series to a positive net value first. ### 4. Monte Carlo Simulation #### Geometric Brownian Motion (GBM) ```python paths = monte_carlo_gbm( s0=100.0, mu=0.10, sigma=0.20, # mu/sigma are ANNUALISED n_steps=252, n_paths=10_000, seed=42, # keyword-only; required for a reproducible run ) paths.shape # (10000, 253) -- n_steps + 1 columns paths[:, 0] # exactly s0 on every path ``` **Always pass `seed`.** It is keyword-only so it cannot be supplied by accident, and leaving it None draws fresh OS entropy — the run is then unreproducible and the numbers in your report cannot be regenerated. Use `steps_per_year` if the step is not a 252-day trading day. Column 0 is the starting price, so `paths[:, -1] / paths[:, 0] - 1` is the total return over the whole simulation. Terminal expectation is `s0 * exp(mu * n_steps / steps_per_year)`; the *median* sits lower, at `s0 * exp((mu - 0.5*sigma**2) * T)`, and that gap is the volatility drag, not a bug. #### Simulation Result Analysis ```python summary = analyze_mc_results(paths, confidence=0.95) summary["var"], summary["cvar"] # positive loss magnitudes summary["mean_return"], summary["prob_loss"] summary["worst_5pct_return"], summary["best_5pct_return"] # signed returns ``` `var` / `cvar` are computed with exactly the same order-statistic convention as `historical_var` / `historical_cvar`, so a simulated VaR and a historical VaR are directly comparable. ## Stress-Testing Framework ### Historical Scenario Stress Tests | Scenario | Period | China A-share Drawdown | US Equity Drawdown | BTC Drawdown | 10Y Government Bonds | |------|--------|---------|---------|---------|---------| | 2008 financial crisis | 2008.01-2008.10 | -65% | -50% | N/A | yield ↓ 100bp | | 2015 China equity crash | 2015.06-2015.08 | -45% | -10% | -20% | yield ↓ 50bp | | 2018 trade war | 2018.01-2018.12 | -25% | -20% | -80% | yield ↓ 30bp | | 2020 COVID shock | 2020.01-2020.03 | -15% | -35% | -50% | yield ↓ 80bp | | 2022 hiking cycle | 2022.01-2022.10 | -20% | -25% | -65% | yield ↑ 200bp | ### Hypothetical Scenario Design ```python STRESS_SCENARIOS = { 'rate_shock_up_100bp': { 'equity': -0.10, # equities down 10% 'bond_10y': -0.08, # 10-year bonds down 8% 'bond_2y': -0.02, # short bonds down 2% 'gold': +0.05, # gold up 5% 'btc': -0.15, # BTC down 15% }, 'credit_crisis': { 'equity': -0.25, 'bond_10y': +0.05, # government bonds act as a safe haven 'credit_bond': -0.15, 'gold': +0.10, 'btc': -0.30, }, 'liquidity_dry_up': { 'equity': -0.20, 'bond_10y': -0.05, # when liquidity is poor, everything falls 'gold': -0.05, 'btc': -0.40, 'cash': 0.0, }, 'geopolitical_conflict': { 'equity': -0.15, 'bond_10y': +0.03, 'gold': +0.15, 'oil': +0.30, 'btc': -0.20, }, } ``` ### Stress-Test Implementation Steps 1. **Select a scenario**: either historical or hypothetical 2. **Apply shocks**: multiply scenario shocks by the current positions 3. **Compute portfolio loss**: `portfolio_loss = Σ(weight_i × shock_i × position_i)` 4. **Assess adequacy**: compare loss vs risk budget and whether stop-loss thresholds are triggered ## Tail-Risk Analysis (Extreme Value Theory, EVT) ### POT Method (Peaks Over Threshold) ```python fit = fit_gpd_tail(returns, threshold_pct=5.0) # keep the worst 5% fit["shape_xi"] # ξ>0 fat tail, ξ=0 exponential tail, ξ<0 bounded tail fit["shape_stderr"] # standard error of ξ -- quote ξ with it, never alone fit["scale_sigma"] # in units of loss magnitude fit["tail_type"] # "fat" | "exponential" | "bounded" fit["threshold"], fit["n_exceedances"], fit["exceedance_rate"] ``` Exceedances are non-negative by construction (`threshold - return`, kept only where the return fell below the threshold), so the GPD location is pinned at zero. Letting `loc` float instead lets the optimiser absorb tail mass into a shifted origin and biases `shape_xi`. **`tail_type` is decided against `shape_stderr`, not against exact zero.** A fitted `shape_xi` is a float and is never exactly `0.0`, so a bare `ξ > 0` test would call a genuinely exponential tail "fat" purely on the sign of estimation noise. `"fat"` therefore means `ξ > 2 × shape_stderr`, `"bounded"` means `ξ < -2 × shape_stderr`, and anything in between is `"exponential"` — indistinguishable from zero at this sample size. Measured over 200 refits, that 2σ band labels a truly exponential tail `"exponential"` 96.5% of the time while still catching `ξ = +0.40` and `ξ = -0.35` 100% of the time. A `shape_xi` of 0.03 with a `shape_stderr` of 0.02 is not evidence of a fat tail; get more exceedances before you call it one. Threshold choice is the real judgement call: too high and there is nothing left to fit (fewer than 2 exceedances raises), too low and the EVT limit theorem no longer applies, so the fitted shape stops meaning anything. Check that `shape_xi` is stable across a few nearby `threshold_pct` values before quoting it. ### Tail-Risk Metrics | Metric | Calculation | Meaning | |------|------|------| | Kurtosis | `returns.kurtosis()` | >3 indicates fat tails; China A-shares are often in the 4-8 range | | Skewness | `returns.skew()` | <0 means left-skewed (large drops are more common than large rallies) | | Tail ratio | worst 5% / best 5% | >1 means larger downside risk | | Hill estimator | Tail index | `α<2` implies extremely fat tails | ## Analysis Framework ### Input Requirements ``` Required: - Return series (daily or higher frequency) or net-value series - Portfolio weights (if it is a portfolio) Optional: - Benchmark returns (for relative risk analysis) - Risk budget / constraint settings ``` ### Analysis Steps 1. **Data preprocessing**: compute returns, check missing values, and handle outliers 2. **Descriptive statistics**: mean / volatility / skewness / kurtosis / maximum drawdown 3. **VaR/CVaR calculation**: compare three methods at both 95% and 99% confidence levels 4. **Monte Carlo simulation**: 10,000 paths, output distribution statistics and VaR 5. **Stress testing**: at least 3 historical scenarios + 2 hypothetical scenarios 6. **Tail analysis**: fit GPD and determine tail type 7. **Risk-control recommendations**: provide concrete recommendations based on the results ## Output Format Note the sign flip: the module returns losses as positive numbers, while the report below prints them the way a reader expects to see them (`max_drawdown 0.325` → `-32.5%`). Flip once, here at the presentation layer, and never inside a calculation. ```markdown ## Risk Analysis Report ### Core Risk Metrics | Metric | Value | |------|-----| | Daily volatility | 1.85% | | Annualized volatility | 29.3% | | Maximum drawdown | -32.5% (2024.09.15 → 2024.11.20) | | VaR(95%, 1D) | -2.8% | | CVaR(95%, 1D) | -4.2% | | Skewness | -0.45 | | Kurtosis | 5.2 (fat tail) | ### Stress-Test Results | Scenario | Portfolio Loss | Stop Triggered |
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub