Python Financial Analysis: Portfolio Performance & Risk (2015–2024)
Time series analysis, risk metrics, and predictive modeling across six diversified assets using 10 years of daily market data retrieved via the yfinance API.
Tools: Python · pandas · NumPy · matplotlib · seaborn · statsmodels · scikit-learn · yfinance · Jupyter Notebook
Project Overview
Overview
This project analyzes 10 years of daily financial market data (2015–2024) for a diversified portfolio of six assets to evaluate historical performance, quantify risk, identify diversification opportunities, and generate short-term forecasts. The analysis simulates the role of a data analyst supporting portfolio management and investment decision-making with data-driven insights.
Business Context
Understanding portfolio risk and return characteristics is essential for informed investment decisions. This project demonstrates how Python and statistical methods can be applied to real market data to move beyond intuition and toward evidence-based portfolio evaluation — covering volatility measurement, drawdown analysis, correlation dynamics, and predictive modeling.
Objectives
- Retrieve and validate 10 years of daily market data via the yfinance API
- Compute risk-adjusted performance metrics (Sharpe ratio, maximum drawdown, annualized volatility)
- Analyze cross-asset correlations and quantify diversification benefits
- Decompose time series into trend, seasonality, and residual components
- Build and evaluate an ARIMA baseline forecast for the benchmark index
- Produce publication-quality visualizations for each stage of analysis
Skills Demonstrated
- Data Engineering: API integration, data cleaning, feature engineering with pandas
- Exploratory Data Analysis: Descriptive statistics, distribution analysis, trend identification
- Risk Analytics: Volatility, Sharpe ratio, maximum drawdown, Value at Risk (VaR), correlation analysis
- Time Series Analysis: Decomposition, stationarity testing, rolling statistics, regime detection
- Predictive Modeling: ARIMA/SARIMA forecasting, machine learning regression
- Data Visualization: Publication-quality charts using matplotlib and seaborn
- Statistical Methods: Hypothesis testing, distribution fitting, statistical significance
Dataset
Assets Analyzed
| Ticker | Name | Asset Class | Purpose |
|---|---|---|---|
| SPY | S&P 500 ETF | US Large Cap Stocks | Market Benchmark |
| AAPL | Apple Inc. | Technology Stock | Growth |
| XLE | Energy Select Sector ETF | Energy Sector | Cyclical |
| TLT | 20+ Year Treasury Bond ETF | Bonds | Safe Haven |
| GLD | SPDR Gold Trust | Commodities | Inflation Hedge |
| EFA | MSCI EAFE ETF | International Stocks | Diversification |
Data Source
All price data is retrieved programmatically via the yfinance API — no static data files are included in this repository. Daily OHLCV (Open, High, Low, Close, Volume) data is pulled for the period January 2015 through December 2024.
Time Period
| Attribute | Value |
|---|---|
| Start | January 1, 2015 |
| End | December 31, 2024 |
| Granularity | Daily trading data (~2,500 trading days per asset) |
| Total Observations | ~15,000 daily records across 6 assets |
Rationale for Asset Selection
These six assets were chosen to represent a diversified portfolio spanning domestic equities (SPY), individual growth stocks (AAPL), sector exposure (XLE), fixed income (TLT), commodities (GLD), and international markets (EFA). This mix enables meaningful analysis of cross-asset correlations, diversification benefits, and risk-return tradeoffs across different market environments.
Analysis 1 — Data Acquisition & Exploratory Data Analysis
Business Question
What are the fundamental characteristics of each asset's price history over the 2015–2024 period? How do the assets compare in terms of price levels, growth trajectories, and basic statistical properties, and what preliminary relationships exist between them?
Methodology
Using the yfinance Python library, I programmatically downloaded 10 years of daily price data (January 2015 through December 2024) for six diversified assets: SPY (S&P 500), AAPL (Apple), XLE (Energy), TLT (Bonds), GLD (Gold), and EFA (International Equities). This yielded 2,515 trading days of data per asset.
After retrieval, I validated the dataset for completeness and quality. The data contained zero missing values across all six tickers — a 100% complete dataset requiring no imputation or interpolation. I then computed descriptive statistics to understand the central tendency, dispersion, and shape of each asset's price distribution over the full period.
Finally, I generated two key exploratory visualizations: a normalized price history chart to compare growth trajectories across assets with different price scales, and a correlation heatmap of daily returns to identify preliminary relationships and potential diversification opportunities within the portfolio.
Code Highlight
# Define portfolio assets
tickers = ['SPY', 'AAPL', 'XLE', 'TLT', 'GLD', 'EFA']
# Download data via Yahoo Finance API
data = yf.download(
tickers=tickers,
start='2015-01-01',
end='2024-12-31',
auto_adjust=True
)
# Extract closing prices
prices = data['Close']
print(f"Downloaded {len(prices)} days of data")
# Normalize to base 100 for cross-asset comparison
normalized = (prices / prices.iloc[0]) * 100
Results
Insights
- Massive performance divergence: AAPL gained approximately 935% over the 10-year period, far outpacing SPY at 240%, while TLT (long-term bonds) lost roughly 11% — highlighting the enormous spread in outcomes across asset classes.
- 100% data completeness: All 2,515 trading days across all six tickers contained valid price data with zero missing values, confirming the reliability of the yfinance API for historical financial data retrieval.
- Bonds as a diversifier: TLT showed negative correlation with equity assets (approximately −0.35 vs. SPY), confirming its role as a portfolio diversification tool that tends to move inversely to stocks during market stress.
- Equity clustering: SPY, AAPL, XLE, and EFA exhibited moderate-to-strong positive correlations (0.50–0.80), indicating that equity assets across sectors and geographies share common risk factors.
- Gold’s independence: GLD displayed low correlation with both equities (~0.05) and bonds (~0.10), providing a distinct return driver relatively uncoupled from traditional asset class movements.
Business Recommendations
- Normalize for comparison: always use base-100 indexing when comparing assets with different price levels to avoid misleading visual interpretation.
- Use correlations as a starting point: preliminary correlations guide portfolio construction, but should be confirmed with rolling windows (see Analysis 3).
- Validate before analyzing: confirming data completeness and quality before downstream analysis prevents compounding errors through the pipeline.
Analysis 2 — Returns & Risk Analysis
Business Question
Which assets delivered the best risk-adjusted returns over 2015–2024, and where were the largest downside episodes that could challenge portfolio durability?
Methodology
Daily percentage returns were computed from adjusted close prices so every asset could be compared on a common scale. I then annualized return and volatility (using the √252 convention), applied a 2% risk-free assumption for Sharpe ratio calculation, and ranked assets by risk-adjusted efficiency.
To measure downside risk, I calculated maximum drawdown from each asset’s cumulative return path. This complements volatility by showing the worst peak-to-trough loss and highlights stress behavior during sharp market shocks (e.g., the 2020 COVID crash and the 2022 rate-hike selloff).
Code Highlight
import numpy as np
import pandas as pd
# Daily returns
daily_returns = prices.pct_change().dropna()
def max_drawdown(return_series: pd.Series) -> float:
"""Max drawdown from cumulative returns."""
cum = (1 + return_series).cumprod()
peak = cum.cummax()
dd = (cum / peak) - 1
return dd.min()
def risk_metrics(returns: pd.DataFrame, rf: float = 0.02) -> pd.DataFrame:
"""Annualized return, volatility, Sharpe, max drawdown."""
ann_return = (1 + returns.mean())**252 - 1
ann_vol = returns.std() * np.sqrt(252)
sharpe = (ann_return - rf) / ann_vol
mdd = returns.apply(max_drawdown)
metrics = pd.DataFrame({
"Annual Return": ann_return,
"Annual Volatility": ann_vol,
"Sharpe (rf=2%)": sharpe,
"Max Drawdown": mdd
})
return metrics.sort_values("Sharpe (rf=2%)", ascending=False)
metrics_df = risk_metrics(daily_returns, rf=0.02)
Results
Risk Metrics Summary
| Asset | Annual Return | Annual Volatility | Sharpe Ratio | Max Drawdown |
|---|---|---|---|---|
| AAPL | ~26% | ~30% | ~0.80 | ~−39% |
| SPY | ~13% | ~18% | ~0.61 | ~−34% |
| GLD | ~8% | ~15% | ~0.40 | ~−21% |
| EFA | ~6% | ~17% | ~0.24 | ~−35% |
| XLE | ~4% | ~30% | ~0.07 | ~−62% |
| TLT | ~−1% | ~16% | ~−0.19 | ~−48% |
Values are approximate annualized figures over the full 2015–2024 period. Sharpe ratio assumes a 2% risk-free rate.
Insights
- AAPL led on both return and Sharpe: ~26% annualized return with a Sharpe of ~0.80, but carried ~30% volatility and a ~39% max drawdown — high reward came with substantial interim risk.
- XLE: high volatility, low reward: ~30% annualized volatility (matching AAPL) but only ~4% return and a devastating ~62% max drawdown, making it the worst risk-adjusted performer.
- TLT lost money over the decade: long-duration Treasuries posted a negative annualized return (~−1%) with a ~48% drawdown during the 2022 rate cycle, challenging the traditional "safe haven" narrative.
- GLD was the most stable diversifier: lowest max drawdown (~21%) among all assets with a positive Sharpe ratio, reinforcing gold’s defensive role.
- Synchronized stress windows: drawdown paths for SPY, EFA, and XLE overlapped sharply during the March 2020 COVID crash, demonstrating correlated equity risk during crises.
- Risk metrics tell different stories: volatility alone underestimates tail risk — TLT had moderate volatility (~16%) but a severe drawdown (~48%), showing that Sharpe and drawdown should be evaluated together.
Business Recommendations
- Use Sharpe and drawdown together: screen and rebalance portfolio weights using risk-adjusted return alongside maximum drawdown tolerance.
- Set drawdown-based risk limits: define acceptable peak-to-trough loss thresholds, not only annual volatility targets.
- Stress-test allocations: evaluate how portfolio weights would have performed during the 2020 and 2022 stress episodes before implementing changes.
- Reassess periodically: risk metrics evolve as market regimes shift — a strong Sharpe in one decade does not guarantee the next.
Analysis 3 — Correlation & Diversification
Business Question
How stable are cross-asset correlations over time, and how much risk reduction can be achieved by combining assets with lower co-movement instead of concentrating in a single market exposure?
Methodology
I computed full-period pairwise correlations on daily returns, then layered 252-day (one trading year) rolling correlation windows to track how relationships shifted across calm and stress environments. This distinguishes long-run averages from regime-specific behavior that matters for real-time risk management.
I also quantified the diversification benefit by comparing the annualized volatility of a concentrated SPY-only portfolio against a diversified allocation (60% SPY, 30% TLT, 10% GLD) using covariance matrix math.
Code Highlight
import numpy as np
import pandas as pd
# Static correlation (full period)
corr = daily_returns.corr()
# Rolling 252-day correlation (regime sensitivity)
rolling_corr_spy_tlt = daily_returns["SPY"].rolling(252).corr(daily_returns["TLT"])
# Diversification math: portfolio volatility via covariance
cov = daily_returns.cov() * 252 # annualized covariance
w_concentrated = np.array([1, 0, 0]) # SPY only
w_diversified = np.array([0.6, 0.3, 0.1]) # SPY / TLT / GLD
assets = ["SPY", "TLT", "GLD"]
cov_3 = cov.loc[assets, assets].values
vol_concentrated = np.sqrt(w_concentrated.T @ cov_3 @ w_concentrated)
vol_diversified = np.sqrt(w_diversified.T @ cov_3 @ w_diversified)
Results
Key Pairwise Correlations (Full Period)
| Asset Pair | Correlation | Interpretation |
|---|---|---|
| SPY – AAPL | ~0.80 | Strong positive (equity clustering) |
| SPY – EFA | ~0.75 | Strong positive (global equity co-movement) |
| SPY – XLE | ~0.55 | Moderate positive (sector link) |
| SPY – TLT | ~−0.35 | Negative (stock-bond diversification) |
| SPY – GLD | ~0.05 | Near zero (independent return driver) |
| TLT – GLD | ~0.10 | Near zero (distinct safe-haven profiles) |
Diversification Benefit
| Portfolio | Weights | Annualized Volatility |
|---|---|---|
| Concentrated | 100% SPY | ~18.5% |
| Diversified | 60% SPY / 30% TLT / 10% GLD | ~12.0% |
Diversification reduced annualized portfolio volatility by approximately 35% relative to SPY-only.
Insights
- ~35% volatility reduction from diversification: the three-asset portfolio (SPY/TLT/GLD) achieved ~12% annualized volatility versus ~18.5% for SPY alone, demonstrating the practical benefit of combining low-correlated assets.
- Equity correlations cluster above 0.50: SPY, AAPL, EFA, and XLE moved together, meaning additional equity-only positions provide limited diversification benefit.
- SPY–TLT correlation was regime-dependent: rolling windows showed the negative correlation weakened and occasionally turned positive during the 2022 rate-hike environment, meaning the stock-bond hedge is not guaranteed.
- Gold remained the most independent asset: GLD correlations stayed near zero across both calm and stress periods, making it the most reliable diversifier in the portfolio.
- Correlations spiked during crises: the March 2020 selloff saw equity correlations converge upward, partially reducing diversification benefits precisely when they are needed most.
Business Recommendations
- Monitor rolling correlations continuously: treat correlation as time-varying and adjust risk budgets when co-movement begins to rise.
- Diversify across risk drivers, not just tickers: adding more equity ETFs provides less benefit than combining equities with bonds and commodities.
- Use covariance-based targets: rebalance using portfolio volatility targets rather than static weight rules to adapt to changing correlations.
- Plan for correlation breakdown: build allocations that anticipate temporary loss of the stock-bond hedge during rate-driven environments.
Analysis 4 — Time Series Decomposition
Business Question
What components drive SPY price movement over time, and how can trend, seasonality, and residual shocks be separated to support clearer regime interpretation and forecasting readiness?
Methodology
I used 50-day and 200-day moving averages as practical regime indicators (the "golden cross" and "death cross" framework), then decomposed the SPY series into trend, seasonal, and residual components using multiplicative seasonal decomposition with a 252-day (one trading year) periodicity.
An Augmented Dickey-Fuller (ADF) test was run on SPY daily returns to confirm stationarity — a prerequisite for the ARIMA modeling in Analysis 5. This combination links visual regime interpretation, structural decomposition, and statistical model-readiness diagnostics.
Code Highlight
import numpy as np
from statsmodels.tsa.seasonal import seasonal_decompose
from statsmodels.tsa.stattools import adfuller
# Moving averages (trend/regime signals)
prices["SPY_MA50"] = prices["SPY"].rolling(50).mean()
prices["SPY_MA200"] = prices["SPY"].rolling(200).mean()
# Multiplicative decomposition (trend/seasonal/residual)
spy_series = prices["SPY"].dropna()
decomp = seasonal_decompose(spy_series, model="multiplicative", period=252)
# Stationarity check on returns (not prices)
adf_stat, p_value, *_ = adfuller(daily_returns["SPY"].dropna())
print(f"ADF Statistic: {adf_stat:.3f}, p-value: {p_value:.4f}")
Results
Stationarity Test
| Series | ADF Statistic | p-value | Conclusion |
|---|---|---|---|
| SPY daily returns | < −20 | < 0.001 | Stationary (reject unit root) |
Daily returns are stationary; raw price levels are not. This confirms return-based inputs are appropriate for ARIMA modeling.
Insights
- Trend dominated long-horizon behavior: the trend component showed a persistent upward trajectory interrupted by sharp structural breaks (COVID crash in March 2020, rate-driven selloff in 2022).
- Seasonality was weak and inconsistent: the seasonal component contributed far less explanatory power than trend, suggesting that calendar-based patterns are not reliable signals for equity forecasting.
- Residuals concentrated shock events: the residual component captured crisis-period spikes that standard trend views hide — useful for identifying stress regimes.
- MA crossovers lagged regime shifts: the 50/200-day moving average crossovers confirmed trend changes but typically lagged actual price reversals by several weeks.
- Returns were strongly stationary: ADF test p-value < 0.001, confirming that return-based modeling inputs (not raw prices) are appropriate for downstream ARIMA forecasting.
Business Recommendations
- Use moving averages as confirmation, not prediction: MA crossovers are useful for validating regime changes but should not be used as primary timing signals.
- Monitor residual spikes as risk alerts: large residual deviations can serve as early-warning signals for tighter exposure controls.
- Combine trend with macro context: trend diagnostics are most valuable when paired with volatility analysis and economic indicators.
- Model returns, not prices: stationarity testing confirms that return-based inputs produce more reliable forecasting pipelines.
Analysis 5 — Forecasting (ARIMA Baseline)
Business Question
Can a classical ARIMA model produce directionally useful out-of-sample SPY price forecasts, and how should forecast uncertainty be interpreted for practical portfolio decision support?
Methodology
I applied a chronological 80/20 train-test split to preserve realistic forecasting order (no future data leakage), then fit an ARIMA(1,1,1) model on the training segment. Predictions were generated for the full test horizon (~500 trading days) and evaluated with MAE and RMSE.
Confidence intervals were included to represent uncertainty explicitly. This is critical because market regime shifts and volatility clustering can quickly degrade point-forecast reliability — forecasts should always be communicated as ranges, not certainties.
Code Highlight
import numpy as np
import pandas as pd
from statsmodels.tsa.arima.model import ARIMA
from sklearn.metrics import mean_absolute_error, mean_squared_error
spy = prices["SPY"]
# Chronological train-test split (80/20)
split = int(len(spy) * 0.8)
train, test = spy.iloc[:split], spy.iloc[split:]
# Fit ARIMA(1,1,1)
model = ARIMA(train, order=(1, 1, 1))
fit = model.fit()
# Forecast with confidence intervals
fc = fit.get_forecast(steps=len(test))
pred = fc.predicted_mean
conf = fc.conf_int() # lower/upper bounds
mae = mean_absolute_error(test, pred)
rmse = np.sqrt(mean_squared_error(test, pred))
print(f"MAE: ${mae:.2f} | RMSE: ${rmse:.2f}")
Results
Forecast Error Metrics
| Metric | Value | Interpretation |
|---|---|---|
| MAE | Moderate | Average absolute prediction error across the test window |
| RMSE | Higher than MAE | Penalizes large errors — confirms forecast degradation during volatile periods |
| Train/Test Split | 80% / 20% | ~2,012 train days / ~503 test days |
| Model Order | ARIMA(1,1,1) | First-order autoregressive + differencing + moving average |
Insights
- ARIMA captured the general trend direction: the model correctly identified the upward drift in SPY over the test period, but could not anticipate the magnitude or timing of the late-sample rally.
- Point forecasts degraded during regime shifts: prediction error increased substantially during high-volatility windows, confirming that univariate linear models struggle with nonlinear market dynamics.
- Confidence intervals widened rapidly: forecast uncertainty grew quickly over longer horizons, meaning ARIMA is most useful for short-term directional guidance, not multi-month point predictions.
- RMSE exceeded MAE meaningfully: the gap indicates that a small number of large errors (during volatile periods) disproportionately drove total forecast error.
- Baseline benchmark, not production model: ARIMA(1,1,1) serves as a transparent, interpretable baseline against which richer models (multivariate, regime-switching, machine learning) should be benchmarked.
Business Recommendations
- Keep ARIMA as a baseline: use it as a reference point in model comparison workflows — any production model should demonstrably outperform this benchmark.
- Add rolling retraining: retrain the model on a rolling window to adapt faster to evolving market structure and reduce drift in predictions.
- Communicate forecasts as ranges: always report prediction intervals alongside point estimates — single-number forecasts create false confidence.
- Explore richer alternatives: test multivariate models (VAR), regime-switching models, and machine learning approaches against this ARIMA baseline to quantify incremental improvement.
Key Findings & Recommendations
Key Findings
- Massive performance dispersion: over 2015–2024, AAPL returned ~935% while TLT lost ~11%, reinforcing that raw price levels are not comparable and base-normalized views are essential for cross-asset interpretation.
- Return and risk are fundamentally linked: the strongest cumulative growth (AAPL, SPY) came from higher-volatility equity exposure, while lower-volatility assets (GLD) produced smoother but more modest outcomes.
- Path risk matters as much as endpoint return: XLE and TLT posted severe interim drawdowns (~62% and ~48%) that would challenge real investor behavior, even where long-run returns appeared acceptable.
- Equity correlations limit within-class diversification: SPY, AAPL, EFA, and XLE correlated between 0.50–0.80, meaning equity-only portfolios carry concentrated risk.
- Cross-asset diversification worked: a 60/30/10 SPY/TLT/GLD portfolio achieved ~35% lower volatility than SPY alone, demonstrating practical diversification benefit.
- Correlations are regime-dependent: rolling analysis showed the SPY–TLT negative correlation weakened during the 2022 rate-hike environment and equity correlations spiked during the 2020 crisis.
- Trend dominated SPY structure: time series decomposition confirmed a persistent long-run upward trend interrupted by short-lived structural breaks, with seasonality contributing minimal explanatory power.
- Forecasting price levels remains difficult: the ARIMA baseline captured directional trend but could not anticipate regime shifts, confirming that univariate models have limited predictive power for multi-month horizons.
Business Recommendations
- Build cross-regime portfolios: combine equities, rates, and real assets rather than relying on multiple equity proxies that share the same latent risk factors.
- Pair return metrics with downside diagnostics: include drawdown, rolling correlation, and volatility in recurring risk reviews — not just return and Sharpe.
- Operationalize correlation monitoring: use rolling windows and stress-period overlays as an ongoing risk dashboard, not a one-time diagnostic.
- Use classical models as baselines: benchmark richer alternatives (multivariate, regime-switching, machine learning) against transparent ARIMA baselines.
- Adopt rolling retraining for forecasting: walk-forward validation ensures models are recalibrated as market regimes evolve.
- Communicate forecasts as ranges: report prediction intervals and disclose model limitations during structural breaks — never present single-point forecasts as certainties.
- Translate analytics into governance: define rebalance triggers, drawdown thresholds, and exception-based risk alerts to make insights actionable in live portfolio processes.
Technical Details
Libraries Used
| Library | Purpose |
|---|---|
| Python 3.10+ | Runtime environment |
| pandas | Data manipulation and analysis |
| NumPy | Numerical computing |
| yfinance | Yahoo Finance API for market data retrieval |
| matplotlib | Static data visualization |
| seaborn | Statistical data visualization |
| statsmodels | Time series analysis, decomposition, ARIMA, statistical tests |
| SciPy | Scientific computing and statistical functions |
| scikit-learn | Machine learning models and evaluation metrics |
| Jupyter Notebook | Interactive analysis environment |
How to Reproduce
- Clone the repository:
git clone https://github.com/nadeaujonny/nadeaujonny.github.io.git - Navigate to the project:
cd projects/python-financial-analysis - Install dependencies:
pip install -r requirements.txt - Run the notebooks in order (01 through 05) in Jupyter
- Charts and outputs will be saved to the
outputs/directory
Project Structure
python-financial-analysis/
├── index.md # This project page
├── requirements.txt # Python dependencies
├── README.md # Project README
├── data/ # Data directory (API-sourced, not committed)
├── notebooks/ # Jupyter notebooks (01-05)
│ ├── 01_data_acquisition_eda.ipynb
│ ├── 02_returns_risk_metrics.ipynb
│ ├── 03_correlation_diversification.ipynb
│ ├── 04_time_series_decomposition.ipynb
│ └── 05_forecasting.ipynb
├── outputs/ # Generated outputs and exports
│ └── figures/ # Charts and visualizations
└── .gitignore # Excludes data files