API und MCP
Portfolixir stellt den unterstützten lokalen Workflow über die JSON-API unter
/api/v1 bereit. Der MCP-Begleitdienst in mcp-server/ ist bewusst dünn:
MCP-Tools rufen ausschließlich die JSON-API auf und greifen nicht direkt auf die
Datenbank zu.
Authentifizierung
API-Anfragen benötigen ein lokales Bearer-Token:
Authorization: Bearer <PORTFOLIXIR_API_TOKEN>
Der MCP-Begleitdienst nutzt PORTFOLIXIR_API_TOKEN, um Portfolixir aufzurufen.
PORTFOLIXIR_MCP_TOKEN ist für den HTTP-Transport erforderlich, damit sich
lokale HTTP-Clients beim Begleitdienst authentifizieren können.
Datenregeln
Alle Antworten nutzen JSON-Umschläge mit entweder data oder errors.
Finanz-Decimals werden als Strings serialisiert, einschließlich Mengen, Preise,
Gebühren, Steuern, Kurs-Schlusswerte und monetärer Summen. Request-Payloads für
diese Werte sollten ebenfalls Strings senden.
DELETE /api/v1/securities/:id ist die Erfolgs-Ausnahme: es liefert
204 No Content mit leerem Body. Clients sollten für diese erfolgreiche
Löschantwort keinen JSON-Body parsen.
Delta-Reads (FR-38). Die beiden wiederkehrenden Sync-Reads — GET
/api/v1/transactions und GET /api/v1/securities — akzeptieren
?since=<ISO8601> (Datetime mit Offset, naive UTC-Datetime oder ein reines
Datum als Tagesbeginn, UTC) und liefern dann nur die Zeilen, die strikt nach
diesem Zeitpunkt angelegt oder geändert wurden (nach updated_at). Die
Antwort spiegelt since, trägt as_of (den Lesezeitpunkt — als nächstes
since verwenden) und eine delta_note mit der Semantik. Löschungen sind
in einem Delta-Read nicht repräsentiert; wer Löschungen erkennen muss,
macht einen vollen Read. Ein ungültiges since ist ein 422. Delta-Reads
sind pull-only: Push-Zustellung (Webhooks an einen konfigurierten
Endpunkt) ist eine separate, weiterhin gegatete Entscheidung (B3.7) und
bewusst nicht Teil dieser Oberfläche.
Die menschliche Sicht desselben Schnitts (Issue #731) liegt auf
/transactions?since= und /securities?since= als Geändert-seit-Chips:
gleicher Parametername, gleiche akzeptierte Formen, gleicher
Strikt-nach-updated_at-Schnitt — ein Link, den der Agent weitergibt, öffnet
also genau die Scheibe, die er gelesen hat. Die eine Abweichung ist bewusst:
wo die API ein ungültiges since mit 422 ablehnt, degradieren die Seiten
zur ungefilterten Liste — ein veraltetes Lesezeichen darf nie stillschweigend
verengen, was der Betreiber sieht.
Wertpapiere
GET /api/v1/securitieslistet Wertpapiere. Zeilen kommen standardmäßig als schlanke Projektion — die feste Whitelistid,name,ticker_symbol,isin,wkn,currency_code,asset_class— damit Routineabfragen klein bleiben;projection=fullliefert den vollständigen Datensatz (Notizen, Feed-Konfiguration, Attribute, Zeitstempel). Ein optionalesfields=(Issue #732, erweitert FR-37, kommagetrennt) wählt eine schlanke Feldauswahl, aufgelöst gegen die Feldliste der vollen Projektion; ein gesetztesfields=ersetztprojection=, denn eine schlanke Feldauswahl ist selbst eine Projektion, Feld für Feld gewählt. Ein unbekannter Name ist ein422, nie ein stiller Fallback. Optionale Query-Parameter:query,sort,direction, holding_status (all,heldodernot_held),data_quality(stale_quote— kein Kurs neuer als 7 Tage, einschließlich nie bepreister Wertpapiere;missing_quote— gar kein Kurs, die engere Menge darin;missing_logo;missing_fx— Issue #717: bepreist, aber ohne gespeicherten Kurs von seiner Währung zum EUR-Hub, das Speichern des Kurses leert also die Menge),projection(slim/full) undlimit/offsetzur Paginierung (beides nichtnegative Ganzzahlen). Nutze diese, um große Kataloge zu paginieren, statt die ganze Tabelle auf einmal zu holen. Die menschliche Sicht dieser Verengungen ist die One-Tap-Chipzeile auf der Wertpapierseite (Issue #717): ihre Chips fahren auf demselben URL-Zustand (holding=,dq=,filter[]=asset_class:is_nil, pluscur[]=undclass[]=für die Währungs- und Effektivklassen-Familien), sodass ein vorgefilterter Link und ein API-Read dieselbe Menge beschreiben.POST /api/v1/securitieslegt ein Wertpapier mit einemsecurity-Objekt an.asset_classist ein stabiler String-Code:equity,etf,fund,government_bond,bond,crypto,commodity,index,other, plus die Zertifikat-/Hebel-Codeswarrant,knock_out,factor_certificate,discount_certificate,bonus_certificate,express_certificate,reverse_convertible. Lass es leer, damit die Klasse beim Lesen aus Name/ISIN/Ticker inferiert wird. Um eine Position aus der Allokations-Steuerbasis (den 100 %) und der Drift-Tabelle herauszuhalten, während sie in den Bewertungssummen und der Performance bleibt — z. B. ein als Wertspeicher gehaltener Bitcoin —, die Position mit einem Bucket versehen und diesen Bucket aus einer Ansicht ausschließen; die Allokation dann unter dieser Ansicht lesen.GET /api/v1/securities/:idliefert ein Wertpapier, einschließlich seineridentifier_aliases— der über den ISIN-Wechsel-Endpunkt unten aufgezeichneten früheren ISINs (jeweils mitid,former_isin,changed_on,note).PATCH /api/v1/securities/:idaktualisiert ein Wertpapier mit einemsecurity-Objekt. Das Booleantreat_quotes_as_raw(Standardfalse) ist die ADR-0028-Notluke für Anbieter, die ihre Historie nach einem Aktiensplit nie rückwirkend anpassen: Mit gesetztem Flag werden die synchronisierten Kurszeilen des Wertpapiers als roh (wie gehandelt) behandelt, sodass die Split-Anpassungsfaktoren auch auf sie wirken.DELETE /api/v1/securities/:idlöscht ein Wertpapier, wenn keine abhängigen Transaktionen oder keine Kurshistorie darauf verweisen; referenzierte Wertpapiere liefern409 Conflict.GET /api/v1/securities/searchdurchsucht konfigurierte Online-Wertpapieranbieter. Query-Parameter:query; optionaltypemitsecurityodercrypto.
ISIN-Wechsel (Identifier-Aliasse)
Wenn eine Kapitalmaßnahme einem bestehenden Wertpapier eine neue ISIN gibt, den Wechsel aufzeichnen, statt die ISIN direkt zu editieren: Die frühere ISIN wird ein journalisierter Alias, und das ISIN-Matching des Imports prüft erst aktuelle ISINs, dann die Aliasse — Re-Importe alter Exporte (frühere ISIN) und neuer Exporte (neue ISIN) treffen so weiter dasselbe Wertpapier, statt ein Duplikat anzulegen (ADR-0029). Eine bloße Umbenennung braucht keinen ISIN-Wechsel — sie ist nur eine Namensänderung.
POST /api/v1/securities/:security_id/isin-changezeichnet den Wechsel mit einemisin_change-Objekt auf: Pflichtfeldnew_isin(normalisiert auf getrimmte Großschreibung), optionalchanged_on(ISO-Datum, Standard heute) undnote. Liefert das aktualisierte Wertpapier einschließlich seineridentifier_aliases. Abgelehnt mit422und benanntem Konflikt, wennnew_isinder aktuellen ISIN entspricht, auf einem anderen Wertpapier live ist oder als frühere ISIN eines anderen Wertpapiers aufgezeichnet ist; ein Wechsel zurück auf eine eigene frühere ISIN verbraucht diesen Alias (ein Revert). Jeder Wertpapier-ISIN-Schreibpfad — Anlegen, Aktualisieren und der Anlege-Pfad des Imports — lehnt symmetrisch eine ISIN ab, die als Alias existiert, und benennt das Alias-Wertpapier.DELETE /api/v1/securities/:security_id/identifier_aliases/:idlöscht einen aufgezeichneten Alias (journalisiert), wenn ein ISIN-Wechsel versehentlich aufgezeichnet wurde; liefert204 No Contentoder404, wenn der Alias nicht zu dem Wertpapier gehört.
Beispiel-Payload für einen ISIN-Wechsel:
{
"isin_change": {
"new_isin": "IE000XZSV718",
"changed_on": "2026-07-01",
"note": "merger rename"
}
}
Beispiel-Payload zum Anlegen:
{
"security": {
"name": "Example ETF",
"ticker_symbol": "EXM",
"currency_code": "EUR"
}
}
Research-Log (ADR-0044)
Was Betreiber oder Agent über ein Wertpapier wissen, wird als nur
anhängbare datierte Einträge festgehalten — das Research-Log des
Wertpapiers — und der aktuelle Thesenstand wird daraus abgeleitet, nie
daneben gepflegt. Einträge werden nie geändert und nie gelöscht: Ein
widerlegter Befund wird zurückgezogen, indem eine retraction angehängt
wird, die ihn ersetzt; beide bleiben lesbar, sodass der nächste Lauf zuerst
den Widerruf sieht statt eine erledigte Prämisse erneut zu prüfen. Es gibt
absichtlich kein PATCH und kein DELETE für einen Eintrag.
Jeder Eintrag trägt kind (thesis, evidence, invalidation_check,
event_result, risk, retraction, decision), body, source_url (eine
http(s)-URL; andere Schemata sind ein 422),
source_quality (primary, secondary_multi, awareness, unverified —
gesetzt, nicht geraten), as_of (das Stichdatum der Aussage, getrennt
von inserted_at: ein heute geschriebener Eintrag über das letzte Quartal
trägt das Datum des Quartals), author (operator, agent, local_model;
die API setzt standardmäßig agent), machine_generated (ein extrahierter
Eintrag ist ein Vorschlag bis zur Bestätigung und muss seine source_url
tragen), supersedes_id (der frühere Eintrag desselben Wertpapiers, den
dieser ersetzt; Pflicht bei einem Widerruf), valid_until (eine datierte
Sperre wie ein Lock-up oder eine selbst auferlegte Kaufsperre) und — nur
bei thesis-Einträgen — conviction (low, medium, high),
invalidation_condition und time_stop. Jeder Eintrag in einer Antwort
trägt zudem superseded_by_ids und ein superseded-Flag, sodass ein
ersetzter Eintrag als ersetzt gezeigt statt verborgen wird. Alle festen
Wertemengen werden geprüft; ein unbekannter Wert ist ein 422 mit dem
Feldnamen, und aus Eingaben entsteht nie ein Atom.
GET /api/v1/securities/:security_id/notes— das Log, neueste zuerst (nachas_of, dann Schreibzeit), mit dem abgeleitetenthesis_stateund einerlog_note, die den Nur-anhängen-Kontrakt benennt.POST /api/v1/securities/:security_id/notes— hängt einen Eintrag aus einemnote-Objekt an (201); journalisiert unter dem API-Token-Akteur.GET /api/v1/notes/unreviewed?days=N— gehaltene Wertpapiere (Nettostückzahl ungleich null über alle Depots), deren neuester Eintrag älter alsNTage ist (Standard 90) oder die keinen haben; Zeilen tragenlast_entry_as_ofunddays_since_last_entry(null, wenn nie geprüft).GET /api/v1/notes/uncorroborated— Einträge, derensource_qualitynichtprimaryist, neueste zuerst; ersetzte Einträge werden übersprungen, sofern nichtinclude_superseded=true; optionalsecurity_id.GET /api/v1/notes/expiring?days=N— Einträge, derenvalid_untilin die nächstenNTage fällt (Standard 30), früheste zuerst, mitdays_until_expiry; aufgehobene (ersetzte) Sperren werden übersprungen; optionalsecurity_id.
Der Thesenstand (thesis_state im Wertpapier-Detail und im Log-Read) ist
die B4.1-Projektion: status (none, intact, retracted), der aktuelle
Thesentext, conviction, invalidation_condition, time_stop, as_of,
last_reviewed_at und last_reviewed_by (der neueste thesis- oder
invalidation_check-Eintrag), derived_from_entry_id (der Thesen-Eintrag,
aus dem er liest) und retracted_by_entry_id (der Widerruf, dessen body
den Grund trägt), dazu ein basis-Satz, der die Ableitung benennt. Die
neueste These, die keine andere These ersetzt, ist die aktuelle; ein
Widerruf, der sie ersetzt, setzt retracted.
Beispiel-Payload zum Anhängen:
{
"note": {
"kind": "retraction",
"body": "10-Q am 2026-08-02 geprüft: kein Lieferantenstreit offengelegt. Zurückgezogen.",
"source_url": "https://example.invalid/sec/10-q",
"source_quality": "primary",
"as_of": "2026-08-02",
"supersedes_id": 41
}
}
Die menschliche Sicht ist der Tab Research im Wertpapier-Detailbereich
auf /securities/:id: der Thesenstand oben, die Einträge neueste zuerst mit
sichtbarer Art und Quellenqualität, ersetzte Einträge als ersetzt markiert,
Widerrufe lesbar und ein Formular, das einen Eintrag als Betreiber anhängt.
Kurse
GET /api/v1/securities/:security_id/quoteslistet die Kurshistorie eines Wertpapiers. Optionale Query-Parameter:fromundto, als ISO-Daten formatiert. Ungültige Datumsfilter liefern422 Unprocessable Entitymit Feldfehlern. Jede Zeile beschreibt ihren Split-Status selbst (ADR-0028):closeist der gespeicherte Wert (wird nie verändert),adjusted_closeder split-bereinigte Anzeigewert,basisdie Speicherbasis der Zeile (rawfür wie gehandelt erfasste manuelle Zeilen,provider_mirrorfür rückwirkend angepasste Sync-Zeilen) undadjusted, ob ein Split-Faktor angewendet wurde. Charts und Bewertungen nutzenadjusted_close; Audits prüfen gegenclose. Ein Wertpapier, dessen Anbieter nie rückwirkend anpasst, lässt sich mittreat_quotes_as_rawmarkieren (siehe Wertpapiere), was die Roh-Basis für seine synchronisierten Zeilen erzwingt.PUT /api/v1/securities/:security_id/quotesführt manuelle Kurszeilen ein (Upsert).POST /api/v1/securities/:security_id/sync_quoteslöst die Kurssynchronisierung eines Wertpapiers aus. Die Antwort enthältstatus(ok,skippedodererror); übersprungene und Fehler-Antworten können einenreasonwiemissing_tickeroderno_provider_adapterenthalten.
Beispiel-Payload für Kurs-Upsert:
{
"quotes": [
{
"date": "2026-05-15",
"close": "123.45",
"source": "manual"
}
]
}
Beispiel-Antwort für Kurssynchronisierung:
{
"data": {
"status": "skipped",
"reason": "missing_ticker"
}
}
Portfolios und Konten
Portfolio-Writes sind veraltet (ADR-0024) — nur Kompatibilität; nutze Buckets/Ansichten zur Gruppierung. Portfolios wurden zu internen Kompatibilitätsdatensätzen herabgestuft: die UI gruppiert ausschließlich über Buckets und Ansichten, und Depots/Geldkonten brauchen keine
portfolio_idmehr (ein deterministisches internes Standard-Portfolio wird automatisch gebunden).POST /api/v1/portfoliosundPATCH /api/v1/portfolios/:portfolio_idfunktionieren weiter, antworten aber mit dem Response-HeaderDeprecation: true. Sunset-Hinweis: nach zwei Releases ohne externe Portfolio-Writes verschmilzt eine Folge-Story die Datensätze in Buckets und Ansichten (das Exit-Kriterium des ADR) — plane Migrationen aufPOST /api/v1/bucketsundPOST /api/v1/viewsjetzt. Jeder hier geschriebene Datensatz bleibt in der schreibgeschützten Admin-Liste „Portfoliodatensätze (Kompatibilität)“ der UI sichtbar, nichts wird unsichtbar.
GET /api/v1/portfolioslistet Portfolios (Kompatibilitätsdatensätze).POST /api/v1/portfolioslegt ein Portfolio mit einemportfolio-Objekt an. Veraltet — antwortet mitDeprecation: true; bevorzuge Buckets/Ansichten.GET /api/v1/cash_accountslistet Geldkonten. Jedes trägt einenbalance(Decimal-String, in der eigenen Währung des Kontos), der beim Lesen aus dem Ledger abgeleitet wird: Beträge werden als positive Größen gespeichert und der Transaktions-typeimpliziert die Richtung (Einzahlungen, Dividenden, Zinsen, Steuererstattungen und Verkäufe fügen Cash hinzu; Entnahmen, Gebühren, Steuern und Käufe entfernen es; eine Geldübertragung belastet ihr Konto und schreibt dem Gegenkonto gut). Einbalance_adjustment-Snapshot (siehe unten) verankert den Saldo an einem genannten absoluten Betrag zu seinem Datum, wonach nur spätere Buchungen ihn anpassen.POST /api/v1/cash_accounts/:id/balanceerfasst einen absoluten Saldo-Snapshot für ein Konto (ADR-0009): den aktuellen Saldo zu einem Datum, statt jede Buchung zu spiegeln. Body{"date": "2026-06-01", "amount": "4250.00"}(notesoptional);amountist ein Decimal-String und darf negativ sein (ein Überziehungskredit). Es speichert einebalance_adjustment-Transaktion und gibt sie zurück. Der Saldo verankert sich dann an diesem Betrag, und nur Buchungen mit einem Datum strikt nach dem Snapshot verändern ihn, sodass Geld zwischen eigenen Konten zu verschieben keine Übertragungsbuchung braucht. Unbekannte Konten liefern404 Not Found.POST /api/v1/cash_accountslegt ein Geldkonto mit einemcash_account-Objekt an.portfolio_idist optional (ADR-0024): fehlt sie, wird das Konto an das deterministische interne Standard-Portfolio gebunden; eine explizite id gewinnt weiterhin (Kompatibilität). Das optionaleliquidity_role(Standardfree_cash) klassifiziert das Konto:free_cashist echtes verfügbares Cash;credit_lineist eine Überziehungs-/Lombard-Linie, deren negativer Saldo eine Verbindlichkeit ist und deren ungenutzter Rahmen nie Liquidität ist (sie zählt nie zum verfügbaren Cash, auch nicht mit positivem Saldo — der Typ schlägt das Vorzeichen);reserveist ein sichtbarer, aber ausgeschlossener Topf. Nurfree_cash-Konten mit nicht-negativem Saldo gehen in das verfügbare Cash der Bewertung und ihrecash_quoteein. Ein unbekannter Wert wird mit422 Unprocessable Entityabgelehnt.GET /api/v1/cash_accounts/:idliefert ein Geldkonto.PATCH /api/v1/cash_accounts/:idaktualisiert ein Geldkonto (name,currency_code,notes,liquidity_role);portfolio_idkann nicht geändert werden.DELETE /api/v1/cash_accounts/:idlöscht ein Geldkonto oder liefert409 Conflict, wenn eine Transaktion oder ein Wertpapierkonto noch darauf verweist.GET /api/v1/securities_accountslistet Depots/Wertpapierkonten.POST /api/v1/securities_accountslegt ein Depot/Wertpapierkonto mit einemsecurities_account-Objekt an.portfolio_idist optional (ADR-0024): fehlt sie, wird das Depot an das deterministische interne Standard-Portfolio gebunden.GET /api/v1/securities_accounts/:idliefert ein Wertpapierkonto.PATCH /api/v1/securities_accounts/:idaktualisiert ein Wertpapierkonto (name,notes,cash_account_id);portfolio_idkann nicht geändert werden.DELETE /api/v1/securities_accounts/:idlöscht ein Wertpapierkonto oder liefert409 Conflict, wenn eine Transaktion noch darauf verweist.
Beispiel-Payloads für Konten:
{
"portfolio": {
"name": "Household Portfolio",
"base_currency_code": "EUR"
}
}
{
"cash_account": {
"portfolio_id": 1,
"name": "Settlement EUR",
"currency_code": "EUR"
}
}
{
"securities_account": {
"portfolio_id": 1,
"cash_account_id": 1,
"name": "Main Depot"
}
}
Transaktionen und Bestände
GET /api/v1/transactionslistet Transaktionen. Optionale Filter:from/to(ISO-Daten, inklusive),portfolio_id,security_id,securities_account_id. Ungültige Filter liefern422 Unprocessable Entitymit dem betreffenden Feld. Ein optionalesfields=(FR-37, kommagetrennt) wählt eine schlanke Feldauswahl: Jede Zeile trägt dann genau die angefragten Felder. Die Namen werden gegen die Feldliste des Serializers validiert — ein unbekannter Name ist ein422, nie ein stiller Fallback. Die menschliche Sicht vonfields=(Issue #732) ist die Spaltenwahl auf der Transaktionshistorie und dem Bestände-Panel: dieselben Projektionen, Spalte für Spalte auf der Seite gewählt — und die Wertpapierliste, deren Spaltenwahl älter als FR-37 ist, bekam die Gegenrichtung als eigenesfields=(siehe Wertpapiere). Ein optionalesrunning_balance_for=<cash_account_id>ergänzt jede Zeile um einenrunning_balance— den Saldo dieses Verrechnungskontos nach der Buchung, als Decimal-String in der Kontowährung — plus einrunning_balance_basisauf oberster Ebene, das Konto, Währung und Berechnungsgrundlage benennt. Zwei Eigenschaften, von denen die Zahl abhängt: Der Lauf umfasst immer die gesamte Historie des Kontos, eine eingeschränkte Abfrage (einfrom, ein Filter) zeigt also echte Salden statt einer Teilsumme; und eine Zeile, die das Konto nicht bewegt, trägtnullstatt den vorherigen Wert zu wiederholen, was sich wie “hier ist nichts passiert” läse. Ein unbekanntes oder nicht-numerisches Konto ist ein422mit dem Feldrunning_balance_for. Das ist das API- und MCP-Gegenstück zur Saldospalte auf der Transaktionsseite.POST /api/v1/transactionslegt eine Transaktion beliebiger buchbarer Art mit einemtransaction-Objekt an (die pro Buchungsart erforderlichen Felder werden serverseitig validiert). Die buchbarentype-Werte sindbuy,sell,dividend,interest,deposit,removal,fee,tax,tax_refund,cash_transfer,inbound_delivery,outbound_deliveryundsecurity_transfer(balance_adjustmentwird über den dedizierten Kontostand-Snapshot-Endpunkt geschrieben,splitüber die Split-Routen weiter unten). Buchungssemantik, die man vor dem ersten Schreiben kennen sollte:gross_amounteiner Dividende ist der NETTO-Geldzufluss auf dem Konto — einbehaltene Steuern gehören intaxes, der Einnahmenbericht rekonstruiert brutto als netto plus einbehaltene Steuer. Eine ohnepriceerfasste Einlieferung (inbound_delivery) geht mit Einstand null in die Kostenbasis ein — der Anschaffungskurs sollte mitgegeben werden, wenn er bekannt ist; eine Auslieferung (outbound_delivery) entnimmt den Einstand zum laufenden Durchschnitt, ihr Kurs ist rein informativ. Bei einer Abgleichdifferenz sollte die fehlende Buchung der richtigen Art nachgetragen werden — Kontostand-Snapshots und unbepreiste Einlieferungen sind letzte Mittel, die Zahlen richtig aussehen lassen und dabei den Einstand verzerren. Beträge sind positive Größen — die Buchungsart bestimmt die Richtung; nurbalance_adjustmentdarf einen negativen (absoluten) Betrag tragen. Eine Steuererstattung — etwa die bei einem Verlustverkauf gutgeschriebene Steuer — ist deshalb nie ein negativertaxes-Wert auf dem Verkauf: Der Verkauf wird mit den tatsächlich einbehaltenen Steuern (oder0) gebucht, dazu eine separatetax_refund-Transaktion, deren positivergross_amountder dem Konto gutgeschriebene Betrag ist (cash_account_idundgross_amountsind ihre Pflichtfelder; das Changeset lehnt ein negativestaxesmit genau diesem Hinweis ab). Ein Wertpapier, das über ein Geldkonto in einer anderen Währung abgerechnet wird (zum Beispiel ein USD-Wertpapier über ein EUR-Konto), wird in der eigenen Währung des Wertpapiers gebucht und trägt die Felder zur währungsübergreifenden Abrechnung:security_amount(Handelsbetrag in der Wertpapierwährung),settlement_amount(im Geldkonto in Kontowährung belasteter oder gutgeschriebener Betrag) undsettlement_fx_rate(Einheiten der Kontowährung je einer Einheit der Wertpapierwährung). Fehlt der Kurs, werden jedoch beide Beträge geliefert, wird er alssettlement_amount / security_amountabgeleitet (der tatsächliche Kurs des Brokers); eine Währungsabweichung ohne Kurs und ohne Beträge zur Ableitung wird abgelehnt. Die Einstandsbasis bleibt in der Wertpapierwährung, sodass die positionsbezogene G/V währungsehrlich ist. Alle drei sind Decimal-Strings und bei Buchungen in gleicher Währungnull.GET /api/v1/transactions/:idliefert eine Transaktion.PATCH /api/v1/transactions/:idaktualisiert eine Transaktion (z. B. um eine falsch importierte Buchung zu korrigieren); die Validierung je Art gilt weiter.DELETE /api/v1/transactions/:idlöscht eine Transaktion. Da Trades und Bestände abgeleitet sind, korrigiert oder entfernt das Korrigieren oder Entfernen der Transaktion auch sie.POST /api/v1/splits/previewzeigt eine Aktiensplit-Buchung (ADR-0028) als Vorschau, ohne etwas zu schreiben. Die Anfrage trägtsecurity_id, das Wirksamkeitsdatumdate(ISO, nicht in der Zukunft) und das Verhältnis als Paar positiver Ganzzahlenratio_numerator/ratio_denominator(10:1vorwärts,1:10Reverse-Split; auf kleinste Terme normalisiert,10:5wird also als2:1gebucht). Die Antwort zeigt je Portfolio mit Bestand die Stückzahl unmittelbar vor und nach dem Wirksamkeitsdatum sowie den resultierenden aktuellen Bestand (alles Decimal-Strings; die Verhältnis-Teile bleiben Ganzzahlen), pluswarnings:effective_date_before_historybedeutet, dass das Wirksamkeitsdatum vor der frühesten erfassten Transaktion des Wertpapiers liegt — die gespeicherten Stückzahlen können bereits post-split sein (der Split-Assistent von Portfolio Performance schreibt die Historie destruktiv um), eine Buchung würde dann doppelt anpassen. Vor dem Buchen die Vorschau prüfen. Die Vorschau zeigt außerdem die gespeicherten Schlusskurse rund um das Wirksamkeitsdatum (quotes_around) und einenquote_basis_check(Fehlklassifikations-Wächter, ADR-0028 §2): ein sichtbarer Sprung deutet auf eine Roh-Serie hin, Kontinuität auf eine bereits angepasste; steht das im Widerspruch zur Klassifikation über diesourceder Zeilen, warnt die Vorschau mitquote_basis_contradiction(für Sync-Serien, die nie rückwirkend anpassen, das Flagtreat_quotes_as_rawdes Wertpapiers setzen statt blind zu buchen), und bei zu wenigen Kursen auf einer Seite meldet sieinsufficient_quotes_to_verify_basis, statt eine saubere Prüfung zu suggerieren.POST /api/v1/splitsbucht den Split: ein Aufruf fächert das Ereignis über alle Portfolios mit Bestand am Wirksamkeitsdatum auf — eine journalisiertesplit-Zeile je Portfolio, atomar eingefügt — und liefert die erzeugten Transaktionen (201, reguläres Transaktionsformat). Ein Portfolio ohne Bestand am Wirksamkeitsdatum erhält keine Zeile. Ein zweiter Split am selben Tag für dasselbe Wertpapier wird mit422abgelehnt und benennt das bestehende Ereignis (ein wiederholter Timeout kann den multiplikativen Effekt nicht verdoppeln); ein Datum in der Zukunft und ein Wertpapier ohne Bestand am Wirksamkeitsdatum werden ebenfalls mit422abgelehnt. Der generische EndpunktPOST /api/v1/transactionslehnt die Artsplitab — diese beiden Routen sind der einzige Schreibpfad für Splits.GET /api/v1/portfolios/:portfolio_id/holdingslistet abgeleitete Bestände eines Portfolios, eine Zeile je (Depot, Wertpapier). Jede Zeile trägtquantity, einen gleitenden Durchschnittavg_costundcost_basis(preisbasiert, sodass Gebühren und Steuern nicht in die Stückkosten einfließen), denlatest_price,market_valueundunrealized_pnl_abs/unrealized_pnl_pctgegen diesen Preis, plussecurity_nameundcurrency_code. Diese Größen sind in der eigenen Währung des Wertpapiers — durch das Kostenpaar der Ledger-Faltung erzwungen (ADR-0033), nicht länger eine Annahme; ein Bestand, dessen Wertpapier keinen Kurs hat, liefertnullfür Preis, Marktwert und G/V. Jede Zeile trägt zusätzlich die ADR-0033-Zerlegung des G/V in Basiswährung:base_cost(der tatsächlich gezahlte Abrechnungsbetrag, mit seinerbase_currency),price_return_abs/price_return_pct(die eigene Kursbewegung des Wertpapiers, zum heutigen Kurs umgerechnet),currency_return_abs/currency_return_pct(der Wechselkurseffekt auf den ursprünglich investierten Betrag) undtotal_return_base_abs/total_return_base_pct— wobeitotal = price + currencyDecimal-exakt gilt. Eine Zeile, deren Zerlegung nicht ableitbar ist, meldetdecomposed: falsemit einemundecomposed_reason("missing_native_cost"— kein Wertpapierwährungs-Leg in der erfassten Buchung, dann sind auchcost_basis/avg_cost/G/Vnull;"missing_base_cost"— das Abrechnungs-Leg ist nicht in der Basiswährung;"missing_fx"— kein gespeicherter aktueller Kurs;"no_price") und niemals eine geratene Zahl. Die Antwort ist selbstbeschreibend (FR-13): sie trägtcurrency_basis: "security_currency"plus einecurrency_basis_note, die benennt, welches Feld in welcher Währung ist, und einas_of-Datum. Bestände werden beim Lesen abgeleitet, ohne gespeicherten Snapshot, daher istas_ofdas Lesedatum. Unbekannte Portfolios liefern404 Not Found. Optionale Filter:security_id,securities_account_id. Ein optionalesfields=(FR-37, kommagetrennt) wählt eine schlanke Feldauswahl je Zeile, validiert gegen die Feldliste des Serializers; ein unbekannter Name ist ein422.GET /api/v1/realized_gains(Issue #724) liefert das Realisiert-Rollup des Cash-flow-Bereichs: FIFO-gematchte realisierte G&V über alle Wertpapiere und Portfolios, gruppiert nach dem Schlussdatum jedes Verkaufs in eine Jahres-/Monatsmatrix in der Basiswährung. FX-Basis ist D-1 (signiert 2026-08-20): jeder Verkauf konvertiert über den EUR-Hub zum jüngsten gespeicherten Kurs seines eigenen Schlusstags; ein Verkauf ohne gespeicherten Kurs zu diesem Datum wird aus jeder konvertierten Summe ausgeschlossen und benannt (excluded:count+securities) — nie zum Kurs eines Nachbardatums konvertiert, nie still verworfen. Die Payload trägtcomputation_basis(Serie, Fenster, Referenz, Lücken) und eineconversion_note; die menschliche Sicht ist/cashflow?tab=realized.GET /api/v1/external_flows(Issue #725) liefert das Ein-/Auszahlungs-Rollup: die gebuchten externen Cash-Flüsse (depositundremoval) über alle Portfolios, je Jahr und Monat mit Einzahlungen, Auszahlungen und Netto. Bewusst enger als dasinvested_capitaldes Performance-Laufs, das zusätzlich ein-/ausgelieferte Wertpapiere zum Marktwert und Saldo-Snapshot-Residuen zählt — der Unterschied steht incomputation_basis.excludes. FX-Basis wie in der Schwester-Facette: EUR-Hub zum Kurs des eigenen Buchungstags, unkonvertierbare Flüsse ausgeschlossen und nach Verrechnungskonto benannt. Die menschliche Sicht ist/cashflow?tab=flows.GET /api/v1/costs(Issue #726) liefert das Kosten-Rollup: Gebühren und Steuern über alle Portfolios, nur auf Übersichtsebene, je Jahr und Monat mit Jahressummen für Gebühren, Steuern und beides zusammen. Die Serie summiert die Gebühren- und Steuer-Nebenbeträge jeder Transaktion plus die eigenständigenfee-/tax-Buchungen;tax_refundwird gegen die Steuern verrechnet. Bruttobeträge werden nie summiert — das Brutto eines Kaufs enthält seine Nebenbeträge, das eines Verkaufs ist um sie gemindert, eine Bruttosumme beschriebe also etwas anderes. Diese Regel steht incomputation_basis.series. FX-Basis wie in den Schwester-Facetten: EUR-Hub zum Kurs des eigenen Buchungstags, unkonvertierbare Kosten ausgeschlossen und nach Währung benannt. Die menschliche Sicht ist/cashflow?tab=costs.GET /api/v1/holdings/by_securityliefert die globale Bewertung je Wertpapier über alle Portfolios hinweg: eineholdings-Zeile je aktuell gehaltenem Wertpapier mitsecurity_id(eine Ganzzahl), Gesamt-quantityund aktuellemmarket_value, umgerechnet in den EUR-Hub, plus einvalued-Flag.valuedistfalse(undmarket_valueistnull), wenn das Wertpapier weder einen Kurs noch einen Handelspreis hat oder kein Wechselkurspfad nach EUR existiert, sodass ein fehlender Kurs oder Kurs einen Wert nie stillschweigend verfälscht. Jede Zeile trägt außerdem den aufgelösten nativenlatest_pricemitprice_currencyundprice_sourcesowie einenunvalued_reason, der sagt, warum eine Zeile unbewertet ist:"no_price"(nichts auflösbar) oder"missing_fx"(der Preis ist bekannt, aber kein gespeicherter Kurspfad erreicht EUR);null, wenn bewertet. Die Zeilen sind nachsecurity_idsortiert. Die Antwort ist selbstbeschreibend: eincurrencyauf oberster Ebene mit"EUR", einas_of-Lesedatum (der Bericht wird beim Lesen abgeleitet, daher istas_ofdas heutige Datum, kein gespeicherter Zeitpunkt) und einnote, das die Hub-Umrechnung beschreibt. Dies ist das portfolioübergreifende Gegenstück in Basiswährung zur Bestandsliste eines einzelnen Portfolios (die in der eigenen Währung jedes Wertpapiers ohne FX bleibt); für Summen und Gewichte eines Portfolios nutze stattdessen den Bewertungs-Endpunkt.GET /api/v1/holdings/negativeliefert den Datenqualitätsbericht zu negativen Beständen: jede (Depot, Wertpapier)-Position mit abgeleiteter Menge unter null — Import-Altlasten aus nicht modellierten Kapitalmaßnahmen — alsrows(mitdepot_name,security_name,isin,portfolio_idund der negativenquantityals Decimal-String) plustotalsmit der Gesamtmenge jedes gelisteten Wertpapiers über alle Depots, sodass Transfer-Altlasten (negativ in einem Depot, positiv in einem anderen) von einer wirklich negativen Gesamtmenge unterscheidbar sind. Selbstbeschreibend mitas_of-Lesedatum undnote. Nichts wird automatisch repariert; korrigiere die Transaktionshistorie des Wertpapiers.POST /api/v1/holdings/reconcilevergleicht eine vom Nutzer gelieferte externe Positionsliste (Brokerauszug oder Depotübersicht, clientseitig in Zeilen geparst) mit den aus dem Ledger abgeleiteten Beständen — strikt lesend: die Liste kommt ausschließlich im Request-Body an, wird nie gespeichert oder geloggt, und es werden keine Daten von irgendwoher geholt (ADR-0029 §6, FR-35). Jede Zeile ist{identifier, quantity}mit optionalercurrencyund optionalem festnagelndemsecurity_id;quantitymuss ein kanonischer Dezimal-String mit Punkt sein (alles andere — Komma-Dezimalzahlen, Tausendertrennzeichen, Exponenten — ist ein422, das die Zeile benennt; Locale-Parsing ist Aufgabe des Clients), und eine leererows-Liste ist ein422. Identifier laufen durch dieselbe Identitätsleiter wie der Import: ein ISIN-förmiger String (Format und Prüfziffer) matcht nur über die ISIN-Stufe (aktuelle ISINs zuerst, dann erfasste frühere ISINs —matched_via: "former_isin"); jeder andere String wird gegen WKN, Ticker+Währung und Name+Währung geprüft, mit der Genau-eine-Regel über die Vereinigung dieser Stufen — ein String, der die WKN des einen und den Ticker eines anderen Wertpapiers trifft, landet unterambiguousmit den Kandidaten, nie als stille Wahl, und eine Zeile ohne Währung kann nicht über Ticker oder Name matchen (unmatchedmit Grundcurrency_required). Die Antwort ist selbstbeschreibend (basismitas_of,scopeund einer Delta-Notiz) und liefert:matched-Zeilen (eine je Wertpapier — Zeilen, die auf dasselbe Wertpapier auflösen, werden aggregiert, externe Mengen summiert, die beitragenden Zeilen gelistet) mit dermatched_via-Stufe (isin,former_isin,wkn,ticker,nameoderpinned),ledger_quantity,external_quantityunddelta(extern - Ledger) als exakte Decimal-Strings — Ticker-/Name-Treffer tragenweak_match: trueund den Hinweis, das Wertpapier vor jeder Buchung zu bestätigen —,ambiguous- undunmatched-Zeilen sowiemissing_from_list(gehaltene Ledger-Positionen, die die externe Liste nicht abdeckt). Die eingebetteteguidanceist Teil des Vertrags: eine Differenz wird durch Buchen der fehlenden Transaktion der richtigen Art gelöst; Saldo-Snapshots und unbepreiste Einlieferungen sind letzte Mittel, die die Kostenbasis verzerren. Optionaler Scope:portfolio_idoderview(eine View-Id; sich gegenseitig ausschließend — beide zugleich sind ein422), Standard ist die gesamte Instanz; ein unbekanntes Portfolio oder eine unbekannte View ist ein404.GET /api/v1/portfolios/:portfolio_id/valuationliefert eine Live-Bewertung eines Portfolios: jede gehaltene Position bepreist aus ihrem letzten Kurs-Schlusswert, eintotal_valueund dasweightjeder bewerteten Position (ihr Anteil am Gesamtwert). Der Marktwert jeder Position wird aus gespeicherten Wechselkursen in diebase_currencydes Portfolios (Top-Level-Feld) umgerechnet; je Position zeigtsecurity_currencydie native Währung. Ein Wertpapier ohne jeden Kurs wird mit dem zuletzt eigenen Handelspreis bepreist (price_source: "trade", gezählt im Top-Leveltrade_priced_count); eine bepreiste Position trägtprice_source: "quote". Eine Position mit weder Preis oder ohne Wechselkurspfad zur Basiswährung wird mitvalued: false,price_source: nullundnullfür Marktwert und Gewicht zurückgegeben, sodass ein fehlender Preis oder Kurs den Gesamtwert nie verzerrt. Unbekannte Portfolios liefern404 Not Found. Gewichte sind rohe Anteile (market_value / total_value), ausgegeben in voller Decimal-Präzision; da sie normalisierte Verhältnisse sind, müssen sie sich nicht exakt zu1summieren (für die Anzeige runden). Marktwerte undtotal_valuesind exakt. Die Bewertung trägt auch Cash:cash_balanceslistet jedes Geldkonto (balancein eigener Währung, plusbase_value/valuednach Umrechnung in die Basiswährung, seinliquidity_roleund eindeployable-Flag),total_cashist die Basiswährungssumme der bewerteten Geldkonten (sodass der negative Saldo einer gezogenen Kreditlinie ihn weiterhin mindert), undtotal_with_cashisttotal_value + total_cash.cash_quoteist der Anteil des verfügbaren Cash am Portfolio: verfügbares Cash ist die Summe derfree_cash-Konten mit nicht-negativem Saldo (deployable: true), und die Quote wird berechnet, als gäbe es die anderen Konten nicht (counting_cash / (total_value + counting_cash),0, wenn noch nichts zu bewerten ist) — sodass ein Reserve-Konto oder eine Kreditlinie gelistet und intotal_cashbleibt, ohne je Schein-Liquidität zu melden. Die Antwort liefert außerdemcounting_cash(Decimal-String) — das verfügbare Cash, das in die Quote eingeht — sodass ein Konsument diecash_quoteselbst rekonstruieren kann. Ein Konto, dessen Währung keinen Kurspfad zur Basis hat, wirdvalued: falsegemeldet und austotal_cashausgeschlossen, spiegelnd, wie unbepreisbare Positionen behandelt werden. Die Antwort ist selbstbeschreibend (FR-13): sie trägt einas_of-Datum (das Lesedatum — die Bewertung wird live ohne gespeicherten Snapshot berechnet) und einevaluation_note, die angibt, dass Summen inbase_currencyüber den EUR-Hub vorliegen und dass die je Position geführten Felderprice_sourceundvalueddie Preis-Aktualität anzeigen. Ein optionalesinclude_positions=false(FR-37) liefert nur den Roll-up — Summen, Cash-Salden und Cash-Quote ohne die Positionszeilen; die Antwort benennt die gelieferte Form überpositions_included.GET /api/v1/portfolios/:portfolio_id/performanceliefert die echte zeitgewichtete Rendite (TTWROR) des Portfolios, berechnet auf die Portfolio-Performance-Art: das Portfolio wird täglich bewertet (Kurse am oder vor jedem Tag, zu den Kursen jenes Tages umgerechnet, plus Cash), externe Flüsse — Einzahlungen, Entnahmen, Lieferungen und Saldo-Snapshot-Sprünge — werden neutralisiert, und tägliche Renditen werden geometrisch verkettet (siehe ADR-0010). Optionale Query-Parameter:period(ytd,1y,3y,5y,max— Standardmax; ein unbekannter Zeitraum liefert422 Unprocessable Entity),year=YYYYfür ein einzelnes Kalenderjahr,from=/to=(ISO-Daten, beide erforderlich,from <= to) für einen freien Zeitraum — beide ehrlich auf die vorhandene Historie begrenzt, ein rückwärtiger oder fehlerhafter Zeitraum liefert422— undseries=true, um die täglichen Punkte aufzunehmen (date,value,flow,cumulative_ttwror). Die Antwort trägtttwror,start_date/end_date,start_value/end_value,net_external_flowsals Decimal-Strings undsuspect_dates— Daten von Buchungen älter als 1970 (Import-Tippfehler), deren Effekte am ersten plausiblen Tag angewendet wurden. Nebenttwrorträgt die Antwort auch die geldgewichtete Renditeirr— die einzelne annualisierte Rate, die die datierten externen Flüsse und den Endwert des Zeitraums auf null abzinst (NPV(r) = Σ cf/(1+r)^(days/365) = 0), die Zahl, die Portfolio Performance neben TTWROR zeigt. Es ist ein Decimal-String odernull, wenn keine Rate existiert (weniger als zwei Flüsse, alle Flüsse mit gleichem Vorzeichen oder der Solver konvergiert nicht). Wertpapiere ohne Kurse werden mit dem zuletzt eigenen Handelspreis bepreist (siehe den Bewertungs-Endpunkt). Unbekannte Portfolios liefern404 Not Found. Da der tägliche Walk aus einem dauerhaft materialisierten abgeleiteten Wert bedient werden kann (ADR-0039), schweigt die Antwort nie über ihre Frische:as_of(ISO-8601-Zeitstempel) ist der Berechnungszeitpunkt des Walks — möglicherweise älter als die Anfrage, wenn sich die zugrunde liegenden Daten seither nicht geändert haben — undstale(Boolean) markiert einen überholten Wert, der ausgeliefert wird, während ein frischer berechnet wird; ein gespeicherter Wert wird von jedem Schreibvorgang invalidiert, der ihn beeinflussen kann,stale: falsebedeutet also aktuell gegenüber dem Ledger. Die Antwort nennt außerdem die Berechnungsbasis der Metrik (computation_basis): Eingangsreihe, wirksames Fenster, Referenzreihe (null— TTWROR/IRR haben keine) und den Umgang mit Lücken.GET /api/v1/portfolios/:portfolio_id/incomeliefert den retrospektiven Ertragsbericht: die bereits im Ledger gebuchten Dividenden und Zinsen, auf drei Arten aggregiert (keine Prognose — der Dividendenkalender ist eine separate Funktion).annualist eine Liste von Jahren (neueste zuerst), jedes mitmonths(eine Map mit Monatszahl-Schlüsseln"1"–"12", jeweils mitdividendsundinterest) und je Jahrdividends_total,interest_totalundtotal.positionsist die Pro-Position-Tabelle:security_id,security_name,security_currency(die ursprüngliche Buchungswährung),gross,tax(die einbehaltene Steuer, aus den auf der Transaktion gespeicherten TAX-Einheiten der Dividende),net(gross - tax),payment_countundlast_payment.transactionsist das Detail je Transaktion für eine Jahres-Aufschlüsselung (kind,date,year,security_id/security_name,currency, das nativenative_gross/native_tax/native_net, dasgross/tax/netin Basiswährung undconverted). Das Brutto einer Dividende ist ihr Netto-Cash (gross_amount) plus die einbehaltene Steuer; Zinsen tragen keine Quellensteuer. Alle Beträge sind Decimal-Strings in derbase_currencydes Portfolios, umgerechnet über den EUR-Hub zum gespeicherten Kurs des jeweiligen Buchungsdatums (dieselbe Mechanik wie der Bewertungs-Endpunkt), mit beibehaltener ursprünglicher Währung;unconverted_countzählt Buchungen ohne Kurspfad (zur Parität umgerechnet), undconversion_notenennt die Basis. Unbekannte Portfolios liefern404 Not Found. Seit ADR-0020 gehört ein SOLL-Zielplan zu einer Sicht (View): Die Lese-/Schreib-Endpunkte für Ziele akzeptieren ein optionalesview(eine View-id). Wird es weggelassen (oder alsnullgesendet), adressiert es den portfolioweiten Gesamt-Plan — das Verhalten vor Einführung der Views. Eine View trägt ihren eigenen Plan, sodass dieselbe Klassifizierung pro View einen anderen Plan halten kann, ohne dass sich die Pläne übereinander summieren. Ein fehlerhaftesviewliefert422 Unprocessable Entity({"view": ["is invalid"]}) und eine unbekannte View-id liefert404 Not Found— derselbe strukturierte Vertrag wie bei den Analyse-Endpunkten.GET /api/v1/portfolios/:portfolio_id/targetslistet die gespeicherten Zielgewichte eines Portfolios (die SOLL-Seite der Allokation). Optionalesclassification_idschränkt die Liste auf einen Baum ein; optionalesviewwählt den Plan (weggelassen = Gesamt). Unbekannte Portfolios liefern404 Not Found.PUT /api/v1/portfolios/:portfolio_id/targetsführt Zielgewichte für eine Klassifizierung ein (Upsert). Der Body ist{"classification_id": id, "targets": [{"category_id": id, "target_weight": "0.25"}]}und kann ein optionales"view": idtragen, um den Plan dieser View zu schreiben (weggelassen = Gesamt). Jedestarget_weightist ein String-Bruch in[0, 1]; Ziele müssen sich nicht zu1summieren. Nur die übergebenen Kategorien werden geändert. Eine Kategorie aus einem anderen Baum liefert422 Unprocessable Entity, und eine unbekannte Klassifizierung liefert404 Not Found.DELETE /api/v1/portfolios/:portfolio_id/targets/:category_identfernt das Zielgewicht eines Portfolios für eine Kategorie und liefert{deleted}(die Zahl der entfernten Zeilen). Optionalesviewwählt den Plan (weggelassen = Gesamt).GET /api/v1/portfolios/:portfolio_id/allocationliefert die SOLL/IST-Aufschlüsselung für eine Klassifizierung (erforderlicherclassification_id-Query-Parameter; ein fehlender liefert422 Unprocessable Entity). Für jede Kategorie meldet esparent_idunddepth(die Kategorien bilden einen Baum),color,own_market_value(direkt zugeordnete Positionen),market_value(ihr ganzer aufgerollter Teilbaum),actual_weight(der aufgerollte Anteil antotal_value),target_weight,drift_weight(actual_weight - target_weight: positiv = übergewichtet, negativ = untergewichtet; ADR-0023) unddrift_value(die Drift in Basiswährung neu ausgewiesen — wie viel zu verkaufen (positiv) oder zu kaufen (negativ) ist, um das Ziel zu erreichen). Jede Zeile trägt zudemchild_target_sum(Decimal-String): die beratende Summe der Ziele ihrer direkten Kinder, odernull, wenn kein direktes Kind ein Ziel trägt — ein Konsistenzhinweis, den die UI gegen das eigenetarget_weightder Zeile abgleichen kann. Eine einem Kind zugeordnete Position zählt zu diesem Kind und jedem Vorfahren, sodass eine übergeordnete Kategorie mit Ziel gegen ihren Teilbaum verglichen wird, statt 0 % zu zeigen; die Zeilen kommen in Baumreihenfolge zurück (Eltern vor ihren Kindern). Da Eltern ihre Kinder aggregieren, summieren sich dieactual_weight-Werte je Kategorie bewusst nicht über die Ebenen zu 1 — nur die Blätter plusunassignedtun es. Jede Kategorie (undunassigned) trägt außerdempositions: die Aufschlüsselung je Wertpapier ihres eigenen (direkt zugeordneten) Werts —security_id,security_name,quantity,market_value,weight, plus die reinen Anzeige-Hinweise fürs Rebalancing (ADR-0023):drift_value(der proportionale Anteil der Position an der Kategorie-Drift) undrebalance_quantity(indikative Stückzahl, die zum impliziten Stückpreis der Bewertung zu verkaufen (positiv) oder zu kaufen (negativ) wäre; ohne Gebühren-/Steuermodell, nie eine Order). Beide Hinweise sind ohne Plan und fürunassigned-Positionen ohne eigenes Positions-Sollnull. Einträge kommen größte zuerst, Wertpapiere über Depots zusammengeführt; das ist es, was der äußerste Ring des Sunburst rendert. Positions-Soll (ADR-0030 Slice 2a): diepositionseiner Kategorie sind die Vereinigung ihrer gehaltenen Positionen und der Positions-Ziel-Zeilen des aktiven Plans, je Wertpapier zusammengeführt. Jeder Eintrag trägt zusätzlichtarget_weight(sein Positions-Soll,nullohne eines),drift_weight(IST-Gewicht − SOLL-Gewicht, ADR-0023-Vorzeichen) undheld. Ein Eintrag mit eigenem Soll leitetdrift_valueundrebalance_quantityaus dieser eigenen Drift ab statt aus dem Kategorie-Anteil. Eine Position mit Soll > 0 ohne Bestand erscheint mit IST 0 (held: false, Menge/Wert/Gewicht"0") und voller Untergewichts-Drift — „hier muss gekauft werden” — mit ihrer indikativen Stückzahl zum letzten gespeicherten Kurs (nullohne Kurs; keiner wird erfunden);quote_datenennt das Datum dieses Kurses (null, wenn der Hinweis nicht kursbasiert ist).heldheißt Bestand vorhanden: ein gehaltenes Wertpapier ohne ermittelbaren Preis wird nie als ohne Bestand gemeldet (es bleibt auf den Unbewertet-Flächen). Jeder Eintrag trägt zudemstale(true, wenn seine abgelegte Positions-Ziel-Zeile nicht mehr zur aktuellen Kategorie des Wertpapiers passt). Eine Position wird nur ausgeblendet, wenn ihr Soll 0/fehlend ist und ihr Bestand null. Jede Kategorie-Zeile trägt zudemconflict(explizites Gewicht und Positions-Summe weichen ab — die Summe steuert) undhas_stale(eine hier abgelegte Positions-Zeile ist veraltet), und die Aufschlüsselung trägtdeep_target_sum— die Summe der effektiven Ziele auf der obersten gezielten Ebene je Teilbaum, die einetop_level_target_sumvon0über einem tiefer gesteckten Plan erklärt. Gehaltene, aber im Baum nicht zugeordnete Wertpapiere werden inunassignedsummiert; auchunassigned-Einträge tragen ihr Positions-Soll. Gewichte sind Anteile der Steuerbasis: der Gesamtwert der bewerteten Positionen (eingeschränkt durch die aktiveview, sofern angegeben), plus das verfügbare Cash (free_cash-Konten mit nicht-negativem Saldo).total_valueist hier diese Steuerbasis (nicht die volle Bewertung). Die Antwort trägt eincash-Objekt —market_value(das zählende Cash),actual_weight(sein Anteil antotal_value),target_weight(das Cash-Ziel des Plans der aktiven View oder0, wenn nicht gesetzt; siehe die Cash-Ziel-Endpunkte unten),drift_weight(actual_weight - target_weight, ADR-0023),drift_value(in Basiswährung neu ausgewiesen) unddistributed(Boolean) — sodass Cash in derselben Drift-Logik wie die Kategorien gesteuert wird. Ist die aktive Klassifizierung der eingebaute Währungs-Baum, wird das Cash jedes Geldkontos seiner eigenen Währungskategorie zugeordnet statt als eigene Cash-Zeile zu erscheinen (EUR-Cash → EUR-Kategorie, USD-Cash → USD usw.); in diesem Fall istcash.distributedtrueund Konsumenten sollten die separate Cash-Zeile weglassen. Da Cash Teil der 100 %-Basis ist, schrumpfen die Kategorie-Prozentsätze entsprechend, sobald Cash vorhanden ist. Dertop_level_target_sumist die Summe der Ziele der Wurzelkategorien plus das Cash-Ziel (außer im Währungs-Baum, wo Cash in Kategorien verteilt wird), verglichen mit1. Um einen Bestand aus der Steuerbasis herauszuhalten, während er weiterhin zum Gesamtvermögen zählt, den Bestand mit einem Bucket versehen und diesen Bucket aus derviewausschließen, unter der die Allokation gelesen wird — er fällt dann aus den eingeschränkten Positionen. Seit ADR-0020 spiegelt die SOLL-Seite den Plan der aktiven View wider: Mitview=<id>werden die Zielgewichte, das Cash-Ziel und dertop_level_target_sumdieser View ausgewiesen (ohneviewder Gesamt-Plan), sodass die Drift-Tabelle pro View gegen einen kohärenten 100 %-Plan steuert. Dietarget_weight-Werte der Kategorien sind die effektiven Ziele (ADR-0030): die Summe der Positions-Zeilen einer Kategorie, sobald welche existieren (Positionen sind die Quelle der Wahrheit), sonst ihr explizites Kategorien-Gewicht — die Σ-Werte verwenden dieselben effektiven Zahlen. Für die rohen Positions-Ziel-Zeilen und den Roll-up je Kategorie (die Pflege-Sicht) dient derposition_targets-Endpunkt oben. Unbekannte Portfolios oder Klassifizierungen liefern404 Not Found. Lese-Ergonomie (FR-37):include_positions=falselässt die Positionszeilen je Kategorie (und inunassigned) für einen reinen Roll-up weg, undmin_drift=<decimal>(eine absolute Drift-Schwelle, z. B.0.02) liefert nur die Kategoriezeilen, deren|drift_weight|sie erreicht — ziellose Kategorien tragen keine Drift und werden mitgefiltert; behaltene Zeilen kommen flach zurück (ein Vorfahre unter der Schwelle fehlt). Die Antwort benennt ihre eigene Basis:positions_included, das angewandtemin_driftundcategories_total(die Zeilenzahl vor dem Filter). Ungültige Werte sind ein422. Die Allokationsseite trägt denselben Filter als Abweichungs-Chips (ein gemeinsames Prädikat, die beiden Oberflächen können also keine unterschiedlichen Kategorien auswählen); die Chips sprechen Prozentpunkte,≥ 5 ppauf dem Bildschirm ist hier alsomin_drift=0.05. Dieselbe Schwelle, gleich geschrieben, gilt eine Ebene tiefer (#740):GET /api/v1/portfolios/:portfolio_id/position_targets?min_drift=<decimal>liefert nur die Positions-Ziel-Zeilen, deren|drift_weight|sie erreicht —drift_weightist das tatsächliche Gewicht des Wertpapiers in der Steuerbasis minus sein Positionsziel, genau wie die Allokation es rechnet; behaltene Zeilen tragendrift_weight, Zeilen ohne Drift werden mitgefiltert, und die Antwort benenntmin_drift,position_targets_total(die Zeilenzahl vor dem Filter) unddrift_basis. Ohnemin_driftist die Form unverändert.tax_context=true(#667) hängt zusätzlich die steuerfreien Trim-Budgets des laufenden Jahres an — ein Eintrag je Inhaber mit erfassten Auszügen, jeweils mit seiner aktivitätsbewusstenstaleness— sodass der Steuer-Spielraum dort lesbar ist, wo die Trim-Entscheidung fällt; der Block benennt, dass er je(Inhaber, Steuerjahr)über Institute rollt und nie auf Portfolio oder View eingeschränkt ist.GET /api/v1/portfolios/:portfolio_id/riskliefert eine Risiko-/Konzentrationssicht für ein Portfolio über die Steuerbasis (der Gesamtwert der bewerteten Positionen, eingeschränkt durch die aktiveview— dieselbe Basis wie die Allocation-Drift). Ein über mehrere Depots gehaltenes Wertpapier wird zu einer Einzeltitel-Position zusammengeführt. Gewichte, Caps und der HHI liegen alle auf einer 0-100-Prozentskala (Decimal-Strings, volle Präzision, keine Rundung):steerable_basisist die Basis, deren Anteil die Gewichte sind, undbase_currencydie Basiswährung des Portfolios.top_holdingssind die größten Einzeltitel-Positionen, größte zuerst, Standard N = 10 (überschreibbar mit demtop_n-Query-Parameter). Jeder Eintrag trägtsecurity_id,security_name,asset_class,market_value,weightund einenseverity(ok/warn/hard). Derseverityist instrumententyp-abhängig: eine Einzelaktie warnt über7und wird hart über10; ein ETF (die Anlageklasseetf) warnt über25und wird nie hart. Überschreibe die Standardwerte mit den Query-Parameternstock_thresholds[warn]/stock_thresholds[hard]undetf_thresholds[warn].hhiträgt den Herfindahl-Hirschman-Index der Einzeltitel-Gewichte (value= Summe der quadrierten Prozentgewichte, auf der0-10000-Skala) plus einband:low(< 1500),moderate([1500, 2500]) oderconcentrated(> 2500). Überschreibe die Schwellen mithhi_bands[low]undhhi_bands[high].asset_class_violationssind opt-in Anlageklassen-Cap-Verletzungen: es gibt keine voreingestellten Standardwerte, Caps werden pro Aufruf mit dem Query-Parameterasset_class_caps[<asset_class>](ein Prozentwert, z. B.asset_class_caps[equity]=50) konfiguriert. Nur Klassen, deren aktuelles Prozentgewicht den Cap übersteigt, kommen zurück, je mitasset_class,current_weight,capundoverage(aktuell − Cap, in Prozentpunkten).
Die Sicht ist eine reine Lese-Ableitung der Live-Bewertung und der Anlageklassen-Klassifizierung — nichts wird gespeichert, sie ist also deterministisch beim Lesen. Eine ungültige Überschreibung (z. B. ein nicht-positiver
top_n) liefert422 Unprocessable Entity; unbekannte Portfolios liefern404 Not Found.GET /api/v1/portfolios/:portfolio_id/cash_targetliest das Cash-Ziel eines Plans, den SOLL-Cash-Anteil an der 100 %-Basis der Allokation (Wertpapiere + zählendes Cash). Die Antwort ist{"cash_target_weight": "0.05"}(ein String-Bruch in[0, 1]odernull, wenn keines gesteuert wird). Optionalesviewwählt den Plan (weggelassen = der Gesamt-Plan). Unbekannte Portfolios liefern404 Not Found, ein fehlerhaftesviewliefert422und eine unbekannte View-id404.PUT /api/v1/portfolios/:portfolio_id/cash_targetsetzt (oder löscht mitnull) das Cash-Ziel eines Plans. Der Body ist{"cash_target_weight": "0.05"}und kann ein optionales"view": idtragen (weggelassen = Gesamt). Es gibt den gespeicherten Wert zurück. Gewichte außerhalb des Bereichs liefern422 Unprocessable Entity. Das Cash-Ziel speist diecash-Zeile der Allokation und dentop_level_target_sumder adressierten View.PATCH /api/v1/portfolios/:portfolio_idpatcht die Stammdaten eines Portfolios. Veraltet (ADR-0024) — antwortet mitDeprecation: true; nur Kompatibilität, nutze Buckets/Ansichten zur Gruppierung. Der Body ist{"portfolio": {...}}. Umzug des Cash-Ziels (ADR-0020): Das Cash-Ziel ist vom Portfolio-Objekt auf den View-gebundenen SOLL-Plan gewandert und wird über die beidencash_target-Endpunkte oben bedient. Aus Kompatibilitätsgründen stellt das Portfolio-Objekt weiterhincash_target_weightbereit — einen String-Bruch in[0, 1](z. B."0.05"für 5 %) odernull, um die Steuerung einer Cash-Quote zu beenden — und das Patchen liest/schreibt das Cash-Ziel des Gesamt-Plans (viewweggelassen). Ein Client, der nur das alte Feld kennt, funktioniert also unverändert weiter; nutzePUT /cash_target?view=<id>für ein View-spezifisches Cash-Ziel. Gewichte außerhalb des Bereichs liefern422 Unprocessable Entity; unbekannte Portfolios liefern404 Not Found. Dascash_target_weightist auch in den vonGET/POST /api/v1/portfolioszurückgegebenen Portfolio-Objekten enthalten (das Gesamt-Cash-Ziel).GET /api/v1/securities/:security_id/tradesliefert FIFO-gematchte Trades eines Wertpapiers: offene Lots, geschlossene Round-Trips (mit realisiertem G/V und Haltedauer in Tagen) und etwaige verwaiste Verkäufe. Jedes offene Lot trägtbuy_price(wie erfasst, Transaktionswährung) plusbuy_price_native— die Basis in Wertpapierwährung, gegen die seinunrealized_pnl_*gerechnet wird (ADR-0033) — und dieselben Zerlegungsfelder in Basiswährung wie die Bestandszeilen (base_cost,price_return_*,currency_return_*,total_return_base_*,decomposed/undecomposed_reason, gegen den EUR-Hub, da FIFO-Lots je Wertpapier über Portfolios hinweg gematcht werden). Ein Lot ohne ableitbares Wertpapierwährungs-Leg meldetnull-G/V statt eines blinden währungsübergreifenden Vergleichs. Die Antwort ist selbstbeschreibend (FR-13): sie trägtmethod: "fifo", sodass ein Client nie annehmen muss, wie Lots gegen Verkäufe gepaart wurden. Optionalesfrom/to(ISO-Daten) filtert jedes Bein nach seinem eigenen Datum: offene Lots nach Eröffnungsdatum, geschlossene Round-Trips nach Schlussdatum, verwaiste Verkäufe nach Verkaufsdatum.
Wechselkurse
GET /api/v1/exchange_rateslistet gespeicherte Wechselkurse. Kurse werden gegen den EUR-Hub gehalten (1 base_currency = rate quote_currency); andere Paare werden durch Triangulation abgeleitet, undGBX(Pence) wird alsGBP × 100behandelt.POST /api/v1/exchange_rates/syncholt Kurse vom konfigurierten Anbieter (standardmäßig EZB) und liefert{provider, status, upserted, scope}.scope=latest(Standard) holt den täglichen Feed — die heutigen Kurse, nichts aus der Vergangenheit.scope=history(Issue #737, Sprint-9-D-1) führt das einmalige Backfill der historischen EZB-Reihe (eurofxref-hist.xml) über denselben Upsert-Pfad aus: jeden veröffentlichten Tag auf einmal, sodass eine datierte Umrechnung (ein realisierter Gewinn, eine Kosten- oder Flussbuchung, die die Cashflow-Facetten wegen eines fehlenden Buchungstagskurses ausgeschlossen und benannt haben) ihren Kurs findet. Die Regel zur Kursverfügbarkeit bleibt unverändert — ein Tag, den die EZB nicht veröffentlicht hat (ein Wochenende, eine nicht gelistete Währung), bleibt ausgeschlossen und benannt; das Backfill füllt Daten, es lockert die Basis „exakter Buchungstagskurs“ nicht. Ein unbekannterscopeist ein422, ein Anbieter ohne Historie antwortet mit422und benenntscope, ein Anbieterfehler liefert502 Bad Gateway. Die menschliche Sicht ist die Schaltfläche Historische Kurse nachladen in den Ausschluss-Hinweisen auf/cashflow.
Klassifizierungen
Klassifizierungsbäume ordnen Wertpapiere wie Ordner. Integrierte Bäume
(asset_class, currency) werden automatisch abgeleitet und ihre Struktur ist
gesperrt; das Bearbeiten der Struktur eines integrierten Baums liefert
422 Unprocessable Entity. Die Mitgliedschaft des Anlageklassen-Baums
ist jedoch nur eine Sicht auf das asset_class-Feld jedes Wertpapiers: in der UI
wird ein Wertpapier zwischen seinen Kategorien gezogen (was dieses Feld
setzt), und derselbe Effekt wird über die API mit PATCH /api/v1/securities/:id
({"security": {"asset_class": "etf"}}) oder dem MCP-Tool securities.update
erzielt. Setze es auf leer/null für „automatisch”, was die Klasse beim Lesen aus
Name/ISIN/Ticker neu inferiert. Der Währungsbaum bleibt intrinsisch und kann nicht
neu zugeordnet werden.
GET /api/v1/classificationslistet jede Klassifizierung als Baum mit ihrencategoriesundassignments({security_id, category_id}). Integrierte Bäume tragenbuilt_in: trueund einenkey.POST /api/v1/classificationslegt eine eigene Klassifizierung aus einemclassification-Objekt an (name, optionalposition,description).PATCH /api/v1/classifications/:idaktualisiert dasclassification-Objekt einer eigenen Klassifizierung (name,position,description— alle optional).DELETE /api/v1/classifications/:idlöscht eine eigene Klassifizierung und kaskadiert ihre Kategorien und Zuordnungen.POST /api/v1/classifications/:classification_id/categoriesfügt einer eigenen Klassifizierung einecategoryhinzu (name, optionalcolor,description,parent_id,position).PATCH /api/v1/classifications/:classification_id/categories/:idpatcht einecategory(name,color,description,parent_id,position— alle optional). Dieclassification_idder Kategorie kann so nicht geändert werden.DELETE /api/v1/classifications/:classification_id/categories/:idlöscht eine Kategorie und kaskadiert ihre Unterkategorien und Zuordnungen.PUT /api/v1/classifications/:classification_id/assignmentsordnet ein Wertpapier einer Kategorie zu (security_id,category_id) und ersetzt jede bestehende Zuordnung dieses Wertpapiers in der Klassifizierung. Die Antwort trägt einenstatusvoncreated,movedoderunchangedplusprevious_category_id.PUT /api/v1/classifications/:classification_id/assignments/bulkordnet viele Wertpapiere in einem Aufruf einer Kategorie zu (category_id,security_ids) und liefert{assigned, category_id, security_ids}.DELETE /api/v1/classifications/:classification_id/assignments/:security_identfernt die Zuordnung eines Wertpapiers aus der Klassifizierung.
Beispiel-Payload für eine Transaktion:
{
"transaction": {
"portfolio_id": 1,
"securities_account_id": 1,
"security_id": 1,
"type": "buy",
"date": "2026-05-15",
"quantity": "10.00000000",
"price": "123.45",
"fees": "1.50",
"taxes": "0",
"currency_code": "EUR"
}
}
Buckets und Views
Buckets sind überlappende Tags, die auf Bestände (Depots, Geldkonten und
einzelne Wertpapier-Positionen) angewendet werden, um Vermögen tag-basiert
einzugrenzen. Views sind benannte, globale Filter über diese Buckets: ein
Bestand passt, wenn er eingeschlossen ist (immer unter include_all, sonst wenn
er einen der Include-Buckets der View trägt) und keinen der Exclude-Buckets der
View trägt — Exclude gewinnt immer. Bucket-Definitions- und
Zuordnungs-Schreibvorgänge werden journalisiert (ADR-0017);
View-Definitions-Schreibvorgänge bewusst nicht (ADR-0018 §5).
GET /api/v1/bucketslistet Buckets (id,name,color).POST /api/v1/bucketslegt einen Bucket aus einembucket-Objekt an (nameerforderlich, optionalescolor). Ein leerer oder doppelter Name ergibt422.GET /api/v1/buckets/:idliefert einen Bucket; unbekannte ids ergeben404.PATCH /api/v1/buckets/:idändertname/coloreines Buckets.DELETE /api/v1/buckets/:idlöscht einen Bucket und entfernt ihn aus jeder Zuordnung und jedem View-Set, Antwort204 No Content.GET /api/v1/viewslistet Views. Jede View trägtinclude_all, das aufgelösteinclude-Set (das Literal"all"unterinclude_all, sonst eine Liste von Bucket-ids) und dieexclude-Liste von Bucket-ids.POST /api/v1/viewslegt eine View aus einemview-Objekt an (nameerforderlich, optionalesinclude_all, Standardtrue).GET /api/v1/views/:idliefert eine View mit ihrem aufgelösten Filter.PATCH /api/v1/views/:idändertname/include_alleiner View.DELETE /api/v1/views/:idlöscht eine View und ihre Bucket-Sets (204).PUT /api/v1/views/:id/bucketsersetzt die Include-/Exclude-Bucket-Sets einer View. Body:{"include": [..], "exclude": [..]}(beide optional, Standard[], Listen von Bucket-ids). Eine fehlerhafte id-Liste ergibt422.GET /api/v1/views/:view_id/valuationliefert die Live-Bewertung einer View über alle Portfolios (ADR-0024) in der Form der Portfolio-Bewertung mitview_idstattportfolio_id; jedes zur View passende Konto zählt genau einmal,overlapnennt die Konten mit mehreren eingeschlossenen Buckets.include_positions=false(FR-37, #740 — derselbe Parameter wie bei der Portfolio-Bewertung) liefert nur den Roll-up: Summen, Cash-Salden und Cash-Quote ohne die Positionszeilen; die Antwort benenntpositions_included. Ein ungültiger Wert ist ein422.GET /api/v1/views/:view_id/performanceliefert TTWROR und geldgewichtete Rendite (IRR) der View über alle Portfolios: exakt der deduplizierte Konten-Scope, den auch die View-Bewertung abdeckt, sodass Gesamtwert und Rendite immer über dieselben Konten sprechen. Geld, das die View-Grenze überquert, zählt als externer Fluss (ADR-0019); Geld zwischen zwei Konten innerhalb der View saldiert sich.?period=(ytd|1y|3y|5y|max, Standardmax),?year=YYYY,?from=/?to=(freier Zeitraum) und?series=trueverhalten sich wie beim Portfolio-Performance-Endpunkt; die Antwort spiegelt dessen Form mitview_idstattportfolio_id, alle Finanzwerte sind Decimal-Strings. Unbekannte und fehlerhafte View-ids liefern404, ein fehlerhafter Zeitraum422.PUT /api/v1/securities_accounts/:id/bucketsersetzt das Standard-Bucket-Set eines Depots (die Buckets, die jede Position erbt, sofern nicht überschrieben). Body:{"bucket_ids": [..]}.PUT /api/v1/cash_accounts/:id/bucketsersetzt das Bucket-Set eines Geldkontos. Body:{"bucket_ids": [..]}.PUT /api/v1/securities_accounts/:id/positions/:security_id/bucketssetzt die Positions-Überschreibung für ein Wertpapier in einem Depot. Ein leeresbucket_idsspeichert den explizit-leeren Zustand (bewusst keine Buckets), unterschieden vom Erben des Depot-Standards; die Überschreibung gewinnt immer gegenüber dem Depot-Standard. Die Antwort nennt das aufgelösteoverride(inherit,explicit_emptyoderexplicit) und dieeffective_bucket_ids.DELETE /api/v1/securities_accounts/:id/positions/:security_id/bucketssetzt die Überschreibung zurück, sodass die Position wieder den Depot-Standard erbt.
Die Analyse-Endpunkte akzeptieren einen optionalen view-Query-Parameter (eine
View-id), um das Ergebnis auf die Bestände der View einzugrenzen:
GET /api/v1/portfolios/:portfolio_id/valuation?view=<id>GET /api/v1/portfolios/:portfolio_id/allocation?classification_id=<id>&view=<id>GET /api/v1/portfolios/:portfolio_id/performance?view=<id>GET /api/v1/portfolios/:portfolio_id/risk?view=<id>
Bei gesetztem view spiegelt die Antwort die aktive View als view: {id, name}
wider (FR-13); der Aufruf ohne View ist unverändert und trägt kein view-Feld.
Eine fehlerhafte View-id ergibt 422, eine unbekannte 404. Derselbe
view-Scope (und derselbe 422/404-Vertrag) gilt für die SOLL-Ziel-Endpunkte
— GET/PUT /api/v1/portfolios/:portfolio_id/targets, DELETE
/api/v1/portfolios/:portfolio_id/targets/:category_id und die
Cash-Ziel-Endpunkte GET/PUT /api/v1/portfolios/:portfolio_id/cash_target —,
wo eine View den SOLL-Plan wählt (weggelassen = der Gesamt-Plan). Der Bestände-
Endpunkt (GET /api/v1/portfolios/:portfolio_id/holdings) ist nicht
view-eingegrenzt: er liefert die Roh-Zeilen pro (Depot, Wertpapier) in der
jeweiligen Wertpapierwährung, sodass ein Client das Buckets/Views-Modell selbst
anhand von securities_account_id und security_id jeder Zeile anwenden kann.
Einstellungen
Ein minimaler Schlüssel-Wert-Speicher trägt die nutzerseitigen Voreinstellungen (ADR-0024). Heute gibt es eine: die Standard-Ansicht, mit der Vermögensseite und Übersicht öffnen, wenn in der UI keine Ansicht ausdrücklich gewählt wurde. Finanzielle Decimals kommen hier nicht vor.
GET /api/v1/settings/default_viewliefert die aktuelle Voreinstellung:{"data": {"view_id": null, "view": null}}wenn keine gesetzt ist (die eingebaute Alles-Sicht), sonst die id plus einview: {id, name}-Echo.PUT /api/v1/settings/default_viewsetzt sie. Body:{"view_id": <id>}mit einer existierenden View-id, oder{"view_id": null}zum Zurücksetzen auf Alles. Eine unbekannte View-id liefert404(nichts wird geschrieben); eine fehlerhafteview_idliefert422. Die Antwort entspricht demGET-Format.
Importe
Portfolio-Performance-Importe (CSV/JSON v1) laufen ausschließlich über die
Import-Ansicht: Es gibt absichtlich keinen Import-Endpunkt unter
/api/v1 und kein MCP-Tool dafür — der Vorschau-dann-Anwenden-Schritt mit
seinen Zuordnungsentscheidungen ist eine Betreiber-Handlung (ADR-0029). Was
API- und MCP-Konsumenten wissen müssen, ist die Bewahrungsgarantie beim
erneuten Import, denn alles, was ein Agent über diese API schreibt, liegt
neben der importierten Historie:
- Dasselbe Export erneut anwenden ist ein No-op per Inhalts-Hash. Jede bereits vorhandene Transaktionszeile wird als Duplikat übersprungen; kein Wertpapier wird doppelt angelegt; die Antwort des Anwendens meldet die übersprungene Anzahl.
- Was einen erneuten Import unverändert übersteht, gleiche ids, exakte
Decimal-Werte: Klassifizierungs-Zuordnungen; jede Zielplan-Version mit ihren Kategorie- und Positionszielen sowie dem Cash-Ziel;noteundattributesjedes Wertpapiers einschließlich eigener Schlüssel; das Research-Log (/api/v1/securities/:id/notes— die nur anhängbaren Einträge und der daraus abgeleitetethesis_state, ADR-0044); Wertpapier-ids undupdated_at. Festgehalten intest/portfolixir/imports/reimport_preservation_test.exsseit Issue #664 (Research-Log ergänzt durch #748). - Ein veränderter erneuter Import (eine Umbenennung, ein erfasster ISIN-Wechsel, der über einen Alias oder eine explizite Zuordnung aufgelöst wird) hält dieselbe Garantie für die zugeordneten Wertpapiere; nur die wirklich neuen Buchungen landen.
- Nicht abgedeckt: eine in der Quelle geänderte Buchung. Eine bearbeitete
Transaktion hasht anders und wird als neue Zeile neben der alten importiert;
die alte Buchung wird über
PATCH/DELETE /api/v1/transactions/:identfernt oder korrigiert. Die Garantie betrifft, was Portfolixir rund um die Historie pflegt, nicht den Abgleich zweier Versionen der Historie selbst.
Ein Research-Log, ein Plan oder eine Zuordnung „verschwindet“ also nie beim nächsten Import; ein Agent, der etwas anderes beobachtet, hat einen Defekt gefunden, keine dokumentierte Grenze.
Kontraktversion
Die Oberfläche sagt, was sie ist, damit ein Konsument bemerkt, wenn sie sich ändert (ADR-0044 §8). Eine Tool-Beschreibung wird einmal beim Verbinden gelesen; dieser Read ist der Weg, auf dem ein Agent erfährt, dass die Liste oder die Beschreibungen sich bewegt haben.
GET /api/v1/contractliefert das Kontrakt-Manifest:version(eine ganze Zahl, die des neuesten Eintrags),last_changed_at(ISO-Datum),endpoints_total,tools_totalundentriesneueste zuerst — jeder mitversion,date,summary, denendpoints("VERB /api/v1/pfad") undtools, die die Änderung hinzugefügt hat,parameters(ein Satz je Parameter, der zu einem bestehenden Read kam) sowie etwaigenremoved_endpoints/removed_tools. Optionalessince=JJJJ-MM-TTverengt die Einträge auf die streng nach diesem Tag datierten und beantwortetchanged— die?since=-Idee auf den Kontrakt statt auf die Zeilen angewandt:last_changed_atspeichern, damit abfragen und Tool-Liste und Beschreibungen neu lesen, wennchangedtrueist. Ein ungültigessinceist ein422.- Das Manifest wird im Code gepflegt (
PortfolixirWeb.Api.V1.Contract), und ein Meta-Test bindet das/api/v1-Inventar des Routers und das Tool-Inventar des MCP-Begleitdienstes in beiden Richtungen daran, sodass eine ohne Manifest-Eintrag hinzugefügte, umbenannte oder entfernte Route oder ein solches Tool den Build scheitern lässt. Der erste Eintrag hält die Sprint-9-Ergänzungen fest — das Research-Log, den Thesenstand, die Parameterinclude_positions/min_driftauf View-Ebene und Positionsebene (#740), den historischen Backfill-Scope (#737) und diesen Read.
Audit-Journal
Jeder finanzielle Schreibvorgang (Anlegen, Ändern, Löschen) wird in einem append-only Audit-Journal in derselben Datenbanktransaktion wie der Schreibvorgang selbst festgehalten, sodass jede Änderung — auch Löschungen — nachvollziehbar und zurechenbar bleibt. Marktdaten-Synchronisierung (Kurse und Wechselkurse) ist betriebliche Datenpflege und wird bewusst nicht journalisiert.
GET /api/v1/journallistet Journal-Einträge, neueste zuerst. Jeder Eintrag trägtactor_type(owner_ui,api_token_rw,api_token_ro,import_session,system_job) und ein optionalesactor_label, dieoperation(create,update,delete,upsert), den betroffenenresource_type/resource_idsowie diebefore/after-Schnappschüsse (Decimal-Werte sind Strings). Optionale Filter:resource_type,resource_id,actor_type,operation,limit(Standard 100, max. 1000) undinclude_scenarios(true, um persistierte Was-wäre-wenn-Schreibvorgänge einzuschließen; standardmäßig nur echte Schreibvorgänge). Die Antwort ist selbstbeschreibend: einmeta-Objekt nennt denas_of-Zeitpunkt, die Sortierungorder(inserted_at:desc,id:desc), die Anzahlcountund die angewandtenfilters.
Das Journal deckt derzeit die Kontexte Catalog/Fx ab (Wertpapier-Stammdaten); die übrigen Schreibkontexte werden nacheinander scharfgeschaltet.
MCP-Tools
Der MCP-Begleitdienst stellt denselben lokalen Kontrakt als Tool-Aufrufe bereit. Decimal-Eingaben in MCP-Schemata sind Strings.
portfolixir.contract.get— der Kontraktversions-Read (ADR-0044 §8): was die Oberfläche bietet und wann sie sich zuletzt geändert hat, abfragbar mitsince=.portfolixir.securities.listportfolixir.securities.get— vollständiger Datensatz eines Wertpapiers einschließlich seineridentifier_aliases(aufgezeichnete frühere ISINs) und seines abgeleitetenthesis_state(ADR-0044).portfolixir.securities.createportfolixir.securities.updateportfolixir.securities.deleteportfolixir.securities.isin_change— zeichnet einen Kapitalmaßnahmen-ISIN-Wechsel auf, damit Importe über die frühere ISIN weiter zuordnen (ADR-0029).portfolixir.securities.delete_isin_alias— journalisiertes Löschen eines aufgezeichneten Früher-ISIN-Alias.portfolixir.securities.search_onlineportfolixir.notes.list— das Research-Log eines Wertpapiers, neueste zuerst, mit dem abgeleiteten Thesenstand; die Beschreibung benennt, dass Einträge nie verschwinden (ADR-0044).portfolixir.notes.append— der einzige Schreibzugriff auf das Log; ein Widerruf mitsupersedes_idzieht einen Befund zurück.portfolixir.notes.unreviewed— gehaltene Positionen ohne Eintrag seit N Tagen.portfolixir.notes.uncorroborated— Einträge, deren Quellenqualität nichtprimaryist.portfolixir.notes.expiring— datierte Sperren, die in N Tagen ablaufen.portfolixir.quotes.syncportfolixir.quotes.listportfolixir.quotes.upsertportfolixir.portfolios.list— veraltet (ADR-0024): die Beschreibung verweist auf Buckets/Ansichten.portfolixir.portfolios.create— veraltet (ADR-0024): nur Kompatibilität; bevorzugeportfolixir.buckets.create/portfolixir.views.create.portfolixir.cash_accounts.listportfolixir.cash_accounts.createportfolixir.cash_accounts.updateportfolixir.cash_accounts.deleteportfolixir.cash_accounts.set_balanceportfolixir.securities_accounts.listportfolixir.securities_accounts.createportfolixir.securities_accounts.updateportfolixir.securities_accounts.deleteportfolixir.transactions.listportfolixir.transactions.createportfolixir.transactions.updateportfolixir.transactions.deleteportfolixir.splits.previewportfolixir.splits.createportfolixir.holdings.listportfolixir.cashflow.realized_gains— das #724-Rollup mit erklärter FX-Basis und Ausschluss-und-Benennung bei Kurslückenportfolixir.cashflow.external_flows— das #725-Rollup mit erklärtem Unterschied zum investierten Kapitalportfolixir.cashflow.costs— das #726-Rollup mit erklärter Nebenbeträge-statt-Brutto-Regelportfolixir.holdings.by_securityportfolixir.holdings.negativeportfolixir.holdings.reconcile— rein lesender Vergleich einer eingefügten externen Positionsliste mit dem Ledger; die Tool-Beschreibung lenkt den Agenten darauf, die fehlende Transaktion der richtigen Art zu buchen statt Saldo-Snapshots oder unbepreiste Einlieferungen zu nutzen.portfolixir.portfolios.valuationportfolixir.exchange_rates.listportfolixir.exchange_rates.sync—scope=latest(täglicher Feed) oderscope=history(das einmalige historische Backfill, Issue #737).portfolixir.classifications.listportfolixir.classifications.createportfolixir.classifications.categories.createportfolixir.classifications.updateportfolixir.classifications.deleteportfolixir.classifications.categories.updateportfolixir.classifications.categories.deleteportfolixir.classifications.assignportfolixir.classifications.assign_bulkportfolixir.classifications.unassignportfolixir.trades.listportfolixir.targets.listportfolixir.targets.setportfolixir.targets.deleteportfolixir.portfolios.allocationportfolixir.portfolios.riskportfolixir.portfolios.cash_targetportfolixir.portfolios.set_cash_targetportfolixir.portfolios.incomeportfolixir.portfolios.performanceportfolixir.journal.listportfolixir.buckets.listportfolixir.buckets.getportfolixir.buckets.createportfolixir.buckets.updateportfolixir.buckets.deleteportfolixir.views.listportfolixir.views.getportfolixir.views.createportfolixir.views.updateportfolixir.views.deleteportfolixir.views.set_bucketsportfolixir.views.performanceportfolixir.securities_accounts.set_bucketsportfolixir.cash_accounts.set_bucketsportfolixir.securities_accounts.set_position_bucketsportfolixir.securities_accounts.clear_position_bucketsportfolixir.settings.get_default_viewportfolixir.settings.set_default_view
portfolixir.views.performance berechnet die passende portfolioübergreifende
TTWROR/IRR für denselben Konten-Scope; Geld, das die View-Grenze überquert,
wird als externer Fluss behandelt (ADR-0019).
portfolixir.settings.get_default_view /
portfolixir.settings.set_default_view lesen und setzen die
Standard-Ansicht-Voreinstellung (ADR-0024): eine view_id pinnt eine Ansicht,
null (oder weglassen) setzt auf die eingebaute Alles-Sicht zurück.
Die Tools portfolixir.portfolios.valuation,
portfolixir.portfolios.allocation, portfolixir.portfolios.performance und
portfolixir.portfolios.risk akzeptieren ein optionales view (eine View-id),
das das Ergebnis auf die Bestände der Bucket-View eingrenzt; die Antwort spiegelt
dann die aktive View wider.
Seit ADR-0020 akzeptieren auch die SOLL-Ziel-Tools (portfolixir.targets.list,
portfolixir.targets.set, portfolixir.targets.delete) und die Cash-Ziel-Tools
(portfolixir.portfolios.cash_target zum Lesen,
portfolixir.portfolios.set_cash_target zum Setzen oder Löschen) ein optionales
view (eine View-id), das den SOLL-Plan wählt; ohne view wird der
portfolioweite Gesamt-Plan adressiert. Das Cash-Ziel ist vom Portfolio-Objekt auf
den Plan gewandert, aber portfolixir.portfolios.set_cash_target ohne view
steuert weiterhin das Gesamt-Cash-Ziel und hat damit dieselbe Wirkung wie das
alte Portfolio-Feld cash_target_weight. Alle Cash-Ziele und Zielgewichte werden
als Decimal-Strings ausgegeben und akzeptiert.