trading-system-docs/modul-07-risk-manager.md
Red Queen 022c30e537 feat(tolaria): migrate C3H HR wave 1 — 16 safe root↔canonical pairs to schema v1
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)
2026-08-25 19:19:02 +00:00

177 lines
6.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
knowledge_schema: 1
id: object/645f8ff7-5a1a-bcf3-8dfe-0a8cfdf15dbc
type: arch
role: module
representation: source
state: current
---
# 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`