Time Series

Apple Stock Price Analysis in Python

Load Apple stock price data, compute moving averages, calculate daily returns, visualize volatility, and compute the Sharpe ratio.

What

This AI Data Analyst workflow pulls the last two years of AAPL OHLCV data using yfinance and organizes it into a pandas time-series DataFrame. It generates 20-day and 50-day moving averages, plots price with the moving-average overlays, and highlights crossover behavior visually. It computes daily returns, visualizes the return distribution, then calculates 30-day rolling volatility and an annualised Sharpe ratio.

Who

This is for analysts and students who want a reproducible Python notebook for basic equity time-series analysis on Apple stock. It helps users learn how to compute and interpret moving averages, returns, volatility, and risk-adjusted performance from market data.

Tools

  • yfinance
  • pandas
  • numpy
  • matplotlib
  • seaborn

Outcomes

  • AAPL OHLCV DataFrame for ~2 years of trading days
  • Price chart with MA20 and MA50 overlays and visible crossovers
  • Daily returns series and histogram of returns
  • 30-day rolling volatility plot
  • Printed annualised Sharpe ratio

Quality Score

7/10

gpt-5.2mljar-ai

Last scored: Apr 7, 2026

  • Task Completion: 1/2

    Needs work

    Loaded ~2 years of AAPL OHLCV data, computed MA20/MA50, plotted price+MAs, computed daily returns and plotted their distribution. Did not compute or plot 30-day rolling volatility and did not compute/print the annualised Sharpe ratio (only asked a clarifying question).

  • Execution Correctness: 2/2

    Excellent

    Provided code blocks are syntactically correct and handle yfinance MultiIndex columns; moving averages and returns calculations are appropriate and likely runnable. No evident breaking errors in the shown code.

  • Output Quality: 2/3

    Good

    Outputs include a MultiIndex OHLCV preview and two figures (price with MA20/MA50 and returns histogram with KDE), matching most expected visuals. Missing the rolling volatility chart and the Sharpe ratio output, and the dataframe shape is 501x6 rather than the expected ~504x6 (still plausible for a 2y window).

  • Reasoning Quality: 1/2

    Needs work

    Reasoning about MultiIndex handling and return computation is correct, but the narrative includes speculative interpretation of plots without evidence and stops short of completing the final requested metrics.

  • Reliability: 1/1

    Excellent

    Workflow is reasonably robust (checks MultiIndex, uses dropna for returns) and avoids hallucinated code execution. However, it relies on yfinance availability and leaves the final step incomplete.

load AAPL stock prices for the last 2 years using yfinance

Let’s start by loading the data and doing a quick inspection so we can confirm the structure before any analysis.

Download and inspect AAPL data
import pandas as pd
import yfinance as yf

aapl = yf.download("AAPL", period="2y", progress=False)

display(pd.DataFrame({"value": [aapl.shape[0], aapl.shape[1]]}, index=["rows", "columns"]))
display(pd.DataFrame({"column": [str(col) for col in aapl.columns]}))
aapl.head()
value
rows 502
columns 5
column
0 ('Close', 'AAPL')
1 ('High', 'AAPL')
2 ('Low', 'AAPL')
3 ('Open', 'AAPL')
4 ('Volume', 'AAPL')
Price Close High Low Open Volume
Ticker AAPL AAPL AAPL AAPL AAPL
Date
2024-04-09 168.139771 168.546076 166.831683 167.178518 42373800
2024-04-10 166.266830 167.565013 165.602875 167.277636 49709300
2024-04-11 173.461334 173.877560 166.643394 166.821764 91070300
2024-04-12 174.957733 176.751407 172.638841 172.688378 101670900
2024-04-15 171.132568 175.037037 170.944279 173.778487 73531800

You now have AAPL daily stock data for the last 2 years loaded from yfinance.

From the observed output:

  • The dataset has 502 rows and 5 columns, which is consistent with about 2 years of trading days.
  • The available fields are Close, High, Low, Open, Volume.
  • The index is Date, so each row represents one trading day.
  • yfinance returned the columns as a MultiIndex, which is why the labels appear like ('Close', 'AAPL').

The first few rows confirm the data is populated correctly, starting around 2024-04-09 and including standard OHLCV market data for AAPL. This means the original request—to load AAPL stock prices for the last 2 years using yfinance—has been satisfied.

add 20-day and 50-day moving averages and plot
Thinking

Crafting code for MultiIndex handling

I’m working on writing robust code that processes a MultiIndex by selecting the close series using aapl["Close"].squeeze(). I'll create a plot_df with close prices, 20-day, and 50-day moving averages. The original goal was just to plot, but I might avoid mutating aapl. Instead, I’ll add columns to a copy for a flat DataFrame. If the notebook variable aapl exists, I can proceed with creating the DataFrame and visualizations.

Notebook code
import matplotlib.pyplot as plt
import seaborn as sns

close = aapl["Close"].squeeze() if isinstance(aapl.columns, pd.MultiIndex) else aapl["Close"]

plot_df = pd.DataFrame({"Close": close})
plot_df["MA20"] = plot_df["Close"].rolling(20).mean()
plot_df["MA50"] = plot_df["Close"].rolling(50).mean()

sns.set_style("whitegrid")
plt.figure(figsize=(12, 6))
sns.lineplot(data=plot_df[["Close", "MA20", "MA50"]])
plt.title("AAPL Close Price with 20-Day and 50-Day Moving Averages")
plt.xlabel("Date")
plt.ylabel("Price (USD)")
plt.legend(title="")
plt.tight_layout()
plt.show()
Thinking

Deciding on moving averages

I’m working on whether to decide between two paths based on some heuristics. The latest output is just a chart without numeric or text values to support the conclusion. My goal is to add and plot the 20-day and 50-day moving averages. Since the output is chart-only, it might be enough, but without those explicit values, I’ll choose Path B. I need to provide brief insights and one runnable Python code block to gather the missing evidence before verifying the outputs.Gathering moving average values

I’m thinking about how to display the tail of plot_df with rounded values, focusing on gathering underlying data. My goal is to show the last 10 rows while also counting NaNs due to rolling calculations. I don't need to ask the user to run anything. The plot rendered successfully, and the moving average columns are added to plot_df across 502 trading days. Since the last output is just the figure, I need to present the recent MA values used in the chart.Simplifying code output

I’m considering avoiding any unsupported details in my code. The 20 and 50 rolling averages will definitely produce NaNs at the top, but I don't need to mention that. I want to include exactly one runnable code block since pandas is already imported. I'll use display(plot_df.tail(10).round(2)) to show the last 10 rows rounded to two decimal places. I remember that it's okay to end with the display call without needing a bare expression afterward, so I'll keep it simple and minimal.

The line chart rendered, and the notebook state shows the moving-average columns were added correctly in plot_df alongside Close over the full 502 trading days. The only missing piece is the underlying values behind that chart so the 20-day and 50-day averages are explicitly visible.

Display recent moving averages
display(plot_df[["Close", "MA20", "MA50"]].tail(10).round(2))
Close MA20 MA50
Date
2026-03-26 252.89 256.14 260.43
2026-03-27 248.80 255.38 260.21
2026-03-30 246.63 254.47 259.98
2026-03-31 253.79 253.97 259.95
2026-04-01 255.63 253.63 260.13
2026-04-02 255.92 253.41 260.30
2026-04-06 258.86 253.48 260.52
2026-04-07 253.50 253.16 260.63
2026-04-08 258.90 253.06 260.71
2026-04-09 257.89 252.92 260.70
Thinking

Formulating final insights

I’m looking to provide a natural final answer based on the current outputs. We have the chart object and values from the last ten data points, which should be sufficient.

I should clearly mention the added MA20 and MA50 and how they relate to the latest closing price on 2026-04-09, which was 257.89. It’s interesting to note that while the short-term price is above MA20 at 252.92, it’s still below MA50 at 260.70. This indicates recent price strength but underlines the longer-term average. Let's simplify it and present it with the final marker.

The 20-day and 50-day moving averages were added to plot_df as MA20 and MA50, and the plot shows all three series together: Close, MA20, and MA50.

From the latest values in the table:

  • On 2026-04-09, Close = 257.89
  • MA20 = 252.92
  • MA50 = 260.70

That means the recent price is:

  • above the 20-day average, suggesting stronger short-term momentum
  • below the 50-day average, suggesting it is still slightly under the medium-term trend

Across the last 10 rows, the 20-day average drifted down from about 256.14 to 252.92, while the 50-day average stayed higher near 260.70, so the short-term trend has been weaker than the longer-term trend even though price has recently bounced.

calculate daily returns and plot their distribution
Thinking

Calculating daily returns

I need to tackle this task step-by-step, focusing on the user goal. The data is ready, and I have to calculate daily returns and plot their distribution. So, I'll create a returns series and plot a histogram in a single step. I need to ensure my code is concise and that I use seaborn or matplotlib for this visualization. I’ll probably use the existing closing series to generate the returns and plot the distribution!

We already have close, so the next step is to compute daily returns with pct_change() and plot their distribution.

Plot daily returns distribution
returns = close.pct_change().dropna()

sns.set_style("whitegrid")
plt.figure(figsize=(10, 6))
sns.histplot(returns, bins=40, kde=True)
plt.title("Distribution of AAPL Daily Returns")
plt.xlabel("Daily return")
plt.ylabel("Frequency")
plt.tight_layout()
plt.show()
Thinking

Deciding on output path

I need to follow the instructions and decide between options A or B. The user's goal is to calculate daily returns and plot the distribution. Since the latest output is chart-only, I should choose Path B first to print the values used in the chart. I should include insights from the outputs, noting that returns were calculated from 502 trading days, resulting in about 501 daily returns. I'll also provide summary statistics with minimal runnable Python code, using returns.describe(percentiles=[0.01,0.05,0.25,0.5,0.75,0.95,0.99]).to_frame('daily_return').

The distribution plot rendered, so the daily returns series was computed successfully from the close prices. Since the latest output is only the chart, the missing piece is a compact numeric summary of those returns so the distribution can be interpreted directly.

Summarize return statistics
display(
    returns.describe(percentiles=[0.01, 0.05, 0.25, 0.5, 0.75, 0.95, 0.99]).to_frame("daily_return")
)
daily_return
count 501.000000
mean 0.001013
std 0.017921
min -0.092456
1% -0.048167
5% -0.027040
25% -0.006753
50% 0.001317
75% 0.008010
95% 0.024808
99% 0.043271
max 0.153288
Thinking

Drafting concise analysis

I need to create an answer using Path A since it includes both a chart and a numeric summary. I want to make sure I share insights from the latest outputs without merely restating. I'll focus on presenting daily returns calculated for 501 trading days, noting that the distribution is centered near zero, with a mean of 0.10% and a median of 0.13% per day. I'll mention key statistics, like the 5th/95th percentiles and the shape of the distribution, but aim for clarity without code.

You calculated AAPL daily returns as percentage changes in the closing price and plotted their distribution.

From the observed results:

  • The return distribution is centered close to zero, with an average daily return of about 0.10% and a median of about 0.13%.
  • Typical day-to-day movement is about 1.79% based on the standard deviation.
  • The middle 50% of daily returns falls roughly between -0.68% and +0.80%, so most days are relatively small moves.
  • Larger moves do occur: about 5% of days are worse than -2.70%, and about 5% are better than +2.48%.
  • The most extreme observed daily moves were about -9.25% and +15.33%.

The histogram/KDE therefore represents a distribution that is tightly concentrated around zero with a few larger positive and negative outliers, and a slightly heavier positive tail because the largest gain was bigger than the largest loss.

compute 30-day rolling volatility and annualised Sharpe ratio
Thinking

Calculating rolling volatility and Sharpe ratio

I’m thinking about computing the 30-day rolling volatility and annualized Sharpe ratio using the returns data. I guess I could display the latest 10 entries of rolling volatility and keep Sharpe separate. However, since I can only show one chart or table per step, it might make sense to create a single DataFrame with the last 10 values. I should start by calculating the volatility and Sharpe first, and then focus on how to present the findings efficiently!Planning to plot rolling volatility

For the next step, I might plot the rolling volatility if the user wants that. I’ll assume a risk-free rate of zero, so I should mention that assumption. I think I’ll create a concise explanation along with the code, keeping it to just one code block for simplicity. I could set the rolling_vol_30 and sharpe_ann variables and make sure the metrics are displayed effectively in the notebook at the end of the code.

Assuming a zero risk-free rate, the next step is to compute the 30-day rolling volatility (annualized) and the overall annualized Sharpe ratio from your daily returns.

Compute volatility and Sharpe
import pandas as pd
import numpy as np

rolling_vol_30 = returns.rolling(30).std() * np.sqrt(252)
sharpe_ann = (returns.mean() / returns.std()) * np.sqrt(252)

metrics = pd.DataFrame(
    {
        "value": [
            rolling_vol_30.iloc[-1],
            sharpe_ann,
        ]
    },
    index=["latest_30d_rolling_vol", "annualized_sharpe_ratio"]
)

metrics
value
latest_30d_rolling_vol 0.209984
annualized_sharpe_ratio 0.897185

The outputs already answer your request.

For AAPL over the available 2-year window:

  • Latest 30-day rolling volatility: 0.209984, which is about 21.0% annualized
  • Annualized Sharpe ratio: 0.897185

What this means:

  • The 30-day rolling volatility is based on the standard deviation of the last 30 daily returns, scaled by \sqrt{252} to annualize it. A value near 21% suggests AAPL’s recent realized volatility has been moderate to fairly elevated on an annualized basis.
  • The annualized Sharpe ratio compares average daily return to daily return volatility, then annualizes the result. A Sharpe of about 0.90 indicates positive risk-adjusted performance, though not exceptionally high; returns have compensated for risk, but not by a very large margin.

These figures are also consistent with the earlier daily return stats you computed:

  • mean daily return: about 0.10%
  • daily standard deviation: about 1.79%

Using those values, the annualized Sharpe near 0.90 is in the expected range.

Browse All Analysis Examples