Skip to content

Commit 935edff

Browse files
martin-martinclaude
andcommitted
Apply technical review fixes to sample code
- fetch_prices.py: keep full precision and stop when a ticker fails to download instead of overwriting prices.csv - portfolio.py: reject gaps instead of dropna(), validate weights, include starting value in drawdown peaks, use the standard Sharpe ratio, and return NaN for zero volatility - prices.csv: full-precision snapshot downloaded in October 2026 - README.md: current tutorial title and link Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent e6c1302 commit 935edff

4 files changed

Lines changed: 1289 additions & 1271 deletions

File tree

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
# Python for Finance: Analyze a Stock Portfolio With pandas
1+
# Python for Stock Analysis: Build a Portfolio Analyzer
22

3-
Companion materials for the Real Python tutorial.
3+
This folder provides the code examples for the Real Python tutorial [Python for Stock Analysis: Build a Portfolio Analyzer](https://realpython.com/python-stock-portfolio-analysis/).
44

55
- `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)
6+
- `prices.csv`: Snapshot of the prices used in the tutorial (2021-01-04 to 2025-12-31, downloaded in October 2026)
77
- `portfolio.py`: The finished portfolio analyzer
88

9-
Run `python portfolio.py` to print the summary table and save `growth.png`.
9+
Install the dependencies with `python -m pip install -r requirements.txt`, then run `python portfolio.py` to print the summary table and save `growth.png`.
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
import sys
2+
13
import yfinance as yf
24

35
TICKERS = ["AAPL", "JPM", "XOM", "JNJ", "SPY"]
46

57
prices = yf.download(
68
TICKERS, start="2021-01-01", end="2026-01-01", progress=False
79
)["Close"]
8-
prices = prices[TICKERS].round(2)
10+
prices = prices.reindex(columns=TICKERS)
11+
failed = prices.columns[prices.isna().all()].tolist()
12+
if failed:
13+
sys.exit(f"Download failed for {', '.join(failed)}")
914
prices.to_csv("prices.csv")
1015
print(prices.tail())

‎python-stock-portfolio-analysis/portfolio.py‎

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,19 @@ def load_prices(path="prices.csv"):
1111

1212

1313
def calculate_returns(prices):
14-
return prices.pct_change().dropna()
14+
if prices.isna().any().any():
15+
raise ValueError("Found missing prices. Choose dates without gaps.")
16+
if len(prices) < 3:
17+
raise ValueError("You need at least three days of prices.")
18+
return prices.pct_change().iloc[1:]
1519

1620

1721
def build_portfolio(returns, weights):
18-
holdings = returns[list(weights)]
19-
return holdings @ pd.Series(weights)
22+
weights = pd.Series(weights)
23+
if not (weights >= 0).all() or abs(weights.sum() - 1) > 1e-9:
24+
raise ValueError("Weights must be non-negative and add up to 1.")
25+
holdings = returns[weights.index]
26+
return holdings @ weights
2027

2128

2229
def growth_of(returns, initial=10_000):
@@ -32,14 +39,22 @@ def annualized_volatility(returns):
3239
return returns.std() * TRADING_DAYS**0.5
3340

3441

35-
def max_drawdown(returns):
42+
def drawdowns(returns):
3643
growth = (1 + returns).cumprod()
37-
return (growth / growth.cummax() - 1).min()
44+
peak = growth.cummax().clip(lower=1)
45+
return growth / peak - 1
46+
47+
48+
def max_drawdown(returns):
49+
return drawdowns(returns).min()
3850

3951

4052
def sharpe_ratio(returns, risk_free_rate=0.0):
41-
excess = annualized_return(returns) - risk_free_rate
42-
return excess / annualized_volatility(returns)
53+
daily_risk_free = (1 + risk_free_rate) ** (1 / TRADING_DAYS) - 1
54+
excess = returns - daily_risk_free
55+
if excess.std() == 0:
56+
return float("nan")
57+
return excess.mean() / excess.std() * TRADING_DAYS**0.5
4358

4459

4560
def summarize(returns, risk_free_rate=0.0):
@@ -55,13 +70,11 @@ def summarize(returns, risk_free_rate=0.0):
5570

5671

5772
def plot_growth(returns, filename="growth.png"):
58-
growth = growth_of(returns)
59-
drawdown = growth / growth.cummax() - 1
6073
fig, (top, bottom) = plt.subplots(
6174
2, 1, figsize=(10, 7), sharex=True, height_ratios=[3, 1]
6275
)
63-
growth.plot(ax=top, title="Growth of $10,000")
64-
drawdown.plot(ax=bottom, title="Drawdown", legend=False)
76+
growth_of(returns).plot(ax=top, title="Growth of $10,000")
77+
drawdowns(returns).plot(ax=bottom, title="Drawdown", legend=False)
6578
top.yaxis.set_major_formatter("${x:,.0f}")
6679
bottom.yaxis.set_major_formatter("{x:.0%}")
6780
fig.tight_layout()

0 commit comments

Comments
 (0)