What If 20 Lines of Code Beat Most Traders' Intuition?
โ Runtime tested โ the solution passes every example case. ๐ Full written solution: https://timepasshub2539-hue.github.io/leetcode-python/project-build-a-stock-price-analysis-beginner-time-series.html ๐ป Code on GitHub: https://github.com/timepasshub2539-hue/leetcode-python/tree/main/project-build-a-stock-price-analysis-beginner-time-series Chapters: 0:00 What If 20 Lines of Code Beat Most Traders' Intuition? 0:21 The Plan 0:32 Step 1: Imports and Download 0:48 Step 2: The Date Index Is Already There 1:16 Step 3: Grab Closing Prices 1:40 Step 4: Plot the Closing Prices 1:53 Step 5: Resample to Weekly and Monthly 2:09 Step 6: Moving Averages with .rolling() 2:30 Step 7: Compare Trading Volume 2:46 The Complete Program 3:08 Running It: What You'll See 3:31 Where to Take It Further 3:54 That's a Wrap Learn how to analyze real stock price data using Python and pandas โ from downloading data to spotting trends with moving averages. You'll resample daily prices into weekly and monthly views, compare trading volume, and build a complete beginner-friendly time series analysis script from scratch. By the end, you'll have working code you can reuse on any stock ticker. #python #stockanalysis #pandas #timeseries #datascience Watch next: - Archaeologists Ate 3,000-Year-Old Honey โ And Venus Breaks Time Itself #shorts: https://youtu.be/3nJH8DEW5XI - Shift 2D Grid โ LeetCode Daily Python Solution (the trick nobody sees): https://youtu.be/aaYx4Q1R1j0 - Python Lesson 7: while & for Loops Explained (range, break, continue): https://youtu.be/kQS2WKE81b8
Introduction
Every developer eventually runs into time series data โ server metrics, sensor readings, sales numbers, or in this case, stock prices. Stock data happens to be the friendliest on-ramp into the topic, because you already understand the domain. Everyone has an intuitive sense of what a "closing price" or a "moving average" means, so instead of spending half your energy learning unfamiliar business logic, you can spend all of it learning the tool: pandas.
This matters beyond finance. Interviewers and real production systems both care about the same underlying skills โ working with a proper datetime index, aggregating data over time windows, and computing rolling statistics. These are the exact mechanics behind resampling application logs by hour, computing 7-day rolling averages of user signups, or smoothing sensor noise in an IoT pipeline. Learn it once on stock prices, and it transfers everywhere.
In this project, we'll go from a single line of code that downloads real market data, all the way to a complete script that plots price trends, resamples into weekly and monthly views, computes moving averages, and compares trading volume โ in under 20 lines of core logic.
Problem Overview
The task: given daily stock price data for one or more tickers, build a small analysis pipeline that can:
- Download historical price and volume data.
- Ensure the data is indexed by actual calendar dates (not just date-like strings).
- Extract closing prices and visualize them over time.
- Downsample the daily data into weekly and monthly summaries.
- Compute short-term and long-term moving averages to reveal trend direction.
- Compare trading volume alongside price movement.
There's no single "correct output" here in the LeetCode sense โ this is an exploratory data engineering task. The goal is a script that's reusable on any ticker, producing consistent, readable analysis every time.
Example
Suppose we pull two years of daily data for two tickers, say AAPL and MSFT. A single download call returns a table where the top-level columns are price fields (Open, High, Low, Close, Volume) and the second level is the ticker symbol. Pulling Close from that table gives you a clean two-column DataFrame:
| Date | AAPL | MSFT |
|---|---|---|
| 2024-01-02 | 185.64 | 370.87 |
| 2024-01-03 | 184.25 | 366.02 |
| 2024-01-04 | 181.91 | 365.99 |
Resampling this to monthly (taking the last value of each month) collapses hundreds of daily rows into roughly 24 rows โ one per month โ each representing the closing price on the last trading day of that month. That's the price the market actually settled on, which is why "last value" is the standard way to downsample a price series, rather than averaging every day in the month.
Intuition
Here's how an experienced engineer approaches this, before writing any code.
First question: what's the shape of the data I'm working with? Stock data is inherently sequential โ today's price only makes sense relative to yesterday's. That immediately tells you the index shouldn't be a plain integer row number; it should be the date itself. Once dates are the real index (a DatetimeIndex, not text that merely looks like a date), pandas can jump directly to any day, slice by date range, and โ critically โ understand chronological ordering well enough to resample and plot correctly without you doing any manual sorting or formatting.
Second question: how do I go from noisy daily data to something a human can read a trend from? Two tools solve this, and they solve different problems:
- Resampling answers "what happened per week/month?" โ it's
groupby, but the buckets are time intervals instead of categories. - Rolling windows answer "what's the recent trend at every single point?" โ a moving average that slides forward one day at a time, smoothing out day-to-day noise without throwing away granularity.
The reason traders watch a 20-day average against a 200-day average isn't magic โ it's that a short window reacts quickly to recent price action, while a long window changes slowly and represents the underlying trend. When the fast line crosses above the slow line, it's a visual signal that recent momentum has shifted relative to the long-term baseline. That's the entire idea behind the so-called "golden cross" โ nothing more mysterious than two rolling averages of different lengths.
Only after this mental model is in place does it make sense to open an editor and write code.
Brute Force Solution
Before reaching for pandas' built-in resample and rolling methods, imagine doing this manually with raw Python.
Idea: Loop through the daily price list, manually bucket each date into its week or month using date arithmetic, and manually compute averages over sliding index ranges with nested loops.
Algorithm:
- Parse each date string into a
dateobject. - Maintain a dictionary keyed by (year, week) or (year, month), appending prices into lists.
- After grouping, take the last price in each bucket.
- For moving averages, slide a window index across the list and sum/divide each time.
Advantages: No library dependency beyond the standard library; you fully control the bucketing logic.
Disadvantages: Reimplements what pandas already does correctly and efficiently โ including edge cases like uneven month lengths, missing trading days (holidays/weekends), and leap years. It's also slow: naive rolling averages recompute the full sum for every window position instead of reusing the previous sum.
Time complexity: O(nยทk) for rolling averages with a naive re-sum approach, where n is the number of rows and k is the window size. Resampling by manual bucketing is O(n).
Space complexity: O(n) for the grouped buckets and output lists.
This works, but it's a lot of fragile date-handling code for something pandas solves in one method call โ and it's easy to introduce subtle bugs around weekends and holidays.
Optimal Solution
The optimal approach leans entirely on pandas' time series machinery, which is implemented in optimized C under the hood.
Step 1 โ Download aligned data. Pull all tickers in a single call. yfinance aligns them on the same date index automatically, so you don't need to manually join separate downloads.
Step 2 โ Guarantee a real DatetimeIndex. yfinance usually gives you this for free, but converting explicitly with pd.to_datetime is the safety net for any dataset where it isn't guaranteed.
Step 3 โ Extract closing prices. Because multi-ticker downloads come back with a two-level column structure (field, then ticker), selecting 'Close' slices off just that top level, leaving a clean DataFrame.
Step 4 โ Plot. With a proper datetime index, matplotlib automatically lays out the x-axis chronologically โ no manual formatting required.
Step 5 โ Resample. .resample('W').last() and .resample('ME').last() bucket the daily rows into weekly and monthly groups, keeping the final (closing) price of each period.
Step 6 โ Rolling averages. .rolling(window=20).mean() and .rolling(window=200).mean() compute the moving averages directly on the daily closing prices.
Step 7 โ Volume comparison. The same column-selection pattern used for Close applies to Volume.
A mental table of what each tool does:
| Tool | Purpose | Analogy |
|---|---|---|
DatetimeIndex |
Makes dates first-class | Page numbers in a book |
.resample() |
Buckets rows by time period | groupby, but for time |
.rolling() |
Sliding window statistic | Moving average on a chart |
Python Code
import matplotlib.pyplot as plt
import pandas as pd
import yfinance as yf
TICKERS = ["AAPL", "MSFT"]
# Step 1: Download daily price/volume data for both tickers at once
data = yf.download(TICKERS, start="2023-01-01", end="2025-01-01")
# Step 2: Ensure a real datetime index (yfinance already provides this,
# but this is the manual fix for datasets that don't)
data.index = pd.to_datetime(data.index)
# Step 3: Extract closing prices
close_prices = data["Close"]
# Step 4: Plot closing prices
close_prices.plot(title="Closing Prices")
plt.ylabel("Price (USD)")
plt.show()
# Step 5: Resample to weekly and monthly closing prices
weekly = close_prices.resample("W").last()
monthly = close_prices.resample("ME").last()
# Step 6: Moving averages (20-day and 200-day) per ticker
moving_avg_20 = close_prices.rolling(window=20).mean()
moving_avg_200 = close_prices.rolling(window=200).mean()
fig, ax = plt.subplots()
close_prices["AAPL"].plot(ax=ax, label="AAPL Close")
moving_avg_20["AAPL"].plot(ax=ax, label="20-Day MA")
moving_avg_200["AAPL"].plot(ax=ax, label="200-Day MA")
ax.legend()
plt.show()
# Step 7: Compare trading volume
volume = data["Volume"]
volume.plot(title="Trading Volume")
plt.show()
Code Walkthrough
yf.download(TICKERS, ...): downloads both tickers in one API call, returning a DataFrame with a two-level column structure โ field (Open/High/Low/Close/Volume) on top, ticker underneath.pd.to_datetime(data.index): converts the index to a trueDatetimeIndex. This is what unlocks.resample()and chronological plotting.data["Close"]: selects only the closing price field across both tickers, collapsing the two-level columns down to one level (just the ticker names).close_prices.plot(...): matplotlib reads the datetime index directly and lays out the x-axis in calendar order automatically..resample("W").last()/.resample("ME").last(): groups rows into weekly/month-end buckets and keeps the final trading day's price in each bucket โ the price the market actually closed at for that period..rolling(window=20).mean(): computes a trailing 20-day average at every row, sliding forward one day at a time. Same idea for the 200-day version.data["Volume"]: identical extraction pattern toClose, made possible by yfinance's consistent multi-level column layout.
Dry Run
Take a small slice of AAPL closing prices: [184, 185, 183, 186, 188, ...] for early January 2024.
close_prices["AAPL"]picks out just this column from the combined DataFrame..resample("W").last()groups all trading days that fall in the same calendar week and keeps only Friday's (or the last available day's) closing price โ so five daily values collapse into one weekly value..rolling(window=20).mean()on this series producesNaNfor the first 19 rows (not enough history yet), then starting at row 20, each value is the average of the trailing 20 closes. As new prices arrive, the oldest day drops out of the window and the newest day enters โ this is why the line reacts quickly to recent price changes.- The 200-day version follows the same mechanic but needs 200 rows of history before producing its first real number โ which is exactly why a fresh chart shows a flat gap of
NaNs at the start until day 200.
Complexity Analysis
- Time: Downloading is bounded by network I/O, not computation.
.resample()runs in O(n) since it's a single pass grouping by date bucket..rolling(window=k).mean()is O(n) as well โ pandas uses an incremental sum internally rather than recomputing the sum from scratch at every position, unlike the brute force approach's O(nยทk). - Space: O(n) for the original DataFrame, plus O(n) for each derived series (resampled or rolled). Nothing here scales worse than linear in the number of rows.
- Why these are correct: resample and rolling are both single left-to-right passes over sorted time-indexed data โ no nested loops, no re-scanning, which is only possible because the index is properly typed as dates.
Alternative Solutions
You could reach for statsmodels or a dedicated time series library to compute rolling statistics, decompositions, or seasonality โ but for straightforward moving averages and resampling, that's more machinery than the problem needs. Pandas alone handles this cleanly, and reaching for a heavier library before you need decomposition/forecasting features is premature. The pandas-only approach is faster to write, easier to read, and has no additional dependency overhead for a task this size.
Edge Cases
- Empty download (invalid ticker or no data in date range):
close_priceswill be an empty DataFrame;.resample()and.rolling()will simply return empty results without erroring. - Single ticker instead of multiple: the column structure flattens to a single level, so
data["Close"]still works but returns a Series instead of a DataFrame โ worth checking with.ndimif your code assumes multiple tickers. - Window larger than available history:
.rolling(window=200)on fewer than 200 rows produces allNaNvalues โ not an error, just no output yet. - Missing trading days (holidays): resample buckets simply have no row contribution from closed days;
.last()still finds the last available trading day in the period. - Duplicate or unsorted dates: rare with yfinance, but if present, sort the index first โ resample and rolling both assume monotonic ordering.
Common Mistakes
- Forgetting to convert the index to
DatetimeIndexbefore resampling, causing aTypeErroror, worse, a silent wrong grouping by string sort order. - Assuming the 200-day moving average starts producing values immediately, instead of expecting
NaNs for the first 199 rows. - Selecting
'Close'before verifying the multi-ticker column structure, leading to a KeyError when only one ticker was downloaded. - Using
.mean()instead of.last()when resampling prices, which distorts the "close" concept market data is supposed to represent. - Plotting without a proper datetime index and then manually trying to format the x-axis โ unnecessary work pandas already does for you.
- Not handling weekends/holidays explicitly, then being confused why row counts don't match calendar day counts.
Interview Questions
- How does
.resample()differ from.groupby(), and why does resample require a datetime index? - What's the time complexity of a rolling mean over a Series of length n with window size k, and how is it achieved without recomputing the sum each time?
- How would you handle multiple tickers with different date ranges (e.g., one has a shorter trading history)?
- How would you extend this to compute Bollinger Bands using the existing rolling window?
- What's the difference between upsampling and downsampling, and which one applies here?
Similar Problems
- LeetCode 239 โ Sliding Window Maximum: same "moving window over a sequence" mental model as
.rolling(), but for max instead of mean. - LeetCode 1004 โ Max Consecutive Ones III: another sliding window problem, reinforcing window-based thinking.
- LeetCode 480 โ Sliding Window Median: a harder variant of the rolling-statistic idea used here.
- LeetCode 346 โ Moving Average from Data Stream: directly mirrors the rolling mean concept, implemented from scratch instead of via pandas.
Key Takeaways
A datetime index isn't a formality โ it's the foundation that makes resampling, rolling windows, and chronological plotting work correctly and efficiently. Resample answers "what happened per period," rolling answers "what's the trend at every point," and both run in linear time because pandas processes sorted time data in a single pass. These same three tools โ datetime index, resample, rolling โ apply directly to sales data, sensor logs, or any other sequence that moves through time, not just stock prices.
Watch the Video
For the full walkthrough with live output and chart results, watch the video here: {LINK}
About the Series
This project is part of the Daily Python LeetCode series, where each entry breaks down one coding problem or hands-on Python project โ building real intuition for algorithms, data structures, and practical tools like pandas, one small, focused lesson at a time.
Call To Action
If this helped, subscribe on YouTube for daily Python breakdowns, and subscribe to the Substack newsletter to get each write-up delivered directly. Drop a comment with the ticker you tried this on, and share this with anyone learning pandas or time series analysis.
The solution
import yfinance as yf
import matplotlib.pyplot as plt
tickers = ["AAPL", "MSFT"]
data = yf.download(tickers, start="2022-01-01", end="2024-01-01")
close = data["Close"]
volume = data["Volume"]
close.plot(figsize=(10, 5), title="Closing Price"); plt.show()
weekly = close.resample("W").last()
monthly = close.resample("ME").last()
close["AAPL_MA20"] = close["AAPL"].rolling(20).mean()
close["AAPL_MA200"] = close["AAPL"].rolling(200).mean()
close[["AAPL","AAPL_MA20","AAPL_MA200"]].plot(figsize=(10,5)); plt.show()
volume.plot(figsize=(10, 5), title="Trading Volume"); plt.show()Ready to try it yourself? Solve General problems with instant feedback in the practice sandbox.
Practice now โ


