---
title: "6.1.2 Fuentes de texto: EDGAR, CNMV, llamadas y redes"
description: "EDGAR (8-K, 10-K, 10-Q), CNMV, transcripciones y redes: cómo se descargan, se parsean, se fechan y se limpian de copias, spam y bots."
url: "https://pythonparatrading.com/curso-python-trading/nivel-6/fuentes-de-texto-edgar-cnmv-transcripciones-y-redes/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-6/fuentes-de-texto-edgar-cnmv-transcripciones-y-redes.png"
---

# 6.1.2 · Fuentes de texto: EDGAR, CNMV, transcripciones y redes sociales

> Antes de puntuar un texto hay que conseguirlo, saber cuándo se publicó y quitarle lo que sobra: etiquetas HTML, tablas, copias de agencia y cuentas automatizadas. Esta lección recorre las fuentes de texto financiero, ejecuta un cliente de EDGAR contra un servidor local que imita su API, parsea documentos tipo SEC y CNMV, hechos XBRL, transcripciones y publicaciones de redes, y mide contra la verdad cuántas copias y bots se detectan.

- Curso: [Nivel 6: IA generativa, LLMs, agentes, big data y datos alternativos](https://pythonparatrading.com/curso-python-trading/nivel-6/)
- Módulo: 6.1 · NLP financiero clásico y moderno
- Duración: 70 min
- Requisitos: [1.4.1 · Descarga por REST y websockets: paginación, límites y reintentos](https://pythonparatrading.com/curso-python-trading/nivel-1/descarga-rest-websockets-y-reintentos/), [1.5.1 · Estados financieros, ratios, XBRL y SEC EDGAR](https://pythonparatrading.com/curso-python-trading/nivel-1/estados-financieros-xbrl-y-edgar/), [6.1.1 · Tokenización, embeddings, diccionarios financieros y FinBERT](https://pythonparatrading.com/curso-python-trading/nivel-6/tokenizacion-embeddings-loughran-mcdonald-y-finbert/)

## Al terminar esta lección

- Situar cada fuente de texto (noticias, 8-K, 10-K, 10-Q, información privilegiada y OIR de la CNMV, transcripciones, redes) por su formato, su hora y su forma de acceso.
- Usar la API de envíos de EDGAR respetando su política de acceso: cabecera `User-Agent` declarada sin datos personales, ritmo de peticiones y reintentos, y distinguir hora de aceptación y fecha de presentación.
- Convertir HTML y Markdown regulatorios en texto, secciones y tablas con BeautifulSoup y pandas, y los hechos XBRL en series sin duplicados.
- Separar en una transcripción la presentación de la dirección y las preguntas de los analistas.
- Detectar copias sindicadas (también traducidas), spam y bots, y medir el acierto contra la verdad del corpus.

## El mapa de las fuentes de texto

La lección 6.1.1 convirtió texto en puntuaciones. Antes hay un trabajo menos vistoso que decide la calidad de todo lo
demás: conseguir el texto, fecharlo con precisión y limpiarlo. Las fuentes habituales en research financiero:

| Fuente | Formato | Hora | Acceso |
|---|---|---|---|
| Noticias de agencias | Texto con titular, a menudo XML o JSON del proveedor | Al segundo, si el proveedor la conserva | Licencia de pago (lección 1.3.4) |
| 8-K (hechos materiales, resultados) | HTML o texto, con anexos (Exhibit 99.1) | Hora de aceptación en EDGAR | Público, API sin clave |
| 10-K y 10-Q (informes anual y trimestral) | HTML con XBRL en línea | Hora de aceptación | Público, API sin clave |
| Información privilegiada y OIR de la CNMV | PDF o texto, con hora de registro | Hora de registro | Público, web y RSS |
| Transcripciones de *conference calls* | Turnos de palabra con hablante y rol | La de la llamada | Proveedores de pago; audio en la web del emisor |
| Redes sociales | Publicaciones cortas con *cashtags* (`$TICKER`) | Al segundo | API de cada plataforma, con sus términos |

```mermaid
flowchart TB
  F[Fuente] --> D[Descarga]
  D --> P[Parseo]
  P --> L[Limpieza]
  L --> X[Tabla con hora]
  X --> S[Puntuación 6.1.1]
```

El curso trae una versión sintética de cada fuente en los datos del nivel 6, con documentos inventados que siguen la
forma de los reales: 4.020 documentos regulatorios tipo SEC y tipo CNMV, 13.067 hechos XBRL, 840 llamadas y 61.697
publicaciones de redes, además de las noticias de la lección anterior. Todos dicen en su texto que son sintéticos. Las
descargas reales necesitan red y van en bloques `skip`; donde aporta, un servidor local imita la API y el código del
cliente se ejecuta de verdad.

## EDGAR: el historial de envíos de cada empresa

La SEC publica en `data.sec.gov`, sin autenticación ni claves, el historial de envíos de cada entidad en
`/submissions/CIK##########.json` (el CIK con diez dígitos y ceros a la izquierda)[^1]. La lección 1.5.1 usó la otra
mitad de la API, los hechos XBRL de `companyfacts`; aquí interesa el índice de documentos, que dice qué se presentó,
cuándo y dónde está:

```mermaid
flowchart TB
  S[submissions JSON] --> R[filings.recent]
  R --> A[accessionNumber]
  R --> T[acceptanceDateTime]
  R --> P[primaryDocument]
  A --> U[URL del documento]
  P --> U
```

`filings.recent` está en formato columnar (una lista por campo): `accessionNumber`, `filingDate`, `reportDate`,
`acceptanceDateTime`, `form`, `items` (los del 8-K), `primaryDocument` y otros. Contiene al menos un año o los 1.000
envíos más recientes; los anteriores van en ficheros adicionales que enumera `filings.files`. El documento se descarga
de los archivos de EDGAR en `/Archives/edgar/data/<CIK>/<número de registro sin guiones>/<documento>`, la estructura de
directorios que describe la propia SEC[^2].

La **política de acceso justo** de la SEC tiene dos reglas que afectan al código[^2]: un máximo actual de 10
peticiones por segundo, y una cabecera `User-Agent` que identifique a quien accede (su ejemplo: «Sample Company Name
AdminContact@&lt;sample company domain&gt;.com»). Para cumplirla sin publicar datos personales, el curso usa un
marcador con un dominio reservado para ejemplos (`example.com`, RFC 2606): en un uso real va el nombre de la
organización y un buzón de contacto **de rol** (no el correo de una persona), leído de la configuración y no escrito en
el código que se comparte.

```python skip
import json
import os
import time
import urllib.request

# Organización y buzón de rol, desde la configuración (lección 0.2.4)
UA = os.environ.get("EDGAR_USER_AGENT",
                    "Curso PPT contacto@example.com")

def get(url):
    req = urllib.request.Request(url, headers={"User-Agent": UA})
    with urllib.request.urlopen(req, timeout=30) as r:
        datos = r.read()
    time.sleep(0.15)  # muy por debajo de 10 peticiones por segundo
    return datos

cik = 320193
sub = json.loads(get("https://data.sec.gov/submissions/"
                     f"CIK{cik:010d}.json"))
reciente = sub["filings"]["recent"]
i = reciente["form"].index("10-K")
acc = reciente["accessionNumber"][i].replace("-", "")
doc = reciente["primaryDocument"][i]
html = get(f"https://www.sec.gov/Archives/edgar/data/{cik}/{acc}/{doc}")
```

Para ejecutar ese mismo cliente sin red, la celda siguiente levanta un servidor HTTP local que sirve, con la forma de
`submissions`, los documentos sintéticos de una empresa del curso. Como el real, rechaza las peticiones sin
`User-Agent` (con un 403); para simular la saturación, responde 429 (*Too Many Requests*) a una de cada tres
descargas de documentos, siempre la misma, para que la salida no dependa del reloj:

```python
import json
import re
import threading
import time
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

import numpy as np
import pandas as pd

docs = pd.read_parquet("filings6_documentos.parquet")
xbrl = pd.read_parquet("filings6_xbrl.parquet")
CIK = xbrl.merge(docs[["id_doc", "emisor"]], on="id_doc").query(
    "emisor == 'Luklux Apparel Corp.'")["cik"].iloc[0]
mios = docs[docs["emisor"] == "Luklux Apparel Corp."].sort_values("ts")
mios = mios.assign(
    acc=[f"{CIK}-{t:%y}-{n:06d}" for n, t in
         enumerate(mios["ts"], start=1)],
    archivo=[f"{i.lower()}.htm" for i in mios["id_doc"]])

def submissions():
    r = mios.iloc[::-1]  # lo más reciente primero, como la API
    ny = r["ts"].dt.tz_convert("America/New_York")
    return {"cik": CIK, "name": "Luklux Apparel Corp.",
            "filings": {"recent": {
                "accessionNumber": r["acc"].tolist(),
                "acceptanceDateTime": ny.dt.strftime(
                    "%Y-%m-%dT%H:%M:%S").tolist(),
                "form": r["tipo"].tolist(),
                "items": r["items"].tolist(),
                "primaryDocument": r["archivo"].tolist()}}}

class EdgarLocal(BaseHTTPRequestHandler):
    peticiones = 0
    def do_GET(self):
        if not self.headers.get("User-Agent", "").strip():
            return self.responder(403, b"Declare your User-Agent")
        if self.path.startswith("/Archives/"):
            EdgarLocal.peticiones += 1
            if EdgarLocal.peticiones % 3 == 0:  # saturación simulada
                return self.responder(429, b"Too Many Requests")
        if self.path == f"/submissions/CIK{CIK}.json":
            cuerpo = json.dumps(submissions()).encode()
            return self.responder(200, cuerpo)
        fila = mios[mios["archivo"] == self.path.rsplit("/", 1)[-1]]
        if len(fila):
            return self.responder(200, fila["texto"].iloc[0].encode())
        self.responder(404, b"Not Found")
    def responder(self, codigo, cuerpo):
        self.send_response(codigo)
        self.end_headers()
        self.wfile.write(cuerpo)
    def log_message(self, *args):
        pass

servidor = ThreadingHTTPServer(("127.0.0.1", 0), EdgarLocal)
threading.Thread(target=servidor.serve_forever, daemon=True).start()
BASE = f"http://127.0.0.1:{servidor.server_port}"
print(len(mios), "documentos de Luklux en el servidor local")
```
```output
62 documentos de Luklux en el servidor local
```

El cliente es el del bloque real con tres cambios: la URL base, un reintento con espera creciente cuando el servidor
devuelve 429 y un error claro cuando falta la cabecera:

```python
from collections import Counter

UA = "Curso PPT contacto@example.com"

reintentos = Counter()

def get(url, ua=UA, intentos=4):
    cab = {"User-Agent": ua} if ua else {}
    for n in range(intentos):
        try:
            req = urllib.request.Request(url, headers=cab)
            with urllib.request.urlopen(req, timeout=10) as r:
                return r.read()
        except urllib.error.HTTPError as e:
            if e.code != 429 or n == intentos - 1:
                raise
            reintentos[e.code] += 1
            time.sleep(0.05 * 2 ** n)  # espera creciente

try:
    get(f"{BASE}/submissions/CIK{CIK}.json", ua=None)
except urllib.error.HTTPError as e:
    print("sin User-Agent:", e.code, e.read().decode())

sub = json.loads(get(f"{BASE}/submissions/CIK{CIK}.json"))
rec = pd.DataFrame(sub["filings"]["recent"])
print(rec["form"].value_counts().to_dict())
rec.head(3)
```
```output
{'8-K': 31, '10-Q': 21, '10-K': 7, '10-Q/A': 3}
        accessionNumber   acceptanceDateTime  form items primaryDocument
0  9900000059-25-000062  2025-11-12T16:09:51  10-Q           d003973.htm
1  9900000059-25-000061  2025-11-03T07:34:37   8-K  2.02     d003934.htm
2  9900000059-25-000060  2025-07-30T16:57:37  10-Q           d003768.htm
```

Con la tabla de envíos, descargar todos los 10-K de la empresa es un bucle. Cuando el servidor responde 429, el
cliente espera y reintenta (tres veces en siete descargas); en el real, la espera fija entre peticiones evita llegar ahí:

```python
diez_k = rec[rec["form"] == "10-K"]
textos = {}
for acc, archivo in zip(diez_k["accessionNumber"],
                        diez_k["primaryDocument"]):
    url = (f"{BASE}/Archives/edgar/data/{int(CIK)}/"
           f"{acc.replace('-', '')}/{archivo}")
    textos[acc] = get(url).decode()
print(len(textos), "10-K descargados;",
      sum(len(t) for t in textos.values()), "caracteres;",
      "reintentos tras un 429:", reintentos[429])
```
```output
7 10-K descargados; 17485 caracteres; reintentos tras un 429: 3
```

### Hora de aceptación y fecha de presentación

`acceptanceDateTime` es el instante en que EDGAR aceptó el envío; `filingDate`, la fecha en que se considera
presentado. No siempre coinciden: según la regla 13 de la Regulation S-T, lo que se envía después de las 17:30 de la
costa este se considera presentado el siguiente día hábil (con excepciones hasta las 22:00 para algunos formularios,
como los 3, 4 y 5)[^3]. Los 10-K y 10-Q sintéticos se presentan tras el cierre de la bolsa, entre las 16:00 y las
18:30 de Nueva York:

```python
from pandas.tseries.holiday import USFederalHolidayCalendar

festivos = USFederalHolidayCalendar().holidays("2018", "2026")
periodicos = docs[docs["tipo"].str.match(r"10-[KQ]")].copy()
ny = periodicos["ts"].dt.tz_convert("America/New_York")
dia = ny.dt.tz_localize(None).dt.normalize()
tarde = ny.dt.hour * 60 + ny.dt.minute >= 17 * 60 + 30
siguiente = np.busday_offset(dia.to_numpy().astype("datetime64[D]"),
                             1, roll="forward",
                             holidays=festivos.to_numpy()
                             .astype("datetime64[D]"))
periodicos["filing_date"] = dia.where(~tarde, pd.to_datetime(siguiente))
print(f"aceptados después de las 17:30: {tarde.mean():.1%}")
periodicos.loc[tarde, ["tipo", "ts", "filing_date"]].head(3)
```
```output
aceptados después de las 17:30: 28.9%
     tipo                        ts filing_date
1  10-Q/A 2019-01-02 22:58:24+00:00  2019-01-03
3  10-K/A 2019-01-08 23:06:37+00:00  2019-01-09
5  10-Q/A 2019-01-24 22:42:21+00:00  2019-01-25
```

*Fig. — Hora de aceptación de los 10-K, 10-Q y sus enmiendas sintéticos: los aceptados desde las 17:30 de Nueva York (29 %) reciben la fecha de presentación del siguiente día hábil.* Barras por tramos de 15 minutos de 16:00 a 18:15. Entre 16:00 y 17:15 hay unos 220-250 informes por tramo (126 en el primero); los tramos de 17:30 y 17:45 (destacados) tienen 244 y 252, y los de 18:00 y 18:15, 13 y 11.

Un 29 % de los informes periódicos (10-K, 10-Q y sus enmiendas) lleva una fecha de presentación posterior a su aceptación. Para un estudio de
eventos (lección 6.1.3) eso es un retraso de un día, no una anticipación: quien use `filingDate` llega tarde, pero no
usa información futura. El error peligroso es el contrario: fechar con `reportDate` (el fin del periodo) o con la
fecha en que una base de datos incorporó el documento sin saber cuál es.

### Búsqueda de texto completo

La web de EDGAR tiene una búsqueda de texto completo de los envíos electrónicos desde 2001[^4]. La página la usa
llamando a un servicio JSON (`efts.sec.gov/LATEST/search-index`) que la SEC **no documenta como API**: se puede
consultar con la misma política de acceso, sabiendo que puede cambiar sin aviso. Para un proceso estable, lo
documentado es la API de envíos y los índices diarios y trimestrales de EDGAR.

```python skip
import urllib.parse

q = urllib.parse.urlencode({"q": '"material weakness"',
                            "forms": "8-K",
                            "startdt": "2024-01-01",
                            "enddt": "2024-12-31"})
r = json.loads(get(f"https://efts.sec.gov/LATEST/search-index?{q}"))
```

## CNMV: información privilegiada y otra información relevante

Los emisores españoles comunican a la CNMV lo que puede afectar a su cotización. Hasta el 8 de febrero de 2020 lo
hacían como **hechos relevantes**; desde entonces, en dos registros separados: **información privilegiada** (la del
artículo 17 del Reglamento de abuso de mercado, el MAR) y **otra información relevante** (OIR). La CNMV conserva el
registro de hechos relevantes «hasta 08/02/2020» junto a los dos nuevos, y hoy la difusión la regulan los artículos
226 y 227 de la Ley 6/2023 del Mercado de Valores[^5]. Para un histórico, hay que unir las dos series.

La CNMV ofrece consulta web de cada registro, avisos por correo y canales **RSS 2.0** de información privilegiada y
de OIR[^6]; no hemos encontrado una API documentada. Leer un RSS es una petición y un parseo de XML:

```python skip
import xml.etree.ElementTree as ET

url = ("https://www.cnmv.es/portal/Otra-Informacion-Relevante/"
       "RSS.asmx/GetNoticiasCNMV")
req = urllib.request.Request(url, headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=30) as r:
    canal = ET.fromstring(r.read())
items = [{c.tag: c.text for c in item}
         for item in canal.iter("item")]
```

Los campos de cada `item` (título, enlace, fecha) son los de RSS 2.0; los concretos del canal de la CNMV hay que
comprobarlos en el propio canal antes de depender de ellos. Lo importante es la fecha: con un RSS se recoge lo
publicado al consultarlo y la hora de la publicación hay que leerla del propio elemento, no del momento de la descarga.

Los documentos sintéticos tipo CNMV del curso son Markdown, con una tabla de magnitudes, una nota al pie y la
previsión en texto. El cambio de régimen está en los datos:

```python
cnmv = docs[docs["formato"] == "markdown"]
print(cnmv.groupby("tipo")["ts"].agg(["count", "min", "max"]))
hora_madrid = cnmv["ts"].dt.tz_convert("Europe/Madrid")
print("hora de Madrid:", hora_madrid.dt.strftime("%H:%M").min(),
      "a", hora_madrid.dt.strftime("%H:%M").max())
ultimo = cnmv["texto"].iloc[-1]
print(ultimo.split("## Principales magnitudes")[1]
      .split("## Evolución")[0].strip())
```
```output
      count                       min                       max
tipo
HR       60 2019-02-04 06:50:10+00:00 2019-11-18 06:58:48+00:00
OIR     360 2020-02-10 06:21:06+00:00 2025-11-14 06:53:55+00:00
hora de Madrid: 07:17 a 08:12
| (millones de USD) | Tercer trimestre 2025 |
|---|---|
| Ventas | 5.044,2 |
| Resultado neto¹ | 196,8 |
| Beneficio por acción (USD) | 8,28 |
| Patrimonio neto | 4.140,6 |

¹ Atribuido a la sociedad dominante.
```

Todas se publican entre las 7:17 y las 8:12 de Madrid: antes de la apertura del mercado continuo español (9:00) y,
con más motivo, de la NYSE. La tabla Markdown se convierte en un `DataFrame` partiendo por `|`, con la convención
española de números (punto de miles, coma decimal) y la nota al pie separada de la cifra:

```python
def tabla_markdown(texto):
    filas = [l.strip().strip("|").split("|")
             for l in texto.splitlines()
             if l.startswith("|") and not l.startswith("|---")]
    cab, *cuerpo = [[c.strip() for c in f] for f in filas]
    return pd.DataFrame(cuerpo, columns=cab)

def numero_es(x):
    x = re.sub(r"[¹²³]", "", x).replace(".", "").replace(",", ".")
    return float(x)

t = tabla_markdown(cnmv["texto"].iloc[-1])
t["valor"] = t.iloc[:, 1].map(numero_es)
t["nota"] = t.iloc[:, 0].str.extract(r"([¹²³])", expand=False)
t
```
```output
            (millones de USD) Tercer trimestre 2025    valor nota
0                      Ventas               5.044,2  5044.20  NaN
1             Resultado neto¹                 196,8   196.80    ¹
2  Beneficio por acción (USD)                  8,28     8.28  NaN
3             Patrimonio neto               4.140,6  4140.60  NaN
```

## Documentos tipo SEC: del HTML a secciones y tablas

Los documentos tipo SEC del curso son HTML con secciones `<h2>` (los *Items*), tablas y notas `<sup>`. Los reales son
mucho más largos y desordenados (estilos en línea, tablas anidadas, XBRL en línea), pero la técnica es la misma:
BeautifulSoup para recorrer el árbol y `pandas.read_html` para las tablas. Un 8-K de resultados (Item 2.02) trae el
comunicado como anexo:

```python
from io import StringIO

from bs4 import BeautifulSoup

ocho_k = docs[(docs["tipo"] == "8-K") & (docs["items"] == "2.02")]
sopa = BeautifulSoup(ocho_k["texto"].iloc[0], "html.parser")
for h2 in sopa.find_all("h2"):
    print("·", h2.get_text())
print([li.get_text() for li in sopa.find_all("li")])
tabla = pd.read_html(StringIO(str(sopa.find("table"))))[0]
tabla
```
```output
· Item 2.02 Results of Operations and Financial Condition
· Exhibit 99.1
['Revenue of $22,638 million', 'Net loss of $11.5 million, or $-0.01 per share', 'Stores at quarter end: 441.']
      (USD millions)  Q4 2018
0            Revenue  22637.9
1  Net income (loss)   (11.5)
2  Diluted EPS (USD)    -0.01
```

El Item 2.02 se **suministra** (*furnished*) y no se **presenta** (*filed*) a efectos de la responsabilidad de la
sección 18 de la Exchange Act; el plazo general de un 8-K es de cuatro días hábiles desde el hecho[^7]. Para un quant,
dos detalles prácticos: la cifra negativa va entre paréntesis (`(11.5)`), que `read_html` deja como texto, y el
número de la tabla (22.637,9 millones) aparece redondeado en la lista (22.638), así que hay que decidir qué cifra
manda. Convertir y comprobar:

```python
def numero_en(x):
    x = str(x).replace(",", "").replace("$", "").strip()
    negativo = x.startswith("(") and x.endswith(")")
    return -float(x.strip("()")) if negativo else float(x)

tabla["valor"] = tabla.iloc[:, 1].map(numero_en)
tabla
```
```output
      (USD millions)  Q4 2018     valor
0            Revenue  22637.9  22637.90
1  Net income (loss)   (11.5)    -11.50
2  Diluted EPS (USD)    -0.01     -0.01
```

Para las señales de texto interesa más el **texto por secciones** que las tablas. La función siguiente devuelve un
diccionario que asocia a cada sección su texto en cualquier documento del corpus; la lección 6.1.3 la usa con la sección de riesgos de
los 10-K:

```python
def secciones(html):
    sopa = BeautifulSoup(html, "html.parser")
    out = {}
    for h2 in sopa.find_all("h2"):
        partes = []
        for nodo in h2.find_next_siblings():
            if nodo.name == "h2":
                break
            partes.append(nodo.get_text(" ", strip=True))
        out[h2.get_text(strip=True)] = " ".join(partes)
    return out

s10k = secciones(next(iter(textos.values())))
{k: len(v.split()) for k, v in s10k.items()}
```
```output
{'Item 1. Business': 14, 'Item 1A. Risk Factors': 223, 'Part II, Item 8. Financial Statements': 23, "Item 7. Management's Discussion and Analysis": 13}
```

## Hechos XBRL: duplicados y el cuarto trimestre

La lección 1.5.1 explicó los hechos XBRL y su versión point-in-time. Los del corpus (`filings6_xbrl.parquet`) tienen
dos trampas de los reales. La primera: un 10-Q del primer trimestre trae el trimestre y el acumulado del año, que son
el mismo periodo, así que el mismo hecho aparece dos veces:

```python
clave = ["id_doc", "concepto", "periodo_inicio", "periodo_fin"]
print("filas:", len(xbrl),
      "| duplicadas:", xbrl.duplicated(clave).sum())
print("duplicadas con distinto valor:",
      xbrl.duplicated(clave).sum()
      - xbrl.duplicated(clave + ["valor"]).sum())
hechos = xbrl.drop_duplicates(clave)
```
```output
filas: 13067 | duplicadas: 900
duplicadas con distinto valor: 0
```

La segunda: el 10-K da el año y **no hay 10-Q del cuarto trimestre**. Las ventas del cuarto trimestre se calculan
como las del año menos el acumulado de nueve meses del tercer 10-Q, con los hechos de los documentos originales (sin
las enmiendas, que la lección 1.5.1 trata como versiones posteriores). Se comprueba contra las ventas trimestrales de `acciones6_ventas.csv`:

```python
ventas = hechos[hechos["concepto"] == "us-gaap:Revenues"].copy()
ventas["dias"] = (ventas["periodo_fin"]
                  - ventas["periodo_inicio"]).dt.days
anual = ventas[(ventas["forma"] == "10-K") & (ventas["dias"] > 360)]
nueve = ventas[(ventas["forma"] == "10-Q")
               & ventas["dias"].between(270, 275)]
nueve = nueve.assign(periodo_fin=nueve["periodo_fin"]
                     + pd.offsets.QuarterEnd(1))
q4 = anual.merge(nueve, on=["cik", "periodo_fin"],
                 suffixes=("_año", "_9m"))
q4["q4_musd"] = (q4["valor_año"] - q4["valor_9m"]) / 1e6

emisores = docs.drop_duplicates("emisor").set_index("emisor")
cik_tk = (xbrl.merge(docs[["id_doc", "emisor"]], on="id_doc")
          .drop_duplicates("cik").set_index("cik")["emisor"]
          .map(emisores["ticker"]))
q4["ticker"] = q4["cik"].map(cik_tk)
v = pd.read_csv("acciones6_ventas.csv", parse_dates=["fin_trimestre"])
q4 = q4.merge(v, left_on=["ticker", "periodo_fin"],
              right_on=["ticker", "fin_trimestre"])
error = (q4["q4_musd"] - q4["ventas"]).abs()
print(len(q4), "cuartos trimestres; error máximo",
      round(error.max(), 2), "millones")
```
```output
342 cuartos trimestres; error máximo 0.0 millones
```

Los 342 cuartos trimestres calculados coinciden exactamente con las ventas publicadas: los hechos están redondeados a
100.000 USD (`decimales` = −5), la misma precisión que la tabla de ventas en millones con un decimal. Con datos reales hay que añadir los cambios de concepto entre años y los ejercicios que no acaban en diciembre
(lección 1.5.1). Las 6 empresas que cambiaron de ticker exigen unir por el ticker vigente en cada fecha, o mejor por
un identificador permanente (`id_empresa`, el CIK).

## Transcripciones de conference calls

Una transcripción es una lista de turnos con hablante, rol y texto. La estructura importa más que el texto completo:
la **presentación** la escribe la dirección y casi siempre es optimista; las **preguntas** de los analistas reflejan
lo que preocupa al mercado. El corpus tiene 840 llamadas de 30 empresas:

```python
llam = pd.read_parquet("llamadas6_transcripciones.parquet")
una = llam[llam["id_llamada"] == llam["id_llamada"].iloc[0]]
print(una[["turno", "seccion", "rol", "hablante"]]
      .head(7).to_string(index=False))
print(llam.groupby(["seccion", "rol"]).size())
```
```output
 turno      seccion                   rol       hablante
     1 presentacion              operador      Operadora
     2 presentacion relaciones_inversores Alba Salbasgon
     3 presentacion             directivo   Alba Varlazu
     4 presentacion             directivo    Nils Bastel
     5    preguntas              operador      Operadora
     6    preguntas              analista Rocco Basbasmi
     7    preguntas             directivo    Nils Bastel
seccion       rol
preguntas     analista                 3790
              directivo                3790
              operador                 1680
presentacion  directivo                1680
              operador                  840
              relaciones_inversores     840
dtype: int64
```

Con los turnos separados, el tono de la dirección y el de los analistas se miden por separado con el léxico de la
lección 6.1.1:

```python
lex = pd.read_csv("texto6_lexico.csv").query("lista == 'financiera'")
listas = {i: (set(g.loc[g["categoria"] == "positiva", "palabra"]),
              set(g.loc[g["categoria"] == "negativa", "palabra"]))
          for i, g in lex.groupby("idioma")}

def tono(texto, idioma):
    pos, neg = listas[idioma]
    w = re.findall(r"[a-záéíóúüñ]+", texto.lower())
    p, n = sum(x in pos for x in w), sum(x in neg for x in w)
    return (p - n) / (p + n + 1)

grupo = np.where(llam["rol"] == "analista", "analistas",
                 np.where(llam["rol"] == "directivo", "direccion",
                          "otros"))
textos_ll = (llam.assign(grupo=grupo)
             .groupby(["id_llamada", "grupo", "idioma"])["texto"]
             .agg(" ".join).reset_index())
textos_ll["tono"] = [tono(t, i) for t, i in
                     zip(textos_ll["texto"], textos_ll["idioma"])]
t_ll = textos_ll.pivot(index="id_llamada", columns="grupo",
                       values="tono")
ver_ll = pd.read_csv("llamadas6_verdad.csv").set_index("id_llamada")
print(t_ll[["direccion", "analistas"]].mean().round(2))
print("correlación con el tono verdadero de los analistas:",
      round(t_ll["analistas"].corr(ver_ll["tono_analistas"]), 2))
```
```output
grupo
direccion    0.75
analistas    0.01
dtype: float64
correlación con el tono verdadero de los analistas: 0.89
```

La dirección suena positiva de media (0,75) y los analistas, casi neutros (0,01). El tono de diccionario de las preguntas se
correlaciona mucho con el verdadero (0,89) porque las preguntas del generador reutilizan frases de plantilla, la mayoría con
palabras del léxico: con transcripciones reales no cabe esperar tanto y conviene un modelo (lecciones 6.1.1
y 6.2.3, donde se extraen previsiones y tono con un LLM).

## Redes sociales: cashtags, spam y bots

Las publicaciones de redes son cortas, informales y ruidosas. El corpus imita los problemas típicos: *cashtags*
(`$TICKER`) para saber de qué empresa hablan, spam con varios cashtags y un enlace, cuentas automatizadas que copian
un texto minutos después y campañas de *pump* (subir el precio de una empresa pequeña con publicaciones coordinadas).
La verdad (`social6_verdad.parquet`) dice qué es cada publicación:

```python
posts = pd.read_parquet("social6_posts.parquet")
clase = pd.read_parquet("social6_verdad.parquet")[["id_post", "clase"]]
posts = posts.merge(clase, on="id_post")
posts["cashtags"] = posts["texto"].str.findall(r"\$([A-Z]{3,5})\b")
posts["n_tags"] = posts["cashtags"].str.len()
print(posts["clase"].value_counts().to_dict())
print(f"sin cashtag reconocible: {(posts['n_tags'] == 0).mean():.1%}")
posts.groupby("clase")[["n_tags", "seguidores_autor",
                        "antiguedad_cuenta_dias"]].median()
```
```output
{'organica': 47720, 'bot_copia': 9956, 'spam': 2090, 'pump': 1931}
sin cashtag reconocible: 0.0%
           n_tags  seguidores_autor  antiguedad_cuenta_dias
clase
bot_copia     1.0              29.0                   150.0
organica      1.0              97.0                  1763.0
pump          1.0              30.0                   483.0
spam          3.0              29.0                   188.5
```

Todas las publicaciones del corpus llevan al menos un cashtag reconocible; en datos reales, las que no lo llevan no se
pueden asignar a una empresa sin resolver nombres y apodos (lección 6.7.5) y se descartan para la señal por empresa. Las medianas ya separan las clases: el spam lleva varios cashtags, y los bots y las cuentas de los pumps
tienen pocos seguidores y cuentas mucho más jóvenes que las orgánicas. Tres reglas sencillas, sin mirar la verdad:

```python
posts = posts.sort_values("ts")
spam = (posts["n_tags"] >= 2) & posts["texto"].str.contains(
    r"\.example|link|canal|bot|VIP|gratis|free", case=False)
# copia: el mismo texto ya se publicó en las 2 horas anteriores
anterior = posts.groupby("texto")["ts"].shift()
copia = (posts["ts"] - anterior) < pd.Timedelta(hours=2)
posts["regla"] = np.select([spam, copia], ["spam", "copia"], "ok")
pd.crosstab(posts["clase"], posts["regla"])
```
```output
regla      copia     ok  spam
clase
bot_copia   9956      0     0
organica       8  47712     0
pump          95   1836     0
spam           0      0  2090
```

La regla de spam encuentra las 2.090 publicaciones de spam sin tocar ninguna orgánica. La de copias encuentra las
9.956 copias de bots, con un coste pequeño: 8 publicaciones orgánicas que, por casualidad, repiten la frase de otra
persona en dos horas (en
redes reales, las frases hechas y los textos de plantilla de algunas aplicaciones hacen lo mismo). Las campañas de pump apenas
las detectan estas reglas (95 de 1.931 publicaciones): sus textos varían y no llevan enlaces. Se reconocen por el conjunto (muchas cuentas
nuevas que hablan de golpe de una empresa pequeña y un volumen anormal), y es mejor tratarlas como un riesgo de la
señal que como una señal: en el corpus, las cuatro campañas suben el precio un 48-58 % y lo devuelven en pocas
sesiones (README del nivel 6).

## Copias de agencia: una historia, un evento

En el corpus de noticias, 12.266 de las 39.570 piezas son **copias sindicadas** de otra: la misma historia en otra
agencia, de 2 minutos a 6 horas después, con el titular retocado («ACTUALIZA:», «Última hora:»), el cuerpo recortado
o traducida al otro idioma. La lección 6.1.3 mide el daño de contarlas como eventos. Para agruparlas sin la verdad,
dos claves: el **titular normalizado** (sin prefijos ni mayúsculas) y una **huella de cifras** (los números del texto,
normalizados entre la convención española y la inglesa), las dos con la empresa y una ventana de 6 horas:

```python
noticias = pd.read_parquet("noticias6_corpus.parquet").sort_values("ts")
PREFIJO = re.compile(r"^(ACTUALIZA|Última hora|UPDATE|Breaking):\s*")

def cifras(texto, idioma):
    out = set()
    for x in re.findall(r"\d[\d.,]*\d|\d", texto):
        if idioma == "es":
            x = x.replace(".", "").replace(",", ".")
        else:
            x = x.replace(",", "")
        out.add(round(float(x), 1))
    return tuple(sorted(out))

def agrupar(df, usar_cifras=True, ventana=pd.Timedelta(hours=6)):
    grupo, visto = np.arange(len(df)), {}
    for i, r in enumerate(df.itertuples()):
        claves = [("t", r.ticker, PREFIJO.sub("", r.titular).lower())]
        c = cifras(r.titular + " " + r.cuerpo, r.idioma)
        if usar_cifras and c:
            claves.append(("c", r.ticker, c))
        for k in claves:
            if k in visto and r.ts - visto[k][1] <= ventana:
                grupo[i] = visto[k][0]
                break
        for k in claves:
            visto[k] = (grupo[i], r.ts)
    return grupo

noticias["g_titular"] = agrupar(noticias, usar_cifras=False)
noticias["g_cifras"] = agrupar(noticias)
print(noticias[["g_titular", "g_cifras"]].nunique().to_dict())
```
```output
{'g_titular': 30675, 'g_cifras': 27898}
```

¿Cuánto se parecen esos grupos a las historias verdaderas? Dos medidas por pares de noticias: la **precisión** (de los
pares que el método pone juntos, cuántos son de verdad la misma historia) y la **exhaustividad** (de los pares que
son la misma historia, cuántos junta el método):

```python
from collections import Counter

historia = pd.read_csv("noticias6_verdad.csv")[["id_noticia",
                                                 "id_historia"]]
noticias = noticias.merge(historia, on="id_noticia")

def pares(etiquetas):
    return sum(n * (n - 1) // 2 for n in Counter(etiquetas).values())

def medir(col):
    juntos = pares(zip(noticias[col], noticias["id_historia"]))
    return pd.Series({
        "grupos": noticias[col].nunique(),
        "precision": juntos / pares(noticias[col]),
        "exhaustividad": juntos / pares(noticias["id_historia"])})

print("historias verdaderas:", noticias["id_historia"].nunique())
metodos = ["g_titular", "g_cifras"]
pd.DataFrame({c: medir(c) for c in metodos}).T.round(3)
```
```output
historias verdaderas: 27162
            grupos  precision  exhaustividad
g_titular  30675.0      0.999          0.653
g_cifras   27898.0      0.997          0.929
```

El titular normalizado no junta casi nunca historias distintas (precisión del 99,9 %), pero solo encuentra dos de cada
tres pares de copias: se le escapan las traducciones y los titulares reescritos. La huella de cifras sube la
exhaustividad al 92,9 % con una pérdida mínima de precisión (99,7 %) y deja 27.898 grupos frente a 27.162 historias
verdaderas: las que quedan fuera son traducciones
sin ninguna cifra en el texto (un nombramiento, una junta), que solo comparten la empresa y la hora. La figura de la
cabecera resume el resultado. Para esas, la herramienta es un embedding **multilingüe**: dos textos con el mismo
significado en dos idiomas quedan cerca (lo que MiniLM, inglés, no consigue; lección 6.1.1).

## Ejercicios

1. La regla de copias de redes marca también publicaciones orgánicas. Añade una condición sobre la cuenta (por
   ejemplo, antigüedad menor de un año) y mide de nuevo contra la verdad. ¿Qué ganas y qué pierdes?

**Solución.**

```python
joven = posts["antiguedad_cuenta_dias"] < 365
posts["regla2"] = np.select([spam, copia & joven],
                            ["spam", "copia"], "ok")
pd.crosstab(posts["clase"], posts["regla2"])
```
```output
regla2     copia     ok  spam
clase
bot_copia   5711   4245     0
organica       1  47719     0
pump          47   1884     0
spam           0      0  2090
```

La condición sobre la cuenta reduce las orgánicas marcadas como copia de 8 a 1, pero deja escapar 4.245 de las 9.956
copias, las que publican bots con cuentas de más de un año. Es el compromiso habitual de cualquier filtro: cada regla se mide con una tabla como esta
(con datos reales, sobre una muestra etiquetada a mano) y se elige según qué error es más caro para la señal.

2. Los 8-K de reexpresión (Item 4.02) anuncian que unos estados financieros ya publicados no son fiables. Lista los
   del corpus con su hora de Nueva York y di en qué sesión podría reaccionar el precio.

**Solución.**

```python
r402 = docs[docs["items"] == "4.02"].copy()
r402["hora_ny"] = r402["ts"].dt.tz_convert("America/New_York")
print(len(r402), "reexpresiones")
print(r402["hora_ny"].dt.strftime("%H:%M").agg(["min", "max"]))
r402[["ticker", "hora_ny"]].head(3)
```
```output
120 reexpresiones
min    06:30
max    07:53
Name: hora_ny, dtype: str
  ticker                   hora_ny
0   MURI 2019-01-02 07:43:04-05:00
2   ZEZU 2019-01-08 06:32:00-05:00
4   DITI 2019-01-24 07:18:08-05:00
```

Todos se presentan antes de la apertura de la NYSE, así que la sesión de reacción es la del mismo día (lección
6.1.3). En el corpus, el 10-Q/A o 10-K/A con las cifras corregidas llega esa misma tarde: el anuncio y las cifras son
dos eventos distintos, con horas distintas.

3. Diseña un proceso diario que recoja los 8-K de 500 empresas de EDGAR. ¿Cuántas peticiones necesita, cuánto tarda
   respetando la política de acceso y qué guardarías de cada envío?

**Solución.**

Una petición de `submissions` por empresa (500) más una por documento nuevo (unas decenas al día): con una espera de
0,15 s entre peticiones, unos 80 segundos, muy por debajo de las 10 peticiones por segundo. Alternativa: el índice
diario de EDGAR, una sola descarga con todos los envíos del día. De cada envío se guardaría el CIK, el número de
registro, el formulario, los items, `acceptanceDateTime` (con zona), `filingDate`, la URL y el documento original sin
modificar (para poder reprocesarlo con un parser mejor), además de la hora de descarga. El `User-Agent`, con el nombre
de la organización y un buzón de rol, en la configuración; reintentos con espera creciente ante 429 y 5xx; y un
registro de errores para no perder envíos.

[^1]: SEC, «EDGAR Application Programming Interfaces» (<https://www.sec.gov/search-filings/edgar-application-programming-interfaces>,
    consultada el 2026-10-23): historial en `data.sec.gov/submissions/CIK##########.json`, «at least one year's of
    filing or to 1,000 (whichever is more)», ficheros adicionales para los anteriores, y «these APIs do not require any
    authentication or API keys to access».
[^2]: SEC, «Accessing EDGAR Data» (<https://www.sec.gov/os/accessing-edgar-data>, consultada el 2026-10-23): «Current
    max request rate: 10 requests/second», cabecera `User-Agent` declarada, rutas
    `/Archives/edgar/data/<CIK>/<número de registro>/…` e índices diarios y completos desde 1994.
[^3]: 17 CFR 232.13(a)(2) (<https://www.law.cornell.edu/cfr/text/17/232.13>): lo enviado después de las 17:30 de la
    costa este «shall be deemed filed as of the next business day»; las excepciones hasta las 22:00, en (a)(3) y (a)(4).
[^4]: SEC, «EDGAR Full Text Search FAQ» (<https://www.sec.gov/edgar/search/efts-faq.html>, consultada el 2026-10-23):
    búsqueda en el texto de los envíos electrónicos desde 2001. La página no documenta el servicio JSON que usa.
[^5]: CNMV, registros oficiales de entidades emisoras: «Hechos relevantes de emisores de valores hasta 08/02/2020»,
    «Información privilegiada» y «Otra información relevante» (<https://www.cnmv.es/portal/otra-informacion-relevante/consulta-oir?lang=es>);
    Ley 6/2023, de 17 de marzo, de los Mercados de Valores y de los Servicios de Inversión, artículos 226 y 227
    (<https://www.boe.es/buscar/act.php?id=BOE-A-2023-7053>). Consultados el 2026-10-23.
[^6]: CNMV, canales RSS (<https://www.cnmv.es/portal/gpage?id=RSS>, consultado el 2026-10-23): información
    privilegiada, otra información relevante, notas de prensa y otros contenidos.
[^7]: SEC, Form 8-K, instrucciones generales B.1 y B.2 (<https://www.sec.gov/files/form8-k.pdf>): plazo de cuatro días
    hábiles y la información del Item 2.02 «shall not be deemed to be "filed" for purposes of Section 18».

## Autoevaluación

1. La SEC pide declarar un `User-Agent` en las peticiones automáticas a EDGAR. ¿Qué pondrías en el código de un repositorio compartido?

   - A. El nombre de la organización y un buzón de contacto genérico de la organización, leído de la configuración; nunca un correo personal escrito en el código
   - B. Tu nombre y tu correo personal, escritos en el código
   - C. Nada: la cabecera es opcional
   - D. El `User-Agent` de un navegador para pasar desapercibido

   *Respuesta A: El nombre de la organización y un buzón de contacto genérico de la organización, leído de la configuración; nunca un correo personal escrito en el código.* La política de acceso de la SEC pide identificarse (su ejemplo es «Sample Company Name AdminContact@<dominio>.com») y limita el ritmo a 10 peticiones por segundo. Un buzón de rol y la configuración fuera del código cumplen sin publicar datos personales; disfrazarse de navegador incumple la política.

2. Un 10-Q se acepta en EDGAR a las 17:52 de Nueva York de un martes. ¿Qué fecha de presentación (`filingDate`) tendrá y qué fecha conviene usar en un estudio de eventos?

   - A. El martes, y el martes
   - B. El miércoles (lo aceptado después de las 17:30 se considera presentado el siguiente día hábil); para el evento cuenta la hora de aceptación: el precio puede reaccionar desde la sesión del miércoles
   - C. El lunes
   - D. El jueves, porque la SEC tarda dos días

   *Respuesta B: El miércoles (lo aceptado después de las 17:30 se considera presentado el siguiente día hábil); para el evento cuenta la hora de aceptación: el precio puede reaccionar desde la sesión del miércoles.* La regla 13 de la Regulation S-T fija las 17:30 de la costa este como límite del día de presentación. La hora de aceptación (`acceptanceDateTime`) es la que dice cuándo pudo conocerse; la sesión de reacción se calcula con ella (lección 6.1.3).

3. Desde el 8 de febrero de 2020, ¿cómo publican los emisores españoles en la CNMV lo que antes eran «hechos relevantes»?

   - A. Siguen llamándose hechos relevantes
   - B. Solo por nota de prensa
   - C. Separados en información privilegiada (artículo 17 del Reglamento de abuso de mercado) y otra información relevante (OIR), en dos registros distintos
   - D. Solo en inglés

   *Respuesta C: Separados en información privilegiada (artículo 17 del Reglamento de abuso de mercado) y otra información relevante (OIR), en dos registros distintos.* La CNMV mantiene el registro de hechos relevantes hasta el 08/02/2020 y, desde entonces, los de información privilegiada y OIR. La Ley 6/2023 del Mercado de Valores regula hoy su difusión (artículos 226 y 227). Una serie histórica tiene que unir los dos regímenes.

4. Una noticia se publica en una agencia en español y, dos horas después, otra la traduce al inglés. ¿Qué detecta mejor la copia?

   - A. El titular idéntico
   - B. La similitud de palabras
   - C. El idioma
   - D. La empresa, una ventana de pocas horas y las cifras del texto normalizadas (3.075,9 y 3,075.9 son el mismo número)

   *Respuesta D: La empresa, una ventana de pocas horas y las cifras del texto normalizadas (3.075,9 y 3,075.9 son el mismo número).* Una traducción no comparte palabras, pero sí la empresa y las cifras. En el corpus, añadir la huella de cifras (con una ventana de 6 horas) sube la exhaustividad por pares del 65 % al 93 % sin apenas unir historias distintas (precisión del 99,7 %).

5. En el corpus de redes, las copias exactas de bots las publican cuentas nuevas minutos después del original. ¿Por qué conviene quitarlas antes de medir el sentimiento minorista?

   - A. Porque multiplican una sola opinión y la hacen pasar por consenso, y su volumen puede ser fabricado (como en las campañas de pump)
   - B. Porque ocupan disco
   - C. Porque los bots siempre son negativos
   - D. No conviene: más publicaciones dan más señal

   *Respuesta A: Porque multiplican una sola opinión y la hacen pasar por consenso, y su volumen puede ser fabricado (como en las campañas de pump).* La señal minorista es la opinión de personas distintas. Una copia de bot es la misma frase repetida; contarla infla el volumen y sesga el sentimiento hacia lo que el operador del bot quiere.

## Ideas clave

- Cada fuente tiene su formato (HTML, Markdown, JSON, texto libre), su hora y su forma de acceso. La hora de publicación es parte del dato: sin ella no hay estudio de eventos honesto.
- EDGAR da el historial de cada entidad en `data.sec.gov/submissions/CIK##########.json`, sin claves, con un máximo de 10 peticiones por segundo y un `User-Agent` declarado. En código compartido, un buzón de rol de la organización, nunca datos personales.
- Lo aceptado después de las 17:30 de Nueva York recibe la fecha de presentación del siguiente día hábil: para el evento, la hora de aceptación. En el corpus, el 29 % de los 10-K, 10-Q y sus enmiendas.
- En la CNMV, los hechos relevantes se publican hasta el 8 de febrero de 2020; después, información privilegiada y otra información relevante, con sus registros, avisos y canales RSS.
- Los hechos XBRL vienen duplicados entre periodos que coinciden (el primer trimestre es también el acumulado del año) y el cuarto trimestre no se publica: se calcula como año menos nueve meses.
- Copias de agencia, traducciones, spam y bots se eliminan antes de medir. En el corpus, una huella de titular y cifras agrupa las copias con una precisión del 99,7 % y una exhaustividad del 92,9 %; los bots se reconocen por texto repetido desde cuentas nuevas.

## Recursos

### Documentación

- [SEC: EDGAR Application Programming Interfaces (submissions y XBRL)](https://www.sec.gov/search-filings/edgar-application-programming-interfaces) · sec.gov
- [SEC: Accessing EDGAR Data (acceso justo, User-Agent, estructura de directorios)](https://www.sec.gov/os/accessing-edgar-data) · sec.gov
- [SEC: preguntas frecuentes de la búsqueda de texto completo de EDGAR](https://www.sec.gov/edgar/search/efts-faq.html) · sec.gov
- [17 CFR 232.13: fecha de presentación de los documentos electrónicos (Cornell LII)](https://www.law.cornell.edu/cfr/text/17/232.13) · law.cornell.edu
- [CNMV: consulta de otra información relevante](https://www.cnmv.es/portal/otra-informacion-relevante/consulta-oir?lang=es) · cnmv.es
- [CNMV: canales RSS (información privilegiada, OIR y otros)](https://www.cnmv.es/portal/gpage?id=RSS) · cnmv.es

### Datos

- [Datos del nivel 6: documentos regulatorios sintéticos (Parquet)](https://pythonparatrading.com/curso/datos/nivel-6/filings6_documentos.parquet) · Descarga · filings6_documentos.parquet

Lección anterior: [6.1.1 · Tokenización, embeddings, diccionarios financieros y FinBERT](https://pythonparatrading.com/curso-python-trading/nivel-6/tokenizacion-embeddings-loughran-mcdonald-y-finbert/)

Lección siguiente: [6.1.3 · Event studies con señales de texto, alineación temporal y topic modeling](https://pythonparatrading.com/curso-python-trading/nivel-6/event-studies-con-texto-y-topic-modeling/)
