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)
182 lines
6.6 KiB
Markdown
182 lines
6.6 KiB
Markdown
---
|
||
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`
|