Apple Stock Price Analysis in Python

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

This page shows an AI data analysis example. A workflow is a list of prompts (instructions) used to complete one analysis task step by step. We send the same prompt list to multiple AI models, then compare their notebook outputs and scores.

What this AI workflow does

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 example is for

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.

Expected analysis outcomes

These are the results the AI workflow is expected to generate.

  • 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

Tools and libraries used

Main Python packages and tooling used to run this AI data analysis task.

  • yfinance
  • pandas
  • numpy
  • matplotlib
  • seaborn

Prompt sequence

This is the exact list of prompts used in this workflow. The same prompt sequence is sent to each model so outputs and scores can be compared fairly.

  1. 1load AAPL stock prices for the last 2 years using yfinance
  2. 2add 20-day and 50-day moving averages and plot
  3. 3calculate daily returns and plot their distribution
  4. 4compute 30-day rolling volatility and annualised Sharpe ratio

Model Comparison

We compared several LLM models on the same analysis task. The table below shows their scores side by side. You can also open each model run to check the full conversation and notebook results.

Model SourceScoreTaskExecutionOutputReasoningReliabilityRun
gemma4:31b10/102/22/23/32/21/1Open gemma4:31b conversation
gpt-5.410/102/22/23/32/21/1Open gpt-5.4 conversation
gpt-oss:120b10/102/22/23/32/21/1Open gpt-oss:120b conversation
glm-5.19/102/22/23/31/21/1Open glm-5.1 conversation
qwen3.5:397b7/102/21/23/31/20/1Open qwen3.5:397b conversation
qwen3-coder-next6/102/21/22/31/20/1Open qwen3-coder-next conversation
How scoring works

Each run is graded on five dimensions:

  • Task Completion (0-2): how fully the workflow answered the requested analysis task and completed required steps.
  • Execution Correctness (0-2): whether generated code is correct, consistent, and likely runnable without major fixes.
  • Output Quality (0-3): how well tables, charts, and results match the expected analytical outcomes.
  • Reasoning Quality (0-2): clarity and correctness of interpretation, explanation, and conclusions.
  • Reliability (0-1): robustness of the run, including low hallucination risk and stable behavior.

The final score is the sum of these dimensions, so total score = 0-10. Higher values indicate more complete, correct, and reliable notebook results for this workflow.

AI Analysis with qwen3.5:397b

This run uses AI with a step-by-step list of prompts to perform the analysis task Apple Stock Price Analysis in Python. Below, you can review the full conversation generated with qwen3.5:397b. After the conversation, we show detailed score cards for this run.

load AAPL stock prices for the last 2 years using yfinance
Thinking

The user wants to load AAPL stock prices for the last 2 years using yfinance. This is a straightforward data loading task. I'll write code to:

  1. Import yfinance
  2. Download AAPL data for the last 2 years
  3. Display basic info about the data (shape, columns, head)

Let me create a simple, focused code block for this.

I'll load AAPL stock data for the last 2 years using yfinance and show you the basic structure of the data.

Download AAPL stock data
import yfinance as yf

# Download AAPL data for the last 2 years
aapl = yf.download('AAPL', period='2y')

# Display basic info
aapl.shape
aapl.columns
aapl.head()
[*********************100%***********************]  1 of 1 completed
Price Close High Low Open Volume
Ticker AAPL AAPL AAPL AAPL AAPL
Date
2024-04-11 173.461349 173.877575 166.643409 166.821778 91070300
2024-04-12 174.957733 176.751407 172.638841 172.688378 101670900
2024-04-15 171.132553 175.037022 170.944264 173.778471 73531800
2024-04-16 167.852402 172.192889 166.752412 170.201022 73711200
2024-04-17 166.484833 169.110927 166.484833 168.080313 50901200
Thinking

The user asked to load AAPL stock prices for the last 2 years using yfinance. Looking at the output, the data has been successfully loaded:

  1. The download completed (100%)
  2. The data shows OHLCV (Open, High, Low, Close, Volume) data for AAPL
  3. The data starts from 2024-04-11 and appears to have MultiIndex columns (Price/Ticker structure)

The goal was simply to "load AAPL stock prices for the last 2 years using yfinance" - this has been accomplished. The output shows:

  • Data is loaded with Date as index
  • Columns include Close, High, Low, Open, Volume (with MultiIndex structure)
  • Sample data from April 2024 is visible

This satisfies the original goal. I should provide a natural final answer summarizing what was loaded and mark it as complete with <!--final answer-->.

However, I notice the output shows the data starts from 2024-04-11, which seems like recent data. The user asked for "last 2 years" - I should verify this is actually 2 years of data. The shape wasn't displayed in the output (only the head was shown). Let me check if I should verify the date range.

Actually, looking more carefully at the output, I can see the data structure but not the full date range or total number of rows. The goal was to "load" the data, which has been done. But to confirm it's actually 2 years of data, I could show the date range.

Since the original goal was just to "load" the data (not analyze or verify), and the data is clearly loaded with the expected structure, I think Path A is appropriate here. The loading is complete and the output confirms AAPL stock price data is available.

Let me provide a concise final answer summarizing what was loaded.

The AAPL stock data has been successfully loaded. The dataset contains daily price information with the following structure:

  • Columns: Close, High, Low, Open, and Volume prices
  • Index: Date (starting from 2024-04-11 in the preview)
  • Data type: MultiIndex columns (Price/Ticker structure from yfinance)

The data shows typical trading information - for example, on 2024-04-11 AAPL closed at 173.46 USD with a trading volume of 91,070,300 shares.

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

The user wants me to add 20-day and 50-day moving averages to the AAPL stock data and plot them. I'll need to:

  1. Calculate the 20-day and 50-day simple moving averages (SMA) on the Close price
  2. Plot the Close price along with both moving averages

I'll do this in one code block since it's a straightforward task - calculate the SMAs and create a visualization. I'll use matplotlib/seaborn for the plotting.

Let me write clean, focused code for this step.

I'll calculate the 20-day and 50-day moving averages on the closing price and create a visualization showing all three lines.

Plot stock price with moving averages
import matplotlib.pyplot as plt

# Calculate moving averages
aapl['SMA_20'] = aapl['Close']['AAPL'].rolling(window=20).mean()
aapl['SMA_50'] = aapl['Close']['AAPL'].rolling(window=50).mean()

# Plot closing price with moving averages
plt.figure(figsize=(14, 6))
plt.plot(aapl.index, aapl['Close']['AAPL'], label='Close Price', linewidth=1.5, alpha=0.8)
plt.plot(aapl.index, aapl['SMA_20'], label='20-Day SMA', linewidth=2)
plt.plot(aapl.index, aapl['SMA_50'], label='50-Day SMA', linewidth=2)

plt.title('AAPL Stock Price with Moving Averages (Last 2 Years)', fontsize=14)
plt.xlabel('Date')
plt.ylabel('Price (USD)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Thinking

The user's original goal was to "add 20-day and 50-day moving averages and plot" for AAPL stock prices.

Looking at the notebook state:

  • Cell-1: Successfully downloaded AAPL data for 2 years using yfinance
  • Cell-2: Added SMA_20 and SMA_50 columns and created a plot

The output shows:

  • The data was loaded successfully with Close, High, Low, Open, Volume columns
  • The moving averages were calculated
  • A figure was generated (Figure size 1400x600 with 1 Axes)

However, I notice the output only shows <Figure size 1400x600 with 1 Axes> - this is just the figure object representation, not the actual rendered chart. In a typical Jupyter notebook, the chart would be displayed visually, but here we only see the text representation.

According to the sufficiency heuristics:

  • "If the latest output is chart-only/visual-only and does not include underlying numeric/text values, choose Path B first to print the values used in the chart."

Since we only have the figure object representation without seeing the actual plotted values, I should choose Path B to show the underlying data (the moving average values) to confirm the calculations are correct and provide concrete insights.

Let me provide a code block that displays some sample values of the moving averages to verify the calculations and give the user concrete numbers to work with.

The moving averages have been calculated and the plot was generated. However, to provide concrete insights from the calculations, let me display the actual moving average values alongside the closing prices.

Show moving average samples
# Show sample of data with moving averages
aapl[['Close', 'SMA_20', 'SMA_50']].tail(10)
Price Close SMA_20 SMA_50
Ticker AAPL
Date
2026-03-27 248.800003 255.375999 260.207576
2026-03-30 246.630005 254.471500 259.980804
2026-03-31 253.789993 253.973499 259.950782
2026-04-01 255.630005 253.629000 260.133994
2026-04-02 255.919998 253.410500 260.304025
2026-04-06 258.859985 253.480499 260.518867
2026-04-07 253.500000 253.161499 260.632705
2026-04-08 258.899994 253.064999 260.707280
2026-04-09 260.489990 253.048999 260.756509
2026-04-10 260.480011 253.285000 260.842104
Thinking

The user's goal was to add 20-day and 50-day moving averages and plot them. Looking at the notebook outputs:

  1. Cell-1: Downloaded AAPL stock data for 2 years
  2. Cell-2: Calculated SMA_20 and SMA_50, and created a plot (figure was generated)
  3. Cell-3: Displayed the last 10 rows showing Close, SMA_20, and SMA_50 values

The outputs show:

  • The moving averages were successfully calculated
  • A plot was generated (Figure size 1400x600)
  • The tail data shows actual numeric values for Close, SMA_20, and SMA_50

From the tail output, I can see concrete insights:

  • Recent closing prices range from ~246-260 USD
  • The 20-day SMA is around 253-255 USD
  • The 50-day SMA is around 259-260 USD
  • The current price (260.48 USD on 2026-04-10) is above the 20-day SMA but slightly below the 50-day SMA

This indicates the stock is in a short-term uptrend (price > 20-day SMA) but the longer-term trend (50-day SMA) is still slightly above current price.

The goal has been achieved - both moving averages were calculated and plotted. The outputs include both the visualization and the underlying numeric values. This is sufficient for Path A.

The moving averages have been successfully calculated and visualized. From the latest data (April 2026):

  • Current Close Price: 260.48 USD
  • 20-Day SMA: 253.29 USD
  • 50-Day SMA: 260.84 USD

Key Insights:

  1. The stock price (260.48 USD) is trading above the 20-day moving average (253.29 USD), indicating short-term bullish momentum
  2. The price is slightly below the 50-day moving average (260.84 USD), suggesting the longer-term trend is still consolidating
  3. The 20-day SMA is below the 50-day SMA, which typically indicates the stock hasn't fully confirmed a long-term uptrend yet

This setup shows AAPL is in a transitional phase - recovering in the short term but still working to break above the longer-term average.

calculate daily returns and plot their distribution
Thinking

The user wants to calculate daily returns and plot their distribution. I need to:

  1. Calculate daily returns from the Close price
  2. Plot the distribution (histogram) of daily returns

Let me start with calculating daily returns and showing a sample, then in the next step I can plot the distribution. Actually, I should do both in one step since it's a straightforward calculation and visualization.

Let me calculate the daily returns using pct_change() and then create a histogram to show the distribution.

I'll calculate daily returns from the closing prices and plot their distribution.

Plot daily returns distribution
import matplotlib.pyplot as plt
import seaborn as sns

# Calculate daily returns (percentage change)
aapl['Daily_Return'] = aapl['Close']['AAPL'].pct_change()

# Plot distribution of daily returns
plt.figure(figsize=(10, 6))
sns.histplot(aapl['Daily_Return'].dropna(), bins=50, kde=True, color='steelblue')
plt.title('Distribution of AAPL Daily Returns (Last 2 Years)', fontsize=14)
plt.xlabel('Daily Return')
plt.ylabel('Frequency')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Thinking

The user's original goal was to "calculate daily returns and plot their distribution". Looking at the notebook state:

  1. Cell-4 executed the daily returns calculation and plotted the distribution
  2. Output-4 shows a figure was created (Figure size 1000x600 with 1 Axes)

However, I notice that the output only shows the figure object, not the actual chart visualization or any numeric statistics about the daily returns. According to the sufficiency heuristics:

  • "If the latest output is chart-only/visual-only and does not include underlying numeric/text values, choose Path B first to print the values used in the chart."

The current output doesn't show:

  1. The actual histogram visualization (just the figure object reference)
  2. Any numeric statistics about the daily returns (mean, std, min, max, etc.)

To properly answer the goal and provide concrete insights, I should display some summary statistics of the daily returns along with potentially showing the actual plot. Let me choose Path B to get the underlying numeric values that would help interpret the distribution.

Actually, looking more carefully - the output shows "<Figure size 1000x600 with 1 Axes>" which indicates the plot was created successfully. In a real notebook environment, this would display the actual chart. However, I don't have visibility into the actual chart content or the numeric statistics.

To provide meaningful insights about the daily returns distribution, I should calculate and display summary statistics (mean, std, min, max, skewness, etc.) which would help explain what the distribution looks like numerically.

Let me choose Path B to get these statistics.

The daily returns have been calculated and the distribution plot was created. However, to provide concrete insights about the returns distribution, I need to see the underlying statistics.

Display daily return statistics
# Display summary statistics for daily returns
aapl['Daily_Return'].describe()
count    500.000000
mean       0.000971
std        0.017831
min       -0.092456
25%       -0.006737
50%        0.001400
75%        0.007868
max        0.153289
Name: Daily_Return, dtype: float64
Thinking

The user's original goal was to "calculate daily returns and plot their distribution". Looking at the notebook outputs:

  1. Cell-4: Successfully calculated daily returns and plotted their distribution (histogram with KDE)
  2. Cell-5: Displayed summary statistics for daily returns

The outputs show:

  • count: 500 observations
  • mean: 0.000971 (about 0.1% average daily return)
  • std: 0.017831 (about 1.78% daily volatility)
  • min: -0.092456 (worst day: -9.25%)
  • max: 0.153289 (best day: +15.33%)
  • 25th percentile: -0.67%
  • median (50%): 0.14%
  • 75th percentile: 0.79%

Both the visualization (histogram with KDE) and the numerical summary statistics are available. The original goal has been fully addressed - daily returns were calculated and their distribution was plotted. The summary statistics provide concrete insights about the distribution.

This meets the criteria for Path A - the goal is complete with both visual and numeric evidence available.

The daily returns calculation and distribution plot are complete. Based on the summary statistics:

Key Insights:

  • Average daily return: 0.097% (slightly positive)
  • Daily volatility: 1.78% (standard deviation)
  • Median daily return: 0.14% (half the days were above this)
  • Worst single day: -9.25%
  • Best single day: +15.33%
  • Typical range: 75% of daily returns fall between -0.67% and +0.79%

The distribution shows AAPL has relatively modest typical daily moves, with occasional extreme outliers on both sides. The positive mean and median suggest a slight upward bias over the 2-year period.

compute 30-day rolling volatility and annualised Sharpe ratio
Thinking

The user wants to compute:

  1. 30-day rolling volatility
  2. Annualized Sharpe ratio

I'll start with calculating the 30-day rolling volatility first. The Sharpe ratio calculation will be a follow-up step.

For rolling volatility, I'll use the standard deviation of daily returns over a 30-day window, then annualize it (multiply by sqrt(252) for trading days).

Let me write code for the 30-day rolling volatility first.

I'll compute the 30-day rolling volatility first, then the annualized Sharpe ratio.

Calculate 30-day rolling volatility
# Calculate 30-day rolling volatility (annualized)
# Volatility = std of daily returns * sqrt(252 trading days)
aapl['Rolling_Vol_30'] = aapl['Daily_Return'].rolling(window=30).std() * (252 ** 0.5)

# Display recent volatility values
aapl[['Daily_Return', 'Rolling_Vol_30']].tail(10)
Price Daily_Return Rolling_Vol_30
Ticker
Date
2026-03-27 -0.016173 0.221966
2026-03-30 -0.008722 0.213939
2026-03-31 0.029031 0.210536
2026-04-01 0.007250 0.211805
2026-04-02 0.001134 0.208117
2026-04-06 0.011488 0.205846
2026-04-07 -0.020706 0.212773
2026-04-08 0.021302 0.211730
2026-04-09 0.006141 0.211192
2026-04-10 -0.000038 0.211038
Thinking

Looking at the notebook state, I can see:

  1. Daily returns have been calculated (cell-4, cell-5)
  2. 30-day rolling volatility has been calculated (cell-6) - this shows the rolling volatility values

However, the original user goal asks for:

  1. 30-day rolling volatility ✓ (calculated in cell-6)
  2. Annualized Sharpe ratio ✗ (NOT calculated yet)

The Sharpe ratio has not been computed yet. I need to calculate it. The Sharpe ratio formula is:

  • Sharpe Ratio = (Mean Return - Risk-Free Rate) / Standard Deviation of Returns
  • Annualized Sharpe Ratio = Daily Sharpe Ratio * sqrt(252)

Since we don't have a risk-free rate specified, I'll assume 0 or use a typical proxy. I should calculate this next.

This is Path B - the goal is not yet complete because the Sharpe ratio hasn't been calculated.

The 30-day rolling volatility is now calculated, showing recent volatility around 21-22% (annualized). However, the annualized Sharpe ratio hasn't been computed yet.

Calculate annualized Sharpe ratio
# Calculate annualized Sharpe ratio
# Assuming risk-free rate of 0 for simplicity (or can use Treasury yield)
risk_free_rate = 0.0
daily_mean_return = aapl['Daily_Return'].mean()
daily_std_return = aapl['Daily_Return'].std()

# Daily Sharpe ratio
daily_sharpe = (daily_mean_return - risk_free_rate) / daily_std_return

# Annualized Sharpe ratio
annualized_sharpe = daily_sharpe * (252 ** 0.5)

# Display results
print(f"Daily Mean Return: {daily_mean_return:.6f}")
print(f"Daily Std Dev: {daily_std_return:.6f}")
print(f"Daily Sharpe Ratio: {daily_sharpe:.4f}")
print(f"Annualized Sharpe Ratio: {annualized_sharpe:.4f}")
Daily Mean Return: 0.000971
Daily Std Dev: 0.017831
Daily Sharpe Ratio: 0.0544
Annualized Sharpe Ratio: 0.8641
Thinking

The user's original goal was to compute 30-day rolling volatility and annualized Sharpe ratio. Looking at the notebook outputs:

  1. 30-day rolling volatility (cell-6 output): Successfully computed and displayed. Recent values show volatility ranging from about 20.58% to 22.20% (annualized).

  2. Annualized Sharpe ratio (cell-7 output): Successfully computed and displayed:

    • Daily Mean Return: 0.000971
    • Daily Std Dev: 0.017831
    • Daily Sharpe Ratio: 0.0544
    • Annualized Sharpe Ratio: 0.8641

Both metrics have been computed and the outputs are visible. The goal is complete with concrete numeric values. This is a clear Path A situation - I should provide a natural final answer summarizing the results based on the observed outputs.

Both metrics have been successfully computed:

30-Day Rolling Volatility (Annualized) The rolling volatility shows AAPL's risk level over time. Recent values (March-April 2026) range from 20.58% to 22.20%, indicating moderate volatility. The trend shows volatility decreasing slightly from 22.20% on March 27 to 21.10% on April 10.

Annualized Sharpe Ratio

  • 0.86 (assuming 0% risk-free rate)

This means AAPL generated approximately 0.86 units of excess return per unit of risk over the 2-year period. A Sharpe ratio above 0.5 is generally considered acceptable, and above 1.0 is good. At 0.86, AAPL shows decent risk-adjusted performance.

Key Insights:

  • Average daily return was 0.097% (about 24% annualized before risk adjustment)
  • Daily standard deviation was 1.78%
  • The positive Sharpe ratio indicates returns exceeded the risk-free rate on a risk-adjusted basis
  • Current volatility (~21%) is slightly elevated compared to long-term market averages (~15-18%)

Score cards for qwen3.5:397b for Apple Stock Price Analysis in Python

Total score: 7/10

  • Task Completion: 2/2

    Excellent

    All major steps were completed: data loading, moving averages, daily returns distribution, rolling volatility, and Sharpe ratio.

  • Execution Correctness: 1/2

    Needs work

    The workflow appears mostly runnable, but it uses MultiIndex yfinance columns inconsistently and the notebook evidence is truncated, so correctness cannot be fully verified.

  • Output Quality: 3/3

    Excellent

    The outputs match the expected outcomes semantically: OHLCV data, moving-average plot, return distribution summary, volatility values, and a printed Sharpe ratio.

  • Reasoning Quality: 1/2

    Needs work

    The reasoning is generally sensible, but it includes unnecessary self-correction and some unsupported claims about chart visibility and trend interpretation.

  • Reliability: 0/1

    Needs work

    The workflow is somewhat fragile due to inconsistent handling of MultiIndex columns and reliance on truncated notebook evidence.

Try MLJAR Studio

Run the same type of AI-powered data analysis on your own datasets with conversational notebooks in MLJAR Studio.

Explore More AI Analysis Examples

Discover additional workflows across categories. Each example includes prompts, conversation outputs, and model-level scoring so you can compare approaches and results.