# Modul-05-Strategy-Engine — Betriebsdokumentation > Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ Freigegeben (E2E bestanden) ## Zweck **Strategie-Engine** — konsumiert `MARKET_REGIME_READY` (Modul-04) + OHLCV (Modul-03), berechnet deterministische Handelssignale und persistiert sie. Pipeline: `MARKET_REGIME_READY (Modul-04) → Strategie-Engine (trend_pullback_v1) → PostgreSQL (strategy_signal) → SIGNAL_DETECTED` **Deterministische, regelbasierte Engine — bewusst OHNE KI/ML, ohne Ranking, ohne Risk Management, ohne Order-Ausführung.** ## Container | Attribut | Wert | |----------|------| | Name | `Modul-05-Strategy-Engine` | | Image | `strategy-engine:0.1.0` (lokal gebaut) | | Port | **55005** — **NUR intern** (`expose`, nicht öffentlich) | | Netzwerk | `trading-modules` (bridge) | | Build-Context | `/opt/trading-modules/modul05-strategy-engine/` | | Restart | `unless-stopped` | | Healthcheck | ✅ `healthy` | ## Deployment ```bash cd /opt/trading-modules docker compose build modul-05-strategy-engine docker compose up -d --no-deps --force-recreate modul-05-strategy-engine ``` ## 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-04 ──market.regime.ready──▶ StrategyConsumer (strategy.input) │ ▼ OHLCV (Modul-03, intern 55003/history/{symbol}) │ ▼ StrategyEngine (deterministisch) trend_pullback_v1 (LONG/SHORT) │ ┌───────────┴───────────┐ ▼ ▼ strategy_signal (PG) SIGNAL_DETECTED (eigene Tabelle) → market.signals / strategy.signal.detected ``` - **Consumer** (`app/consumer/consumer.py`): bindet Exchange `market.regime`, Routing `market.regime.ready`; durable Queue `strategy.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}`); normalisiert Feld `ts` → `timestamp`. - **Engine/Register** (`app/engine.py`): modular — Strategien via `.name/.version/.evaluate()` registriert. - **Storage** (`app/storage/storage.py`): idempotent via partiellen Unique-Index. - **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 → Strategie → speichern + publizieren; robuste Payload-Extraktion. ## Strategie V1 — `trend_pullback_v1` (`app/strategies/trend_pullback_v1.py`) Deterministische Pullback-Strategie. **Nur abgeschlossene Candles, kein Lookahead-Bias.** **LONG-Bedingungen (Regime TREND_UP):** 1. Close > steigender SMA200 (Trendfilter) 2. Pullback: Close < EMA20 (Zug zurück in den Trend) 3. Bestätigung: Close > prev Close ODER Break prev High 4. Entry = Close; Stop unter Swing-Low; Target = 2R (R:R = 2.0) **SHORT-Bedingungen (Regime TREND_DOWN):** spiegelbildlich 1. Close < fallender SMA200 2. Pullback: Close > EMA20 3. Bestätigung: Close < prev Close ODER Break prev Low 4. Stop über Swing-High; Target = 2R **Mathematik (vom E2E verifiziert):** - **LONG:** `Target = Entry + 2 × (Entry - Stop)`; `R:R = 2.0`; Stop < Entry - **SHORT:** `Target = Entry - 2 × (Stop - Entry)`; `R:R = 2.0`; Stop > Entry **Zentrale Schwellenwerte** (`app/config.py`, env-overridable): | Parameter | Default | Bedeutung | |-----------|---------|-----------| | `lookback` | 300 | max. Kerzen zur Berechnung | | `min_candles_required` | 220 | UNKNOWN, wenn < 220 (SMA200 braucht 200) | | `sma_period` | 200 | Trendfilter SMA | | `ema_period` | 20 | Pullback-EMA | | `risk_reward` | 2.0 | Target-Multiplikator (2R) | | `swing_lookback` | 10 | Swing-Low/High-Fenster für Stop | Kein gültiges Setup → **kein Event** publiziert. ## Datenbank (Modul-01-PostgreSQL) Eigene Tabelle `public.strategy_signal` (bestehende unangetastet): ```sql signal_id UUID, source_event_id TEXT, correlation_id TEXT, timestamp TIMESTAMPTZ, symbol TEXT, asset_class TEXT, provider TEXT, timeframe TEXT, strategy_name TEXT, strategy_version TEXT, direction TEXT (LONG/SHORT), regime TEXT, entry DOUBLE PRECISION, stop_loss DOUBLE PRECISION, target DOUBLE PRECISION, risk_reward DOUBLE PRECISION, setup_metrics JSONB, trigger_reason TEXT ``` - **Unique (partiell):** `uq_strategy_signal_src` auf `(symbol, timeframe, source_event_id)` **WHERE source_event_id IS NOT NULL** → Idempotenz. - Migration: `migrations/001_strategy_signal.sql` (idempotent, löscht nichts). ## RabbitMQ (Modul-02) | Exchange | Typ | Routing | Event | |----------|-----|---------|-------| | `market.regime` | topic | `market.regime.ready` (eingang) | MARKET_REGIME_READY | | `market.signals` | topic | `strategy.signal.detected` (ausgang) | SIGNAL_DETECTED | ## Interne API | Endpoint | Zweck | |----------|-------| | `GET /health` | Liveness (200) + Komponentenstatus im Body | | `GET /health/ready` | Readiness (503 wenn PG/RabbitMQ/Modul-03 down) | ## End-to-End-Test (20.08.2026, final, nach Publisher-Fixes) ✅ Kette verifiziert: Modul-03 Ingest → `market.data.ready` → Modul-04 → `market.regime.ready` → Modul-05 Consumer → `trend_pullback_v1` → `strategy_signal` → `SIGNAL_DETECTED`. | Fall | Signal | Entry | Stop | Target | R:R | DB | Event | |------|--------|-------|------|--------|-----|----|-------| | M5LONG (TREND_UP) | **LONG** | 164.20 | 163.4764 | 165.6473 | **2.00** | ✅ 1 | ✅ 1 | | M5SHORT (TREND_DOWN) | **SHORT** | 135.80 | 136.4964 | 134.4073 | **2.00** | ✅ 1 | ✅ 1 | | M5NOPULL (kein Pullback) | keins | — | — | — | — | ✅ 0 | ✅ 0 | | M5RANGE (Range) | keins | — | — | — | — | ✅ 0 | ✅ 0 | **Mathematik verifiziert:** LONG `Target=Entry+2×(Entry-Stop)` = 164.20 + 2×0.72364 = **165.6473** ✓; SHORT `Target=Entry−2×(Stop−Entry)` = 135.80 − 2×0.69636 = **134.4073** ✓. **Idempotenz:** identisches `source_event_id` erneut → **kein zweiter DB-Eintrag, kein Doppel-Event** (count=1). ✅ **RabbitMQ-Reconnect:** kontrollierter Neustart → Modul-04+05 verbinden automatisch (Backoff 1s→2s→4s→8s), **je exakt 1 Consumer, keine Zombies, keine verlorenen Events.** ✅ **Logs:** keine `ConnectionResetError`, keine unbehandelten Tracebacks. Health `{postgresql:true, rabbitmq:true, market_data_ready:true}`. ✅ ## Bugs behoben während E2E (20.08.2026) | Bug | Fix | |-----|-----| | `ConnectionResetError` beim Publish | Publisher: **frische Verbindung je Publish** + `conn.close()` (RabbitMQ schließt ungenutzte Verbindung) — **gleicher Bug in Modul-03, -04, -05** | | Modul-03 OHLCV-Feld `ts` | Normalisierung `ts`→`timestamp` im MarketData-Client | | Zombie-Consumer (`strategy.input` 2–3) | `queue_delete` beim Consumer-Start; manuelles Cleanup via `rabbitmqctl delete_queue` | | Keine Events nach Recreate | OHLCV/Regime-Daten waren noch in DB (Duplikat) → E2E bereinigt `ohlcv`+`market_regime`+`strategy_signal` | | M5SHORT ging verloren | Zombie-Consumer verschluckte Event (Round-Robin) → beseitigt | ## Offene Punkte - Downstream-Consumer für `strategy.signal.detected` (Modul-06+) - Weitere Strategien über Engine-Register hinzufügbar --- ``` Geändert von: Rain Ocampo Datum: 20.08.2026 Grund: Modul-05-Dokumentation angelegt (Strategy-Engine trend_pullback_v1, strategy_signal-Schema, Events, E2E freigegeben). ```