Este es el material gemelo del artículo «Scraping de tipos de cambio». Buena parte de aquel se traslada aquí casi sin cambios — en especial la disciplina de tipos de datos (nada de float para los precios) y las prácticas de caché. Pero las cotizaciones bursátiles tienen su propia especificidad: tickers y plazas bursátiles, sesiones de negociación y festivos, operaciones corporativas (splits y dividendos) y — la gran diferencia frente a los tipos de los bancos centrales — el licenciamiento de los datos. La información bursátil en tiempo real está regulada por las bolsas y los reguladores y, a diferencia del tipo de cambio oficial de libre publicación, ni mucho menos siempre se puede obtener gratis, y menos aún redistribuir.
1. Qué es una «cotización» y qué datos existen
Según la tarea, por «cotización» se entienden cosas distintas, y esto es lo primero que hay que definir:
- Last / precio actual — el precio de la última operación. Lo que muestra el widget de «precio de la acción ahora».
- Barra OHLCV (vela) — Open, High, Low, Close y Volume de un intervalo (minuto, hora, día). La base de los gráficos y los backtests.
- Bid/Ask (libro de órdenes) — los mejores precios de compra y venta. Hacen falta para trading; suelen ser los datos más «caros» y más sujetos a licencia.
- EOD (end-of-day) — los precios de cierre de la jornada. Baratos o gratuitos; sirven para analítica y para el seguimiento de carteras.
- Adjusted close — el cierre ajustado por splits y dividendos, para que el histórico sea continuo.
Y la bifurcación clave por «frescura»: tiempo real → retardo de 15--20 minutos → end-of-day. Cuanto más frescos y granulares son los datos, más estricta es la licencia y más alto el precio.
2. Las bolsas como fuente primaria
La fuente primaria de las cotizaciones son las propias bolsas: NYSE y NASDAQ en Estados Unidos, las plazas de Euronext, la Bolsa de Londres, la Deutsche Börse o, en España, BME (Bolsa de Madrid). Pero, a diferencia de los bancos centrales, las bolsas casi nunca regalan un API público: sus datos en tiempo real son un producto con licencia que se distribuye a través de vendedores autorizados, y lo que se publica gratis en webs y portales suele llevar un retardo de 15--20 minutos. Para el desarrollador, la vía práctica son los APIs de la siguiente sección.
| Bolsa | Qué cotiza | Acceso a los datos |
|---|---|---|
| NYSE / NASDAQ (EE. UU.) | acciones, ETF | tiempo real con licencia vía vendedores; retardo de ~15 min en portales gratuitos |
| BME — Bolsa de Madrid | acciones españolas, índice IBEX 35 | cotizaciones con retardo en la web; tiempo real a través de BME Market Data (licencia) |
| Euronext, LSE, Deutsche Börse | acciones europeas | mismo esquema: vendedores y licencias |
| B3 (Brasil), BMV (México) | mercados latinoamericanos | web con retardo; datos completos vía vendedores |
Un puente cómodo hacia el artículo de divisas: muchos de los APIs de la siguiente sección (Finnhub, Twelve Data, Alpha Vantage) no cubren solo acciones, sino también pares de divisas y cripto. Es decir, una sola integración resuelve a la vez las cotizaciones y el tipo de cambio de mercado — de mercado, que no es el tipo oficial de referencia del banco central.
Una particularidad práctica de estas fuentes: cada proveedor tiene su propio dialecto de JSON. Alpha Vantage numera las claves («1. open», «4. close»), Yahoo Finance anida los valores en arrays paralelos dentro de indicators, Twelve Data devuelve los precios como cadenas. La moraleja es siempre la misma: mapee los campos por su nombre y valide la respuesta, en lugar de confiar en un orden fijo (lo verá en los ejemplos).
3. APIs internacionales
Aquí no existe una fuente libre «a lo banco central»: todos exigen clave y aplican límites, y el tiempo real es casi siempre de pago.
| Servicio | Cobertura | Clave | Límite gratuito | Notas |
|---|---|---|---|---|
| Finnhub | acciones de EE. UU. e internacionales, FX, cripto | necesaria | ~60 peticiones/min | tiene WebSocket; histórico limitado en el plan gratuito |
| Twelve Data | acciones, FX, cripto | necesaria | ~800 peticiones/día | OHLC, indicadores, REST limpio |
| Alpha Vantage | 200 000+ tickers, 20+ bolsas | necesaria | 25 peticiones/día (5/min) | EOD e indicadores; tiempo real de EE. UU. de pago |
| yfinance (no oficial, Yahoo) | muy amplia | no hace falta | sin límite explícito, pero es scraping | para prototipos y aprendizaje, no para producción |
| EODHD | 150 000+ tickers globales | necesaria | de prueba | fuerte en descarga masiva de histórico |
| Financial Modeling Prep | precios + fundamentales | necesaria | con límite | estados financieros, múltiplos |
| Tiingo | EOD + fundamentales de EE. UU. | necesaria | con límite | end-of-day limpio |
| Marketstack / Polygon.io | global / tiempo real EE. UU. | necesaria | de prueba / de pago en la práctica | Polygon: baja latencia, ticks |
Un detalle reciente importante: IEX Cloud cerró el 31 de agosto de 2024. Si alguna guía todavía lo recomienda, es información desactualizada; los sustitutos más cercanos son Alpha Vantage y Financial Modeling Prep.
Los planes gratuitos van perfectos para un prototipo, pero chocan rápido con los límites (las 25 peticiones diarias de Alpha Vantage son, literalmente, un par de decenas de tickers al día). Por eso, con un plan gratuito la lógica se construye siempre alrededor de la caché y del almacenamiento local: se descarga el histórico una vez y después solo se actualizan los puntos recientes.
4. En qué las cotizaciones son más difíciles que los tipos de cambio
Varias diferencias que rompen un extractor ingenuo:
- El ticker no es único. Un mismo símbolo puede negociarse en varias bolsas (Telefónica cotiza en Madrid como TEF y, vía ADR, también en Nueva York). Por eso la clave del instrumento es el par «bolsa + ticker», y aún más fiables son los identificadores internacionales ISIN o FIGI.
- Sesiones de negociación y festivos. Cada bolsa tiene su horario, su zona horaria y sus sesiones previas y posteriores. «El último precio» en fin de semana es el precio del viernes.
- Operaciones corporativas. Un split 1:10 «hunde» el precio 10 veces — pero no es una caída del mercado, sino un recálculo técnico. Es el análogo del campo
nominaldel artículo de divisas: ignórelo y obtendrá una anomalía falsa. Dividendos y splits se corrigen con el adjusted close. - La divisa del instrumento. El precio del valor se expresa en la divisa de su bolsa (USD, EUR, GBP...), y para una cartera en una sola moneda hay que convertirlo — justo ahí entran los tipos de cambio del artículo vecino.
- El volumen (volume). Es un número entero, pero enorme: millones y miles de millones de títulos. Necesita un tipo entero de 64 bits.
5. Cinco soluciones en distintos lenguajes
Los ejemplos recorren cuatro proveedores y formatos distintos (Alpha Vantage aparece dos veces, en dos cortes: instantánea y velas diarias). En todos, el acento está en las dos cosas del artículo de divisas: el precio se guarda en un tipo decimal (no float) y el volumen, en un entero de 64 bits.
5.1. PHP — Alpha Vantage (GLOBAL_QUOTE, precios como cadenas)
<?php
declare(strict_types=1);
/**
* Última cotización de una acción vía Alpha Vantage (GLOBAL_QUOTE).
* Plan gratuito: 25 peticiones/día — la caché es obligatoria.
*/
function lastPrice(string $ticker): ?string
{
$url = sprintf(
'https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=%s&apikey=%s',
urlencode($ticker),
getenv('ALPHAVANTAGE_KEY')
);
$raw = file_get_contents($url);
if ($raw === false) {
throw new RuntimeException('No se pudieron obtener los datos de Alpha Vantage');
}
$json = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
// Las claves llegan numeradas: "01. symbol", "05. price"...
$quote = $json['Global Quote'] ?? [];
if (!$quote) {
return null; // ticker desconocido o límite diario agotado
}
// El precio llega como cadena: lo conservamos tal cual, sin pasar por float
return $quote['05. price'] ?? null;
}
echo 'IBM: ' . (lastPrice('IBM') ?? 'sin datos') . " USD\n";
echo 'AAPL: ' . (lastPrice('AAPL') ?? 'sin datos') . " USD\n";Lo ilustrativo aquí: Alpha Vantage devuelve los precios como cadenas JSON, así que la precisión decimal sobrevive a json_decode sin esfuerzo — basta con no convertirla a float. Para la aritmética, igual que en el artículo de divisas, BCMath o una librería Money. Fíjese también en que un ticker inexistente o un límite agotado no llegan como error HTTP: la respuesta viene vacía o con una nota de aviso, y hay que tratar ese caso de forma explícita en lugar de confiar en un formato fijo.
5.2. Python — Finnhub (cotización actual, Decimal)
import os
import requests
from decimal import Decimal
FINNHUB_TOKEN = os.environ["FINNHUB_TOKEN"] # clave gratuita, ~60 peticiones/min
def finnhub_quote(symbol: str) -> dict[str, Decimal]:
"""Cotización actual de un símbolo (en el plan gratuito, mercado de EE. UU.)."""
resp = requests.get(
"https://finnhub.io/api/v1/quote",
params={"symbol": symbol, "token": FINNHUB_TOKEN},
timeout=10,
)
resp.raise_for_status()
d = resp.json()
# Decimal(str(...)) fija exactamente el valor que llegó en el JSON
return {
"current": Decimal(str(d["c"])), # precio actual
"open": Decimal(str(d["o"])),
"high": Decimal(str(d["h"])),
"low": Decimal(str(d["l"])),
"prev_close": Decimal(str(d["pc"])),
}
if __name__ == "__main__":
q = finnhub_quote("AAPL")
print(f"AAPL: {q['current']} USD (apertura {q['open']}, máximo {q['high']})")5.3. JavaScript / Node.js — Twelve Data (OHLC, volumen)
// Gratis: ~800 peticiones al día. La clave es obligatoria.
const API_KEY = process.env.TWELVE_DATA_KEY;
async function twelveQuote(symbol) {
const url = new URL("https://api.twelvedata.com/quote");
url.searchParams.set("symbol", symbol);
url.searchParams.set("apikey", API_KEY);
const res = await fetch(url);
const d = await res.json();
if (d.status === "error") throw new Error(d.message);
return {
symbol: d.symbol,
open: d.open, // cadenas: no las convertimos a Number sin necesidad
high: d.high,
low: d.low,
close: d.close,
volume: d.volume, // el volumen es un entero grande: cadena/BigInt
exchange: d.exchange,
};
}
twelveQuote("MSFT").then((q) =>
console.log(`${q.symbol} (${q.exchange}): close ${q.close}, vol ${q.volume}`)
);En JavaScript no hay tipo decimal y Number es un double. Por eso los precios se dejan como cadenas; para la aritmética de precios se usa decimal.js/big.js, y para el volumen, el BigInt nativo.
5.4. Go — Alpha Vantage (velas diarias OHLCV)
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"sort"
"time"
)
// Alpha Vantage: gratis, 25 peticiones/día, 5/min.
type avDaily struct {
Series map[string]struct {
Open string `json:"1. open"`
High string `json:"2. high"`
Low string `json:"3. low"`
Close string `json:"4. close"`
Volume string `json:"5. volume"`
} `json:"Time Series (Daily)"`
}
func dailyBars(symbol string) (avDaily, error) {
key := os.Getenv("ALPHAVANTAGE_KEY")
url := fmt.Sprintf(
"https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&symbol=%s&apikey=%s",
symbol, key,
)
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Get(url)
if err != nil {
return avDaily{}, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var out avDaily
if err := json.Unmarshal(body, &out); err != nil {
return avDaily{}, err
}
return out, nil
}
func main() {
bars, err := dailyBars("IBM")
if err != nil {
panic(err)
}
// localizamos la vela más reciente por fecha
dates := make([]string, 0, len(bars.Series))
for d := range bars.Series {
dates = append(dates, d)
}
sort.Strings(dates)
last := dates[len(dates)-1]
b := bars.Series[last]
// los precios se quedan como cadenas; para cálculos, shopspring/decimal
fmt.Printf("IBM %s: O=%s H=%s L=%s C=%s V=%s\n",
last, b.Open, b.High, b.Low, b.Close, b.Volume)
}Lo cómodo es que Alpha Vantage entrega los precios como cadenas: la precisión decimal se conserva «de fábrica»; basta con no convertirlas a float64.
5.5. C# / .NET — Yahoo Finance (endpoint no oficial, decimal)
using System.Text.Json;
// ⚠️ Endpoint no oficial de Yahoo Finance: el mismo que usa la librería yfinance.
// Vale para prototipos y aprendizaje, pero sin garantías y NO para producción.
public static class YahooChart
{
private static readonly HttpClient Http = new();
public static async Task<(string Date, decimal Close)> LastDailyCloseAsync(string symbol)
{
var url = $"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?interval=1d&range=5d";
var req = new HttpRequestMessage(HttpMethod.Get, url);
req.Headers.UserAgent.ParseAdd("Mozilla/5.0"); // Yahoo exige un User-Agent
var resp = await Http.SendAsync(req);
resp.EnsureSuccessStatusCode();
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStreamAsync());
var result = doc.RootElement.GetProperty("chart").GetProperty("result")[0];
var timestamps = result.GetProperty("timestamp");
var closes = result.GetProperty("indicators")
.GetProperty("quote")[0].GetProperty("close");
var i = timestamps.GetArrayLength() - 1;
var unix = timestamps[i].GetInt64();
var close = closes[i].GetDecimal(); // decimal: correcto para un precio
var date = DateTimeOffset.FromUnixTimeSeconds(unix).ToString("yyyy-MM-dd");
return (date, close);
}
}
class Program
{
static async Task Main()
{
var (date, close) = await YahooChart.LastDailyCloseAsync("AAPL");
Console.WriteLine($"AAPL close {date}: {close} USD");
}
}Este ejemplo muestra con honestidad la ruta «de scraping» vía Yahoo: no hace falta clave y los datos están cerca del tiempo real, pero el endpoint es no oficial y puede cambiar sin previo aviso — no se puede construir producción encima. Los símbolos siguen la convención de Yahoo: los valores de la Bolsa de Madrid llevan el sufijo .MC (TEF.MC, ITX.MC).
6. En qué tipo de datos almacenar las cotizaciones
La regla básica es exactamente la misma que en el artículo sobre tipos de cambio: los precios y las magnitudes monetarias, solo en un tipo decimal; nada de float/double. La coma flotante binaria acumula errores de redondeo y, en backtests de periodos largos, eso produce descuadres imposibles de conciliar.
| Campo | Tipo | Por qué |
|---|---|---|
| Precio (open/high/low/close, last) | NUMERIC(18,6) / Decimal / decimal |
precisión sin pérdidas |
| Volumen (volume) | BIGINT / int64 |
miles de millones de títulos no caben en un int normal |
| Adjusted close | NUMERIC(18,6) |
guardarlo junto al precio «crudo», no en su lugar |
| Momento de la barra | TIMESTAMPTZ |
siempre con la zona horaria de la bolsa |
| Divisa del instrumento | CHAR(3) (ISO 4217) |
para convertir la cartera |
Ejemplo de tabla para velas (OHLCV):
CREATE TABLE quotes (
id BIGSERIAL PRIMARY KEY,
source VARCHAR(16) NOT NULL, -- 'FINNHUB', 'AV', 'TWELVE', 'YAHOO'
exchange VARCHAR(16) NOT NULL, -- bolsa: 'NASDAQ', 'BME'...
ticker VARCHAR(20) NOT NULL, -- AAPL, TEF...
isin CHAR(12), -- identificador fiable del instrumento
ccy CHAR(3) NOT NULL, -- divisa del precio: USD, EUR...
ts TIMESTAMPTZ NOT NULL, -- momento de la barra/cotización
interval VARCHAR(8) NOT NULL DEFAULT '1d', -- 1m | 1h | 1d
open NUMERIC(18,6) NOT NULL,
high NUMERIC(18,6) NOT NULL,
low NUMERIC(18,6) NOT NULL,
close NUMERIC(18,6) NOT NULL,
adj_close NUMERIC(18,6), -- ajustado por splits/dividendos
volume BIGINT NOT NULL DEFAULT 0,
fetched_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (source, exchange, ticker, interval, ts)
);Conviene almacenar aparte las operaciones corporativas (los splits con su coeficiente y los dividendos con su fecha), porque cuando aparecen hay que recalcular retroactivamente el histórico de adj_close.
7. Consejos prácticos
- Distinga tiempo real, retardo y EOD. Para la mayoría de las tareas (cartera, analítica, dashboard) bastan los datos con retardo o end-of-day: son más baratos y su licencia es más sencilla.
- Respete las licencias. Es la gran diferencia frente a los tipos de los bancos centrales. Los datos bursátiles en tiempo real están regulados (las bolsas, FINRA, la SEC), y hasta volver a mostrarlos a sus usuarios puede exigir un acuerdo. Por eso, por ejemplo, el tiempo real de EE. UU. en Alpha Vantage es de pago. Antes de publicar datos, revise las condiciones de la fuente.
- Constrúyalo todo alrededor de la caché. Con un límite de 25 peticiones al día no hay otra: descargue el histórico una vez y, a partir de ahí, solo actualizaciones incrementales; el resto se sirve desde su propia base.
- Trate los splits como el «nominal». Un salto del precio de N veces es casi siempre una operación corporativa, no un movimiento del mercado. La comprobación simple «variación frente al día anterior mayor que X%» caza tanto splits como errores de extracción.
- Identifique el instrumento por bolsa + ticker (y mejor por ISIN/FIGI). Un mismo ticker vive en varias plazas.
- Recuerde las sesiones y los festivos. Una respuesta vacía en fin de semana es lo normal; tome la última fecha disponible de la respuesta, no la solicitada.
- No construya producción sobre el scraping de Yahoo. Para un prototipo, perfecto; para un servicio que debe vivir años, use un API con garantías.
8. Dónde se aplica
- Trackers de cartera — valor actual de los activos, P&L, conversión a la divisa base.
- Trading algorítmico y bots — señales y ejecución (aquí ya hacen falta tiempo real y libro de órdenes).
- Dashboards y BI — visualización del mercado, cortes sectoriales.
- Screeners y backtesting — filtrado de valores y validación de estrategias sobre el histórico (OHLCV + adjusted).
- Robo-advisors y fintech — recomendaciones y gestión automatizada.
- Contabilidad y valoración — revalorización de inversiones a precio de mercado en una fecha.
- Alertas — avisos cuando el precio alcanza un nivel definido.
En resumen
La extracción de cotizaciones bursátiles se parece técnicamente a la de tipos de cambio: los mismos tipos decimales, la misma caché, la misma cautela con los saltos «técnicos» (solo que aquí el papel del nominal lo juegan los splits). Pero se añaden tres cosas: el ticker va ligado a una bolsa (y, mejor, a un ISIN/FIGI), el histórico exige ajustes por operaciones corporativas y — lo más importante en la práctica — los datos tienen licencia: gratis casi solo hay retardo y end-of-day, y el tiempo real cuesta dinero y viene atado a condiciones. Para empezar bastan los planes gratuitos de Finnhub, Twelve Data o Alpha Vantage, que además cubren pares de divisas y enlazan así con el tema del artículo vecino sobre tipos de cambio. Sume la disciplina de tipos, la caché y el respeto a las licencias, y tendrá cotizaciones en las que apoyar sus cálculos.