Python 免费股票市场数据 API 调用教程
学习在 Ubuntu 中用 Python、requests 和 pandas 调用免费股票市场数据 API,将 JSON 加载为 DataFrame,并输出单个 ticker 的 20 日平均成交量。
通过 Python 调用免费的股票市场数据 API,只需发送一次 HTTP 请求,并使用大多数人已经安装的两个库:requests 用于获取 JSON,pandas 用于将其转换为 DataFrame。整个流程无需 API key。本教程可在全新的 Ubuntu 容器中从头运行到尾,最后输出单个 ticker 的 20 个交易日平均成交量。相关端点提供哪些数据,请参阅 免费股票市场数据 API 指南;本页介绍的是同一 API 的 Python 用法。
在全新 Ubuntu 容器中安装 Python、requests 和 pandas
标准 Ubuntu 镜像未预装 Python 的 venv 模块,而近期版本也禁止将软件包安装到系统解释器中。虚拟环境可以同时解决这两个问题,并且在当前各个 Ubuntu 版本中的行为一致。请以 root 身份运行以下命令,或在 apt-get 行前加上 sudo。
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"
这里采用的版本范围是有意设置的。每个范围都接受该库的所有受支持版本,只排除行为未知的未来主版本。这样,随着 Ubuntu 默认 Python 版本升级,同一行命令仍可顺利安装。没有任何依赖被固定到仅在今天存在的某个构建版本。以下内容均假设虚拟环境已激活。
如何在 Python 中调用免费的股票市场数据 API?
这些演示端点可直接响应普通 GET 请求,无需密钥,也无需注册。第一个脚本会请求一条精选查询,并打印返回数据的结构。
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 根据 params 构建查询字符串,raise_for_status() 将任何 4xx 或 5xx 状态转换为异常,.json() 则将响应正文解析为字典。简化其结构后,该字典如下:
{
"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": "..."
}
pandas 需要关注两个键。columns 按顺序列出字段,rows 则包含每条记录对应的一个 JSON 对象,并以这些字段名作为键。sql 键记录生成这些数据的确切查询。每次响应都会返回该查询,因此数据可审计,而不是一个黑箱。向 /api/demo/catalog 发送 GET 请求,可列出所有精选键,以及后续使用的 SQL 端点。
如何将 JSON 加载到 pandas DataFrame?
这些精选查询用于回答固定问题。您选择的 ticker 的日线数据来自无需注册的 SQL 端点 /api/demo/sql。该端点通过 sql 参数接收只读查询,并返回相同的 columns 和 rows 数据对。截至 2026 年 9 月,其限制会显示在每次响应中:500 行、20 秒、最多一年历史数据,且无需密钥。使用密钥的免费 SQL API可将上限提高至每天 100 次查询,并支持更长的历史数据;请求代码相同。
SQL 中设置了两道保护措施,以确保平均值准确。交易时段内更新的表可能包含当前日期的多行数据,而且当天的成交量在市场开市期间仍是不完整的。查询通过 date < today() 截止到昨天,并使用 GROUP BY date 和 max(volume) 合并同一日期的重复行。如果您还不了解日线数据如何从逐笔成交数据中生成,了解 OHLCV 日线数据的构建方式可以说明这些数据行的来源。
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) 直接根据对象列表构建表格,传入 columns 则可保留 API 返回的字段顺序。随后进行两项转换:日期以字符串形式返回,需转换为真正的时间戳;volume 会被强制转换为数值,以防响应将其序列化为文本。按从旧到新的顺序排序后,DataFrame 的排列方式符合图表要求。对恰好 20 行数据执行 df["volume"].mean(),即可得到 20 个交易日的平均值。最后一行以百万为单位输出该平均值,并列出其涵盖的日期范围。
20日平均成交量实际衡量什么?
平均成交量可以平滑嘈杂的日度序列。单个交易日的成交股数会受到指数再平衡、期权到期日、财报发布日期和新闻标题影响而大幅波动。20个交易日约为一个日历月,既足以削弱这些尖峰,又足够短,能够跟踪一只股票交易活跃度的变化。下方图表使用同一张日度数据表,计算脚本输出的同一指标,覆盖最近几个月,并将原始交易日数据与滚动平均值并列展示。
| 交易日 | 成交量(百万) | 20日均值(百万) |
|---|---|---|
| 2026-08-17 | 38.2 | 51.9 |
| 2026-08-18 | 53.4 | 52.5 |
| 2026-08-19 | 50.5 | 53 |
| 2026-08-20 | 41 | 53.1 |
| 2026-08-21 | 46.9 | 53 |
| 2026-08-24 | 34.7 | 52.3 |
| 2026-08-25 | 25.9 | 51 |
| 2026-08-26 | 34 | 49.9 |
| 2026-08-27 | 32.4 | 47.8 |
| 2026-08-28 | 38.6 | 43.1 |
| 2026-08-31 | 41.2 | 41.4 |
| 2026-09-01 | 53.2 | 40.6 |
| 2026-09-02 | 33.8 | 39.8 |
| 2026-09-03 | 37.2 | 39.4 |
| 2026-09-04 | 39.6 | 39.7 |
| 2026-09-08 | 35.5 | 39.2 |
| 2026-09-09 | 65.6 | 40.6 |
| 2026-09-10 | 70 | 42 |
| 2026-09-11 | 50.7 | 42.5 |
| 2026-09-14 | 39.3 | 43.1 |
每个数字背后的完整 SQL
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日度序列每个交易日对应一个数据点;平滑后的序列取该交易日及此前19个交易日的平均值。图表覆盖2026-08-17至2026-10-02,其中有34个交易日具备完整的20日窗口。在这些交易日中的最后一个交易日,AAPL成交33.3百万股,20日平均成交量为42.2百万股。这个平均值就是本页面生成当天脚本输出的数字。若今天运行脚本,纳入计算的20个交易日已经发生变化,数字也会随之变化。
更换 ticker
将 TICKER 替换为其他代码后,脚本即可适用于表格中的任何一只在美国上市的证券。下面的比较对四个家喻户晓的名称执行相同的20个交易日计算,用于快速了解“平均成交量”在一只指数 ETF 和三只超大盘股之间的规模差异。
| ticker | 20日均值(百万) | 首个交易日 | 最后交易日 |
|---|---|---|---|
| NVDA | 110 | 2026-09-04 | 2026-10-02 |
| SPY | 46 | 2026-09-04 | 2026-10-02 |
| AAPL | 42.2 | 2026-09-04 | 2026-10-02 |
| MSFT | 21.3 | 2026-09-04 | 2026-10-02 |
每个数字背后的完整 SQL
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四者中,NVDA的20个交易日平均成交量最高,为每个交易日110百万股;MSFT最低,为21.3百万股。所有数据均根据2026-09-04至2026-10-02期间的交易日计算。这里的 AAPL 数值与上文追踪结果的终值相同:定义一致,只计算一次,无论出现在哪里。
429 响应是什么样的?如何礼貌地重试?
限流以 HTTP 状态码的形式返回,而不是作为数据返回。响应会携带状态码 429 Too Many Requests,通常还会带有 Retry-After 请求头,其中说明需要等待的秒数;响应正文并不是您请求的数据。因此,fetch 辅助函数会先检查 status_code,再进行解析。处理规则依次如下:
200:解析并返回 JSON。429或任何5xx:如果存在Retry-After,则等待相应时间;否则采用退避策略(等待 2、4、8 秒,随后等待 16 秒),然后重试,总计最多四次。- 其他任何
4xx:停止重试。响应正文会明确说明查询被拒绝的原因,例如请求的表不存在,或只读限制拒绝了相关语句。继续重试不会改变结果。例如,请求未知表时,返回的会是400。
养成两个习惯,可以避免触发限流。精选结果最多每十分钟刷新一次;如果循环轮询速度更快,获取到的只是相同字节,而将响应内容缓存在本地无需成本。此外,只请求所需的数据:计算 20 个交易日的平均值时使用 LIMIT 20,不要请求一整年的数据后再丢弃大部分结果。网络故障(例如连接中断或 DNS 短暂异常)会以 requests.RequestException 的形式出现,辅助函数不会捕获这类错误;如果脚本需要无人值守运行,请将调用放在 try/except 中。
常见问题
Python 是否有免费的股票市场数据 API?
有。本指南中的演示端点可接收来自 requests 的免认证 GET 请求,并返回可直接加载到 pandas 的 JSON:/api/demo?q=<key> 提供整理好的查询,/api/demo/sql 提供手写的只读 SQL。每次最多返回 500 行数据,历史数据最长覆盖一年。无需密钥,也无需注册。
在 Python 中获取股票数据是否需要 API 密钥?
本指南使用的演示层级不需要。免费密钥可将上限提高到每天 100 次查询,并提供更长的历史数据;Python 代码无需修改。将相同端点封装为 LLM 工具的开发者,可以从 面向 AI 智能体的市场数据技能 开始。
如何将 JSON API 响应转换为 pandas DataFrame?
使用 resp.json() 解析响应内容,然后将行对象列表传递给 pd.DataFrame(rows, columns=columns)。在进行算术运算前,使用 pd.to_datetime 转换日期字符串,使用 pd.to_numeric 转换数字字符串;JSON 没有日期类型,一些 API 会将小数序列化为文本。
调用股票 API 时,429 错误意味着什么?
HTTP 429 表示请求过多,服务器正在对调用方实施限流。如果响应包含 Retry-After 标头,请等待其中给出的秒数;如果没有,则采用指数退避,并缓存响应,不要反复请求每十分钟才更新一次的结果。
如何在 pandas 中计算平均成交量?
按每个交易日加载一行数据,并包含 volume 列。保留最近 twenty 个完整交易日,然后调用 df["volume"].mean()。如需基于更长历史数据计算滚动平均值,请使用 df["volume"].rolling(20).mean();上方的跟踪面板就是这样绘制的。
上方每个面板下方都附有完全相同的 SQL,脚本的原理也相同,只是在前面加入 requests 调用。免费 API 密钥。每天 100 次查询,22 年历史数据,可对原始成交记录执行 SQL。无需银行卡。获取 API 密钥