C3H HUMAN REVIEW WAVE 1: migriert 16 freigegebene HR-A PAIR_MIGRATION_SAFE Paare. Pro Pair: Root (representation=source) + Canonical (representation=canonical, derived_from=Root-ID) atomar. Body byte-genau unveraendert. IDs stabil aus C3 Mapping Preview v2. modul-12/18/19/vps.md NICHT angefasst. Knowledge schema v1. (Red Queen)
162 lines
8.4 KiB
Markdown
162 lines
8.4 KiB
Markdown
---
|
||
knowledge_schema: 1
|
||
id: object/0aef122a-790b-b17a-9176-cc32df98c5a1
|
||
type: arch
|
||
role: module
|
||
representation: source
|
||
state: current
|
||
---
|
||
# 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).
|
||
```
|