CMS y plataformas 13 min de lectura

Extracción de cotizaciones bursátiles: APIs, fuentes, código y almacenamiento de datos

De dónde obtener cotizaciones bursátiles: APIs de mercado, portales financieros, ejemplos de código en cinco lenguajes y almacenamiento del histórico.

EW
Equipo Web-Scraping.es
Recopilación de datos para las necesidades del negocio
Publicado: 18 marzo 2025

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 nominal del 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
<?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)

python
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)

javascript
// 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)

go
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)

c#
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):

sql
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.