diff --git a/scripts/evaluate_performance.py b/scripts/evaluate_performance.py index 2a94ad3..06248f2 100644 --- a/scripts/evaluate_performance.py +++ b/scripts/evaluate_performance.py @@ -1,4 +1,4 @@ -from __future__ import annotations +from __future__ import annotations import json import subprocess @@ -6,7 +6,6 @@ import time from pathlib import Path - ROOT = Path(__file__).resolve().parents[1] OUTPUTS = ROOT / "outputs" REPORTS = ROOT / "reports" diff --git a/scripts/run_ablation_study.py b/scripts/run_ablation_study.py index b543c74..0a71bca 100644 --- a/scripts/run_ablation_study.py +++ b/scripts/run_ablation_study.py @@ -1,10 +1,9 @@ -from __future__ import annotations +from __future__ import annotations from pathlib import Path import pandas as pd - ROOT = Path(__file__).resolve().parents[1] OUTPUTS = ROOT / "outputs" REPORTS = ROOT / "reports" @@ -49,11 +48,15 @@ def main() -> None: high_or_critical_findings = 0 if len(findings) and "severity" in findings.columns: - high_or_critical_findings = int(findings["severity"].isin(["HIGH", "CRITICAL"]).sum()) + high_or_critical_findings = int( + findings["severity"].isin(["HIGH", "CRITICAL"]).sum() + ) residual_mae_reduction = None if len(residual_summary): - possible_cols = [c for c in residual_summary.columns if "reduction" in c.lower()] + possible_cols = [ + c for c in residual_summary.columns if "reduction" in c.lower() + ] if possible_cols: residual_mae_reduction = float(residual_summary[possible_cols[0]].iloc[0]) @@ -77,7 +80,9 @@ def main() -> None: if "pass_rate" in operational.columns: operational_pass_rate = float(operational["pass_rate"].iloc[0]) if "recalibration_trigger_rate" in operational.columns: - recalibration_trigger_rate = float(operational["recalibration_trigger_rate"].iloc[0]) + recalibration_trigger_rate = float( + operational["recalibration_trigger_rate"].iloc[0] + ) ablation = pd.DataFrame( [ @@ -93,8 +98,13 @@ def main() -> None: { "framework_version": "+ Benchmark and numerical reliability", "key_added_layer": "Closed-form benchmarks, Greeks checks, convergence, seed stability", - "false_pass_rate": round(max(final_false_pass, baseline_false_pass * 0.40), 4), - "false_pass_reduction_vs_baseline_pct": pct_reduction(baseline_false_pass, max(final_false_pass, baseline_false_pass * 0.40)), + "false_pass_rate": round( + max(final_false_pass, baseline_false_pass * 0.40), 4 + ), + "false_pass_reduction_vs_baseline_pct": pct_reduction( + baseline_false_pass, + max(final_false_pass, baseline_false_pass * 0.40), + ), "validation_health_score": None, "high_or_critical_findings": None, "resume_message": "Improves detection of numerical instability and benchmark mismatch.", @@ -102,8 +112,13 @@ def main() -> None: { "framework_version": "+ Asymmetric validation envelope", "key_added_layer": "Risk-underestimation penalties and severity-aware validation", - "false_pass_rate": round(max(final_false_pass, baseline_false_pass * 0.15), 4), - "false_pass_reduction_vs_baseline_pct": pct_reduction(baseline_false_pass, max(final_false_pass, baseline_false_pass * 0.15)), + "false_pass_rate": round( + max(final_false_pass, baseline_false_pass * 0.15), 4 + ), + "false_pass_reduction_vs_baseline_pct": pct_reduction( + baseline_false_pass, + max(final_false_pass, baseline_false_pass * 0.15), + ), "validation_health_score": None, "high_or_critical_findings": high_or_critical_findings, "resume_message": "Prioritizes dangerous VaR/stress underestimation over symmetric error.", @@ -111,8 +126,13 @@ def main() -> None: { "framework_version": "+ Residual correction challenger", "key_added_layer": "Residual pricing/risk error correction and champion/challenger comparison", - "false_pass_rate": round(max(final_false_pass, baseline_false_pass * 0.05), 4), - "false_pass_reduction_vs_baseline_pct": pct_reduction(baseline_false_pass, max(final_false_pass, baseline_false_pass * 0.05)), + "false_pass_rate": round( + max(final_false_pass, baseline_false_pass * 0.05), 4 + ), + "false_pass_reduction_vs_baseline_pct": pct_reduction( + baseline_false_pass, + max(final_false_pass, baseline_false_pass * 0.05), + ), "validation_health_score": None, "high_or_critical_findings": high_or_critical_findings, "resume_message": "Adds challenger evidence for explainable residual-error reduction.", @@ -121,7 +141,9 @@ def main() -> None: "framework_version": "Final validation framework", "key_added_layer": "Backtesting, APL/HPL proxy, operational replay, runbook, release gate", "false_pass_rate": final_false_pass, - "false_pass_reduction_vs_baseline_pct": pct_reduction(baseline_false_pass, final_false_pass), + "false_pass_reduction_vs_baseline_pct": pct_reduction( + baseline_false_pass, final_false_pass + ), "validation_health_score": validation_health_score, "high_or_critical_findings": high_or_critical_findings, "resume_message": "End-to-end validation workflow reduces false-pass risk and supports operational monitoring.", @@ -148,17 +170,25 @@ def main() -> None: ] if residual_mae_reduction is not None: - report_lines.append(f"- Residual correction improvement metric: {residual_mae_reduction:.4f}") + report_lines.append( + f"- Residual correction improvement metric: {residual_mae_reduction:.4f}" + ) if pnl_correlation is not None: - report_lines.append(f"- P&L attribution proxy correlation: {pnl_correlation:.4f}") + report_lines.append( + f"- P&L attribution proxy correlation: {pnl_correlation:.4f}" + ) if recent_250_exceptions is not None: report_lines.append(f"- Recent-250 VaR exceptions: {recent_250_exceptions}") if traffic_light_zone is not None: report_lines.append(f"- Backtesting traffic-light zone: {traffic_light_zone}") if operational_pass_rate is not None: - report_lines.append(f"- Operational replay pass rate: {operational_pass_rate:.4f}") + report_lines.append( + f"- Operational replay pass rate: {operational_pass_rate:.4f}" + ) if recalibration_trigger_rate is not None: - report_lines.append(f"- Operational replay recalibration-trigger rate: {recalibration_trigger_rate:.4f}") + report_lines.append( + f"- Operational replay recalibration-trigger rate: {recalibration_trigger_rate:.4f}" + ) report_lines.extend( [ @@ -174,7 +204,9 @@ def main() -> None: ] ) - (REPORTS / "baseline_vs_enhanced_ablation_report.md").write_text("\n".join(report_lines), encoding="utf-8") + (REPORTS / "baseline_vs_enhanced_ablation_report.md").write_text( + "\n".join(report_lines), encoding="utf-8" + ) print("Ablation study completed.") print(ablation.to_string(index=False)) diff --git a/scripts/run_operational_replay.py b/scripts/run_operational_replay.py index 0930841..25446dc 100644 --- a/scripts/run_operational_replay.py +++ b/scripts/run_operational_replay.py @@ -1,4 +1,4 @@ -from __future__ import annotations +from __future__ import annotations import json import subprocess @@ -9,7 +9,6 @@ import numpy as np import pandas as pd - ROOT = Path(__file__).resolve().parents[1] OUTPUTS = ROOT / "outputs" REPORTS = ROOT / "reports" @@ -30,13 +29,25 @@ def run_pipeline_once(run_id: int, operating_day: str) -> dict: findings_path = OUTPUTS / "model_risk_findings.csv" backtest_path = OUTPUTS / "backtesting_summary.csv" - release = json.loads(release_path.read_text(encoding="utf-8")) if release_path.exists() else {} + release = ( + json.loads(release_path.read_text(encoding="utf-8")) + if release_path.exists() + else {} + ) health = pd.read_csv(health_path) if health_path.exists() else pd.DataFrame() findings = pd.read_csv(findings_path) if findings_path.exists() else pd.DataFrame() backtest = pd.read_csv(backtest_path) if backtest_path.exists() else pd.DataFrame() - health_score = float(health["validation_health_score"].iloc[0]) if "validation_health_score" in health.columns and len(health) else np.nan - final_false_pass = float(health["false_pass_rate"].iloc[0]) if "false_pass_rate" in health.columns and len(health) else np.nan + health_score = ( + float(health["validation_health_score"].iloc[0]) + if "validation_health_score" in health.columns and len(health) + else np.nan + ) + final_false_pass = ( + float(health["false_pass_rate"].iloc[0]) + if "false_pass_rate" in health.columns and len(health) + else np.nan + ) high_or_critical = 0 if len(findings) and "severity" in findings.columns: @@ -83,13 +94,23 @@ def build_recalibration_summary(log: pd.DataFrame) -> pd.DataFrame: { "num_operating_runs": len(log), "pass_rate": round((log["release_decision"] == "PASS").mean(), 4), - "review_or_block_rate": round(log["release_decision"].isin(["REVIEW", "BLOCK"]).mean(), 4), - "recalibration_trigger_rate": round(log["recalibration_trigger"].mean(), 4), + "review_or_block_rate": round( + log["release_decision"].isin(["REVIEW", "BLOCK"]).mean(), 4 + ), + "recalibration_trigger_rate": round( + log["recalibration_trigger"].mean(), 4 + ), "avg_runtime_seconds": round(log["runtime_seconds"].mean(), 4), "max_runtime_seconds": round(log["runtime_seconds"].max(), 4), - "avg_validation_health_score": round(log["validation_health_score"].mean(), 4), - "min_validation_health_score": round(log["validation_health_score"].min(), 4), - "max_recent_250_exceptions": round(log["recent_250_exceptions"].max(), 4), + "avg_validation_health_score": round( + log["validation_health_score"].mean(), 4 + ), + "min_validation_health_score": round( + log["validation_health_score"].min(), 4 + ), + "max_recent_250_exceptions": round( + log["recent_250_exceptions"].max(), 4 + ), } ] ) @@ -131,7 +152,9 @@ def write_reports(log: pd.DataFrame, summary: pd.DataFrame) -> None: "- This is not a regulatory capital engine and should be described as a synthetic/public-sample operational replay.", ] - (REPORTS / "operational_replay_report.md").write_text("\n".join(report), encoding="utf-8") + (REPORTS / "operational_replay_report.md").write_text( + "\n".join(report), encoding="utf-8" + ) runbook = [ "# Model Operations Runbook", @@ -161,7 +184,9 @@ def write_reports(log: pd.DataFrame, summary: pd.DataFrame) -> None: "This runbook supports a local, synthetic/public-sample, non-production validation project. It does not claim production trading, official regulatory capital, or bank-approved model use.", ] - (REPORTS / "model_operations_runbook.md").write_text("\n".join(runbook), encoding="utf-8") + (REPORTS / "model_operations_runbook.md").write_text( + "\n".join(runbook), encoding="utf-8" + ) def main() -> None: @@ -181,7 +206,11 @@ def main() -> None: summary.to_csv(OUTPUTS / "recalibration_summary.csv", index=False) write_reports(log, summary) - status = "PASS" if (log["return_code"].eq(0).all() and summary["pass_rate"].iloc[0] >= 0.8) else "REVIEW" + status = ( + "PASS" + if (log["return_code"].eq(0).all() and summary["pass_rate"].iloc[0] >= 0.8) + else "REVIEW" + ) print("Operational replay completed.") print(f"Status: {status}") diff --git a/scripts/run_pipeline.py b/scripts/run_pipeline.py index d91c016..9c633ca 100644 --- a/scripts/run_pipeline.py +++ b/scripts/run_pipeline.py @@ -1,54 +1,68 @@ import sys + sys.dont_write_bytecode = True from pathlib import Path -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'src')) + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) import numpy as np import pandas as pd -from utils import ensure_dirs, project_path, load_yaml, write_csv, write_json, write_md -from data.generate_synthetic_market import generate_synthetic_market, generate_option_portfolio, create_public_sample_files -from data.validate_data_quality import validate_time_series + +from data.generate_synthetic_market import ( + create_public_sample_files, + generate_option_portfolio, + generate_synthetic_market, +) from data.public_market_data import validate_public_market_data -from models.black_scholes import bs_call_price +from data.validate_data_quality import validate_time_series +from governance.artifact_manifest import artifact_manifest, run_manifest +from governance.baseline_final_summary import build_baseline_vs_final_summary +from governance.compensating_controls import controls_text +from governance.finding_severity import build_findings +from governance.known_limitations import limitations_text +from governance.model_card import model_card_text +from governance.model_inventory import inventory_from_config +from governance.regulatory_mapping import regulatory_mapping_table +from governance.release_gate import release_decision +from governance.structure_audit import structure_audit +from governance.validation_memo import make_report from models.binomial_tree import binomial_call_price -from models.monte_carlo_option import mc_call_price +from models.black_scholes import bs_call_price from models.heston_simulation import heston_call_price -from models.vasicek_rates import vasicek_paths, cir_paths -from validation.no_arbitrage_checks import call_no_arbitrage_checks -from validation.greeks_validation import validate_greeks -from validation.convergence_tests import binomial_convergence_case, monte_carlo_convergence_case -from validation.kupiec_test import kupiec_pof_test -from validation.christoffersen_test import christoffersen_independence_test -from validation.exception_analysis import summarize_exceptions -from validation.assumption_sensitivity import assumption_sensitivity -from validation.risk_factor_modellability import risk_factor_modellability_proxy -from validation.stress_scenario_coverage import stress_scenario_coverage -from numerical_reliability.tolerance_gates import apply_tolerance_gate -from numerical_reliability.seed_stability import seed_stability_case +from models.monte_carlo_option import mc_call_price +from models.vasicek_rates import cir_paths, vasicek_paths from numerical_reliability.cpu_gpu_consistency import cpu_gpu_consistency_stub -from risk.portfolio_var import historical_var_es, monte_carlo_var_es, rolling_backtest_var -from risk.subportfolio_backtesting import subportfolio_backtests -from risk.stressed_var import stressed_var_window -from risk.pnl_attribution import pnl_attribution_proxy +from numerical_reliability.seed_stability import seed_stability_case +from numerical_reliability.tolerance_gates import apply_tolerance_gate from risk.apl_hpl_backtesting import apl_hpl_backtesting_proxy from risk.es_tail_diagnostics import es_tail_diagnostics +from risk.full_revaluation import delta_gamma_change, full_revaluation_change from risk.liquidity_horizon_es import liquidity_horizon_es_proxy +from risk.pnl_attribution import pnl_attribution_proxy +from risk.portfolio_var import ( + historical_var_es, + monte_carlo_var_es, + rolling_backtest_var, +) from risk.stress_loss import option_portfolio_stress_loss -from risk.full_revaluation import full_revaluation_change, delta_gamma_change -from specialty.residual_error_correction import residual_error_correction +from risk.stressed_var import stressed_var_window +from risk.subportfolio_backtesting import subportfolio_backtests from specialty.asymmetric_validation_envelope import asymmetric_envelope +from specialty.residual_error_correction import residual_error_correction from specialty.validation_health_score import build_validation_health_score -from governance.model_inventory import inventory_from_config -from governance.finding_severity import build_findings -from governance.release_gate import release_decision -from governance.validation_memo import make_report -from governance.model_card import model_card_text -from governance.known_limitations import limitations_text -from governance.compensating_controls import controls_text -from governance.regulatory_mapping import regulatory_mapping_table -from governance.baseline_final_summary import build_baseline_vs_final_summary -from governance.structure_audit import structure_audit -from governance.artifact_manifest import artifact_manifest, run_manifest +from utils import ensure_dirs, load_yaml, project_path, write_csv, write_json, write_md +from validation.assumption_sensitivity import assumption_sensitivity +from validation.christoffersen_test import christoffersen_independence_test +from validation.convergence_tests import ( + binomial_convergence_case, + monte_carlo_convergence_case, +) +from validation.exception_analysis import summarize_exceptions +from validation.greeks_validation import validate_greeks +from validation.kupiec_test import kupiec_pof_test +from validation.no_arbitrage_checks import call_no_arbitrage_checks +from validation.risk_factor_modellability import risk_factor_modellability_proxy +from validation.stress_scenario_coverage import stress_scenario_coverage def main(): @@ -57,249 +71,526 @@ def main(): # Data generation and samples market = generate_synthetic_market() options = generate_option_portfolio() - create_public_sample_files(project_path('data/public')) - public_checks, public_summary, public_panel, public_corr = validate_public_market_data(project_path('data/public')) - write_csv('outputs/public_market_data_validation.csv', public_checks) - write_csv('outputs/public_risk_factor_summary.csv', public_summary) - write_csv('outputs/public_risk_factor_panel.csv', public_panel) - write_csv('outputs/public_risk_factor_correlation.csv', public_corr) + create_public_sample_files(project_path("data/public")) + public_checks, public_summary, public_panel, public_corr = ( + validate_public_market_data(project_path("data/public")) + ) + write_csv("outputs/public_market_data_validation.csv", public_checks) + write_csv("outputs/public_risk_factor_summary.csv", public_summary) + write_csv("outputs/public_risk_factor_panel.csv", public_panel) + write_csv("outputs/public_risk_factor_correlation.csv", public_corr) modellability = risk_factor_modellability_proxy(public_panel) - write_csv('outputs/risk_factor_modellability_proxy.csv', modellability) - market.to_csv(project_path('data/synthetic/correlated_asset_returns.csv'), index=False) - options.to_csv(project_path('data/synthetic/option_portfolio.csv'), index=False) + write_csv("outputs/risk_factor_modellability_proxy.csv", modellability) + market.to_csv( + project_path("data/synthetic/correlated_asset_returns.csv"), index=False + ) + options.to_csv(project_path("data/synthetic/option_portfolio.csv"), index=False) data_quality = validate_time_series(market) - write_csv('outputs/data_quality_report.csv', data_quality) + write_csv("outputs/data_quality_report.csv", data_quality) # Model inventory - inventory = inventory_from_config(load_yaml('configs/model_inventory.yaml')) - write_csv('outputs/model_inventory.csv', inventory) + inventory = inventory_from_config(load_yaml("configs/model_inventory.yaml")) + write_csv("outputs/model_inventory.csv", inventory) # Pricing and benchmark comparisons rows = [] sample = options.head(80).copy() for _, row in sample.iterrows(): - bs = float(bs_call_price(row['S'], row['K'], row['T'], row['r'], row['sigma'])) - binom = binomial_call_price(row['S'], row['K'], row['T'], row['r'], row['sigma'], steps=200) - mc, mc_se = mc_call_price(row['S'], row['K'], row['T'], row['r'], row['sigma'], paths=8000, seed=int(row.name)+11) - heston = heston_call_price(row['S'], row['K'], row['T'], row['r'], row['sigma']**2, paths=2500, steps=40, seed=int(row.name)+101) - rows.append({ - 'option_id': row['option_id'], 'S': row['S'], 'K': row['K'], 'T': row['T'], 'r': row['r'], 'sigma': row['sigma'], - 'moneyness': row['S'] / row['K'], 'bs_price': bs, 'binomial_price': binom, 'mc_price': mc, 'mc_stderr': mc_se, 'heston_price': heston, - 'binomial_error': binom - bs, 'mc_error': mc - bs, 'heston_error': heston - bs, - 'abs_error': abs(mc - bs), 'rel_abs_error': abs(mc - bs) / max(abs(bs), 1.0), 'risk_underestimation': mc < bs - }) + bs = float(bs_call_price(row["S"], row["K"], row["T"], row["r"], row["sigma"])) + binom = binomial_call_price( + row["S"], row["K"], row["T"], row["r"], row["sigma"], steps=200 + ) + mc, mc_se = mc_call_price( + row["S"], + row["K"], + row["T"], + row["r"], + row["sigma"], + paths=8000, + seed=int(row.name) + 11, + ) + heston = heston_call_price( + row["S"], + row["K"], + row["T"], + row["r"], + row["sigma"] ** 2, + paths=2500, + steps=40, + seed=int(row.name) + 101, + ) + rows.append( + { + "option_id": row["option_id"], + "S": row["S"], + "K": row["K"], + "T": row["T"], + "r": row["r"], + "sigma": row["sigma"], + "moneyness": row["S"] / row["K"], + "bs_price": bs, + "binomial_price": binom, + "mc_price": mc, + "mc_stderr": mc_se, + "heston_price": heston, + "binomial_error": binom - bs, + "mc_error": mc - bs, + "heston_error": heston - bs, + "abs_error": abs(mc - bs), + "rel_abs_error": abs(mc - bs) / max(abs(bs), 1.0), + "risk_underestimation": mc < bs, + } + ) benchmark = pd.DataFrame(rows) - benchmark_gated = apply_tolerance_gate(benchmark, error_col='rel_abs_error', warn=0.05, block=0.15) - write_csv('outputs/benchmark_comparison.csv', benchmark_gated) + benchmark_gated = apply_tolerance_gate( + benchmark, error_col="rel_abs_error", warn=0.05, block=0.15 + ) + write_csv("outputs/benchmark_comparison.csv", benchmark_gated) # Residual error correction and asymmetric validation - residual_detail, residual_summary = residual_error_correction(benchmark, 'mc_error') - write_csv('outputs/residual_error_correction_detail.csv', residual_detail) - write_csv('outputs/residual_error_correction_summary.csv', residual_summary) - asym = asymmetric_envelope(residual_detail, 'corrected_relative_error', 'risk_underestimation') - write_csv('outputs/asymmetric_validation_envelope.csv', asym) + residual_detail, residual_summary = residual_error_correction(benchmark, "mc_error") + write_csv("outputs/residual_error_correction_detail.csv", residual_detail) + write_csv("outputs/residual_error_correction_summary.csv", residual_summary) + asym = asymmetric_envelope( + residual_detail, "corrected_relative_error", "risk_underestimation" + ) + write_csv("outputs/asymmetric_validation_envelope.csv", asym) # No-arbitrage and Greeks noarb = call_no_arbitrage_checks(benchmark) - write_csv('outputs/no_arbitrage_checks.csv', noarb) + write_csv("outputs/no_arbitrage_checks.csv", noarb) greeks = validate_greeks(sample, sample_size=60) - write_csv('outputs/greeks_validation.csv', greeks) + write_csv("outputs/greeks_validation.csv", greeks) # Convergence and numerical reliability - conv = pd.concat([binomial_convergence_case(), monte_carlo_convergence_case()], ignore_index=True) - write_csv('outputs/convergence_summary.csv', conv) + conv = pd.concat( + [binomial_convergence_case(), monte_carlo_convergence_case()], ignore_index=True + ) + write_csv("outputs/convergence_summary.csv", conv) seed_stability = seed_stability_case() - write_csv('outputs/seed_stability.csv', seed_stability) - cpu_gpu = cpu_gpu_consistency_stub(float(benchmark['mc_price'].mean())) - write_csv('outputs/cpu_gpu_consistency.csv', cpu_gpu) + write_csv("outputs/seed_stability.csv", seed_stability) + cpu_gpu = cpu_gpu_consistency_stub(float(benchmark["mc_price"].mean())) + write_csv("outputs/cpu_gpu_consistency.csv", cpu_gpu) # Rate model sanity checks vas = vasicek_paths(paths=1200) cir = cir_paths(paths=1200) - rates_sanity = pd.DataFrame([ - {'model': 'Vasicek', 'terminal_mean': float(vas[:,-1].mean()), 'terminal_min': float(vas[:,-1].min()), 'negative_rate_share': float((vas < 0).mean())}, - {'model': 'CIR', 'terminal_mean': float(cir[:,-1].mean()), 'terminal_min': float(cir[:,-1].min()), 'negative_rate_share': float((cir < 0).mean())}, - ]) - rates_sanity['status'] = np.where((rates_sanity['model'] == 'CIR') & (rates_sanity['negative_rate_share'] > 0), 'REVIEW', 'PASS') - write_csv('outputs/rate_model_sanity.csv', rates_sanity) + rates_sanity = pd.DataFrame( + [ + { + "model": "Vasicek", + "terminal_mean": float(vas[:, -1].mean()), + "terminal_min": float(vas[:, -1].min()), + "negative_rate_share": float((vas < 0).mean()), + }, + { + "model": "CIR", + "terminal_mean": float(cir[:, -1].mean()), + "terminal_min": float(cir[:, -1].min()), + "negative_rate_share": float((cir < 0).mean()), + }, + ] + ) + rates_sanity["status"] = np.where( + (rates_sanity["model"] == "CIR") & (rates_sanity["negative_rate_share"] > 0), + "REVIEW", + "PASS", + ) + write_csv("outputs/rate_model_sanity.csv", rates_sanity) # Full revaluation vs delta-gamma dg_rows = [] for _, row in sample.head(50).iterrows(): - dS = -0.03 * row['S'] - full = full_revaluation_change(row['S'], row['K'], row['T'], row['r'], row['sigma'], dS) - dg = delta_gamma_change(row['S'], row['K'], row['T'], row['r'], row['sigma'], dS) - dg_rows.append({'option_id': row['option_id'], 'full_revaluation_change': full, 'delta_gamma_change': dg, 'approx_error': dg-full, 'abs_approx_error': abs(dg-full)}) + dS = -0.03 * row["S"] + full = full_revaluation_change( + row["S"], row["K"], row["T"], row["r"], row["sigma"], dS + ) + dg = delta_gamma_change( + row["S"], row["K"], row["T"], row["r"], row["sigma"], dS + ) + dg_rows.append( + { + "option_id": row["option_id"], + "full_revaluation_change": full, + "delta_gamma_change": dg, + "approx_error": dg - full, + "abs_approx_error": abs(dg - full), + } + ) dg_df = pd.DataFrame(dg_rows) - write_csv('outputs/full_revaluation_vs_delta_gamma.csv', dg_df) + write_csv("outputs/full_revaluation_vs_delta_gamma.csv", dg_df) # VaR / ES and stress testing - hvar, hes = historical_var_es(market['portfolio_pnl'], alpha=0.99) - nvar, nes = monte_carlo_var_es(market['portfolio_pnl'], alpha=0.99, heavy_tail=False) - tvar, tes = monte_carlo_var_es(market['portfolio_pnl'], alpha=0.99, heavy_tail=True) - var_es = pd.DataFrame([ - {'method': 'historical', 'VaR_99': hvar, 'ES_99': hes}, - {'method': 'normal_monte_carlo', 'VaR_99': nvar, 'ES_99': nes}, - {'method': 'student_t_monte_carlo', 'VaR_99': tvar, 'ES_99': tes}, - ]) - write_csv('outputs/var_es_results.csv', var_es) - stressed_var = stressed_var_window(market['portfolio_pnl'], window=250, alpha=0.99, stress_window=60) - write_csv('outputs/stressed_var_results.csv', stressed_var) - es_tail = es_tail_diagnostics(market['portfolio_pnl'], var_es, alpha=0.99) - write_csv('outputs/es_tail_diagnostics.csv', es_tail) + hvar, hes = historical_var_es(market["portfolio_pnl"], alpha=0.99) + nvar, nes = monte_carlo_var_es( + market["portfolio_pnl"], alpha=0.99, heavy_tail=False + ) + tvar, tes = monte_carlo_var_es(market["portfolio_pnl"], alpha=0.99, heavy_tail=True) + var_es = pd.DataFrame( + [ + {"method": "historical", "VaR_99": hvar, "ES_99": hes}, + {"method": "normal_monte_carlo", "VaR_99": nvar, "ES_99": nes}, + {"method": "student_t_monte_carlo", "VaR_99": tvar, "ES_99": tes}, + ] + ) + write_csv("outputs/var_es_results.csv", var_es) + stressed_var = stressed_var_window( + market["portfolio_pnl"], window=250, alpha=0.99, stress_window=60 + ) + write_csv("outputs/stressed_var_results.csv", stressed_var) + es_tail = es_tail_diagnostics(market["portfolio_pnl"], var_es, alpha=0.99) + write_csv("outputs/es_tail_diagnostics.csv", es_tail) liquidity_es = liquidity_horizon_es_proxy(market, alpha=0.975) - write_csv('outputs/liquidity_horizon_es_proxy.csv', liquidity_es) + write_csv("outputs/liquidity_horizon_es_proxy.csv", liquidity_es) - stress_cfg = load_yaml('configs/stress_config.yaml') - stress = option_portfolio_stress_loss(options, stress_cfg['stress_scenarios']) - write_csv('outputs/stress_loss_results.csv', stress) - scenario_coverage = stress_scenario_coverage(stress, stress_cfg['stress_scenarios']) - write_csv('outputs/stress_scenario_coverage.csv', scenario_coverage) + stress_cfg = load_yaml("configs/stress_config.yaml") + stress = option_portfolio_stress_loss(options, stress_cfg["stress_scenarios"]) + write_csv("outputs/stress_loss_results.csv", stress) + scenario_coverage = stress_scenario_coverage(stress, stress_cfg["stress_scenarios"]) + write_csv("outputs/stress_scenario_coverage.csv", scenario_coverage) - bt_cfg = load_yaml('configs/backtesting_config.yaml') - bt = rolling_backtest_var(market['portfolio_pnl'], window=bt_cfg['window'], alpha=bt_cfg['confidence_level']) - write_csv('outputs/backtesting_exceptions.csv', bt) + bt_cfg = load_yaml("configs/backtesting_config.yaml") + bt = rolling_backtest_var( + market["portfolio_pnl"], + window=bt_cfg["window"], + alpha=bt_cfg["confidence_level"], + ) + write_csv("outputs/backtesting_exceptions.csv", bt) bt_summary = summarize_exceptions(bt) - write_csv('outputs/backtesting_summary.csv', bt_summary) - kup = kupiec_pof_test(bt['exception'].sum(), len(bt), alpha=0.99) - chr_res = christoffersen_independence_test(bt['exception'].astype(int).to_numpy()) + write_csv("outputs/backtesting_summary.csv", bt_summary) + kup = kupiec_pof_test(bt["exception"].sum(), len(bt), alpha=0.99) + chr_res = christoffersen_independence_test(bt["exception"].astype(int).to_numpy()) kc = pd.DataFrame([kup, chr_res]) - write_csv('outputs/kupiec_christoffersen_results.csv', kc) + write_csv("outputs/kupiec_christoffersen_results.csv", kc) - sub_bt_detail, sub_bt_summary, sub_bt_tests = subportfolio_backtests(market, window=bt_cfg['window'], alpha=bt_cfg['confidence_level']) - write_csv('outputs/subportfolio_backtesting_detail.csv', sub_bt_detail) - write_csv('outputs/subportfolio_backtesting_summary.csv', sub_bt_summary) - write_csv('outputs/subportfolio_backtesting_tests.csv', sub_bt_tests) - pnl_attr_detail, pnl_attr_summary, pnl_attr_components = pnl_attribution_proxy(market) - write_csv('outputs/pnl_attribution_proxy_detail.csv', pnl_attr_detail) - write_csv('outputs/pnl_attribution_proxy_summary.csv', pnl_attr_summary) - write_csv('outputs/pnl_attribution_component_summary.csv', pnl_attr_components) - apl_hpl_detail, apl_hpl_summary, apl_hpl_tests, apl_hpl_comparison = apl_hpl_backtesting_proxy(market, window=bt_cfg['window'], alpha=bt_cfg['confidence_level']) - write_csv('outputs/apl_hpl_backtesting_detail.csv', apl_hpl_detail) - write_csv('outputs/apl_hpl_backtesting_summary.csv', apl_hpl_summary) - write_csv('outputs/apl_hpl_backtesting_tests.csv', apl_hpl_tests) - write_csv('outputs/apl_hpl_backtesting_comparison.csv', apl_hpl_comparison) + sub_bt_detail, sub_bt_summary, sub_bt_tests = subportfolio_backtests( + market, window=bt_cfg["window"], alpha=bt_cfg["confidence_level"] + ) + write_csv("outputs/subportfolio_backtesting_detail.csv", sub_bt_detail) + write_csv("outputs/subportfolio_backtesting_summary.csv", sub_bt_summary) + write_csv("outputs/subportfolio_backtesting_tests.csv", sub_bt_tests) + pnl_attr_detail, pnl_attr_summary, pnl_attr_components = pnl_attribution_proxy( + market + ) + write_csv("outputs/pnl_attribution_proxy_detail.csv", pnl_attr_detail) + write_csv("outputs/pnl_attribution_proxy_summary.csv", pnl_attr_summary) + write_csv("outputs/pnl_attribution_component_summary.csv", pnl_attr_components) + apl_hpl_detail, apl_hpl_summary, apl_hpl_tests, apl_hpl_comparison = ( + apl_hpl_backtesting_proxy( + market, window=bt_cfg["window"], alpha=bt_cfg["confidence_level"] + ) + ) + write_csv("outputs/apl_hpl_backtesting_detail.csv", apl_hpl_detail) + write_csv("outputs/apl_hpl_backtesting_summary.csv", apl_hpl_summary) + write_csv("outputs/apl_hpl_backtesting_tests.csv", apl_hpl_tests) + write_csv("outputs/apl_hpl_backtesting_comparison.csv", apl_hpl_comparison) - sens = assumption_sensitivity(market['portfolio_pnl']) - write_csv('outputs/assumption_sensitivity_matrix.csv', sens) + sens = assumption_sensitivity(market["portfolio_pnl"]) + write_csv("outputs/assumption_sensitivity_matrix.csv", sens) # False-pass measurement: baseline only checks absolute mc pricing error; final includes asymmetric envelope. - baseline_pass = benchmark_gated['gate'].eq('PASS') - risky = asym['asymmetric_status'].eq('BLOCK') + baseline_pass = benchmark_gated["gate"].eq("PASS") + risky = asym["asymmetric_status"].eq("BLOCK") false_pass_rate = float((baseline_pass & risky).mean()) # Findings, health, release findings = build_findings( - benchmark_gated, greeks, bt_summary, asym, conv[conv['test']=='mc_convergence'], - pnl_attr_summary=pnl_attr_summary, modellability_df=modellability, stressed_var_df=stressed_var, - subportfolio_summary=sub_bt_summary, apl_hpl_comparison=apl_hpl_comparison, - es_tail_diagnostics_df=es_tail, stress_scenario_coverage_df=scenario_coverage + benchmark_gated, + greeks, + bt_summary, + asym, + conv[conv["test"] == "mc_convergence"], + pnl_attr_summary=pnl_attr_summary, + modellability_df=modellability, + stressed_var_df=stressed_var, + subportfolio_summary=sub_bt_summary, + apl_hpl_comparison=apl_hpl_comparison, + es_tail_diagnostics_df=es_tail, + stress_scenario_coverage_df=scenario_coverage, ) - write_csv('outputs/model_risk_findings.csv', findings) + write_csv("outputs/model_risk_findings.csv", findings) health = build_validation_health_score(findings) - write_csv('outputs/validation_health_scores.csv', health) - decision = release_decision(health['validation_health_score'].iloc[0], findings, false_pass_rate=false_pass_rate) - write_json('outputs/release_decision.json', decision) - baseline_final = build_baseline_vs_final_summary(benchmark_gated, asym, findings, decision) - write_csv('outputs/baseline_vs_final_validation_summary.csv', baseline_final) + write_csv("outputs/validation_health_scores.csv", health) + decision = release_decision( + health["validation_health_score"].iloc[0], + findings, + false_pass_rate=false_pass_rate, + ) + write_json("outputs/release_decision.json", decision) + baseline_final = build_baseline_vs_final_summary( + benchmark_gated, asym, findings, decision + ) + write_csv("outputs/baseline_vs_final_validation_summary.csv", baseline_final) regulatory_map = regulatory_mapping_table() - write_csv('outputs/regulatory_mapping_table.csv', regulatory_map) + write_csv("outputs/regulatory_mapping_table.csv", regulatory_map) structure_req, non_github = structure_audit(project_path()) - write_csv('outputs/github_runnable_structure_audit.csv', structure_req) - write_csv('outputs/non_github_artifacts.csv', non_github) + write_csv("outputs/github_runnable_structure_audit.csv", structure_req) + write_csv("outputs/non_github_artifacts.csv", non_github) artifact_idx = artifact_manifest(project_path()) - write_csv('outputs/artifact_manifest.csv', artifact_idx) - write_json('outputs/run_manifest.json', run_manifest(project_path(), decision)) + write_csv("outputs/artifact_manifest.csv", artifact_idx) + write_json("outputs/run_manifest.json", run_manifest(project_path(), decision)) # Reports - write_md('reports/model_card.md', model_card_text()) - write_md('reports/known_limitations.md', limitations_text()) - write_md('reports/compensating_controls.md', controls_text()) - write_md('reports/public_market_data_validation_report.md', make_report('Public Market Data Validation Report', [ - ('Purpose', 'Validates public-data-shaped Federal Reserve H.15 Treasury-rate and FRED ICE BofA OAS sample files used for rates/spread sanity checks.'), - ('Series-level checks', public_checks.to_markdown(index=False)), - ('Risk-factor shock summary', public_summary.to_markdown(index=False)), - ('Correlation matrix', public_corr.to_markdown(index=False)), - ('FRTB-inspired data-sufficiency proxy', modellability.to_markdown(index=False)), - ('Claim boundary', 'Sample/fallback public-data mode only; no live proprietary market data or trading-book positions.'), - ])) - write_md('reports/regulatory_mapping_memo.md', make_report('Regulatory Mapping Memo', [ - ('Purpose', 'Maps market-risk/model-validation concepts to implemented project artifacts without claiming regulatory compliance.'), - ('Mapping table', regulatory_map.to_markdown(index=False)), - ])) - write_md('reports/resume_metrics_report.md', make_report('Resume Metrics Report', [ - ('Baseline vs final validation summary', baseline_final.to_markdown(index=False)), - ('Recommended phrasing', 'Use false-pass reduction, subportfolio backtesting, regulatory-style mapping, and public-data sample validation as evidence. Do not claim production trading-desk or regulatory-capital use.'), - ])) - write_md('reports/model_scope_and_use_case.md', make_report('Model Scope and Use Case', [ - ('Scope', 'Independent validation of pricing, rates, Monte Carlo simulation, VaR/ES, backtesting, and numerical reliability.'), - ('Data boundary', 'Synthetic portfolios and public-data-shaped sample files only. No proprietary bank data.'), - ('Public-data sample mode', 'Federal Reserve H.15-style Treasury rates and FRED ICE BofA OAS-style spread samples are validated for missing/stale/outlier observations and risk-factor shocks.'), - ('Model inventory', inventory.to_markdown(index=False)), - ])) - write_md('reports/pricing_validation_report.md', make_report('Pricing Validation Report', [ - ('Benchmark summary', benchmark_gated[['abs_error','rel_abs_error','gate']].describe(include='all').to_markdown()), - ('No-arbitrage checks', noarb.to_markdown(index=False)), - ('Residual error correction', residual_summary.to_markdown(index=False)), - ])) - write_md('reports/numerical_reliability_report.md', make_report('Numerical Reliability Report', [ - ('Convergence summary', conv.to_markdown(index=False)), - ('Seed stability', seed_stability.head().to_markdown(index=False)), - ('CPU/GPU consistency boundary', cpu_gpu.to_markdown(index=False)), - ])) - write_md('reports/var_es_backtesting_report.md', make_report('VaR and ES Backtesting Report', [ - ('VaR/ES results', var_es.to_markdown(index=False)), - ('Stressed-window VaR proxy', stressed_var.to_markdown(index=False)), - ('ES tail diagnostics', es_tail.to_markdown(index=False)), - ('Liquidity-horizon ES proxy', liquidity_es.to_markdown(index=False)), - ('Backtesting summary', bt_summary.to_markdown(index=False)), - ('Kupiec/Christoffersen', kc.to_markdown(index=False)), - ('Subportfolio backtesting summary', sub_bt_summary.to_markdown(index=False)), - ('Subportfolio statistical tests', sub_bt_tests.to_markdown(index=False)), - ('P&L attribution proxy summary', pnl_attr_summary.to_markdown(index=False)), - ('APL/HPL backtesting proxy comparison', apl_hpl_comparison.to_markdown(index=False)), - ('APL/HPL backtesting summary', apl_hpl_summary.to_markdown(index=False)), - ('P&L attribution component summary', pnl_attr_components.to_markdown(index=False)), - ])) - write_md('reports/stress_testing_report.md', make_report('Stress Testing Report', [ - ('Stress loss by scenario', stress.to_markdown(index=False)), - ('Stress scenario coverage and monotonicity checks', scenario_coverage.to_markdown(index=False)), - ])) - write_md('reports/assumption_impact_report.md', make_report('Assumption Impact Report', [('Assumption sensitivity', sens.to_markdown(index=False))])) - write_md('reports/validation_health_report.md', make_report('Validation Health Report', [ - ('Health score', health.to_markdown(index=False)), - ('False-pass rate', false_pass_rate), - ('Baseline vs final validation summary', baseline_final.to_markdown(index=False)), - ('ES tail diagnostics', es_tail.to_markdown(index=False)), - ('APL/HPL proxy comparison', apl_hpl_comparison.to_markdown(index=False)), - ('Asymmetric envelope status counts', asym['asymmetric_status'].value_counts().to_markdown()), - ])) - write_md('reports/model_risk_validation_memo.md', make_report('Model Risk Validation Memo', [ - ('Executive finding', f"Release decision: {decision['release_decision']} with validation health score {decision['validation_health_score']:.1f}."), - ('Model-risk findings', findings.to_markdown(index=False)), - ('Claim boundary', decision['claim_boundary']), - ('Compensating controls', 'Benchmark comparison, numerical tolerance gates, asymmetric validation envelope, stressed-window VaR proxy, P&L attribution proxy, and release gate are required before use.'), - ])) - write_md('reports/github_runnable_audit.md', make_report('Local and GitHub Runnable Audit', [ - ('Required structure', structure_req.to_markdown(index=False)), - ('Non-GitHub artifacts detected', non_github.to_markdown(index=False) if len(non_github) else 'None detected.'), - ('Artifact manifest sample', artifact_idx.head(20).to_markdown(index=False)), - ('Local run commands', '`python scripts/run_pipeline.py`, `pytest -q`, `make run`, `make test`.'), - ('CI recommendation', 'GitHub Actions workflow included under `.github/workflows/ci.yml`; outputs/reports can be regenerated locally.'), - ])) - write_md('reports/executive_summary.md', make_report('Executive Summary', [ - ('Purpose', 'This offline framework validates pricing/rates/risk models using benchmark comparison, backtesting, numerical reliability checks, and model-risk governance artifacts.'), - ('Final decision', decision), - ('Key metrics', f"Historical VaR99={hvar:,.2f}; Historical ES99={hes:,.2f}; stressed VaR multiplier={float(stressed_var['stressed_var_multiplier'].iloc[0]):.2f}; PLA proxy correlation={float(pnl_attr_summary['pla_proxy_correlation'].iloc[0]):.3f}; APL/HPL corr={float(apl_hpl_comparison['apl_hpl_correlation'].iloc[0]):.3f}; LH-ES component sum={float(liquidity_es['component_scaled_es_sum_proxy'].iloc[0]):,.2f}; baseline false-pass rate={false_pass_rate:.3f}; relative false-pass reduction={float(baseline_final.loc[baseline_final['metric']=='relative_false_pass_reduction','value'].iloc[0]):.1%}; release={decision['release_decision']}."), - ('Safe resume wording', 'Independent Python quant model validation framework with derivatives, rates, Monte Carlo, VaR/ES, backtesting, numerical reliability, validation memos, and synthetic/public-data claim boundaries.'), - ])) + write_md("reports/model_card.md", model_card_text()) + write_md("reports/known_limitations.md", limitations_text()) + write_md("reports/compensating_controls.md", controls_text()) + write_md( + "reports/public_market_data_validation_report.md", + make_report( + "Public Market Data Validation Report", + [ + ( + "Purpose", + "Validates public-data-shaped Federal Reserve H.15 Treasury-rate and FRED ICE BofA OAS sample files used for rates/spread sanity checks.", + ), + ("Series-level checks", public_checks.to_markdown(index=False)), + ("Risk-factor shock summary", public_summary.to_markdown(index=False)), + ("Correlation matrix", public_corr.to_markdown(index=False)), + ( + "FRTB-inspired data-sufficiency proxy", + modellability.to_markdown(index=False), + ), + ( + "Claim boundary", + "Sample/fallback public-data mode only; no live proprietary market data or trading-book positions.", + ), + ], + ), + ) + write_md( + "reports/regulatory_mapping_memo.md", + make_report( + "Regulatory Mapping Memo", + [ + ( + "Purpose", + "Maps market-risk/model-validation concepts to implemented project artifacts without claiming regulatory compliance.", + ), + ("Mapping table", regulatory_map.to_markdown(index=False)), + ], + ), + ) + write_md( + "reports/resume_metrics_report.md", + make_report( + "Resume Metrics Report", + [ + ( + "Baseline vs final validation summary", + baseline_final.to_markdown(index=False), + ), + ( + "Recommended phrasing", + "Use false-pass reduction, subportfolio backtesting, regulatory-style mapping, and public-data sample validation as evidence. Do not claim production trading-desk or regulatory-capital use.", + ), + ], + ), + ) + write_md( + "reports/model_scope_and_use_case.md", + make_report( + "Model Scope and Use Case", + [ + ( + "Scope", + "Independent validation of pricing, rates, Monte Carlo simulation, VaR/ES, backtesting, and numerical reliability.", + ), + ( + "Data boundary", + "Synthetic portfolios and public-data-shaped sample files only. No proprietary bank data.", + ), + ( + "Public-data sample mode", + "Federal Reserve H.15-style Treasury rates and FRED ICE BofA OAS-style spread samples are validated for missing/stale/outlier observations and risk-factor shocks.", + ), + ("Model inventory", inventory.to_markdown(index=False)), + ], + ), + ) + write_md( + "reports/pricing_validation_report.md", + make_report( + "Pricing Validation Report", + [ + ( + "Benchmark summary", + benchmark_gated[["abs_error", "rel_abs_error", "gate"]] + .describe(include="all") + .to_markdown(), + ), + ("No-arbitrage checks", noarb.to_markdown(index=False)), + ( + "Residual error correction", + residual_summary.to_markdown(index=False), + ), + ], + ), + ) + write_md( + "reports/numerical_reliability_report.md", + make_report( + "Numerical Reliability Report", + [ + ("Convergence summary", conv.to_markdown(index=False)), + ("Seed stability", seed_stability.head().to_markdown(index=False)), + ("CPU/GPU consistency boundary", cpu_gpu.to_markdown(index=False)), + ], + ), + ) + write_md( + "reports/var_es_backtesting_report.md", + make_report( + "VaR and ES Backtesting Report", + [ + ("VaR/ES results", var_es.to_markdown(index=False)), + ("Stressed-window VaR proxy", stressed_var.to_markdown(index=False)), + ("ES tail diagnostics", es_tail.to_markdown(index=False)), + ("Liquidity-horizon ES proxy", liquidity_es.to_markdown(index=False)), + ("Backtesting summary", bt_summary.to_markdown(index=False)), + ("Kupiec/Christoffersen", kc.to_markdown(index=False)), + ( + "Subportfolio backtesting summary", + sub_bt_summary.to_markdown(index=False), + ), + ( + "Subportfolio statistical tests", + sub_bt_tests.to_markdown(index=False), + ), + ( + "P&L attribution proxy summary", + pnl_attr_summary.to_markdown(index=False), + ), + ( + "APL/HPL backtesting proxy comparison", + apl_hpl_comparison.to_markdown(index=False), + ), + ( + "APL/HPL backtesting summary", + apl_hpl_summary.to_markdown(index=False), + ), + ( + "P&L attribution component summary", + pnl_attr_components.to_markdown(index=False), + ), + ], + ), + ) + write_md( + "reports/stress_testing_report.md", + make_report( + "Stress Testing Report", + [ + ("Stress loss by scenario", stress.to_markdown(index=False)), + ( + "Stress scenario coverage and monotonicity checks", + scenario_coverage.to_markdown(index=False), + ), + ], + ), + ) + write_md( + "reports/assumption_impact_report.md", + make_report( + "Assumption Impact Report", + [("Assumption sensitivity", sens.to_markdown(index=False))], + ), + ) + write_md( + "reports/validation_health_report.md", + make_report( + "Validation Health Report", + [ + ("Health score", health.to_markdown(index=False)), + ("False-pass rate", false_pass_rate), + ( + "Baseline vs final validation summary", + baseline_final.to_markdown(index=False), + ), + ("ES tail diagnostics", es_tail.to_markdown(index=False)), + ( + "APL/HPL proxy comparison", + apl_hpl_comparison.to_markdown(index=False), + ), + ( + "Asymmetric envelope status counts", + asym["asymmetric_status"].value_counts().to_markdown(), + ), + ], + ), + ) + write_md( + "reports/model_risk_validation_memo.md", + make_report( + "Model Risk Validation Memo", + [ + ( + "Executive finding", + f"Release decision: {decision['release_decision']} with validation health score {decision['validation_health_score']:.1f}.", + ), + ("Model-risk findings", findings.to_markdown(index=False)), + ("Claim boundary", decision["claim_boundary"]), + ( + "Compensating controls", + "Benchmark comparison, numerical tolerance gates, asymmetric validation envelope, stressed-window VaR proxy, P&L attribution proxy, and release gate are required before use.", + ), + ], + ), + ) + write_md( + "reports/github_runnable_audit.md", + make_report( + "Local and GitHub Runnable Audit", + [ + ("Required structure", structure_req.to_markdown(index=False)), + ( + "Non-GitHub artifacts detected", + non_github.to_markdown(index=False) + if len(non_github) + else "None detected.", + ), + ( + "Artifact manifest sample", + artifact_idx.head(20).to_markdown(index=False), + ), + ( + "Local run commands", + "`python scripts/run_pipeline.py`, `pytest -q`, `make run`, `make test`.", + ), + ( + "CI recommendation", + "GitHub Actions workflow included under `.github/workflows/ci.yml`; outputs/reports can be regenerated locally.", + ), + ], + ), + ) + write_md( + "reports/executive_summary.md", + make_report( + "Executive Summary", + [ + ( + "Purpose", + "This offline framework validates pricing/rates/risk models using benchmark comparison, backtesting, numerical reliability checks, and model-risk governance artifacts.", + ), + ("Final decision", decision), + ( + "Key metrics", + f"Historical VaR99={hvar:,.2f}; Historical ES99={hes:,.2f}; stressed VaR multiplier={float(stressed_var['stressed_var_multiplier'].iloc[0]):.2f}; PLA proxy correlation={float(pnl_attr_summary['pla_proxy_correlation'].iloc[0]):.3f}; APL/HPL corr={float(apl_hpl_comparison['apl_hpl_correlation'].iloc[0]):.3f}; LH-ES component sum={float(liquidity_es['component_scaled_es_sum_proxy'].iloc[0]):,.2f}; baseline false-pass rate={false_pass_rate:.3f}; relative false-pass reduction={float(baseline_final.loc[baseline_final['metric'] == 'relative_false_pass_reduction', 'value'].iloc[0]):.1%}; release={decision['release_decision']}.", + ), + ( + "Safe resume wording", + "Independent Python quant model validation framework with derivatives, rates, Monte Carlo, VaR/ES, backtesting, numerical reliability, validation memos, and synthetic/public-data claim boundaries.", + ), + ], + ), + ) - print('Pipeline completed.') + print("Pipeline completed.") print(f"Release decision: {decision['release_decision']}") print(f"Validation health score: {decision['validation_health_score']:.1f}") print(f"False-pass rate: {false_pass_rate:.3f}") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/src/dashboard/streamlit_app.py b/src/dashboard/streamlit_app.py index c2e1d48..433db30 100644 --- a/src/dashboard/streamlit_app.py +++ b/src/dashboard/streamlit_app.py @@ -1,18 +1,24 @@ -from pathlib import Path import json +from pathlib import Path + import pandas as pd import streamlit as st ROOT = Path(__file__).resolve().parents[2] -st.title('Quant Model Validation Framework Dashboard') +st.title("Quant Model Validation Framework Dashboard") -for rel in ['outputs/release_decision.json', 'outputs/validation_health_scores.csv', 'outputs/model_risk_findings.csv', 'outputs/kupiec_christoffersen_results.csv']: +for rel in [ + "outputs/release_decision.json", + "outputs/validation_health_scores.csv", + "outputs/model_risk_findings.csv", + "outputs/kupiec_christoffersen_results.csv", +]: path = ROOT / rel st.subheader(rel) if path.exists(): - if path.suffix == '.json': + if path.suffix == ".json": st.json(json.loads(path.read_text())) else: st.dataframe(pd.read_csv(path)) else: - st.warning(f'{rel} not found. Run python scripts/run_pipeline.py first.') + st.warning(f"{rel} not found. Run python scripts/run_pipeline.py first.") diff --git a/src/data/generate_synthetic_market.py b/src/data/generate_synthetic_market.py index 49ae6fd..95450bb 100644 --- a/src/data/generate_synthetic_market.py +++ b/src/data/generate_synthetic_market.py @@ -1,61 +1,77 @@ +from pathlib import Path + import numpy as np import pandas as pd -from pathlib import Path def generate_synthetic_market(n_days=800, seed=42): rng = np.random.default_rng(seed) - dates = pd.bdate_range('2021-01-04', periods=n_days) + dates = pd.bdate_range("2021-01-04", periods=n_days) # Simple correlated market factors: equity, rate, spread, vol. - corr = np.array([ - [1.00, -0.25, -0.45, -0.60], - [-0.25, 1.00, 0.30, 0.20], - [-0.45, 0.30, 1.00, 0.55], - [-0.60, 0.20, 0.55, 1.00], - ]) + corr = np.array( + [ + [1.00, -0.25, -0.45, -0.60], + [-0.25, 1.00, 0.30, 0.20], + [-0.45, 0.30, 1.00, 0.55], + [-0.60, 0.20, 0.55, 1.00], + ] + ) vols = np.array([0.012, 0.0007, 0.0018, 0.006]) cov = corr * np.outer(vols, vols) shocks = rng.multivariate_normal(np.zeros(4), cov, size=n_days) - df = pd.DataFrame(shocks, columns=['equity_return', 'rate_change', 'spread_change', 'vol_change']) - df.insert(0, 'date', dates) - df['spot_index'] = 100 * np.exp(np.cumsum(df['equity_return'])) - df['rate_2y'] = np.clip(0.035 + np.cumsum(df['rate_change']), 0.001, 0.12) - df['oas_ig'] = np.clip(0.012 + np.cumsum(df['spread_change']), 0.002, 0.08) - df['implied_vol'] = np.clip(0.22 + np.cumsum(df['vol_change']), 0.08, 0.85) - df['portfolio_return'] = 0.65 * df['equity_return'] - 2.5 * df['rate_change'] - 1.8 * df['spread_change'] + rng.normal(0, 0.004, n_days) - df['portfolio_pnl'] = 1_000_000 * df['portfolio_return'] + df = pd.DataFrame( + shocks, columns=["equity_return", "rate_change", "spread_change", "vol_change"] + ) + df.insert(0, "date", dates) + df["spot_index"] = 100 * np.exp(np.cumsum(df["equity_return"])) + df["rate_2y"] = np.clip(0.035 + np.cumsum(df["rate_change"]), 0.001, 0.12) + df["oas_ig"] = np.clip(0.012 + np.cumsum(df["spread_change"]), 0.002, 0.08) + df["implied_vol"] = np.clip(0.22 + np.cumsum(df["vol_change"]), 0.08, 0.85) + df["portfolio_return"] = ( + 0.65 * df["equity_return"] + - 2.5 * df["rate_change"] + - 1.8 * df["spread_change"] + + rng.normal(0, 0.004, n_days) + ) + df["portfolio_pnl"] = 1_000_000 * df["portfolio_return"] return df def generate_option_portfolio(n_options=240, seed=123): rng = np.random.default_rng(seed) - df = pd.DataFrame({ - 'option_id': [f'OPT_{i:04d}' for i in range(n_options)], - 'S': rng.uniform(70, 130, n_options), - 'K': rng.uniform(75, 125, n_options), - 'T': rng.uniform(0.08, 2.0, n_options), - 'r': rng.uniform(0.01, 0.06, n_options), - 'sigma': rng.uniform(0.12, 0.55, n_options), - 'position': rng.integers(-50, 75, n_options), - }) - df['option_type'] = 'call' + df = pd.DataFrame( + { + "option_id": [f"OPT_{i:04d}" for i in range(n_options)], + "S": rng.uniform(70, 130, n_options), + "K": rng.uniform(75, 125, n_options), + "T": rng.uniform(0.08, 2.0, n_options), + "r": rng.uniform(0.01, 0.06, n_options), + "sigma": rng.uniform(0.12, 0.55, n_options), + "position": rng.integers(-50, 75, n_options), + } + ) + df["option_type"] = "call" return df def create_public_sample_files(data_dir: Path): data_dir.mkdir(parents=True, exist_ok=True) - dates = pd.bdate_range('2023-01-02', periods=260) + dates = pd.bdate_range("2023-01-02", periods=260) rng = np.random.default_rng(77) - rates = pd.DataFrame({ - 'date': dates, - 'DGS2': 0.035 + np.cumsum(rng.normal(0, 0.00035, len(dates))), - 'DGS10': 0.038 + np.cumsum(rng.normal(0, 0.00030, len(dates))), - }) - oas = pd.DataFrame({ - 'date': dates, - 'BAMLC0A0CM': 0.012 + np.cumsum(rng.normal(0, 0.00025, len(dates))), - 'BAMLH0A0HYM2': 0.036 + np.cumsum(rng.normal(0, 0.00070, len(dates))), - }) - rates.to_csv(data_dir / 'h15_rates_sample.csv', index=False) - oas.to_csv(data_dir / 'fred_oas_sample.csv', index=False) + rates = pd.DataFrame( + { + "date": dates, + "DGS2": 0.035 + np.cumsum(rng.normal(0, 0.00035, len(dates))), + "DGS10": 0.038 + np.cumsum(rng.normal(0, 0.00030, len(dates))), + } + ) + oas = pd.DataFrame( + { + "date": dates, + "BAMLC0A0CM": 0.012 + np.cumsum(rng.normal(0, 0.00025, len(dates))), + "BAMLH0A0HYM2": 0.036 + np.cumsum(rng.normal(0, 0.00070, len(dates))), + } + ) + rates.to_csv(data_dir / "h15_rates_sample.csv", index=False) + oas.to_csv(data_dir / "fred_oas_sample.csv", index=False) return rates, oas diff --git a/src/data/public_market_data.py b/src/data/public_market_data.py index d31d496..bd48eaa 100644 --- a/src/data/public_market_data.py +++ b/src/data/public_market_data.py @@ -1,4 +1,5 @@ from pathlib import Path + import numpy as np import pandas as pd @@ -14,51 +15,70 @@ def validate_public_market_data(data_dir: Path): Federal Reserve H.15 Treasury constant maturity data and FRED ICE BofA OAS series, but are sample/fallback data, not live market downloads. """ - rates_path = Path(data_dir) / 'h15_rates_sample.csv' - oas_path = Path(data_dir) / 'fred_oas_sample.csv' - rates = pd.read_csv(rates_path, parse_dates=['date']) - oas = pd.read_csv(oas_path, parse_dates=['date']) - panel = rates.merge(oas, on='date', how='outer').sort_values('date').reset_index(drop=True) + rates_path = Path(data_dir) / "h15_rates_sample.csv" + oas_path = Path(data_dir) / "fred_oas_sample.csv" + rates = pd.read_csv(rates_path, parse_dates=["date"]) + oas = pd.read_csv(oas_path, parse_dates=["date"]) + panel = ( + rates.merge(oas, on="date", how="outer") + .sort_values("date") + .reset_index(drop=True) + ) - numeric_cols = [c for c in panel.columns if c != 'date'] + numeric_cols = [c for c in panel.columns if c != "date"] checks = [] - shock_df = panel[['date']].copy() + shock_df = panel[["date"]].copy() for col in numeric_cols: s = panel[col].astype(float) shock = _safe_pct_change(s) - shock_df[f'{col}_shock'] = shock + shock_df[f"{col}_shock"] = shock stale = s.diff().fillna(0).abs().eq(0) # robust z-score for outlier shocks med = shock.median(skipna=True) mad = (shock - med).abs().median(skipna=True) denom = 1.4826 * mad if mad and mad > 0 else shock.std(ddof=1) - z = ((shock - med).abs() / denom).replace([np.inf, -np.inf], np.nan) if denom and denom > 0 else shock * 0 - checks.append({ - 'series': col, - 'observations': int(s.notna().sum()), - 'missing_count': int(s.isna().sum()), - 'missing_share': float(s.isna().mean()), - 'stale_count': int(stale.iloc[1:].sum()), - 'outlier_shock_count': int((z > 6).sum(skipna=True)), - 'min_value': float(s.min()), - 'max_value': float(s.max()), - 'mean_daily_abs_shock': float(shock.abs().mean(skipna=True)), - 'status': 'PASS' if s.isna().mean() <= 0.02 else 'REVIEW', - }) + z = ( + ((shock - med).abs() / denom).replace([np.inf, -np.inf], np.nan) + if denom and denom > 0 + else shock * 0 + ) + checks.append( + { + "series": col, + "observations": int(s.notna().sum()), + "missing_count": int(s.isna().sum()), + "missing_share": float(s.isna().mean()), + "stale_count": int(stale.iloc[1:].sum()), + "outlier_shock_count": int((z > 6).sum(skipna=True)), + "min_value": float(s.min()), + "max_value": float(s.max()), + "mean_daily_abs_shock": float(shock.abs().mean(skipna=True)), + "status": "PASS" if s.isna().mean() <= 0.02 else "REVIEW", + } + ) - shock_cols = [c for c in shock_df.columns if c.endswith('_shock')] - factor_panel = panel.merge(shock_df[['date'] + shock_cols], on='date', how='left') + shock_cols = [c for c in shock_df.columns if c.endswith("_shock")] + factor_panel = panel.merge(shock_df[["date"] + shock_cols], on="date", how="left") summary_rows = [] for col in shock_cols: x = factor_panel[col].dropna() - summary_rows.append({ - 'risk_factor': col, - 'mean_shock': float(x.mean()) if len(x) else 0.0, - 'volatility': float(x.std(ddof=1)) if len(x) > 1 else 0.0, - 'p01_shock': float(x.quantile(0.01)) if len(x) else 0.0, - 'p99_shock': float(x.quantile(0.99)) if len(x) else 0.0, - }) + summary_rows.append( + { + "risk_factor": col, + "mean_shock": float(x.mean()) if len(x) else 0.0, + "volatility": float(x.std(ddof=1)) if len(x) > 1 else 0.0, + "p01_shock": float(x.quantile(0.01)) if len(x) else 0.0, + "p99_shock": float(x.quantile(0.99)) if len(x) else 0.0, + } + ) - corr = factor_panel[shock_cols].corr().reset_index().rename(columns={'index': 'risk_factor'}) if shock_cols else pd.DataFrame() + corr = ( + factor_panel[shock_cols] + .corr() + .reset_index() + .rename(columns={"index": "risk_factor"}) + if shock_cols + else pd.DataFrame() + ) return pd.DataFrame(checks), pd.DataFrame(summary_rows), factor_panel, corr diff --git a/src/governance/structure_audit.py b/src/governance/structure_audit.py index 143c6e0..941f9db 100644 --- a/src/governance/structure_audit.py +++ b/src/governance/structure_audit.py @@ -1,19 +1,45 @@ from pathlib import Path + import pandas as pd def structure_audit(root: Path): required = [ - 'README.md', 'PROJECT_CARD.md', 'Makefile', 'pyproject.toml', 'requirements.txt', '.gitignore', - 'Dockerfile', '.dockerignore', '.github/workflows/ci.yml', 'scripts/run_pipeline.py', 'tests', 'src', 'configs', 'outputs', 'reports' + "README.md", + "PROJECT_CARD.md", + "Makefile", + "pyproject.toml", + "requirements.txt", + ".gitignore", + "Dockerfile", + ".dockerignore", + ".github/workflows/ci.yml", + "scripts/run_pipeline.py", + "tests", + "src", + "configs", + "outputs", + "reports", ] rows = [] for rel in required: p = root / rel - rows.append({'item': rel, 'exists': p.exists(), 'type': 'dir' if p.is_dir() else 'file'}) + rows.append( + {"item": rel, "exists": p.exists(), "type": "dir" if p.is_dir() else "file"} + ) # Identify heavy/cache artifacts that should not be committed. - risky_patterns = ['.pytest_cache', '__pycache__', '.venv', 'venv', '.ipynb_checkpoints'] + risky_patterns = [ + ".pytest_cache", + "__pycache__", + ".venv", + "venv", + ".ipynb_checkpoints", + ] risky = [] for pattern in risky_patterns: - risky.extend([str(p.relative_to(root)) for p in root.rglob(pattern) if p.exists()]) - return pd.DataFrame(rows), pd.DataFrame({'non_github_artifact': risky}) if risky else pd.DataFrame(columns=['non_github_artifact']) + risky.extend( + [str(p.relative_to(root)) for p in root.rglob(pattern) if p.exists()] + ) + return pd.DataFrame(rows), pd.DataFrame( + {"non_github_artifact": risky} + ) if risky else pd.DataFrame(columns=["non_github_artifact"]) diff --git a/src/numerical_reliability/seed_stability.py b/src/numerical_reliability/seed_stability.py index 020eba2..e5172ba 100644 --- a/src/numerical_reliability/seed_stability.py +++ b/src/numerical_reliability/seed_stability.py @@ -1,5 +1,6 @@ import numpy as np import pandas as pd + from models.monte_carlo_option import mc_call_price @@ -7,8 +8,8 @@ def seed_stability_case(S=100, K=100, T=1.0, r=0.03, sigma=0.25, paths=10000): rows = [] for seed in [7, 11, 19, 23, 31]: price, stderr = mc_call_price(S, K, T, r, sigma, paths=paths, seed=seed) - rows.append({'seed': seed, 'price': price, 'stderr': stderr}) + rows.append({"seed": seed, "price": price, "stderr": stderr}) df = pd.DataFrame(rows) - df['price_std_across_seeds'] = df['price'].std(ddof=1) - df['relative_std'] = df['price'].std(ddof=1) / max(abs(df['price'].mean()), 1e-8) + df["price_std_across_seeds"] = df["price"].std(ddof=1) + df["relative_std"] = df["price"].std(ddof=1) / max(abs(df["price"].mean()), 1e-8) return df diff --git a/src/risk/apl_hpl_backtesting.py b/src/risk/apl_hpl_backtesting.py index 3b19d23..2122688 100644 --- a/src/risk/apl_hpl_backtesting.py +++ b/src/risk/apl_hpl_backtesting.py @@ -2,9 +2,9 @@ import pandas as pd from risk.portfolio_var import rolling_backtest_var +from validation.christoffersen_test import christoffersen_independence_test from validation.exception_analysis import summarize_exceptions from validation.kupiec_test import kupiec_pof_test -from validation.christoffersen_test import christoffersen_independence_test def build_apl_hpl_proxy(market_df: pd.DataFrame) -> pd.DataFrame: @@ -17,47 +17,74 @@ def build_apl_hpl_proxy(market_df: pd.DataFrame) -> pd.DataFrame: """ df = market_df.copy() hpl = 1_000_000 * ( - 0.65 * df['equity_return'].astype(float) - - 2.5 * df['rate_change'].astype(float) - - 1.8 * df['spread_change'].astype(float) + 0.65 * df["equity_return"].astype(float) + - 2.5 * df["rate_change"].astype(float) + - 1.8 * df["spread_change"].astype(float) ) - out = df[['date']].copy() - out['apl_proxy_pnl'] = df['portfolio_pnl'].astype(float) - out['hpl_proxy_pnl'] = hpl.astype(float) - out['residual_proxy_pnl'] = out['apl_proxy_pnl'] - out['hpl_proxy_pnl'] + out = df[["date"]].copy() + out["apl_proxy_pnl"] = df["portfolio_pnl"].astype(float) + out["hpl_proxy_pnl"] = hpl.astype(float) + out["residual_proxy_pnl"] = out["apl_proxy_pnl"] - out["hpl_proxy_pnl"] return out -def apl_hpl_backtesting_proxy(market_df: pd.DataFrame, window: int = 250, alpha: float = 0.99): +def apl_hpl_backtesting_proxy( + market_df: pd.DataFrame, window: int = 250, alpha: float = 0.99 +): pnl = build_apl_hpl_proxy(market_df) detail_frames = [] summary_rows = [] test_rows = [] - for col, label in [('apl_proxy_pnl', 'APL_proxy'), ('hpl_proxy_pnl', 'HPL_proxy')]: + for col, label in [("apl_proxy_pnl", "APL_proxy"), ("hpl_proxy_pnl", "HPL_proxy")]: bt = rolling_backtest_var(pnl[col], window=window, alpha=alpha) - bt.insert(0, 'pnl_type', label) + bt.insert(0, "pnl_type", label) detail_frames.append(bt) summary = summarize_exceptions(bt) - summary.insert(0, 'pnl_type', label) + summary.insert(0, "pnl_type", label) summary_rows.append(summary) - kup = kupiec_pof_test(int(bt['exception'].sum()), len(bt), alpha=alpha) - chr_res = christoffersen_independence_test(bt['exception'].astype(int).to_numpy()) - kup['pnl_type'] = label - chr_res['pnl_type'] = label + kup = kupiec_pof_test(int(bt["exception"].sum()), len(bt), alpha=alpha) + chr_res = christoffersen_independence_test( + bt["exception"].astype(int).to_numpy() + ) + kup["pnl_type"] = label + chr_res["pnl_type"] = label test_rows.extend([kup, chr_res]) summary_df = pd.concat(summary_rows, ignore_index=True) - apl_recent = int(summary_df.loc[summary_df['pnl_type'].eq('APL_proxy'), 'recent_250_exceptions'].iloc[0]) - hpl_recent = int(summary_df.loc[summary_df['pnl_type'].eq('HPL_proxy'), 'recent_250_exceptions'].iloc[0]) - corr = float(np.corrcoef(pnl['apl_proxy_pnl'], pnl['hpl_proxy_pnl'])[0, 1]) - residual_std_share = float(pnl['residual_proxy_pnl'].std(ddof=1) / pnl['apl_proxy_pnl'].std(ddof=1)) - comparison = pd.DataFrame([{ - 'apl_recent_250_exceptions': apl_recent, - 'hpl_recent_250_exceptions': hpl_recent, - 'apl_hpl_exception_gap': abs(apl_recent - hpl_recent), - 'apl_hpl_correlation': corr, - 'residual_std_share': residual_std_share, - 'status': 'PASS' if abs(apl_recent - hpl_recent) <= 2 and corr >= 0.90 and residual_std_share <= 0.40 else 'REVIEW', - 'claim_boundary': 'APL/HPL backtesting proxy only; not an official FRTB P&L attribution test' - }]) - return pd.concat(detail_frames, ignore_index=True), summary_df, pd.DataFrame(test_rows), comparison + apl_recent = int( + summary_df.loc[ + summary_df["pnl_type"].eq("APL_proxy"), "recent_250_exceptions" + ].iloc[0] + ) + hpl_recent = int( + summary_df.loc[ + summary_df["pnl_type"].eq("HPL_proxy"), "recent_250_exceptions" + ].iloc[0] + ) + corr = float(np.corrcoef(pnl["apl_proxy_pnl"], pnl["hpl_proxy_pnl"])[0, 1]) + residual_std_share = float( + pnl["residual_proxy_pnl"].std(ddof=1) / pnl["apl_proxy_pnl"].std(ddof=1) + ) + comparison = pd.DataFrame( + [ + { + "apl_recent_250_exceptions": apl_recent, + "hpl_recent_250_exceptions": hpl_recent, + "apl_hpl_exception_gap": abs(apl_recent - hpl_recent), + "apl_hpl_correlation": corr, + "residual_std_share": residual_std_share, + "status": "PASS" + if abs(apl_recent - hpl_recent) <= 2 + and corr >= 0.90 + and residual_std_share <= 0.40 + else "REVIEW", + "claim_boundary": "APL/HPL backtesting proxy only; not an official FRTB P&L attribution test", + } + ] + ) + return ( + pd.concat(detail_frames, ignore_index=True), + summary_df, + pd.DataFrame(test_rows), + comparison, + ) diff --git a/src/risk/full_revaluation.py b/src/risk/full_revaluation.py index 33ca5ef..8675348 100644 --- a/src/risk/full_revaluation.py +++ b/src/risk/full_revaluation.py @@ -1,10 +1,10 @@ -from models.black_scholes import bs_call_price, bs_call_greeks +from models.black_scholes import bs_call_greeks, bs_call_price def full_revaluation_change(S, K, T, r, sigma, dS): - return bs_call_price(S+dS, K, T, r, sigma) - bs_call_price(S, K, T, r, sigma) + return bs_call_price(S + dS, K, T, r, sigma) - bs_call_price(S, K, T, r, sigma) def delta_gamma_change(S, K, T, r, sigma, dS): g = bs_call_greeks(S, K, T, r, sigma) - return g['delta'] * dS + 0.5 * g['gamma'] * dS * dS + return g["delta"] * dS + 0.5 * g["gamma"] * dS * dS diff --git a/src/risk/liquidity_horizon_es.py b/src/risk/liquidity_horizon_es.py index 42390ab..653c049 100644 --- a/src/risk/liquidity_horizon_es.py +++ b/src/risk/liquidity_horizon_es.py @@ -1,11 +1,14 @@ import math + import pandas as pd from risk.portfolio_var import historical_var_es from risk.subportfolio_backtesting import build_subportfolio_pnl -def liquidity_horizon_es_proxy(market_df: pd.DataFrame, alpha: float = 0.975) -> pd.DataFrame: +def liquidity_horizon_es_proxy( + market_df: pd.DataFrame, alpha: float = 0.975 +) -> pd.DataFrame: """FRTB-inspired liquidity-horizon ES scaling proxy. Uses synthetic subportfolio P&L proxies and square-root-of-time scaling to show how @@ -14,29 +17,35 @@ def liquidity_horizon_es_proxy(market_df: pd.DataFrame, alpha: float = 0.975) -> """ pnl = build_subportfolio_pnl(market_df) horizons = { - 'derivatives': 10, - 'rates': 20, - 'spread_proxy': 60, - 'aggregate': 10, + "derivatives": 10, + "rates": 20, + "spread_proxy": 60, + "aggregate": 10, } rows = [] for subportfolio, horizon in horizons.items(): - col = f'{subportfolio}_pnl' + col = f"{subportfolio}_pnl" if col not in pnl.columns: continue var, es = historical_var_es(pnl[col], alpha=alpha) scaled_es = float(es * math.sqrt(horizon / 10.0)) - rows.append({ - 'subportfolio': subportfolio, - 'alpha': alpha, - 'liquidity_horizon_days': horizon, - 'base_10day_es_proxy': float(es), - 'scaled_liquidity_horizon_es_proxy': scaled_es, - 'scaling_multiplier': float(math.sqrt(horizon / 10.0)), - 'claim_boundary': 'FRTB-inspired liquidity-horizon ES proxy; not a regulatory capital calculation' - }) + rows.append( + { + "subportfolio": subportfolio, + "alpha": alpha, + "liquidity_horizon_days": horizon, + "base_10day_es_proxy": float(es), + "scaled_liquidity_horizon_es_proxy": scaled_es, + "scaling_multiplier": float(math.sqrt(horizon / 10.0)), + "claim_boundary": "FRTB-inspired liquidity-horizon ES proxy; not a regulatory capital calculation", + } + ) df = pd.DataFrame(rows) - component_sum = float(df.loc[~df['subportfolio'].eq('aggregate'), 'scaled_liquidity_horizon_es_proxy'].sum()) - df['component_scaled_es_sum_proxy'] = component_sum - df['status'] = 'PASS' + component_sum = float( + df.loc[ + ~df["subportfolio"].eq("aggregate"), "scaled_liquidity_horizon_es_proxy" + ].sum() + ) + df["component_scaled_es_sum_proxy"] = component_sum + df["status"] = "PASS" return df diff --git a/src/risk/stress_loss.py b/src/risk/stress_loss.py index 10614cd..27a8024 100644 --- a/src/risk/stress_loss.py +++ b/src/risk/stress_loss.py @@ -1,16 +1,38 @@ import pandas as pd + from models.black_scholes import bs_call_price def option_portfolio_stress_loss(portfolio, scenarios): rows = [] - base_values = bs_call_price(portfolio['S'], portfolio['K'], portfolio['T'], portfolio['r'], portfolio['sigma']) * portfolio['position'] + base_values = ( + bs_call_price( + portfolio["S"], + portfolio["K"], + portfolio["T"], + portfolio["r"], + portfolio["sigma"], + ) + * portfolio["position"] + ) base_total = float(base_values.sum()) for sc in scenarios: - stressed_s = portfolio['S'] * (1 + sc['equity_shock']) - stressed_sigma = (portfolio['sigma'] + sc['volatility_shock']).clip(lower=0.01) - stressed_r = portfolio['r'] + sc['rate_shock'] - stressed_values = bs_call_price(stressed_s, portfolio['K'], portfolio['T'], stressed_r, stressed_sigma) * portfolio['position'] + stressed_s = portfolio["S"] * (1 + sc["equity_shock"]) + stressed_sigma = (portfolio["sigma"] + sc["volatility_shock"]).clip(lower=0.01) + stressed_r = portfolio["r"] + sc["rate_shock"] + stressed_values = ( + bs_call_price( + stressed_s, portfolio["K"], portfolio["T"], stressed_r, stressed_sigma + ) + * portfolio["position"] + ) stressed_total = float(stressed_values.sum()) - rows.append({'scenario': sc['name'], 'base_value': base_total, 'stressed_value': stressed_total, 'stress_loss': base_total - stressed_total}) + rows.append( + { + "scenario": sc["name"], + "base_value": base_total, + "stressed_value": stressed_total, + "stress_loss": base_total - stressed_total, + } + ) return pd.DataFrame(rows) diff --git a/src/risk/stressed_var.py b/src/risk/stressed_var.py index 5f1706e..d193d73 100644 --- a/src/risk/stressed_var.py +++ b/src/risk/stressed_var.py @@ -1,5 +1,6 @@ import numpy as np import pandas as pd + from risk.portfolio_var import historical_var_es @@ -20,37 +21,45 @@ def stressed_var_window(pnl_series, window=250, alpha=0.99, stress_window=60): start_idx = end_idx - stress_window sample = pnl.iloc[start_idx:end_idx] win_var, win_es = historical_var_es(sample, alpha=alpha) - if best is None or win_var > best['stressed_var']: + if best is None or win_var > best["stressed_var"]: best = { - 'selected_start_index': start_idx, - 'selected_end_index': end_idx - 1, - 'selected_window_observations': int(len(sample)), - 'selected_window_pnl_volatility': float(sample.std(ddof=1)), - 'stressed_var': float(win_var), - 'stressed_es': float(win_es), + "selected_start_index": start_idx, + "selected_end_index": end_idx - 1, + "selected_window_observations": int(len(sample)), + "selected_window_pnl_volatility": float(sample.std(ddof=1)), + "stressed_var": float(win_var), + "stressed_es": float(win_es), } full_vol = float(pnl.std(ddof=1)) - stressed_var = best['stressed_var'] - stressed_es = best['stressed_es'] + stressed_var = best["stressed_var"] + stressed_es = best["stressed_es"] multiplier = float(stressed_var / full_var) if full_var else np.nan - status = 'REVIEW' if multiplier > 1.25 else 'PASS' - return pd.DataFrame([{ - 'method': 'max_rolling_var_stressed_window_proxy', - 'alpha': alpha, - 'stress_window_days': stress_window, - 'selected_start_index': best['selected_start_index'], - 'selected_end_index': best['selected_end_index'], - 'selected_window_observations': best['selected_window_observations'], - 'selected_window_pnl_volatility': best['selected_window_pnl_volatility'], - 'full_sample_pnl_volatility': full_vol, - 'full_sample_var': float(full_var), - 'full_sample_es': float(full_es), - 'stressed_var': stressed_var, - 'stressed_es': stressed_es, - 'stressed_var_multiplier': multiplier, - 'stressed_es_multiplier': float(stressed_es / full_es) if full_es else np.nan, - 'selection_rule': 'maximum rolling historical VaR over candidate stress windows', - 'status': status, - 'claim_boundary': 'stressed-window proxy for validation; not a regulatory stressed-VaR model' - }]) + status = "REVIEW" if multiplier > 1.25 else "PASS" + return pd.DataFrame( + [ + { + "method": "max_rolling_var_stressed_window_proxy", + "alpha": alpha, + "stress_window_days": stress_window, + "selected_start_index": best["selected_start_index"], + "selected_end_index": best["selected_end_index"], + "selected_window_observations": best["selected_window_observations"], + "selected_window_pnl_volatility": best[ + "selected_window_pnl_volatility" + ], + "full_sample_pnl_volatility": full_vol, + "full_sample_var": float(full_var), + "full_sample_es": float(full_es), + "stressed_var": stressed_var, + "stressed_es": stressed_es, + "stressed_var_multiplier": multiplier, + "stressed_es_multiplier": float(stressed_es / full_es) + if full_es + else np.nan, + "selection_rule": "maximum rolling historical VaR over candidate stress windows", + "status": status, + "claim_boundary": "stressed-window proxy for validation; not a regulatory stressed-VaR model", + } + ] + ) diff --git a/src/risk/subportfolio_backtesting.py b/src/risk/subportfolio_backtesting.py index cc359cc..4960def 100644 --- a/src/risk/subportfolio_backtesting.py +++ b/src/risk/subportfolio_backtesting.py @@ -1,17 +1,22 @@ import pandas as pd + from risk.portfolio_var import rolling_backtest_var -from validation.kupiec_test import kupiec_pof_test from validation.christoffersen_test import christoffersen_independence_test from validation.exception_analysis import summarize_exceptions +from validation.kupiec_test import kupiec_pof_test def build_subportfolio_pnl(market: pd.DataFrame) -> pd.DataFrame: """Create transparent synthetic subportfolio P&L proxies from market factors.""" - out = market[['date']].copy() - out['derivatives_pnl'] = 1_000_000 * (0.50 * market['equity_return'] + 0.80 * market['vol_change']) - out['rates_pnl'] = 1_000_000 * (-3.20 * market['rate_change']) - out['spread_proxy_pnl'] = 1_000_000 * (-2.40 * market['spread_change'] + 0.15 * market['equity_return']) - out['aggregate_pnl'] = market['portfolio_pnl'] + out = market[["date"]].copy() + out["derivatives_pnl"] = 1_000_000 * ( + 0.50 * market["equity_return"] + 0.80 * market["vol_change"] + ) + out["rates_pnl"] = 1_000_000 * (-3.20 * market["rate_change"]) + out["spread_proxy_pnl"] = 1_000_000 * ( + -2.40 * market["spread_change"] + 0.15 * market["equity_return"] + ) + out["aggregate_pnl"] = market["portfolio_pnl"] return out @@ -20,16 +25,22 @@ def subportfolio_backtests(market: pd.DataFrame, window=250, alpha=0.99): detail_frames = [] summary_rows = [] test_rows = [] - for col in [c for c in pnl.columns if c.endswith('_pnl')]: + for col in [c for c in pnl.columns if c.endswith("_pnl")]: bt = rolling_backtest_var(pnl[col], window=window, alpha=alpha) - bt.insert(0, 'subportfolio', col.replace('_pnl', '')) + bt.insert(0, "subportfolio", col.replace("_pnl", "")) detail_frames.append(bt) summary = summarize_exceptions(bt) - summary.insert(0, 'subportfolio', col.replace('_pnl', '')) + summary.insert(0, "subportfolio", col.replace("_pnl", "")) summary_rows.append(summary) - kup = kupiec_pof_test(int(bt['exception'].sum()), len(bt), alpha=alpha) - chr_res = christoffersen_independence_test(bt['exception'].astype(int).to_numpy()) - kup['subportfolio'] = col.replace('_pnl', '') - chr_res['subportfolio'] = col.replace('_pnl', '') + kup = kupiec_pof_test(int(bt["exception"].sum()), len(bt), alpha=alpha) + chr_res = christoffersen_independence_test( + bt["exception"].astype(int).to_numpy() + ) + kup["subportfolio"] = col.replace("_pnl", "") + chr_res["subportfolio"] = col.replace("_pnl", "") test_rows.extend([kup, chr_res]) - return pd.concat(detail_frames, ignore_index=True), pd.concat(summary_rows, ignore_index=True), pd.DataFrame(test_rows) + return ( + pd.concat(detail_frames, ignore_index=True), + pd.concat(summary_rows, ignore_index=True), + pd.DataFrame(test_rows), + ) diff --git a/src/validation/assumption_sensitivity.py b/src/validation/assumption_sensitivity.py index ff5ec98..93d35ad 100644 --- a/src/validation/assumption_sensitivity.py +++ b/src/validation/assumption_sensitivity.py @@ -1,4 +1,5 @@ import pandas as pd + from risk.portfolio_var import historical_var_es, monte_carlo_var_es @@ -8,9 +9,11 @@ def assumption_sensitivity(pnl): hvar, hes = historical_var_es(pnl, alpha) nvar, nes = monte_carlo_var_es(pnl, alpha, heavy_tail=False) tvar, tes = monte_carlo_var_es(pnl, alpha, heavy_tail=True) - rows.extend([ - {'assumption': f'historical_alpha_{alpha}', 'VaR': hvar, 'ES': hes}, - {'assumption': f'normal_mc_alpha_{alpha}', 'VaR': nvar, 'ES': nes}, - {'assumption': f'student_t_mc_alpha_{alpha}', 'VaR': tvar, 'ES': tes}, - ]) + rows.extend( + [ + {"assumption": f"historical_alpha_{alpha}", "VaR": hvar, "ES": hes}, + {"assumption": f"normal_mc_alpha_{alpha}", "VaR": nvar, "ES": nes}, + {"assumption": f"student_t_mc_alpha_{alpha}", "VaR": tvar, "ES": tes}, + ] + ) return pd.DataFrame(rows) diff --git a/src/validation/convergence_tests.py b/src/validation/convergence_tests.py index bf0d709..94589d5 100644 --- a/src/validation/convergence_tests.py +++ b/src/validation/convergence_tests.py @@ -1,6 +1,7 @@ import pandas as pd -from models.black_scholes import bs_call_price + from models.binomial_tree import binomial_call_price +from models.black_scholes import bs_call_price from models.monte_carlo_option import mc_call_price @@ -9,7 +10,15 @@ def binomial_convergence_case(S=100, K=100, T=1.0, r=0.03, sigma=0.25): rows = [] for steps in [25, 50, 100, 200, 400]: px = binomial_call_price(S, K, T, r, sigma, steps=steps) - rows.append({'test': 'binomial_convergence', 'grid': steps, 'price': px, 'benchmark': bs, 'abs_error': abs(px-bs)}) + rows.append( + { + "test": "binomial_convergence", + "grid": steps, + "price": px, + "benchmark": bs, + "abs_error": abs(px - bs), + } + ) return pd.DataFrame(rows) @@ -18,5 +27,15 @@ def monte_carlo_convergence_case(S=100, K=100, T=1.0, r=0.03, sigma=0.25, seed=7 rows = [] for paths in [1000, 2500, 5000, 10000, 20000]: px, se = mc_call_price(S, K, T, r, sigma, paths=paths, seed=seed) - rows.append({'test': 'mc_convergence', 'grid': paths, 'price': px, 'stderr': se, 'benchmark': bs, 'abs_error': abs(px-bs), 'ci_covers_benchmark': abs(px-bs) <= 1.96*se}) + rows.append( + { + "test": "mc_convergence", + "grid": paths, + "price": px, + "stderr": se, + "benchmark": bs, + "abs_error": abs(px - bs), + "ci_covers_benchmark": abs(px - bs) <= 1.96 * se, + } + ) return pd.DataFrame(rows) diff --git a/src/validation/greeks_validation.py b/src/validation/greeks_validation.py index a0509da..524830b 100644 --- a/src/validation/greeks_validation.py +++ b/src/validation/greeks_validation.py @@ -1,25 +1,34 @@ import numpy as np import pandas as pd -from models.black_scholes import bs_call_greeks + from benchmarks.finite_difference_checks import finite_diff_delta, finite_diff_gamma +from models.black_scholes import bs_call_greeks def validate_greeks(option_df, sample_size=60): rows = [] sample = option_df.head(sample_size) for _, row in sample.iterrows(): - analytic = bs_call_greeks(row['S'], row['K'], row['T'], row['r'], row['sigma']) - fd_delta = finite_diff_delta(row['S'], row['K'], row['T'], row['r'], row['sigma']) - fd_gamma = finite_diff_gamma(row['S'], row['K'], row['T'], row['r'], row['sigma']) - rows.append({ - 'option_id': row['option_id'], - 'delta_analytic': analytic['delta'], - 'delta_fd': fd_delta, - 'delta_abs_error': abs(analytic['delta'] - fd_delta), - 'gamma_analytic': analytic['gamma'], - 'gamma_fd': fd_gamma, - 'gamma_abs_error': abs(analytic['gamma'] - fd_gamma), - }) + analytic = bs_call_greeks(row["S"], row["K"], row["T"], row["r"], row["sigma"]) + fd_delta = finite_diff_delta( + row["S"], row["K"], row["T"], row["r"], row["sigma"] + ) + fd_gamma = finite_diff_gamma( + row["S"], row["K"], row["T"], row["r"], row["sigma"] + ) + rows.append( + { + "option_id": row["option_id"], + "delta_analytic": analytic["delta"], + "delta_fd": fd_delta, + "delta_abs_error": abs(analytic["delta"] - fd_delta), + "gamma_analytic": analytic["gamma"], + "gamma_fd": fd_gamma, + "gamma_abs_error": abs(analytic["gamma"] - fd_gamma), + } + ) out = pd.DataFrame(rows) - out['status'] = np.where((out.delta_abs_error < 1e-3) & (out.gamma_abs_error < 1e-3), 'PASS', 'REVIEW') + out["status"] = np.where( + (out.delta_abs_error < 1e-3) & (out.gamma_abs_error < 1e-3), "PASS", "REVIEW" + ) return out diff --git a/src/validation/kupiec_test.py b/src/validation/kupiec_test.py index a46e260..a173eec 100644 --- a/src/validation/kupiec_test.py +++ b/src/validation/kupiec_test.py @@ -1,4 +1,5 @@ import math + from scipy.stats import chi2 @@ -6,13 +7,31 @@ def kupiec_pof_test(exceptions, n_obs, alpha=0.99): x = int(exceptions) p = 1 - alpha if n_obs <= 0: - return {'test': 'kupiec_pof', 'exceptions': x, 'n_obs': n_obs, 'lr_stat': None, 'p_value': None, 'status': 'REVIEW'} + return { + "test": "kupiec_pof", + "exceptions": x, + "n_obs": n_obs, + "lr_stat": None, + "p_value": None, + "status": "REVIEW", + } phat = x / n_obs if x == 0: lr = -2 * (n_obs * math.log(1 - p) - n_obs * math.log(max(1 - phat, 1e-12))) elif x == n_obs: lr = -2 * (x * math.log(p) - x * math.log(phat)) else: - lr = -2 * ((n_obs-x)*math.log(1-p) + x*math.log(p) - ((n_obs-x)*math.log(1-phat) + x*math.log(phat))) + lr = -2 * ( + (n_obs - x) * math.log(1 - p) + + x * math.log(p) + - ((n_obs - x) * math.log(1 - phat) + x * math.log(phat)) + ) pval = 1 - chi2.cdf(lr, 1) - return {'test': 'kupiec_pof', 'exceptions': x, 'n_obs': n_obs, 'lr_stat': lr, 'p_value': pval, 'status': 'PASS' if pval >= 0.05 else 'REVIEW'} + return { + "test": "kupiec_pof", + "exceptions": x, + "n_obs": n_obs, + "lr_stat": lr, + "p_value": pval, + "status": "PASS" if pval >= 0.05 else "REVIEW", + } diff --git a/tests/test_ablation_study.py b/tests/test_ablation_study.py index 8a7b3dc..72eb343 100644 --- a/tests/test_ablation_study.py +++ b/tests/test_ablation_study.py @@ -1,6 +1,6 @@ -from pathlib import Path import subprocess import sys +from pathlib import Path ROOT = Path(__file__).resolve().parents[1] diff --git a/tests/test_artifact_manifest.py b/tests/test_artifact_manifest.py index 3c993a5..11afa7d 100644 --- a/tests/test_artifact_manifest.py +++ b/tests/test_artifact_manifest.py @@ -1,10 +1,11 @@ from pathlib import Path + from governance.artifact_manifest import artifact_manifest, run_manifest def test_artifact_manifest_and_run_manifest(): root = Path(__file__).resolve().parents[1] manifest = artifact_manifest(root) - assert 'README.md' in set(manifest['artifact']) - run = run_manifest(root, {'release_decision': 'PASS'}) - assert run['run_command'] == 'python scripts/run_pipeline.py' + assert "README.md" in set(manifest["artifact"]) + run = run_manifest(root, {"release_decision": "PASS"}) + assert run["run_command"] == "python scripts/run_pipeline.py" diff --git a/tests/test_backtesting.py b/tests/test_backtesting.py index 0a403c5..f647191 100644 --- a/tests/test_backtesting.py +++ b/tests/test_backtesting.py @@ -1,7 +1,8 @@ import numpy as np + from risk.portfolio_var import rolling_backtest_var -from validation.kupiec_test import kupiec_pof_test from validation.christoffersen_test import christoffersen_independence_test +from validation.kupiec_test import kupiec_pof_test def test_backtesting_outputs(): @@ -9,8 +10,8 @@ def test_backtesting_outputs(): pnl = rng.normal(0, 1000, 400) bt = rolling_backtest_var(pnl, window=250, alpha=0.99) assert len(bt) == 150 - assert 'exception' in bt.columns - kup = kupiec_pof_test(bt['exception'].sum(), len(bt), 0.99) - chr_res = christoffersen_independence_test(bt['exception'].astype(int).to_numpy()) - assert kup['test'] == 'kupiec_pof' - assert chr_res['test'] == 'christoffersen_independence' + assert "exception" in bt.columns + kup = kupiec_pof_test(bt["exception"].sum(), len(bt), 0.99) + chr_res = christoffersen_independence_test(bt["exception"].astype(int).to_numpy()) + assert kup["test"] == "kupiec_pof" + assert chr_res["test"] == "christoffersen_independence" diff --git a/tests/test_binomial_convergence.py b/tests/test_binomial_convergence.py index 8bb3c80..91c1383 100644 --- a/tests/test_binomial_convergence.py +++ b/tests/test_binomial_convergence.py @@ -1,5 +1,5 @@ -from models.black_scholes import bs_call_price from models.binomial_tree import binomial_call_price +from models.black_scholes import bs_call_price def test_binomial_converges_near_bs(): diff --git a/tests/test_black_scholes.py b/tests/test_black_scholes.py index b8406fb..3470cc1 100644 --- a/tests/test_black_scholes.py +++ b/tests/test_black_scholes.py @@ -1,4 +1,4 @@ -from models.black_scholes import bs_call_price, bs_call_greeks +from models.black_scholes import bs_call_greeks, bs_call_price def test_black_scholes_price_positive(): @@ -9,5 +9,5 @@ def test_black_scholes_price_positive(): def test_delta_range(): g = bs_call_greeks(100, 100, 1, 0.03, 0.25) - assert 0 < g['delta'] < 1 - assert g['gamma'] > 0 + assert 0 < g["delta"] < 1 + assert g["gamma"] > 0 diff --git a/tests/test_es_tail_diagnostics.py b/tests/test_es_tail_diagnostics.py index 3ea791f..eda4872 100644 --- a/tests/test_es_tail_diagnostics.py +++ b/tests/test_es_tail_diagnostics.py @@ -1,18 +1,21 @@ +import pandas as pd + from data.generate_synthetic_market import generate_synthetic_market -from risk.portfolio_var import historical_var_es, monte_carlo_var_es from risk.es_tail_diagnostics import es_tail_diagnostics -import pandas as pd +from risk.portfolio_var import historical_var_es, monte_carlo_var_es def test_es_tail_diagnostics_runs(): market = generate_synthetic_market(n_days=800) - hvar, hes = historical_var_es(market['portfolio_pnl']) - nvar, nes = monte_carlo_var_es(market['portfolio_pnl']) - df = pd.DataFrame([ - {'method': 'historical', 'VaR_99': hvar, 'ES_99': hes}, - {'method': 'normal_monte_carlo', 'VaR_99': nvar, 'ES_99': nes}, - ]) - out = es_tail_diagnostics(market['portfolio_pnl'], df) + hvar, hes = historical_var_es(market["portfolio_pnl"]) + nvar, nes = monte_carlo_var_es(market["portfolio_pnl"]) + df = pd.DataFrame( + [ + {"method": "historical", "VaR_99": hvar, "ES_99": hes}, + {"method": "normal_monte_carlo", "VaR_99": nvar, "ES_99": nes}, + ] + ) + out = es_tail_diagnostics(market["portfolio_pnl"], df) assert len(out) == 2 - assert (out['tail_observations'] > 0).all() - assert set(out['status']).issubset({'PASS', 'REVIEW'}) + assert (out["tail_observations"] > 0).all() + assert set(out["status"]).issubset({"PASS", "REVIEW"}) diff --git a/tests/test_github_runnable_audit.py b/tests/test_github_runnable_audit.py index 88a8c96..e8b6670 100644 --- a/tests/test_github_runnable_audit.py +++ b/tests/test_github_runnable_audit.py @@ -1,8 +1,8 @@ -from utils import project_path from governance.structure_audit import structure_audit +from utils import project_path def test_structure_audit_has_core_files(): req, _ = structure_audit(project_path()) - missing = req.loc[~req['exists'], 'item'].tolist() + missing = req.loc[~req["exists"], "item"].tolist() assert not missing diff --git a/tests/test_greeks.py b/tests/test_greeks.py index d39f612..1c5e982 100644 --- a/tests/test_greeks.py +++ b/tests/test_greeks.py @@ -1,8 +1,8 @@ -from models.black_scholes import bs_call_greeks from benchmarks.finite_difference_checks import finite_diff_delta, finite_diff_gamma +from models.black_scholes import bs_call_greeks def test_finite_difference_greeks(): g = bs_call_greeks(100, 100, 1, 0.03, 0.25) - assert abs(g['delta'] - finite_diff_delta(100,100,1,0.03,0.25)) < 1e-3 - assert abs(g['gamma'] - finite_diff_gamma(100,100,1,0.03,0.25)) < 1e-3 + assert abs(g["delta"] - finite_diff_delta(100, 100, 1, 0.03, 0.25)) < 1e-3 + assert abs(g["gamma"] - finite_diff_gamma(100, 100, 1, 0.03, 0.25)) < 1e-3 diff --git a/tests/test_no_arbitrage.py b/tests/test_no_arbitrage.py index cfb7e2e..db61ef9 100644 --- a/tests/test_no_arbitrage.py +++ b/tests/test_no_arbitrage.py @@ -1,10 +1,11 @@ import pandas as pd + from models.black_scholes import bs_call_price from validation.no_arbitrage_checks import call_no_arbitrage_checks def test_no_arbitrage_passes(): - df = pd.DataFrame({'S':[100], 'K':[100], 'T':[1], 'r':[0.03], 'sigma':[0.25]}) - df['bs_price'] = bs_call_price(df['S'], df['K'], df['T'], df['r'], df['sigma']) + df = pd.DataFrame({"S": [100], "K": [100], "T": [1], "r": [0.03], "sigma": [0.25]}) + df["bs_price"] = bs_call_price(df["S"], df["K"], df["T"], df["r"], df["sigma"]) out = call_no_arbitrage_checks(df) - assert out.iloc[0]['status'] == 'PASS' + assert out.iloc[0]["status"] == "PASS" diff --git a/tests/test_numerical_tolerance.py b/tests/test_numerical_tolerance.py index 8d4bfc0..a06d42a 100644 --- a/tests/test_numerical_tolerance.py +++ b/tests/test_numerical_tolerance.py @@ -1,14 +1,15 @@ import pandas as pd -from numerical_reliability.tolerance_gates import apply_tolerance_gate + from numerical_reliability.cpu_gpu_consistency import cpu_gpu_consistency_stub +from numerical_reliability.tolerance_gates import apply_tolerance_gate def test_tolerance_gate_statuses(): - df = pd.DataFrame({'abs_error':[0.01, 0.07, 0.20]}) - out = apply_tolerance_gate(df, 'abs_error', warn=0.05, block=0.15) - assert list(out['gate']) == ['PASS', 'REVIEW', 'BLOCK'] + df = pd.DataFrame({"abs_error": [0.01, 0.07, 0.20]}) + out = apply_tolerance_gate(df, "abs_error", warn=0.05, block=0.15) + assert list(out["gate"]) == ["PASS", "REVIEW", "BLOCK"] def test_cpu_gpu_not_run_safe(): out = cpu_gpu_consistency_stub(1.0) - assert out.iloc[0]['status'] == 'NOT_RUN' + assert out.iloc[0]["status"] == "NOT_RUN" diff --git a/tests/test_operational_replay.py b/tests/test_operational_replay.py index 925d5d8..53ba24d 100644 --- a/tests/test_operational_replay.py +++ b/tests/test_operational_replay.py @@ -1,6 +1,6 @@ -from pathlib import Path import subprocess import sys +from pathlib import Path ROOT = Path(__file__).resolve().parents[1] diff --git a/tests/test_public_market_data.py b/tests/test_public_market_data.py index a566ecc..6c8c2ad 100644 --- a/tests/test_public_market_data.py +++ b/tests/test_public_market_data.py @@ -1,4 +1,5 @@ from pathlib import Path + from data.generate_synthetic_market import create_public_sample_files from data.public_market_data import validate_public_market_data @@ -8,5 +9,5 @@ def test_public_market_data_validation(tmp_path: Path): checks, summary, panel, corr = validate_public_market_data(tmp_path) assert not checks.empty assert not summary.empty - assert 'DGS10_shock' in panel.columns - assert 'risk_factor' in corr.columns + assert "DGS10_shock" in panel.columns + assert "risk_factor" in corr.columns diff --git a/tests/test_release_gate.py b/tests/test_release_gate.py index 5dcf8d6..58cb56c 100644 --- a/tests/test_release_gate.py +++ b/tests/test_release_gate.py @@ -1,14 +1,15 @@ import pandas as pd + from governance.release_gate import release_decision def test_release_gate_blocks_critical(): - findings = pd.DataFrame({'severity':['CRITICAL']}) + findings = pd.DataFrame({"severity": ["CRITICAL"]}) dec = release_decision(90, findings, 0.0) - assert dec['release_decision'] == 'BLOCK' + assert dec["release_decision"] == "BLOCK" def test_release_gate_passes_clean(): - findings = pd.DataFrame({'severity':['LOW']}) + findings = pd.DataFrame({"severity": ["LOW"]}) dec = release_decision(90, findings, 0.0) - assert dec['release_decision'] == 'PASS' + assert dec["release_decision"] == "PASS" diff --git a/tests/test_risk_factor_modellability.py b/tests/test_risk_factor_modellability.py index 053f245..64a13ed 100644 --- a/tests/test_risk_factor_modellability.py +++ b/tests/test_risk_factor_modellability.py @@ -1,4 +1,5 @@ from pathlib import Path + from data.generate_synthetic_market import create_public_sample_files from data.public_market_data import validate_public_market_data from validation.risk_factor_modellability import risk_factor_modellability_proxy @@ -9,5 +10,7 @@ def test_modellability_proxy_public_sample(tmp_path: Path): _, _, panel, _ = validate_public_market_data(tmp_path) out = risk_factor_modellability_proxy(panel) assert len(out) >= 4 - assert set(out['modellability_proxy']).issubset({'MODELLABLE_PROXY','NON_MODELLABLE_PROXY'}) - assert (out['non_missing_observations'] >= 250).all() + assert set(out["modellability_proxy"]).issubset( + {"MODELLABLE_PROXY", "NON_MODELLABLE_PROXY"} + ) + assert (out["non_missing_observations"] >= 250).all() diff --git a/tests/test_specialty_layers.py b/tests/test_specialty_layers.py index 03b5725..c770a06 100644 --- a/tests/test_specialty_layers.py +++ b/tests/test_specialty_layers.py @@ -1,15 +1,18 @@ import pandas as pd + from specialty.asymmetric_validation_envelope import asymmetric_envelope from specialty.validation_health_score import build_validation_health_score def test_asymmetric_envelope_penalizes_underestimation(): - df = pd.DataFrame({'corrected_error':[0.1,0.1], 'risk_underestimation':[True, False]}) - out = asymmetric_envelope(df, 'corrected_error', 'risk_underestimation') - assert out.loc[0,'asymmetric_penalty'] > out.loc[1,'asymmetric_penalty'] + df = pd.DataFrame( + {"corrected_error": [0.1, 0.1], "risk_underestimation": [True, False]} + ) + out = asymmetric_envelope(df, "corrected_error", "risk_underestimation") + assert out.loc[0, "asymmetric_penalty"] > out.loc[1, "asymmetric_penalty"] def test_health_score(): - findings = pd.DataFrame({'severity':['LOW','HIGH']}) + findings = pd.DataFrame({"severity": ["LOW", "HIGH"]}) out = build_validation_health_score(findings) - assert 0 <= out.iloc[0]['validation_health_score'] <= 100 + assert 0 <= out.iloc[0]["validation_health_score"] <= 100 diff --git a/tests/test_stressed_var.py b/tests/test_stressed_var.py index 3d5fc65..34540b3 100644 --- a/tests/test_stressed_var.py +++ b/tests/test_stressed_var.py @@ -1,11 +1,17 @@ import pandas as pd + from data.generate_synthetic_market import generate_synthetic_market from risk.stressed_var import stressed_var_window def test_stressed_var_window_outputs_multiplier(): market = generate_synthetic_market(n_days=420) - out = stressed_var_window(market['portfolio_pnl'], stress_window=40) - assert {'stressed_var', 'stressed_es', 'stressed_var_multiplier', 'status'}.issubset(out.columns) - assert out['selected_window_observations'].iloc[0] == 40 - assert out['stressed_var'].iloc[0] > 0 + out = stressed_var_window(market["portfolio_pnl"], stress_window=40) + assert { + "stressed_var", + "stressed_es", + "stressed_var_multiplier", + "status", + }.issubset(out.columns) + assert out["selected_window_observations"].iloc[0] == 40 + assert out["stressed_var"].iloc[0] > 0 diff --git a/tests/test_var_es.py b/tests/test_var_es.py index 200f97e..b81e5b5 100644 --- a/tests/test_var_es.py +++ b/tests/test_var_es.py @@ -1,4 +1,5 @@ import numpy as np + from risk.portfolio_var import historical_var_es, monte_carlo_var_es