Modul-05-Strategy-Engine freigegeben: Doku + README + ports-reference

This commit is contained in:
Rain Ocampo 2026-08-20 04:28:58 +00:00 committed by Rain Ocampo
parent 3cc710030c
commit b75e15bb94
3 changed files with 168 additions and 5 deletions

View file

@ -31,10 +31,11 @@ Jede Änderung folgt dem **Notation-Format**: Autor (`Alice` / `Rain Ocampo`) +
| 01 | PostgreSQL | ✅ healthy (postgres:16-alpine) | 55432 | | 01 | PostgreSQL | ✅ healthy (postgres:16-alpine) | 55432 |
| 02 | RabbitMQ | ✅ healthy (rabbitmq:3-management) | 55672 / 15672 | | 02 | RabbitMQ | ✅ healthy (rabbitmq:3-management) | 55672 / 15672 |
| 03 | Market-Data | ✅ healthy (market-data:0.1.0) | 55003 (intern) | | 03 | Market-Data | ✅ healthy (market-data:0.1.0) | 55003 (intern) |
| 04 | Market-Regime | ⬜ Platzhalter (alpine) | 55004 | | 04 | Market-Regime | ✅ healthy (market-regime:1.0.0) | 55004 (intern) |
| 0517 | … | ⬜ Platzhalter (alpine) | 5500555017 | | 05 | Strategy-Engine | ✅ healthy (strategy-engine:0.1.0) | 55005 (intern) |
| 0617 | … | ⬜ Platzhalter (alpine) | 5500655017 |
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 Datum: 20.08.2026
Grund: Initiale Repository-Struktur und Modul-03-Dokumentation angelegt. 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.

154
modul-05-strategy-engine.md Normal file
View file

@ -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=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 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` 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).
```

View file

@ -13,8 +13,8 @@
| 02 | Modul-02-RabbitMQ | RabbitMQ AMQP | **keiner (intern, `expose`)** | **nein** (nur intern) | | 02 | Modul-02-RabbitMQ | RabbitMQ AMQP | **keiner (intern, `expose`)** | **nein** (nur intern) |
| 02 | Modul-02-RabbitMQ | RabbitMQ Management | **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) | | 03 | Modul-03-Market-Data | FastAPI | 55003 (intern, `expose`) | **nein** (nur intern) |
| 04 | Modul-04-Market-Regime | — | 55004 | (Platzhalter) | | 04 | Modul-04-Market-Regime | FastAPI | 55004 (intern, expose) | nein (nur intern) |
| 05 | Modul-05-Strategy-Engine | — | 55005 | (Platzhalter) | | 05 | Modul-05-Strategy-Engine | FastAPI | 55005 (intern, expose) | nein (nur intern) |
| 0617 | … | — | 5500655017 | (Platzhalter) | | 0617 | … | — | 5500655017 | (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. > **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 Datum: 20.08.2026
Grund: Modul-01/02 von öffentlichen Ports auf interne `expose`-Bindings umgestellt (Security-Fix), Doku aktualisiert. 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.