Skip to content
··10 Min. Lesezeit

Einen selbstevaluierenden Finanz-Chatbot bauen: Eine Reise durch Daten, Code und Herausforderungen

Ich habe einen Finanz-Chatbot gebaut, der S&P-500-Fragen anhand von SEC-Daten beantwortet – mit einem Selbstkritik-Agenten, der Antworten verbessert, bevor du sie siehst.

Update (2026): Das unten beschriebene Finanz-Chatbot-Projekt wurde schließlich zum S&P-500-Agenten (MVP gestartet im Sep 2024). Danach verlagerte ich den Fokus auf Sydney für Blog-Inhalte und zwei neue Produkte: STRATUM (Marketing Intelligence) und DIALOGUE (Podcast-Generierung). Die hier beschriebenen SEC-Datenpipeline- und Selbstevaluierungsmuster haben alle beeinflusst.

Sydney fragen →


Originalpost vom April 2024, unten zur Referenz erhalten.

In diesem Blogbeitrag möchte ich meine Erfahrungen bei der Arbeit an einem faszinierenden Hobbyprojekt teilen – dem Aufbau eines Finanz-Chatbots/Agenten mit Selbstevaluierungsfähigkeiten. Dieses Projekt zielt darauf ab, einen Agenten zu schaffen, der Fragen zu Finanzbedingungen und -trends bei Unternehmen im S&P 500 in den USA beantworten kann, indem er Daten direkt aus offiziellen SEC-Einreichungen verwendet, um Genauigkeit zu gewährleisten und Halluzinationen zu vermeiden. Die Hauptvorteile dieses Agenten sind (oder werden sein :P):

  • Er ermöglicht es dir, Fragen zu Finanzbedingungen und -trends bei Unternehmen im S&P 500 in den USA zu stellen.
  • Er verwendet Daten direkt aus den offiziellen Finanzeinreichungen dieser Unternehmen bei der SEC, um Halluzinationen zu vermeiden
  • Er liefert detaillierte Referenzen unter jeder Antwort, damit du die Antwort nach Wunsch überprüfen kannst
  • Die Datenbank enthält Finanzeinreichungsdaten der letzten 5–10 Jahre, sodass du den Agenten bitten kannst, über Trends im Zeitverlauf nachzudenken
  • Bevor jede Antwort an dich zurückgegeben wird, gibt es einen weiteren Agenten, dessen Aufgabe es ist, die Entwurfsantwort des LLM zu kritisieren und Verbesserungsvorschläge zu machen
  • Diese Vorschläge und der gesamte Kontext werden dann mit dem ursprünglichen Agenten geteilt. Der Agent kann dann entscheiden, unterschiedliche Abfragen zu formulieren, um bessere Informationen aus der Datenbank abzurufen, oder die Vorschläge einfach in die endgültige Antwort zu integrieren
  • Diese endgültige Antwort wird dann dem Benutzer bereitgestellt.

Offensichtlich habe ich keinen Zugang zum Bloomberg-Terminal, also habe ich keine Ahnung, ob der Bloomberg-Chatbot all das bereits kann. (Meine informierte Vermutung ist, dass er es in gewissem Maße kann. Wie gut der Selbstkritik-Teil und die Überarbeitung ist, bin ich nicht so sicher – aber ich würde es gerne wissen :P)

Jedenfalls wollte ich das ausprobieren, da es sich für mein Lernen auf diesem Level komplex genug anfühlt und potenziell nützlich sein kann.

Ich möchte Fragen wie diese beantworten:

  • Wie hat sich Apples Marketing-Ausgaben-Trend über die letzten 5–10 Jahre entwickelt?
  • Was sind alle großen Übernahmen, die XYZ-Unternehmen in den letzten 5 Jahren getätigt hat?
  • Vergleiche die F&E-Ausgaben von Nvidia und Microsoft über die letzten 5 Jahre
  • usw.

Also, wo stehe ich mit diesem Projekt? Was habe ich gelernt? Was sind die Herausforderungen?

Wie kann ich die Daten von der SEC erhalten?

Eine der anfänglichen Herausforderungen war das Abrufen der notwendigen Daten von der SEC. Obwohl die SEC Anleitungen und Dokumentationen zum Zugriff auf EDGAR-Daten bereitstellt (und hier), dauerte es einige Zeit, den Prozess des Herunterladens von Finanzeinreichungen für jedes Unternehmen in großem Maßstab zu verstehen.

Gibt es eine andere Möglichkeit, zum nächsten Schritt vorzugehen, ohne die Einreichungen von der SEC selbst herunterladen und verarbeiten zu müssen?

Ich habe die LangChain-Dokumentation durchgesehen und festgestellt, dass es einen Finanzretriever namens Kay.ai gab. Ich testete den Retriever, um zu sehen, wie der Rest des Workflows funktionieren könnte. Der Retriever funktioniert für grundlegende Abfragen wie erwartet. Er unterstützt jedoch keine asynchronen Aufrufe oder erweiterte Metadaten-Filterung. Also entschied ich mich, diesen Teil weiterhin selbst zu erkunden.

Teilen von echten Python-Skripten

Python-Skript zum Herunterladen von Finanzeinreichungen von SEC EDGAR

Nach vielen Versuchen und Fehlern, mit Hilfe von chatGPT, ist das der Code, der automatisch XBRL- und TXT-Einreichungen aus der U.S. Securities and Exchange Commission (SEC)-Datenbank herunterlädt. Er ist dafür konzipiert, aktuelle Einreichungen für bestimmte Unternehmen anhand ihrer Central Index Key (CIK) abzurufen.

Warum will ich sowohl die .zip-Datei als auch die .txt-Datei herunterladen?

Die .txt-Datei für jedes Unternehmen ist super umfassend. Sie enthält viele wertvolle Metadaten zu jeder Einreichung, wie den Formulartyp (10K oder 10Q), den Berichtszeitraum, das Einreichungsdatum, die Unternehmens-CIK/-name usw. Das sind genau die Arten von Metadaten, die ich später für den Aufbau des Agenten benötige. All das ist ordentlich am Anfang der Datei erfasst, wie:

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

Die .txt-Datei enthält neben dem Kernfinanzbericht noch viele andere Daten. Daher ist jede Datei sehr groß (z.B. 10+ MB oder sogar 40+ MB). Und für ein Unternehmen möchte ich alle 10K und 10Q der letzten 5–10 Jahre herunterladen, was leicht über 20 Dateien pro Unternehmen bedeutet. Das Verarbeiten/Bereinigen unnötiger Zeichen/Chunking dieser großen Dateien ist äußerst unproduktiv, weil es auf einem normalen Laptop lange dauern wird und die Embedding-Kosten astronomisch sein werden. Also musste ich einen anderen Weg finden.

Hier kommt die .zip-Datei ins Spiel. In jeder der .zip-Dateien findest du den Kernfinanzbericht im .htm-Format und anderen Inhalt. Das Problem ist, dass diese Kernfinanzberichte über die Jahre hinweg und über Unternehmen hinweg nicht einheitlich benannt sind. Und der .htm-Bericht hat nicht alle wertvollen Metadaten in einem so ordentlichen Format wie die .txt-Datei.

Metadaten mit dem Hauptfinanzbericht kombinieren

Das ist das Skript, das den Prozess der Extraktion von Finanzberichten und ihrer zugehörigen Metadaten aus den Einreichungen mehrerer Unternehmen automatisiert. Es ist dafür konzipiert, die von der SEC EDGAR-Datenbank heruntergeladenen Dateien zu verarbeiten, die sowohl .zip- als auch .txt-Dateien enthält.

Du kannst sehen, dass ich einige Annahmen gemacht habe:

  • Du lädst sowohl die .txt- als auch die .zip-Versionen für jeden Finanzbericht herunter
  • Der Kernfinanzbericht ist die größte .htm-Datei innerhalb jeder .zip-Datei. Das „sollte" zutreffen, da der andere Inhalt aus der Haupt-„.htm"-Datei extrahiert wird

Unnötigen Inhalt/Zeichen vor dem Chunking bereinigen

Die gute Nachricht ist, dass jeder Bericht (entweder 10K oder 10Q) mit dem obigen Ansatz jetzt eine Größe von weniger als 3 MB hat. Aber er ist immer noch viel zu lang und enthält so viele Informationen, die wir nicht brauchen, sodass wir ihn noch weiter bereinigen müssen, bevor wir ihn chunken. Andernfalls wird der Embedding-Prozess sehr lange laufen und viel Geld kosten. Stell dir vor, wenn du nur 0,10 $ pro Bericht für das Embedding ausgibst, sind das bereits ca. 2,50 $ pro Unternehmen für 10K und 10Q-Berichte der letzten 5 Jahre. Wenn du die meisten S&P-500-Unternehmen abdecken oder den Zeitraum auf die letzten 10 Jahre ausdehnen möchtest, summiert sich das schnell.

Also ist hier das Skript für die Bereinigung. Nach dem Prozess ist jeder Output weniger als 0,2 MB, 10x kleiner. Jede Datei enthält immer noch alle wertvollen Metadaten, über die wir vorhin gesprochen haben.

Jetzt sind wir bereit für den Chunking/Embedding-Schritt.

Welchen Vektorspeicher sollte ich verwenden?

Es gibt viele Optionen hier (mit mehr als 50 Vektorspeicheroptionen). Da ich jedoch im nächsten Schritt erweiterte Filterung mit Metadaten durchführen muss, scheint ein Self-Querying-Retriever geeignet zu sein. Ich sage „scheint", weil sie auch Nachteile haben. Ich habe bisher mit mehreren Optionen experimentiert, darunter Chroma, ElasticSearch und FAISS.

Während Chroma und ElasticSearch robuste Funktionalität boten, waren ihre Indexgrößen relativ groß (550+ MB für Chroma und 800+ MB für ElasticSearch). Diese Indizes enthalten nur Embeddings für 5 Testunternehmen. Das ist nicht gut, weil die endgültige Indexgröße bei der Skalierung auf den Rest des S&P 500 100x größer sein kann. Auch wieder nicht geeignet für meinen lokalen Laptop :D

FAISS-Index hingegen ist für dieselben 5 Testunternehmen nur etwa 200+ MB. FAISS fehlt jedoch viel von der erweiterten Filterung/nativen Integration mit LangChain, also muss ich weiter untersuchen. Wenn du Vorschläge hast, lass es mich wissen. (Weitere Erkundung alternativer Vektorspeicher wie Weaviate oder Pinecone könnte sinnvoll sein?)

Query Structuring

Obwohl es gut ist, dass ich viele Metadaten in jede Einreichung/jeden Chunk aufnehme und sie als Teil der Embeddings im Vektorspeicher speichere, wie sagen wir der Maschine, welche Filter zu verwenden sind? Lance von LangChain erklärte „Query structuring for metadata filters" in diesem Video.

Ich folgte dem Ansatz und erstellte das unten stehende pydantic-Objekt. Dieses Objekt enthält Felder, die tatsächlichen Metadaten-Tags aus Finanzeinreichungen entsprechen, wie Formulartypen (10-K, 10-Q), Berichtszeiträume, Einreichungsdaten und Unternehmenskennungen.

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

Zum Beispiel ist dies die Liste der Fragen und die Antworten des LLM. Du kannst sehen, dass das LLM in einigen Fällen den „conformed period of reporting" auslässt:

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, was erhofft mir dieser lange (LANGE) Beitrag zu bewirken?

1. Die geteilten Python-Codes helfen dir in irgendeiner Weise.

2. Du kannst deine Gedanken/Kommentare zurückteilen oder mich beraten zu:

  • Wie kann ich das Query-Structuring verbessern, das ich mache? Hinweis: Ich verwende auch das LLM, um Unter-Fragen zu einer Eingabefrage zu generieren.
  • Wie kann ich das Embedding/die Nutzung des Vekterspeichers verbessern, besonders den Filterteil?
  • Oder andere Vorschläge, die mir noch nicht eingefallen sind :)

Update Juli 2024

Dieser Chatbot funktioniert NOCH NICHT – wenn du also den aktuellen Chatbot auf meiner Website ausprobierst und Finanzfragen stellst, weiß er nichts :D

Bleib neugierig :)

Hast du mit SEC-Einreichungen gearbeitet oder versucht, einen selbstevaluierenden Agenten zu bauen? Ich würde gerne hören, wie du die Vektorspeicher- und Metadaten-Filterungs-Herausforderungen angegangen bist.

Viele Grüße,

Chandler

P.S: Es gibt einen neu veröffentlichten Kurs, den ich zu absolvieren plane: „Generative AI for Software Development Skill Certificate" auf Coursera.

Weiterlesen

Mein Weg
Vernetzen
Sprache
Einstellungen