164 lines
9.4 KiB
Markdown
164 lines
9.4 KiB
Markdown
---
|
||
type: Trading-Modul-System
|
||
_organized: true
|
||
---
|
||
|
||
# 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).
|
||
```
|