--- 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 0–100, 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 0–100) │ ┌───────────┴───────────┐ ▼ ▼ 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 0–100, 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 0–100; **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). ```