C3H HUMAN REVIEW WAVE 1: migriert 16 freigegebene HR-A PAIR_MIGRATION_SAFE Paare. Pro Pair: Root (representation=source) + Canonical (representation=canonical, derived_from=Root-ID) atomar. Body byte-genau unveraendert. IDs stabil aus C3 Mapping Preview v2. modul-12/18/19/vps.md NICHT angefasst. Knowledge schema v1. (Red Queen)
9.6 KiB
| knowledge_schema | id | type | role | representation | state | derived_from | _organized |
|---|---|---|---|---|---|---|---|
| 1 | object/7ff050c3-ebaa-d0ae-5aa4-6e9da67d4773 | arch | module | canonical | current | object/603e6b87-115a-e258-5bfb-7d94d6d78c97 | true |
Modul-08: Portfolio-Manager
Status: FREIGEGEBEN (20.08.2026)
Deterministischer Portfolio-Manager. Konsumiert RISK_APPROVED aus Modul-07,
prüft das Gesamtportfolio gegen zentrale Limits und publiziert
TRADE_APPROVED oder PORTFOLIO_REJECTED.
Keine KI/ML. Keine Broker-Order. FAIL-CLOSED.
1. Rolle in der Kette
Modul-03 Ingest → 04 Regime → 05 Signal → 06 Ranking
→ SIGNAL_RANKED (market.rankings/signal.ranked)
→ Modul-07 Risk-Check → RISK_APPROVED (market.risk/risk.approved)
→ Modul-08 Portfolio-Check → portfolio_decision-Tabelle
→ TRADE_APPROVED / PORTFOLIO_REJECTED (market.portfolio)
Modul-08 prüft NICHT erneut die Positionsgrößenlogik aus Modul-07, sondern ausschließlich Portfolio-Risiken.
2. Events
| Richtung | Exchange | Routing-Key | Event |
|---|---|---|---|
| Input (Consumer) | market.risk |
risk.approved |
RISK_APPROVED |
| Output (Publisher) | market.portfolio |
trade.approved |
TRADE_APPROVED |
| Output (Publisher) | market.portfolio |
portfolio.rejected |
PORTFOLIO_REJECTED |
Consumer-Queue: portfolio.input (durable, manuelles Ack, prefetch=1).
3. V1 Portfolio-Regeln (zentral konfigurierbar, env-overridable)
| Regel | Config-Feld | Default |
|---|---|---|
| max. Anzahl offener Positionen | max_positions |
5 |
| max. gesamtes Open Risk % | max_open_risk_percent |
0.05 (5%) |
| max. Exposure pro Position % | max_exposure_per_position_percent |
0.20 |
| max. Exposure pro Symbol % | max_exposure_per_symbol_percent |
0.30 |
| max. Exposure pro Assetklasse % | max_exposure_per_asset_class_percent |
0.50 |
| max. Long-Exposure % | max_long_exposure_percent |
0.60 |
| max. Short-Exposure % | max_short_exposure_percent |
0.60 |
| Duplikat Symbol/Strategie erlauben | allow_duplicate_symbol_strategy |
false |
| Korrelation anwenden | apply_correlation |
false (vorbereitet) |
| Account Equity | account_equity |
10000.0 |
Alle Limits sind Prozentwerte des account_equity. Bei account_equity <= 0
→ FAIL-CLOSED PORTFOLIO_REJECTED.
Später geplant (V2): Sector/Country/Currency-Exposure, Korrelation (erst wenn verlässliche Daten vorhanden).
4. FAIL-CLOSED
Jede Exception, fehlende oder ungültige kritische Eingabe führt zu
PORTFOLIO_REJECTED, niemals zu TRADE_APPROVED. Fehlende kritische
Felder (quantity, risk, entry) → PORTFOLIO_REJECTED mit Reason-Codes.
5. Portfoliozustand (autoritative Quelle)
Der Portfoliozustand wird NICHT nur aus Events angenommen. Die Architektur trennt die Quelle des Zustands vom Entscheidungs-Code:
app/portfolio/state.pydefiniertPortfolioStateProvider(Interface) undPortfolioState/OpenPosition.- V1-Provider liest offene Positionen aus der eigenen
portfolio_decision- Tabelle (alleTRADE_APPROVED-Entscheidungen). - Später kann ein Broker-/Execution-Provider als autoritative Quelle ergänzt werden, ohne den Entscheidungs-Code zu ändern.
6. Race-Condition-Schutz (atomar)
Zwei nahezu gleichzeitige RISK_APPROVED dürfen nicht beide auf demselben
alten Portfoliozustand genehmigt werden und dadurch Limits überschreiten.
Lösung (app/storage/storage.py → evaluate_and_save_atomic):
- Advisory Lock (
pg_advisory_xact_lock) serialisiert parallele Portfolio-Entscheidungen. - Innerhalb einer Transaktion: Zustand lesen → Engine bewerten → speichern → Commit.
- Der zweite Prozess wartet auf den Lock und bewertet auf dem aktualisierten Zustand (inkl. der ersten Entscheidung).
7. Idempotenz
- Unique-Index
uq_portfolio_decision_srcaufsource_risk_decision_id(partial, nur wo nicht NULL). - Gleiches
source_risk_decision_id→ keine Doppelentscheidung/Event. - Zusätzlich prüft der Service vor der Verarbeitung, ob die Quelle bereits entschieden wurde (Skip + Ack).
8. DB-Tabelle portfolio_decision
Jede Entscheidung wird gespeichert — auch PORTFOLIO_REJECTED.
| Spalte | Typ | Beschreibung |
|---|---|---|
id |
BIGSERIAL PK | |
portfolio_decision_id |
TEXT | eigene Entscheidungs-ID |
source_risk_decision_id |
TEXT | id des RISK_APPROVED (Modul-07) |
source_ranking_id |
TEXT | durchgereichtes Ranking (Modul-06) |
source_signal_id |
TEXT | durchgereichtes Signal (Modul-05) |
source_event_id |
TEXT | |
correlation_id |
TEXT | |
timestamp |
TIMESTAMPTZ | Entscheidungszeitpunkt (UTC) |
symbol |
TEXT | |
asset_class |
TEXT | |
strategy |
TEXT | |
direction |
TEXT | LONG / SHORT |
timeframe |
TEXT | |
proposed_quantity |
DOUBLE | aus RISK_APPROVED |
proposed_risk |
DOUBLE | actual_risk_amount |
proposed_exposure |
DOUBLE | qty × entry |
current_portfolio_risk |
DOUBLE | offenes Risiko VOR |
new_portfolio_risk |
DOUBLE | offenes Risiko NACH |
exposure_before |
DOUBLE | Gesamt-Exposure VOR |
exposure_after |
DOUBLE | Gesamt-Exposure NACH |
decision |
TEXT | TRADE_APPROVED / PORTFOLIO_REJECTED |
reason_codes |
TEXT[] | z. B. {OK} oder {EXPOSURE_PER_POSITION_EXCEEDED,...} |
rule_version |
TEXT | portfolio_v1@1.0.0 |
details |
JSONB | Begründung + Zwischenwerte |
portfolio_snapshot |
JSONB | Portfoliozustand zum Zeitpunkt |
created_at |
TIMESTAMPTZ |
9. Reason-Codes
| Code | Bedeutung |
|---|---|
OK |
alle Limits eingehalten → TRADE_APPROVED |
MAX_POSITIONS_EXCEEDED |
max. offene Positionen erreicht |
OPEN_RISK_EXCEEDED |
gesamtes Open Risk überschritten |
EXPOSURE_PER_POSITION_EXCEEDED |
Exposure pro Position überschritten |
EXPOSURE_PER_SYMBOL_EXCEEDED |
Exposure pro Symbol überschritten |
EXPOSURE_PER_ASSET_CLASS_EXCEEDED |
Exposure pro Assetklasse überschritten |
LONG_EXPOSURE_EXCEEDED |
Long-Exposure überschritten |
SHORT_EXPOSURE_EXCEEDED |
Short-Exposure überschritten |
DUPLICATE_SYMBOL_STRATEGY |
doppelte Position Symbol/Strategie |
QUANTITY_MISSING / RISK_AMOUNT_MISSING / ENTRY_MISSING |
fehlende kritische Daten (FAIL-CLOSED) |
EQUITY_MISSING |
account_equity fehlt/ungültig (FAIL-CLOSED) |
10. Architektur
app/
config.py # zentrale Konfiguration (env-overridable)
core/
models.py # PortfolioDecision + Event-Modelle
service.py # Orchestrator (Consumer→Engine→Storage→Publisher)
portfolio/
engine.py # Registry der Portfolio-Verfahren
portfolio_v1.py # deterministische V1-Prüfung (FAIL-CLOSED)
state.py # PortfolioState-Provider (autoritative Quelle)
storage/
storage.py # idempotent + atomare Race-Condition-Lösung
publisher/
publisher.py # frische Verbindung je Publish + close
consumer/
consumer.py # manuelles Ack, Reconnect-Backoff, Zombie-Schutz
api/
main.py # FastAPI (Health/Readiness/Decisions/State)
migrations/
001_portfolio_decision.sql
Dockerfile
requirements.txt
test_portfolio.py # 11 Unit-Tests (Engine-Logik)
11. Tests
Unit-Tests (test_portfolio.py, 11/11 grün)
- erstes gültiges Portfolio-Trade → TRADE_APPROVED
- max Positions überschritten → PORTFOLIO_REJECTED
- max Open Risk überschritten → PORTFOLIO_REJECTED
- Symbol-Exposure überschritten → PORTFOLIO_REJECTED
- Long/Short-Exposure-Limit → PORTFOLIO_REJECTED
- Duplicate Symbol/Strategie → PORTFOLIO_REJECTED
- fehlender Portfoliozustand → FAIL-CLOSED
- Idempotenz (deterministisch)
- Assetklassen-Exposure → PORTFOLIO_REJECTED
- Exposure pro Position → PORTFOLIO_REJECTED
- Short-Exposure → PORTFOLIO_REJECTED
E2E (Kette 03→04→05→06→07→08, alle Checks grün)
- A: Kette 03→08, Portfolio-Decision in DB (LONG & SHORT)
- B: exakt 1 Portfolio-Event je Symbol (TRADE_APPROVED/PORTFOLIO_REJECTED)
- C: Pflichtfelder in Portfolio-Decision vorhanden
- D: Idempotenz (identisches RISK_APPROVED → kein Doppel-Decision)
- E: parallele RISK_APPROVED → beide verarbeitet, keine Race-Verlust
- F: erstes gültiges Portfolio-Trade → TRADE_APPROVED
Reconnect-Test
RabbitMQ gestoppt → Consumer erkennt Ausfall, Reconnect-Backoff (1s→2s→4s→8s). RabbitMQ gestartet → Consumer verbindet sich neu, alle 5 Queues haben exakt 1 Consumer, Health/Readiness 200.
12. Health / Readiness
GET /health→ Liveness (immer 200, Komponentenstatus im Body)GET /health/ready→ Readiness (200 nur wenn PG + RabbitMQ erreichbar)GET /decisions/{symbol}→ gespeicherte Entscheidungen (inkl. REJECTED)GET /portfolio-rules→ verfügbare Portfolio-RegelnGET /portfolio/state→ aktueller Portfoliozustand
Port: 55008 (nur Docker-intern, kein öffentlicher Host-Port).
13. Deploy
- Compose-Service:
modul-08-portfolio-manager - Image:
portfolio-manager:0.1.0 - Container:
Modul-08-Portfolio-Manager - Netz:
trading-modules restart: unless-stopped- Env: PG + RabbitMQ-Zugangsdaten (aus Compose)
14. Wichtige Fixes (20.08.2026)
Consumer-Bug (alle Module 04–08): process_data_events(time_limit=None)
verarbeitete nur EIN Event und baute danach die Verbindung neu auf. Der
erneute _connect() führte queue_delete aus und löschte wartende Events
(z. B. ein zweites, nahezu gleichzeitiges RISK_APPROVED) unwiederbringlich.
Fix: innere Schleife while not stop: process_data_events(time_limit=1.0)
hält die Verbindung am Leben. Betroffen und gefixt: Modul-04, 05, 06, 07, 08.