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)
6.6 KiB
| knowledge_schema | id | type | role | representation | state |
|---|---|---|---|---|---|
| 1 | object/645f8ff7-5a1a-bcf3-8dfe-0a8cfdf15dbc | arch | module | source | 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-ConsumerSIGNAL_RANKED, manuelles ACK, Zombie-Schutz (queue_deletebeim 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 aufsource_ranking_id).app/publisher/publisher.py— frische Verbindung je Publish +conn.close()imfinally(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_deletebeim Start).
Tests
Unit-Tests (test_risk.py) — 10/10 grün
- gültiger LONG → APPROVED + korrekte Größe
- gültiger SHORT → APPROVED
- Score unter Threshold → REJECTED
- falscher Stop → REJECTED
- fehlende/ungültige Daten → REJECTED
- Positionsgröße/Risiko mathematisch korrekt
- Quantity-Step/Min/Max korrekt
- Daily-Loss/Drawdown-Limit → REJECTED
- Idempotenz
- 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(Blockmodul-07-risk-manager) - Nur
expose: 55007(kein öffentlicher Port) restart: unless-stopped- Image:
risk-manager:0.1.0