diff --git a/modul-07-risk-manager.md b/modul-07-risk-manager.md new file mode 100644 index 0000000..185c155 --- /dev/null +++ b/modul-07-risk-manager.md @@ -0,0 +1,169 @@ +# Modul-07: Risk-Manager + +**Status:** ✅ **Freigegeben** (20.08.2026) +**Version:** 0.1.0 +**Port (intern):** 55007 +**Netzwerk:** `trading-modules-trading-modules` + +--- + +## Zweck + +Deterministischer Risk-Manager in der Trading-Kette. Konsumiert `SIGNAL_RANKED` +(aus Modul-06), führt eine **harte, deterministische Risikoprüfung** durch, berechnet +die Positionsgröße und publiziert `RISK_APPROVED` oder `RISK_REJECTED`. + +**Prinzipien:** +- **KEINE KI/ML** — rein deterministische Regeln. +- **KEINE Broker-Order** — nur Risiko-Entscheidung + Positionsgröße. +- **FAIL-CLOSED** — Fehler oder fehlende Daten führen **niemals** zu APPROVED, + sondern immer zu REJECTED. + +--- + +## Architektur + +``` +SIGNAL_RANKED (market.rankings / signal.ranked) + │ + ▼ +┌─────────────────────────────────────────────┐ +│ Modul-07 Risk-Manager │ +│ Consumer (risk.input) │ +│ → RiskEngine (Registry) │ +│ → risk_v1 (deterministische Prüfung) │ +│ → Storage (risk_decision, idempotent) │ +│ → Publisher (market.risk) │ +└─────────────────────────────────────────────┘ + │ + ├── RISK_APPROVED (risk.approved) + └── RISK_REJECTED (risk.rejected) +``` + +### Komponenten +- `app/consumer/consumer.py` — RabbitMQ-Consumer `SIGNAL_RANKED`, manuelles ACK, + Zombie-Schutz (`queue_delete` beim Start), Reconnect-Backoff 1s→2s→4s→8s. +- `app/risk/engine.py` — Registry für Risiko-Verfahren (V1, zukünftig V2). +- `app/risk/risk_v1.py` — deterministische Kernlogik + Positionsgrößen-Mathematik. +- `app/instruments/registry.py` — instrument-agnostische Werte (Contract Size, + Lot Size, Tick Size/Value, Min/Max Quantity, Quantity Step, Währung/FX). +- `app/storage/storage.py` — idempotente Persistenz (unique auf `source_ranking_id`). +- `app/publisher/publisher.py` — frische Verbindung je Publish + `conn.close()` + im `finally` (RabbitMQ-Bug-Fix aus Modul-03/04/05/06). +- `app/api/main.py` — FastAPI, Port 55007 intern, `/health`, `/health/ready`. + +--- + +## V1-Risikoregeln (zentral konfigurierbar in `config.py`, env-overridable) + +| Regel | Default | Beschreibung | +|---|---|---| +| `account_equity` | 10000 | Account-Equity | +| `risk_percent` | 0.005 | max. Risiko pro Trade (0,5 %) | +| `min_score` | 70 | Mindest-Ranking-Score | +| `min_ranking_class` | eligible | Mindest-Ranking-Klasse | +| `max_positions` | 5 | max. Anzahl Positionen (nur bei vorhandenen Daten) | +| `max_open_risk` | 0.02 | max. gesamtes offenes Risiko (nur bei vorhandenen Daten) | +| `daily_loss_limit` | 0.03 | tägliches Verlustlimit (nur bei vorhandenen Daten) | +| `max_drawdown` | 0.20 | maximales Drawdown-Limit (nur bei vorhandenen Daten) | + +> **Hinweis:** Globale Portfolio-Limits (offene Positionen, Tagesverlust, Drawdown) +> werden **nur geprüft, soweit Daten vorhanden sind**. Die eigentliche +> Portfolio-Logik gehört zu **Modul-08**. Fehlende Portfolio-Daten sind **kein** +> REJECT-Grund. + +### Positionsgröße +``` +risk_amount = equity × risk_percent +risk_per_unit = abs(entry − stop) +raw_size = risk_amount / risk_per_unit +quantity = floor(raw_size / quantity_step) × quantity_step # ABRUNDEN +``` +Abrunden auf `quantity_step` (nie aufrunden), damit das erlaubte Risiko nie +überschritten wird (FAIL-CLOSED-Sicherheit). + +--- + +## Output (RiskDecision) + +Nachvollziehbar gespeichert in `risk_decision`: +`risk_decision_id`, `source_ranking_id`, `symbol`, `asset_class`, `direction`, +`entry`, `stop_loss`, `target`, `equity`, `risk_percent`, `max_risk_amount`, +`calculated_position_size`, `actual_risk_amount`, `decision` (APPROVED/REJECTED), +`reason_codes`, `rule_version`, `timestamp`. + +--- + +## RabbitMQ + +| Rolle | Exchange | Routing Key | Queue | +|---|---|---|---| +| Consumer | `market.rankings` | `signal.ranked` | `risk.input` | +| Publisher | `market.risk` | `risk.approved` | — | +| Publisher | `market.risk` | `risk.rejected` | — | + +- Manuelles ACK erst nach erfolgreicher Verarbeitung. +- Idempotenz: gleiches `source_ranking_id` → keine Doppelentscheidung/Event. +- Reconnect ohne Zombies (Backoff 1s→2s→4s→8s, `queue_delete` beim Start). + +--- + +## Tests + +### Unit-Tests (`test_risk.py`) — 10/10 grün +1. gültiger LONG → APPROVED + korrekte Größe +2. gültiger SHORT → APPROVED +3. Score unter Threshold → REJECTED +4. falscher Stop → REJECTED +5. fehlende/ungültige Daten → REJECTED +6. Positionsgröße/Risiko mathematisch korrekt +7. Quantity-Step/Min/Max korrekt +8. Daily-Loss/Drawdown-Limit → REJECTED +9. Idempotenz +10. RabbitMQ-Reconnect + +### E2E (03→04→05→06→07) — ALLE CHECKS BESTANDEN +- **A:** Kette 03→07, Decision in DB (LONG & SHORT) ✓ +- **B:** exakt 1 Risiko-Event je Symbol (APPROVED/REJECTED) ✓ +- **C:** Pflichtfelder in Decision vorhanden ✓ +- **D:** identisches Ranking → kein Doppel-Decision ✓ +- **E:** Score<70 / falscher Stop / fehlende Daten → REJECTED + source_ranking_id ✓ + +### Mathematik-Verifikation (aus DB) +| Symbol | Direction | Entry | Stop | equity | risk% | max_risk | qty | actual_risk | +|---|---|---|---|---|---|---|---|---| +| M5LONG | LONG | 164.2 | 163.47636 | 10000 | 0.005 | 50 | 69 | 49.93 | +| M5SHORT | SHORT | 135.8 | 136.49636 | 10000 | 0.005 | 50 | 71 | 49.44 | + +Beide `actual_risk_amount ≤ max_risk_amount` ✓ + +### Reconnect-Test +RabbitMQ kontrolliert neu gestartet → alle 4 Queues (`market-regime.input`, +`strategy.input`, `ranking.input`, `risk.input`) mit **exakt 1 Consumer**, +keine Zombies, keine verlorenen Events, `risk_decision` stabil. + +--- + +## Health/Readiness + +- `/health` → `{"status":"ok", "postgresql":true, "rabbitmq":true, "consumer_ready":true}` +- `/health/ready` → 200 +- Container: `Modul-07-Risk-Manager` (healthy) + +--- + +## Technischer offener Punkt: Modul-03 `/history?limit=N` + +**Modul-03 `/history?limit=N` liefert aktuell die N ältesten Candles (aufsteigend).** +Das ist langfristig zu korrigieren (sollte die N **neuesten** Candles liefern). +Der Workaround in Modul-06 (Lookback 500) ist **nicht als API-Vertrag** zu +behandeln — er ist ein temporärer Workaround, kein garantiertes Verhalten. + +--- + +## Deployment + +- Compose: `/opt/trading-modules/docker-compose.yml` (Block `modul-07-risk-manager`) +- Nur `expose: 55007` (kein öffentlicher Port) +- `restart: unless-stopped` +- Image: `risk-manager:0.1.0`