portfolioriskpro 2.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- portfolioriskpro-2.1.0/PKG-INFO +19 -0
- portfolioriskpro-2.1.0/portfoliorisk/__init__.py +20 -0
- portfolioriskpro-2.1.0/portfoliorisk/data.py +32 -0
- portfolioriskpro-2.1.0/portfoliorisk/garch.py +68 -0
- portfolioriskpro-2.1.0/portfoliorisk/montecarlo.py +22 -0
- portfolioriskpro-2.1.0/portfoliorisk/neural.py +176 -0
- portfolioriskpro-2.1.0/portfoliorisk/optimize.py +63 -0
- portfolioriskpro-2.1.0/portfoliorisk/pipeline.py +152 -0
- portfolioriskpro-2.1.0/portfoliorisk/report.py +230 -0
- portfolioriskpro-2.1.0/portfoliorisk/risk.py +22 -0
- portfolioriskpro-2.1.0/portfoliorisk/stress.py +17 -0
- portfolioriskpro-2.1.0/portfolioriskpro.egg-info/PKG-INFO +19 -0
- portfolioriskpro-2.1.0/portfolioriskpro.egg-info/SOURCES.txt +16 -0
- portfolioriskpro-2.1.0/portfolioriskpro.egg-info/dependency_links.txt +1 -0
- portfolioriskpro-2.1.0/portfolioriskpro.egg-info/requires.txt +9 -0
- portfolioriskpro-2.1.0/portfolioriskpro.egg-info/top_level.txt +1 -0
- portfolioriskpro-2.1.0/setup.cfg +4 -0
- portfolioriskpro-2.1.0/setup.py +22 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: portfolioriskpro
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: Smart portfolio risk analysis with Efficient Frontier and LSTM forecasting
|
|
5
|
+
Author: Yash
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Requires-Dist: PyPortfolioOpt>=1.5.0
|
|
8
|
+
Requires-Dist: arch>=5.0.0
|
|
9
|
+
Requires-Dist: numpy>=1.21.0
|
|
10
|
+
Requires-Dist: pandas>=1.3.0
|
|
11
|
+
Requires-Dist: scipy>=1.7.0
|
|
12
|
+
Requires-Dist: yfinance>=0.2.0
|
|
13
|
+
Provides-Extra: deep
|
|
14
|
+
Requires-Dist: tensorflow>=2.10.0; extra == "deep"
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: provides-extra
|
|
17
|
+
Dynamic: requires-dist
|
|
18
|
+
Dynamic: requires-python
|
|
19
|
+
Dynamic: summary
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
portfoliorisk v2.0.0
|
|
3
|
+
====================
|
|
4
|
+
Quantitative portfolio risk analysis with smart allocation and deep learning.
|
|
5
|
+
|
|
6
|
+
Upgrades in v2.0.0:
|
|
7
|
+
- Smart weight allocation (Efficient Frontier / Maximum Sharpe Ratio)
|
|
8
|
+
- LSTM Neural Network volatility forecasting (TensorFlow)
|
|
9
|
+
|
|
10
|
+
Usage
|
|
11
|
+
-----
|
|
12
|
+
import portfoliorisk as pr
|
|
13
|
+
pr.run(["AAPL", "NVDA", "JPM", "XOM", "GLD", "MSFT", "AMZN"], investment=50000)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .pipeline import run
|
|
17
|
+
|
|
18
|
+
__version__ = "2.0.1"
|
|
19
|
+
__author__ = "Yash"
|
|
20
|
+
__all__ = ["run"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
portfoliorisk.data
|
|
3
|
+
==================
|
|
4
|
+
Download and pre-process historical price data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import yfinance as yf
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def fetch_data(tickers: list, period: str = "5y"):
|
|
13
|
+
"""
|
|
14
|
+
Download adjusted closing prices and compute log returns.
|
|
15
|
+
|
|
16
|
+
Returns
|
|
17
|
+
-------
|
|
18
|
+
data : pd.DataFrame – adjusted close prices
|
|
19
|
+
log_returns : pd.DataFrame – daily log returns
|
|
20
|
+
"""
|
|
21
|
+
data = yf.download(tickers, period=period, auto_adjust=True, progress=False)["Close"]
|
|
22
|
+
if isinstance(data, pd.Series):
|
|
23
|
+
data = data.to_frame(name=tickers[0])
|
|
24
|
+
data = data[tickers]
|
|
25
|
+
data = data.dropna()
|
|
26
|
+
|
|
27
|
+
if data.empty:
|
|
28
|
+
raise ValueError(f"No data returned for tickers: {tickers}. "
|
|
29
|
+
"Check ticker symbols and internet connection.")
|
|
30
|
+
|
|
31
|
+
log_returns = np.log(data / data.shift(1)).dropna()
|
|
32
|
+
return data, log_returns
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
portfoliorisk.garch
|
|
3
|
+
===================
|
|
4
|
+
GARCH(1,1) volatility modelling and forecasting.
|
|
5
|
+
Returns conditional volatility series so LSTM can use it as input.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from arch import arch_model
|
|
13
|
+
ARCH_AVAILABLE = True
|
|
14
|
+
except ImportError:
|
|
15
|
+
ARCH_AVAILABLE = False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def fit_garch(port_returns: pd.Series, horizon: int = 7) -> dict:
|
|
19
|
+
"""
|
|
20
|
+
Fit a GARCH(1,1) model with Student-t errors to portfolio returns
|
|
21
|
+
and produce a short-term volatility forecast.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
dict with keys:
|
|
26
|
+
omega, alpha, beta, nu – model parameters
|
|
27
|
+
current_vol_annualised – current conditional vol (annualised %)
|
|
28
|
+
forecast_vols – array of GARCH forecast daily vols
|
|
29
|
+
conditional_vol_series – full array of daily conditional vols (for LSTM input)
|
|
30
|
+
log_likelihood, aic, bic – model fit statistics
|
|
31
|
+
available – True if arch library is installed
|
|
32
|
+
"""
|
|
33
|
+
if not ARCH_AVAILABLE:
|
|
34
|
+
return {
|
|
35
|
+
"available": False,
|
|
36
|
+
"conditional_vol_series": np.array([]),
|
|
37
|
+
"note": "Install arch library: pip install arch"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
scaled = port_returns * 100
|
|
41
|
+
model = arch_model(scaled, vol="Garch", p=1, q=1, dist="t")
|
|
42
|
+
res = model.fit(disp="off")
|
|
43
|
+
|
|
44
|
+
params = res.params
|
|
45
|
+
forecasts = res.forecast(horizon=horizon)
|
|
46
|
+
fvol = np.sqrt(forecasts.variance.values[-1])
|
|
47
|
+
|
|
48
|
+
current_vol_ann = float(
|
|
49
|
+
np.sqrt(res.conditional_volatility.iloc[-1] ** 2 * 252)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Full conditional volatility series — used as LSTM training input
|
|
53
|
+
cond_vol_series = res.conditional_volatility.values / 100 # rescale back
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
"available": True,
|
|
57
|
+
"omega": float(params.get("omega", np.nan)),
|
|
58
|
+
"alpha": float(params.get("alpha[1]", np.nan)),
|
|
59
|
+
"beta": float(params.get("beta[1]", np.nan)),
|
|
60
|
+
"nu": float(params.get("nu", np.nan)),
|
|
61
|
+
"current_vol_annualised": current_vol_ann,
|
|
62
|
+
"forecast_vols": fvol.tolist(),
|
|
63
|
+
"conditional_vol_series": cond_vol_series,
|
|
64
|
+
"log_likelihood": float(res.loglikelihood),
|
|
65
|
+
"aic": float(res.aic),
|
|
66
|
+
"bic": float(res.bic),
|
|
67
|
+
"num_observations": int(res.nobs),
|
|
68
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
portfoliorisk.montecarlo
|
|
3
|
+
========================
|
|
4
|
+
Monte Carlo simulation under Normal and Student-t distributions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_monte_carlo(port_mean, port_std, initial_investment,
|
|
11
|
+
num_simulations=10_000, time_horizon=252,
|
|
12
|
+
dist="normal", df=3):
|
|
13
|
+
if dist == "normal":
|
|
14
|
+
sim_returns = np.random.normal(port_mean, port_std,
|
|
15
|
+
(time_horizon, num_simulations))
|
|
16
|
+
elif dist == "t":
|
|
17
|
+
sim_returns = np.random.standard_t(df=df,
|
|
18
|
+
size=(time_horizon, num_simulations))
|
|
19
|
+
sim_returns = sim_returns * port_std + port_mean
|
|
20
|
+
else:
|
|
21
|
+
raise ValueError(f"Unknown distribution '{dist}'.")
|
|
22
|
+
return initial_investment * np.exp(np.cumsum(sim_returns, axis=0))
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
portfoliorisk.neural
|
|
3
|
+
====================
|
|
4
|
+
LSTM Neural Network for volatility forecasting.
|
|
5
|
+
|
|
6
|
+
Combines GARCH(1,1) conditional volatility estimates with a deep learning
|
|
7
|
+
LSTM model to produce more precise short-term volatility forecasts.
|
|
8
|
+
|
|
9
|
+
Architecture:
|
|
10
|
+
Input : Rolling window of GARCH conditional volatility + squared returns
|
|
11
|
+
Model : 2-layer LSTM → Dense → output
|
|
12
|
+
Output : 7-day ahead volatility forecast
|
|
13
|
+
|
|
14
|
+
Why LSTM?
|
|
15
|
+
GARCH assumes a fixed decay rate for volatility shocks. LSTM learns
|
|
16
|
+
non-linear, time-varying patterns in volatility that GARCH cannot capture —
|
|
17
|
+
such as volatility spikes around earnings, macro events, or market regimes.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
import warnings
|
|
22
|
+
warnings.filterwarnings("ignore")
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import tensorflow as tf
|
|
26
|
+
from tensorflow.keras.models import Sequential
|
|
27
|
+
from tensorflow.keras.layers import LSTM, Dense, Dropout
|
|
28
|
+
from tensorflow.keras.callbacks import EarlyStopping
|
|
29
|
+
TF_AVAILABLE = True
|
|
30
|
+
except ImportError:
|
|
31
|
+
TF_AVAILABLE = False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _build_lstm(input_shape):
|
|
35
|
+
"""Build a 2-layer LSTM model for volatility sequence prediction."""
|
|
36
|
+
model = Sequential([
|
|
37
|
+
LSTM(64, input_shape=input_shape, return_sequences=True),
|
|
38
|
+
Dropout(0.2),
|
|
39
|
+
LSTM(32, return_sequences=False),
|
|
40
|
+
Dropout(0.2),
|
|
41
|
+
Dense(16, activation="relu"),
|
|
42
|
+
Dense(1, activation="linear")
|
|
43
|
+
])
|
|
44
|
+
model.compile(optimizer="adam", loss="mse")
|
|
45
|
+
return model
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _make_sequences(series: np.ndarray, lookback: int = 20):
|
|
49
|
+
"""
|
|
50
|
+
Convert a 1-D time series into supervised (X, y) sequences.
|
|
51
|
+
X: windows of length `lookback`, y: next value.
|
|
52
|
+
"""
|
|
53
|
+
X, y = [], []
|
|
54
|
+
for i in range(lookback, len(series)):
|
|
55
|
+
X.append(series[i - lookback: i])
|
|
56
|
+
y.append(series[i])
|
|
57
|
+
return np.array(X), np.array(y)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def forecast_with_lstm(
|
|
61
|
+
garch_conditional_vol: np.ndarray,
|
|
62
|
+
port_returns: np.ndarray,
|
|
63
|
+
horizon: int = 7,
|
|
64
|
+
lookback: int = 20,
|
|
65
|
+
epochs: int = 50,
|
|
66
|
+
batch_size: int = 32,
|
|
67
|
+
) -> dict:
|
|
68
|
+
"""
|
|
69
|
+
Train an LSTM on GARCH conditional volatility series and forecast
|
|
70
|
+
volatility for the next `horizon` trading days.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
garch_conditional_vol : 1-D array of GARCH daily conditional volatility
|
|
75
|
+
port_returns : 1-D array of daily portfolio log returns
|
|
76
|
+
horizon : number of days to forecast (default 7)
|
|
77
|
+
lookback : LSTM sequence window length (default 20 days)
|
|
78
|
+
epochs : training epochs (default 50)
|
|
79
|
+
batch_size : mini-batch size (default 32)
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
dict with keys:
|
|
84
|
+
lstm_available : bool
|
|
85
|
+
forecast_vols : list of horizon daily volatility forecasts (annualised %)
|
|
86
|
+
train_loss : final training loss (MSE)
|
|
87
|
+
model_summary : string description of the architecture
|
|
88
|
+
note : explanation of what was done
|
|
89
|
+
"""
|
|
90
|
+
if not TF_AVAILABLE:
|
|
91
|
+
return {
|
|
92
|
+
"lstm_available": False,
|
|
93
|
+
"note": "TensorFlow not installed. Run: pip install tensorflow",
|
|
94
|
+
"forecast_vols": [],
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
# ── 1. Prepare input features ──────────────────────────────────────
|
|
99
|
+
# Combine GARCH vol and squared returns as dual input signal
|
|
100
|
+
sq_returns = np.array(port_returns) ** 2
|
|
101
|
+
garch_vol = np.array(garch_conditional_vol)
|
|
102
|
+
|
|
103
|
+
# Align lengths
|
|
104
|
+
min_len = min(len(sq_returns), len(garch_vol))
|
|
105
|
+
sq_returns = sq_returns[-min_len:]
|
|
106
|
+
garch_vol = garch_vol[-min_len:]
|
|
107
|
+
|
|
108
|
+
# Normalise to [0, 1] for training stability
|
|
109
|
+
vol_min, vol_max = garch_vol.min(), garch_vol.max()
|
|
110
|
+
vol_norm = (garch_vol - vol_min) / (vol_max - vol_min + 1e-8)
|
|
111
|
+
|
|
112
|
+
# ── 2. Build sequences ─────────────────────────────────────────────
|
|
113
|
+
X, y = _make_sequences(vol_norm, lookback)
|
|
114
|
+
X = X.reshape((X.shape[0], X.shape[1], 1)) # (samples, timesteps, features)
|
|
115
|
+
|
|
116
|
+
if len(X) < 50:
|
|
117
|
+
return {
|
|
118
|
+
"lstm_available": False,
|
|
119
|
+
"note": "Insufficient data for LSTM training (need >70 observations).",
|
|
120
|
+
"forecast_vols": [],
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# ── 3. Train/test split (80/20) ────────────────────────────────────
|
|
124
|
+
split = int(len(X) * 0.8)
|
|
125
|
+
X_train, y_train = X[:split], y[:split]
|
|
126
|
+
|
|
127
|
+
# ── 4. Build and train model ───────────────────────────────────────
|
|
128
|
+
tf.random.set_seed(42)
|
|
129
|
+
model = _build_lstm((lookback, 1))
|
|
130
|
+
|
|
131
|
+
early_stop = EarlyStopping(monitor="loss", patience=5, restore_best_weights=True)
|
|
132
|
+
history = model.fit(
|
|
133
|
+
X_train, y_train,
|
|
134
|
+
epochs=epochs,
|
|
135
|
+
batch_size=batch_size,
|
|
136
|
+
callbacks=[early_stop],
|
|
137
|
+
verbose=0
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# ── 5. Autoregressive forecast ─────────────────────────────────────
|
|
141
|
+
# Start from the last `lookback` observations and predict step-by-step
|
|
142
|
+
last_seq = vol_norm[-lookback:].copy()
|
|
143
|
+
forecasts = []
|
|
144
|
+
|
|
145
|
+
for _ in range(horizon):
|
|
146
|
+
inp = last_seq.reshape(1, lookback, 1)
|
|
147
|
+
pred = model.predict(inp, verbose=0)[0][0]
|
|
148
|
+
forecasts.append(float(pred))
|
|
149
|
+
last_seq = np.append(last_seq[1:], pred)
|
|
150
|
+
|
|
151
|
+
# ── 6. Denormalise forecasts back to original vol scale ────────────
|
|
152
|
+
forecasts_denorm = [v * (vol_max - vol_min) + vol_min for v in forecasts]
|
|
153
|
+
|
|
154
|
+
# Annualise: daily vol * sqrt(252) * 100 for % display
|
|
155
|
+
forecasts_annual = [v * np.sqrt(252) * 100 for v in forecasts_denorm]
|
|
156
|
+
|
|
157
|
+
train_loss = float(history.history["loss"][-1])
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
"lstm_available": True,
|
|
161
|
+
"forecast_vols": forecasts_annual,
|
|
162
|
+
"train_loss": train_loss,
|
|
163
|
+
"epochs_run": len(history.history["loss"]),
|
|
164
|
+
"model_summary": "2-layer LSTM (64→32 units) + Dropout(0.2) + Dense(16) → Dense(1)",
|
|
165
|
+
"note": (
|
|
166
|
+
"LSTM trained on GARCH conditional volatility series. "
|
|
167
|
+
"Forecasts are annualised volatility percentages."
|
|
168
|
+
),
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
except Exception as e:
|
|
172
|
+
return {
|
|
173
|
+
"lstm_available": False,
|
|
174
|
+
"note": f"LSTM training failed: {str(e)}",
|
|
175
|
+
"forecast_vols": [],
|
|
176
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
portfoliorisk.optimize
|
|
3
|
+
======================
|
|
4
|
+
Smart portfolio weight allocation using Efficient Frontier (Maximum Sharpe Ratio)
|
|
5
|
+
with a minimum weight constraint so every stock receives capital.
|
|
6
|
+
|
|
7
|
+
Strategy:
|
|
8
|
+
- Every stock gets a guaranteed minimum floor allocation (default 5%)
|
|
9
|
+
- The remaining capital is distributed optimally using Maximum Sharpe Ratio
|
|
10
|
+
- This ensures diversification while still being data-driven
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from pypfopt import EfficientFrontier, risk_models, expected_returns
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def optimize_portfolio(data, risk_free_rate: float = 0.02, min_weight: float = 0.05):
|
|
18
|
+
"""
|
|
19
|
+
Compute smart portfolio weights ensuring every stock receives capital.
|
|
20
|
+
|
|
21
|
+
Every asset gets at least min_weight (floor allocation).
|
|
22
|
+
The remaining weight is distributed using Maximum Sharpe Ratio optimisation.
|
|
23
|
+
This combines guaranteed diversification with data-driven intelligence.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
data : pd.DataFrame of adjusted close prices
|
|
28
|
+
risk_free_rate : annualised risk-free rate (default 2%)
|
|
29
|
+
min_weight : minimum allocation per stock (default 5%)
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
weights : dict {ticker: weight} -- constrained optimal weights
|
|
34
|
+
perf : tuple (expected_return, annual_volatility, sharpe_ratio)
|
|
35
|
+
"""
|
|
36
|
+
n = len(data.columns)
|
|
37
|
+
mu = expected_returns.mean_historical_return(data)
|
|
38
|
+
S = risk_models.sample_cov(data)
|
|
39
|
+
|
|
40
|
+
# Each stock gets at least min_weight
|
|
41
|
+
# Max weight is capped so no single stock dominates
|
|
42
|
+
max_weight = max(1.0 - (n - 1) * min_weight, min_weight + 0.01)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
ef = EfficientFrontier(mu, S, weight_bounds=(min_weight, max_weight))
|
|
46
|
+
ef.max_sharpe(risk_free_rate=risk_free_rate)
|
|
47
|
+
weights = dict(ef.clean_weights())
|
|
48
|
+
perf = ef.portfolio_performance(verbose=False, risk_free_rate=risk_free_rate)
|
|
49
|
+
except Exception:
|
|
50
|
+
# Fallback to equal weights if constrained optimisation fails
|
|
51
|
+
equal_w = round(1.0 / n, 6)
|
|
52
|
+
weights = {t: equal_w for t in data.columns}
|
|
53
|
+
w_arr = np.array([equal_w] * n)
|
|
54
|
+
ann_ret = float((mu.values * w_arr).sum())
|
|
55
|
+
ann_vol = float(np.sqrt(np.dot(w_arr.T, np.dot(S.values, w_arr))))
|
|
56
|
+
sharpe = (ann_ret - risk_free_rate) / ann_vol if ann_vol > 0 else 0.0
|
|
57
|
+
perf = (ann_ret, ann_vol, sharpe)
|
|
58
|
+
|
|
59
|
+
# Normalise so weights sum exactly to 1.0
|
|
60
|
+
total = sum(weights.values())
|
|
61
|
+
weights = {t: round(w / total, 6) for t, w in weights.items()}
|
|
62
|
+
|
|
63
|
+
return weights, perf
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
portfoliorisk.pipeline
|
|
3
|
+
======================
|
|
4
|
+
Full risk analysis pipeline — smart weights + LSTM neural network.
|
|
5
|
+
|
|
6
|
+
Two major upgrades over v1.0.x:
|
|
7
|
+
1. Smart weight allocation — Efficient Frontier (Maximum Sharpe Ratio)
|
|
8
|
+
instead of naive equal weighting. The optimizer studies historical
|
|
9
|
+
return, volatility, and correlations to find the best allocation.
|
|
10
|
+
|
|
11
|
+
2. LSTM volatility forecast — A 2-layer LSTM neural network is trained
|
|
12
|
+
on the GARCH conditional volatility series to produce a more precise
|
|
13
|
+
7-day ahead forecast that captures non-linear volatility patterns.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import warnings
|
|
18
|
+
warnings.filterwarnings("ignore")
|
|
19
|
+
|
|
20
|
+
from .data import fetch_data
|
|
21
|
+
from .optimize import optimize_portfolio
|
|
22
|
+
from .montecarlo import run_monte_carlo
|
|
23
|
+
from .risk import compute_var_cvar, compute_max_drawdown
|
|
24
|
+
from .garch import fit_garch
|
|
25
|
+
from .neural import forecast_with_lstm
|
|
26
|
+
from .stress import run_stress_tests
|
|
27
|
+
from .report import print_report
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run(tickers: list, investment: float = 10_000):
|
|
31
|
+
"""
|
|
32
|
+
Run the complete portfolio risk analysis pipeline and print a report.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
tickers : list of ticker symbols, e.g. ['AAPL', 'JPM', 'XOM', 'GLD']
|
|
37
|
+
investment : total investment amount in USD (default $10,000)
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
dict with all computed results
|
|
42
|
+
|
|
43
|
+
Example
|
|
44
|
+
-------
|
|
45
|
+
>>> import portfoliorisk as pr
|
|
46
|
+
>>> pr.run(['AAPL', 'NVDA', 'JPM', 'XOM', 'GLD', 'MSFT', 'AMZN'], investment=50000)
|
|
47
|
+
"""
|
|
48
|
+
# ── Validate inputs ────────────────────────────────────────────────────
|
|
49
|
+
if not isinstance(tickers, list) or len(tickers) < 2:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
"Please provide at least 2 ticker symbols as a list.\n"
|
|
52
|
+
" Example: pr.run(['AAPL', 'TSLA', 'GLD'], investment=10000)"
|
|
53
|
+
)
|
|
54
|
+
if investment <= 0:
|
|
55
|
+
raise ValueError("Investment amount must be a positive number.")
|
|
56
|
+
|
|
57
|
+
tickers = [t.upper().strip() for t in tickers]
|
|
58
|
+
|
|
59
|
+
# ── Fixed internal settings ────────────────────────────────────────────
|
|
60
|
+
PERIOD = "5y"
|
|
61
|
+
NUM_SIMULATIONS = 10_000
|
|
62
|
+
TIME_HORIZON = 252
|
|
63
|
+
CONFIDENCE = 0.95
|
|
64
|
+
T_DF = 3
|
|
65
|
+
RISK_FREE_RATE = 0.02
|
|
66
|
+
RANDOM_SEED = 42
|
|
67
|
+
STRESS_SCENARIOS = {
|
|
68
|
+
"2008 Financial Crisis (-35%)": -0.35,
|
|
69
|
+
"Tech Sector Meltdown (-20%)": -0.20,
|
|
70
|
+
"Interest Rate Hike (-10%)": -0.10,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
np.random.seed(RANDOM_SEED)
|
|
74
|
+
|
|
75
|
+
# ── 1. Fetch data ──────────────────────────────────────────────────────
|
|
76
|
+
print(" [1/7] Downloading market data...")
|
|
77
|
+
data, log_returns = fetch_data(tickers, PERIOD)
|
|
78
|
+
|
|
79
|
+
# ── 2. Smart weight optimisation (Efficient Frontier) ─────────────────
|
|
80
|
+
print(" [2/7] Optimising portfolio weights (Efficient Frontier)...")
|
|
81
|
+
weights, perf = optimize_portfolio(data, RISK_FREE_RATE)
|
|
82
|
+
|
|
83
|
+
# ── 3. Portfolio daily return series ──────────────────────────────────
|
|
84
|
+
w_arr = np.array([weights[t] for t in tickers])
|
|
85
|
+
port_returns = log_returns.dot(w_arr)
|
|
86
|
+
port_mean = port_returns.mean()
|
|
87
|
+
port_std = port_returns.std()
|
|
88
|
+
|
|
89
|
+
# ── 4. Monte Carlo — Normal ────────────────────────────────────────────
|
|
90
|
+
print(" [3/7] Running Monte Carlo simulations (Normal)...")
|
|
91
|
+
paths_normal = run_monte_carlo(port_mean, port_std, investment,
|
|
92
|
+
NUM_SIMULATIONS, TIME_HORIZON, dist="normal")
|
|
93
|
+
var_n, cvar_n = compute_var_cvar(paths_normal[-1], investment, CONFIDENCE)
|
|
94
|
+
|
|
95
|
+
# ── 5. Monte Carlo — Fat Tail ──────────────────────────────────────────
|
|
96
|
+
print(" [4/7] Running Monte Carlo simulations (Fat-Tail)...")
|
|
97
|
+
paths_fat = run_monte_carlo(port_mean, port_std, investment,
|
|
98
|
+
NUM_SIMULATIONS, TIME_HORIZON, dist="t", df=T_DF)
|
|
99
|
+
var_f, cvar_f = compute_var_cvar(paths_fat[-1], investment, CONFIDENCE)
|
|
100
|
+
|
|
101
|
+
# ── 6. Max Drawdown ────────────────────────────────────────────────────
|
|
102
|
+
mdd = compute_max_drawdown(port_returns)
|
|
103
|
+
|
|
104
|
+
# ── 7. GARCH — fit and get conditional vol series ──────────────────────
|
|
105
|
+
print(" [5/7] Fitting GARCH(1,1) volatility model...")
|
|
106
|
+
garch_res = fit_garch(port_returns)
|
|
107
|
+
|
|
108
|
+
# ── 8. LSTM Neural Network forecast ───────────────────────────────────
|
|
109
|
+
print(" [6/7] Training LSTM neural network for volatility forecast...")
|
|
110
|
+
lstm_res = {"lstm_available": False, "note": "GARCH unavailable", "forecast_vols": []}
|
|
111
|
+
if garch_res.get("available") and len(garch_res.get("conditional_vol_series", [])) > 0:
|
|
112
|
+
lstm_res = forecast_with_lstm(
|
|
113
|
+
garch_conditional_vol = garch_res["conditional_vol_series"],
|
|
114
|
+
port_returns = port_returns.values,
|
|
115
|
+
horizon = 7,
|
|
116
|
+
lookback = 20,
|
|
117
|
+
epochs = 50,
|
|
118
|
+
batch_size = 32,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# ── 9. Stress Tests ────────────────────────────────────────────────────
|
|
122
|
+
print(" [7/7] Running stress tests...")
|
|
123
|
+
stress_res = run_stress_tests(investment, STRESS_SCENARIOS)
|
|
124
|
+
|
|
125
|
+
# ── 10. Pack results ───────────────────────────────────────────────────
|
|
126
|
+
results = {
|
|
127
|
+
"tickers": tickers,
|
|
128
|
+
"period": PERIOD,
|
|
129
|
+
"observations": len(data),
|
|
130
|
+
"investment": investment,
|
|
131
|
+
"weights": weights,
|
|
132
|
+
"weight_method": "Efficient Frontier (Maximum Sharpe Ratio)",
|
|
133
|
+
"expected_return": perf[0],
|
|
134
|
+
"annual_volatility": perf[1],
|
|
135
|
+
"sharpe_ratio": perf[2],
|
|
136
|
+
"var_normal": var_n,
|
|
137
|
+
"cvar_normal": cvar_n,
|
|
138
|
+
"var_fattail": var_f,
|
|
139
|
+
"cvar_fattail": cvar_f,
|
|
140
|
+
"max_drawdown": mdd,
|
|
141
|
+
"garch": garch_res,
|
|
142
|
+
"lstm": lstm_res,
|
|
143
|
+
"stress": stress_res,
|
|
144
|
+
"confidence": CONFIDENCE,
|
|
145
|
+
"t_df": T_DF,
|
|
146
|
+
"num_simulations": NUM_SIMULATIONS,
|
|
147
|
+
"time_horizon": TIME_HORIZON,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# ── 11. Print report ───────────────────────────────────────────────────
|
|
151
|
+
print_report(results)
|
|
152
|
+
return results
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
portfoliorisk.report
|
|
3
|
+
====================
|
|
4
|
+
Terminal report printer — includes smart weights and LSTM forecast section.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class C:
|
|
11
|
+
RESET = "\033[0m"; BOLD = "\033[1m"; DIM = "\033[2m"
|
|
12
|
+
BLUE = "\033[34m"; CYAN = "\033[36m"; GREEN = "\033[32m"
|
|
13
|
+
YELLOW = "\033[33m"; RED = "\033[31m"; WHITE = "\033[97m"
|
|
14
|
+
BG_BLUE = "\033[44m"; BG_DARK= "\033[100m"; BG_GREEN="\033[42m"
|
|
15
|
+
BG_RED = "\033[41m"
|
|
16
|
+
|
|
17
|
+
W = 72
|
|
18
|
+
|
|
19
|
+
def _line(char="─", color=C.DIM): print(f"{color}{char*W}{C.RESET}")
|
|
20
|
+
def _header(text, bg=C.BG_BLUE):
|
|
21
|
+
pad = W - len(text) - 2; l, r = pad//2, pad - pad//2
|
|
22
|
+
print(f"{bg}{C.WHITE}{C.BOLD} {'─'*l} {text} {'─'*r} {C.RESET}")
|
|
23
|
+
def _section(text):
|
|
24
|
+
print(); print(f"{C.CYAN}{C.BOLD} ▌ {text}{C.RESET}"); _line()
|
|
25
|
+
def _row(label, value, color=C.WHITE, indent=4):
|
|
26
|
+
dots = "." * (W - indent - len(label) - len(str(value)) - 2)
|
|
27
|
+
print(f"{' '*indent}{C.DIM}{label} {dots}{C.RESET} {color}{C.BOLD}{value}{C.RESET}")
|
|
28
|
+
def _bullet(label, value, good=True):
|
|
29
|
+
icon = f"{C.GREEN}●{C.RESET}" if good else f"{C.RED}●{C.RESET}"
|
|
30
|
+
print(f" {icon} {C.DIM}{label}:{C.RESET} {C.WHITE}{C.BOLD}{value}{C.RESET}")
|
|
31
|
+
def _verdict(positive, good_msg, bad_msg):
|
|
32
|
+
print()
|
|
33
|
+
if positive:
|
|
34
|
+
print(f" {C.BG_GREEN}{C.WHITE}{C.BOLD} VERDICT : {good_msg} {C.RESET}")
|
|
35
|
+
else:
|
|
36
|
+
print(f" {C.BG_RED}{C.WHITE}{C.BOLD} VERDICT : {bad_msg} {C.RESET}")
|
|
37
|
+
print()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def print_report(r: dict):
|
|
41
|
+
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
42
|
+
inv = r["investment"]
|
|
43
|
+
conf_pct = int(r["confidence"] * 100)
|
|
44
|
+
g = r["garch"]
|
|
45
|
+
lstm = r.get("lstm", {})
|
|
46
|
+
|
|
47
|
+
print()
|
|
48
|
+
_header(" PORTFOLIORISK · FULL RISK ANALYSIS REPORT ", C.BG_BLUE)
|
|
49
|
+
print(f" {C.DIM}Generated : {now} | Tickers : {', '.join(r['tickers'])}"
|
|
50
|
+
f" | Period : {r['period']} | Obs : {r['observations']:,}{C.RESET}")
|
|
51
|
+
_line("=", C.BLUE)
|
|
52
|
+
|
|
53
|
+
# ── 1. Portfolio Composition ───────────────────────────────────────────
|
|
54
|
+
_section("1 PORTFOLIO COMPOSITION (Smart Allocation — Efficient Frontier)")
|
|
55
|
+
print(f" {C.DIM}Method: {C.WHITE}{C.BOLD}{r['weight_method']}{C.RESET}")
|
|
56
|
+
print(f" {C.DIM}Each stock's allocation is determined by its historical return,{C.RESET}")
|
|
57
|
+
print(f" {C.DIM}volatility, and correlation — not split equally.{C.RESET}")
|
|
58
|
+
print()
|
|
59
|
+
for ticker, w in r["weights"].items():
|
|
60
|
+
if w < 0.001:
|
|
61
|
+
# Zero-weight assets shown dimly
|
|
62
|
+
print(f" {C.DIM}{ticker:6s} {'░'*40} 0.00% (optimizer excluded){C.RESET}")
|
|
63
|
+
continue
|
|
64
|
+
bar_len = int(w * 40)
|
|
65
|
+
bar = "█" * bar_len + "░" * (40 - bar_len)
|
|
66
|
+
alloc = inv * w
|
|
67
|
+
print(f" {C.CYAN}{ticker:6s}{C.RESET} {C.BLUE}{bar}{C.RESET} "
|
|
68
|
+
f"{C.WHITE}{C.BOLD}{w*100:5.2f}%{C.RESET} {C.DIM}${alloc:,.2f}{C.RESET}")
|
|
69
|
+
|
|
70
|
+
active = {t: w for t, w in r["weights"].items() if w >= 0.001}
|
|
71
|
+
_verdict(True,
|
|
72
|
+
f"POSITIVE - Optimizer allocated capital across {len(active)} stocks based on data-driven risk-return analysis.",
|
|
73
|
+
"NEGATIVE - Allocation failed.")
|
|
74
|
+
|
|
75
|
+
# ── 2. Optimisation Performance ───────────────────────────────────────
|
|
76
|
+
_section("2 PORTFOLIO PERFORMANCE (Maximum Sharpe Ratio)")
|
|
77
|
+
_row("Expected Annual Return", f"{r['expected_return']*100:.2f}%", C.GREEN)
|
|
78
|
+
_row("Annual Volatility", f"{r['annual_volatility']*100:.2f}%", C.YELLOW)
|
|
79
|
+
_row("Sharpe Ratio", f"{r['sharpe_ratio']:.4f}", C.GREEN)
|
|
80
|
+
print(f"\n {C.DIM}A Sharpe Ratio above 1.0 means the portfolio earns more return{C.RESET}")
|
|
81
|
+
print(f" {C.DIM}per unit of risk than a typical market benchmark (S&P 500 ≈ 0.5).{C.RESET}")
|
|
82
|
+
sharpe_good = r["sharpe_ratio"] > 1.0
|
|
83
|
+
_verdict(sharpe_good,
|
|
84
|
+
f"POSITIVE - Sharpe Ratio {r['sharpe_ratio']:.2f} is excellent. Data-driven allocation outperforms equal weighting.",
|
|
85
|
+
f"NEGATIVE - Sharpe Ratio {r['sharpe_ratio']:.2f} < 1.0. Poor risk-adjusted return. Reconsider stock selection.")
|
|
86
|
+
|
|
87
|
+
# ── 3. Monte Carlo ─────────────────────────────────────────────────────
|
|
88
|
+
_section(f"3 MONTE CARLO SIMULATION ({r['num_simulations']:,} paths, {r['time_horizon']} days)")
|
|
89
|
+
print(f" {C.DIM}Investment : {C.WHITE}{C.BOLD}${inv:,.2f}{C.RESET}\n")
|
|
90
|
+
print(f" {C.BOLD}-- Normal Distribution --{C.RESET}")
|
|
91
|
+
_row(f" {conf_pct}% VaR", f"${r['var_normal']:,.2f}", C.YELLOW)
|
|
92
|
+
_row(f" {conf_pct}% CVaR", f"${r['cvar_normal']:,.2f}", C.YELLOW)
|
|
93
|
+
pct_n = r["var_normal"] / inv * 100
|
|
94
|
+
print(f"\n {C.DIM}Normal model: max expected 1-year loss = {C.RESET}{C.YELLOW}{C.BOLD}{pct_n:.1f}%{C.RESET}")
|
|
95
|
+
print()
|
|
96
|
+
print(f" {C.BOLD}-- Student-t (Fat-Tail, df={r['t_df']}) --{C.RESET}")
|
|
97
|
+
_row(f" {conf_pct}% VaR", f"${r['var_fattail']:,.2f}", C.RED)
|
|
98
|
+
_row(f" {conf_pct}% CVaR", f"${r['cvar_fattail']:,.2f}", C.RED)
|
|
99
|
+
pct_f = r["var_fattail"] / inv * 100
|
|
100
|
+
mult = r["var_fattail"] / r["var_normal"] if r["var_normal"] else 0
|
|
101
|
+
print(f"\n {C.DIM}Fat-tail model: realistic crash loss = {C.RESET}{C.RED}{C.BOLD}{pct_f:.1f}%{C.RESET}"
|
|
102
|
+
f"{C.DIM} ({mult:.1f}x larger than Normal){C.RESET}")
|
|
103
|
+
mc_good = pct_f < 25
|
|
104
|
+
_verdict(mc_good,
|
|
105
|
+
f"POSITIVE - Fat-tail downside {pct_f:.1f}% is within acceptable range.",
|
|
106
|
+
f"NEGATIVE - Fat-tail downside {pct_f:.1f}% is HIGH. Add defensive assets (GLD, bonds) to hedge.")
|
|
107
|
+
|
|
108
|
+
# ── 4. Max Drawdown ────────────────────────────────────────────────────
|
|
109
|
+
_section("4 HISTORICAL MAXIMUM DRAWDOWN")
|
|
110
|
+
mdd_pct = r["max_drawdown"] * 100
|
|
111
|
+
mdd_col = C.RED if mdd_pct < -25 else C.YELLOW
|
|
112
|
+
_row("Max Historical Drawdown", f"{mdd_pct:.2f}%", mdd_col)
|
|
113
|
+
print(f"\n {C.DIM}At worst, portfolio fell {C.RESET}{mdd_col}{C.BOLD}{abs(mdd_pct):.2f}%{C.RESET}"
|
|
114
|
+
f"{C.DIM} from its peak before recovering.{C.RESET}")
|
|
115
|
+
mdd_good = mdd_pct > -25
|
|
116
|
+
_verdict(mdd_good,
|
|
117
|
+
f"POSITIVE - Drawdown of {mdd_pct:.2f}% is manageable. Portfolio shows resilience.",
|
|
118
|
+
f"NEGATIVE - Drawdown of {mdd_pct:.2f}% is severe. Consider lower-volatility assets.")
|
|
119
|
+
|
|
120
|
+
# ── 5. GARCH ───────────────────────────────────────────────────────────
|
|
121
|
+
_section("5 GARCH(1,1) VOLATILITY MODEL (Student-t errors)")
|
|
122
|
+
if not g.get("available"):
|
|
123
|
+
print(f" {C.YELLOW}{g.get('note','GARCH unavailable')}{C.RESET}")
|
|
124
|
+
else:
|
|
125
|
+
persist = g["alpha"] + g["beta"]
|
|
126
|
+
_row("omega (base variance)", f"{g['omega']:.6f}")
|
|
127
|
+
_row("alpha (ARCH shock term)", f"{g['alpha']:.4f}")
|
|
128
|
+
_row("beta (GARCH persistence)", f"{g['beta']:.4f}")
|
|
129
|
+
_row("alpha + beta", f"{persist:.4f}",
|
|
130
|
+
C.GREEN if persist < 0.95 else C.RED)
|
|
131
|
+
_row("nu (degrees of freedom)", f"{g['nu']:.4f}")
|
|
132
|
+
_row("Current Volatility (annual)", f"{g['current_vol_annualised']:.2f}%", C.YELLOW)
|
|
133
|
+
_row("AIC / BIC", f"{g['aic']:.2f} / {g['bic']:.2f}")
|
|
134
|
+
print(f"\n {C.DIM}GARCH 7-Day Forecast (scaled units):{C.RESET}")
|
|
135
|
+
for i, v in enumerate(g["forecast_vols"], 1):
|
|
136
|
+
bar = "|" * int(v * 8)
|
|
137
|
+
trend = "↗" if i == 1 else ("↘" if v < g["forecast_vols"][i-2] else "→")
|
|
138
|
+
print(f" Day {i}: {C.BLUE}{bar:<20}{C.RESET} {v:.5f} {trend}")
|
|
139
|
+
garch_good = g["current_vol_annualised"] < 20 and persist < 0.97
|
|
140
|
+
_verdict(garch_good,
|
|
141
|
+
f"POSITIVE - Volatility {g['current_vol_annualised']:.1f}% stable. Normal market conditions.",
|
|
142
|
+
f"NEGATIVE - Volatility ELEVATED at {g['current_vol_annualised']:.1f}%. High persistence ({persist:.4f}). Consider hedging.")
|
|
143
|
+
|
|
144
|
+
# ── 6. LSTM Neural Network ─────────────────────────────────────────────
|
|
145
|
+
_section("6 LSTM NEURAL NETWORK (Deep Learning Volatility Forecast)")
|
|
146
|
+
if not lstm.get("lstm_available"):
|
|
147
|
+
print(f" {C.YELLOW}LSTM: {lstm.get('note', 'Unavailable')}{C.RESET}")
|
|
148
|
+
print(f" {C.DIM}Install TensorFlow to enable: pip install tensorflow{C.RESET}")
|
|
149
|
+
else:
|
|
150
|
+
print(f" {C.DIM}Architecture: {lstm.get('model_summary','')}{C.RESET}")
|
|
151
|
+
print(f" {C.DIM}Trained for {lstm.get('epochs_run', 0)} epochs | "
|
|
152
|
+
f"Final MSE Loss: {lstm.get('train_loss', 0):.6f}{C.RESET}")
|
|
153
|
+
print(f"\n {C.DIM}Note: {lstm.get('note','')}{C.RESET}")
|
|
154
|
+
print()
|
|
155
|
+
print(f" {C.BOLD}7-Day Annualised Volatility Forecast (LSTM):{C.RESET}")
|
|
156
|
+
fvols = lstm.get("forecast_vols", [])
|
|
157
|
+
if fvols:
|
|
158
|
+
for i, v in enumerate(fvols, 1):
|
|
159
|
+
bar = "█" * int(v / 3)
|
|
160
|
+
trend = "↗" if i == 1 else ("↘" if v < fvols[i-2] else "→")
|
|
161
|
+
color = C.RED if v > 22 else (C.YELLOW if v > 17 else C.GREEN)
|
|
162
|
+
print(f" Day {i}: {color}{bar:<20}{C.RESET} {color}{v:.2f}%{C.RESET} {trend}")
|
|
163
|
+
|
|
164
|
+
avg_lstm = sum(fvols) / len(fvols)
|
|
165
|
+
garch_now = g.get("current_vol_annualised", 0) if g.get("available") else 0
|
|
166
|
+
direction = "RISING" if avg_lstm > garch_now else "FALLING"
|
|
167
|
+
dir_color = C.RED if direction == "RISING" else C.GREEN
|
|
168
|
+
print(f"\n {C.DIM}GARCH current vol: {garch_now:.2f}% → "
|
|
169
|
+
f"LSTM avg forecast: {C.RESET}{dir_color}{C.BOLD}{avg_lstm:.2f}% ({direction}){C.RESET}")
|
|
170
|
+
lstm_good = avg_lstm < 22
|
|
171
|
+
_verdict(lstm_good,
|
|
172
|
+
f"POSITIVE - LSTM forecasts stable volatility (~{avg_lstm:.1f}% avg). No spike expected in next 7 days.",
|
|
173
|
+
f"NEGATIVE - LSTM forecasts ELEVATED volatility (~{avg_lstm:.1f}% avg). Risk is expected to remain high or rise.")
|
|
174
|
+
|
|
175
|
+
# ── 7. Stress Tests ────────────────────────────────────────────────────
|
|
176
|
+
_section("7 STRESS TEST RESULTS")
|
|
177
|
+
print(f" {C.DIM}Investment : {C.WHITE}{C.BOLD}${inv:,.2f}{C.RESET}\n")
|
|
178
|
+
all_surv = True
|
|
179
|
+
for name, s in r["stress"].items():
|
|
180
|
+
severity = C.RED if abs(s["shock"]) >= 0.25 else (C.YELLOW if abs(s["shock"]) >= 0.15 else C.GREEN)
|
|
181
|
+
survivable= s["post_value"] > inv * 0.5
|
|
182
|
+
if not survivable: all_surv = False
|
|
183
|
+
icon = "[+] SURVIVABLE" if survivable else "[-] CRITICAL"
|
|
184
|
+
print(f" {severity}{C.BOLD}{name}{C.RESET}")
|
|
185
|
+
print(f" Shock {s['shock']*100:+.0f}% → Remaining: {C.WHITE}{C.BOLD}${s['post_value']:,.0f}{C.RESET}"
|
|
186
|
+
f" Loss: {severity}-${s['loss']:,.0f}{C.RESET} "
|
|
187
|
+
f"{C.GREEN if survivable else C.RED}{icon}{C.RESET}")
|
|
188
|
+
print()
|
|
189
|
+
_verdict(all_surv,
|
|
190
|
+
"POSITIVE - Portfolio survives ALL stress scenarios with >50% capital intact.",
|
|
191
|
+
"NEGATIVE - Portfolio FAILS at least one scenario. Critical capital loss risk.")
|
|
192
|
+
|
|
193
|
+
# ── Executive Summary ──────────────────────────────────────────────────
|
|
194
|
+
_line("=", C.BLUE)
|
|
195
|
+
_header(" EXECUTIVE SUMMARY ", C.BG_DARK)
|
|
196
|
+
print()
|
|
197
|
+
_bullet("Weight Method", r["weight_method"], good=True)
|
|
198
|
+
_bullet("Expected Annual Return",f"{r['expected_return']*100:.2f}%", good=r["expected_return"]>0.10)
|
|
199
|
+
_bullet("Sharpe Ratio", f"{r['sharpe_ratio']:.2f} (>1.0 = excellent)", good=r["sharpe_ratio"]>1)
|
|
200
|
+
_bullet(f"{conf_pct}% VaR (Fat-Tail)",f"${r['var_fattail']:,.2f} ({pct_f:.1f}% of portfolio)", good=pct_f<25)
|
|
201
|
+
_bullet("CVaR (Worst-Case)", f"${r['cvar_fattail']:,.2f}", good=False)
|
|
202
|
+
_bullet("Max Drawdown", f"{r['max_drawdown']*100:.2f}%", good=r["max_drawdown"]>-0.25)
|
|
203
|
+
if g.get("available"):
|
|
204
|
+
persist = g["alpha"] + g["beta"]
|
|
205
|
+
_bullet("GARCH Volatility", f"{g['current_vol_annualised']:.2f}% (annualised)", good=g["current_vol_annualised"]<20)
|
|
206
|
+
_bullet("Volatility Persist",f"{persist:.4f} ({'normal' if persist<0.97 else 'very slow decay'})", good=persist<0.97)
|
|
207
|
+
if lstm.get("lstm_available") and lstm.get("forecast_vols"):
|
|
208
|
+
avg_lstm = sum(lstm["forecast_vols"]) / len(lstm["forecast_vols"])
|
|
209
|
+
_bullet("LSTM 7-Day Avg Forecast", f"{avg_lstm:.2f}% annualised vol", good=avg_lstm<22)
|
|
210
|
+
_bullet("Stress Tests", "All survivable" if all_surv else "FAILED", good=all_surv)
|
|
211
|
+
|
|
212
|
+
overall = sum([
|
|
213
|
+
r["sharpe_ratio"] > 1.0,
|
|
214
|
+
pct_f < 25,
|
|
215
|
+
r["max_drawdown"] > -0.25,
|
|
216
|
+
g.get("current_vol_annualised", 99) < 20,
|
|
217
|
+
all_surv,
|
|
218
|
+
])
|
|
219
|
+
print()
|
|
220
|
+
if overall >= 4:
|
|
221
|
+
print(f" {C.BG_GREEN}{C.WHITE}{C.BOLD} [+] OVERALL : STRONG PORTFOLIO -- {overall}/5 checks passed {C.RESET}")
|
|
222
|
+
elif overall >= 3:
|
|
223
|
+
print(f" {C.BG_BLUE}{C.WHITE}{C.BOLD} [~] OVERALL : MODERATE PORTFOLIO -- {overall}/5 checks passed -- review flagged areas {C.RESET}")
|
|
224
|
+
else:
|
|
225
|
+
print(f" {C.BG_RED}{C.WHITE}{C.BOLD} [-] OVERALL : HIGH RISK PORTFOLIO -- {overall}/5 checks passed -- consider rebalancing {C.RESET}")
|
|
226
|
+
|
|
227
|
+
print()
|
|
228
|
+
_line("=", C.BLUE)
|
|
229
|
+
print(f" {C.DIM}portfoliorisk | pypi.org/project/portfoliorisk | {now}{C.RESET}")
|
|
230
|
+
print()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
portfoliorisk.risk
|
|
3
|
+
==================
|
|
4
|
+
VaR, CVaR, and Maximum Drawdown calculations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def compute_var_cvar(terminal_values, initial_investment, confidence=0.95):
|
|
12
|
+
alpha = 1.0 - confidence
|
|
13
|
+
cutoff = np.percentile(terminal_values, alpha * 100)
|
|
14
|
+
tail = terminal_values[terminal_values <= cutoff]
|
|
15
|
+
return initial_investment - cutoff, initial_investment - tail.mean()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def compute_max_drawdown(port_returns: pd.Series) -> float:
|
|
19
|
+
cumulative = (1 + port_returns).cumprod()
|
|
20
|
+
running_max = cumulative.cummax()
|
|
21
|
+
drawdown = (cumulative - running_max) / running_max
|
|
22
|
+
return float(drawdown.min())
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
portfoliorisk.stress
|
|
3
|
+
====================
|
|
4
|
+
Scenario-based stress testing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_stress_tests(initial_investment: float, scenarios: dict) -> dict:
|
|
9
|
+
results = {}
|
|
10
|
+
for name, shock in scenarios.items():
|
|
11
|
+
post_value = initial_investment * (1 + shock)
|
|
12
|
+
results[name] = {
|
|
13
|
+
"shock": shock,
|
|
14
|
+
"post_value": post_value,
|
|
15
|
+
"loss": initial_investment - post_value,
|
|
16
|
+
}
|
|
17
|
+
return results
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: portfolioriskpro
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: Smart portfolio risk analysis with Efficient Frontier and LSTM forecasting
|
|
5
|
+
Author: Yash
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Requires-Dist: PyPortfolioOpt>=1.5.0
|
|
8
|
+
Requires-Dist: arch>=5.0.0
|
|
9
|
+
Requires-Dist: numpy>=1.21.0
|
|
10
|
+
Requires-Dist: pandas>=1.3.0
|
|
11
|
+
Requires-Dist: scipy>=1.7.0
|
|
12
|
+
Requires-Dist: yfinance>=0.2.0
|
|
13
|
+
Provides-Extra: deep
|
|
14
|
+
Requires-Dist: tensorflow>=2.10.0; extra == "deep"
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: provides-extra
|
|
17
|
+
Dynamic: requires-dist
|
|
18
|
+
Dynamic: requires-python
|
|
19
|
+
Dynamic: summary
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
setup.py
|
|
2
|
+
portfoliorisk/__init__.py
|
|
3
|
+
portfoliorisk/data.py
|
|
4
|
+
portfoliorisk/garch.py
|
|
5
|
+
portfoliorisk/montecarlo.py
|
|
6
|
+
portfoliorisk/neural.py
|
|
7
|
+
portfoliorisk/optimize.py
|
|
8
|
+
portfoliorisk/pipeline.py
|
|
9
|
+
portfoliorisk/report.py
|
|
10
|
+
portfoliorisk/risk.py
|
|
11
|
+
portfoliorisk/stress.py
|
|
12
|
+
portfolioriskpro.egg-info/PKG-INFO
|
|
13
|
+
portfolioriskpro.egg-info/SOURCES.txt
|
|
14
|
+
portfolioriskpro.egg-info/dependency_links.txt
|
|
15
|
+
portfolioriskpro.egg-info/requires.txt
|
|
16
|
+
portfolioriskpro.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
portfoliorisk
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
from setuptools import setup, find_packages
|
|
3
|
+
|
|
4
|
+
setup(
|
|
5
|
+
name="portfolioriskpro",
|
|
6
|
+
version="2.1.0",
|
|
7
|
+
author="Yash",
|
|
8
|
+
description="Smart portfolio risk analysis with Efficient Frontier and LSTM forecasting",
|
|
9
|
+
packages=find_packages(),
|
|
10
|
+
install_requires=[
|
|
11
|
+
"PyPortfolioOpt>=1.5.0",
|
|
12
|
+
"arch>=5.0.0",
|
|
13
|
+
"numpy>=1.21.0",
|
|
14
|
+
"pandas>=1.3.0",
|
|
15
|
+
"scipy>=1.7.0",
|
|
16
|
+
"yfinance>=0.2.0",
|
|
17
|
+
],
|
|
18
|
+
extras_require={
|
|
19
|
+
"deep": ["tensorflow>=2.10.0"]
|
|
20
|
+
},
|
|
21
|
+
python_requires=">=3.8",
|
|
22
|
+
)
|