Construyendo un chatbot financiero auto-evaluable: Un viaje a través de datos, código y dificultades
Construí un chatbot financiero que responde preguntas sobre el S&P 500 usando datos de la SEC — con un agente de autocrítica que mejora las respuestas antes de que las veas.
Actualización (2026): El proyecto del chatbot financiero que se describe a continuación eventualmente se convirtió en el agente S&P 500 (MVP lanzado en septiembre de 2024). Después de publicar eso, cambié el foco a Sydney para el contenido del blog y dos nuevos productos: STRATUM (inteligencia de marketing) y DIALOGUE (generación de podcasts). El pipeline de datos de la SEC y los patrones de auto-evaluación descritos aquí los informaron a todos.
La publicación original de abril de 2024 se conserva a continuación para contexto.
En esta publicación del blog, quiero compartir mi experiencia trabajando en un fascinante proyecto de hobby — construir un chatbot/agente financiero con capacidades de auto-evaluación. Este proyecto tiene como objetivo crear un agente que pueda responder preguntas sobre condiciones financieras y tendencias en las empresas del S&P 500 en EE. UU., usando datos directamente de los archivos oficiales de la SEC para garantizar la precisión y evitar alucinaciones. Los principales beneficios de este agente son (o serán :P):
- Permitirte hacer preguntas sobre condiciones financieras y tendencias en las empresas del S&P 500 en EE. UU.
- Usa datos directamente de los archivos financieros oficiales de estas empresas con la SEC, para evitar alucinaciones
- Proporciona referencias detalladas debajo de cada respuesta para que puedas verificar la respuesta si lo deseas
- La base de datos tiene los últimos 5-10 años de datos de archivos financieros para que puedas pedirle al agente que razone sobre tendencias a lo largo del tiempo
- Antes de que se te devuelva cada respuesta, hay otro agente cuyo trabajo es criticar el borrador de respuesta del LLM y proponer cómo mejorarlo
- Estas sugerencias y todo el contexto se comparten luego con el agente original. El agente puede entonces decidir formular diferentes consultas para recuperar mejor información de la base de datos o simplemente incorporar las sugerencias en la respuesta final
- Esta respuesta final se proporciona entonces al usuario.
Obviamente, no tengo acceso a la terminal de Bloomberg, así que no tengo idea de si el chatbot de Bloomberg ya puede hacer todo lo anterior. (Mi suposición informada es que puede — en cierta medida. Qué tan buena es la parte de autocrítica y la revisión, no estoy muy seguro pero me encantaría saber :P)
De todas formas, quería intentarlo ya que se siente suficientemente complejo para mi aprendizaje en esta etapa y puede ser potencialmente útil.
Quiero responder preguntas como:
- ¿Cuál es la tendencia del gasto en marketing de Apple durante los últimos 5-10 años?
- ¿Cuáles son todas las principales adquisiciones realizadas por la empresa XYZ en los últimos 5 años?
- Compara el gasto en I+D de Nvidia y Microsoft durante los últimos 5 años
- etc.
Ok, ¿dónde estoy con este proyecto? ¿Qué he aprendido? ¿Cuáles son las dificultades?
¿Cómo puedo obtener los datos de la SEC?
Uno de los desafíos iniciales que enfrenté fue obtener los datos necesarios de la SEC. Si bien la SEC proporciona guías y documentación sobre el acceso a los datos de EDGAR (y aquí), me llevó algo de tiempo comprender el proceso de descarga de archivos financieros para cada empresa a escala.
¿Existe otra forma de proceder al siguiente paso sin tener que descargar y procesar los archivos de la SEC yo mismo?
Revisé la documentación de LangChain y encontré que había un retriever financiero llamado Kay.ai. Probé el retriever para ver cómo podría funcionar el resto del flujo de trabajo. El retriever funciona como se esperaba para consultas básicas. Sin embargo, no admite llamadas asíncronas ni filtrado avanzado de metadatos. Así que decidí seguir explorando hacerlo yo mismo.
Compartiendo scripts reales de Python
Script de Python para descargar archivos financieros de SEC EDGAR
Después de muchas pruebas y errores, con la ayuda de chatGPT, este es el código que descarga automáticamente archivos XBRL y TXT de la base de datos de la Comisión de Valores y Bolsa de EE. UU. (SEC). Está diseñado para obtener archivos recientes de empresas especificadas usando su Clave de Índice Central (CIK).
¿Por qué quiero descargar tanto el archivo .zip como el archivo .txt?
El archivo .txt para cada empresa es súper completo. Tiene mucho metadato valioso sobre cada archivo, como el tipo de formulario (10K o 10Q), el período de informe, la fecha de presentación, el CIK/nombre de la empresa, etc... Estos son los tipos de metadatos que necesitaré más adelante para construir el agente. Todo esto está ordenadamente capturado en la parte superior del archivo como:
<SEC-DOCUMENT>0000320193-19-000119.txt : 20191031
<SEC-HEADER>0000320193-19-000119.hdr.sgml : 20191031
<ACCEPTANCE-DATETIME>20191030181236
ACCESSION NUMBER: 0000320193-19-000119
CONFORMED SUBMISSION TYPE: 10-K
PUBLIC DOCUMENT COUNT: 96
CONFORMED PERIOD OF REPORT: 20190928
FILED AS OF DATE: 20191031
DATE AS OF CHANGE: 20191030
FILER:
COMPANY DATA:
COMPANY CONFORMED NAME: Apple Inc.
CENTRAL INDEX KEY: 0000320193
STANDARD INDUSTRIAL CLASSIFICATION: ELECTRONIC COMPUTERS [3571]
IRS NUMBER: 942404110
STATE OF INCORPORATION: CA
FISCAL YEAR END: 0928
FILING VALUES:
FORM TYPE: 10-K
SEC ACT: 1934 Act
SEC FILE NUMBER: 001-36743
FILM NUMBER: 191181423
BUSINESS ADDRESS:
STREET 1: ONE APPLE PARK WAY
CITY: CUPERTINO
STATE: CA
ZIP: 95014
BUSINESS PHONE: (408) 996-1010
MAIL ADDRESS:
STREET 1: ONE APPLE PARK WAY
CITY: CUPERTINO
STATE: CA
ZIP: 95014
FORMER COMPANY:
FORMER CONFORMED NAME: APPLE INC
DATE OF NAME CHANGE: 20070109
FORMER COMPANY:
FORMER CONFORMED NAME: APPLE COMPUTER INC
DATE OF NAME CHANGE: 19970808
</SEC-HEADER>
El archivo .txt también tiene una tonelada de otros datos, además del informe financiero principal. Debido a eso, cada archivo es muy grande (como 10+ MB o incluso 40+ MB). Y para una sola empresa, quiero descargar todos los 10K y 10Q de los últimos 5-10 años, así que fácilmente hablamos de 20+ archivos por empresa. Intentar procesar/limpiar caracteres innecesarios/hacer chunking de estos archivos grandes es altamente improductivo porque tardará mucho tiempo en hacerse con un portátil normal y además el coste del embedding será astronómico. Así que necesitaba encontrar otra forma.
Aquí es donde entra el archivo .zip. En cada uno de los archivos .zip, encontrarás el informe financiero principal en formato .htm y otros contenidos. El problema es que estos informes financieros principales no tienen un nombre consistente a lo largo de los años, en todas las empresas. Y el informe .htm no tiene todos los valiosos metadatos en un formato ordenado como el archivo .txt.
Combinando los metadatos con el informe financiero principal
Este es el script que automatiza el proceso de extracción de estados financieros y sus metadatos asociados de los archivos de múltiples empresas. Está diseñado para manejar los archivos descargados de la base de datos SEC EDGAR, que incluye tanto archivos .zip como .txt.
Puedes ver que hice algunas suposiciones:
- Descargas tanto las versiones .txt como .zip de cada informe financiero
- El informe financiero principal es el archivo .htm más grande dentro de cada archivo .zip. Esto "debería" ser cierto ya que el otro contenido se extrae del archivo ".htm" principal
Limpiar el contenido/caracteres innecesarios antes del chunking
La buena noticia es que usando el enfoque anterior, cada informe (ya sea 10K o 10Q) ahora tiene un tamaño de menos de 3 MB. Pero todavía es demasiado largo y contiene tanta información que no necesitamos, así que necesitamos limpiarlo aún más antes del chunking. De lo contrario, el proceso de embedding se ejecutará durante mucho tiempo y costará mucho dinero. Imagina que si solo gastas $0.1 por informe para el embedding, eso ya son unos $2.5/empresa para los informes 10K y 10Q de los últimos 5 años. Si quieres cubrir la mayor parte del S&P 500 o extender el período a los últimos 10 años, los costes se acumulan muy rápidamente.
Así que aquí está el script para hacer la limpieza. Después del proceso, cada salida es inferior a 0.2 Mb, 10 veces más pequeña. Cada archivo aún tiene todos los valiosos metadatos de los que hablamos anteriormente.
Ahora, estamos listos para el paso de chunking/embedding.
¿Qué vector store debería usar?
Hay muchas opciones aquí (con más de 50 opciones de vector store). Pero como necesito hacer un filtrado avanzado en el siguiente paso usando metadatos, un retriever de auto-consulta parece adecuado. Digo "parece" porque vienen con desventajas. He experimentado con varias opciones, incluyendo Chroma, ElasticSearch y FAISS hasta ahora.
Si bien Chroma y ElasticSearch proporcionaban una funcionalidad robusta, sus tamaños de índice eran relativamente grandes (550+ MB para Chroma y 800+ MB para ElasticSearch). Estos índices solo incluyen los embeddings para 5 empresas de prueba SOLAMENTE. Esto no es bueno porque a medida que escalo al resto del S&P 500, el tamaño final del índice puede ser 100 veces mayor. De nuevo, no es adecuado para mi portátil local :D
El índice FAISS, por otro lado, es de solo unos 200+ MB para las mismas 5 empresas de prueba. FAISS carece de muchas de las funcionalidades avanzadas de filtrado/integración nativa con LangChain, así que necesito investigar más. Si tienes alguna sugerencia, ¡dímela! (¿Podría ser beneficiosa una mayor exploración de vector stores alternativos como Weaviate o Pinecone?)
Estructuración de consultas
Si bien es bueno que incluya muchos metadatos en cada archivo/chunk y que los tenga como parte de los embeddings almacenados en el vector store, ¿cómo le decimos a la máquina qué filtros usar? Lance de LangChain explicó la "Estructuración de consultas para filtros de metadatos" en este video.
Seguí el enfoque y creé el objeto pydantic a continuación. Este objeto incluye campos que corresponden a etiquetas de metadatos reales que se encuentran en los archivos financieros, como tipos de formularios (10-K, 10-Q), períodos de informe, fechas de presentación e identificadores de empresas.
import datetime
from typing import Optional
from pydantic import BaseModel, Field
class FinancialFilingsSearch(BaseModel):
"""Search over a database of financial filings for a company, using the actual metadata tags from the filings."""
content_search: str = Field(
...,
description="Similarity search query applied to the content of the financial filings with the SEC.",
)
conformed_submission_type: str = Field(
None,
description="Filter for the type of the SEC filing, such as 10-K (annual report) or 10-Q (quarterly report). ",
)
conformed_period_of_report: Optional[datetime.date] = Field(
None,
description= "Filter for the end date (format: YYYYMMDD) of the reporting period for the filing. For a 10-Q, it's the quarter-end date, and for a 10-K, it's the fiscal year-end date. ",
)
filed_as_of_date: Optional[datetime.date] = Field(
None,
description="Filter for the date (YYYYMMDD) on which the filing was officially submitted to the SEC. Only use if explicitly specified.",
)
# date_as_of_change: Optional[datetime.date] = Field(
# None,
# description="If any information in the filing was updated or amended after the initial filing date, this date reflects when those changes were made.",
# )
company_conformed_name: str = Field(
None,
description="Filter for official name of the company as registered with the SEC",
)
central_index_key: str = Field(
None,
description="Central Index Key (CIK): A unique identifier assigned by the SEC to all entities (companies, individuals, etc.) who file with the SEC.",
)
standard_industrial_classification: Optional[str] = Field(
None,
description="he Standard Industrial Classification Codes that appear in a company's disseminated EDGAR filings indicate the company's type of business. Only use if explicitly specified.",
)
# irs_number: Optional[str] = Field(
# None,
# description="IRS number to filter by.",
# )
# state_of_incorporation: Optional[str] = Field(
# None,
# description="State of incorporation to filter by.",
# )
# fiscal_year_end: Optional[str] = Field(
# None,
# description="The end date of the company's fiscal year, which is used for financial reporting and taxation purposes, like Dec 31 or Sep30",
# )
form_type: str = Field(
None,
description="Form type to filter by, such as 10-K or 10-Q.",
)
# sec_file_number: Optional[str] = Field(
# None,
# description="SEC file number to filter by.",
# )
# film_number: Optional[str] = Field(
# None,
# description="Film number to filter by.",
# )
# former_company: Optional[str] = Field(
# None,
# description="Former company name to filter by.",
# )
# former_conformed_name: Optional[str] = Field(
# None,
# description="Former conformed name to filter by.",
# )
# date_of_name_change: Optional[datetime.date] = Field(
# None,
# description="Date of name change to consider.",
# )
# Set up language models
llm_35 = ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0) # GPT-3.5 model
llm_4 = ChatOpenAI(model="gpt-4-turbo-2024-04-09", temperature=0) # GPT-4 model for more complex tasks
from langchain_core.prompts import ChatPromptTemplate
system = """You are an expert at converting user questions into database queries. \
You have access to a vector store of financial filings from public companies to the SEC, for building LLM-powered application. \
Given a question, return a detailed database query optimized to retrieve the most relevant results. \
Be as detailed as possible with your returned query, including all relevant fields and filters. \
Always include conformed_period_of_report. \
If there are acronyms or words you are not familiar with, do not try to rephrase them."""
prompt = ChatPromptTemplate.from_messages(
[
("system", system),
("human", "\{question\}"),
]
)
# Assuming `llm` is your already initialized LLM instance
structured_llm = llm_35.with_structured_output(FinancialFilingsSearch)
query_analyzer = prompt | structured_llm
Por ejemplo, esta es la lista de preguntas y las respuestas del LLM. Puedes ver que el LLM omite el "conformed period of reporting" en algunos casos:
Question: What was Google's advertising and marketing spending in the 10-K report for the year 2018?
\{'content_search': 'advertising and marketing spending', 'company_conformed_name': 'Alphabet Inc.', 'conformed_submission_type': '10-K', 'form_type': '10-K', 'central_index_key': '0001652044'\}
Question: What was Google's advertising and marketing spending in the 10-K report for the year 2019?
\{'content_search': 'advertising and marketing spending', 'company_conformed_name': 'Alphabet Inc.', 'conformed_submission_type': '10-K', 'form_type': '10-K', 'central_index_key': '0001652044'\}
Question: What was Google's advertising and marketing spending in the 10-K report for the year 2020?
\{'content_search': 'advertising and marketing spending', 'company_conformed_name': 'Alphabet Inc.', 'conformed_submission_type': '10-K', 'form_type': '10-K', 'conformed_period_of_report': '2020'\}
Question: What was Google's advertising and marketing spending in the 10-K report for the year 2021?
\{'content_search': 'advertising and marketing spending', 'company_conformed_name': 'Alphabet Inc.', 'conformed_submission_type': '10-K', 'form_type': '10-K', 'conformed_period_of_report': '2021'\}
Question: What was Google's advertising and marketing spending in the 10-K report for the year 2022?
\{'content_search': 'advertising and marketing spending', 'company_conformed_name': 'Alphabet Inc.', 'form_type': '10-K'\}
Question: How has Google's advertising and marketing spending trended from 2018 to 2022 according to 10-K filings?
\{'content_search': 'advertising and marketing spending', 'company_conformed_name': 'Alphabet Inc.', 'form_type': '10-K'\}
Ok, ¿qué espero lograr con esta larga (LARGA) publicación?
1. Que los códigos de Python compartidos te ayuden de alguna manera.
2. Que puedas compartir tus pensamientos/comentarios o asesorarme sobre:
- ¿Cómo mejorar la estructuración de consultas que estoy haciendo? Por tu información, también estoy usando el LLM para generar sub-preguntas relacionadas con una pregunta de entrada.
- ¿Cómo puedo mejorar el embedding/uso del vector store, especialmente la parte de filtrado?
- o cualquier otra sugerencia en la que haya pensado :)
Actualización julio 2024
Este chatbot AÚN NO está funcionando, así que si intentas usar el chatbot actual de mi sitio y haces preguntas financieras, no las sabe :D
Ten paciencia con la brecha de curiosidad :)
¿Has trabajado con archivos de la SEC o has intentado construir un agente de auto-evaluación? Me encantaría saber cómo abordaste los desafíos del vector store y el filtrado de metadatos.
Un abrazo,
Chandler
P.D.: Hay un nuevo curso que planeo tomar "Generative AI for Software Development Skill Certificate" en Coursera.





