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.
@@ -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,9 @@
1
+ PyPortfolioOpt>=1.5.0
2
+ arch>=5.0.0
3
+ numpy>=1.21.0
4
+ pandas>=1.3.0
5
+ scipy>=1.7.0
6
+ yfinance>=0.2.0
7
+
8
+ [deep]
9
+ tensorflow>=2.10.0
@@ -0,0 +1 @@
1
+ portfoliorisk
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )