API gratuita de datos bursátiles en Python
Tutorial de Ubuntu para consultar una API gratuita con requests y pandas, cargar JSON en un DataFrame y calcular el volumen promedio de 20 sesiones.
Consultar una API gratuita de datos bursátiles desde Python requiere una solicitud HTTP y dos bibliotecas que la mayoría ya tiene instaladas: requests para obtener el JSON y pandas para convertirlo en un DataFrame. No se necesita una clave de API. Esta guía se ejecuta de principio a fin en un contenedor Ubuntu nuevo y termina mostrando el volumen promedio de 20 sesiones de un solo ticker. El contenido de los endpoints se explica en la guía sobre API gratuita de datos bursátiles; esta página aborda el uso de la misma API desde Python.
Configuración de Python, requests y pandas en un contenedor Ubuntu nuevo
Una imagen estándar de Ubuntu no incluye el módulo venv de Python y, en las versiones recientes, el sistema impide instalar paquetes en el intérprete global. Un entorno virtual evita ambos problemas y se comporta de la misma manera en las versiones actuales de Ubuntu. Ejecute estos comandos como root o anteponga sudo a las líneas apt-get.
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"
Los rangos de versiones son deliberados. Cada uno admite cualquier versión mantenida de la biblioteca y solo excluye una futura versión mayor cuyo comportamiento se desconoce. Así, la misma línea sigue instalándose correctamente a medida que Ubuntu actualiza su versión predeterminada de Python. No se fija ninguna dependencia a una compilación disponible únicamente hoy. Todo lo que sigue supone que el entorno está activo.
¿Cómo llamo a una API gratuita de datos bursátiles en Python?
Los endpoints de demostración responden a solicitudes GET simples, sin clave ni registro. El primer script solicita una de las consultas seleccionadas y muestra la estructura de la respuesta.
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 construye la cadena de consulta a partir de params, raise_for_status() convierte cualquier código de estado 4xx o 5xx en una excepción y .json() analiza el cuerpo como un diccionario. Reducido a su estructura, ese diccionario se ve así:
{
"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": "..."
}
Dos claves son importantes para pandas. columns indica los campos en orden y rows contiene un objeto JSON por registro, con esos nombres como claves. La clave sql es la consulta exacta que generó los datos y se devuelve con cada respuesta. Esto permite auditar los datos en lugar de tratarlos como una caja negra. Una solicitud GET a /api/demo/catalog muestra todas las claves seleccionadas y el endpoint SQL que se utilizará a continuación.
¿Cómo cargo el JSON en un DataFrame de pandas?
Las consultas seleccionadas responden preguntas específicas. Las barras diarias del ticker que elijas se obtienen desde el endpoint SQL sin registro en /api/demo/sql. Este recibe una consulta de solo lectura en el parámetro sql y devuelve el mismo par columns y rows. Sus límites, a septiembre de 2026, aparecen en cada respuesta: 500 filas, 20 segundos, un año de historial y sin clave. La API SQL gratuita con una clave eleva el límite a 100 consultas diarias y permite consultar un historial más amplio. El código de solicitud es idéntico.
Dos controles en la consulta mantienen la media ajustada. Una tabla que se actualiza durante la sesión puede contener más de una fila para el día en curso, y el volumen de ese día es parcial mientras el mercado está abierto. La consulta se detiene en el día anterior con date < today() y agrupa las filas duplicadas de una fecha con GROUP BY date y max(volume). Si no has revisado cómo se construye una barra diaria a partir del tape, cómo se construyen las barras OHLCV explica de dónde provienen esas filas.
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) construye la tabla directamente a partir de la lista de objetos, y pasar columns conserva el orden de campos de la API. Después se realizan dos conversiones: las fechas llegan como cadenas y se convierten en timestamps reales, y volume se convierte a número por si alguna respuesta lo serializa como texto. Ordenar de más antiguo a más reciente deja el DataFrame en el orden que espera un gráfico. df["volume"].mean() sobre exactamente veinte filas corresponde a la media de 20 sesiones, y la última línea la imprime en millones junto con el rango de fechas que abarca.
¿Qué mide realmente el volumen promedio de 20 días?
El volumen promedio suaviza una serie diaria volátil. El número de acciones negociadas en una sola sesión puede variar por rebalanceos de índices, vencimientos de opciones, fechas de resultados y titulares. Veinte sesiones equivalen aproximadamente a un mes calendario. Es un periodo lo bastante largo para atenuar esos picos y lo bastante corto para reflejar un cambio en la actividad de negociación de una acción. El panel siguiente calcula la misma estadística que muestra el script, a partir de la misma tabla diaria y para los últimos meses, con las sesiones sin suavizar y el promedio móvil lado a lado.
| sesión | volumen (millones) | promedio 20d (millones) |
|---|---|---|
| 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 |
El SQL exacto detrás de cada cifra
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 sessionLa serie diaria tiene un punto por sesión. La serie suavizada es el promedio de esa sesión y de las 19 anteriores. El panel abarca desde 2026-08-17 hasta 2026-10-02. Incluye 34 sesiones que tienen una ventana completa de 20 sesiones anteriores. En la última de esas sesiones, AAPL negoció 33.3 millones de acciones, frente a un promedio de 20 sesiones de 42.2 millones. Ese promedio es la cifra que mostró el script el día en que se generó esta página. Si lo ejecuta hoy, las 20 sesiones habrán cambiado y la cifra también.
Cambiar el ticker
Cambie TICKER y el script funcionará con cualquier símbolo cotizado en Estados Unidos que aparezca en la tabla. La comparación siguiente aplica el mismo cálculo de 20 sesiones a cuatro nombres ampliamente conocidos. Sirve para dimensionar rápidamente qué significa «volumen promedio» entre un ETF de índice y tres empresas de megacapitalización.
| ticker | promedio 20d (millones) | primera sesión | última sesión |
|---|---|---|---|
| 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 |
El SQL exacto detrás de cada cifra
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 DESCNVDA registra el mayor promedio de 20 sesiones de los cuatro, con 110 millones de acciones por sesión, y MSFT el menor, con 21.3 millones. Todas las cifras se calculan sobre las sesiones comprendidas entre 2026-09-04 y 2026-10-02. La cifra de AAPL aquí es la misma con la que termina el rastreo anterior: una sola definición, calculada una vez, dondequiera que aparezca.
¿Cómo se ve un 429 y cómo reintento correctamente?
El límite de solicitudes llega como un código de estado HTTP, no como datos. La respuesta incluye el estado 429 Too Many Requests y a menudo un encabezado Retry-After que indica cuántos segundos hay que esperar; el cuerpo no contiene los datos solicitados. Por eso el helper fetch comprueba status_code antes de analizar la respuesta. Sus reglas, en orden, son las siguientes:
200: analiza y devuelve el JSON.429o cualquier5xx: esperaRetry-Aftercuando está disponible. De lo contrario, aplica un backoff (2, 4, 8 y después 16 segundos) y vuelve a intentarlo, hasta cuatro intentos en total.- Cualquier otro
4xx: detiene el proceso. El cuerpo explica exactamente por qué se rechazó la consulta: una tabla inexistente o una instrucción que el control de solo lectura rechazó. Reintentar no cambia la respuesta. Por ejemplo, una tabla desconocida devuelve un400.
Dos hábitos ayudan a evitar el límite. Los resultados seleccionados se actualizan como máximo cada diez minutos. Un bucle que consulta con mayor frecuencia descarga los mismos bytes, y almacenar localmente la respuesta en caché no tiene costo. Además, solicita solo lo que necesitas: LIMIT 20 para un promedio de 20 sesiones, en lugar de un año de filas que después tendrás que descartar. Las fallas de red —una conexión interrumpida o un problema temporal de DNS— aparecen como requests.RequestException, que el helper no captura. Si el script se ejecuta sin supervisión, envuelve la llamada en try/except.
Preguntas frecuentes
¿Existe una API gratuita de datos bursátiles para Python?
Sí. Los endpoints de demostración de esta guía responden solicitudes GET sin autenticación desde requests con JSON que se carga directamente en pandas: consultas seleccionadas en /api/demo?q=<key> y SQL de solo lectura escrito manualmente en /api/demo/sql, con un límite de 500 filas y un año de historial. No se necesita clave ni registro.
¿Necesito una API key para obtener datos bursátiles en Python?
No para el nivel de demostración utilizado aquí. Una clave gratuita eleva el límite a 100 consultas diarias y permite acceder a un historial más amplio. El código de Python no cambia. Los desarrolladores que convierten estos mismos endpoints en herramientas para un LLM pueden comenzar con habilidades de datos de mercado para agentes de IA.
¿Cómo convierto una respuesta JSON de una API en un DataFrame de pandas?
Analiza el cuerpo con resp.json() y pasa la lista de objetos de filas a pd.DataFrame(rows, columns=columns). Convierte las cadenas de fecha con pd.to_datetime y las cadenas numéricas con pd.to_numeric antes de hacer cálculos. JSON no tiene un tipo de fecha, y algunas API serializan los decimales como texto.
¿Qué significa un error 429 al llamar a una API bursátil?
HTTP 429 significa Too Many Requests: el servidor está limitando la frecuencia de solicitudes del cliente. Espera el número de segundos indicado en el encabezado Retry-After cuando se envíe. Si no se envía, aumenta progresivamente el intervalo entre solicitudes. También almacena en caché las respuestas en lugar de consultar repetidamente un resultado que solo se actualiza cada diez minutos.
¿Cómo se calcula el volumen promedio en pandas?
Carga una fila por sesión con una columna volume, conserva las últimas veinte sesiones completas y ejecuta df["volume"].mean(). Para calcularlo de forma móvil sobre un historial más extenso, utiliza df["volume"].rolling(20).mean(). Eso es lo que muestra el panel de trazas anterior.
Todos los paneles anteriores incluyen el SQL exacto debajo. El script sigue la misma idea, con una llamada requests al inicio. API key gratuita. 100 consultas diarias y 22 años de historial, con SQL sobre el flujo bruto de operaciones. Sin tarjeta. Obtén una API key