trading-system-docs/notes/trading/system-docs/modul-05-strategy-engine.md

159 lines
8.3 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.

---
type: Modul
_organized: true
---
# 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=Entry2×(StopEntry)` = 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 1s2s4s8s), **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` 23) | `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).
```