Stock Market Analytics Pipeline

Fetch, store, and analyze real stock data with Python and SQLite.

Introduction

30 Second Summary

Stock prices change every second. Quant engineers at hedge funds and banks write code to analyze these patterns, calculate risk, and make data-driven investment decisions.

In this project, you will build a Python analytics pipeline that fetches real stock market data, stores it in a database, computes quantitative risk and return metrics, and generates professional visualizations.

What You'll Build

A Python-based stock market analytics pipeline that downloads historical data for 4 tech stocks, stores it in a SQLite database, computes risk/return metrics, and produces three visualization charts.

By the end of this project, you'll have:

  • A working Python script (stock_analytics.py) that fetches real stock market data using yfinance and stores it in a SQLite database.
  • Calculated core quant metrics (daily returns, annualized volatility, Sharpe ratio, and correlation) for multiple stocks.
  • Three professional visualizations (price chart, return distribution histogram, and correlation heatmap) saved as PNG files.
  • Secret Mission: Build an equal-weight portfolio and compare its risk-adjusted performance against individual stocks.

Are there any prerequisites?

This project assumes basic Python knowledge (variables, functions, loops) and basic SQL knowledge (CREATE TABLE, INSERT, SELECT). You don't need any prior finance experience.

Set Up Your Quant Python Environment

Before you can analyze stock market data, you need the right toolkit installed.

In this step, you'll install four Python libraries that handle everything from downloading live market data to generating professional charts. You'll also create the main script file that will power your entire analytics pipeline.

In this step, get ready to:
  • Verify Python 3.11+ is installed.
  • Install yfinance, pandas, numpy, and matplotlib.
  • Create stock_analytics.py with imports and configuration constants.
Verify your Python version

The libraries you'll install require Python 3.11 or higher. Let's confirm your version before installing anything.

  • Open the integrated terminal in Google Antigravity by pressing Ctrl+` (macOS) or Ctrl+` (Windows).
  • Check your Python version by running this command:
python --version

You should see a version number like Python 3.11.x or higher printed in the terminal.

Seeing a version lower than 3.11?

The libraries in this project (specifically pandas and numpy) require Python 3.11 or higher. If your version is lower, update Python through your system's package manager or download the latest version from the official Python website.

Help me update Python to 3.11 or higher on my system.

Install the required libraries

Your analytics pipeline needs four external libraries. yfinance downloads real stock market data. pandas organizes data into tables. numpy handles math operations. matplotlib generates charts.

  • Install all four libraries by running this command:
pip install yfinance==1.4.1 pandas==3.0.3 numpy==2.4.6 matplotlib==3.10.9

Why pin specific versions?

The == syntax locks each library to an exact version. This ensures your script behaves identically regardless of when you run it.

  • Verify everything installed correctly by running this command:
python -c "import yfinance, pandas, numpy, matplotlib; print('All libraries installed successfully')"

You should see All libraries installed successfully printed in the terminal.

Seeing a ModuleNotFoundError?

Make sure the install command completed without errors. Scroll up in the terminal to check for any red text during installation.

If pip is not recognized, try running pip3 instead.

Help me fix a library installation error in my Python environment.

Create your analytics script with imports and configuration

Every quant script starts with imports (the libraries you'll use) and configuration constants (the settings that control the analysis). Keeping configuration at the top makes it easy to change your analysis parameters later.

  • Create a new file called stock_analytics.py by clicking File in the top menu bar, then New File.
  • Save the file immediately as stock_analytics.py by pressing Cmd+S (macOS) or Ctrl+S (Windows) and typing the filename.
  • Add the import statements by copying this code into the file:
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import sqlite3
from datetime import datetime, timedelta

What do these imports do?

  • yfinance as yf downloads historical stock prices from Yahoo Finance.
  • pandas as pd organizes data into DataFrames (like spreadsheets in code).
  • numpy as np provides math functions like square root for volatility calculations.
  • matplotlib.pyplot as plt creates charts and saves them as image files.
  • sqlite3 is Python's built-in SQLite database driver. No installation needed.
  • datetime and timedelta let you calculate date ranges (like "1 year ago from today").
  • Add the configuration constants below your imports by copying this code:
# Configuration
TICKERS = ["AAPL", "MSFT", "GOOGL", "AMZN"]
END_DATE = datetime.now()
START_DATE = END_DATE - timedelta(days=365)
RISK_FREE_RATE = 0.05
DB_NAME = "market_data.db"

What do these constants control?

  • TICKERS lists the four tech stocks you'll analyze: Apple, Microsoft, Google, and Amazon.
  • START_DATE and END_DATE define a 1-year window ending today.
  • RISK_FREE_RATE is set to 0.05 (5%), representing the return you'd get from a "risk-free" US Treasury bond. You'll use this to calculate the Sharpe ratio later.
  • DB_NAME is the filename for your SQLite database where stock prices get stored.
  • Save stock_analytics.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Verify the file has no syntax errors by running this in the terminal:
python stock_analytics.py

The script should finish with no output and no errors. A clean exit means all imports resolved and your configuration is valid.

Seeing a SyntaxError or ImportError?

Double-check that each import line matches the code above exactly. A missing comma in the TICKERS list or a typo in a library name will cause an error.

Help me fix an error when running my stock_analytics.py script.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here's what your complete stock_analytics.py should look like at this point:

import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import sqlite3
from datetime import datetime, timedelta

# Configuration
TICKERS = ["AAPL", "MSFT", "GOOGL", "AMZN"]
END_DATE = datetime.now()
START_DATE = END_DATE - timedelta(days=365)
RISK_FREE_RATE = 0.05
DB_NAME = "market_data.db"

Your environment is locked and loaded. Next up, you'll use these libraries to download real stock market data and store it in a database.

Fetch Stock Data and Store in SQLite

Your Python environment is set up with all the libraries you need. Now it's time to pull real market data and build a database around it.

Every quant analysis starts with data acquisition. In this step, you'll download one year of historical stock prices for four tech giants, then persist that data in a SQLite database so you can query it with SQL later.

In this step, get ready to:
  • Download 1 year of closing prices for AAPL, MSFT, GOOGL, and AMZN.
  • Create a SQLite database and store the price data in a structured table.
  • Verify the data loaded correctly by querying row counts per ticker.
Fetch closing prices from the market

The yfinance library connects to Yahoo Finance and returns historical price data as a pandas DataFrame. You'll use it to grab adjusted closing prices for all four tickers in one call.

  • In stock_analytics.py, add the following code below the DB_NAME = "market_data.db" line:
# Step 2: Fetch stock data
print("Downloading stock data...")
data = yf.download(TICKERS, start=START_DATE.strftime("%Y-%m-%d"), end=END_DATE.strftime("%Y-%m-%d"))
close_prices = data["Close"]
print(f"Downloaded {len(close_prices)} trading days of data.\n")

What does this code do?

yf.download() fetches historical market data for all four tickers in a single network call. It returns a DataFrame with multi-level columns, one per data type (Open, High, Low, Close, Volume).

data["Close"] extracts just the closing prices into a simpler DataFrame where each column is a ticker symbol. The closing price is the standard reference point for daily return calculations in quantitative finance.

  • Save stock_analytics.py.
  • Run the script in your terminal by running:
python stock_analytics.py
  • You should see output like Downloading stock data... followed by Downloaded 250 trading days of data. (the exact number depends on market calendar).

Seeing a network or download error?

  • Make sure you have an active internet connection. yfinance pulls data from Yahoo Finance servers.
  • Double-check that you installed yfinance correctly in Step 1. Run pip install yfinance==1.4.1 again to confirm.
  • If you see a warning about missing data for one ticker, check the ticker symbols match exactly: AAPL, MSFT, GOOGL, AMZN.

Help me debug a yfinance download error in my stock analytics script.

Create the database and store price data

Now you'll persist the downloaded prices into a SQLite database file. This lets you query the data with SQL and keeps it available even after the script finishes running.

  • Below the fetch code you just added, add the database connection and table creation:
# Store in SQLite database
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()

cursor.execute("""
    CREATE TABLE IF NOT EXISTS stock_prices (
        date TEXT,
        ticker TEXT,
        close_price REAL,
        PRIMARY KEY (date, ticker)
    )
""")

What does this code do?

sqlite3.connect(DB_NAME) opens (or creates) a database file called market_data.db in your project folder. The cursor object lets you execute SQL statements against this database.

The CREATE TABLE IF NOT EXISTS statement defines the table schema with three columns. The composite primary key on (date, ticker) ensures you never store duplicate entries for the same stock on the same day.

  • Save stock_analytics.py and run the script again.
  • Confirm that market_data.db now appears in your project folder by running:
ls
  • You should see market_data.db alongside stock_analytics.py in the output.

Don't see market_data.db?

  • Make sure you saved the file before running. Unsaved edits won't execute.
  • Check that your terminal is in the same folder as stock_analytics.py. Run ls to confirm you can see the script file.

Help me figure out why market_data.db isn't being created when I run my script.

The database exists, but it's empty. Next you'll loop through every row of downloaded price data and insert it into the table.

  • Below the CREATE TABLE statement, add the data insertion loop:
for date_idx in close_prices.index:
    for ticker in TICKERS:
        price = close_prices.loc[date_idx, ticker]
        if pd.notna(price):
            cursor.execute(
                "INSERT OR REPLACE INTO stock_prices (date, ticker, close_price) VALUES (?, ?, ?)",
                (date_idx.strftime("%Y-%m-%d"), ticker, float(price))
            )

conn.commit()

What does this code do?

The nested loop walks through every date and ticker combination in the DataFrame. For each row, it checks pd.notna(price) to skip any missing data (weekends and holidays produce NaN values).

INSERT OR REPLACE ensures that re-running the script updates existing rows instead of crashing with a duplicate key error. The conn.commit() call writes all pending changes to disk in one batch.

  • Save stock_analytics.py.

You won't see new output yet. The next section adds a verification query that proves the data stored correctly.

Verify the stored data

The best way to confirm your database loaded correctly is to query it. You'll run a SQL GROUP BY query that counts how many rows exist for each ticker.

  • Below the conn.commit() line, add the verification query and close the connection:
# Verify data
print("Data stored in SQLite database:")
cursor.execute("SELECT ticker, COUNT(*) as rows FROM stock_prices GROUP BY ticker")
for row in cursor.fetchall():
    print(f"  {row[0]}: {row[1]} rows")
print()

conn.close()

What does this code do?

The SELECT ... GROUP BY ticker query counts rows per stock symbol. Each ticker should have approximately 250 rows, one for each trading day in the past year.

conn.close() releases the database file so other programs can access it. Always close database connections when you're done writing.

  • Save stock_analytics.py and run the script:
python stock_analytics.py
  • You should see the download message followed by a table showing each ticker with approximately 250 rows:
Downloading stock data...
Downloaded 251 trading days of data.

Data stored in SQLite database:
  AAPL: 251 rows
  AMZN: 251 rows
  GOOGL: 251 rows
  MSFT: 251 rows

Why approximately 250 rows?

US stock markets are open roughly 252 days per year. Your exact number will vary depending on when you run the script, because it pulls the most recent 365 calendar days. Any number between 240 and 260 is normal.

Seeing 0 rows or an error?

  • If you see 0 rows, the download may have failed silently. Check that the yf.download() line printed a non-zero number of trading days.
  • If you see a syntax error, check that the triple-quoted SQL string in cursor.execute() has matching """ on both sides.
  • If you see a database is locked error, make sure no other program has market_data.db open. Close any DB browser tools and try again.

Help me debug why my stock data isn't being stored in SQLite correctly.

✔️ Awesome, I've got everything!

Great. Double check you've saved stock_analytics.py and that your terminal output shows row counts for all four tickers.

ⓧ I'd like to double check the full code

Here's the complete stock_analytics.py file at this point:

import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import sqlite3
from datetime import datetime, timedelta

# Configuration
TICKERS = ["AAPL", "MSFT", "GOOGL", "AMZN"]
END_DATE = datetime.now()
START_DATE = END_DATE - timedelta(days=365)
RISK_FREE_RATE = 0.05
DB_NAME = "market_data.db"

# Step 2: Fetch stock data
print("Downloading stock data...")
data = yf.download(TICKERS, start=START_DATE.strftime("%Y-%m-%d"), end=END_DATE.strftime("%Y-%m-%d"))
close_prices = data["Close"]
print(f"Downloaded {len(close_prices)} trading days of data.\n")

# Store in SQLite database
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()

cursor.execute("""
    CREATE TABLE IF NOT EXISTS stock_prices (
        date TEXT,
        ticker TEXT,
        close_price REAL,
        PRIMARY KEY (date, ticker)
    )
""")

for date_idx in close_prices.index:
    for ticker in TICKERS:
        price = close_prices.loc[date_idx, ticker]
        if pd.notna(price):
            cursor.execute(
                "INSERT OR REPLACE INTO stock_prices (date, ticker, close_price) VALUES (?, ?, ?)",
                (date_idx.strftime("%Y-%m-%d"), ticker, float(price))
            )

conn.commit()

# Verify data
print("Data stored in SQLite database:")
cursor.execute("SELECT ticker, COUNT(*) as rows FROM stock_prices GROUP BY ticker")
for row in cursor.fetchall():
    print(f"  {row[0]}: {row[1]} rows")
print()

conn.close()

You've got a real market dataset stored in a queryable database. Next up, you'll calculate quantitative risk and return metrics from this data.

Calculate Returns and Risk Metrics

Your script now pulls real market data and stores it in a database. That is the data acquisition side of the pipeline done.

The next question a quant engineer asks is: how did each stock actually perform, and how risky was that performance? In this step, you will compute daily returns, annualize them into yearly figures, and calculate the Sharpe ratio for each stock.

In this step, get ready to:
  • Calculate daily percentage returns from closing prices.
  • Compute annualized return and annualized volatility for each stock.
  • Calculate the Sharpe ratio and print a formatted metrics table.
Calculate daily returns and annualized metrics

A daily return measures how much a stock's price changed from one trading day to the next, expressed as a fraction. To compare stocks on a yearly basis, you multiply the average daily return by 252 (the number of US trading days per year) and scale the standard deviation by the square root of 252.

  • In stock_analytics.py, find the line that says conn.close(). Add the following code below it:
# Step 3: Calculate returns and risk metrics
daily_returns = close_prices.pct_change().dropna()

annualized_returns = daily_returns.mean() * 252
annualized_volatility = daily_returns.std() * np.sqrt(252)

What does this code do?

  • close_prices.pct_change() computes the fractional change between each day's closing price and the previous day's. The first row becomes NaN because there is no prior day to compare against.
  • .dropna() removes that first NaN row so all remaining values are usable numbers.
  • Multiplying by 252 annualizes the average daily return. Multiplying the standard deviation by np.sqrt(252) annualizes the volatility. Both use 252 because US equity markets have approximately 252 trading days per year.
Calculate the Sharpe ratio and display results

The Sharpe ratio tells you how much return you earn per unit of risk. A higher Sharpe means better risk-adjusted performance. The formula is: (annualized return minus risk-free rate) divided by annualized volatility.

  • Add this code directly below the annualized_volatility line:
sharpe_ratios = (annualized_returns - RISK_FREE_RATE) / annualized_volatility

print("=" * 60)
print(f"{'Stock':<8}{'Ann. Return':<15}{'Ann. Volatility':<18}{'Sharpe Ratio':<12}")
print("=" * 60)
for ticker in TICKERS:
    print(f"{ticker:<8}{annualized_returns[ticker]:>10.2%}{'':>5}{annualized_volatility[ticker]:>10.2%}{'':>8}{sharpe_ratios[ticker]:>8.2f}")
print("=" * 60)
print()

What does this code do?

  • RISK_FREE_RATE is the 0.05 (5%) constant you set in your configuration section. It represents the return you could earn risk-free (approximating current US Treasury rates).
  • The Sharpe formula subtracts the risk-free rate from the annualized return, then divides by volatility. A Sharpe above 1.0 is generally considered good.
  • The print() block uses Python f-string formatting to align columns. :<8 means left-aligned in 8 characters. :>10.2% means right-aligned, 10 characters wide, formatted as a percentage with 2 decimal places.
  • Save stock_analytics.py.
  • Run the script in your terminal by running:
python stock_analytics.py

After the download and database output from Step 2, you should see a formatted table with columns for Stock, Ann. Return, Ann. Volatility, and Sharpe Ratio. Each of the 4 tickers appears on its own row with percentage values and a decimal Sharpe score.

Seeing a KeyError or empty table?

  • If you see KeyError on a ticker name, make sure the TICKERS list at the top of your file matches exactly: ["AAPL", "MSFT", "GOOGL", "AMZN"]. Ticker symbols are case-sensitive.
  • If the table prints but shows nan values, confirm that .dropna() is chained directly after .pct_change() with no space between them.
  • If you get an ImportError for numpy, re-run the install command from Step 1 and try again.

help me debug my stock analytics metrics calculation

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import sqlite3
from datetime import datetime, timedelta

# Configuration
TICKERS = ["AAPL", "MSFT", "GOOGL", "AMZN"]
END_DATE = datetime.now()
START_DATE = END_DATE - timedelta(days=365)
RISK_FREE_RATE = 0.05
DB_NAME = "market_data.db"

# Step 2: Fetch stock data
print("Downloading stock data...")
data = yf.download(TICKERS, start=START_DATE.strftime("%Y-%m-%d"), end=END_DATE.strftime("%Y-%m-%d"))
close_prices = data["Close"]
print(f"Downloaded {len(close_prices)} trading days of data.\n")

# Store in SQLite database
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()

cursor.execute("""
    CREATE TABLE IF NOT EXISTS stock_prices (
        date TEXT,
        ticker TEXT,
        close_price REAL,
        PRIMARY KEY (date, ticker)
    )
""")

for date_idx in close_prices.index:
    for ticker in TICKERS:
        price = close_prices.loc[date_idx, ticker]
        if pd.notna(price):
            cursor.execute(
                "INSERT OR REPLACE INTO stock_prices (date, ticker, close_price) VALUES (?, ?, ?)",
                (date_idx.strftime("%Y-%m-%d"), ticker, float(price))
            )

conn.commit()

# Verify data
print("Data stored in SQLite database:")
cursor.execute("SELECT ticker, COUNT(*) as rows FROM stock_prices GROUP BY ticker")
for row in cursor.fetchall():
    print(f"  {row[0]}: {row[1]} rows")
print()

conn.close()

# Step 3: Calculate returns and risk metrics
daily_returns = close_prices.pct_change().dropna()

annualized_returns = daily_returns.mean() * 252
annualized_volatility = daily_returns.std() * np.sqrt(252)
sharpe_ratios = (annualized_returns - RISK_FREE_RATE) / annualized_volatility

print("=" * 60)
print(f"{'Stock':<8}{'Ann. Return':<15}{'Ann. Volatility':<18}{'Sharpe Ratio':<12}")
print("=" * 60)
for ticker in TICKERS:
    print(f"{ticker:<8}{annualized_returns[ticker]:>10.2%}{'':>5}{annualized_volatility[ticker]:>10.2%}{'':>8}{sharpe_ratios[ticker]:>8.2f}")
print("=" * 60)
print()

Your pipeline now fetches data, stores it, and crunches the numbers. Next up, you will turn these raw metrics into professional visualizations that tell the story at a glance.

Visualize the Analysis

You now have hard numbers for each stock's risk and return profile. Numbers alone can be hard to interpret, especially when comparing multiple assets over time.

In this step, you will turn your analysis into three professional charts using matplotlib. These visualizations tell the story that raw metrics cannot: how prices diverged, how returns are distributed, and how correlated these stocks really are.

In this step, get ready to:
  • Create a normalized price performance chart comparing all 4 stocks.
  • Plot overlaid histograms showing the distribution of daily returns.
  • Build a correlation heatmap with annotated values.
Create the normalized price performance chart

Raw stock prices are misleading when comparing companies at different price levels. A $200 stock gaining $10 is very different from a $3000 stock gaining $10. Normalizing all prices to start at $1 puts every stock on the same scale so you can see which one actually grew the most.

  • In stock_analytics.py, scroll to the bottom of the file (after the metrics print statements).
  • Add the normalized price chart by copying this code at the end of the file:
# Step 4: Visualizations

# Chart 1: Normalized price performance
normalized_prices = close_prices / close_prices.iloc[0]

plt.figure(figsize=(10, 6))
for ticker in TICKERS:
    plt.plot(normalized_prices.index, normalized_prices[ticker], label=ticker)
plt.title("Normalized Price Performance (Starting at $1)")
plt.xlabel("Date")
plt.ylabel("Growth of $1")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("price_performance.png", dpi=150)
plt.close()
print("Saved: price_performance.png")

What does this code do?

The line close_prices / close_prices.iloc[0] divides every row by the first row's prices. This converts all prices to a "growth of $1" scale, so each stock starts at 1.0 on the chart.

The loop plots one line per ticker on the same figure. plt.savefig() writes the chart to a PNG file at 150 dpi, and plt.close() frees the figure's memory so it does not interfere with the next chart.

  • Save stock_analytics.py.
  • Run the script in your terminal by running:
python stock_analytics.py
  • You should see Saved: price_performance.png printed in the terminal, and a new price_performance.png file in your project folder.

Not seeing the PNG file?

  • Make sure you saved stock_analytics.py before running it.
  • Check that the indentation of the plt.plot() line is inside the for loop (one level indented), while plt.savefig() is outside the loop (no indentation).
  • If you see an error about close_prices not being defined, the script may have failed during the download step. Check your internet connection and re-run.

Why isn't my price chart being saved?

Create the return distribution histogram

A histogram of daily returns shows you how "spread out" a stock's movements are. Wider distributions mean more volatile stocks. Overlaying all four on the same plot makes the comparison instant.

  • Add the histogram code directly below the print("Saved: price_performance.png") line:
# Chart 2: Return distribution histogram
plt.figure(figsize=(10, 6))
for ticker in TICKERS:
    plt.hist(daily_returns[ticker], bins=50, alpha=0.5, label=ticker)
plt.title("Distribution of Daily Returns")
plt.xlabel("Daily Return")
plt.ylabel("Frequency")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("return_distribution.png", dpi=150)
plt.close()
print("Saved: return_distribution.png")

What does this code do?

The plt.hist() call plots a histogram for each stock's daily returns. The bins=50 parameter creates 50 buckets to group return values into.

Setting alpha=0.5 makes each histogram semi-transparent so you can see all four overlaid on each other. Without transparency, the last plotted stock would hide the others.

  • Save stock_analytics.py and run the script again:
python stock_analytics.py
  • You should now see Saved: return_distribution.png in the terminal output, and a second PNG file in your project folder.

Seeing a blank or empty histogram?

  • Make sure the histogram code is placed after the Step 3 metrics calculation. It depends on daily_returns being computed earlier in the script.
  • If you see a warning about NaN values, confirm that the .dropna() call is still present on the daily_returns line from Step 3.

Why is my histogram showing no data?

Build the correlation heatmap

A correlation matrix shows how closely two stocks move together. Values near 1.0 mean they tend to rise and fall in sync. This is critical information for portfolio construction because highly correlated stocks provide less diversification benefit.

  • Add the correlation heatmap code directly below the print("Saved: return_distribution.png") line:
# Chart 3: Correlation heatmap
correlation_matrix = daily_returns.corr()

fig, ax = plt.subplots(figsize=(8, 6))
im = ax.imshow(correlation_matrix.values, cmap="RdYlGn", vmin=0, vmax=1)

ax.set_xticks(range(len(TICKERS)))
ax.set_yticks(range(len(TICKERS)))
ax.set_xticklabels(TICKERS)
ax.set_yticklabels(TICKERS)

for i in range(len(TICKERS)):
    for j in range(len(TICKERS)):
        ax.text(j, i, f"{correlation_matrix.values[i, j]:.2f}",
                ha="center", va="center", fontsize=12, fontweight="bold")

plt.title("Correlation Matrix of Daily Returns")
plt.colorbar(im)
plt.tight_layout()
plt.savefig("correlation_heatmap.png", dpi=150)
plt.close()
print("Saved: correlation_heatmap.png")
print("\nAnalysis complete!")

What does this code do?

The daily_returns.corr() method computes pairwise correlation between every stock's daily returns. The result is a 4x4 matrix.

Instead of a line chart, this uses ax.imshow() to render the matrix as a colored grid. The RdYlGn colormap shades low correlations red and high correlations green.

The nested for loops write the exact correlation value on each cell, so you can read both the color and the number at a glance.

  • Save stock_analytics.py and run the script one final time:
python stock_analytics.py
  • You should see all three Saved: messages in the terminal, followed by Analysis complete!.
  • Confirm that three PNG files exist in your project folder by running:
ls *.png
  • You should see price_performance.png, return_distribution.png, and correlation_heatmap.png listed in the output.

Missing one or more PNG files?

  • If the heatmap file is missing, check that you used plt.subplots() (not plt.figure()) for Chart 3. The heatmap uses a different pattern because it needs direct axis control.
  • Look for a syntax error in the ax.text() line. The f-string formatting must be exactly f"{correlation_matrix.values[i, j]:.2f}" with the colon inside the braces.
  • Make sure each chart section ends with plt.close(). Without it, matplotlib can mix figures together.

Help me debug my correlation heatmap code.

What should I see in each chart?

  • Price performance: four diverging lines starting at $1. Some stocks will have grown more than others over the year.
  • Return distribution: roughly bell-shaped curves centered near zero. More volatile stocks have wider, flatter distributions.
  • Correlation heatmap: values between 0 and 1. Tech stocks typically show moderate to high correlation (0.5 to 0.8) since they respond to similar market forces.

✔️ Awesome, I've got everything!

Great. Double check you have saved stock_analytics.py and that all three PNG files are in your project folder.

ⓧ I'd like to double check the full code

Here is the complete stock_analytics.py file as it should look at the end of this step:

import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import sqlite3
from datetime import datetime, timedelta

# Configuration
TICKERS = ["AAPL", "MSFT", "GOOGL", "AMZN"]
END_DATE = datetime.now()
START_DATE = END_DATE - timedelta(days=365)
RISK_FREE_RATE = 0.05
DB_NAME = "market_data.db"

# Step 2: Fetch stock data
print("Downloading stock data...")
data = yf.download(TICKERS, start=START_DATE.strftime("%Y-%m-%d"), end=END_DATE.strftime("%Y-%m-%d"))
close_prices = data["Close"]
print(f"Downloaded {len(close_prices)} trading days of data.\n")

# Store in SQLite database
conn = sqlite3.connect(DB_NAME)
cursor = conn.cursor()

cursor.execute("""
    CREATE TABLE IF NOT EXISTS stock_prices (
        date TEXT,
        ticker TEXT,
        close_price REAL,
        PRIMARY KEY (date, ticker)
    )
""")

for date_idx in close_prices.index:
    for ticker in TICKERS:
        price = close_prices.loc[date_idx, ticker]
        if pd.notna(price):
            cursor.execute(
                "INSERT OR REPLACE INTO stock_prices (date, ticker, close_price) VALUES (?, ?, ?)",
                (date_idx.strftime("%Y-%m-%d"), ticker, float(price))
            )

conn.commit()

# Verify data
print("Data stored in SQLite database:")
cursor.execute("SELECT ticker, COUNT(*) as rows FROM stock_prices GROUP BY ticker")
for row in cursor.fetchall():
    print(f"  {row[0]}: {row[1]} rows")
print()

conn.close()

# Step 3: Calculate returns and risk metrics
daily_returns = close_prices.pct_change().dropna()

annualized_returns = daily_returns.mean() * 252
annualized_volatility = daily_returns.std() * np.sqrt(252)
sharpe_ratios = (annualized_returns - RISK_FREE_RATE) / annualized_volatility

print("=" * 60)
print(f"{'Stock':<8}{'Ann. Return':<15}{'Ann. Volatility':<18}{'Sharpe Ratio':<12}")
print("=" * 60)
for ticker in TICKERS:
    print(f"{ticker:<8}{annualized_returns[ticker]:>10.2%}{'':>5}{annualized_volatility[ticker]:>10.2%}{'':>8}{sharpe_ratios[ticker]:>8.2f}")
print("=" * 60)
print()

# Step 4: Visualizations

# Chart 1: Normalized price performance
normalized_prices = close_prices / close_prices.iloc[0]

plt.figure(figsize=(10, 6))
for ticker in TICKERS:
    plt.plot(normalized_prices.index, normalized_prices[ticker], label=ticker)
plt.title("Normalized Price Performance (Starting at $1)")
plt.xlabel("Date")
plt.ylabel("Growth of $1")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("price_performance.png", dpi=150)
plt.close()
print("Saved: price_performance.png")

# Chart 2: Return distribution histogram
plt.figure(figsize=(10, 6))
for ticker in TICKERS:
    plt.hist(daily_returns[ticker], bins=50, alpha=0.5, label=ticker)
plt.title("Distribution of Daily Returns")
plt.xlabel("Daily Return")
plt.ylabel("Frequency")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("return_distribution.png", dpi=150)
plt.close()
print("Saved: return_distribution.png")

# Chart 3: Correlation heatmap
correlation_matrix = daily_returns.corr()

fig, ax = plt.subplots(figsize=(8, 6))
im = ax.imshow(correlation_matrix.values, cmap="RdYlGn", vmin=0, vmax=1)

ax.set_xticks(range(len(TICKERS)))
ax.set_yticks(range(len(TICKERS)))
ax.set_xticklabels(TICKERS)
ax.set_yticklabels(TICKERS)

for i in range(len(TICKERS)):
    for j in range(len(TICKERS)):
        ax.text(j, i, f"{correlation_matrix.values[i, j]:.2f}",
                ha="center", va="center", fontsize=12, fontweight="bold")

plt.title("Correlation Matrix of Daily Returns")
plt.colorbar(im)
plt.tight_layout()
plt.savefig("correlation_heatmap.png", dpi=150)
plt.close()
print("Saved: correlation_heatmap.png")
print("\nAnalysis complete!")

Your pipeline now fetches data, stores it, crunches the numbers, and produces professional-grade charts. Up next in the Secret Mission, you will combine these individual stocks into a portfolio and see whether diversification actually reduces risk.

Secret mission

Build an Equal-Weight Portfolio Simulation

You've analyzed individual stocks. But real quant engineers think in portfolios. Can you combine all four stocks into a single equal-weight portfolio and prove whether diversification actually improves risk-adjusted returns?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project runs entirely locally, so there are no ongoing costs. The files and libraries stay on your machine until you remove them.

Resources you used:

  • stock_analytics.py (Python script).
  • market_data.db (SQLite database).
  • PNG visualization files (price_performance.png, return_distribution.png, correlation_heatmap.png, portfolio_comparison.png).
  • Python libraries installed via pip (yfinance, pandas, numpy, matplotlib).

Keep everything running

No action needed. Choose this if you're still actively building or want to experiment further with the analytics pipeline.

  • Your script, database, and PNG files remain in your project folder.
  • The installed Python libraries (yfinance, pandas, numpy, matplotlib) stay available for future projects.
  • You can re-run stock_analytics.py any time to refresh the data with the latest stock prices.

Pause - I'll come back to this later

Since everything is local files and installed libraries, there is nothing to pause. Your project is already in a paused state whenever you close Google Antigravity.

  • Close Google Antigravity. Your files are saved on disk.
  • When you come back, reopen the project folder and run stock_analytics.py again to refresh data.

Delete - I don't want to use this again

Remove all project files and uninstall the Python libraries to start fresh.

  • Delete the project files by running this in your terminal:
rm stock_analytics.py market_data.db price_performance.png return_distribution.png correlation_heatmap.png portfolio_comparison.png
  • Uninstall the Python libraries by running:
pip uninstall yfinance pandas numpy matplotlib -y
  • Confirm the files are gone by running:
ls

You should no longer see any of the project files in the output.

That's a Wrap!

That's a Wrap!

Nice work! You just built a complete stock market analytics pipeline that fetches real data, stores it in a database, computes quantitative metrics, and produces professional visualizations.

You've learned how to:

  • Fetch real historical stock data with yfinance and persist it in a SQLite database using Python's built-in sqlite3 module.
  • Calculate core quant metrics including daily returns, annualized volatility, and Sharpe ratio for multiple stocks.
  • Generate professional data visualizations with matplotlib: normalized price performance charts, return distribution histograms, and correlation heatmaps.
  • Secret Mission: Constructed an equal-weight portfolio and demonstrated the diversification benefit on risk-adjusted returns.

Ready to quiz yourself?