trading-system-docs/modul-06-signal-ranking.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

167 lines
9.5 KiB
Markdown
Raw 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/2b4c4170-b5b2-8a2b-fdee-8ea5eff191b5
type: arch
role: module
representation: source
state: current
---
# Modul-06-Signal-Ranking — Betriebsdokumentation
> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ **Freigegeben** (E2E+Idempotenz+Reconnect+Doku grün)
## Zweck
**Signal-Ranking** — konsumiert `SIGNAL_DETECTED` (Modul-05) + OHLCV (Modul-03), bewertet die Signalqualität deterministisch (Score 0100, vollständig nachvollziehbar und konfigurierbar), persistiert das Ergebnis und publiziert `SIGNAL_RANKED`.
Pipeline: `SIGNAL_DETECTED (Modul-05) → Signal-Ranking (signal_quality_v1) → PostgreSQL (signal_ranking) → SIGNAL_RANKED`
**Deterministische, regelbasierte Ranking-Engine — bewusst OHNE KI/ML. Keine Positionsgröße, kein Risk Management, keine Broker-/Order-Ausführung (Trade-Freigabe macht später der Risk Manager).** Modular aufgebaut, damit später eine ML/KI-Engine als ZUSÄTZLICHE Ranking-Engine registriert werden kann.
## Container
| Attribut | Wert |
|----------|------|
| Name | `Modul-06-Signal-Ranking` |
| Image | `signal-ranking:0.1.0` (lokal gebaut) |
| Port | **55006****NUR intern** (`expose`, nicht öffentlich) |
| Netzwerk | `trading-modules` (bridge) |
| Build-Context | `/opt/trading-modules/modul06-signal-ranking/` |
| Restart | `unless-stopped` |
| Healthcheck | ✅ `healthy` |
## Deployment
```bash
cd /opt/trading-modules
docker compose build modul-06-signal-ranking
docker compose up -d --no-deps --force-recreate modul-06-signal-ranking
```
## Environment Variables (Compose)
| Variable | Wert (Default) | Zweck |
|----------|---------------|-------|
| `PG_HOST` | `Modul-01-PostgreSQL` | Docker-interner Servicename |
| `PG_PORT` | `5432` | intern |
| `PG_USER/PASSWORD/DB` | `trading` | aus `.env`-Defaults |
| `RABBITMQ_HOST` | `Modul-02-RabbitMQ` | Docker-interner Servicename |
| `RABBITMQ_PORT` | `5672` | intern |
| `RABBITMQ_USER/PASSWORD/VHOST` | `trading` | **vhost `trading`** |
| `LOG_LEVEL` | `INFO` | Strukturiertes Logging |
Keine festen IPs — nur Docker-interne Hostnamen. Creds via Compose env + Defaults.
## Architektur
```
Modul-05 ──signal.detected──▶ RankingConsumer (ranking.input)
OHLCV (Modul-03, intern 55003/history/{symbol})
RankingEngine (deterministisch, Registry)
signal_quality_v1 (9 Komponenten, Score 0100)
┌───────────┴───────────┐
▼ ▼
signal_ranking (PG) SIGNAL_RANKED
(eigene Tabelle) → market.rankings / signal.ranked
```
- **Consumer** (`app/consumer/consumer.py`): bindet Exchange `market.signals`, Routing `strategy.signal.detected`; durable Queue `ranking.input`; manuelles Ack erst nach erfolgreicher Verarbeitung; Reconnect mit Backoff + `conn.close()`; **`queue_delete` beim Start** (entfernt verwaiste/Zombie-Consumer).
- **MarketData-Client** (`app/marketdata/client.py`): liest OHLCV über interne FastAPI Modul-03 (`http://Modul-03-Market-Data:55003/history/{symbol}`); hoher Lookback, damit die letzten geschlossenen Kerzen enthalten sind.
- **Engine/Register** (`app/ranking/engine.py`): modular — Ranking-Engines via `.name/.version/rank()` registriert; KI/ML später ergänzbar.
- **Storage** (`app/storage/storage.py`): idempotent via partiellen Unique-Index auf `source_signal_id`.
- **Publisher** (`app/publisher/publisher.py`): **frische Verbindung je Publish** + `conn.close()` im finally (verhindert `ConnectionResetError` durch RabbitMQ-Closed-Verbindungen).
- **Service** (`app/core/service.py`): Pipeline Event → OHLCV → Ranking → speichern + publizieren; robuste Payload-Extraktion; setzt `source_signal_id` aus dem Event-Top-Level, falls die Engine es nicht aus dem inneren payload ziehen konnte.
## Ranking-Engine V1 — `signal_quality_v1` (`app/ranking/signal_quality_v1.py`)
Deterministische Qualitätsbewertung. **9 Komponenten, Score 0100, vollständig nachvollziehbar und konfigurierbar.** Jede Komponente liefert 0..max; Summe = `total_score`. Komponenten werden einzeln in der DB gespeichert (`comp_*`).
| Komponente | Max | Bewertet |
|-----------|-----|----------|
| `regime` | 20 | Market-Regime-Qualität / Confidence |
| `trend` | 15 | Trendstärke |
| `pullback` | 15 | Pullback-Qualität |
| `trigger` | 10 | Entry-Bestätigung |
| `rr` | 10 | Chance/Risiko-Verhältnis |
| `volatility` | 10 | Volatilitätszustand |
| `distance` | 10 | Distanz Entry→Stop |
| `data_quality` | 5 | Datenqualität / Candle-Anzahl |
| `consistency` | 5 | Setup-Konsistenz |
**Klassifizierung:** `total_score >= eligible_threshold``eligible`; darunter `weak` (wird trotzdem gespeichert).
**Zentrale Schwellenwerte** (`app/config.py`, env-overridable):
| Parameter | Default | Bedeutung |
|-----------|---------|-----------|
| `max_score` | 100 | Score-Obergrenze |
| `eligible_threshold` | 70 | Score ≥ 70 → `eligible` |
| `ranking_lookback` | 500 | max. Kerzen für Trend/Volatilität (Modul-03 liefert die N ältesten, daher hoch) |
| `min_candles_required` | 60 | unter dieser Zahl → Datenqualität-Punktabzug |
| `full_candles_target` | 250 | ab dieser Zahl → volle Datenqualität |
| `pullback_depth_min/max` | 0.5 / 4.0 | Abstand Entry zu Trend-Referenz in ATR |
| `vol_ideal_lo/hi` | 0.005 / 0.030 | ATR %-Fenster für "ideale" Volatilität |
Keine Trade-Freigabe — nur Qualitätsbewertung.
## Datenbank (Modul-01-PostgreSQL)
Eigene Tabelle `public.signal_ranking` (bestehende unangetastet):
```sql
ranking_id UUID, source_signal_id TEXT, source_event_id TEXT, correlation_id TEXT,
timestamp TIMESTAMPTZ, symbol TEXT, asset_class TEXT, provider TEXT, timeframe TEXT,
ranking_name TEXT, ranking_version TEXT, ranking_class TEXT (eligible/weak),
total_score DOUBLE PRECISION, comp_regime/comp_trend/comp_pullback/comp_trigger/comp_rr/
comp_volatility/comp_distance/comp_data_quality/comp_consistency DOUBLE PRECISION,
details JSONB, reasons JSONB
```
- **Unique (partiell):** `uq_signal_ranking_src` auf `(symbol, timeframe, source_signal_id)` **WHERE source_signal_id IS NOT NULL** → Idempotenz (`source_signal_id` nie doppelt).
- Migration: `migrations/001_signal_ranking.sql` (idempotent, löscht nichts).
## RabbitMQ (Modul-02)
| Exchange | Typ | Routing | Event |
|----------|-----|---------|-------|
| `market.signals` | topic | `strategy.signal.detected` (eingang) | SIGNAL_DETECTED |
| `market.rankings` | topic | `signal.ranked` (ausgang) | SIGNAL_RANKED |
## Interne API
| Endpoint | Zweck |
|----------|-------|
| `GET /health` | Liveness (200) + Komponentenstatus im Body |
| `GET /health/ready` | Readiness (503 wenn PG/RabbitMQ/Modul-03 down) |
| `GET /rankings/{symbol}` | Rankings eines Symbols |
| `GET /rankings/latest/{symbol}` | Neuestes Ranking eines Symbols |
| `GET /ranking-engines` | Registrierte Ranking-Engines |
## End-to-End-Test (20.08.2026, final) ✅
Kette verifiziert: Modul-03 Ingest → `market.data.ready` → Modul-04 → `market.regime.ready` → Modul-05 → `SIGNAL_DETECTED` → Modul-06 Consumer → `signal_quality_v1``signal_ranking``SIGNAL_RANKED`.
| Fall | Ranking | Score | Class | DB | SIGNAL_RANKED |
|------|---------|-------|-------|----|---------------|
| M5LONG (TREND_UP) | **eligible** | **72.2** | eligible | ✅ 1 | ✅ 1 |
| M5SHORT (TREND_DOWN) | **eligible** | **73.6** | eligible | ✅ 1 | ✅ 1 |
| M5NOPULL (kein Pullback) | — | — | — | ✅ 0 | ✅ 0 |
| M5RANGE (Range) | — | — | — | ✅ 0 | ✅ 0 |
**Verifizierte Eigenschaften:**
- Gültiger LONG/SHORT → **genau 1** `SIGNAL_RANKED`; NOPULL/RANGE → **0** Ranking + 0 Event. ✅
- DB-Eintrag je gültigem Signal vorhanden; `source_signal_id` korrekt übernommen (z.B. `effd782c...`, `42dd2042...`). ✅
- `total_score` immer 0100; **Summe aller Komponenten == total_score**. ✅
- `eligible`/`weak` korrekt gemäß Threshold (72.2/73.6 ≥ 70 → eligible). ✅
- **Idempotenz:** identisches `SIGNAL_DETECTED` erneut → **kein zweiter DB-Eintrag, kein zweites SIGNAL_RANKED** (count=1). ✅
- **RabbitMQ-Reconnect:** kontrollierter Neustart → Modul-04/05/06 verbinden automatisch (Backoff 1s→2s→4s→8s), **je exakt 1 Consumer, keine Zombies, keine verlorenen Events.**
- **Logs:** keine unbehandelten Tracebacks nach Reconnect. Health/Readiness `{postgresql:true, rabbitmq:true, market_data:true, consumer_ready:true}`. ✅
## Bugs behoben während E2E (20.08.2026)
| Bug | Fix |
|-----|-----|
| `source_signal_id` leer | Service setzt `source_signal_id` aus dem Event-Top-Level, falls die Engine es nicht aus dem inner-Payload ziehen konnte |
| OHLCV-Lookback zu klein (120) | `ranking_lookback` auf 500 erhöht — Modul-03 `/history?limit=N` liefert die N **ältesten** Kerzen; zu kleiner Lookback → Engine bewertete auf veralteten Kerzen (Konsistenz/Trend/Pullback schief) |
| `direction`-Spalte existierte nicht | E2E-Query auf `details`/Modell angepasst |
## Offene Punkte
- Downstream-Consumer für `signal.ranked` (Modul-07+ — noch NICHT begonnen)
- KI/ML-Ranking-Engine über Registry ergänzbar
- Trade-Freigabe später durch Risk Manager (noch nicht gebaut)
---
```
Geändert von: Rain Ocampo
Datum: 20.08.2026
Grund: Modul-06-Dokumentation angelegt (Signal-Ranking signal_quality_v1, signal_ranking-Schema, Events, E2E final grün).
```