Tutorial

Stock beta: what it means and how to calculate it in Python

Beta is a slope: how far a stock tends to move when the market moves 1%. Compute it from daily bars in a few lines of Python, and see why two oil majors carried a negative one for most of 2026.

On this page
  1. What beta means, with a real stock in each range
  2. The beta formula
  3. Beta is a slope you can see
  4. Calculate beta in Python from daily bars
  5. Why two websites show different betas for the same stock
  6. Negative beta: why oil stocks moved against the market in 2026
  7. What beta does not tell you
  8. Questions

Key takeaways

  • Beta is the slope of a stock’s daily returns plotted against the market’s: 1.5 means it moved about 1.5% for each 1% move in the benchmark, and a negative beta means it tended to move the other way.
  • The formula is the covariance of stock and market returns divided by the variance of market returns, which equals correlation times the ratio of the two volatilities.
  • Over the year to 28 September 2026, US:GS had a beta of 1.59 against US500, while US:XOM (-0.56) and US:CVX (-0.49) moved against the index as oil climbed.
  • The same stock gets different betas from different choices: US:DIS measured 0.66 on daily returns and 1.18 on weekly returns over the same year.
  • Read beta next to R²: the index explained 41% of the daily moves in US:GS but only 3% of those in US:KO, so the beta of US:KO says very little.

Stock beta measures how much a stock’s price tends to move when the market moves. A beta of 1.0 means the stock rose or fell about 1% for each 1% move in the benchmark; 1.5 means about 1.5%; 0 means no consistent link; and a negative beta means it tended to move the other way. Statistically, beta is the slope of a straight line fitted through the stock’s daily returns plotted against the market’s.

This tutorial computes beta from scratch with daily bars from a stock API: ten large US stocks and three ETFs, each measured against US500, the US 500 large-cap benchmark. Every number comes from one year of sessions ending 28 September 2026. Four of the ten came out below zero, something textbooks treat as a rarity, and negative betas were in the news that week.

One-year beta against US500, to 28 September 2026

  1. -0.56US:XOM
  2. -0.23US:KO
  3. 0.03US:MCD
  4. 0.78US:JPM
  5. 1.00US500ETF
  6. 1.25US:BA
  7. 1.59US:GS
Daily returns over 251 sessions. At 1 a stock moves with the index; below 0 it moves against it.

What beta means, with a real stock in each range

  • Beta above 1Amplifies the market. US:GS measured 1.59: on a day the index fell 1%, it fell about 1.6% on average, and it rose about as much on the way up. Brokers, airlines and other cyclical businesses tend to land here.
  • Beta between 0 and 1Moves with the market, but less. US:JPM measured 0.78 and US:DIS 0.66. A stock here still follows the index; it just follows it at a smaller scale.
  • Beta near 0No consistent link to the index. US:MCD measured 0.03 and US:PG -0.04: over this year their daily moves had almost nothing to do with the market’s.
  • Beta below 0Tends to move against the market. US:XOM (-0.56) and US:CVX (-0.49) rose on many of the days the index fell, as the same oil spikes that hurt the index lifted the producers.

The beta formula

Beta compares how a stock and the market move together, their covariance, with how much the market moves on its own, its variance. Divide one by the other and you get the slope of the regression line. The second form below is the same number written with correlation and volatility, and it is the one that explains most surprises:

β = Cov(Rs, Rm) ÷ Var(Rm)β = ρ × σs ÷ σm

Rs
Daily returns of the stock: each close divided by the previous close, minus 1.
Rm
Daily returns of the market on the same dates, here US500.
ρ
Correlation between the two return series, from -1 to 1.
σs, σm
Volatility of the stock and of the market: the standard deviation of their returns.
US:GS, year to 28 September 2026: correlation 0.64, volatility 32.2% against 13.0% for the index, so beta = 0.64 × 32.2 ÷ 13.0 = 1.59.

A high beta needs two things at once: a strong correlation with the index and more volatility than the index. US:IBM was almost four times as volatile as US500 over the year, 49% against 13%, but its correlation was only 0.24, so its beta came out at 0.91. Volatile is not the same as high-beta.

Beta is a slope you can see

Plot every session as a dot, with the index’s return across and the stock’s return up, and beta is the slope of the best straight line through the cloud. Here are all 251 sessions for US:GS and US:XOM on one chart:

Daily returns against US500, 29 Sep 2025 to 28 Sep 2026

  • US:GS, beta 1.59
  • US:XOM, beta -0.56

Across: US500 daily return. Up: stock daily return.

Each dot is one session. The lines are least-squares fits, and their slopes are the betas.Daily closes from TickerLayer /indices/agg and /stocks/agg.

The cloud matters as much as the slope. The US:GS dots stay much closer to their line, so the index describes a good share of its moves. The US:XOM dots spread widely around a gentle downward line: the relationship is real but loose. How tightly the dots hug the line is what R² measures, and it decides how much weight a beta deserves.

Calculate beta in Python from daily bars

The script loads daily closes from the aggregate bars endpoint, joins each stock to the index on the dates both traded, turns closes into returns, and prints beta, correlation, R² and annualized volatility. It needs Python 3.9 or newer and pip install pandas requests, and it makes 14 requests: one per symbol and one for the index.

beta.pyPython
"""Beta of US stocks and ETFs against US500, from one year of daily bars."""
import math
import os

import pandas as pd
import requests

BASE_URL = "https://api.tickerlayer.com"
API_KEY = os.environ["TICKERLAYER_API_KEY"]
START, END = "2025-09-26", "2026-09-28"  # the first close, then one year of sessions


def daily_closes(asset, symbol):
    """Daily closes as a Series indexed by session date, following next_offset."""
    url = f"{BASE_URL}/{asset}/agg/{symbol}/1/day/{START}/{END}"
    params = {"sort": "asc", "limit": 5000, "offset": 0}
    rows = []
    while True:
        resp = requests.get(url, params=params, headers={"x-api-key": API_KEY}, timeout=15)
        resp.raise_for_status()
        body = resp.json()
        rows.extend(body["results"])
        if body.get("next_offset") is None:
            break
        params["offset"] = body["next_offset"]
    if not rows:
        raise SystemExit(f"No bars for {symbol}: check the symbol, the dates and your plan.")
    dates = pd.to_datetime([r["t"] for r in rows], unit="ms", utc=True).date
    return pd.Series([r["c"] for r in rows], index=dates, name=symbol)


def beta_stats(stock, market):
    """Beta, correlation, R² and annualized volatility of daily returns."""
    both = pd.concat([stock, market], axis=1, join="inner").pct_change(fill_method=None).dropna()
    s, m = both.iloc[:, 0], both.iloc[:, 1]
    corr = s.corr(m)
    return s.cov(m) / m.var(), corr, corr**2, s.std() * math.sqrt(252), len(both)


market = daily_closes("indices", "US500")
stocks = ["US:GS", "US:BA", "US:JPM", "US:IBM", "US:DIS", "US:MCD", "US:PG", "US:KO", "US:CVX", "US:XOM"]
etfs = ["US500ETF", "USGOLD", "USTBOND"]

rows = []
for asset, symbols in (("stocks", stocks), ("etfs", etfs)):
    for symbol in symbols:
        rows.append((symbol, *beta_stats(daily_closes(asset, symbol), market)))

print(f"{'symbol':<10}{'beta':>6}{'corr':>7}{'R²':>6}{'vol':>6}{'days':>6}")
for symbol, beta, corr, r2, vol, days in sorted(rows, key=lambda r: r[1], reverse=True):
    print(f"{symbol:<10}{beta:>6.2f}{corr:>7.2f}{r2:>6.2f}{vol:>6.0%}{days:>6}")
Output
symbol      beta   corr    R²   vol  days
US:GS       1.59   0.64  0.41   32%   251
US:BA       1.25   0.47  0.22   34%   251
US500ETF    1.00   1.00  0.99   13%   251
US:IBM      0.91   0.24  0.06   49%   251
USGOLD      0.80   0.35  0.12   30%   251
US:JPM      0.78   0.45  0.20   22%   251
US:DIS      0.66   0.32  0.10   27%   251
USTBOND     0.19   0.26  0.07   10%   251
US:MCD      0.03   0.02  0.00   19%   251
US:PG      -0.04  -0.03  0.00   20%   251
US:KO      -0.23  -0.16  0.03   19%   251
US:CVX     -0.49  -0.27  0.07   24%   251
US:XOM     -0.56  -0.28  0.08   26%   251

days counts daily returns: 252 closes from 26 September 2025 give 251 returns. The join on dates matters more than it looks. Join by position instead and one missing bar shifts every later return by a day, which quietly drags the beta toward zero.

US500ETF, a fund that tracks the index, is the built-in sanity check: a beta of 1.00 with an R² of 0.99 says the pipeline is right. The gold ETF USGOLD measured 0.80 and the long Treasury ETF USTBOND 0.19. In this window both moved with stocks more often than against them, so neither hedged the index, whatever their reputation.

Why two websites show different betas for the same stock

Look up one stock’s beta on two finance sites and the numbers often disagree. Neither is necessarily wrong: beta depends on choices that rarely appear next to the number. Here is the same year of data measured three ways:

StockDaily, 1 yearDaily, 6 monthsWeekly, 1 year
US:GS1.591.751.03
US:BA1.251.601.61
US:IBM0.910.531.44
US:DIS0.660.601.18
US:JPM0.780.570.58
US:KO-0.23-0.35-0.02
US:XOM-0.56-1.05-1.01
Beta against US500 to 28 September 2026. The weekly column uses each week’s last close: 53 returns instead of 251.
  • Return frequency. Daily, weekly or monthly. Weekly and monthly betas rest on far fewer points, and they catch stocks that react to the market a day late, which daily betas miss.
  • Window. One, two or five years. A longer window is steadier and slower to notice that a business has changed. US:XOM’s six-month beta is almost twice its one-year beta.
  • Benchmark. A broad US index, a sector index or a world index. Beta is always relative to something, and a stock can be high-beta against one benchmark and low against another.
  • Adjustment. Some publishers shrink raw betas toward 1.0 because extreme values tend not to last. The betas here are raw.
  • Prices. Raw daily bars need split adjustment before you compute returns, or a split shows up as a crash. The historical stock data guide shows the fix.

Pick one convention, write it next to every beta you publish, and never compare betas built differently.

Negative beta: why oil stocks moved against the market in 2026

Late September brought headlines about a record number of large US stocks with a negative beta. The oil majors show the mechanism. WTI crude rose from $65 to $93 a barrel over the year, peaking at $113 in early April and trading above $100 again in mid-September, and oil spikes were bad days for the broad index and good days for producers:

  • -0.37Correlation of US500 with WTI, daily returns
  • +0.61Correlation of US:XOM with WTI
  • -0.56Beta of US:XOM against US500
Year to 28 September 2026, 251 sessions, from /indices/agg, /stocks/agg and /commodities/agg daily bars.

A negative beta is not a permanent trait. In the first quarter of 2026 the 60-day beta of US:XOM hovered around zero. It turned negative in early April, after the oil spike, and has stayed below -0.5 since mid-April. A rolling window shows beta changing as the market’s main driver changes:

60-day rolling beta against US500

  • US:XOM
  • US:GS
  • US:KO
Each point is the beta of the 60 sessions ending that day, sampled every fifth session from 23 December 2025.Computed from TickerLayer daily bars.

The rolling version is two more lines on top of beta.py. rolling(60) keeps a moving window of 60 daily returns, and the ratio of the rolling covariance to the rolling variance is the beta of each window:

rolling_beta.pyPython
# Uses daily_closes() from beta.py.
market = daily_closes("indices", "US500")
stock = daily_closes("stocks", "US:XOM")
both = pd.concat([stock, market], axis=1, join="inner").pct_change(fill_method=None).dropna()
s, m = both.iloc[:, 0], both.iloc[:, 1]

rolling_beta = (s.rolling(60).cov(m) / m.rolling(60).var()).dropna()
print(rolling_beta.iloc[::21].round(2).to_string())
Output
2025-12-22    0.16
2026-01-23   -0.01
2026-02-24    0.11
2026-03-25    0.03
2026-04-24   -0.66
2026-05-26   -1.06
2026-06-25   -1.00
2026-07-27   -0.97
2026-08-25   -0.75
2026-09-24   -1.09

US:GS went the other way. Its 60-day beta climbed from 1.16 at the end of 2025 to 2.18 on 28 September 2026, its highest reading of the year, so the stock now amplifies the index more than its one-year figure of 1.59 suggests. Sixty sessions is about the shortest window that gives a usable beta; anything shorter mostly measures noise.

What beta does not tell you

Beta tells you

  • How far a stock moved with its benchmark, on average, in the window you chose
  • Which way it tended to go on the market’s big days
  • How sensitivity adds up: a portfolio’s beta is the value-weighted average of its holdings’ betas

Beta does not tell you

  • How volatile the stock is: US:IBM was the most volatile of the ten, with a beta under 1
  • Whether the market explains the stock at all: that is R²
  • What happens next: every beta is backward-looking, and one oil shock can reshape it

R² is the share of a stock’s daily variance that the index explains. US500ETF scores 0.99 because it is built to track the index. US:GS scores 0.41. US:KO scores 0.03, so its beta of -0.23 describes almost nothing: the stock rose 33% over the year while the index explained 3% of its day-to-day moves.

Single sessions matter less than you might expect. On 14 July 2026 US:IBM fell 25% while US500 rose 0.4%. Dropping that one day moves the one-year beta only from 0.91 to 0.95: a crash on a quiet market day lands far from the line but barely tilts it. It shows up in volatility and R² instead. A crash on a day the whole market falls is a different story, and that is why a beta measured across one market shock can look nothing like the next year’s.

Before you trust a beta

  • Stock and index joined on the dates both traded, never by row position
  • Closes adjusted for splits inside the window
  • At least 60 daily returns, and a year when you can get it
  • Clean bars: a stale or bad print becomes a fake return, as the bad ticks guide shows
  • R² published next to the beta
  • Window, return frequency and benchmark written next to the number

Beta is correlation scaled by relative volatility, so it pairs naturally with a correlation matrix in Python when you want to see how a whole basket moves together rather than one stock against one index.

Questions

What is a good beta for a stock?

There is no good or bad beta, only one that fits a purpose. A beta above 1 amplifies market moves in both directions, a beta below 1 dampens them, and a beta near 0 says the market does not explain the stock. Read it next to R² and the window it was measured over.

What does a beta of 1.5 mean?

Over the measurement window the stock moved about 1.5% for each 1% move in its benchmark, on average: up about 1.5% on a day the index rose 1%, down about 1.5% on a day it fell 1%. Single days can differ a lot; beta is the slope of the average relationship.

Can a stock have a negative beta?

Yes. A negative beta means the stock tended to rise when the market fell, and the other way round. Over the year to 28 September 2026, US:XOM had a beta of -0.56 against US500, as oil price spikes lifted it while weighing on the broad index.

How do you calculate beta?

Take daily returns of the stock and of the market on the same dates, then divide the covariance of the two by the variance of the market returns. In pandas that is stock.cov(market) / market.var() on two aligned return Series.

What is the difference between beta and correlation?

Correlation measures how consistently two return series move together, from -1 to 1. Beta also scales by relative volatility: it equals correlation times the stock’s volatility divided by the market’s, so a weakly correlated but very volatile stock can still have a beta near 1.

Is beta the same as volatility?

No. Volatility is how much a stock moves; beta is how much of that movement follows the market. US:IBM was the most volatile of the ten stocks in this guide at 49% a year, yet its beta was 0.91.

Keep reading

Ready to integrate?

Start with the free tier, explore the docs, and connect via REST or WebSocket in minutes.