--- knowledge_schema: 1 id: object/d87e2842-ed93-9f5b-e8ea-3b4de31f4bc6 type: arch role: module representation: canonical state: current derived_from: object/645f8ff7-5a1a-bcf3-8dfe-0a8cfdf15dbc _organized: true --- # 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`