From ed4e76c08750b05db09544c2939fd392e35416d2 Mon Sep 17 00:00:00 2001 From: Rain Ocampo Date: Thu, 20 Aug 2026 04:28:58 +0000 Subject: [PATCH] Modul-05-Strategy-Engine freigegeben: Doku + README + ports-reference --- README.md | 11 ++- modul-05-strategy-engine.md | 154 ++++++++++++++++++++++++++++++++++++ ports-reference.md | 8 +- 3 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 modul-05-strategy-engine.md diff --git a/README.md b/README.md index a0b96b8..aed91ef 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,11 @@ Jede Änderung folgt dem **Notation-Format**: Autor (`Alice` / `Rain Ocampo`) + | 01 | PostgreSQL | ✅ healthy (postgres:16-alpine) | 55432 | | 02 | RabbitMQ | ✅ healthy (rabbitmq:3-management) | 55672 / 15672 | | 03 | Market-Data | ✅ healthy (market-data:0.1.0) | 55003 (intern) | -| 04 | Market-Regime | ⬜ Platzhalter (alpine) | 55004 | -| 05–17 | … | ⬜ Platzhalter (alpine) | 55005–55017 | +| 04 | Market-Regime | ✅ healthy (market-regime:1.0.0) | 55004 (intern) | +| 05 | Strategy-Engine | ✅ healthy (strategy-engine:0.1.0) | 55005 (intern) | +| 06–17 | … | ⬜ Platzhalter (alpine) | 55006–55017 | -Details: siehe `modul-03-market-data.md` (vollständig implementiert), weitere Module folgen. +Details: siehe `modul-03-market-data.md`, `modul-04-market-regime.md`, `modul-05-strategy-engine.md` (vollständig implementiert), weitere Module folgen. --- @@ -59,3 +60,7 @@ Geändert von: Rain Ocampo Datum: 20.08.2026 Grund: Initiale Repository-Struktur und Modul-03-Dokumentation angelegt. ``` + +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: README aktualisiert — Modul-04 und Modul-05 als healthy/freigegeben eingetragen (Ports intern), Details-Link ergänzt. diff --git a/modul-05-strategy-engine.md b/modul-05-strategy-engine.md new file mode 100644 index 0000000..55a9eb2 --- /dev/null +++ b/modul-05-strategy-engine.md @@ -0,0 +1,154 @@ +# 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). +``` diff --git a/ports-reference.md b/ports-reference.md index a50dbc8..4f66dc3 100644 --- a/ports-reference.md +++ b/ports-reference.md @@ -13,8 +13,8 @@ | 02 | Modul-02-RabbitMQ | RabbitMQ AMQP | **keiner (intern, `expose`)** | **nein** (nur intern) | | 02 | Modul-02-RabbitMQ | RabbitMQ Management | **keiner (intern, `expose`)** | **nein** (nur intern) | | 03 | Modul-03-Market-Data | FastAPI | 55003 (intern, `expose`) | **nein** (nur intern) | -| 04 | Modul-04-Market-Regime | — | 55004 | (Platzhalter) | -| 05 | Modul-05-Strategy-Engine | — | 55005 | (Platzhalter) | +| 04 | Modul-04-Market-Regime | FastAPI | 55004 (intern, expose) | nein (nur intern) | +| 05 | Modul-05-Strategy-Engine | FastAPI | 55005 (intern, expose) | nein (nur intern) | | 06–17 | … | — | 55006–55017 | (Platzhalter) | > **SECURITY-FIX 20.08.2026:** Modul-01 (PostgreSQL) und Modul-02 (RabbitMQ) haben **keine Host-Port-Bindings** mehr. Die ehemaligen öffentlichen Ports **55432, 55672, 15672 sind geschlossen** und von außen nicht mehr erreichbar (verifiziert). Adminzugriff nur noch per SSH-Tunnel ins `trading-modules`-Netz oder `docker exec`. Alles läuft über `expose:` → nur im internen Docker-Netzwerk. @@ -47,3 +47,7 @@ Geändert von: Rain Ocampo Datum: 20.08.2026 Grund: Modul-01/02 von öffentlichen Ports auf interne `expose`-Bindings umgestellt (Security-Fix), Doku aktualisiert. ``` + +Geändert von: Rain Ocampo +Datum: 20.08.2026 +Grund: ports-reference aktualisiert — Modul-04 und Modul-05 von Platzhalter auf real (FastAPI, intern expose, nicht öffentlich) eingetragen.