
Ein Trading-Agent ist in diesem Projekt kein System, das selbstständig Aktien kauft oder den nächsten Kurs vorhersagt. Er ist ein lokaler Entscheidungsassistent: Er kennt die Transaktionshistorie und den aktuellen Bestand, ordnet Watchlist-Kandidaten ein, prüft Daten und Portfolio-Grenzen und bereitet daraus eine nachvollziehbare Empfehlung vor.
Der eigentliche Anspruch besteht nicht darin, möglichst viele Kennzahlen in einen Prompt zu schreiben. Entscheidend ist die Reihenfolge: Fakten werden importiert und validiert, SQLite speichert sie dauerhaft, spezialisierte Python-Module berechnen und prüfen die Regeln, OpenWebUI stellt die Werkzeuge bereit und das lokale Sprachmodell erklärt das Ergebnis. Eine Order bleibt immer eine bewusste Entscheidung des Menschen.
Diese Seite beschreibt den Gesamtaufbau des Projekts. Die vertiefenden Artikel behandeln anschließend die Datenarchitektur sowie Watchlist und Portfolio-Fit im Detail.
Warum ein eigener lokaler Trading-Agent?
Eine einzelne Aktienanalyse lässt sich in jedem Chat führen. Schwieriger wird es, wenn die Analyse den tatsächlichen Depotkontext berücksichtigen soll: Welche Positionen bestehen bereits? Welche Käufe, Teilverkäufe und Gewinnmitnahmen gab es? Ist eine Aktie zwar interessant, würde das Depot damit aber zu stark in einen Sektor oder Faktor konzentriert? Und auf welche Daten stützt sich diese Einschätzung?
Genau für diese Fragen entsteht der Trading-Agent. Er trennt Beobachtung von Strategie, Analyse von Ausführung und Modelltext von der eigentlichen Datenbasis. Statt sich auf eine alte Chat-Antwort zu verlassen, kann eine neue Prüfung stets auf den vorhandenen Transaktionen, Kursreihen, Marktsnapshots und validierten Fundamentaldaten aufsetzen.
Der Schwerpunkt liegt auf Swing-Entscheidungen mit einem Horizont von etwa drei bis sechs Monaten. Langfristige ETFs bilden den Kern des Depots; aktiv gesteuerte Einzelaktien und Swing-Positionen ergänzen ihn. Eine gute Unternehmensanalyse reicht dafür nicht aus: Auch Positionsgröße, Liquidität, laufende Kampagnen und das bestehende Exposure gehören zur Entscheidung.
Was „lokal“ in diesem Projekt bedeutet
Lokal bedeutet nicht, dass niemals eine externe Quelle genutzt wird. Kurse, Unternehmensberichte oder offizielle Investor-Relations-Unterlagen können selbstverständlich aus dem Netz stammen. Lokal bleiben jedoch die Datenhaltung, die Rechenlogik, die Tool-Schicht und die Modellinferenz: Die Trading-Daten liegen in einer eigenen SQLite-Datenbank, die Workflows laufen als Python-Code im eigenen KI-Stack und OpenWebUI ist die lokale Bedienoberfläche.
Das schafft drei Vorteile. Erstens bleibt die entscheidende Datenbasis unter eigener Kontrolle. Zweitens sind Berechnungen mit einem Stichtag erneut ausführbar, statt nur aus einer Chat-Erinnerung zu bestehen. Drittens lassen sich klare Grenzen technisch erzwingen: Ein Sprachmodell kann eine Empfehlung erklären, aber weder die Datenbank beliebig umschreiben noch eine Broker-Order auslösen.
Architektur: Daten, Regeln, Modell und Mensch
Der Trading-Agent ist eine spezialisierte Anwendung innerhalb des lokalen KI-Stacks. Er besteht nicht aus einem großen monolithischen Programm, sondern aus klar getrennten Schichten. Jede Schicht hat eine eigene Aufgabe und darf nicht stillschweigend die Verantwortung einer anderen übernehmen.
Parqet-Export und externe Datenquellen
↓
Python: Import, Zuordnung, Normalisierung, Validierung
↓
SQLite: Transaktionen, Positionen, Markt- und Fundamentaldaten
↓
Python: Analyse, Guardrails, Sizing, Swing-Lifecycle, Orchestrierung
↓
OpenWebUI: Agentenprofil, Tool-Aufruf und Ergebnisdarstellung
↓
lokales Sprachmodell: Einordnung und Erklärung
↓
Mensch: Kauf, Verkauf oder keine Aktion
Der wichtigste Grundsatz lautet: Das Modell ist nicht die Datenhaltung und der Prompt ist nicht die Handelslogik. Python und SQLite liefern die belastbaren Fakten und erzwingen die Regeln. Das Sprachmodell fasst sie zusammen, beantwortet Rückfragen und macht die Begründung lesbar.
Die Datenbasis: Fakten statt Chat-Erinnerung
Die zentrale SQLite-Datenbank ist der verbindliche Speicher für die Fakten des Trading-Agenten. Sie hält Stammdaten, Buchungen, Markt- und Fundamentaldaten sowie die daraus abgeleiteten Arbeitszustände getrennt. Die aktuelle Schema-Version ist 2.0. Alle nachfolgend genannten Tabellen, Felder und Beziehungen stammen aus diesem Schema; es werden keine beispielhaften oder hypothetischen Felder ergänzt.
Die wichtigste Regel lautet: transactions ist die historische Wahrheit des Depots. positions ist daraus abgeleitet und kann neu aufgebaut werden. Ein Importer schreibt daher zuerst fachlich geprüfte Buchungen und berechnet anschließend ausschließlich die betroffenen Positionen neu.
Datenbankschema: vollständige Tabellen- und Schlüsselreferenz

Legende: PK = Primärschlüssel, FK = Fremdschlüssel. INTEGER PRIMARY KEY AUTOINCREMENT erzeugt eine fortlaufende technische ID. Bei Tabellen mit security_id INTEGER PRIMARY KEY ist dieselbe ID zugleich Primär- und Fremdschlüssel: Es kann genau eine Zeile pro Wertpapier geben.
security.id
├─< source_symbols.security_id ──> data_sources.id
├─< transactions.security_id ──> swing_campaign_event.transaction_id
├── positions.security_id
├── watchlist.security_id
├── market_snapshot.security_id ──> data_sources.id
├─< market_data.security_id ──> data_sources.id
├─< fundamentals / estimates / ratings / price_targets / events / news .security_id ──> data_sources.id
├─< decisions.security_id <─ analysis_history.decision_id
├─< analysis_history.security_id
├─< strategy_assignment.security_id <─ swing_campaign.strategy_assignment_id
├─< candidate_promotion.security_id
└─< swing_campaign.security_id <─ swing_campaign_event.campaign_id
metadata, imports und fx_rates sind eigenständige technische Tabellen ohne Fremdschlüssel.
Die Pfeile zeigen stets auf den referenzierten Schlüssel. Die meisten fachlichen Daten hängen mit ON DELETE CASCADE an security.id: Wird ein Wertpapier entfernt, werden seine abhängigen Daten mit entfernt. Bei data_sources gibt es bewusst kein Cascade; eine Datenquelle darf nicht verschwinden, solange Datensätze auf sie verweisen.
Technische Basis und Wertpapieridentität
| Tabelle und Schlüssel | Felder – vollständig | Beziehung, Regeln und Zweck |
|---|---|---|
metadataPK key TEXT | key TEXT: technischer Name; value TEXT: gespeicherter Wert; updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP: letzte Änderung. | Schema- und Betriebsmetadaten, etwa schema_version. Keine FK. |
data_sourcesPK id INTEGER | id INTEGER PRIMARY KEY AUTOINCREMENT: Quellen-ID; name TEXT NOT NULL UNIQUE: eindeutiger Quellenname; source_type TEXT: Quellentyp; url TEXT: Herkunftsadresse; active INTEGER NOT NULL DEFAULT 1: aktiv/inaktiv; created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP: Anlagezeit. | Zentrales Verzeichnis externer Datenlieferanten. source_id in neun fachlichen Tabellen referenziert diese ID. |
securityPK id INTEGER | id INTEGER PRIMARY KEY AUTOINCREMENT: interne Wertpapier-ID; symbol TEXT, isin TEXT, wkn TEXT: Kennungen; name TEXT NOT NULL: Bezeichnung; exchange TEXT, currency TEXT, country TEXT: Handelsplatz, Währung, Land; asset_type TEXT NOT NULL DEFAULT 'stock': Instrumentart; sector TEXT, industry TEXT: Klassifikation; active INTEGER NOT NULL DEFAULT 1: aktiv/inaktiv; created_at, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP: Zeitstempel. | Der Identitätsanker des Modells. ISIN ist, falls vorhanden, eindeutig. Indizes beschleunigen Suche über ISIN, Symbol, Name und Aktivstatus. |
source_symbolsPK id INTEGERFK security_id, source_id | id INTEGER PRIMARY KEY AUTOINCREMENT: Mapping-ID; security_id INTEGER NOT NULL: Wertpapier; source_id INTEGER NOT NULL: Datenquelle; symbol TEXT NOT NULL: Provider-Symbol; exchange TEXT, currency TEXT: Provider-Kontext; verified_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP: geprüfter Zeitpunkt. | security_id → security.id ON DELETE CASCADE; source_id → data_sources.id ON DELETE CASCADE. Genau ein Provider-Symbol je Kombination aus Wertpapier und Quelle (UNIQUE(security_id, source_id)). |
Import, Buchungen, Bestand und Watchlist
| Tabelle und Schlüssel | Felder – vollständig | Beziehung, Regeln und Zweck |
|---|---|---|
importsPK id INTEGER | id INTEGER PRIMARY KEY AUTOINCREMENT: Lauf-ID; import_type TEXT NOT NULL: Art des Imports; source TEXT: Quellbezeichnung; file_name TEXT: Datei; started_at, completed_at TEXT: Laufzeit; records_total, records_imported, records_failed INTEGER: Zähler; status TEXT: Ergebnis; notes TEXT: Hinweise; created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP: Anlagezeit. | Protokolliert einen Importlauf. Keine FK; die Tabelle dokumentiert den Ablauf, sie ist nicht die Buchungshistorie. |
transactionsPK id INTEGERFK security_id | id INTEGER PRIMARY KEY AUTOINCREMENT: Buchungs-ID; security_id INTEGER NOT NULL: Wertpapier; transaction_type TEXT NOT NULL: fachlicher Buchungstyp; transaction_date TEXT NOT NULL: Buchungsdatum; shares, price, amount REAL: Menge, Einzelpreis, Betrag; fees, taxes REAL DEFAULT 0: Nebenkosten; currency TEXT: Währung; broker TEXT: Depotstelle; external_id TEXT: Quell-ID; notes TEXT: Zusatzinfo; created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP: Anlagezeit. | security_id → security.id ON DELETE CASCADE. external_id ist, falls gesetzt, eindeutig. Indizes auf (security_id, transaction_date) und Datum sichern die zeitliche Rekonstruktion der Positionen. |
positionsPK/FK security_id INTEGER | security_id INTEGER PRIMARY KEY: Wertpapier und eindeutiger Positionsschlüssel; shares REAL NOT NULL DEFAULT 0: Bestand; avg_cost, remaining_cost_basis REAL: Durchschnitts- und Restkostenbasis; currency TEXT: Kostenbasiswährung; invested_amount REAL: investierter Betrag; realized_gain REAL DEFAULT 0: realisiertes Ergebnis; first_transaction_at, last_transaction_at TEXT: zeitliche Grenzen; transaction_count INTEGER NOT NULL DEFAULT 0: Zahl der Buchungen; updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP: Rechenzeitpunkt. | security_id → security.id ON DELETE CASCADE. Eins-zu-eins-Abbild des aktuellen, aus transactions berechneten Zustands; niemals Ersatz für Buchungen. |
watchlistPK/FK security_id INTEGER | security_id INTEGER PRIMARY KEY: beobachtetes Wertpapier; status TEXT NOT NULL DEFAULT 'WATCH': Beobachtungsstatus; priority INTEGER: Reihenfolge; entry_reason, thesis TEXT: Anlass und These; target_entry REAL: beobachteter Einstieg; notes TEXT: Notizen; added_at, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP: Zeitstempel. | security_id → security.id ON DELETE CASCADE. Genau eine Watchlist-Zeile je Wertpapier; Indizes auf Status und Priorität. |
Marktdaten, Berichte und externe Evidenz
| Tabelle und Schlüssel | Felder – vollständig | Beziehung, Regeln und Zweck |
|---|---|---|
market_snapshotPK/FK security_idFK source_id | security_id INTEGER PRIMARY KEY: Wertpapier; as_of_at TEXT: Stichtag; price, previous_close, open, high, low REAL: Kurswerte; volume REAL: Volumen; market_cap, shares_outstanding REAL: Marktkapitalisierung und Aktienzahl; currency TEXT: Kurswährung; source_id INTEGER: Quelle; fetched_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP: Abrufzeit. | security_id → security.id ON DELETE CASCADE; source_id → data_sources.id. Kompakter letzter Marktstand, eine Zeile je Wertpapier. |
market_dataPK id INTEGERFK security_id, source_id | id INTEGER PRIMARY KEY AUTOINCREMENT: Kurszeilen-ID; security_id INTEGER NOT NULL: Wertpapier; trade_date TEXT NOT NULL: Handelstag; open, high, low, close, adjusted_close REAL: OHLC und adjustierter Schluss; volume REAL: Volumen; currency TEXT: Währung; source_id INTEGER: Quelle; fetched_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP: Abrufzeit. | security_id → security.id ON DELETE CASCADE; source_id → data_sources.id. Eindeutig je (security_id, trade_date, source_id); Basis für SMA und Momentum. |
fundamentalsPK id INTEGERFK security_id, source_id | id INTEGER PRIMARY KEY AUTOINCREMENT; security_id INTEGER NOT NULL; period_end TEXT NOT NULL, period_type TEXT NOT NULL: Berichtsperiode; fiscal_year INTEGER, fiscal_quarter INTEGER: Geschäftsjahr/-quartal; filing_date TEXT: Veröffentlichungsdatum; currency TEXT: Währung; revenue, gross_profit, operating_income, ebit, ebitda, net_income REAL: GuV; eps_basic, eps_diluted REAL: Ergebnis je Aktie; operating_cash_flow, capex, free_cash_flow REAL: Cashflow; cash, total_debt, total_assets, total_liabilities, total_equity, shares_outstanding REAL: Bilanz/Anteile; source_id INTEGER; fetched_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP. | security_id → security.id ON DELETE CASCADE; source_id → data_sources.id. Eindeutig je Wertpapier, Periodenende, Periodentyp und Quelle. |
estimatesPK id INTEGERFK security_id, source_id | id INTEGER PRIMARY KEY AUTOINCREMENT; security_id INTEGER NOT NULL; metric TEXT NOT NULL: geschätzte Kennzahl; period_end TEXT NOT NULL, period_type TEXT: Bezugsperiode; as_of_date TEXT NOT NULL: Snapshotdatum; estimate_mean, estimate_median, estimate_high, estimate_low REAL: Konsensband; analyst_count INTEGER; currency TEXT; source_id INTEGER; fetched_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP. | security_id → security.id ON DELETE CASCADE; source_id → data_sources.id. Eindeutig je Wertpapier, Metrik, Periode, Snapshot und Quelle; erlaubt die Berechnung von Revisionen. |
ratingsPK id INTEGERFK security_id, source_id | id INTEGER PRIMARY KEY AUTOINCREMENT; security_id INTEGER NOT NULL; as_of_date TEXT NOT NULL: Snapshotdatum; strong_buy, buy, hold, sell, strong_sell INTEGER: Zahl der Empfehlungen; analyst_count INTEGER: Abdeckung; source_id INTEGER; fetched_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP. | security_id → security.id ON DELETE CASCADE; source_id → data_sources.id. Eindeutig je Wertpapier, Datum und Quelle. |
price_targetsPK id INTEGERFK security_id, source_id | id INTEGER PRIMARY KEY AUTOINCREMENT; security_id INTEGER NOT NULL; as_of_date TEXT NOT NULL; target_mean, target_median, target_high, target_low REAL: Kurszielband; analyst_count INTEGER; currency TEXT; source_id INTEGER; fetched_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP. | security_id → security.id ON DELETE CASCADE; source_id → data_sources.id. Eindeutig je Wertpapier, Datum und Quelle. |
eventsPK id INTEGERFK security_id, source_id | id INTEGER PRIMARY KEY AUTOINCREMENT; security_id INTEGER NOT NULL; event_type TEXT NOT NULL: Ereignisart; event_date TEXT NOT NULL: Datum; period_end TEXT: zugehörige Periode; title TEXT: Titel; actual_value, estimated_value, surprise_percent REAL: Ergebnis, Erwartung, Überraschung; currency TEXT; notes TEXT; source_id INTEGER; created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP. | security_id → security.id ON DELETE CASCADE; source_id → data_sources.id. Dauerhafte kursrelevante Ereignisse. |
newsPK id INTEGERFK security_id, source_id | id INTEGER PRIMARY KEY AUTOINCREMENT; security_id INTEGER NOT NULL; published_at TEXT NOT NULL; title TEXT NOT NULL, summary TEXT; url TEXT; publisher TEXT, category TEXT; sentiment REAL; is_material INTEGER NOT NULL DEFAULT 0: dauerhaft relevant; expires_at TEXT: Ablauf eines Cache-Eintrags; source_id INTEGER; fetched_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP. | security_id → security.id ON DELETE CASCADE; source_id → data_sources.id. URL ist, wenn vorhanden, eindeutig; nicht materielle News können ablaufen. |
Entscheidungen, Strategie und Kampagnen
| Tabelle und Schlüssel | Felder – vollständig | Beziehung, Regeln und Zweck |
|---|---|---|
decisionsPK id INTEGERFK security_id | id INTEGER PRIMARY KEY AUTOINCREMENT; security_id INTEGER NOT NULL; decision_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP; status TEXT NOT NULL; confidence REAL; price_at_decision REAL; fundamental_score, valuation_score, momentum_score, balance_sheet_score, risk_score, portfolio_fit_score REAL: Teilbewertungen; reason TEXT, next_action TEXT; created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP. | security_id → security.id ON DELETE CASCADE. Strukturierte, zeitbezogene Entscheidungsvorlage. |
analysis_historyPK id INTEGERFK security_id, decision_id | id INTEGER PRIMARY KEY AUTOINCREMENT; security_id INTEGER NOT NULL; analysis_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP; analysis_type TEXT; summary TEXT, full_analysis TEXT: Kurz- und Langform; model TEXT: verwendetes Modell; decision_id INTEGER: optionale Entscheidung; created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP. | security_id → security.id ON DELETE CASCADE; decision_id → decisions.id ON DELETE SET NULL. Wird eine Entscheidung gelöscht, bleibt die Analyse erhalten, nur der Verweis wird geleert. |
strategy_assignmentPK id INTEGERFK security_id | id INTEGER PRIMARY KEY AUTOINCREMENT; security_id INTEGER NOT NULL; strategy_type TEXT NOT NULL: nur long_term, swing, tactical oder unknown; effective_from TEXT NOT NULL, effective_to TEXT: Gültigkeitsintervall; source TEXT; rationale TEXT; created_at, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP. | security_id → security.id ON DELETE CASCADE. effective_to darf nicht vor Beginn liegen; UNIQUE(security_id, effective_from). Trigger verhindern überlappende Strategieintervalle. |
candidate_promotionPK id INTEGERFK security_id | id INTEGER PRIMARY KEY AUTOINCREMENT; security_id INTEGER NOT NULL; evaluated_at TEXT NOT NULL; status TEXT NOT NULL: nur watching, ready, promoted, rejected oder deferred; source TEXT NOT NULL; rationale TEXT; details_json TEXT: strukturierte Details; approved_at TEXT, approved_by TEXT; created_at, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP. | security_id → security.id ON DELETE CASCADE. Audit-Spur für die Überführung eines Kandidaten. |
fx_ratesPK id INTEGER | id INTEGER PRIMARY KEY AUTOINCREMENT; rate_date TEXT NOT NULL; base_currency TEXT NOT NULL; quote_currency TEXT NOT NULL; rate REAL NOT NULL; source TEXT NOT NULL; fetched_at TEXT NOT NULL. | Keine FK. Es gilt die ECB-Konvention 1 EUR = N Quote-Währung: Basis ist genau EUR, beide Währungen sind dreistellige Großbuchstaben und verschieden, Rate > 0. Eindeutig je Datum, Währungspaar und Quelle. |
swing_campaignPK id INTEGERFK security_id, strategy_assignment_id | id INTEGER PRIMARY KEY AUTOINCREMENT; security_id INTEGER NOT NULL; strategy_assignment_id INTEGER NOT NULL; opened_at TEXT NOT NULL; original_quantity REAL NOT NULL; reference_avg_cost REAL, reference_currency TEXT: optionaler Referenzeinstieg; status TEXT NOT NULL: open oder closed; closed_at TEXT; source TEXT NOT NULL; rationale TEXT; created_at, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP. | security_id → security.id ON DELETE CASCADE; strategy_assignment_id → strategy_assignment.id ON DELETE RESTRICT. Menge > 0; Kosten und Währung nur gemeinsam; genau eine offene Kampagne je Wertpapier. Trigger erzwingt eine passende Swing-Strategie desselben Wertpapiers. |
swing_campaign_eventPK id INTEGERFK campaign_id, transaction_id | id INTEGER PRIMARY KEY AUTOINCREMENT; campaign_id INTEGER NOT NULL; event_type TEXT NOT NULL: nur baseline, add, tp1_signal, tp1_execution, tp2_signal, tp2_execution, manual_reduction, stop_execution oder close; event_at TEXT NOT NULL; quantity REAL, price REAL, currency TEXT; transaction_id INTEGER; source TEXT NOT NULL; external_event_id TEXT; notes TEXT; created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP. | campaign_id → swing_campaign.id ON DELETE RESTRICT; transaction_id → transactions.id ON DELETE RESTRICT. Menge/Preis müssen positiv sein; ein Preis erfordert Währung. Externe Ereignis-ID und Transaktions-ID sind jeweils, falls gesetzt, eindeutig. Trigger prüft, dass die verknüpfte Transaktion zur Kampagnen-Security gehört. |
Abgeleitete Views: keine eigenen Daten, aber feste Ausgabe-Felder
Die drei Views speichern keine eigenen Zeilen. Sie lesen die Tabellen oben in einer für Tools geeigneten Form:
v_active_positionskombiniertpositionsundsecurityfür Bestände mitshares > 0. Ausgabe:security_id, Symbol, ISIN, WKN, Name, Börse, Wertpapier- und Kostenbasiswährung, Stückzahl, Durchschnittskosten, Restkostenbasis, realisierter Gewinn und letztes Buchungsdatum.v_watchlistkombiniertwatchlist,securityund optionalmarket_snapshot. Ausgabe:security_id, Identitäts- und Klassifikationsfelder, Watchlist-Status, Priorität, Zielkurs, Begründung, These, Aufnahmedatum sowie Preis, Vortagesschluss, Volumen, Marktkapitalisierung und Marktstichtag.v_portfolio_marketkombiniertpositions,securityund optionalmarket_snapshot. Ausgabe: Bestand/Kostenbasis und Marktpreis sowiemarket_value,market_value_currencyundvaluation_status. Der Wert wird nur berechnet, wenn Stückzahl und Kurs vorliegen und die Währungen übereinstimmen; sonst erklärt der Status den fehlenden Wert.
Damit sind auch die Referenzen zwischen den technischen IDs klar: Eine neue Datenquelle wird über data_sources.id registriert, ihr Ticker über source_symbols.source_id einer vorhandenen security.id zugeordnet. Ein Depotimport schreibt nach Auflösung dieser Wertpapier-ID in transactions.security_id; Positionen, Watchlist, Markt- und Entscheidungsdaten verwenden danach dieselbe ID. Die IDs werden nicht aus Symbolen oder Namen rekonstruiert.
Welche Skripte und Module wofür zuständig sind
Die Umsetzung bleibt bewusst pragmatisch: kleine, klar abgegrenzte Python-Module statt einer zusätzlichen Server-Landschaft. Die wichtigsten Aufgaben sind getrennt, damit ein Importer keine Analyseentscheidung trifft und eine Analyse keine Transaktion verändern kann.
Import-ParqetTransactions.pyundparqet_import.pylesen Parqet-Exporte ein, erkennen Duplikate und Konflikte, ordnen Wertpapiere zu und bauen betroffene Positionen aus den Transaktionen neu auf.- Die Market-Data-Workflows aktualisieren Kurshistorien und kompakte Marktsnapshots inkrementell. Für eine einfache Übersicht muss deshalb nicht bei jeder Anfrage die gesamte Kurshistorie erneut ausgewertet werden.
Research-TradingFundamentals.pybildet den Workflow für Fundamentals: Quellen und Berichtsperioden werden erfasst, strukturierte Kandidaten geprüft und nur plausible, zuordenbare Werte übernommen.trading_analytics.pyunterstützt die Watchlist-Analyse;analysis_engine.pybaut den fachlichen Analyse- und Portfolio-Kontext für eine Security zu einem Stichtag auf.entry_sizing.pyberechnet einen möglichen Ersteinstieg innerhalb der Kapitalgrenzen.swing_lifecycle.pybewertet bestehende Swing-Positionen, Teilgewinnziele und den verbleibenden Runner.decision_engine.pyverdichtet technische Bedingungen, Strategie und Guardrails zu einer Entscheidungsvorlage.trading_orchestrator.pybündelt die einzelnen rein lesenden Prüfungen zu einem Gesamtbild.trading_sqlite.pyist die gezielte OpenWebUI-Tool-Schicht. Sie stellt die benötigten Funktionen der lokalen Oberfläche bereit, ohne daraus einen allgemeinen Datenbankzugriff für das Modell zu machen.
Diese Trennung ist kein Selbstzweck. Sie macht Fehler zuordenbar: Ein falscher Kurs gehört in den Market-Data-Workflow, eine fehlende Wertpapierzuordnung in den Import, eine unplausible Positionsgröße in die Sizing-Regeln – nicht in den Prompt des Sprachmodells.
Datenimport: So lässt sich ein Importer für ein anderes Tool bauen
Ein Depotimport ist keine einfache Übernahme einer CSV in eine Tabelle. Jede Quelle bezeichnet Buchungen anders, verwendet eigene Spaltennamen, Dezimalformate, Währungen und Kennungen. Der Importer muss diese Quelldaten deshalb zuerst in ein kanonisches Transaktionsmodell übersetzen. Erst danach darf er die bestehende Depot-Historie prüfen und gegebenenfalls erweitern.
Für einen neuen Broker, ein Portfolio-Tracking-Tool oder einen CSV-Export beginnt die Arbeit mit einer Feldzuordnung. Der neue Importer muss nicht dieselben Spalten erwarten wie Parqet; er muss aber dieselbe fachliche Bedeutung liefern.
| Fachliches Zielfeld | Mindestens benötigte Information aus dem Export | Warum sie nötig ist |
|---|---|---|
| Wertpapieridentität | Bevorzugt ISIN, alternativ eindeutige Kombination aus Symbol, Börse und Währung | Ordnet die Buchung einer vorhandenen security_id zu. |
| Transaktionstyp | Kauf, Verkauf, Dividende, Gebühr, Steuer, Einlieferung oder vergleichbares Quellereignis | Bestimmt die fachliche Behandlung und verhindert falsche Vorzeichen. |
| Datum und Uhrzeit | Ausführungs- oder Buchungszeitpunkt | Bestimmt die korrekte Reihenfolge der Historie und die Berechnung der Position. |
| Stückzahl und Preis | Anzahl sowie Kurs je Stück, soweit vorhanden | Ermöglicht die Berechnung von Bestand, Kostenbasis und Teilverkäufen. |
| Betrag, Währung, Gebühren, Steuern | Brutto- oder Nettobetrag sowie alle separat gelieferten Nebenkosten | Hält die wirtschaftliche Bedeutung der Buchung nachvollziehbar fest. |
| Externe Buchungs-ID | Stabile ID des Exportwerkzeugs, wenn vorhanden | Erlaubt eine sichere Duplikaterkennung über mehrere Importläufe hinweg. |
Der Importablauf gliedert sich anschließend in klar getrennte Schritte:
- Lesen und normalisieren: Der Importer liest CSV, JSON oder API-Antworten, behandelt Zeichensatz, Trennzeichen, Dezimalkomma und Datumsformat und übersetzt Quellereignisse in das kanonische Transaktionsmodell.
- Validieren: Pflichtfelder, Zahlenwerte, Währung, Transaktionstyp und fachliche Plausibilität werden geprüft. Eine unvollständige oder nicht numerisch lesbare Zeile ist INVALID, nicht „best guess“.
- Security auflösen: Der Importer sucht zuerst über ISIN und bekannte Provider-Zuordnungen. Ist ein Wertpapier unbekannt oder mehrdeutig, wird die Zeile als UNKNOWN_SECURITY klassifiziert und nicht stillschweigend neu angelegt.
- Idempotenz prüfen: Aus den wirtschaftlich relevanten Feldern entsteht ein stabiler Fingerprint; wenn die Quelle eine externe Buchungs-ID liefert, wird auch sie genutzt. Ein unveränderter zweiter Import darf nur Duplikate erkennen und keine zweite Buchung erzeugen.
- Vorschau und Klassifikation erzeugen: Jede Zeile wird vor dem Schreiben als DUPLICATE, NEW, NEW_HISTORICAL, CONFLICT, UNKNOWN_SECURITY oder INVALID ausgewiesen. So kann der Benutzer sehen, was der Import tun würde.
- Explizit und atomar schreiben: Erst ein bewusster Write-Schritt übernimmt zulässige neue Buchungen. Direkt vor dem Schreiben wird der Plan erneut unter Schreibschutz geprüft, damit sich CSV oder Datenbank nicht zwischen Vorschau und Übernahme verändert haben. Der Import läuft als Datenbanktransaktion und sichert den Zustand vor der Änderung.
- Positionen neu aufbauen: Nur die betroffenen
security_idwerden aus ihrer vollständigen Transaktionshistorie neu berechnet. Der Importer schreibt nicht einfach eine neue Stückzahl inpositions. - Ergebnis protokollieren und prüfen: Importbericht, Anzahl der Klassen, abgelehnte Zeilen und die neu berechneten Positionen werden kontrolliert. Datenbankintegrität, Fremdschlüssel und erwartete Transaktionsanzahl bilden den technischen Abschluss.
Als vereinfachte Regel sieht der Kern eines solchen Importers so aus:
Quelle lesen
↓
in kanonische Transaktion normalisieren
↓
validieren und Security auflösen
↓
Duplikat, Konflikt oder neue Buchung klassifizieren
↓
Vorschau ausgeben
↓
nur nach expliziter Freigabe atomar schreiben
↓
betroffene Positionen aus transactions neu berechnen
↓
Importbericht und Integritätsprüfung
Besonders wichtig ist die Behandlung historischer Zeilen. Taucht eine neue Buchung auf, die zeitlich vor bereits vorhandenen Transaktionen liegt, verändert sie möglicherweise Einstandswerte und alle späteren Teilverkäufe. Deshalb werden solche Zeilen als NEW_HISTORICAL getrennt ausgewiesen und nur mit einer zusätzlichen bewussten Freigabe übernommen. Konflikte werden nie automatisch „zusammengeführt“.
So lässt sich derselbe Importvertrag auf nahezu jedes andere Tool übertragen: Nur der Reader und die Feldzuordnung sind quellspezifisch. Security-Auflösung, Validierung, Fingerprint, Vorschau, atomarer Write, Positions-Rebuild und Prüfbericht bleiben gleich. Genau diese Trennung macht den Importpfad erweiterbar, ohne die historische Wahrheit des Depots zu gefährden.
Market Data und Fundamentals
Kursdaten werden als Historie und als aktueller Snapshot gespeichert. Die Historie ist nötig für Trend, Momentum sowie SMA50 und SMA200. Der Snapshot hält häufig benötigte aktuelle Werte kompakt bereit. Nach einem ersten Backfill ergänzt der Updater nur fehlende Handelstage, statt jedes Mal den gesamten Zeitraum neu zu laden.
Fundamentaldaten folgen einem strengeren Weg als Kurse. Umsatz, Ergebnis, EPS, Free Cashflow, Cash und Verschuldung beziehen sich auf Berichtsperioden, Einheiten und Bilanzlogik. Das Sprachmodell kann offizielle Berichte lesen, interpretieren und strukturierte Kandidaten liefern. Python prüft anschließend Datentypen, Perioden, Währung, Einheiten, Plausibilität und Quellenbezug. Erst dann werden Werte in die Datenbank geschrieben.
Nicht vorhandene Daten werden nicht durch plausible Annahmen ersetzt. Analysten-Revisionen, Ratings, Kursziele, Events und News sind im Datenmodell bereits vorgesehen, werden in Version 0.1 aber noch nicht befüllt. Sie gehören deshalb derzeit nicht zu den aktiven Entry-Regeln.
OpenWebUI, Tools und Prompt
OpenWebUI ist das Cockpit des Trading-Agenten. Dort werden das lokale Modell, der Systemprompt und die erlaubten Tool-Funktionen zusammengeführt. Bei einer Frage wie „Gibt es einen neuen Swing-Kandidaten?“ ruft der Agent nicht beliebig Daten ab, sondern verwendet die dafür vorgesehenen Analyse- und Orchestrierungsfunktionen.
Der Prompt hat dabei eine wichtige, aber begrenzte Rolle. Er legt fest, wie der Agent antwortet: Datenqualität benennen, zwischen Fakten und Interpretation unterscheiden, keine Order behaupten, Risiken offen nennen und bei fehlenden Informationen nicht spekulieren. Die fachlichen Grenzen selbst liegen jedoch im Code: Positionsgrenzen, Cash-Reserve, technische Filter und die Bedingung einer expliziten Swing-Zuweisung werden nicht durch Sprache, sondern durch die Python-Module geprüft.
Das verhindert zwei typische Fehler. Erstens kann ein Modell nicht aus einer allgemeinen Watchlist eigenmächtig einen Kaufkandidaten machen. Zweitens kann eine überzeugend formulierte Antwort keine fehlenden Kursdaten, eine ungültige Währungsumrechnung oder ein überschrittenes Portfolio-Limit übergehen.
Wie eine Entscheidung vorbereitet wird
Eine Watchlist ist zunächst nur ein Arbeitsvorrat. Für eine echte Entry-Prüfung muss ein Wert ausdrücklich der Swing-Strategie zugewiesen sein. Danach prüft das System, ob aktuell keine Position und keine offene Swing-Kampagne bestehen, ob alle nötigen Kontextdaten vorliegen und ob der Kurs in Euro bewertet werden kann.
Der aktuelle technische Filter verlangt einen Kurs über SMA50 und SMA200 sowie einen SMA50 über SMA200. Zusätzlich begrenzen Portfolio-Guardrails die Empfehlung: höchstens 10 % für den Ersteinstieg, höchstens 20 % je Wertpapier, maximal 40 % Swing-Anteil und eine Cash-Reserve von 10.000 Euro. Erfüllt ein Kandidat zwar technische Bedingungen, scheitert aber an einem dieser Limits, lautet die richtige Antwort bewusst: kein neuer Einstieg.
Das Ergebnis ist eine begründete, rein lesende Empfehlung. Sie kann erklären, warum ein Kandidat technisch interessant ist, welche Begrenzung greift und welche Positionsgröße unter den Regeln möglich wäre. Sie erzeugt weder eine Order noch eine neue Transaktion oder eine Swing-Kampagne.
Swing-Lifecycle: Teilgewinnmitnahmen und Runner
Ist eine Position nach einem manuellen Kauf entstanden und einer Swing-Kampagne zugeordnet, bewertet der Lifecycle den weiteren Verlauf. TP1 realisiert 25 % der ursprünglichen Stückzahl. TP2 realisiert insgesamt 75 %. Die verbleibende Restposition – der Runner – wird nicht nach einer festen Haltedauer beendet, sondern anhand der technischen Lage mit SMA50 und SMA200 erneut beurteilt.
Auch hier bleibt die Trennung wichtig: Der Agent kann eine Teilgewinnmitnahme oder ein Halten des Runners empfehlen. Er setzt keinen Broker-Stop und verkauft nicht selbst. Nach der manuellen Ausführung kommt die tatsächliche Transaktion zurück in den Importpfad und wird wieder Teil der Datenbasis.
Aktueller Stand: Version 0.1.0
Version 0.1.0 umfasst Trading-Datenbank, Parqet-Import, Market Data, Fundamentals, FX-Umrechnung, Portfolio-Kontext und Guardrails, Strategy Assignments, Swing-Lifecycle, Entry-Sizing, Watchlist-Analyse, Candidate Decision, Strategy Suggestion, Orchestrator, Corporate Actions und die OpenWebUI-Integration. Die Kernpfade sind dabei bewusst auf Analyse und Empfehlung begrenzt.
Ebenso wichtig sind die Grenzen des aktuellen Stands: Es gibt keine Broker-Anbindung und keinen automatischen Handel. Ein vollständiger Scheduler für alle Datenquellen ist noch nicht umgesetzt. Analysten- und Ereignisdaten sind vorbereitet, aber nicht aktiv befüllt. Die Tabellen decisions und analysis_history existieren, werden aber noch nicht für eine dauerhafte Entscheidungshistorie geschrieben. Der Agent ist damit kein fertiger Autopilot, sondern ein kontrollierter lokaler Entscheidungsassistent im Ausbau.
Weiterführende Artikel
Trading-Agent: Architektur, Datenbasis und Investmententscheidungen vertieft Datenmodell, Import, Validierung und die Architektur des Datenlayers.
Watchlist und Portfolio-Fit im Trading-Agenten beschreibt Watchlist, Strategy Assignment, Portfolio-Guardrails und den Weg zu einer möglichen Entry-Empfehlung.
Grundsatz
Der Trading-Agent ersetzt keine Anlageentscheidung. Er macht Datenlage, Regeln, Grenzen und mögliche nächste Schritte sichtbar.
Der Beitrag beschreibt einen persönlichen, regelbasierten Workflow und keine Anlageberatung.
