Strasmore Research
Learn am Matt ConnorBy Matt Connor · Updated 2026-10-04 · data as of October 4, 2026 · refreshed weekly

Free Stock Market Data API for Python

Use requests and pandas call free stock market data API for Python on Ubuntu, load JSON into DataFrame, and print 20-day average volume.

To call free stock market data API from Python, na one HTTP request plus two libraries wey most people don already get: requests to fetch the JSON and pandas to turn am into DataFrame. No API key dey involved. This walkthrough go run from start to finish for fresh Ubuntu container, and e go end by printing the 20-session average volume for one ticker. The free stock market data API guide explain wetin the endpoints dey serve; this page na the Python side of the same API.

How to set up Python, requests and pandas for fresh Ubuntu container

Fresh Ubuntu image no dey come with Python venv module, and newer releases no dey allow make you install packages inside the system interpreter. Virtual environment dey solve both problems and e dey work the same way for every current Ubuntu release. Run these commands as root, or put sudo before the apt-get lines.

export DEBIAN_FRONTEND=noninteractive
apt-get update && apt-get install -y python3 python3-venv
python3 -m venv .venv
. .venv/bin/activate
pip install "requests>=2.31,<3" "pandas>=2.0,<4"

The version ranges get purpose. Each one dey accept any maintained release of the library and e exclude only future major version wey behaviour never clear. This one make the same line install cleanly as Ubuntu dey move its default Python forward. Nothing dey pin to a build wey only dey available today. Everything wey follow assume say the environment dey active.

How I fit call free stock market data API for Python?

The demo endpoints dey answer plain GET requests without key or signup. The first script dey ask for one of the curated queries and print the shape of wetin come back.

import json
import requests

BASE = "https://ai.strasmore.com/api/demo"

resp = requests.get(BASE, params={"q": "dividend_yield_leaders"}, timeout=30)
resp.raise_for_status()
payload = resp.json()

print(list(payload.keys()))
print(payload["columns"])
print(json.dumps(payload["rows"][0], indent=2))

requests.get dey build the query string from params, raise_for_status() dey turn any 4xx or 5xx status into exception, and .json() dey parse the body into dictionary. If we trim am to show only the shape, the dictionary look like this:

{
  "key":     "dividend_yield_leaders",
  "label":   "...",
  "nl":      "Which large-cap US stocks currently have the highest dividend yields?",
  "sql":     "SELECT ...",
  "columns": ["as_of", "ticker", "dividend_yield_pct", "price", "market_cap_bn", "price_to_earnings"],
  "rows":    [{"as_of": "<date>", "ticker": "<symbol>", "dividend_yield_pct": <number>, ...}, ...],
  "elapsed": "...",
  "source":  "Strasmore Research",
  "more":    "..."
}

Two keys matter for pandas. columns dey name the fields in order, while rows hold one JSON object for each record, with those names as keys. sql na the exact query wey produce the numbers. E dey return with every response, and na wetin make the data auditable instead of black box. GET request to /api/demo/catalog dey list every curated key plus the SQL endpoint wey come next.

How you fit load the JSON enter pandas DataFrame?

The curated queries dey answer fixed questions. Daily bars for any ticker wey you choose dey come from the no-signup SQL endpoint for /api/demo/sql. E dey take read-only query inside sql parameter and return the same columns and rows pair. As of September 2026, e limits dey show for every response: 500 rows, 20 seconds, one year of history, and no key. The free SQL API with key dey raise the limit to 100 queries per day across deeper history. The request code remain the same.

Two guards inside the SQL dey keep the average accurate. Table wey dey update during the session fit get more than one row for the current day. The volume for that day still partial while market dey open. The query stops at yesterday with date < today(). E also join any duplicate rows for one date with GROUP BY date and max(volume). If you never check how daily bar dey come from the tape, how dem dey build OHLCV bars explains where those rows come from.

import time
import requests
import pandas as pd

SQL_URL = "https://ai.strasmore.com/api/demo/sql"
TICKER = "AAPL"

SQL = f"""
SELECT date, max(volume) AS volume
FROM stocks_daily_aggs
WHERE ticker = '{TICKER}'
  AND date < today()
GROUP BY date
ORDER BY date DESC
LIMIT 20
"""


def fetch(sql, attempts=4):
    """GET the SQL endpoint. Back off on 429 and 5xx; stop on any other 4xx."""
    delay = 2.0
    for attempt in range(1, attempts + 1):
        resp = requests.get(SQL_URL, params={"sql": " ".join(sql.split())}, timeout=30)
        if resp.status_code == 200:
            return resp.json()
        if resp.status_code == 429 or resp.status_code >= 500:
            try:
                wait = max(1.0, float(resp.headers.get("Retry-After")))
            except (TypeError, ValueError):
                wait = delay
            print(f"HTTP {resp.status_code}; waiting {wait:.0f}s (attempt {attempt} of {attempts})")
            time.sleep(wait)
            delay *= 2
            continue
        raise SystemExit(f"HTTP {resp.status_code}: {resp.text[:400]}")
    raise SystemExit("gave up after repeated 429 or 5xx responses")


payload = fetch(SQL)
df = pd.DataFrame(payload["rows"], columns=payload["columns"])
df["date"] = pd.to_datetime(df["date"])
df["volume"] = pd.to_numeric(df["volume"])
df = df.sort_values("date").reset_index(drop=True)

print(df.to_string(index=False))

avg_20 = df["volume"].mean()
first, last = df["date"].iloc[0], df["date"].iloc[-1]
print(f"{TICKER} 20-session average volume: {avg_20 / 1e6:.1f}M shares "
      f"({first:%Y-%m-%d} to {last:%Y-%m-%d}, {len(df)} sessions)")

pd.DataFrame(rows, columns=columns) builds the table directly from the list of objects. Passing columns keeps the API field order. Two conversions follow. Dates first arrive as strings, then become real timestamps. volume changes to number in case any response serialize am as text. Sorting from oldest to newest puts the frame in the order wey chart dey expect. df["volume"].mean() across exactly twenty rows na the 20-session average. The last line prints am in millions, together with the date range wey e cover.

Wetin 20-day average volume really dey measure?

Average volume dey smooth one noisy daily series. Share count for one session fit swing because of index rebalances, option expirations, earnings dates and headlines. Twenty sessions na roughly one calendar month. E long enough to reduce the effect of those spikes, but still short enough to show change for how actively people dey trade the stock. The panel below dey calculate the same statistic wey the script print, from the same daily table, across the last few months. E show the raw sessions and the rolling average side by side.

QueryAAPL daily volume and im trailing 20-session average, millions of shares
34 rows (showing 20)
sessionvolume millionsavg 20d millions
2026-08-1738.251.9
2026-08-1853.452.5
2026-08-1950.553
2026-08-204153.1
2026-08-2146.953
2026-08-2434.752.3
2026-08-2525.951
2026-08-263449.9
2026-08-2732.447.8
2026-08-2838.643.1
2026-08-3141.241.4
2026-09-0153.240.6
2026-09-0233.839.8
2026-09-0337.239.4
2026-09-0439.639.7
2026-09-0835.539.2
2026-09-0965.640.6
2026-09-107042
2026-09-1150.742.5
2026-09-1439.343.1
The exact SQL behind every number
SELECT
    session,
    volume_millions,
    avg_20d_millions
FROM
(
    SELECT
        toString(d)                                                                            AS session,
        round(vol / 1e6, 1)                                                                    AS volume_millions,
        round(avg(vol) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) / 1e6, 1)  AS avg_20d_millions,
        count() OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW)                    AS sessions_in_window
    FROM
    (
        SELECT
            date                    AS d,
            toFloat64(max(volume))  AS vol
        FROM global_markets.stocks_daily_aggs
        WHERE ticker = 'AAPL'
          AND date >= today() - 75
          AND date <  today()
        GROUP BY date
    )
)
WHERE sessions_in_window = 20
ORDER BY session
Run am yourself

The daily series get one point for each session. The smoother series na the average of that session plus the nineteen sessions before am. The panel cover 2026-08-17 to 2026-10-02, with 34 sessions wey each get full twenty-session window behind am. For the last of those sessions, people trade 33.3 million AAPL shares, compared with 20-session average of 42.2 million. Na this number the script print for the day wey dem generate this page. If you run am today, the twenty sessions don move forward, and the figure go move with dem.

Ticker dey change

Change TICKER and script go work for any US-listed symbol wey dey inside table. The comparison below dey run the same 20-session calculation for four household names. E give quick sense of wetin “average volume” mean across one index ETF and three mega-caps.

QueryTrailing 20-session average volume for four household tickers, millions of shares
tickeravg 20d millionsfirst sessionlast session
NVDA1102026-09-042026-10-02
SPY462026-09-042026-10-02
AAPL42.22026-09-042026-10-02
MSFT21.32026-09-042026-10-02
The exact SQL behind every number
SELECT
    ticker,
    round(avg(vol) / 1e6, 1)   AS avg_20d_millions,
    toString(min(d))           AS first_session,
    toString(max(d))           AS last_session
FROM
(
    SELECT
        ticker,
        d,
        vol,
        row_number() OVER (PARTITION BY ticker ORDER BY d DESC) AS rn
    FROM
    (
        SELECT
            ticker,
            date                    AS d,
            toFloat64(max(volume))  AS vol
        FROM global_markets.stocks_daily_aggs
        WHERE ticker IN ('SPY', 'NVDA', 'AAPL', 'MSFT')
          AND date >= today() - 45
          AND date <  today()
        GROUP BY ticker, date
    )
)
WHERE rn <= 20
GROUP BY ticker
HAVING count() = 20
ORDER BY avg_20d_millions DESC
Run am yourself

NVDA get the highest 20-session average among the four, at 110 million shares per session. MSFT get the lowest, at 21.3 million. All the figures na for sessions from 2026-09-04 to 2026-10-02. AAPL figure here na the same number wey the trace above end with: one definition, calculated once, anywhere e appear.

How 429 dey look, and how I fit retry am politely?

Rate limit dey show as HTTP status code, no be as data. Response carry status 429 Too Many Requests and e often get Retry-After header wey hold the number of seconds to wait. The body no be the data wey you request, na why fetch helper dey check status_code before e parse anything. The rules be these, in order:

  1. 200: parse and return the JSON.
  2. 429 or any 5xx: wait for Retry-After when e dey present. If e no dey, back off (2, 4, 8, then 16 seconds), then try again. Do this four attempts altogether.
  3. Any other 4xx: stop. The body explain exactly why dem reject the query, whether na table wey no exist or statement wey the read-only gate refuse. Retrying no go change the answer. For example, unknown table go return as 400.

Two habits fit help you avoid the limit. Curated results dey refresh at most every ten minutes. If loop dey poll faster than that, e go fetch the same bytes again, so caching the payload locally no cost anything. Also, ask only for wetin you need: LIMIT 20 for a 20-session average, no be one full year of rows wey you go later discard. Network failures, like dropped connection or DNS hiccup, dey show as requests.RequestException, and the helper no dey catch dem. Wrap the call inside try/except if the script go run unattended.

FAQ

Stock market data API wey free for Python dey?

Yes. The demo endpoints for this guide dey answer GET requests wey no need authentication from requests. E return JSON wey pandas fit load directly. Curated queries dey for /api/demo?q=<key>, while hand-written read-only SQL dey for /api/demo/sql. Dem cap am at 500 rows and one year of history. No key and no signup.

I need API key to get stock data for Python?

No be for the demo tier wey we use here. Free key go raise the limit to 100 queries per day and give deeper history. The Python code no go change. Builders wey dey wrap the same endpoints as tools for an LLM fit start with market data skills for AI agents.

How I fit convert JSON API response to pandas DataFrame?

Parse the body with resp.json(), then pass the list of row objects to pd.DataFrame(rows, columns=columns). Convert date strings with pd.to_datetime and numeric strings with pd.to_numeric before you do arithmetic. JSON no get date type, and some APIs serialize decimals as text.

Wetin 429 error mean when I dey call stock API?

HTTP 429 mean Too Many Requests. E mean say server dey rate-limit the caller. Wait for the number of seconds wey Retry-After header show, if server send the header. If e no send am, increase the delay exponentially. Also cache responses instead of polling result wey dey refresh only every ten minutes.

How una dey calculate average volume for pandas?

Load one row for each session with a volume column. Keep the last twenty complete sessions, then call df["volume"].mean(). For rolling version across longer history, use df["volume"].rolling(20).mean(). Na that one the trace panel above dey draw.


Every panel above come with the exact SQL underneath, and the script dey use the same idea with a requests call for front. Free API key. 100 queries per day, 22 years of history, SQL over the raw tape. No card. Get an API key

#python#stock market api#free market data#requests#pandas#tutorial