Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions risk_analytics_suite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Quant Risk Analytics Suite

Quantitative Risk Framework for Multi-Asset Portfolios. This project provides a modular research environment for estimating multi-factor models, decomposing risk, and visualising portfolio diagnostics through an interactive dashboard.

## Key Features
- **Data Engineering** – pull, cache, and preprocess multi-asset price series together with macro factors.
- **Factor Modelling** – construct Fama-French style factors with momentum, liquidity, and custom macro extensions; run rolling regressions to obtain exposures.
- **Dimensionality Reduction** – perform Ledoit-Wolf shrinkage covariance estimation and principal component analysis on the return matrix.
- **Risk Analytics** – compute historical, Monte Carlo, and Extreme Value Theory (EVT) based VaR / CVaR metrics with Kupiec backtesting utilities.
- **Stress Testing** – evaluate bespoke macro shock scenarios and produce interactive loss waterfalls.
- **Attribution & Performance** – breakdown portfolio returns into factor contributions and residuals; run simple backtests with turnover-aware costs.
- **Visualisation & Reporting** – produce Plotly charts and a Streamlit dashboard for quick iteration.

## Project Layout
```
risk_analytics_suite/
├── README.md
├── requirements.txt
├── configs/
│ ├── base.yaml
│ ├── factors.yaml
│ └── risk.yaml
├── data/
│ ├── raw/
│ └── processed/
├── risklab/
│ ├── __init__.py
│ ├── io.py
│ ├── preprocess.py
│ ├── factors.py
│ ├── pca.py
│ ├── risk.py
│ ├── stress.py
│ ├── attribution.py
│ ├── backtest.py
│ └── viz.py
├── experiments/
│ ├── exp_factor_exposure.ipynb
│ ├── exp_var_compare.ipynb
│ └── exp_stress_scenarios.ipynb
└── app/
└── dashboard.py
```

## Quickstart
1. **Clone & create an isolated environment**
```bash
git clone https://github.com/<your-user>/quant-risk-analytics-suite.git
cd quant-risk-analytics-suite/risk_analytics_suite
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --upgrade pip
pip install -r requirements.txt
```

2. **Fetch a sample dataset**
```bash
python -m risk_analytics_suite.risklab.io --config base
```
The command reads `configs/base.yaml`, downloads the configured tickers from Yahoo Finance, and caches the CSV in `data/raw/`.
Adjust tickers or the time range by editing `configs/base.yaml` (or by creating a new config file and passing `--config my_config`).

3. **Run exploratory notebooks (optional)**
```bash
jupyter lab
```
Open the notebooks in `experiments/` to reproduce the factor exposure, VaR comparison, or stress scenario studies. Each notebook assumes that the cached prices from step 2 are available.

4. **Execute the automated checks**
```bash
pytest
```
This repository currently contains placeholder tests; add your own regression or integration tests as you expand the toolkit.

5. **Launch the Streamlit dashboard**
```bash
streamlit run app/dashboard.py
```
The app loads prices/factors based on the active configuration and surfaces VaR, CVaR, factor exposures, and stress-test views. Use `--server.port <port>` when running on shared infrastructure (e.g., cloud notebooks).

6. **(Optional) Use the modules programmatically**
```python
from risk_analytics_suite.risklab import io, preprocess, risk

prices = io.fetch_prices(["SPY", "QQQ"], start="2020-01-01", end="2024-01-01")
returns = preprocess.to_log_returns(prices)
portfolio_var = risk.var_historical(returns["SPY"], alpha=0.95)
print(portfolio_var)
```
Each submodule exposes well-documented functions so you can compose your own research scripts or notebooks.

## Reproducibility
Configuration is managed via [Hydra](https://hydra.cc) YAML files. Adjust tickers, windows, and confidence levels without modifying code. Compose alternative configurations under `configs/` and call the relevant module with `--config <name>`. A ``make reproduce`` workflow can be added to fetch fresh data and execute notebooks end-to-end.

## License
MIT
Empty file.
53 changes: 53 additions & 0 deletions risk_analytics_suite/app/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Streamlit dashboard for the Quant Risk Analytics Suite."""
from __future__ import annotations

import streamlit as st

from risk_analytics_suite.risklab import io, preprocess, factors, risk, viz


st.set_page_config(page_title="Quant Risk Analytics Suite", layout="wide")
st.title("Quant Risk Analytics Suite")

config = io.load_hydra_config()
dataset = config.dataset


@st.cache_data(show_spinner=False)
def load_data():
prices = io.fetch_prices(
dataset.tickers,
dataset.start,
dataset.end,
dataset.get("source", "yfinance"),
dataset.get("frequency", "1d"),
)
returns = preprocess.to_log_returns(prices)
return prices, returns


prices, returns = load_data()
st.sidebar.header("Portfolio Setup")
selected = st.sidebar.multiselect("Assets", list(returns.columns), default=list(returns.columns)[:4])
weights = st.sidebar.slider("Equal Weight Portfolio", 0.0, 1.0, 1.0)
portfolio_returns = returns[selected].mean(axis=1) * weights

st.subheader("Portfolio Performance")
fig_equity = viz.line_chart((1 + portfolio_returns).cumprod(), title="Equity Curve")
st.plotly_chart(fig_equity, use_container_width=True)

st.subheader("Risk Metrics")
alpha = st.sidebar.select_slider("VaR Confidence", options=[0.90, 0.95, 0.975, 0.99], value=0.95)
var_hist = risk.var_historical(portfolio_returns, alpha=alpha)
cvar_hist = risk.cvar_historical(portfolio_returns, alpha=alpha)
st.metric("Historical VaR", f"{var_hist:.2%}")
st.metric("Historical CVaR", f"{cvar_hist:.2%}")

st.subheader("Factor Exposures")
specs = [
factors.FactorSpec(name="market", method="fama_french", params={"level": "mkt"}),
factors.FactorSpec(name="momentum", method="momentum", params={"window": 126}),
]
factor_df = factors.build_factors(returns[selected], specs)
exposures = factors.regress_exposure(returns[selected], factor_df)
st.plotly_chart(viz.heatmap(exposures.drop(columns="r2"), title="Factor Betas"), use_container_width=True)
10 changes: 10 additions & 0 deletions risk_analytics_suite/configs/base.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
dataset:
tickers: ["SPY", "QQQ", "EFA", "TLT", "GLD", "USO", "XLF", "XLE"]
benchmark: "SPY"
start: "2015-01-01"
end: "2024-01-01"
source: "yfinance"
frequency: "1d"
preprocess:
winsor_limits: [0.01, 0.99]
standardize: true
29 changes: 29 additions & 0 deletions risk_analytics_suite/configs/factors.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
factors:
- name: "market"
method: "fama_french"
params:
level: "mkt"
- name: "size"
method: "fama_french"
params:
level: "smb"
- name: "value"
method: "fama_french"
params:
level: "hml"
- name: "momentum"
method: "momentum"
params:
window: 252
- name: "liquidity"
method: "liquidity"
params:
volume_window: 63
- name: "vix"
method: "macro"
params:
symbol: "^VIX"
- name: "term_spread"
method: "macro"
params:
fred_series: "T10Y2Y"
13 changes: 13 additions & 0 deletions risk_analytics_suite/configs/risk.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
risk:
alpha: 0.95
backtest_window: 252
methods:
- historical
- monte_carlo
- evt
mc:
n_paths: 10000
seed: 42
evt:
threshold_quantile: 0.9
tail: "lower"
25 changes: 25 additions & 0 deletions risk_analytics_suite/experiments/exp_factor_exposure.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Placeholder\n",
"This notebook documents the ${nb/exp_/} experiment. Populate with data pulls and analysis steps."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
25 changes: 25 additions & 0 deletions risk_analytics_suite/experiments/exp_stress_scenarios.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Placeholder\n",
"This notebook documents the ${nb/exp_/} experiment. Populate with data pulls and analysis steps."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
25 changes: 25 additions & 0 deletions risk_analytics_suite/experiments/exp_var_compare.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Placeholder\n",
"This notebook documents the ${nb/exp_/} experiment. Populate with data pulls and analysis steps."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
15 changes: 15 additions & 0 deletions risk_analytics_suite/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
numpy
pandas
scipy
scikit-learn
statsmodels
numba
plotly
streamlit
pydantic
hydra-core
yfinance
fredapi
matplotlib
seaborn
pyyaml
14 changes: 14 additions & 0 deletions risk_analytics_suite/risklab/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""RiskLab package exposing analytics utilities."""
from . import io, preprocess, factors, pca, risk, stress, attribution, backtest, viz

__all__ = [
"io",
"preprocess",
"factors",
"pca",
"risk",
"stress",
"attribution",
"backtest",
"viz",
]
43 changes: 43 additions & 0 deletions risk_analytics_suite/risklab/attribution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Factor attribution utilities."""
from __future__ import annotations

from typing import Mapping

import numpy as np
import pandas as pd
import statsmodels.api as sm


def factor_attribution(
weights: Mapping[str, float],
factor_returns: pd.DataFrame,
benchmark_weights: Mapping[str, float] | None = None,
) -> pd.DataFrame:
"""Decompose returns into factor contributions."""

weights = pd.Series(weights, dtype=float)
factor_portfolio = factor_returns.mul(weights, axis=1)
contributions = factor_portfolio.sum(axis=1)
df = pd.DataFrame({"factor_contribution": contributions})
if benchmark_weights is not None:
benchmark = pd.Series(benchmark_weights, dtype=float)
active = weights - benchmark.reindex(weights.index).fillna(0.0)
df["active_weight"] = active.reindex(weights.index).sum()
return df


def performance_breakdown(returns: pd.DataFrame, factors: pd.DataFrame) -> pd.DataFrame:
"""Calculate rolling R-squared and residual volatility."""

aligned_returns, aligned_factors = returns.align(factors, join="inner", axis=0)
resid_vol = {}
for asset in aligned_returns:
y = aligned_returns[asset]
X = sm.add_constant(aligned_factors)
model = sm.OLS(y, X, missing="drop")
results = model.fit()
resid_vol[asset] = {
"r2": results.rsquared,
"resid_vol": results.resid.std(ddof=0) * np.sqrt(252),
}
return pd.DataFrame(resid_vol).T
34 changes: 34 additions & 0 deletions risk_analytics_suite/risklab/backtest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Simple backtesting engine."""
from __future__ import annotations

from typing import Mapping

import pandas as pd


def run_backtest(
signals: pd.DataFrame,
prices: pd.DataFrame | None = None,
costs: float = 0.0005,
rebalance: str = "W-FRI",
) -> dict[str, pd.Series]:
"""Turn trading signals into portfolio performance."""

weights = signals.resample(rebalance).last().fillna(0.0)
weights = weights.div(weights.abs().sum(axis=1), axis=0).fillna(0.0)
if prices is None:
returns = signals.pct_change().fillna(0.0)
else:
returns = prices.pct_change().reindex(weights.index, method="ffill").fillna(0.0)
aligned_returns = returns.reindex(weights.index).fillna(0.0)
turnover = weights.diff().abs().sum(axis=1).fillna(0.0)
gross = (weights.shift().fillna(0.0) * aligned_returns).sum(axis=1)
net = gross - costs * turnover
cum = (1 + net).cumprod()
stats = {
"returns": net,
"gross_returns": gross,
"turnover": turnover,
"equity_curve": cum,
}
return stats
Loading