Skip to content

Commit e6c1302

Browse files
martin-martinclaude
andcommitted
Sample code for: Python for Stock Analysis: Build a Portfolio Analyzer
Port the materials from the CMS protected download (python-finance-portfolio-analysis-materials) unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 2fa006c commit e6c1302

5 files changed

Lines changed: 1355 additions & 0 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Python for Finance: Analyze a Stock Portfolio With pandas
2+
3+
Companion materials for the Real Python tutorial.
4+
5+
- `fetch_prices.py`: Downloads adjusted daily closing prices with yfinance and saves them to `prices.csv`
6+
- `prices.csv`: Snapshot of the prices used in the tutorial (2021-01-04 to 2025-12-31)
7+
- `portfolio.py`: The finished portfolio analyzer
8+
9+
Run `python portfolio.py` to print the summary table and save `growth.png`.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import yfinance as yf
2+
3+
TICKERS = ["AAPL", "JPM", "XOM", "JNJ", "SPY"]
4+
5+
prices = yf.download(
6+
TICKERS, start="2021-01-01", end="2026-01-01", progress=False
7+
)["Close"]
8+
prices = prices[TICKERS].round(2)
9+
prices.to_csv("prices.csv")
10+
print(prices.tail())
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import matplotlib.pyplot as plt
2+
import pandas as pd
3+
4+
TRADING_DAYS = 252
5+
WEIGHTS = {"AAPL": 0.4, "JPM": 0.2, "XOM": 0.2, "JNJ": 0.2}
6+
BENCHMARK = "SPY"
7+
8+
9+
def load_prices(path="prices.csv"):
10+
return pd.read_csv(path, index_col="Date", parse_dates=True)
11+
12+
13+
def calculate_returns(prices):
14+
return prices.pct_change().dropna()
15+
16+
17+
def build_portfolio(returns, weights):
18+
holdings = returns[list(weights)]
19+
return holdings @ pd.Series(weights)
20+
21+
22+
def growth_of(returns, initial=10_000):
23+
return initial * (1 + returns).cumprod()
24+
25+
26+
def annualized_return(returns):
27+
years = len(returns) / TRADING_DAYS
28+
return (1 + returns).prod() ** (1 / years) - 1
29+
30+
31+
def annualized_volatility(returns):
32+
return returns.std() * TRADING_DAYS**0.5
33+
34+
35+
def max_drawdown(returns):
36+
growth = (1 + returns).cumprod()
37+
return (growth / growth.cummax() - 1).min()
38+
39+
40+
def sharpe_ratio(returns, risk_free_rate=0.0):
41+
excess = annualized_return(returns) - risk_free_rate
42+
return excess / annualized_volatility(returns)
43+
44+
45+
def summarize(returns, risk_free_rate=0.0):
46+
sharpe = returns.apply(sharpe_ratio, risk_free_rate=risk_free_rate)
47+
return pd.DataFrame(
48+
{
49+
"Return": returns.apply(annualized_return),
50+
"Volatility": returns.apply(annualized_volatility),
51+
"Max Drawdown": returns.apply(max_drawdown),
52+
"Sharpe": sharpe,
53+
}
54+
)
55+
56+
57+
def plot_growth(returns, filename="growth.png"):
58+
growth = growth_of(returns)
59+
drawdown = growth / growth.cummax() - 1
60+
fig, (top, bottom) = plt.subplots(
61+
2, 1, figsize=(10, 7), sharex=True, height_ratios=[3, 1]
62+
)
63+
growth.plot(ax=top, title="Growth of $10,000")
64+
drawdown.plot(ax=bottom, title="Drawdown", legend=False)
65+
top.yaxis.set_major_formatter("${x:,.0f}")
66+
bottom.yaxis.set_major_formatter("{x:.0%}")
67+
fig.tight_layout()
68+
fig.savefig(filename)
69+
70+
71+
if __name__ == "__main__":
72+
prices = load_prices()
73+
returns = calculate_returns(prices)
74+
returns["Portfolio"] = build_portfolio(returns, WEIGHTS)
75+
comparison = returns[["Portfolio", BENCHMARK]]
76+
print(summarize(comparison, risk_free_rate=0.03).round(3))
77+
plot_growth(comparison)

0 commit comments

Comments
 (0)