From c0ee29f0fedfe8f6294a35e502a5fb81da1b563a Mon Sep 17 00:00:00 2001 From: root Date: Thu, 20 Aug 2026 05:07:08 +0000 Subject: [PATCH] =?UTF-8?q?Modul-06:=20Signal-Ranking=20Doku=20(signal=5Fq?= =?UTF-8?q?uality=5Fv1,=20E2E=20final=20gr=C3=BCn)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modul-06-signal-ranking.md | 159 +++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 modul-06-signal-ranking.md diff --git a/modul-06-signal-ranking.md b/modul-06-signal-ranking.md new file mode 100644 index 0000000..56361fe --- /dev/null +++ b/modul-06-signal-ranking.md @@ -0,0 +1,159 @@ +# Modul-06-Signal-Ranking — Betriebsdokumentation + +> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ⏳ Finaler Abschluss (E2E+Idempotenz+Reconnect 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). +```