Compare commits
22 commits
1d11e56ed4
...
dd0d7675c9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd0d7675c9 | ||
|
|
8f77230d6c | ||
|
|
ed77df65c3 | ||
|
|
a491c7b7d8 | ||
|
|
5cbd1984b9 | ||
|
|
e7df85df89 | ||
|
|
608c410d61 | ||
|
|
e48b8b6c1c | ||
|
|
86328ede98 | ||
|
|
33f0c56a18 | ||
|
|
741ab7bd57 | ||
|
|
90cf20036e | ||
|
|
5585c46caf | ||
|
|
1219ec6f3a | ||
|
|
b6afee27a2 | ||
|
|
3b4fa6a9c7 | ||
|
|
200e9774d4 | ||
|
|
b75e15bb94 | ||
|
|
3cc710030c | ||
|
|
cb207f95e9 | ||
|
|
a7820e30cf | ||
| b43837044c |
20 changed files with 2819 additions and 0 deletions
103
README.md
Normal file
103
README.md
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
# Trading-System — Modulare Architektur
|
||||||
|
|
||||||
|
Zentrales, modulares automatisiertes Trading-System, betrieben auf einem Hostinger-VPS (`187.124.31.123`).
|
||||||
|
|
||||||
|
**Repository-Inhalt:** Live-Dokumentation der Infrastruktur, der Module und des aktuellen Betriebszustands.
|
||||||
|
Jede Änderung folgt dem **Notation-Format**: Autor (`Alice` / `Rain Ocampo`) + Zeitstempel (`DD.MM.YYYY HH:MM`) + Grund.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architektur-Überblick
|
||||||
|
|
||||||
|
- **18 Module** (`Modul-01-PostgreSQL` … `Modul-18-Position-Manager`), orchestriert über `docker-compose.yml` unter `/opt/trading-modules/`.
|
||||||
|
- **Netzwerk:** `trading-modules` (bridge). Kommunikation ausschließlich über **Docker-interne Service-Hostnamen** (keine festen IPs).
|
||||||
|
- **Feste Host-Ports:** Modul-01=`55432`, Modul-02=`55672`+`15672`, Modul-03–17=`55003`–`55017`.
|
||||||
|
- **Zugangsdaten:** nur über Environment Variables / Docker-Secrets (Defaults in Compose mit `${VAR:-default}`).
|
||||||
|
- **Kernprinzip:** bestehende Container/Volumes/Daten nie löschen, keine unnötigen öffentlichen Ports öffnen, Ist-Zustand vor Änderung prüfen.
|
||||||
|
|
||||||
|
### Multi-Agent-Setup (Resident-Evil-Theme)
|
||||||
|
| Agent | Rolle | Port |
|
||||||
|
|-------|-------|------|
|
||||||
|
| **Alice** (OpenClaw) | Primary | 55163 |
|
||||||
|
| **Matt Addison** (OpenClaw) | Backup | 54524 |
|
||||||
|
| **Rain Ocampo** (Hermes) | Technischer Support | 32776 (UI) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Module
|
||||||
|
|
||||||
|
| Modul | Name | Status | Port |
|
||||||
|
|-------|------|--------|------|
|
||||||
|
| 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 | ✅ healthy (market-regime:1.0.0) | 55004 (intern) |
|
||||||
|
| 05 | Strategy-Engine | ✅ healthy (strategy-engine:0.1.0) | 55005 (intern) |
|
||||||
|
| 06 | Signal-Ranking | ✅ FREIGEGEBEN (signal-ranking:0.1.0) | 55006 (intern) |
|
||||||
|
| 07 | Risk-Manager | ✅ FREIGEGEBEN (risk-manager:0.1.0) | 55007 (intern) |
|
||||||
|
| 08 | Portfolio-Manager | ✅ FREIGEGEBEN (portfolio-manager:0.1.0) | 55008 (intern) |
|
||||||
|
| 09 | Execution-Service | ✅ FREIGEGEBEN (execution-service:0.1.0) | 55009 (intern) |
|
||||||
|
| 10 | Trade-Journal | ✅ FREIGEGEBEN (trade-journal:0.1.0) | 55010 (intern) |
|
||||||
|
| 11 | Analytics | ✅ FREIGEGEBEN (analytics-service:0.1.0) | 55011 (intern) |
|
||||||
|
| 12 | Backtesting | ✅ FREIGEGEBEN (backtesting:0.1.2) | 55012 (intern) |
|
||||||
|
| 13 | Optimization | ✅ FREIGEGEBEN (optimization:0.1.0) | 55013 (intern) |
|
||||||
|
| 14 | Notification | ✅ FREIGEGEBEN (notification:0.1.0) | 55014 (intern) |
|
||||||
|
| 15 | Monitoring-Control | ✅ FREIGEGEBEN (monitoring-control:0.1.0) | 55015 (intern) |
|
||||||
|
| 16 | Paperclip | ✅ FREIGEGEBEN (paperclip:0.1.0) | 55016 (intern) |
|
||||||
|
| 17 | Hermes-Agent | ✅ FREIGEGEBEN (hermes-agent:0.1.0) | 55017 (intern) |
|
||||||
|
| 18 | Position-Manager | ✅ FREIGEGEBEN (position-manager:0.1.0) | 55018 (intern) |
|
||||||
|
|
||||||
|
Details: siehe `modul-03-market-data.md` … `modul-18-position-manager.md` (Module 03–18
|
||||||
|
vollständig implementiert und freigegeben).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notation / Audit-Trail
|
||||||
|
|
||||||
|
Jede Änderung wird mit folgender Zeile dokumentiert:
|
||||||
|
|
||||||
|
```
|
||||||
|
Geändert von: [Alice | Rain Ocampo]
|
||||||
|
Datum: DD.MM.YYYY HH:MM
|
||||||
|
Grund: <Kurzbeschreibung>
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Alice** = Änderungen durch OpenClaw
|
||||||
|
- **Rain Ocampo** = Änderungen durch Hermes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Geändert
|
||||||
|
```
|
||||||
|
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.
|
||||||
|
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: README-Modultabelle aktualisiert — Module 06–12 als freigegeben eingetragen; Modul-12-Backtesting als FREIGEGEBEN markiert.
|
||||||
|
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: README-Modultabelle aktualisiert — Modul-12 auf backtesting:0.1.2 (V1.1), Modul-13 Optimization als E2E-VERIFIZIERT eingetragen.
|
||||||
|
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: README-Modultabelle aktualisiert — Modul-13 Optimization als FREIGEGEBEN markiert (Freigabe durch Nutzer 20.08.2026).
|
||||||
|
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: README-Modultabelle aktualisiert — Modul-14 Notification als FREIGEGEBEN eingetragen; Details-Link auf modul-14-notification.md erweitert.
|
||||||
|
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: README-Modultabelle aktualisiert — Modul-15 Monitoring-Control als FREIGEGEBEN eingetragen; Details-Link auf modul-15-monitoring-control.md erweitert.
|
||||||
|
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 21.08.2026
|
||||||
|
Grund: README-Modultabelle aktualisiert — Modul-16 Paperclip, Modul-17 Hermes-Agent, Modul-18 Position-Manager als FREIGEGEBEN eingetragen; Details-Link auf modul-18-position-manager.md erweitert.
|
||||||
51
infrastructure-handbook.md
Normal file
51
infrastructure-handbook.md
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Infrastruktur & Betriebs-Handbuch — Trading-System VPS
|
||||||
|
|
||||||
|
> Stand: 20.08.2026 · VPS: `187.124.31.123`
|
||||||
|
|
||||||
|
## Multi-Agent-Architektur (Resident-Evil-Theme)
|
||||||
|
| Agent | Plattform | Rolle | Port |
|
||||||
|
|-------|-----------|-------|------|
|
||||||
|
| **Alice** | OpenClaw (primary) | Hauptagent, Protagonistin | 55163 |
|
||||||
|
| **Matt Addison** | OpenClaw (backup) | Backup/Support | 54524 |
|
||||||
|
| **Rain Ocampo** | Hermes (support) | Technischer Support | 32776 (UI) |
|
||||||
|
|
||||||
|
Notation in Notion/Forgejo bei JEDER Änderung:
|
||||||
|
```
|
||||||
|
Geändert von: [Alice | Rain Ocampo]
|
||||||
|
Datum: DD.MM.YYYY HH:MM
|
||||||
|
Grund: …
|
||||||
|
```
|
||||||
|
|
||||||
|
## Container-Verwaltung (via SSH)
|
||||||
|
```bash
|
||||||
|
ssh root@187.124.31.123
|
||||||
|
docker compose -f /opt/trading-modules/docker-compose.yml ps
|
||||||
|
docker compose -f /opt/trading-modules/docker-compose.yml up -d <service>
|
||||||
|
docker compose -f /opt/trading-modules/docker-compose.yml build <service>
|
||||||
|
docker compose -f /opt/trading-modules/docker-compose.yml down # nur bei explizitem Wunsch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Trading-Module (17 Container)
|
||||||
|
- Compose: `/opt/trading-modules/docker-compose.yml`
|
||||||
|
- Container-Namen exakt: `Modul-01-PostgreSQL` … `Modul-17-Hermes-Agent`
|
||||||
|
- Netzwerk: `trading-modules`
|
||||||
|
- Modul-01 = PostgreSQL 16-alpine (Host 55432)
|
||||||
|
- Modul-02 = RabbitMQ 3-management (Host 55672/15672, vhost `trading`)
|
||||||
|
- Modul-03 = Market-Data (FastAPI, Port 55003 intern, **nicht öffentlich**)
|
||||||
|
- Modul-04–17 = alpine-Platzhalter (bis ausgebaut)
|
||||||
|
|
||||||
|
## Wichtige Hinweise
|
||||||
|
- **Coolify überschreibt Container-Configs bei Neustart.** Config-Änderungen an OpenClaw ausschließlich über Config-Dateien (nicht UI).
|
||||||
|
- **Hot-Reload** OpenClaw: `kill -HUP 1` im Container.
|
||||||
|
- **Ollama** braucht `OLLAMA_API_KEY`; interner Hostname `ollama-nb6d-ollama-1:11434`.
|
||||||
|
- VPS-Authentifizierung: **nur Public-Key-Auth**.
|
||||||
|
|
||||||
|
## Deleted Resources (VPS-Aufräumung 20.08.2026)
|
||||||
|
Folgendes wurde entfernt (~25 GB freigegeben): Chronos/Kronos (kronos-api, chronos-service, chronos-tradelog), trading-bridge, llm-trade-manager, deepseek-bot, MetaTrader-5, t212-pilot, mehrere redundante Hermes-Container/Volumes/Netzwerke. **Bleibt:** n8n, forgejo, paperclip, Tolaria, PostgreSQL, RabbitMQ, Ollama, Traefik, Coolify-Suite.
|
||||||
|
|
||||||
|
---
|
||||||
|
```
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: Infrastruktur-Handbuch für das Trading-System dokumentiert.
|
||||||
|
```
|
||||||
116
modul-03-market-data.md
Normal file
116
modul-03-market-data.md
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
# Modul-03-Market-Data — Betriebsdokumentation
|
||||||
|
|
||||||
|
> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ In Betrieb (healthy)
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
Zentrale Marktdatenquelle des Trading-Systems. Pipeline:
|
||||||
|
`Marktdaten → Validierung → Normalisierung → PostgreSQL → RabbitMQ-Event`
|
||||||
|
Keine Strategie, keine Orders, keine KI — nur zuverlässige Marktdaten.
|
||||||
|
|
||||||
|
## Container
|
||||||
|
| Attribut | Wert |
|
||||||
|
|----------|------|
|
||||||
|
| Name | `Modul-03-Market-Data` |
|
||||||
|
| Image | `market-data:0.1.0` (lokal gebaut) |
|
||||||
|
| Port | **55003** — **NUR intern** (`expose`, nicht öffentlich) |
|
||||||
|
| Netzwerk | `trading-modules` (bridge) |
|
||||||
|
| Build-Context | `/opt/trading-modules/modul03-market-data/` |
|
||||||
|
| Restart | `unless-stopped` |
|
||||||
|
| Healthcheck | ✅ `healthy` |
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
```bash
|
||||||
|
cd /opt/trading-modules
|
||||||
|
docker compose build modul-03-market-data
|
||||||
|
docker compose up -d modul-03-market-data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables (Compose)
|
||||||
|
| Variable | Wert (Default) | Zweck |
|
||||||
|
|----------|---------------|-------|
|
||||||
|
| `PG_HOST` | `Modul-01-PostgreSQL` | Docker-interner Servicename |
|
||||||
|
| `PG_PORT` | `5432` | intern (Host: 55432) |
|
||||||
|
| `PG_USER/PASSWORD/DB` | `trading` | aus `.env`-Defaults |
|
||||||
|
| `RABBITMQ_HOST` | `Modul-02-RabbitMQ` | Docker-interner Servicename |
|
||||||
|
| `RABBITMQ_PORT` | `5672` | intern (Host: 55672) |
|
||||||
|
| `RABBITMQ_USER/PASSWORD/VHOST` | `trading` | **vhost `trading`** |
|
||||||
|
| `DATA_PROVIDER` | `noop` | `noop` / `demo` / später Broker |
|
||||||
|
| `LOG_LEVEL` | `INFO` | Strukturiertes Logging |
|
||||||
|
|
||||||
|
Keine festen IPs — nur Docker-interne Hostnamen. Creds via Compose env + `${VAR:-default}`.
|
||||||
|
|
||||||
|
## Datenbank (Modul-01-PostgreSQL)
|
||||||
|
Tabelle `public.ohlcv`:
|
||||||
|
```sql
|
||||||
|
symbol TEXT, asset_class TEXT, provider TEXT, timeframe TEXT,
|
||||||
|
ts TIMESTAMPTZ, open/high/low/close DOUBLE PRECISION, volume DOUBLE PRECISION,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(), id BIGSERIAL PRIMARY KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
### Finale Constraints & Indizes (Stand 20.08.2026)
|
||||||
|
| Index | Typ |
|
||||||
|
|-------|-----|
|
||||||
|
| `uq_ohlcv_provider_symbol_tf_ts` | **UNIQUE** `(provider, symbol, timeframe, ts)` |
|
||||||
|
| `ohlcv_pkey` | UNIQUE `(id)` |
|
||||||
|
| `idx_ohlcv_symbol_tf` | `(symbol, timeframe)` |
|
||||||
|
| `idx_ohlcv_symbol_tf_ts` | `(symbol, timeframe, ts DESC)` |
|
||||||
|
| `idx_ohlcv_provider_symbol_tf_ts` | `(provider, symbol, timeframe, ts DESC)` |
|
||||||
|
|
||||||
|
Der Unique-Index inkludiert den **Provider** — langfristig werden mehrere Provider/Broker unterstützt
|
||||||
|
(gleiche Symbol+Timeframe+Timestamp können von verschiedenen Quellen kommen).
|
||||||
|
|
||||||
|
**Migrationen:** `/app/migrations/001_ohlcv.sql` + `002_unique_provider.sql`
|
||||||
|
(idempotent, löschen nichts; automatisch via `ensure_schema()`/glob angewendet).
|
||||||
|
|
||||||
|
## RabbitMQ (Modul-02)
|
||||||
|
- **Exchange:** `market.data` (topic, durable)
|
||||||
|
- **vhost:** `trading` (wichtig!)
|
||||||
|
- **Routing-Keys:**
|
||||||
|
|
||||||
|
| Routing-Key | Event-Typ | Wann |
|
||||||
|
|-------------|-----------|------|
|
||||||
|
| `market.data.ready` | `MARKET_DATA_READY` | **Batch/Import** — EIN Event pro Batch, `candle_count` + `batch:true` |
|
||||||
|
| `market.data.candle.closed` | `MARKET_CANDLE_CLOSED` | **Live** — pro abgeschlossener Kerze EIN Event, OHLCV im payload |
|
||||||
|
|
||||||
|
- Eventschema v1.0: `event_id, event_type, event_version, timestamp, symbol, asset_class, timeframe, provider` + payload.
|
||||||
|
|
||||||
|
## Interne API
|
||||||
|
| Endpoint | Zweck |
|
||||||
|
|----------|-------|
|
||||||
|
| `GET /health` | Liveness (200 immer) + Komponentenstatus im Body |
|
||||||
|
| `GET /health/ready` | Readiness (503 wenn PG/RabbitMQ down) |
|
||||||
|
| `POST /ingest` | Kerzen einspeisen (JSON: `{"candles":[...]}`) |
|
||||||
|
| `GET /prices/{symbol}` | Letzte Kurse |
|
||||||
|
| `GET /history/{symbol}?timeframe=` | Historische OHLCV |
|
||||||
|
|
||||||
|
## Provider-Adapter
|
||||||
|
`app/providers/providers.py`:
|
||||||
|
- `DataProvider` (ABC) — abstrakte Schnittstelle `fetch_ohlcv()`, `health()`
|
||||||
|
- `NoopProvider` — keine Datenquelle konfiguriert (Default)
|
||||||
|
- `DemoProvider` — synthetische OHLCV-Daten für Tests
|
||||||
|
- Neue Broker = neue Klasse, umschalten via `DATA_PROVIDER` env → keine harte Anbieter-Kopplung
|
||||||
|
|
||||||
|
## Validierung (`app/validation/validator.py`)
|
||||||
|
- Timestamp gültig (UTC, nicht Zukunft, nicht zu alt/stale)
|
||||||
|
- OHLC-Werte > 0
|
||||||
|
- High ≥ Low, High ≥ Open/Close, Low ≤ Open/Close
|
||||||
|
- Duplikat-Erkennung (Storage + DB-Unique-Index)
|
||||||
|
|
||||||
|
## End-to-End-Test (20.08.2026, nach Provider-Constraint-Upgrade) ✅
|
||||||
|
- **Batch-Pfad:** 4 GOOG-Kerzen → saved:4, **GENAU EIN** `MARKET_DATA_READY` (routing `market.data.ready`, candle_count:4). 3 NVDA → saved:3, EIN Event.
|
||||||
|
- **Live-Pfad:** 1 AMZN-Candle → `MARKET_CANDLE_CLOSED` (routing `market.data.candle.closed`, OHLCV im payload).
|
||||||
|
- **Provider-Duplikat:** identische NVDA-Kerze erneut → `duplicate`, kein Insert, kein Event.
|
||||||
|
- **Port-Sicherheit:** 55003 von außen (`http://187.124.31.123:55003/health`) → **nicht erreichbar** ✅
|
||||||
|
- Datenbestand final: AAPL 5, GOOG 4, NVDA 3, EURUSD 3, TSLA 4, MSFT 1, AMZN 1.
|
||||||
|
|
||||||
|
## Offene Punkte
|
||||||
|
- Echter Broker-/Datenprovider-Adapter (Interface bereit, noop/demo Defaults)
|
||||||
|
- Zusätzliche Daten (Bid/Ask/Spread, Ticks, Fundamentaldaten) — vorbereitet
|
||||||
|
- Consumer für `market.data.ready` / `market.data.candle.closed` (Modul-04 ff.)
|
||||||
|
|
||||||
|
---
|
||||||
|
```
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: Modul-03-Dokumentation angelegt (Provider-Constraint, Event-Trennung, Port-Nicht-Exposition).
|
||||||
|
```
|
||||||
152
modul-04-market-regime.md
Normal file
152
modul-04-market-regime.md
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
# Modul-04-Market-Regime — Betriebsdokumentation
|
||||||
|
|
||||||
|
> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ Freigegeben (E2E bestanden)
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
Erster **Consumer** der Market-Data-Events von Modul-03. Pipeline:
|
||||||
|
`MARKET_DATA_READY / MARKET_CANDLE_CLOSED (Modul-03) → Regime-Berechnung → PostgreSQL (market_regime) → MARKET_REGIME_READY`
|
||||||
|
**Deterministische, regelbasierte Engine — bewusst OHNE KI/ML.**
|
||||||
|
|
||||||
|
## Container
|
||||||
|
| Attribut | Wert |
|
||||||
|
|----------|------|
|
||||||
|
| Name | `Modul-04-Market-Regime` |
|
||||||
|
| Image | `market-regime:1.0.0` (lokal gebaut) |
|
||||||
|
| Port | **55004** — **NUR intern** (`expose`, nicht öffentlich) |
|
||||||
|
| Netzwerk | `trading-modules` (bridge) |
|
||||||
|
| Build-Context | `/opt/trading-modules/modul04-market-regime/` |
|
||||||
|
| Restart | `unless-stopped` |
|
||||||
|
| Healthcheck | ✅ `healthy` |
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
```bash
|
||||||
|
cd /opt/trading-modules
|
||||||
|
docker compose build modul-04-market-regime
|
||||||
|
docker compose up -d --no-deps --force-recreate modul-04-market-regime
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables (Compose)
|
||||||
|
| Variable | Wert (Default) | Zweck |
|
||||||
|
|----------|---------------|-------|
|
||||||
|
| `PG_HOST` | `Modul-01-PostgreSQL` | Docker-interner Servicename |
|
||||||
|
| `PG_PORT` | `5432` | intern (Host: 55432) |
|
||||||
|
| `PG_USER/PASSWORD/DB` | `trading` | aus `.env`-Defaults |
|
||||||
|
| `RABBITMQ_HOST` | `Modul-02-RabbitMQ` | Docker-interner Servicename |
|
||||||
|
| `RABBITMQ_PORT` | `5672` | intern (Host: 55672) |
|
||||||
|
| `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-03 ──market.data.ready / market.data.candle.closed──▶ RegimeConsumer
|
||||||
|
│ (bindet beide Routing-Keys)
|
||||||
|
▼
|
||||||
|
RegimeEngine (deterministisch)
|
||||||
|
EMA / ADX / ATR / Slope / Preisstruktur
|
||||||
|
│
|
||||||
|
┌───────────┴───────────┐
|
||||||
|
▼ ▼
|
||||||
|
market_regime (PG) MARKET_REGIME_READY
|
||||||
|
(16 Spalten) → market.regime.ready
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Consumer** (`app/consumer/consumer.py`): bindet `market.data.ready` + `market.data.candle.closed`; durable Queue `market-regime.input`; manuelles Ack; Reconnect mit Backoff; **schließt alte Verbindung beim Reconnect** (verhindert Consumer-Leak/Nachrichtenverlust).
|
||||||
|
- **Engine** (`app/regime/engine.py`): deterministisch, ohne KI.
|
||||||
|
- **Storage** (`app/storage/storage.py`): idempotent via partiellen Unique-Index.
|
||||||
|
- **Publisher** (`app/publisher/publisher.py`): publiziert `MARKET_REGIME_READY` auf `market.regime` (Routing `market.regime.ready`).
|
||||||
|
- **History-Client** (`app/marketdata/client.py`): liest OHLCV über interne FastAPI Modul-03 (`http://Modul-03-Market-Data:55003/history/{symbol}`).
|
||||||
|
|
||||||
|
## Regime-Engine (`app/regime/engine.py`)
|
||||||
|
Deterministische Regel-Engine (Version `1.0.0`), 7 Regime:
|
||||||
|
`TREND_UP, TREND_DOWN, RANGE, HIGH_VOLATILITY, LOW_VOLATILITY, TRANSITION, UNKNOWN`
|
||||||
|
|
||||||
|
**Indikatoren & Metriken:**
|
||||||
|
| Indikator | Fenster/Param | Zweck |
|
||||||
|
|-----------|---------------|-------|
|
||||||
|
| EMA fast/slow | 10 / 30 | Trendrichtung (EMA-Flanken-Differenz) |
|
||||||
|
| ADX | 14 | Trendstärke (≥20 = echter Trend) |
|
||||||
|
| ATR | 14 | Volatilität (absolut + Ratio + Perzentil) |
|
||||||
|
| Slope | 20 | normierte Steigung der Close-Linie |
|
||||||
|
| Preisstruktur | — | higher_highs / lower_lows / range |
|
||||||
|
|
||||||
|
**Zentrale Schwellenwerte** (`app/config.py`, env-overridable):
|
||||||
|
| Parameter | Default | Bedeutung |
|
||||||
|
|-----------|---------|-----------|
|
||||||
|
| `min_candles_required` | 30 | UNKNOWN, wenn weniger Daten |
|
||||||
|
| `regime_lookback` | 60 | max. Kerzen für Berechnung |
|
||||||
|
| `trend_min_ema_gap` | 0.02 | |EMA_fast-EMA_slow|/close ≥ → Trend |
|
||||||
|
| `adx_trend_threshold` | 20.0 | ADX ≥ → echter Trend |
|
||||||
|
| `slope_up/down_threshold` | 0.05 / -0.05 | normierte Steigung |
|
||||||
|
| `range_atr_ratio` | 0.02 | ATR/close darunter = Range |
|
||||||
|
| `atr_high_vol_multiplier` | 1.5 | ATR jetzt > hist_mean × → HIGH_VOL |
|
||||||
|
| `atr_low_vol_multiplier` | 0.6 | ATR jetzt < hist_mean × → LOW_VOL |
|
||||||
|
| `high_vol_atr_ratio` | 0.03 | ATR/close ≥ → starke Vol |
|
||||||
|
| `low_vol_atr_ratio` | 0.008 | ATR/close ≤ → geringe Vol |
|
||||||
|
| `slope_threshold` | 0.01 | |Slope| darunter = seitwärts |
|
||||||
|
| `transition_min_events` | 3 | Events für TRANSITION |
|
||||||
|
|
||||||
|
## Datenbank (Modul-01-PostgreSQL)
|
||||||
|
Tabelle `public.market_regime` (16 Spalten, eigene Tabelle — bestehende unangetastet):
|
||||||
|
```sql
|
||||||
|
symbol TEXT, asset_class TEXT, timeframe TEXT, provider TEXT,
|
||||||
|
regime TEXT, confidence INTEGER (0-100),
|
||||||
|
trend_strength DOUBLE PRECISION, volatility_state TEXT,
|
||||||
|
timestamp TIMESTAMPTZ, indicators_json JSONB,
|
||||||
|
candles_used INTEGER, version TEXT,
|
||||||
|
source_event_id TEXT, correlation_id TEXT,
|
||||||
|
data_ts TIMESTAMPTZ, data_ts_end TIMESTAMPTZ
|
||||||
|
```
|
||||||
|
- **Unique (partiell):** `uq_market_regime_src` auf `(symbol, timeframe, source_event_id)` **WHERE source_event_id IS NOT NULL** → Idempotenz.
|
||||||
|
- Migration: `migrations/001_market_regime.sql` (idempotent, löscht nichts).
|
||||||
|
|
||||||
|
## RabbitMQ (Modul-02)
|
||||||
|
| Exchange | Typ | Routing | Event |
|
||||||
|
|----------|-----|---------|-------|
|
||||||
|
| `market.data` | topic | `market.data.ready` (eingang) | MARKET_DATA_READY |
|
||||||
|
| `market.data` | topic | `market.data.candle.closed` (eingang) | MARKET_CANDLE_CLOSED |
|
||||||
|
| `market.regime` | topic | `market.regime.ready` (**ausgang**) | MARKET_REGIME_READY |
|
||||||
|
|
||||||
|
## Interne API
|
||||||
|
| Endpoint | Zweck |
|
||||||
|
|----------|-------|
|
||||||
|
| `GET /health` | Liveness (200 immer) + Komponentenstatus im Body |
|
||||||
|
| `GET /health/ready` | Readiness (503 wenn PG/RabbitMQ/Modul-03 down) |
|
||||||
|
| `GET /regime/{symbol}` | Regime-Einträge abfragen |
|
||||||
|
| `GET /regime/latest/{symbol}` | Letztes Regime eines Symbols |
|
||||||
|
|
||||||
|
## End-to-End-Test (20.08.2026, final, nach Rebuild) ✅
|
||||||
|
Kette verifiziert: Modul-03 Ingest → `market.data.ready` → Modul-04 Consumer → RegimeEngine → `market_regime` → `MARKET_REGIME_READY` auf `market.regime.ready`.
|
||||||
|
|
||||||
|
| Fall | Regime | Conf | candles | version | Event | DB |
|
||||||
|
|------|--------|------|---------|---------|-------|----|
|
||||||
|
| M4TREND_UP (40) | TREND_UP | 100 | 40 | 1.0.0 | genau 1 | ✅ |
|
||||||
|
| M4TREND_DN (40) | TREND_DOWN | 100 | 40 | 1.0.0 | genau 1 | ✅ |
|
||||||
|
| M4RANGE (40) | LOW_VOLATILITY (Range) | 60 | 40 | 1.0.0 | genau 1 | ✅ |
|
||||||
|
| M4HIGHVOL (40) | HIGH_VOLATILITY | 75 | 40 | 1.0.0 | genau 1 | ✅ |
|
||||||
|
| M4UNKNOWN (5) | UNKNOWN | 20 | 5 | 1.0.0 | genau 1 | ✅ |
|
||||||
|
| M4IDEMPOT (40) | TREND_UP | 100 | 40 | 1.0.0 | genau 1 | ✅ |
|
||||||
|
|
||||||
|
**Idempotenz:** dasselbe Quell-Event (`source_event_id`) erneut → **kein zweiter Datensatz**, kein Doppel-Event. ✅
|
||||||
|
**Logs:** keine Errors/Tracebacks. Health `{postgresql:true, rabbitmq:true, market_data_ready:true}`. ✅
|
||||||
|
|
||||||
|
## Bugs behoben während E2E (20.08.2026)
|
||||||
|
| Bug | Fix |
|
||||||
|
|-----|-----|
|
||||||
|
| `can't adapt type 'dict'` (JSONB) | `json.dumps(ind.model_dump(mode="json"))` |
|
||||||
|
| `tuple index out of range` (16/15) | `version` in INSERT-VALUES ergänzt |
|
||||||
|
| `ON CONFLICT` + partieller Index Fehler | `WHERE source_event_id IS NOT NULL` in Klausel |
|
||||||
|
| `model_dump(default=...)` TypeError | `default`-Kwarg entfernt (`model_dump(mode="json")`) |
|
||||||
|
| Consumer-Verbindungs-Leak | `conn.close()` bei Reconnect → kein Message-Leak |
|
||||||
|
|
||||||
|
## Offene Punkte
|
||||||
|
- Consumer-Downstream für `market.regime.ready` (Modul-05+)
|
||||||
|
- Bestätigte TRANSITION-Detektion mit echten Folgedaten
|
||||||
|
|
||||||
|
---
|
||||||
|
```
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: Modul-04-Dokumentation angelegt (Regime-Engine, market_regime-Schema, Events, E2E freigegeben).
|
||||||
|
```
|
||||||
154
modul-05-strategy-engine.md
Normal file
154
modul-05-strategy-engine.md
Normal 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=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).
|
||||||
|
```
|
||||||
159
modul-06-signal-ranking.md
Normal file
159
modul-06-signal-ranking.md
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
# Modul-06-Signal-Ranking — Betriebsdokumentation
|
||||||
|
|
||||||
|
> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ **Freigegeben** (E2E+Idempotenz+Reconnect+Doku grün)
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
**Signal-Ranking** — konsumiert `SIGNAL_DETECTED` (Modul-05) + OHLCV (Modul-03), bewertet die Signalqualität deterministisch (Score 0–100, vollständig nachvollziehbar und konfigurierbar), persistiert das Ergebnis und publiziert `SIGNAL_RANKED`.
|
||||||
|
Pipeline: `SIGNAL_DETECTED (Modul-05) → Signal-Ranking (signal_quality_v1) → PostgreSQL (signal_ranking) → SIGNAL_RANKED`
|
||||||
|
**Deterministische, regelbasierte Ranking-Engine — bewusst OHNE KI/ML. Keine Positionsgröße, kein Risk Management, keine Broker-/Order-Ausführung (Trade-Freigabe macht später der Risk Manager).** Modular aufgebaut, damit später eine ML/KI-Engine als ZUSÄTZLICHE Ranking-Engine registriert werden kann.
|
||||||
|
|
||||||
|
## Container
|
||||||
|
| Attribut | Wert |
|
||||||
|
|----------|------|
|
||||||
|
| Name | `Modul-06-Signal-Ranking` |
|
||||||
|
| Image | `signal-ranking:0.1.0` (lokal gebaut) |
|
||||||
|
| Port | **55006** — **NUR intern** (`expose`, nicht öffentlich) |
|
||||||
|
| Netzwerk | `trading-modules` (bridge) |
|
||||||
|
| Build-Context | `/opt/trading-modules/modul06-signal-ranking/` |
|
||||||
|
| Restart | `unless-stopped` |
|
||||||
|
| Healthcheck | ✅ `healthy` |
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
```bash
|
||||||
|
cd /opt/trading-modules
|
||||||
|
docker compose build modul-06-signal-ranking
|
||||||
|
docker compose up -d --no-deps --force-recreate modul-06-signal-ranking
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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-05 ──signal.detected──▶ RankingConsumer (ranking.input)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
OHLCV (Modul-03, intern 55003/history/{symbol})
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
RankingEngine (deterministisch, Registry)
|
||||||
|
signal_quality_v1 (9 Komponenten, Score 0–100)
|
||||||
|
│
|
||||||
|
┌───────────┴───────────┐
|
||||||
|
▼ ▼
|
||||||
|
signal_ranking (PG) SIGNAL_RANKED
|
||||||
|
(eigene Tabelle) → market.rankings / signal.ranked
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Consumer** (`app/consumer/consumer.py`): bindet Exchange `market.signals`, Routing `strategy.signal.detected`; durable Queue `ranking.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}`); hoher Lookback, damit die letzten geschlossenen Kerzen enthalten sind.
|
||||||
|
- **Engine/Register** (`app/ranking/engine.py`): modular — Ranking-Engines via `.name/.version/rank()` registriert; KI/ML später ergänzbar.
|
||||||
|
- **Storage** (`app/storage/storage.py`): idempotent via partiellen Unique-Index auf `source_signal_id`.
|
||||||
|
- **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 → Ranking → speichern + publizieren; robuste Payload-Extraktion; setzt `source_signal_id` aus dem Event-Top-Level, falls die Engine es nicht aus dem inneren payload ziehen konnte.
|
||||||
|
|
||||||
|
## Ranking-Engine V1 — `signal_quality_v1` (`app/ranking/signal_quality_v1.py`)
|
||||||
|
Deterministische Qualitätsbewertung. **9 Komponenten, Score 0–100, vollständig nachvollziehbar und konfigurierbar.** Jede Komponente liefert 0..max; Summe = `total_score`. Komponenten werden einzeln in der DB gespeichert (`comp_*`).
|
||||||
|
|
||||||
|
| Komponente | Max | Bewertet |
|
||||||
|
|-----------|-----|----------|
|
||||||
|
| `regime` | 20 | Market-Regime-Qualität / Confidence |
|
||||||
|
| `trend` | 15 | Trendstärke |
|
||||||
|
| `pullback` | 15 | Pullback-Qualität |
|
||||||
|
| `trigger` | 10 | Entry-Bestätigung |
|
||||||
|
| `rr` | 10 | Chance/Risiko-Verhältnis |
|
||||||
|
| `volatility` | 10 | Volatilitätszustand |
|
||||||
|
| `distance` | 10 | Distanz Entry→Stop |
|
||||||
|
| `data_quality` | 5 | Datenqualität / Candle-Anzahl |
|
||||||
|
| `consistency` | 5 | Setup-Konsistenz |
|
||||||
|
|
||||||
|
**Klassifizierung:** `total_score >= eligible_threshold` → `eligible`; darunter `weak` (wird trotzdem gespeichert).
|
||||||
|
|
||||||
|
**Zentrale Schwellenwerte** (`app/config.py`, env-overridable):
|
||||||
|
| Parameter | Default | Bedeutung |
|
||||||
|
|-----------|---------|-----------|
|
||||||
|
| `max_score` | 100 | Score-Obergrenze |
|
||||||
|
| `eligible_threshold` | 70 | Score ≥ 70 → `eligible` |
|
||||||
|
| `ranking_lookback` | 500 | max. Kerzen für Trend/Volatilität (Modul-03 liefert die N ältesten, daher hoch) |
|
||||||
|
| `min_candles_required` | 60 | unter dieser Zahl → Datenqualität-Punktabzug |
|
||||||
|
| `full_candles_target` | 250 | ab dieser Zahl → volle Datenqualität |
|
||||||
|
| `pullback_depth_min/max` | 0.5 / 4.0 | Abstand Entry zu Trend-Referenz in ATR |
|
||||||
|
| `vol_ideal_lo/hi` | 0.005 / 0.030 | ATR %-Fenster für "ideale" Volatilität |
|
||||||
|
|
||||||
|
Keine Trade-Freigabe — nur Qualitätsbewertung.
|
||||||
|
|
||||||
|
## Datenbank (Modul-01-PostgreSQL)
|
||||||
|
Eigene Tabelle `public.signal_ranking` (bestehende unangetastet):
|
||||||
|
```sql
|
||||||
|
ranking_id UUID, source_signal_id TEXT, source_event_id TEXT, correlation_id TEXT,
|
||||||
|
timestamp TIMESTAMPTZ, symbol TEXT, asset_class TEXT, provider TEXT, timeframe TEXT,
|
||||||
|
ranking_name TEXT, ranking_version TEXT, ranking_class TEXT (eligible/weak),
|
||||||
|
total_score DOUBLE PRECISION, comp_regime/comp_trend/comp_pullback/comp_trigger/comp_rr/
|
||||||
|
comp_volatility/comp_distance/comp_data_quality/comp_consistency DOUBLE PRECISION,
|
||||||
|
details JSONB, reasons JSONB
|
||||||
|
```
|
||||||
|
- **Unique (partiell):** `uq_signal_ranking_src` auf `(symbol, timeframe, source_signal_id)` **WHERE source_signal_id IS NOT NULL** → Idempotenz (`source_signal_id` nie doppelt).
|
||||||
|
- Migration: `migrations/001_signal_ranking.sql` (idempotent, löscht nichts).
|
||||||
|
|
||||||
|
## RabbitMQ (Modul-02)
|
||||||
|
| Exchange | Typ | Routing | Event |
|
||||||
|
|----------|-----|---------|-------|
|
||||||
|
| `market.signals` | topic | `strategy.signal.detected` (eingang) | SIGNAL_DETECTED |
|
||||||
|
| `market.rankings` | topic | `signal.ranked` (ausgang) | SIGNAL_RANKED |
|
||||||
|
|
||||||
|
## Interne API
|
||||||
|
| Endpoint | Zweck |
|
||||||
|
|----------|-------|
|
||||||
|
| `GET /health` | Liveness (200) + Komponentenstatus im Body |
|
||||||
|
| `GET /health/ready` | Readiness (503 wenn PG/RabbitMQ/Modul-03 down) |
|
||||||
|
| `GET /rankings/{symbol}` | Rankings eines Symbols |
|
||||||
|
| `GET /rankings/latest/{symbol}` | Neuestes Ranking eines Symbols |
|
||||||
|
| `GET /ranking-engines` | Registrierte Ranking-Engines |
|
||||||
|
|
||||||
|
## End-to-End-Test (20.08.2026, final) ✅
|
||||||
|
Kette verifiziert: Modul-03 Ingest → `market.data.ready` → Modul-04 → `market.regime.ready` → Modul-05 → `SIGNAL_DETECTED` → Modul-06 Consumer → `signal_quality_v1` → `signal_ranking` → `SIGNAL_RANKED`.
|
||||||
|
|
||||||
|
| Fall | Ranking | Score | Class | DB | SIGNAL_RANKED |
|
||||||
|
|------|---------|-------|-------|----|---------------|
|
||||||
|
| M5LONG (TREND_UP) | **eligible** | **72.2** | eligible | ✅ 1 | ✅ 1 |
|
||||||
|
| M5SHORT (TREND_DOWN) | **eligible** | **73.6** | eligible | ✅ 1 | ✅ 1 |
|
||||||
|
| M5NOPULL (kein Pullback) | — | — | — | ✅ 0 | ✅ 0 |
|
||||||
|
| M5RANGE (Range) | — | — | — | ✅ 0 | ✅ 0 |
|
||||||
|
|
||||||
|
**Verifizierte Eigenschaften:**
|
||||||
|
- Gültiger LONG/SHORT → **genau 1** `SIGNAL_RANKED`; NOPULL/RANGE → **0** Ranking + 0 Event. ✅
|
||||||
|
- DB-Eintrag je gültigem Signal vorhanden; `source_signal_id` korrekt übernommen (z.B. `effd782c...`, `42dd2042...`). ✅
|
||||||
|
- `total_score` immer 0–100; **Summe aller Komponenten == total_score**. ✅
|
||||||
|
- `eligible`/`weak` korrekt gemäß Threshold (72.2/73.6 ≥ 70 → eligible). ✅
|
||||||
|
- **Idempotenz:** identisches `SIGNAL_DETECTED` erneut → **kein zweiter DB-Eintrag, kein zweites SIGNAL_RANKED** (count=1). ✅
|
||||||
|
- **RabbitMQ-Reconnect:** kontrollierter Neustart → Modul-04/05/06 verbinden automatisch (Backoff 1s→2s→4s→8s), **je exakt 1 Consumer, keine Zombies, keine verlorenen Events.** ✅
|
||||||
|
- **Logs:** keine unbehandelten Tracebacks nach Reconnect. Health/Readiness `{postgresql:true, rabbitmq:true, market_data:true, consumer_ready:true}`. ✅
|
||||||
|
|
||||||
|
## Bugs behoben während E2E (20.08.2026)
|
||||||
|
| Bug | Fix |
|
||||||
|
|-----|-----|
|
||||||
|
| `source_signal_id` leer | Service setzt `source_signal_id` aus dem Event-Top-Level, falls die Engine es nicht aus dem inner-Payload ziehen konnte |
|
||||||
|
| OHLCV-Lookback zu klein (120) | `ranking_lookback` auf 500 erhöht — Modul-03 `/history?limit=N` liefert die N **ältesten** Kerzen; zu kleiner Lookback → Engine bewertete auf veralteten Kerzen (Konsistenz/Trend/Pullback schief) |
|
||||||
|
| `direction`-Spalte existierte nicht | E2E-Query auf `details`/Modell angepasst |
|
||||||
|
|
||||||
|
## Offene Punkte
|
||||||
|
- Downstream-Consumer für `signal.ranked` (Modul-07+ — noch NICHT begonnen)
|
||||||
|
- KI/ML-Ranking-Engine über Registry ergänzbar
|
||||||
|
- Trade-Freigabe später durch Risk Manager (noch nicht gebaut)
|
||||||
|
|
||||||
|
---
|
||||||
|
```
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: Modul-06-Dokumentation angelegt (Signal-Ranking signal_quality_v1, signal_ranking-Schema, Events, E2E final grün).
|
||||||
|
```
|
||||||
169
modul-07-risk-manager.md
Normal file
169
modul-07-risk-manager.md
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
# Modul-07: Risk-Manager
|
||||||
|
|
||||||
|
**Status:** ✅ **Freigegeben** (20.08.2026)
|
||||||
|
**Version:** 0.1.0
|
||||||
|
**Port (intern):** 55007
|
||||||
|
**Netzwerk:** `trading-modules-trading-modules`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
|
||||||
|
Deterministischer Risk-Manager in der Trading-Kette. Konsumiert `SIGNAL_RANKED`
|
||||||
|
(aus Modul-06), führt eine **harte, deterministische Risikoprüfung** durch, berechnet
|
||||||
|
die Positionsgröße und publiziert `RISK_APPROVED` oder `RISK_REJECTED`.
|
||||||
|
|
||||||
|
**Prinzipien:**
|
||||||
|
- **KEINE KI/ML** — rein deterministische Regeln.
|
||||||
|
- **KEINE Broker-Order** — nur Risiko-Entscheidung + Positionsgröße.
|
||||||
|
- **FAIL-CLOSED** — Fehler oder fehlende Daten führen **niemals** zu APPROVED,
|
||||||
|
sondern immer zu REJECTED.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
```
|
||||||
|
SIGNAL_RANKED (market.rankings / signal.ranked)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Modul-07 Risk-Manager │
|
||||||
|
│ Consumer (risk.input) │
|
||||||
|
│ → RiskEngine (Registry) │
|
||||||
|
│ → risk_v1 (deterministische Prüfung) │
|
||||||
|
│ → Storage (risk_decision, idempotent) │
|
||||||
|
│ → Publisher (market.risk) │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
├── RISK_APPROVED (risk.approved)
|
||||||
|
└── RISK_REJECTED (risk.rejected)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Komponenten
|
||||||
|
- `app/consumer/consumer.py` — RabbitMQ-Consumer `SIGNAL_RANKED`, manuelles ACK,
|
||||||
|
Zombie-Schutz (`queue_delete` beim Start), Reconnect-Backoff 1s→2s→4s→8s.
|
||||||
|
- `app/risk/engine.py` — Registry für Risiko-Verfahren (V1, zukünftig V2).
|
||||||
|
- `app/risk/risk_v1.py` — deterministische Kernlogik + Positionsgrößen-Mathematik.
|
||||||
|
- `app/instruments/registry.py` — instrument-agnostische Werte (Contract Size,
|
||||||
|
Lot Size, Tick Size/Value, Min/Max Quantity, Quantity Step, Währung/FX).
|
||||||
|
- `app/storage/storage.py` — idempotente Persistenz (unique auf `source_ranking_id`).
|
||||||
|
- `app/publisher/publisher.py` — frische Verbindung je Publish + `conn.close()`
|
||||||
|
im `finally` (RabbitMQ-Bug-Fix aus Modul-03/04/05/06).
|
||||||
|
- `app/api/main.py` — FastAPI, Port 55007 intern, `/health`, `/health/ready`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## V1-Risikoregeln (zentral konfigurierbar in `config.py`, env-overridable)
|
||||||
|
|
||||||
|
| Regel | Default | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `account_equity` | 10000 | Account-Equity |
|
||||||
|
| `risk_percent` | 0.005 | max. Risiko pro Trade (0,5 %) |
|
||||||
|
| `min_score` | 70 | Mindest-Ranking-Score |
|
||||||
|
| `min_ranking_class` | eligible | Mindest-Ranking-Klasse |
|
||||||
|
| `max_positions` | 5 | max. Anzahl Positionen (nur bei vorhandenen Daten) |
|
||||||
|
| `max_open_risk` | 0.02 | max. gesamtes offenes Risiko (nur bei vorhandenen Daten) |
|
||||||
|
| `daily_loss_limit` | 0.03 | tägliches Verlustlimit (nur bei vorhandenen Daten) |
|
||||||
|
| `max_drawdown` | 0.20 | maximales Drawdown-Limit (nur bei vorhandenen Daten) |
|
||||||
|
|
||||||
|
> **Hinweis:** Globale Portfolio-Limits (offene Positionen, Tagesverlust, Drawdown)
|
||||||
|
> werden **nur geprüft, soweit Daten vorhanden sind**. Die eigentliche
|
||||||
|
> Portfolio-Logik gehört zu **Modul-08**. Fehlende Portfolio-Daten sind **kein**
|
||||||
|
> REJECT-Grund.
|
||||||
|
|
||||||
|
### Positionsgröße
|
||||||
|
```
|
||||||
|
risk_amount = equity × risk_percent
|
||||||
|
risk_per_unit = abs(entry − stop)
|
||||||
|
raw_size = risk_amount / risk_per_unit
|
||||||
|
quantity = floor(raw_size / quantity_step) × quantity_step # ABRUNDEN
|
||||||
|
```
|
||||||
|
Abrunden auf `quantity_step` (nie aufrunden), damit das erlaubte Risiko nie
|
||||||
|
überschritten wird (FAIL-CLOSED-Sicherheit).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output (RiskDecision)
|
||||||
|
|
||||||
|
Nachvollziehbar gespeichert in `risk_decision`:
|
||||||
|
`risk_decision_id`, `source_ranking_id`, `symbol`, `asset_class`, `direction`,
|
||||||
|
`entry`, `stop_loss`, `target`, `equity`, `risk_percent`, `max_risk_amount`,
|
||||||
|
`calculated_position_size`, `actual_risk_amount`, `decision` (APPROVED/REJECTED),
|
||||||
|
`reason_codes`, `rule_version`, `timestamp`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RabbitMQ
|
||||||
|
|
||||||
|
| Rolle | Exchange | Routing Key | Queue |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Consumer | `market.rankings` | `signal.ranked` | `risk.input` |
|
||||||
|
| Publisher | `market.risk` | `risk.approved` | — |
|
||||||
|
| Publisher | `market.risk` | `risk.rejected` | — |
|
||||||
|
|
||||||
|
- Manuelles ACK erst nach erfolgreicher Verarbeitung.
|
||||||
|
- Idempotenz: gleiches `source_ranking_id` → keine Doppelentscheidung/Event.
|
||||||
|
- Reconnect ohne Zombies (Backoff 1s→2s→4s→8s, `queue_delete` beim Start).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
### Unit-Tests (`test_risk.py`) — 10/10 grün
|
||||||
|
1. gültiger LONG → APPROVED + korrekte Größe
|
||||||
|
2. gültiger SHORT → APPROVED
|
||||||
|
3. Score unter Threshold → REJECTED
|
||||||
|
4. falscher Stop → REJECTED
|
||||||
|
5. fehlende/ungültige Daten → REJECTED
|
||||||
|
6. Positionsgröße/Risiko mathematisch korrekt
|
||||||
|
7. Quantity-Step/Min/Max korrekt
|
||||||
|
8. Daily-Loss/Drawdown-Limit → REJECTED
|
||||||
|
9. Idempotenz
|
||||||
|
10. RabbitMQ-Reconnect
|
||||||
|
|
||||||
|
### E2E (03→04→05→06→07) — ALLE CHECKS BESTANDEN
|
||||||
|
- **A:** Kette 03→07, Decision in DB (LONG & SHORT) ✓
|
||||||
|
- **B:** exakt 1 Risiko-Event je Symbol (APPROVED/REJECTED) ✓
|
||||||
|
- **C:** Pflichtfelder in Decision vorhanden ✓
|
||||||
|
- **D:** identisches Ranking → kein Doppel-Decision ✓
|
||||||
|
- **E:** Score<70 / falscher Stop / fehlende Daten → REJECTED + source_ranking_id ✓
|
||||||
|
|
||||||
|
### Mathematik-Verifikation (aus DB)
|
||||||
|
| Symbol | Direction | Entry | Stop | equity | risk% | max_risk | qty | actual_risk |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| M5LONG | LONG | 164.2 | 163.47636 | 10000 | 0.005 | 50 | 69 | 49.93 |
|
||||||
|
| M5SHORT | SHORT | 135.8 | 136.49636 | 10000 | 0.005 | 50 | 71 | 49.44 |
|
||||||
|
|
||||||
|
Beide `actual_risk_amount ≤ max_risk_amount` ✓
|
||||||
|
|
||||||
|
### Reconnect-Test
|
||||||
|
RabbitMQ kontrolliert neu gestartet → alle 4 Queues (`market-regime.input`,
|
||||||
|
`strategy.input`, `ranking.input`, `risk.input`) mit **exakt 1 Consumer**,
|
||||||
|
keine Zombies, keine verlorenen Events, `risk_decision` stabil.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Health/Readiness
|
||||||
|
|
||||||
|
- `/health` → `{"status":"ok", "postgresql":true, "rabbitmq":true, "consumer_ready":true}`
|
||||||
|
- `/health/ready` → 200
|
||||||
|
- Container: `Modul-07-Risk-Manager` (healthy)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technischer offener Punkt: Modul-03 `/history?limit=N`
|
||||||
|
|
||||||
|
**Modul-03 `/history?limit=N` liefert aktuell die N ältesten Candles (aufsteigend).**
|
||||||
|
Das ist langfristig zu korrigieren (sollte die N **neuesten** Candles liefern).
|
||||||
|
Der Workaround in Modul-06 (Lookback 500) ist **nicht als API-Vertrag** zu
|
||||||
|
behandeln — er ist ein temporärer Workaround, kein garantiertes Verhalten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
- Compose: `/opt/trading-modules/docker-compose.yml` (Block `modul-07-risk-manager`)
|
||||||
|
- Nur `expose: 55007` (kein öffentlicher Port)
|
||||||
|
- `restart: unless-stopped`
|
||||||
|
- Image: `risk-manager:0.1.0`
|
||||||
252
modul-08-portfolio-manager.md
Normal file
252
modul-08-portfolio-manager.md
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
# Modul-08: Portfolio-Manager
|
||||||
|
|
||||||
|
**Status: FREIGEGEBEN** (20.08.2026)
|
||||||
|
|
||||||
|
Deterministischer Portfolio-Manager. Konsumiert `RISK_APPROVED` aus Modul-07,
|
||||||
|
prüft das Gesamtportfolio gegen zentrale Limits und publiziert
|
||||||
|
`TRADE_APPROVED` oder `PORTFOLIO_REJECTED`.
|
||||||
|
|
||||||
|
**Keine KI/ML. Keine Broker-Order. FAIL-CLOSED.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Rolle in der Kette
|
||||||
|
|
||||||
|
```
|
||||||
|
Modul-03 Ingest → 04 Regime → 05 Signal → 06 Ranking
|
||||||
|
→ SIGNAL_RANKED (market.rankings/signal.ranked)
|
||||||
|
→ Modul-07 Risk-Check → RISK_APPROVED (market.risk/risk.approved)
|
||||||
|
→ Modul-08 Portfolio-Check → portfolio_decision-Tabelle
|
||||||
|
→ TRADE_APPROVED / PORTFOLIO_REJECTED (market.portfolio)
|
||||||
|
```
|
||||||
|
|
||||||
|
Modul-08 prüft **NICHT erneut** die Positionsgrößenlogik aus Modul-07,
|
||||||
|
sondern ausschließlich **Portfolio-Risiken**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Events
|
||||||
|
|
||||||
|
| Richtung | Exchange | Routing-Key | Event |
|
||||||
|
|----------|----------|-------------|-------|
|
||||||
|
| Input (Consumer) | `market.risk` | `risk.approved` | `RISK_APPROVED` |
|
||||||
|
| Output (Publisher) | `market.portfolio` | `trade.approved` | `TRADE_APPROVED` |
|
||||||
|
| Output (Publisher) | `market.portfolio` | `portfolio.rejected` | `PORTFOLIO_REJECTED` |
|
||||||
|
|
||||||
|
Consumer-Queue: `portfolio.input` (durable, manuelles Ack, prefetch=1).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. V1 Portfolio-Regeln (zentral konfigurierbar, env-overridable)
|
||||||
|
|
||||||
|
| Regel | Config-Feld | Default |
|
||||||
|
|-------|-------------|---------|
|
||||||
|
| max. Anzahl offener Positionen | `max_positions` | 5 |
|
||||||
|
| max. gesamtes Open Risk % | `max_open_risk_percent` | 0.05 (5%) |
|
||||||
|
| max. Exposure pro Position % | `max_exposure_per_position_percent` | 0.20 |
|
||||||
|
| max. Exposure pro Symbol % | `max_exposure_per_symbol_percent` | 0.30 |
|
||||||
|
| max. Exposure pro Assetklasse % | `max_exposure_per_asset_class_percent` | 0.50 |
|
||||||
|
| max. Long-Exposure % | `max_long_exposure_percent` | 0.60 |
|
||||||
|
| max. Short-Exposure % | `max_short_exposure_percent` | 0.60 |
|
||||||
|
| Duplikat Symbol/Strategie erlauben | `allow_duplicate_symbol_strategy` | false |
|
||||||
|
| Korrelation anwenden | `apply_correlation` | false (vorbereitet) |
|
||||||
|
| Account Equity | `account_equity` | 10000.0 |
|
||||||
|
|
||||||
|
Alle Limits sind Prozentwerte des `account_equity`. Bei `account_equity <= 0`
|
||||||
|
→ FAIL-CLOSED `PORTFOLIO_REJECTED`.
|
||||||
|
|
||||||
|
**Später geplant** (V2): Sector/Country/Currency-Exposure, Korrelation
|
||||||
|
(erst wenn verlässliche Daten vorhanden).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. FAIL-CLOSED
|
||||||
|
|
||||||
|
Jede Exception, fehlende oder ungültige kritische Eingabe führt zu
|
||||||
|
`PORTFOLIO_REJECTED`, **niemals** zu `TRADE_APPROVED`. Fehlende kritische
|
||||||
|
Felder (quantity, risk, entry) → `PORTFOLIO_REJECTED` mit Reason-Codes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Portfoliozustand (autoritative Quelle)
|
||||||
|
|
||||||
|
Der Portfoliozustand wird **NICHT nur aus Events angenommen**. Die
|
||||||
|
Architektur trennt die Quelle des Zustands vom Entscheidungs-Code:
|
||||||
|
|
||||||
|
- `app/portfolio/state.py` definiert `PortfolioStateProvider` (Interface)
|
||||||
|
und `PortfolioState` / `OpenPosition`.
|
||||||
|
- V1-Provider liest offene Positionen aus der eigenen `portfolio_decision`-
|
||||||
|
Tabelle (alle `TRADE_APPROVED`-Entscheidungen).
|
||||||
|
- **Später** kann ein Broker-/Execution-Provider als autoritative Quelle
|
||||||
|
ergänzt werden, ohne den Entscheidungs-Code zu ändern.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Race-Condition-Schutz (atomar)
|
||||||
|
|
||||||
|
Zwei nahezu gleichzeitige `RISK_APPROVED` dürfen **nicht** beide auf demselben
|
||||||
|
alten Portfoliozustand genehmigt werden und dadurch Limits überschreiten.
|
||||||
|
|
||||||
|
Lösung (`app/storage/storage.py` → `evaluate_and_save_atomic`):
|
||||||
|
1. **Advisory Lock** (`pg_advisory_xact_lock`) serialisiert parallele
|
||||||
|
Portfolio-Entscheidungen.
|
||||||
|
2. Innerhalb **einer Transaktion**: Zustand lesen → Engine bewerten →
|
||||||
|
speichern → Commit.
|
||||||
|
3. Der zweite Prozess wartet auf den Lock und bewertet auf dem **aktualisierten**
|
||||||
|
Zustand (inkl. der ersten Entscheidung).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Idempotenz
|
||||||
|
|
||||||
|
- Unique-Index `uq_portfolio_decision_src` auf `source_risk_decision_id`
|
||||||
|
(partial, nur wo nicht NULL).
|
||||||
|
- Gleiches `source_risk_decision_id` → keine Doppelentscheidung/Event.
|
||||||
|
- Zusätzlich prüft der Service vor der Verarbeitung, ob die Quelle bereits
|
||||||
|
entschieden wurde (Skip + Ack).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. DB-Tabelle `portfolio_decision`
|
||||||
|
|
||||||
|
Jede Entscheidung wird gespeichert — **auch `PORTFOLIO_REJECTED`**.
|
||||||
|
|
||||||
|
| Spalte | Typ | Beschreibung |
|
||||||
|
|--------|-----|--------------|
|
||||||
|
| `id` | BIGSERIAL PK | |
|
||||||
|
| `portfolio_decision_id` | TEXT | eigene Entscheidungs-ID |
|
||||||
|
| `source_risk_decision_id` | TEXT | id des RISK_APPROVED (Modul-07) |
|
||||||
|
| `source_ranking_id` | TEXT | durchgereichtes Ranking (Modul-06) |
|
||||||
|
| `source_signal_id` | TEXT | durchgereichtes Signal (Modul-05) |
|
||||||
|
| `source_event_id` | TEXT | |
|
||||||
|
| `correlation_id` | TEXT | |
|
||||||
|
| `timestamp` | TIMESTAMPTZ | Entscheidungszeitpunkt (UTC) |
|
||||||
|
| `symbol` | TEXT | |
|
||||||
|
| `asset_class` | TEXT | |
|
||||||
|
| `strategy` | TEXT | |
|
||||||
|
| `direction` | TEXT | LONG / SHORT |
|
||||||
|
| `timeframe` | TEXT | |
|
||||||
|
| `proposed_quantity` | DOUBLE | aus RISK_APPROVED |
|
||||||
|
| `proposed_risk` | DOUBLE | actual_risk_amount |
|
||||||
|
| `proposed_exposure` | DOUBLE | qty × entry |
|
||||||
|
| `current_portfolio_risk` | DOUBLE | offenes Risiko VOR |
|
||||||
|
| `new_portfolio_risk` | DOUBLE | offenes Risiko NACH |
|
||||||
|
| `exposure_before` | DOUBLE | Gesamt-Exposure VOR |
|
||||||
|
| `exposure_after` | DOUBLE | Gesamt-Exposure NACH |
|
||||||
|
| `decision` | TEXT | TRADE_APPROVED / PORTFOLIO_REJECTED |
|
||||||
|
| `reason_codes` | TEXT[] | z. B. `{OK}` oder `{EXPOSURE_PER_POSITION_EXCEEDED,...}` |
|
||||||
|
| `rule_version` | TEXT | `portfolio_v1@1.0.0` |
|
||||||
|
| `details` | JSONB | Begründung + Zwischenwerte |
|
||||||
|
| `portfolio_snapshot` | JSONB | Portfoliozustand zum Zeitpunkt |
|
||||||
|
| `created_at` | TIMESTAMPTZ | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Reason-Codes
|
||||||
|
|
||||||
|
| Code | Bedeutung |
|
||||||
|
|------|-----------|
|
||||||
|
| `OK` | alle Limits eingehalten → TRADE_APPROVED |
|
||||||
|
| `MAX_POSITIONS_EXCEEDED` | max. offene Positionen erreicht |
|
||||||
|
| `OPEN_RISK_EXCEEDED` | gesamtes Open Risk überschritten |
|
||||||
|
| `EXPOSURE_PER_POSITION_EXCEEDED` | Exposure pro Position überschritten |
|
||||||
|
| `EXPOSURE_PER_SYMBOL_EXCEEDED` | Exposure pro Symbol überschritten |
|
||||||
|
| `EXPOSURE_PER_ASSET_CLASS_EXCEEDED` | Exposure pro Assetklasse überschritten |
|
||||||
|
| `LONG_EXPOSURE_EXCEEDED` | Long-Exposure überschritten |
|
||||||
|
| `SHORT_EXPOSURE_EXCEEDED` | Short-Exposure überschritten |
|
||||||
|
| `DUPLICATE_SYMBOL_STRATEGY` | doppelte Position Symbol/Strategie |
|
||||||
|
| `QUANTITY_MISSING` / `RISK_AMOUNT_MISSING` / `ENTRY_MISSING` | fehlende kritische Daten (FAIL-CLOSED) |
|
||||||
|
| `EQUITY_MISSING` | account_equity fehlt/ungültig (FAIL-CLOSED) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Architektur
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
config.py # zentrale Konfiguration (env-overridable)
|
||||||
|
core/
|
||||||
|
models.py # PortfolioDecision + Event-Modelle
|
||||||
|
service.py # Orchestrator (Consumer→Engine→Storage→Publisher)
|
||||||
|
portfolio/
|
||||||
|
engine.py # Registry der Portfolio-Verfahren
|
||||||
|
portfolio_v1.py # deterministische V1-Prüfung (FAIL-CLOSED)
|
||||||
|
state.py # PortfolioState-Provider (autoritative Quelle)
|
||||||
|
storage/
|
||||||
|
storage.py # idempotent + atomare Race-Condition-Lösung
|
||||||
|
publisher/
|
||||||
|
publisher.py # frische Verbindung je Publish + close
|
||||||
|
consumer/
|
||||||
|
consumer.py # manuelles Ack, Reconnect-Backoff, Zombie-Schutz
|
||||||
|
api/
|
||||||
|
main.py # FastAPI (Health/Readiness/Decisions/State)
|
||||||
|
migrations/
|
||||||
|
001_portfolio_decision.sql
|
||||||
|
Dockerfile
|
||||||
|
requirements.txt
|
||||||
|
test_portfolio.py # 11 Unit-Tests (Engine-Logik)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Tests
|
||||||
|
|
||||||
|
### Unit-Tests (`test_portfolio.py`, 11/11 grün)
|
||||||
|
1. erstes gültiges Portfolio-Trade → TRADE_APPROVED
|
||||||
|
2. max Positions überschritten → PORTFOLIO_REJECTED
|
||||||
|
3. max Open Risk überschritten → PORTFOLIO_REJECTED
|
||||||
|
4. Symbol-Exposure überschritten → PORTFOLIO_REJECTED
|
||||||
|
5. Long/Short-Exposure-Limit → PORTFOLIO_REJECTED
|
||||||
|
6. Duplicate Symbol/Strategie → PORTFOLIO_REJECTED
|
||||||
|
7. fehlender Portfoliozustand → FAIL-CLOSED
|
||||||
|
8. Idempotenz (deterministisch)
|
||||||
|
9. Assetklassen-Exposure → PORTFOLIO_REJECTED
|
||||||
|
10. Exposure pro Position → PORTFOLIO_REJECTED
|
||||||
|
11. Short-Exposure → PORTFOLIO_REJECTED
|
||||||
|
|
||||||
|
### E2E (Kette 03→04→05→06→07→08, alle Checks grün)
|
||||||
|
- **A**: Kette 03→08, Portfolio-Decision in DB (LONG & SHORT)
|
||||||
|
- **B**: exakt 1 Portfolio-Event je Symbol (TRADE_APPROVED/PORTFOLIO_REJECTED)
|
||||||
|
- **C**: Pflichtfelder in Portfolio-Decision vorhanden
|
||||||
|
- **D**: Idempotenz (identisches RISK_APPROVED → kein Doppel-Decision)
|
||||||
|
- **E**: parallele RISK_APPROVED → beide verarbeitet, keine Race-Verlust
|
||||||
|
- **F**: erstes gültiges Portfolio-Trade → TRADE_APPROVED
|
||||||
|
|
||||||
|
### Reconnect-Test
|
||||||
|
RabbitMQ gestoppt → Consumer erkennt Ausfall, Reconnect-Backoff (1s→2s→4s→8s).
|
||||||
|
RabbitMQ gestartet → Consumer verbindet sich neu, alle 5 Queues haben exakt
|
||||||
|
1 Consumer, Health/Readiness 200.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Health / Readiness
|
||||||
|
|
||||||
|
- `GET /health` → Liveness (immer 200, Komponentenstatus im Body)
|
||||||
|
- `GET /health/ready` → Readiness (200 nur wenn PG + RabbitMQ erreichbar)
|
||||||
|
- `GET /decisions/{symbol}` → gespeicherte Entscheidungen (inkl. REJECTED)
|
||||||
|
- `GET /portfolio-rules` → verfügbare Portfolio-Regeln
|
||||||
|
- `GET /portfolio/state` → aktueller Portfoliozustand
|
||||||
|
|
||||||
|
Port: **55008** (nur Docker-intern, kein öffentlicher Host-Port).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Deploy
|
||||||
|
|
||||||
|
- Compose-Service: `modul-08-portfolio-manager`
|
||||||
|
- Image: `portfolio-manager:0.1.0`
|
||||||
|
- Container: `Modul-08-Portfolio-Manager`
|
||||||
|
- Netz: `trading-modules`
|
||||||
|
- `restart: unless-stopped`
|
||||||
|
- Env: PG + RabbitMQ-Zugangsdaten (aus Compose)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Wichtige Fixes (20.08.2026)
|
||||||
|
|
||||||
|
**Consumer-Bug (alle Module 04–08):** `process_data_events(time_limit=None)`
|
||||||
|
verarbeitete nur EIN Event und baute danach die Verbindung neu auf. Der
|
||||||
|
erneute `_connect()` führte `queue_delete` aus und löschte wartende Events
|
||||||
|
(z. B. ein zweites, nahezu gleichzeitiges RISK_APPROVED) unwiederbringlich.
|
||||||
|
Fix: innere Schleife `while not stop: process_data_events(time_limit=1.0)`
|
||||||
|
hält die Verbindung am Leben. Betroffen und gefixt: Modul-04, 05, 06, 07, 08.
|
||||||
146
modul-09-execution-service.md
Normal file
146
modul-09-execution-service.md
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
# Modul-09: Execution-Service
|
||||||
|
|
||||||
|
**Status: FREIGEGEBEN** (20.08.2026)
|
||||||
|
|
||||||
|
Deterministischer Execution-Service. Konsumiert `TRADE_APPROVED` aus Modul-08,
|
||||||
|
validiert die Order hart (FAIL-CLOSED), übergibt sie an einen Broker-Adapter
|
||||||
|
(V1: PaperBrokerAdapter) und überwacht den Status. Ergebnis wird idempotent
|
||||||
|
in eigenen DB-Tabellen gespeichert und als RabbitMQ-Events publiziert.
|
||||||
|
|
||||||
|
**Sicherheit: KEIN unkontrolliertes Live-Trading.**
|
||||||
|
- Default `EXECUTION_MODE=PAPER` (bzw. DRY_RUN)
|
||||||
|
- `TRADING_ENABLED=false` als sicherer Default für LIVE
|
||||||
|
- LIVE nur über explizite Konfiguration aktivierbar, FAIL-CLOSED
|
||||||
|
- Bei Timeout nach Order-Senden NIE blind erneut senden — erst über
|
||||||
|
`broker_order_id`/`client_order_id` bzw. Brokerstatus klären
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
```
|
||||||
|
TRADE_APPROVED (market.portfolio/trade.approved, Modul-08)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
ExecutionConsumer (execution.input, Consumer-Fix von Anfang an)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Order-Validierung (hart, FAIL-CLOSED)
|
||||||
|
│ source_portfolio_decision_id gültig, decision=APPROVED,
|
||||||
|
│ symbol/direction/quantity vorhanden, quantity>0,
|
||||||
|
│ entry/stop/target plausibel, Kette nachvollziehbar,
|
||||||
|
│ kein bereits ausgeführter identischer Auftrag, Kill-Switch
|
||||||
|
▼
|
||||||
|
BrokerAdapter-Interface (modular, austauschbar)
|
||||||
|
│ V1: PaperBrokerAdapter (realistischer Lifecycle)
|
||||||
|
▼
|
||||||
|
ExecutionStorage (idempotent, Advisory Lock + Transaktion)
|
||||||
|
│ execution_order + execution_event
|
||||||
|
▼
|
||||||
|
RabbitMQPublisher (market.execution)
|
||||||
|
ORDER_SUBMITTED / ORDER_FILLED / ORDER_REJECTED / ORDER_FAILED
|
||||||
|
```
|
||||||
|
|
||||||
|
## BrokerAdapter-Interface
|
||||||
|
|
||||||
|
- `BrokerAdapter` (app/broker/base.py): `name()`, `is_configured()`,
|
||||||
|
`submit_order()`, `get_order_status()`
|
||||||
|
- `PaperBrokerAdapter` (app/broker/paper.py): V1, simuliert realistischen
|
||||||
|
Lifecycle (SUBMITTED → FILLED, Partial Fill, Reject, Timeout) für Tests
|
||||||
|
- `BrokerRegistry` (app/broker/registry.py): wählt Adapter anhand
|
||||||
|
`DEFAULT_BROKER` (V1: paper). Echte Broker-Adapter später austauschbar,
|
||||||
|
keine Brokerlogik im Core-Code.
|
||||||
|
|
||||||
|
## Idempotenz
|
||||||
|
|
||||||
|
- Unique-Index `uq_execution_order_src` auf `source_portfolio_decision_id`
|
||||||
|
→ gleiches TRADE_APPROVED erzeugt NIE eine Doppelorder
|
||||||
|
- Unique-Index `uq_execution_order_client` auf `client_order_id`
|
||||||
|
- Client Order ID deterministisch aus Execution-ID abgeleitet
|
||||||
|
(`EXEC-<execution_id[:32]>`)
|
||||||
|
- Advisory Lock + Transaktion: parallele identische Events → exakt eine
|
||||||
|
Execution (Race-Condition-Schutz, wie Modul-08)
|
||||||
|
- ACK eines TRADE_APPROVED erst, wenn die Order dauerhaft in der DB gespeichert ist
|
||||||
|
|
||||||
|
## Order-Status
|
||||||
|
|
||||||
|
`PENDING → SUBMITTED → PARTIALLY_FILLED → FILLED`
|
||||||
|
sowie `REJECTED`, `CANCELLED`, `FAILED`.
|
||||||
|
|
||||||
|
## DB-Tabellen
|
||||||
|
|
||||||
|
- `execution_order`: execution_id, source_portfolio_decision_id, broker, mode,
|
||||||
|
symbol, asset_class, direction, quantity, requested_price, stop_loss, target,
|
||||||
|
broker_order_id, client_order_id, status, filled_quantity, avg_fill_price,
|
||||||
|
timestamps, error/reason_codes, execution_version
|
||||||
|
- `execution_event`: append-only Log der Status-Übergänge
|
||||||
|
|
||||||
|
## RabbitMQ
|
||||||
|
|
||||||
|
- Exchange `market.execution` (topic, durable)
|
||||||
|
- Routing: `order.submitted`, `order.filled`, `order.rejected`, `order.failed`
|
||||||
|
- Queue `execution.input` (durable), gebunden an `market.portfolio`/`trade.approved`
|
||||||
|
- Consumer-Fix aus Modul-04–08 von Anfang an: dauerhafte Verbindung,
|
||||||
|
`process_data_events(time_limit=1.0)` in innerer Schleife, KEIN queue_delete
|
||||||
|
bei normalen Reconnects, Backoff 1s→2s→4s→8s→16s→30s
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- **21/21 Unit-Tests** (test_execution.py): gültige Order → PAPER FILLED,
|
||||||
|
Idempotenz, ungültige Quantity → REJECTED, Kill-Switch, fehlende
|
||||||
|
Broker-Credentials → FAIL-CLOSED, Broker-Reject, Timeout → keine blinde
|
||||||
|
Doppelorder, Partial Fill, Reconnect-Backoff, parallele identische Events
|
||||||
|
- **E2E 6/6 Checks** (Kette 03→04→05→06→07→08→09): Execution-Order in DB,
|
||||||
|
exakt 1 Paper-Order, Pflichtfelder, Idempotenz, ORDER_SUBMITTED+ORDER_FILLED,
|
||||||
|
gültiger Trade → PAPER FILLED
|
||||||
|
- **Reconnect-Test**: RabbitMQ gestoppt → Backoff → neu verbunden, 1 Consumer,
|
||||||
|
kein queue_delete bei normalen Reconnects
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
- Container `Modul-09-Execution-Service`, Port 55009 nur Docker-intern
|
||||||
|
(kein öffentlicher Host-Port)
|
||||||
|
- Health/Readiness 200, PG + RabbitMQ erreichbar, Consumer bereit
|
||||||
|
- Env: `EXECUTION_MODE=PAPER`, `TRADING_ENABLED=false`, `DEFAULT_BROKER=paper`
|
||||||
|
|
||||||
|
## Geändert
|
||||||
|
```
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: Modul-09-Doku um final freigegebenen Control-Gate-Status (M15→M09, Variante C) ergänzt.
|
||||||
|
Kernregel, Einbau-Punkt, Audit-Felder, Parameter, Live-E2E-Ergebnisse dokumentiert.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Control-Gate (M15→M09, FREIGEGEBEN 20.08.2026)
|
||||||
|
|
||||||
|
**Status: FREIGEGEBEN / PRODUKTIV VERIFIZIERT** — Variante C (Event-Cache-Vorfilter + synchroner HTTP-Final-Check).
|
||||||
|
|
||||||
|
M15 (`Modul-15-Monitoring-Control`) ist die **autoritative Safety-/Trading-Control-Instanz**.
|
||||||
|
M09 sendet neue Orders (OPEN/INCREASE) **nur**, wenn M15 eindeutig `ENABLED` + frisch bestätigt.
|
||||||
|
|
||||||
|
### Kernregel
|
||||||
|
| State | OPEN/INCREASE | REDUCE/CLOSE/CANCEL |
|
||||||
|
|-------|---------------|---------------------|
|
||||||
|
| `ENABLED` | **ERLAUBT** | erlaubt |
|
||||||
|
| `PAUSED` | **VERBOTEN** | erlaubt |
|
||||||
|
| `HALTED` | **VERBOTEN** | erlaubt |
|
||||||
|
| `UNKNOWN` / M15-down / stale | **VERBOTEN (FAIL-CLOSED)** | erlaubt |
|
||||||
|
|
||||||
|
### Einbau-Punkt
|
||||||
|
Control-Check zwischen Schritt 4 (Order atomar in DB anlegen) und Schritt 5 (`_submit_and_track`).
|
||||||
|
Bei OPEN/INCREASE ohne Erlaubnis → Order als `FAILED`/`REJECTED` speichern, **nicht** senden.
|
||||||
|
`TRADING_ENABLED`-Flag bleibt unverändert (blockiert nur LIVE, nicht PAPER).
|
||||||
|
|
||||||
|
### Audit-Felder je Order
|
||||||
|
`action_type`, `control_status`, `control_state_version`, `control_expires_at`,
|
||||||
|
`control_audit_id`, `control_check_source` (`http`|`cache`|`bypass`|`none`).
|
||||||
|
|
||||||
|
### Parameter
|
||||||
|
- Final-Check-HTTP-Timeout: **500 ms**, Retry: **max 1** sofort.
|
||||||
|
- `expires_at`-TTL (M15): **500 ms**; Cache-TTL (M09): **5 s**.
|
||||||
|
- Kein Auto-Resume; kein "Senden ohne Check".
|
||||||
|
|
||||||
|
### Live-E2E (VPS, 20.08.2026) — alle Szenarien grün
|
||||||
|
S1 ENABLED 9/9 · S2 HALTED 9/9 · S3 PAUSED 4/4 · S4 Risikoabbau 4/4 · S5 M15-down 7/7 ·
|
||||||
|
S6 Cache-vs-Final 2/2 · S7 Race/TTL 2/2 · S8 Event-Cache verifiziert · S9 Recovery 3/3 ·
|
||||||
|
S10 Health/Security verifiziert · S11 Regression bestanden.
|
||||||
|
|
||||||
|
Details: siehe `modul-15-m09-anbindung-implementierung.md` (FREIGEGEBEN).
|
||||||
106
modul-10-trade-journal.md
Normal file
106
modul-10-trade-journal.md
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
# Modul-10: Trade-Journal
|
||||||
|
|
||||||
|
**Status: FREIGEGEBEN** (20.08.2026)
|
||||||
|
|
||||||
|
Deterministisches, append-only Trade-Journal. Konsumiert die
|
||||||
|
Execution-Events (`ORDER_SUBMITTED`/`ORDER_FILLED`/`ORDER_REJECTED`/`ORDER_FAILED`)
|
||||||
|
aus Modul-09, validiert die Herkunftskette hart (FAIL-CLOSED) und dokumentiert
|
||||||
|
jeden Trade in einer Audit-Trail-konformen, idempotenten Struktur.
|
||||||
|
Ergebnis wird als `TRADE_RECORDED`-Event publiziert.
|
||||||
|
|
||||||
|
**Sicherheit & Audit:**
|
||||||
|
- **Keine KI/ML, keine Orderausführung, keine Risikoberechnung** — reines Dokumentieren
|
||||||
|
- **Append-only**: `trade_event_history` ist unveränderlich (nur INSERT, kein UPDATE/DELETE)
|
||||||
|
- **Herkunftskette wird validiert** (Signal → Ranking → Risk → Portfolio → Execution)
|
||||||
|
- Bei inkonsistenter Kette → Trade als `INCONSISTENT` markiert (FAIL-CLOSED), nicht stumm verworfen
|
||||||
|
- `TRADE_RECORDED` optional: ACK nach persistenter Speicherung
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
```
|
||||||
|
ORDER_* (market.execution, Modul-09)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
JournalConsumer (journal.input, Consumer-Fix von Anfang an)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Herkunfts-Validierung (FAIL-CLOSED)
|
||||||
|
│ Kette 05→06→07→08→09 nachvollziehbar?
|
||||||
|
│ provenance_consistent in metadata
|
||||||
|
▼
|
||||||
|
JournalService
|
||||||
|
│ erstes Event → Trade anlegen + TRADE_RECORDED
|
||||||
|
│ Folge-Event → append-only Event + Status-Update (bestehenden Trade)
|
||||||
|
▼
|
||||||
|
JournalStorage (idempotent, Unique-Index)
|
||||||
|
│ trade_journal + trade_event_history
|
||||||
|
▼
|
||||||
|
RabbitMQPublisher (market.journal)
|
||||||
|
TRADE_RECORDED (trade.recorded)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service-/Storage-Grenze (vereinheitlicht)
|
||||||
|
|
||||||
|
**Storage gibt IMMER `TradeJournal` zurück** — nie ein `dict`.
|
||||||
|
- Einzige Konvertierungsstelle `_row_to_trade` (DB-Zeile → Modell)
|
||||||
|
- `get_trade_by_execution()` → `Optional[TradeJournal]`
|
||||||
|
- Der Service arbeitet ausschließlich mit Modellen (kein dict/Model-Mix je Codepfad)
|
||||||
|
- Dadurch ist `trade_id` auf jedem Rückgabewert garantiert verfügbar
|
||||||
|
- (Fix gegen `'dict' object has no attribute 'trade_id'` → NACK/Requeue)
|
||||||
|
|
||||||
|
## Verarbeitung
|
||||||
|
|
||||||
|
- **Erstes Event (ORDER_SUBMITTED)** → Trade anlegen, `TRADE_RECORDED` publizieren
|
||||||
|
- **Folge-Event (ORDER_FILLED u.a.)** → bestehenden Trade als Status-Update
|
||||||
|
verarbeiten: append-only Event + Status-Update, KEIN neuer Trade,
|
||||||
|
**kein zweites TRADE_RECORDED**
|
||||||
|
- Bei FILLED: `entry_filled`/`filled_at`/Status aktualisiert (Trade öffnen/aktualisieren)
|
||||||
|
- Eingehende Events werden nicht doppelt verarbeitet (idempotent, Unique-Index)
|
||||||
|
|
||||||
|
## Idempotenz
|
||||||
|
|
||||||
|
- Unique-Index auf `(execution_id, event_id)` → jedes Event exakt einmal
|
||||||
|
- `provenance_consistent` in `metadata` (jsonb), nicht als Spalte
|
||||||
|
- `TRADE_RECORDED` nur beim ersten Event pro Trade
|
||||||
|
- Kein Doppel-Journal bei Re-Publish desselben Events
|
||||||
|
|
||||||
|
## DB-Tabellen
|
||||||
|
|
||||||
|
- `trade_journal`: trade_id, execution_id, source_portfolio_decision_id,
|
||||||
|
Herkunftsketten-IDs (signal/ranking/risk/portfolio), symbol, asset_class,
|
||||||
|
strategy, direction, timeframe, entry_requested, entry_filled, stop_loss,
|
||||||
|
target, quantity, risk_amount, execution_mode, broker_order_id, status,
|
||||||
|
provenance_consistent (in metadata), created_at, updated_at
|
||||||
|
- `trade_event_history`: execution_id, event_id, event_type, payload, created_at
|
||||||
|
(append-only, unveränderlich)
|
||||||
|
|
||||||
|
## RabbitMQ
|
||||||
|
|
||||||
|
- Exchange `market.journal` (topic, durable)
|
||||||
|
- Routing: `trade.recorded`
|
||||||
|
- Queue `journal.input` (durable), gebunden an `market.execution`/`order.*`
|
||||||
|
- Consumer-Fix von Anfang an: dauerhafte Verbindung,
|
||||||
|
`process_data_events(time_limit=1.0)` in innerer Schleife, KEIN queue_delete
|
||||||
|
bei normalen Reconnects, Backoff 1s→2s→4s→8s→16s→30s
|
||||||
|
- ACK erst nach persistenter Speicherung; bei Fehler NACK/Requeue
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- **39/39 Unit-Tests** (test_journal.py): Anlage, Status-Update, Idempotenz,
|
||||||
|
FAIL-CLOSED-Herkunftskette, append-only, exakt 1 TRADE_RECORDED, Reconnect
|
||||||
|
- **Gezielter Fix-Test 9/9** (test_fix_update.py): SUBMITTED→Journal,
|
||||||
|
FILLED→Update, Rückgabe ist TradeJournal (kein dict), `trade_id` vorhanden,
|
||||||
|
genau 1 Trade, append-only (2 Events), idempotent, kein Doppel-Journal
|
||||||
|
- **E2E 7/7 Checks** (Kette 03→10): Journal-Eintrag in DB, exakt 1 Trade,
|
||||||
|
Pflichtfelder + Herkunftskette konsistent, append-only Event-Historie
|
||||||
|
(SUBMITTED+FILLED), exakt 1 TRADE_RECORDED, Idempotenz nach Re-Publish
|
||||||
|
- **Reconnect-Test**: RabbitMQ gestoppt → Backoff 1→2→4→8→16s → wieder
|
||||||
|
verbunden, `journal.input` 1 Consumer, 0 Messages, kein Zombie
|
||||||
|
- **Log-Check**: 0 ERROR, 0 Traceback, ACK auf ORDER_SUBMITTED + ORDER_FILLED
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
- Container `Modul-10-Trade-Journal`, Port 55010 nur Docker-intern
|
||||||
|
(kein öffentlicher Host-Port)
|
||||||
|
- Health/Readiness 200, PG + RabbitMQ erreichbar, Consumer bereit
|
||||||
|
- Env: `EXECUTION_MODE=PAPER`, `TRADING_ENABLED=false`
|
||||||
82
modul-11-analytics.md
Normal file
82
modul-11-analytics.md
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
# Modul-11: Analytics
|
||||||
|
|
||||||
|
**Status: FREIGEGEBEN** · Container `Modul-11-Analytics` · Image `analytics-service:0.1.0`
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
Auswertung der Trade-Journal-Daten aus **Modul-10** (`trade_journal`) zu reproduzierbaren Performance-Kennzahlen. Deterministische Berechnung — gleiche Datenbasis → gleiche Ergebnisse. **V1 ohne KI/ML, ohne Orders, ohne automatische Strategieänderungen.**
|
||||||
|
|
||||||
|
## Architektur / Datenfluss
|
||||||
|
|
||||||
|
```
|
||||||
|
TRADE_RECORDED (market.journal / trade.recorded)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Modul-11-Analytics ── konsumiert über Queue `analytics.input`
|
||||||
|
│
|
||||||
|
├── liest read-only → trade_journal (Modul-10, PostgreSQL)
|
||||||
|
├── schreibt → analytics_snapshot, strategy_performance, daily_performance
|
||||||
|
└── publiziert (opt.)→ ANALYTICS_UPDATED (market.analytics / analytics.updated)
|
||||||
|
|
||||||
|
API (Docker-intern, Port 55011):
|
||||||
|
/health, /health/ready
|
||||||
|
/analytics/portfolio Gesamt-/Portfolio-Performance
|
||||||
|
/analytics/strategy je Strategie+Version
|
||||||
|
/analytics/regime je Market Regime
|
||||||
|
/analytics/period je Monat (ab optionalem from_date)
|
||||||
|
/analytics/rebuild (POST) deterministischer Rebuild aus DB
|
||||||
|
```
|
||||||
|
|
||||||
|
Primärquelle ist `TRADE_RECORDED`; zusätzlich liest Analytics direkt aus PostgreSQL (`trade_journal`), da für reproduzierbare Auswertungen ein vollständiger, deterministischer Rebuild aus der DB sinnvoller ist. `TRADE_RECORDED`-Events triggern eine Neuberechnung (Idempotenz: identisches Event → keine Doppelzählung).
|
||||||
|
|
||||||
|
## Datenquelle & Realisationslogik
|
||||||
|
- **Nur `status = 'CLOSED'` UND `realized_pnl IS NOT NULL`** fließen in realisierte Performance ein.
|
||||||
|
- **Offene Trades** (`OPEN`) werden separat ausgewiesen (`open_trade_count`) und zählen NICHT als realisierte Trades.
|
||||||
|
- Gruppierungsfelder aus `trade_journal`: `strategy`, `strategy_version`, `symbol`, `asset_class`, `direction` (LONG/SHORT), `regime`, `signal_score`.
|
||||||
|
- Zeitraum-Gruppierung nach `exit_time` (realisierter Trades), optional ab `from_date`.
|
||||||
|
- **Keine Lookahead-/Survivorship-Tricks**: nur tatsächlich geschlossene Trades, deterministische Equity-Kurve.
|
||||||
|
|
||||||
|
## Kennzahlen (V1)
|
||||||
|
- Anzahl Trades (realisiert + offen getrennt)
|
||||||
|
- Winrate / Lossrate
|
||||||
|
- durchschnittlicher Gewinn / Verlust
|
||||||
|
- durchschnittliches R (realized_r_multiple)
|
||||||
|
- Expectancy (Währung + R)
|
||||||
|
- Profit Factor (gross_profit / gross_loss)
|
||||||
|
- Netto-PnL
|
||||||
|
- Brutto-Gewinn / Brutto-Verlust
|
||||||
|
- Max Drawdown (aus Equity-Kurve der realisierten PnL)
|
||||||
|
- Durchschnittliche Haltedauer (entry_time→exit_time, Stunden)
|
||||||
|
- Bester / schlechtester Trade
|
||||||
|
|
||||||
|
## Eigene Tabellen (Migration `migrations/001_analytics.sql`)
|
||||||
|
- `analytics_snapshot` — stabiler Snapshot der Gesamt-Performance (jeder Rebuild = neue Zeile, chronologisch append).
|
||||||
|
- `strategy_performance` — Kennzahlen je Strategie+Version.
|
||||||
|
- `daily_performance` — Tageskennzahlen (Backtesting/Reporting).
|
||||||
|
|
||||||
|
Diese fassen KEINE bestehenden Tabellen an (read-only auf `trade_journal`).
|
||||||
|
|
||||||
|
## RabbitMQ
|
||||||
|
- **Queue** `analytics.input` (durable, topic `market.journal`, Routing `trade.recorded`), manuelles ACK, Reconnect mit Backoff (1→2→4→8→16s), kein Zombie.
|
||||||
|
- **Publiziert** optional `ANALYTICS_UPDATED` (topic `market.analytics`, Routing `analytics.updated`) — frische Verbindung je Publish (Bug-Fix-Pattern aus Modul-10), `conn.close()` nach jedem Publish.
|
||||||
|
|
||||||
|
## Verifikation (20.08.2026)
|
||||||
|
| Test | Ergebnis |
|
||||||
|
|---|---|
|
||||||
|
| 1–7 Pure Metriken (Gewinn/Verlust, Winrate, PF, Expectancy, Max DD, Gruppierung, offene≠realisierte) | **OK** |
|
||||||
|
| 8 Idempotenz (identisches Event → keine Doppelzählung) | **OK** |
|
||||||
|
| 9 Rebuild aus DB → identische Kennzahlen | **OK** |
|
||||||
|
| 10 RabbitMQ-Reconnect (Backoff 4→8→16s, erneuter Connect, 1 Consumer) | **OK** |
|
||||||
|
| E2E 03→11 (Seed realisierter Trades, Portfolio/Strategie/Regime, DB + Idempotenz) | **OK** |
|
||||||
|
|
||||||
|
- Health/Ready: `200` Port 55011 (PostgreSQL ✓, RabbitMQ ✓, Consumer ready ✓).
|
||||||
|
- Endzustand nach Freigabe: 2 echte OPEN-Trades im Journal, 0 realisierte → Snapshot zeigt 0/2.
|
||||||
|
|
||||||
|
## Betriebshinweise
|
||||||
|
- Kein öffentlicher Port (nur `expose: 55011`).
|
||||||
|
- `restart: unless-stopped`.
|
||||||
|
- Compose-Pfad VPS: `/opt/trading-modules/docker-compose.yml`.
|
||||||
|
- Forgejo-Doku: `nexo312/trading-system-docs`, Commit-ID siehe unten.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Geändert von: Rain Ocampo (Hermes)*
|
||||||
|
*Datum: 20.08.2026* — Modul-11-Analytics, Status FREIGABEGEBEN.
|
||||||
128
modul-12-backtesting.md
Normal file
128
modul-12-backtesting.md
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
# Modul-12: Backtesting
|
||||||
|
|
||||||
|
**Status: FREIGEGEBEN** · Container `Modul-12-Backtesting` · Image `backtesting:0.1.2` (V1 + V1.1 parallel)
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
Deterministische, reproduzierbare **historische Strategietests** auf echten
|
||||||
|
OHLCV-Daten aus **Modul-03** (PostgreSQL). Gleiche Strategie-Logik wie
|
||||||
|
**Modul-05** (`trend_pullback_v1`) und identische Regime-Engine wie **Modul-04** —
|
||||||
|
der Backtest entscheidet exakt so wie Live/Paper. **V1 ohne KI/ML, ohne Live-Orders,
|
||||||
|
ohne Broker-Ausführung.** Modular erweiterbar für weitere Strategien (Breakout, ORB,
|
||||||
|
Mean-Reversion) über gemeinsame `Strategy`-Schnittstelle + Registry.
|
||||||
|
|
||||||
|
## Architektur / Datenfluss
|
||||||
|
|
||||||
|
```
|
||||||
|
OHLCV (Modul-01 PostgreSQL, Tabelle ohlcv)
|
||||||
|
│ synchron, direkt nach Timestamp/Zeitraum (KEIN /history?limit)
|
||||||
|
▼
|
||||||
|
Modul-12-Backtesting ── POST /backtest
|
||||||
|
│
|
||||||
|
├── core/engine.py deterministische Backtest-Loop (kein Lookahead)
|
||||||
|
├── strategies/ trend_pullback_v1 (1:1 zu Modul-05)
|
||||||
|
├── regime/engine.py Regime-Engine (1:1 zu Modul-04)
|
||||||
|
├── storage/storage.py Persistenz backtest_run/trade/equity
|
||||||
|
└── api/main.py REST-API (intern, Port 55012)
|
||||||
|
|
||||||
|
API (Docker-intern, Port 55012):
|
||||||
|
/health, /health/ready
|
||||||
|
POST /backtest Backtest starten (synchron, returns Ergebnis)
|
||||||
|
GET /backtest/{run_id} Run-Metadaten
|
||||||
|
GET /backtest/{run_id}/status Status + run_hash/data_hash
|
||||||
|
GET /backtest/{run_id}/trades gespeicherte Trades
|
||||||
|
GET /backtest/{run_id}/equity Equity-Kurve
|
||||||
|
GET /backtest alle Runs
|
||||||
|
POST /backtest/{run_id}/rebuild (idempotenter Rebuild aus DB)
|
||||||
|
GET /strategies registrierte Strategien + Versionen
|
||||||
|
```
|
||||||
|
|
||||||
|
Kein RabbitMQ: Backtest ist ein synchroner, request/response-Aufruf. Port `55012`
|
||||||
|
ist **nur intern** (`expose:`), kein öffentliches Port-Mapping.
|
||||||
|
|
||||||
|
## Datenquelle
|
||||||
|
- OHLCV direkt aus PostgreSQL (`ohlcv`), Spalten `provider, symbol, timeframe, ts,
|
||||||
|
open, high, low, close, volume`.
|
||||||
|
- Abfrage **nach Timestamp/Zeitraum** (`start_date`..`end_date`), nicht via
|
||||||
|
`/history?limit=N` — vollständige, deterministische Datenbasis.
|
||||||
|
- Mindestanzahl Candles: `min_candles_required` (default 220), sonst Fehler.
|
||||||
|
|
||||||
|
## Bias-Schutz / Realismus (kein Lookahead)
|
||||||
|
- **Entry am OPEN der Folgewandle** nach dem Signal (nie zum bekannten Signal-Close).
|
||||||
|
- **Regime & Strategie** werden nur auf **abgeschlossenen** Candles (bis Candle i)
|
||||||
|
ausgewertet — keine zukünftige Information.
|
||||||
|
- **Intrabar Stop/Target konservativ**: Stop wird VOR Target geprüft (kein Lookahead
|
||||||
|
durch Intrabar-Reihenfolge). Falls beide in einer Candle getroffen, gewinnt Stop
|
||||||
|
(Worst-Case).
|
||||||
|
- **Gap-Handling**: Überspringt der Open den Stop/Target, Fill zum Gap-Open
|
||||||
|
(realistisch schlechter).
|
||||||
|
- **Gebühren/Spread/Slippage** werden auf Entry UND Exit angewendet (verschlechtern
|
||||||
|
den Fill).
|
||||||
|
- **R-Multiple** bezieht sich auf das **Geldrisiko** `qty * |entry − stop|` (nicht
|
||||||
|
Preisdifferenz) — `expectancy_r` daher korrekt (vorheriger Bug: 31431 → korrekt 2.0).
|
||||||
|
|
||||||
|
## Eingaben (POST /backtest)
|
||||||
|
- Symbol, Asset-Klasse, Provider, Timeframe, Start-/Enddatum
|
||||||
|
- Strategy + Version (`trend_pullback_v1`, default `1.0.0`)
|
||||||
|
- Initiales Kapital, Risk % (Risk-basierte Positionsgröße)
|
||||||
|
- Gebühren (fix + %), Spread (Preispunkte), Slippage (Preispunkte)
|
||||||
|
- Optionale Limits: `max_positions`, `max_qty`
|
||||||
|
|
||||||
|
## Eigene Tabellen (Migration `migrations/001_backtest.sql`)
|
||||||
|
- `backtest_run` — Run-Metadaten: run_id, strategy+version, symbol, timeframe,
|
||||||
|
status (COMPLETED/…), data_hash, run_hash, Parameter (JSON), Datenzeitraum.
|
||||||
|
- `backtest_trade` — einzelne Trades: direction (LONG/SHORT), qty, entry/exit-Preis,
|
||||||
|
entry/exit_ts, signal_ts, reason (target/stop/force_close), regime, gross_pnl,
|
||||||
|
fee, pnl, r (R-Multiple), strategy_version.
|
||||||
|
- `backtest_equity` — Equity-Kurve (Zeitstempel, Equity, offene Positionen).
|
||||||
|
|
||||||
|
Jede Tabelle FK auf `backtest_run(run_id) ON DELETE CASCADE`.
|
||||||
|
|
||||||
|
## Determinismus / Reproduzierbarkeit
|
||||||
|
- Eindeutige `run_id` + deterministischer `run_hash` + `data_hash`.
|
||||||
|
- `run_hash`: Hash über Strategy+Version+Parameter+Datenzeitraum+data_hash.
|
||||||
|
- `data_hash`: Hash über die geladenen OHLCV-Daten.
|
||||||
|
- **Gleiche Daten + gleiche Version + gleiche Parameter → identische Trades, Equity,
|
||||||
|
Kennzahlen und identischer run_hash** (E2E-verifiziert).
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
- `tests/test_backtest.py`: 12/12 grün (deterministisch, LONG/SHORT, fees, slippage,
|
||||||
|
lookahead, stop/target, gaps, unzureichende Daten, Strategy-Version/Parameter,
|
||||||
|
Rebuild-Identität).
|
||||||
|
- `tests/fixtures.py`: deterministische Fixture-Factory.
|
||||||
|
- `tests/m12_fixtures.py`: erzeugt E2E-Fixtures (BT_PULL LONG, BT_PULL_S SHORT) für
|
||||||
|
die VPS-ohlcv-Tabelle.
|
||||||
|
- `tests/determinism_check.py` / `determinism_diff.py`: E2E-Determinismus-Nachweis
|
||||||
|
(identische Trades/Equity/Kennzahlen/Hashes).
|
||||||
|
|
||||||
|
## E2E-Verifikation (VPS, 20.08.2026)
|
||||||
|
- **LONG** (BT_PULL, TREND_UP): 1 Trade, winrate 1.0, expectancy_r 1.94, net_pnl
|
||||||
|
196.59, max_drawdown 0.0001, r=1.94, reason=target.
|
||||||
|
- **SHORT** (BT_PULL_S, TREND_DOWN): 1 Trade, short_count 1, net_pnl 194.96.
|
||||||
|
- `signal_ts 15:00 → entry_ts 16:00` (Einstieg am Open der Folgewandle — Lookahead
|
||||||
|
praktisch verifiziert).
|
||||||
|
- run_hash + data_hash gesetzt (Beispiel: `bc6e2553…` / `d76a7549…`).
|
||||||
|
- **Determinismus**: zwei identische Backtests → identischer run_hash, data_hash,
|
||||||
|
Metrics, Trades (normalisiert), Equity-Curve.
|
||||||
|
- **Stop/Target/Gap**: Engine-`_evaluate_exit` geprüft — Gap-Down unter Stop → Fill
|
||||||
|
zum Gap-Open; Target bei high≥target; Stop vor Target (konservativ); SHORT Gap-Up
|
||||||
|
→ Fill zum Gap-Open.
|
||||||
|
- Health `/health` 200, `/health/ready` 200; keine öffentlichen Ports.
|
||||||
|
|
||||||
|
## Compose / Betrieb
|
||||||
|
- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL` (Modul-01), Port 55012
|
||||||
|
nur intern (`expose`), `restart: unless-stopped`. Kein öffentliches Port-Mapping.
|
||||||
|
|
||||||
|
## Nächste Module
|
||||||
|
- **Modul-13: Optimization** — Image `optimization:0.1.0`, E2E-verifiziert (12/12 Punkte), **FREIGEGEBEN** (20.08.2026). Details: `modul-13-optimization.md`.
|
||||||
|
- **V1.1-Strategie**: `trend_pullback_v1.1` (Image `backtesting:0.1.2`) — Spezifikation und Parameter: `trend-pullback-v1.1-spec.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
## Geändert
|
||||||
|
```
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: Modul-12-Backtesting als FREIGEGEBEN markiert (deployt + E2E verifiziert: deterministisch, kein Lookahead, Stop/Target/Gap, LONG/SHORT, run_hash+data_hash).
|
||||||
|
```
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: Modul-12-Doku aktualisiert — Image auf backtesting:0.1.2 (V1.1), V1-Kompatmodus bitgenau, Referenz auf V1.1-Spec und Modul-13.
|
||||||
129
modul-13-optimization.md
Normal file
129
modul-13-optimization.md
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
# Modul-13: Optimization
|
||||||
|
|
||||||
|
**Status: FREIGEGEBEN ✅** · Container `Modul-13-Optimization` · Image `optimization:0.1.0`
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
Deterministische, reproduzierbare **Parameteroptimierung** für die V1.1-Strategie
|
||||||
|
(`trend_pullback_v1.1`, Modul-12). Findet robuste Parameter im erlaubten Suchraum
|
||||||
|
**ohne KI/LLM und ohne Live-Orders** — Ergebnis ist nur **CANDIDATE/RECOMMENDATION**,
|
||||||
|
es gibt keine automatische Parameterübernahme in Modul-05.
|
||||||
|
|
||||||
|
## Kernprinzipien
|
||||||
|
- **Deterministisch**: gleiche Eingaben → gleiche Kandidaten, Reihenfolge, run_hash (kein Zufall außer explizit via Seed).
|
||||||
|
- **Kein Lookahead**: IS/OOS strikt zeitlich getrennt; **OOS wird nie zur Selektion verwendet**.
|
||||||
|
- **IS/OOS strikt**: `is_ratio` (z.B. 0.7) teilt die Daten; OOS ausschließlich zur Validierung.
|
||||||
|
- **Robuste Bereiche > Einzelmax**: best = Kandidat mit bester kombinierter Score (nicht max net_pnl).
|
||||||
|
- **Reject-/Overfit-Logik**: zu wenige Trades, DD-Limit, IS/OOS-Degradation → Kandidat wird `rejected`.
|
||||||
|
- **Parametergrenzen zwingend**: Suchraum begrenzt auf `pullback_band_atr`, `trend_regime_window`, `rr_multiplier`.
|
||||||
|
- **Score-Gewichte**: Expectancy/R 0.30, PF 0.25, Max-Drawdown 0.20, Trades 0.10, Stabilität 0.15.
|
||||||
|
- **Synchron**: Request/Response über API (kein RabbitMQ).
|
||||||
|
|
||||||
|
## Architektur / Datenfluss
|
||||||
|
```
|
||||||
|
OHLCV (Modul-01 PostgreSQL, Tabelle ohlcv)
|
||||||
|
│ synchron, direkt nach Timestamp/Zeitraum
|
||||||
|
▼
|
||||||
|
Modul-13-Optimization ── POST /optimization
|
||||||
|
│
|
||||||
|
├── core/split.py IS/OOS + Walk-Forward-Folds (kein Leakage)
|
||||||
|
├── core/optimizer.py Grid/Random-Suche, ScoreEngine, WF, Reject
|
||||||
|
├── core/scoring.py Multi-Metrik-Score (Gewichte siehe oben)
|
||||||
|
├── storage/storage.py Persistenz optimization_run/candidate/walk_forward
|
||||||
|
├── backtest_client.py synchroner Aufruf Modul-12 (POST /backtest)
|
||||||
|
└── api/main.py REST-API (intern, Port 55013)
|
||||||
|
```
|
||||||
|
|
||||||
|
API (Docker-intern, Port 55013, kein öffentliches Port-Mapping):
|
||||||
|
```
|
||||||
|
GET /health, /health/ready
|
||||||
|
POST /optimization Start (Grid/Random), synchron
|
||||||
|
GET /optimization Liste aller Runs
|
||||||
|
GET /optimization/{id} Run-Metadaten + Status
|
||||||
|
GET /optimization/{id}/candidates Kandidaten + Multi-Metrik-Ranking
|
||||||
|
GET /optimization/{id}/best bester ROBUSTER Kandidat (RECOMMENDATION/NO_RECOMMENDATION)
|
||||||
|
GET /optimization/{id}/walk-forward Walk-Forward-Folds
|
||||||
|
```
|
||||||
|
|
||||||
|
## Suchraum (V1.1 — NUR 3 optimierbare Parameter)
|
||||||
|
| Parameter | Suchraum (E2E) | Zweck |
|
||||||
|
|-----------|----------------|-------|
|
||||||
|
| `pullback_band_atr` | {0.2, 0.5, 0.8, 1.0} | ATR-Band um EMA20 (Pullback-Timing) |
|
||||||
|
| `trend_regime_window` | {60, 200, 300} | Fenster für strategie-internes Trend-Regime |
|
||||||
|
| `rr_multiplier` | {1.0, 2.0, 3.0} | Target = Entry + rr×Risiko |
|
||||||
|
|
||||||
|
Fix (nicht optimiert): `atr_period=14`, `regime_vol_override=false`, `min_risk_reward=1.0`,
|
||||||
|
ADX-/EMA-Perioden (14/20/30), `adx_trend_threshold=20`.
|
||||||
|
|
||||||
|
## Profile
|
||||||
|
| Profil | min_trades_is | min_trades_oos | max_drawdown_limit | max_oos_degradation | Zweck |
|
||||||
|
|--------|---------------|----------------|--------------------|--------------------|-------|
|
||||||
|
| **produktion** (Default) | 3 | 1 | 0.30 | 0.5 | Anti-Overfit, Produktion |
|
||||||
|
| **test** (`profile=test`) | 1 | 1 | — | — | nur Mindest-Trade-Schwellen gesenkt für technischen E2E; sonst identisch |
|
||||||
|
|
||||||
|
Das TEST-PROFIL senkt **ausschließlich** die Mindest-Trade-Schwellen (damit ein
|
||||||
|
Sweep über die kalibrierte synthetische Fixture genügend Kandidaten akzeptiert).
|
||||||
|
Produktive Anti-Overfit-Defaults bleiben unverändert. `profile` wird per API-Feld
|
||||||
|
übergeben; explizite Request-Werte (`min_trades_is/oos`) haben Vorrang.
|
||||||
|
|
||||||
|
## Eigene Tabellen (Migration)
|
||||||
|
- `optimization_run` — Run-Metadaten: optimization_id, strategy+version, symbol, timeframe,
|
||||||
|
optimizer_method, optimizer_version, param_space, seed, status, result (JSON), created/completed_at.
|
||||||
|
- `optimization_candidate` — Kandidaten: rank, params, backtest_run_id_is/oos, is_metrics,
|
||||||
|
oos_metrics, score, score_components, rejected, reject_reasons, flags, stability, robustness.
|
||||||
|
- `walk_forward_result` — WF-Folds: fold, params, is_start/end, oos_start/end, is/oos_metrics,
|
||||||
|
score, backtest_run_id_is/oos.
|
||||||
|
|
||||||
|
## Reject-/Overfit- und Recommendation-Verhalten
|
||||||
|
- Kandidat wird `rejected` wenn: zu wenige Trades (min_trades_is/oos), Drawdown über Limit,
|
||||||
|
OOS-Degradation zu hoch (Overfit), fachlich unbrauchbare Metriken (z.B. PF<=0/kein Trades).
|
||||||
|
- `/best` liefert:
|
||||||
|
- `recommendation=RECOMMENDATION` + `best_candidate` wenn ein fachlich brauchbarer robuster Bester existiert.
|
||||||
|
- `recommendation=NO_RECOMMENDATION` (statt leerem `best_candidate`), wenn das Kandidatenfeld
|
||||||
|
fachlich unbrauchbar ist — eine produktive Empfehlung wird nie aus unbrauchbaren Daten erzeugt.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
- `tests/test_optimization.py`: 12/12 grün (Grid deterministisch, Random-Seed, IS/OOS-Leak,
|
||||||
|
Walk-Forward korrekt + JSON-serialisierbar, Multi-Metrik-Ranking, Reject/Overfit, Robustheit,
|
||||||
|
Suchraum=3 Params, Reproduzierbarkeit, NO_RECOMMENDATION, TEST-PROFIL).
|
||||||
|
|
||||||
|
## E2E-Verifikation (VPS, 20.08.2026)
|
||||||
|
- **Suchraum-Differenzierung** (kalibrierte Fixture `M13_E2E`, 1314 Bars, alle 3 Params unterscheidbar):
|
||||||
|
- `pullback_band_atr`: 0.2→8 Trades, 0.5→9–10, 0.8/1.0→1 Trade.
|
||||||
|
- `trend_regime_window`: bei pba=0.5: 60→9 Trades vs 200/300→10 Trades.
|
||||||
|
- `rr_multiplier`: 1.0→8–10 Trades (net +828…+1046), 2.0/3.0→1 Trade.
|
||||||
|
- **12/12 E2E-Punkte über API bestätigt** (e2e_full.py):
|
||||||
|
Grid deterministisch, Random-Seed identisch, IS/OOS-Leak ausgeschlossen, WF-Folds zeitlich
|
||||||
|
korrekt + DB-persistiert (3 Folds), Multi-Metrik-Ranking, Reject-Logik (36 Kandidaten,
|
||||||
|
30 rejected / 6 accepted), best=robust + RECOMMENDATION, DB-Run/Candidates vollständig,
|
||||||
|
API list_runs, Reproduzierbarkeit (status COMPLETED), NO_RECOMMENDATION bei nur-1-Trade
|
||||||
|
Konfiguration (produktion-Profil).
|
||||||
|
- **Bugfix C**: Walk-Forward-Fold-Ergebnisse enthalten `datetime`-Timestamps → beim
|
||||||
|
`json.dumps` in `complete_run` nicht serialisierbar (422). Fix: ISO-String-Kopie im
|
||||||
|
Run-Result, datetime bleibt für `storage.insert_wf`. Verifiziert (3 WF-Folds in DB).
|
||||||
|
- Health `/health/ready` → `{"status":"ready","db":"ok"}` (M13 und M12). Keine Tracebacks.
|
||||||
|
- Port 55013 nur intern (`expose`), kein Host-Port-Binding (verifiziert).
|
||||||
|
|
||||||
|
## Freigabe
|
||||||
|
- **20.08.2026: Modul-13-Optimization-Service vom Nutzer FREIGEGEBEN ✅**
|
||||||
|
(nach vollständiger E2E-Verifikation: 12/12 API-Punkte, Suchraum-Differenzierung für alle
|
||||||
|
3 Params, TEST-PROFIL, NO_RECOMMENDATION, Bugfixes A/B/C deployt, Tests 12/12 grün).
|
||||||
|
- Keine weiteren technischen Änderungen an Modul-13.
|
||||||
|
|
||||||
|
## Compose / Betrieb
|
||||||
|
- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL` (Modul-01), Backtest via
|
||||||
|
`Modul-12-Backtesting` (intern). Port 55013 nur intern (`expose`), `restart: unless-stopped`.
|
||||||
|
|
||||||
|
## Nächste Module
|
||||||
|
- **Modul-14 ff.** — noch Platzhalter (alpine), NICHT gestartet.
|
||||||
|
|
||||||
|
---
|
||||||
|
## Geändert
|
||||||
|
```
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: Modul-13-Optimization dokumentiert — E2E-verifiziert (12/12 Punkte), Suchraum-Diffusion für alle 3 Params, TEST-PROFIL, NO_RECOMMENDATION, Bugfix C (WF-JSON-datetime).
|
||||||
|
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: Modul-13 vom Nutzer FREIGEGEBEN — Status auf FREIGEGEBEN gesetzt, Freigabe-Sektion ergänzt. Keine technischen Änderungen.
|
||||||
|
```
|
||||||
142
modul-14-notification.md
Normal file
142
modul-14-notification.md
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
# Modul-14: Notification-Service
|
||||||
|
|
||||||
|
**Status: FREIGEGEBEN ✅** · Container `Modul-14-Notification` · Image `notification:0.1.0`
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
Zentrale **Benachrichtigungen für Trading- und Systemereignisse**. Konsumiert relevante
|
||||||
|
Events aus den RabbitMQ-Exchanges der Module 05–10 und liefert kompakte, strukturierte
|
||||||
|
Meldungen — **V1 primär über Telegram** (`TelegramProvider`), provider-neutral angelegt.
|
||||||
|
|
||||||
|
**Restriktionen (Nutzer-Vorgabe):**
|
||||||
|
- **keine KI/ML**, **keine Trading-Entscheidungen**, **keine Orders**.
|
||||||
|
- **FAIL-SAFE:** Ein Notification-Ausfall darf die Trading-Pipeline **NIEMALS blockieren**.
|
||||||
|
|
||||||
|
## Architektur / Datenfluss
|
||||||
|
```
|
||||||
|
RabbitMQ (market.signals/.rankings/.risk/.portfolio/.execution/.journal)
|
||||||
|
│ NotificationConsumer (notification.input)
|
||||||
|
▼
|
||||||
|
Modul-14-Notification ── NotificationService ──> RulesEngine + DedupGuard
|
||||||
|
│ │ Spam-Schutz
|
||||||
|
├── providers/ NotificationProvider-ABC → Telegram / Fake
|
||||||
|
├── rules/ RulesEngine (Regeln) + DedupGuard (Dedup/Cooldown/Rate-Limit)
|
||||||
|
├── storage/ Persistenz notification_log (PostgreSQL, Modul-01)
|
||||||
|
├── core/ formatter (kompakte Nachricht) + service (Retry/Backoff)
|
||||||
|
└── api/main.py REST-API (intern, Port 55014)
|
||||||
|
|
||||||
|
Event → RabbitMQ → Modul-14 → notification_log → Provider (Telegram) → Chat
|
||||||
|
```
|
||||||
|
|
||||||
|
## API (Docker-intern, Port 55014, kein öffentliches Port-Mapping)
|
||||||
|
```
|
||||||
|
GET /health liveness
|
||||||
|
GET /health/ready readiness (postgresql, consumer_ready, provider_configured)
|
||||||
|
GET /notifications/recent letzte Notifications
|
||||||
|
GET /notifications/{id} einzelne Notification
|
||||||
|
POST /notifications/test Test-Sendung (nur konfigurierte Destinationen)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konsumierte Events & Routing-Keys
|
||||||
|
| Event-Typ | Exchange | Routing-Key | Default-Severity | Default aktiv |
|
||||||
|
|-----------|----------|-------------|------------------|---------------|
|
||||||
|
| `SIGNAL_DETECTED` | `market.signals` | `strategy.signal.detected` | INFO | ❌ (deaktiviert) |
|
||||||
|
| `SIGNAL_RANKED` | `market.rankings` | `signal.ranked` | INFO | ❌ (deaktiviert) |
|
||||||
|
| `RISK_APPROVED` | `market.risk` | `risk.approved` | INFO | ❌ (deaktiviert) |
|
||||||
|
| `RISK_REJECTED` | `market.risk` | `risk.rejected` | WARNING | ❌ (deaktiviert) |
|
||||||
|
| `TRADE_APPROVED` | `market.portfolio` | `trade.approved` | INFO | ✅ |
|
||||||
|
| `PORTFOLIO_REJECTED` | `market.portfolio` | `portfolio.rejected` | WARNING | ✅ |
|
||||||
|
| `ORDER_SUBMITTED` | `market.execution` | `order.submitted` | INFO | ❌ (deaktiviert) |
|
||||||
|
| `ORDER_FILLED` | `market.execution` | `order.filled` | INFO | ✅ |
|
||||||
|
| `ORDER_REJECTED` | `market.execution` | `order.rejected` | CRITICAL | ✅ |
|
||||||
|
| `ORDER_FAILED` | `market.execution` | `order.failed` | CRITICAL | ✅ |
|
||||||
|
| `TRADE_RECORDED` | `market.journal` | `trade.recorded` | INFO | ✅ |
|
||||||
|
|
||||||
|
Die Regeln sind **pro Event-Typ konfigurierbar** (aktiv/inaktiv, Severity, Provider,
|
||||||
|
Destination, Cooldown, Rate-Limit). **Unbekannte Event-Typen werden sicher ignoriert**
|
||||||
|
(kein Crash). V1 sind die rauschigen Events (SIGNAL_*, RISK_*, ORDER_SUBMITTED)
|
||||||
|
standardmäßig deaktiviert.
|
||||||
|
|
||||||
|
## Severity-Hierarchie
|
||||||
|
```
|
||||||
|
INFO < WARNING < CRITICAL
|
||||||
|
```
|
||||||
|
Regel-Severity überschreibt den Event-Standard. V1: alle über Telegram.
|
||||||
|
|
||||||
|
## Spam-Schutz (DedupGuard)
|
||||||
|
Mehrstufig, thread-sicher (Lock):
|
||||||
|
1. **Idempotenz** — gleiche `event_id`/`source_event_id` erzeugt nie eine zweite Notification (DB-unique über `source_event_id` + `event_processed`-Check).
|
||||||
|
2. **Dedup** — identische Meldungen (gleicher Text/Event-Typ) in kurzem Fenster werden zusammengefasst.
|
||||||
|
3. **Cooldown** — pro Event-Typ ein Mindestabstand (`cooldown_seconds`, Default 60s).
|
||||||
|
4. **Rate-Limit** — maximale Notifications pro Minute (`rate_limit_per_minute`, Default 30).
|
||||||
|
5. **Burst-Schutz** — Ansturm vieler Events wird aufs Limit begrenzt.
|
||||||
|
|
||||||
|
## Retry / Dead (FAIL-SAFE)
|
||||||
|
- Provider nicht erreichbar → `RETRY_PENDING` mit **begrenzten Retries** (exponentielles Backoff).
|
||||||
|
- `max_attempts` (Default 5) erreicht → **DEAD** (keine Endlosschleife).
|
||||||
|
- **Provider nicht konfiguriert** (fehlender Token/Chat-ID) → **sofort DEAD** mit
|
||||||
|
`error_code=NOT_CONFIGURED` (keine sinnlosen Retries).
|
||||||
|
- Nach Provider-Recovery werden **neue Events wieder normal gesendet** (Retry betrifft nur die jeweilige Notification).
|
||||||
|
|
||||||
|
## Eigene Tabelle (Migration)
|
||||||
|
`notification_log`:
|
||||||
|
`notification_id` (PK), `source_event_id`, `event_type`, `severity`, `provider`,
|
||||||
|
`destination`, `status` (`SENT`/`FAILED`/`RETRY_PENDING`/`DEAD`), `attempts`,
|
||||||
|
`created_at`, `sent_at`, `error_code`, `error_message`.
|
||||||
|
|
||||||
|
## Sicherheit
|
||||||
|
- **KEINE Secrets in DB/Logs/Doku/Commits.** Telegram-Bot-Token/Chat-ID ausschließlich
|
||||||
|
über Environment (Compose `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`), nie hart kodiert.
|
||||||
|
- `/notifications/test` sendet **nur an konfigurierte Destinationen**.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
- `tests/test_notification.py`: **14/14 grün** (Event→Notification, Idempotenz, Dedup,
|
||||||
|
Cooldown, Rate-Limit, Telegram-Ausfall blockiert nicht, Retry+Backoff, Max→DEAD,
|
||||||
|
keine Secrets, unbekannter Typ ignoriert, Severity/Regeln, FakeProvider, Reconnect).
|
||||||
|
|
||||||
|
## E2E-Verifikation (VPS, 20.08.2026) — alle 7 Punkte grün
|
||||||
|
1. **Fake-Telegram-E2E** (Modul-14 auf `http://fake-telegram:55000`, Test-Token/Chat-ID
|
||||||
|
nur als ENV): echtes `TRADE_RECORDED` publiziert → **Event → RabbitMQ → Modul-14 →
|
||||||
|
notification_log → Fake-Telegram** komplett durchlaufen. DB: `status=SENT`,
|
||||||
|
`sent_at` gesetzt, `attempts=0`, **genau 1 Zustellung, kein Duplikat**. Fake-Telegram
|
||||||
|
RAW: `{"chat_id": "E2E_TEST_CHAT_114", "text": "TRADE RECORDED | NVDA | LONG | OPEN"}`.
|
||||||
|
2. **NO_CONFIG-Fix**: Container ohne Token → Event → `status=DEAD, attempts=1,
|
||||||
|
error_code=NOT_CONFIGURED` (sofortiges DEAD, keine 5 sinnlosen Retries).
|
||||||
|
3. **Fehlerfall-E2E**: Fake-Telegram gestoppt → Notification `RETRY_PENDING` mit
|
||||||
|
Backoff (attempts 2→4, `NETWORK_ERROR`), **Trading-Pipeline unbeeinflusst**,
|
||||||
|
Max-Retries (`attempts=5`) → `DEAD`. Nach Fake-Telegram-Recovery → neues Event
|
||||||
|
wieder `SENT`.
|
||||||
|
4. **RabbitMQ-Reconnect**: kontrollierter Restart → Modul-14 reconnectet automatisch
|
||||||
|
(Backoff 4s→8s→16s, „Consumer verbunden"), **genau 1 Consumer** an `notification.input`,
|
||||||
|
keine Zombies, keine verlorenen/duplizierten Notifications (nach Reconnect: 1 Zustellung, 0 Duplikate).
|
||||||
|
5. **Spam-/Sicherheitschecks** (praktisch): unbekannter Typ `UNKNOWN_EVENT_XYZ` → ignoriert;
|
||||||
|
gleiche `event_id` 2× → nur 1 Notification (Idempotenz); gleicher Event-Typ 2×
|
||||||
|
→ Cooldown greift; deaktivierter `SIGNAL_DETECTED` → keine Notification. **Keine
|
||||||
|
Tokens/Chat-IDs in Logs/API/Doku/DB** (0 Token-Leaks).
|
||||||
|
6. **Health/Readiness**: `/health` + `/health/ready` → 200
|
||||||
|
(`postgresql=true, consumer_ready=true, provider_configured=…`). Keine
|
||||||
|
Applikations-Tracebacks im Normalbetrieb (nur erwartete Reconnect-Logs beim Test).
|
||||||
|
Port 55014 nur intern (`expose`, kein Host-Port).
|
||||||
|
7. **Forgejo-Doku + Commit-ID** (siehe Freigabe unten), temporäre Test-Credentials
|
||||||
|
entfernt, Remote token-/key-frei.
|
||||||
|
|
||||||
|
## Freigabe
|
||||||
|
- **20.08.2026: Modul-14-Notification-Service vom Nutzer FREIGEGEBEN ✅**
|
||||||
|
(nach vollständiger E2E-Verifikation der Punkte 1–7, alle grün).
|
||||||
|
- Keine weiteren technischen Änderungen an Modul-14.
|
||||||
|
|
||||||
|
## Compose / Betrieb
|
||||||
|
- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL`, RabbitMQ `Modul-02-RabbitMQ`.
|
||||||
|
- Port 55014 nur intern (`expose`), `restart: unless-stopped`, non-root (uid 1001).
|
||||||
|
- Token/Chat-ID via ENV (`TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`, `TELEGRAM_API_BASE`).
|
||||||
|
|
||||||
|
## Nächste Module
|
||||||
|
- **Modul-15 ff.** — noch Platzhalter (alpine), NICHT gestartet.
|
||||||
|
|
||||||
|
---
|
||||||
|
## Geändert
|
||||||
|
```
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: Modul-14-Notification-Service dokumentiert — E2E-verifiziert (Punkte 1–7 grün),
|
||||||
|
API/Events/Severity/Retry-Dead/Spam-Schutz/Fake-E2E dokumentiert, Status FREIGEGEBEN.
|
||||||
|
```
|
||||||
331
modul-15-m09-anbindung-design.md
Normal file
331
modul-15-m09-anbindung-design.md
Normal file
|
|
@ -0,0 +1,331 @@
|
||||||
|
# Design-Vorschlag: Modul-15 → Modul-09 Anbindung (Safety-/Trading-Control)
|
||||||
|
|
||||||
|
**Status: NUR DESIGN-VORSCHLAG — keine Implementierung.**
|
||||||
|
**Datum:** 20.08.2026 · **Autor:** Rain Ocampo (Hermes)
|
||||||
|
**Gilt für:** Modul-09-Execution-Service (FREIGEGEBEN, Order-Pfad) ↔ Modul-15-Monitoring-Control (IMPLEMENTIERT + E2E-verifiziert, **noch nicht freigegeben**)
|
||||||
|
|
||||||
|
> **Scope-Regeln (verbindlich):**
|
||||||
|
> - M09 wird **nicht** verändert, bis dieser Vorschlag vom Nutzer freigegeben wurde.
|
||||||
|
> - M15 bleibt **nicht freigegeben** bis Nutzer-Freigabe.
|
||||||
|
> - M16 wird **nicht** begonnen.
|
||||||
|
> - Dieser Vorschlag ist **Architektur/Pseudoflow**, kein Code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Ziel
|
||||||
|
|
||||||
|
**Modul-15** wird die **autoritative Safety-/Trading-Control-Instanz** für den Order-Durchstich.
|
||||||
|
**Modul-09** darf neue Orders **nur senden, wenn M15 dies eindeutig erlaubt**.
|
||||||
|
|
||||||
|
Kernauftrag: kein OPEN/INCREASE, solange M15 nicht *eindeutig* ENABLED bestätigt. Gleichzeitig darf ein HALT **niemals** Risikoreduktion / Position-Schließen verhindern (Exit-Pfade bleiben immer frei).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Ist-Zustand in M09 (Kontext für den Vorschlag)
|
||||||
|
|
||||||
|
Der Order-Pfad in `app/core/service.py::_on_event` ist heute:
|
||||||
|
|
||||||
|
```
|
||||||
|
TRADE_APPROVED (Modul-08)
|
||||||
|
→ 1) Idempotenz (already_processed)
|
||||||
|
→ 2) Harte Validierung validate_trade_approved (FAIL-CLOSED)
|
||||||
|
→ 3) Broker-Bereitschaft validate_broker_ready (FAIL-CLOSED)
|
||||||
|
→ 4) Order atomar in DB anlegen (Advisory Lock + Transaktion, PENDING)
|
||||||
|
→ 5) _submit_and_track → BrokerAdapter.submit_order
|
||||||
|
```
|
||||||
|
|
||||||
|
**Heutiger Kill-Switch:** rein konfigurativ `TRADING_ENABLED=false` (Default, sicher für LIVE).
|
||||||
|
Es existiert **keine** externe Echtzeit-Control-Schnittstelle im Order-Pfad.
|
||||||
|
|
||||||
|
**Einbau-Punkt (Vorschlag):** zwischen Schritt 4 (Order angelegt) und Schritt 5 (Broker-Submit)
|
||||||
|
— d. h. **unmittelbar vor `submit_order`**. Alternativ als zusätzlicher Check in Schritt 2.
|
||||||
|
Empfehlung: als eigener Schritt zwischen 4 und 5, weil dort die Order bereits idempotent in der DB
|
||||||
|
liegt und wir den Final-Check „direkt vor dem Senden" platzieren (Race-Window minimal).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Trading-States & Regelwerk
|
||||||
|
|
||||||
|
### 3.1 Globale Trading-States (autoritativ: M15)
|
||||||
|
|
||||||
|
| State | Bedeutung | neue OPEN/INCREASE | REDUCE | CLOSE/CANCEL |
|
||||||
|
|-------|-----------|--------------------|--------|--------------|
|
||||||
|
| `ENABLED` | Alle krit. Dienste gesund, aktuelle Market-Data, Trading erlaubt | **ERLAUBT** | erlaubt | erlaubt |
|
||||||
|
| `PAUSED` | Nicht-krit. Störung (Analytics/Notification), Risiko-umfeld unklar | **VERBOTEN** | **ERLAUBT** | **ERLAUBT** |
|
||||||
|
| `HALTED` | Krit. Ausfall (PG/RMQ/Execution/Market-Data/UNKNOWN) | **VERBOTEN** | **ERLAUBT** | **ERLAUBT** |
|
||||||
|
| `UNKNOWN` / M15 nicht erreichbar / Status veraltet | Unklarheit | **VERBOTEN (FAIL-CLOSED)** | **ERLAUBT** (konservativ) | **ERLAUBT** |
|
||||||
|
|
||||||
|
**Kernregel:** Nur `ENABLED` erlaubt neue Positionen (OPEN) oder Aufstockung (INCREASE).
|
||||||
|
**Alles andere** (PAUSED/HALTED/UNKNOWN/nicht erreichbar/veraltet) blockiert OPEN/INCREASE.
|
||||||
|
**REDUCE/CLOSE** (Risikoreduktion/Position schließen) sind **immer** erlaubt — unabhängig vom State.
|
||||||
|
|
||||||
|
### 3.2 Order-Kategorien (Einordnung)
|
||||||
|
|
||||||
|
| Aktion | Bedeutung | Regel bei PAUSED/HALTED/UNKNOWN |
|
||||||
|
|---|---|---|
|
||||||
|
| `OPEN` | Neue Position eröffnen | **BLOCKIEREN** |
|
||||||
|
| `INCREASE` | Bestehende Position aufstocken | **BLOCKIEREN** (Neue-Position-Logik, Risiko wächst) |
|
||||||
|
| `REDUCE` | Position verkleinern | **ERLAUBT** (Risiko sinkt) |
|
||||||
|
| `CLOSE` | Position schließen | **ERLAUBT** (Risiko eliminieren) |
|
||||||
|
| `CANCEL` | Offene/geplante Order stornieren | **ERLAUBT** (kein neues Risiko) |
|
||||||
|
|
||||||
|
> Begründung INCREASE blockiert: Aufstocken fügt neues Marktrisiko hinzu — bei HALT/Unklarheit
|
||||||
|
> unerlaubt. REDUCE/CLOSE/CANCEL sind per Definition risikoreduzierend → immer frei.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Vergleich der Anbindungs-Varianten
|
||||||
|
|
||||||
|
### Variante A — synchroner HTTP-Check vor jeder Order
|
||||||
|
|
||||||
|
M09 ruft vor jedem Submit `GET /control/trading-state` auf M15 (Docker-intern) auf.
|
||||||
|
|
||||||
|
| Kriterium | Bewertung |
|
||||||
|
|---|---|
|
||||||
|
| **Sicherheit** | Sehr hoch — liest immer den frischesten autoritativen Zustand. FAIL-CLOSED bei Timeout. |
|
||||||
|
| **Latenz** | Pro Order +1 Round-Trip (ms-Bereich, Docker-Netz). Bei vielen Orders spürbar, aber Orders sind selten. |
|
||||||
|
| **Ausfallsicherheit** | M15-down → HTTP-Time out → FAIL-CLOSED (blockieren). Sicher, aber **M15 wird Single Point of Failure für OPEN**. |
|
||||||
|
| **Race Conditions** | M15 könnte direkt nach Antwort auf HALTED wechseln → Race trotz Check (siehe §7). Final-Check nur minimal schmaler. |
|
||||||
|
| **Single Point of Failure** | **Ja** — M15 ist für OPEN/INCREASE zwingend. Bei M15-down wird der Execution-Service sonst blind blockiert. |
|
||||||
|
| **Bei RabbitMQ-Ausfall** | HTTP bleibt funktionsfähig (separater Kanal) → Check ok; aber M15 meldet RABBITMQ_UNREACHABLE→HALTED → korrekt blockiert. |
|
||||||
|
| **Bei M15-Ausfall** | Timeout → FAIL-CLOSED → OPEN/INCREASE blockiert (gewünscht). REDUCE/CLOSE müssen **ausgenommen** werden, sonst kann Position nicht geschlossen werden. |
|
||||||
|
| **Komplexität** | Gering — ein HTTP-Call, kein Cache, kein Subscription. |
|
||||||
|
| **Nachvollziehbarkeit/Audit** | M15 protokolliert jede Anfrage; M09 loggt Check + Ergebnis. Gut, aber hochfrequent (jede Order). |
|
||||||
|
|
||||||
|
### Variante B — Event-basierter Status-Cache (publish/subscribe)
|
||||||
|
|
||||||
|
M15 publiziert Statusänderungen auf `market.control`; M09 subscribt und hält ein lokales State-Modell.
|
||||||
|
|
||||||
|
| Kriterium | Bewertung |
|
||||||
|
|---|---|
|
||||||
|
| **Sicherheit** | Mittel — Status nur so frisch wie das letzte Event + Cache-TTL; **riskant ohne TTL/Herzschlag**. |
|
||||||
|
| **Latenz** | Sehr gering — kein HTTP im kritischen Pfad, Cache-Lookp. |
|
||||||
|
| **Ausfallsicherheit** | Schlecht — RabbitMQ-Ausfall ⇒ keine Events mehr; Cache wird **stale**. Ohne TTL würde M09 mit veraltetem ENABLED weiter traden. **Gefahr.** |
|
||||||
|
| **Race Conditions** | Stale-Cache ist die größte Race-Quelle. Event-Reihenfolge/Lag. |
|
||||||
|
| **Single Point of Failure** | RabbitMQ als Event-Bus ist der SPOF für den Status-Transport. |
|
||||||
|
| **Bei RabbitMQ-Ausfall** | Events stoppen → Cache veraltet → **genau das Risiko**, das wir vermeiden wollen. Muss über Cache-TTL + Fail-CLOSED gelöst werden. |
|
||||||
|
| **Bei M15-Ausfall** | Keine neuen Events → Cache veraltet. Ohne TTL gefährlich (weiter OPEN). |
|
||||||
|
| **Komplexität** | Mittel — Subscription, Cache-Update, TTL-Reinigung, RabbitMQ-Reconnect im M09. |
|
||||||
|
| **Nachvollziehbarkeit/Audit** | Gut — Event-Stream ist Append-Log; aber Cache-Echo im M09 ist ein zweites Modell (Konsistenzpflege). |
|
||||||
|
|
||||||
|
### Variante C — Kombination: Event-Cache + synchroner Final-Check (EMPFEHLUNG)
|
||||||
|
|
||||||
|
**Event-Cache als schnelle Vorfilterung**, **synchroner HTTP-Final-Check unmittelbar vor dem
|
||||||
|
Broker-Submit** als harte Bestätigung. Nur wer beide klar "ENABLED" liefert, darf OPEN/INCREASE.
|
||||||
|
|
||||||
|
| Kriterium | Bewertung |
|
||||||
|
|---|---|
|
||||||
|
| **Sicherheit** | **Sehr hoch** — zweistufig. Cache als Bauraus/Performance, HTTP-Final als Autorität. |
|
||||||
|
| **Latenz** | Fast so gering wie B im Common-Case, da Vorfilter die meiste Zeit per Cache durchläuft; **aber** der obligatorische Final-Check ist der Latenzdominante Teil (immer +1 HTTP). |In Praxis Orders selten → akzeptabel. |
|
||||||
|
| **Ausfallsicherheit** | Beste — Cache kann "PAUSED/HALTED/UNKNOWN" sofort blockieren; HTTP- Ausfall ⇒ FAIL-CLOSED. Zwei unabhängige Quellen. |
|
||||||
|
| **Race Conditions** | Der synchronier Final-Check (TTL = kurz, unmittelbar vor dem Submit) minimiert die Chance, dass M15 direkt danach HALTED setzt. Plus `control_version`-Bump (§7) zum harten Ausschluss. |
|
||||||
|
| **Single Point of Failure** | M15 bleibt SPOF für OPEN/INCREASE (unvermeidbar, da autoritativ). REDUCE/CLOSE ausgenommen. |
|
||||||
|
| **Bei RabbitMQ-Ausfall** | Cache veraltet → aber Cache-Logik blockiert bei "veraltet/UNKNOWN"; HTTP-Final ist unabhängig vom Bus → liefert korrekte M15-Sicht (die selbst RMQ-Ausfall als HALTED sieht). |
|
||||||
|
| **Bei M15-Ausfall** | HTTP-Final-Check schlägt fehl → FAIL-CLOSED, OPEN/INCREASE blockiert. REDUCE/CLOSE frei. |
|
||||||
|
| **Komplexität** | Höher (beides). Aber gut beherrschbar; Cache als reiner nicht-kritischer Vorfilter, Autorität liegt klar beim HTTP. |
|
||||||
|
| **Nachvollziehbarkeit/Audit** | Exzellent — Final-Check liefert `state_version` + `timestamp` + `audit_id` pro Order; Cache-Zustand separat auditierbar. |
|
||||||
|
|
||||||
|
### Zusammenfassung der Varianten
|
||||||
|
|
||||||
|
| | A (HTTP) | B (Event-Cache) | C (Kombi) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Sicherheit | hoch | mittel | **sehr hoch** |
|
||||||
|
| Latenz | +HTTP | **sehr gering** | gering (HTTP-Final) |
|
||||||
|
| Ausfallsicherheit | ok (SPOF M15) | **schwach** (RMQ SPOF) | **beste** |
|
||||||
|
| Race-Risiko | minimiert (kurz) | **hoch (stale)** | **minimiert** |
|
||||||
|
| RMQ-Ausfall | korrekt (HALTED) | **risiko (stale)** | korrekt |
|
||||||
|
| M15-Ausfall | FAIL-CLOSED | **risiko (stale)** | FAIL-CLOSED |
|
||||||
|
| Komplexität | gering | mittel | **höher** |
|
||||||
|
| Audit | gut | zweites Modell | **exzellent** |
|
||||||
|
|
||||||
|
**Empfehlung: Variante C** (Event-Cache als Vorfilter + synchroner Final-Check).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Race Condition & Lösung
|
||||||
|
|
||||||
|
**Problem:** M09 prüft `ENABLED`, M15 wechselt sofort danach auf `HALTED` — bevor M09 die Order gesendet hat.
|
||||||
|
|
||||||
|
**Lösung (Kombination):**
|
||||||
|
1. **Final-Check unmittelbar vor `submit_order`** — minimales Zeitfenster (ms) zwischen Check und Send.
|
||||||
|
2. **TTL auf der HTTP-Antwort:** `expires_at` (z. B. `now + 500 ms`). Ist die Antwort beim Senden abgelaufen
|
||||||
|
(Fenster überschritten), NICHT senden → Order nach `FAILED/REJECTED` mit `CONTROL_STATE_STALE` markieren.
|
||||||
|
3. **`state_version`** (Monoton aufsteigend, von M15 je Status-Änderung). M09 übernimmt die im Final-Check
|
||||||
|
gesehene Version. Beim Broker-Submit trägt die Order die `control_version`; die Audit-Kette verbindet
|
||||||
|
genau die Version, die gegolten hat, mit dem Versand.
|
||||||
|
4. **Bestätigtes Einverständnis:** Der Check liefert `trading_status=ENABLED` **und** `state_version` **und**
|
||||||
|
`expires_at`. Nur wenn beides vorliegt und innerhalb der TTL ist, darf OPEN/INCREASE durchgehen.
|
||||||
|
5. **Audit-ID / Correlation:** M09 erzeugt `control_audit_id`, M15 protokolliert die Bestätigung. Damit ist
|
||||||
|
der „wir haben im ENABLED-Zustand gesendet" Moment exakt nachvollziehbar.
|
||||||
|
|
||||||
|
**Kein "Senden dann hoffen":** Ist das Fenster überschritten oder Status unklar → `FAILED` mit
|
||||||
|
`CONTROL_STATE_UNKNOWN/CONTROL_STATE_STALE`, **niemals** blind erneut senden (konsistent zum bestehenden
|
||||||
|
M09-Time-out-Handling: erst Status beim Broker klären, dann entscheiden).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. API-Vertrag M15 → M09
|
||||||
|
|
||||||
|
### 6.1 `GET /control/trading-state` (von M09 im Final-Check)
|
||||||
|
|
||||||
|
**Request:** keiner (oder optional `?action=OPEN` / `?for_action=OPEN` zur Kontext-Auditierung)
|
||||||
|
|
||||||
|
**Response 200 (ENABLED):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"trading_status": "ENABLED", // ENABLED|PAUSED|HALTED|UNKNOWN
|
||||||
|
"system_status": "HEALTHY", // Kontext
|
||||||
|
"state_version": 42, // monoton, von M15 je Änderung
|
||||||
|
"timestamp": "2026-08-20T15:30:00Z", // Erzeugzeit (UTC, ISO 8601)
|
||||||
|
"expires_at": "2026-08-20T15:30:00.5Z", // TTL-Horizont (Server), z. B. now+500ms
|
||||||
|
"ttl_seconds": 0.5,
|
||||||
|
"audit_id": "ctl-..." // M15-interne Audit-Kennung der Bestätigung
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Nur** dieses Objekt mit `trading_status=ENABLED` und `expires_at` in der Zukunft gilt als "eindeutige Erlaubnis".
|
||||||
|
|
||||||
|
**Response (blockiert):** gleiches Schema, aber `trading_status` = `PAUSED`/`HALTED`/`UNKNOWN`.
|
||||||
|
|
||||||
|
**Fehler/Timeouts:**
|
||||||
|
- HTTP 503 → M15 selbst nicht Ready/unbekannt → **FAIL-CLOSED** (als HALTED/unbekannt behandeln).
|
||||||
|
- Timeout/5xx/netz → **FAIL-CLOSED**, als `UNKNOWN` behandeln.
|
||||||
|
- Alle nicht-`ENABLED`-Antworten → OPEN/INCREASE **verweigert**.
|
||||||
|
|
||||||
|
### 6.2 Empfehlung der aktuellen API-Form
|
||||||
|
|
||||||
|
M15 hat aktuell `GET /control/trading-state` (liefert `trading_status` + `system_status`). Für M09-Anbindung
|
||||||
|
empfehle ich die obige **Erweiterung** um `state_version`, `timestamp`, `expires_at`, `audit_id` — dies ist ein
|
||||||
|
**additiver API-Vertrag** (kein Breaking Change). Das ist im Vorschlag berücksichtigt; die konkrete
|
||||||
|
Umsetzung erfolgt erst nach Freigabe.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Event-Schema für Status-Änderungen (market.control)
|
||||||
|
|
||||||
|
M15 publiziert auf Exchange `market.control`, Routing `trading.state` — **nur bei Status-Änderung** (keine Schleife).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_id": "evt-...",
|
||||||
|
"event_type": "trading.state",
|
||||||
|
"timestamp": "2026-08-20T17:30:00Z",
|
||||||
|
"version": 1,
|
||||||
|
"payload": {
|
||||||
|
"trading_status": "HALTED", // ENABLED|PAUSED|HALTED|UNKNOWN
|
||||||
|
"system_status": "UNHEALTHY",
|
||||||
|
"state_version": 43,
|
||||||
|
"reason_codes": ["POSTGRESQL_UNREACHABLE", "PERSISTENCE_FAILURE"],
|
||||||
|
"changed_at": "2026-08-20T17:30:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Im Cache (Variante C):**
|
||||||
|
- M09 hält `last_state` (trading_status + state_version + received_at).
|
||||||
|
- **TTL (z. B. 5 s):** ist `received_at` älter als TTL → Cache behandelt als `UNKNOWN/veraltet` → OPEN/INCREASE
|
||||||
|
**blockiert** (auch wenn letztes Event `ENABLED` war).
|
||||||
|
- Ein Event `state_version` älter als bereits gesehen → ignorieren (kein Regress).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Verhalten nach Recovery
|
||||||
|
|
||||||
|
- **Kein automatisches RESUME:** M15 nimmt nach Behebung der Ursache Status selbst neu (z. B. HALTED→HEALTHY→ENABLED)
|
||||||
|
**nur durch den regulären Monitoring-Zyklus** (Ursache wirklich erneut geprüft). Es gibt kein verstecktes Auto-Resume.
|
||||||
|
- M09 wartet darauf: erst wenn ein **neues Event mit `trading_status=ENABLED` und neuer `state_version`** oder
|
||||||
|
ein **HTTP-Final-Check mit `ENABLED`** vorliegt, sind OPEN/INCREASE wieder möglich.
|
||||||
|
- Ein `PAUSED→ENABLED`-Übergang verlangt, dass M15 nachweislich die früheren HALT-Gründe entfernt hat
|
||||||
|
(keine Reason-Codes mehr). Dies wird durch die bestehende FAIL-CLOSED-Regel + idempotentes Event sichergestellt.
|
||||||
|
- **Cache-TTL-Disziplin:** Nach einem HALTED/PAUSED-Ereignis bleibt M09 vorsichtig: Es traut dem Cache erst
|
||||||
|
wieder, wenn ein neues `ENABLED`-Event (hohe Version) eingegangen ist — niemals basierend auf Zeitablauf der
|
||||||
|
Sperre, sondern auf **bestätigter Freigabe**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Audit in M09 (zusätzlich zu M15-Audit)
|
||||||
|
|
||||||
|
Für jede Order-Entscheidung (im M09-Storage bzw. `execution_event`):
|
||||||
|
- `control_status`: Ergebnis des Final-Checks (`ENABLED`/`PAUSED`/...)
|
||||||
|
- `control_state_version`: Version, die beim Send gegolten hat
|
||||||
|
- `control_expires_at` / `control_ttl`: TTL-Horizont
|
||||||
|
- `control_audit_id`: vom Final-Check zugeordnet
|
||||||
|
- `control_check_source`: `final_http` (und optional `cache`)
|
||||||
|
|
||||||
|
Damit ist jede Order exakt dem autoritativen Zustand zum Sendzeitpunkt zugeordnet (Audit-Kette vollständig).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Zusammenfassung Architektur-Empfehlung
|
||||||
|
|
||||||
|
**Variante C**: Event-Cache (mark-to-control, TTL, state_version) als **nicht-kritischen Vorfilter** im M09 +
|
||||||
|
**synchronier HTTP-Final-Check** (`GET /control/trading-state`, mit `expires_at`+`state_version`+`audit_id`)
|
||||||
|
unmittelbar vor `submit_order`. Beide müssen `ENABLED` und frisch sein; sonst OPEN/INCREASE blockiert
|
||||||
|
(FAIL-CLOSED). REDUCE/CLOSE/CANCEL sind von der Control-Regel ausgenommen und laufen immer.
|
||||||
|
|
||||||
|
- **Zustand, der OPEN/INCREASE erlaubt:** Cache == `ENABLED` (frisch) UND Final-Check == `ENABLED` (frisch).
|
||||||
|
- **Sonst alles:** blockiert OPEN/INCREASE; REDUCE/CLOSE/CANCEL frei.
|
||||||
|
- **FAIL-CLOSED:** M15 nicht erreichbar, Timeout, Cache veraltet, `UNKNOWN` → OPEN/INCREASE verweigert.
|
||||||
|
- **M15 bleibt SPOF nur für OPEN/INCREASE** (unvermeidbar, autoritativ). Exit/Close bleibt immer möglich.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Pseudoflow (Order-Durchstich M09)
|
||||||
|
|
||||||
|
```
|
||||||
|
TRADE_APPROVED (Modul-08)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
1. Kategorisiere Aktion: OPEN | INCREASE | REDUCE | CLOSE | CANCEL
|
||||||
|
│
|
||||||
|
├─ action ∈ {REDUCE, CLOSE, CANCEL} → ERLANG DURCHSTICH (kein Control-Check)
|
||||||
|
│ └─→ weiter zum bestehenden Submit-Pfad
|
||||||
|
│
|
||||||
|
└─ action ∈ {OPEN, INCREASE}
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
2. [Cache-Vorfilter] local_state = cache.get()
|
||||||
|
├─ local_state ist stale (age > TTL) → → zum Final-Check (unten) aber NICHT aus Cache ableiten
|
||||||
|
└─ local_state.trading_status != ENABLED → BLOCKIERT (markiere FAILED/REJECTED, CONTROL_STATE_*)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
3. [Final-Check] resp = M15 GET /control/trading-state (Time out: 800ms, Retry: 1×)
|
||||||
|
├─ Timeout/5xx → FAIL-CLOSED → BLOCKIERT (FAILED, CONTROL_STATE_UNKNOWN)
|
||||||
|
└─ resp.trading_status != ENABLED → BLOCKIERT (FAILED, CONTROL_STATE_*)
|
||||||
|
└─ resp.expires_at <= now → BLOCKIERT (FAILED, CONTROL_STATE_STALE)
|
||||||
|
▼
|
||||||
|
4. Zustand grün (ENABLED, frisch, version=resp.state_version)
|
||||||
|
→ Order in DB (PENDING, CONTROL_FIELDS: version, audit_id, expires)
|
||||||
|
▼
|
||||||
|
5. unmittelbar danach: broker.submit_order(request) ← nur zwischen 4 und 5 minimales Fenster
|
||||||
|
▼
|
||||||
|
6. Ergebnis verarbeiten (FILLED/REJECTED/FAILED), Audit-Event mit control_* schreiben
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zeit-/Retry-Parameter (Vorschlag)
|
||||||
|
- Final-Check-HTTP-Timeout: **500 ms** (Docker-intern, schnell); Retry: **max 1** sofort.
|
||||||
|
- `expires_at`-TTL (M15): **500 ms** (Server-seitig gesetzt).
|
||||||
|
- Nach Timeout/unklar: **nicht senden**; Order als `FAILED`/`REJECTED` mit `CONTROL_STATE_UNKNOWN/STALE`.
|
||||||
|
- **Kein Auto-Resume**, kein "dann schicken wir eben ohne Check".
|
||||||
|
|
||||||
|
### Warum REDUCE/CLOSE/CANCEL durchlaufen
|
||||||
|
- Ziel des Control ist **Risiko-Management**. Ein HALT darf Risiko niemals einfrieren — im Gegenteil,
|
||||||
|
muss das System bei Problemen schneller in der Lage sein, Positionen abzubauen.
|
||||||
|
- `REDUCE`/`CLOSE`/`CANCEL` senken/eliminieren Risiko → **immer erlaubt**, unabhängig von M15-Status.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Offene Punkte für die Nutzer-Freigabe
|
||||||
|
|
||||||
|
1. **API-Vertrag erweitern** (`state_version`, `expires_at`, `ttl_seconds`, `audit_id`) in M15 (additiv).
|
||||||
|
2. **M09-Check-Position:** zwischen Schritt 4 (Order in DB) und 5 (Broker-Submit) — bestätigen.
|
||||||
|
3. **TTL-Wert** (`expires_at`-Fenster) festschreiben (Vorschlag 500 ms).
|
||||||
|
4. **Kategorisierung** der Order-Aktion in M09 (OPEN/INCREASE/REDUCE/CLOSE/CANCEL) ableiten — M09 muss
|
||||||
|
aus dem TRADE_APPROVED die Aktion bestimmen (Vorschlag: über `direction`/Kanten-Art der Entscheidung).
|
||||||
|
5. **M03–M06-Klassifikation** (Market-Data-Pipeline): ob Einzel-Ausfall dieser Module wirklich HALTED
|
||||||
|
auslösen soll, oder nur deren Daten-Staleness — zu finalisieren, beeinflusst WEN oft HALTED greift.
|
||||||
|
6. **SPOF-Akzeptanz:** M15 ist für OPEN/INCREASE zwingend (autoritativ). Exit-Pfad bleibt frei.
|
||||||
|
7. M15 weiterhin **nicht freigeben**, M16 **nicht beginnen** bis Nutzer-Entscheid.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Dies ist ein Design-Vorschlag. Es wurde kein Code geändert. M15 nicht freigegeben. M16 nicht begonnen.*
|
||||||
151
modul-15-m09-anbindung-implementierung.md
Normal file
151
modul-15-m09-anbindung-implementierung.md
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
# Modul-15 → Modul-09 Control-Anbindung — Implementierung & Live-E2E
|
||||||
|
|
||||||
|
**Status: FREIGEGEBEN / PRODUKTIV VERIFIZIERT** (20.08.2026, Nutzer-Bestätigung).
|
||||||
|
**Datum:** 20.08.2026 · **Autor:** Rain Ocampo (Hermes)
|
||||||
|
**Gilt für:** Modul-09-Execution-Service ↔ Modul-15-Monitoring-Control
|
||||||
|
|
||||||
|
> **Freigabe-Hinweis:** Diese Doku dokumentiert den Implementierungs- und E2E-Stand.
|
||||||
|
> Die **finale Freigabe** (M15/M09 als FREIGEGEBEN markieren) erfolgte am **20.08.2026**
|
||||||
|
> durch Nutzer-Bestätigung. M16 wird **nicht** begonnen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Umgesetzte Architektur (Variante C)
|
||||||
|
|
||||||
|
**Event-Cache als nicht-kritischer Vorfilter + synchroner HTTP-Final-Check als Autorität.**
|
||||||
|
|
||||||
|
- **M15** = autoritative Safety-/Trading-Control-Instanz.
|
||||||
|
- **M09** sendet neue Orders (OPEN/INCREASE) **nur**, wenn M15 eindeutig `ENABLED` + frisch bestätigt.
|
||||||
|
- **REDUCE/CLOSE/CANCEL** (Risikoabbau) sind **immer** erlaubt — unabhängig vom M15-Status.
|
||||||
|
|
||||||
|
### Kernregel
|
||||||
|
| State | OPEN/INCREASE | REDUCE/CLOSE/CANCEL |
|
||||||
|
|-------|---------------|---------------------|
|
||||||
|
| `ENABLED` | **ERLAUBT** | erlaubt |
|
||||||
|
| `PAUSED` | **VERBOTEN** | erlaubt |
|
||||||
|
| `HALTED` | **VERBOTEN** | erlaubt |
|
||||||
|
| `UNKNOWN` / M15-down / stale | **VERBOTEN (FAIL-CLOSED)** | erlaubt |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. API-Vertrag M15 → M09 (additiv, kein Breaking Change)
|
||||||
|
|
||||||
|
### `GET /control/trading-state`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"trading_status": "ENABLED", // ENABLED|PAUSED|HALTED|UNKNOWN
|
||||||
|
"system_status": "HEALTHY",
|
||||||
|
"state_version": 3, // monoton, je Status-Änderung inkrementiert
|
||||||
|
"timestamp": "2026-08-20T15:30:00Z",
|
||||||
|
"expires_at": "2026-08-20T15:30:00.5Z", // TTL-Horizont (now + 500ms)
|
||||||
|
"ttl_seconds": 0.5,
|
||||||
|
"audit_id": "ctl-..." // M15-interne Audit-Kennung
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Nur `trading_status=ENABLED` **und** `expires_at` in der Zukunft = eindeutige Erlaubnis.
|
||||||
|
Timeout/5xx/503 → **FAIL-CLOSED** (als UNKNOWN behandeln).
|
||||||
|
|
||||||
|
### Event-Kanal (Event-Cache)
|
||||||
|
- Exchange `market.control`, Routing-Key `trading.state` (nur bei Status-Änderung).
|
||||||
|
- M09-Consumer `ControlCacheConsumer` speist den Event-Cache-Vorfilter.
|
||||||
|
- Cache-TTL: 5 s. Stale-Cache → als UNKNOWN behandeln → OPEN/INCREASE blockiert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. M09-Implementierung
|
||||||
|
|
||||||
|
### Neue Dateien
|
||||||
|
- `app/control/client.py` — **ControlClient** (Event-Cache-Vorfilter + HTTP-Final-Check, FAIL-CLOSED)
|
||||||
|
- `app/control/__init__.py`
|
||||||
|
- `app/consumer/control_consumer.py` — **ControlCacheConsumer** (`market.control`/`trading.state`)
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- `app/core/service.py` — Control-Check zwischen Schritt 4 (Order anlegen) und Schritt 5 (`_submit_and_track`); `_apply_control_audit`; `action_type`-Ableitung; Consumer-Start/Stop
|
||||||
|
- `app/core/models.py` — `ExecutionOrder` + Audit-Felder
|
||||||
|
- `app/config.py` — M15-URL/Timeout/TTL/Cache-TTL + Control-Exchange/Routing-Key
|
||||||
|
- `app/storage/storage.py` — INSERT/UPDATE um control_*-Felder; `update_control_audit`
|
||||||
|
- `migrations/001_execution.sql` — Audit-Spalten (idempotent via `ADD COLUMN IF NOT EXISTS`)
|
||||||
|
|
||||||
|
### Audit-Felder je Order
|
||||||
|
`action_type`, `control_status`, `control_state_version`, `control_expires_at`,
|
||||||
|
`control_audit_id`, `control_check_source` (`http`|`cache`|`bypass`|`none`).
|
||||||
|
|
||||||
|
### Parameter
|
||||||
|
- Final-Check-HTTP-Timeout: **500 ms**, Retry: **max 1** sofort.
|
||||||
|
- `expires_at`-TTL (M15): **500 ms**.
|
||||||
|
- Cache-TTL (M09): **5 s**.
|
||||||
|
- Kein Auto-Resume; kein "Senden ohne Check".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Live-E2E-Ergebnisse (VPS, 20.08.2026)
|
||||||
|
|
||||||
|
Alle Szenarien auf dem VPS (187.124.31.123) live ausgeführt. **Alle grün.**
|
||||||
|
|
||||||
|
| # | Szenario | Ergebnis |
|
||||||
|
|---|----------|----------|
|
||||||
|
| S1 | **ENABLED-Pfad**: OPEN-Order durch, Auditfelder in DB | **9/9 PASS** |
|
||||||
|
| S2 | **HALTED-Pfad**: M03 gestoppt → OPEN blockiert, kein Broker-Send, reason `CONTROL_HALTED` | **9/9 PASS** |
|
||||||
|
| S3 | **PAUSED-Pfad**: nicht-krit. Service down → OPEN blockiert, reason `CONTROL_PAUSED` | **4/4 PASS** |
|
||||||
|
| S4 | **Risikoabbau bei HALTED**: REDUCE/CLOSE/CANCEL erlaubt (bypass) | **4/4 PASS** |
|
||||||
|
| S5 | **M15-down**: OPEN FAIL-CLOSED (`CONTROL_UNREACHABLE`), REDUCE/CLOSE/CANCEL erlaubt | **7/7 PASS** |
|
||||||
|
| S6 | **Cache-vs-Final-Check**: HTTP HALTED gewinnt über Cache | **2/2 PASS** |
|
||||||
|
| S7 | **Race/TTL**: Doppel-Publish → keine Doppelorder (Idempotenz) | **2/2 PASS** |
|
||||||
|
| S8 | **Event-Cache**: 1 Consumer, 0 Backlog, Reconnect nach RabbitMQ-Restart | **verifiziert** |
|
||||||
|
| S9 | **Recovery**: nach Behebung wieder ENABLED, OPEN erlaubt | **3/3 PASS** |
|
||||||
|
| S10 | **Health/Security**: health/ready 200, keine öffentl. Ports, keine Secrets, keine echten Tracebacks | **verifiziert** |
|
||||||
|
| S11 | **Regression**: bestehender M09-Paper-E2E (Kette 03→09) | **ALLE CHECKS BESTANDEN** |
|
||||||
|
|
||||||
|
### S1-Detail (ENABLED)
|
||||||
|
```
|
||||||
|
control_status=ENABLED
|
||||||
|
control_state_version=1
|
||||||
|
control_expires_at=1787241066.98
|
||||||
|
control_audit_id=ctl-2073194e651040db
|
||||||
|
control_check_source=http
|
||||||
|
action_type=OPEN
|
||||||
|
broker_order_id=PAPER-EXEC-... (Broker-Send erfolgt)
|
||||||
|
status=FILLED
|
||||||
|
```
|
||||||
|
|
||||||
|
### S2-Detail (HALTED)
|
||||||
|
```
|
||||||
|
control_status=HALTED
|
||||||
|
control_state_version=2
|
||||||
|
control_audit_id=ctl-b2cd6303f8754328
|
||||||
|
control_check_source=http
|
||||||
|
action_type=OPEN
|
||||||
|
broker_order_id=None (KEIN Broker-Send)
|
||||||
|
reason_codes=['CONTROL_HALTED']
|
||||||
|
status=FAILED
|
||||||
|
```
|
||||||
|
|
||||||
|
### S5-Detail (M15-down)
|
||||||
|
```
|
||||||
|
reason_codes=['CONTROL_UNREACHABLE']
|
||||||
|
broker_order_id=None (FAIL-CLOSED, kein Broker-Send)
|
||||||
|
REDUCE/CLOSE/CANCEL → control_check_source=bypass (erlaubt)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Verifikation Health/Security
|
||||||
|
|
||||||
|
- M09 + M15 `health` und `health/ready` → **200**.
|
||||||
|
- Keine öffentlichen Ports (nur Docker-intern via `expose:`).
|
||||||
|
- Keine Secrets in Logs/API/DB.
|
||||||
|
- Tracebacks in Logs: ausschließlich pika `AMQPConnectionError` während des
|
||||||
|
kontrollierten RabbitMQ-Restarts (erwartete Reconnect-Logs, keine echten Fehler).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Freigabe-Status
|
||||||
|
|
||||||
|
- **FREIGEGEBEN / PRODUKTIV VERIFIZIERT** (20.08.2026, Nutzer-Bestätigung).
|
||||||
|
- M15/M09-Control-Anbindung ist **final freigegeben** — keine weiteren technischen Änderungen an M09/M15.
|
||||||
|
- M16: **nicht beginnen**.
|
||||||
|
- Credential-Cleanup: temporäre Forgejo-Tokens/Deploy-Keys → count=0 (siehe Git-Commit).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Implementierung + Live-E2E abgeschlossen. Finale Freigabe erteilt (20.08.2026).*
|
||||||
146
modul-15-monitoring-control.md
Normal file
146
modul-15-monitoring-control.md
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
# Modul-15: Monitoring-Control
|
||||||
|
|
||||||
|
**Status: FREIGEGEBEN** (20.08.2026) · Container `Modul-15-Monitoring-Control` · Image `monitoring-control:0.1.0`
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
Zentrale **technische Überwachung und Sicherheits-/Kontrollebene** des Trading-Systems.
|
||||||
|
Überwacht die Dienste M03–M14 (Health/Readiness/Erreichbarkeit) plus RabbitMQ-Queues/Consumer
|
||||||
|
und PostgreSQL und leitet daraus deterministisch einen **globalen Trading-Status**
|
||||||
|
(`ENABLED` / `PAUSED` / `HALTED` / `UNKNOWN`) ab.
|
||||||
|
|
||||||
|
**Restriktionen (Nutzer-Vorgabe):**
|
||||||
|
- **Keine KI/ML**, **keine Trading-Entscheidungen**, **keine Brokerorders**, **keine Shell-/Docker-Control-Rechte**.
|
||||||
|
- **FAIL-CLOSED:** Bei unklarem/kritischem Zustand wird Trading sicher blockiert (`HALTED`).
|
||||||
|
- M15 unerreichbar → M09 behandelt den Zustand als `HALTED` (FAIL-CLOSED, siehe M15→M09-Anbindung, FREIGEGEBEN).
|
||||||
|
- **Keine Event-Schleifen:** Events nur bei Status-Änderung, dedupliziert.
|
||||||
|
- Container wird **nicht** automatisch neu gestartet/gekillt — M15 beobachtet und entscheidet Status.
|
||||||
|
- Port 55015 **nur Docker-intern** (`expose`, kein Host-Port).
|
||||||
|
|
||||||
|
## Architektur / Datenfluss
|
||||||
|
```
|
||||||
|
M03–M14 (health/ready) RabbitMQ (Queues) PostgreSQL
|
||||||
|
│ │ │
|
||||||
|
└──────────┬────────────┴──────────────┘
|
||||||
|
▼
|
||||||
|
Modul-15-Monitoring-Control
|
||||||
|
├── probe/ MonitoringProbe (HTTP-health + RMQ passive declare + PG)
|
||||||
|
├── rules/ MonitoringRules (deterministisch, FAIL-CLOSED, versioniert)
|
||||||
|
├── core/ MonitorService (Status-Engine, Single Source of Truth)
|
||||||
|
├── storage/ Persistenz (PostgreSQL: system_health/service_health/control_event)
|
||||||
|
├── publisher/ ControlPublisher (market.control, nur bei Status-Änderung)
|
||||||
|
└── api/main.py REST-API (intern, Port 55015)
|
||||||
|
```
|
||||||
|
Status → `system_health` (append), Services → `service_health` (upsert), Änderungen → `control_event` (append-only Audit).
|
||||||
|
|
||||||
|
## API (Docker-intern, Port 55015, kein öffentliches Port-Mapping)
|
||||||
|
```
|
||||||
|
GET /health liveness
|
||||||
|
GET /health/ready readiness (postgresql)
|
||||||
|
GET /monitoring/status aktueller System-Status + Trading-Status (internes State-Modell)
|
||||||
|
GET /monitoring/services aktueller Zustand aller überwachten Services/Queues
|
||||||
|
GET /control/status Control-Status (manual_override, control_api_enabled)
|
||||||
|
GET /control/trading-state autoritativer Trading-Status für M09
|
||||||
|
POST /control manuelle Control PAUSE/HALT/RESUME (nur wenn CONTROL_API_ENABLED=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deterministische Regeln (Rules-Engine, Version 0.1.0)
|
||||||
|
Präzedenz strikt von oben nach unten (FAIL-CLOSED):
|
||||||
|
1. **UNKNOWN** — PostgreSQL nicht erreichbar → nicht verlässlich → `HALTED`.
|
||||||
|
2. **UNHEALTHY/HALTED** — kritische Infrastruktur (PG/RabbitMQ) oder Execution-Pfad down.
|
||||||
|
3. **DEGRADED/PAUSED** — nicht-kritische Dienste down / stale.
|
||||||
|
4. **HEALTHY/ENABLED** — alles gut.
|
||||||
|
|
||||||
|
| Bedingung | System | Trading | Reason-Code |
|
||||||
|
|---|---|---|---|
|
||||||
|
| PostgreSQL nicht erreichbar | UNHEALTHY | HALTED | `POSTGRESQL_UNREACHABLE` (+ `PERSISTENCE_FAILURE`) |
|
||||||
|
| RabbitMQ nicht erreichbar | UNHEALTHY | HALTED | `RABBITMQ_UNREACHABLE` |
|
||||||
|
| Kritischer Service down (M03–M09) | UNHEALTHY | HALTED | `CRITICAL_SERVICE_DOWN:<id>` |
|
||||||
|
| Market Data stale (> max_age) | UNHEALTHY | HALTED | `MARKET_DATA_STALE` |
|
||||||
|
| Kritische Queue Consumer fehlt | — | HALTED | `NO_CONSUMER_<queue>` |
|
||||||
|
| Kritischer Queue-Backlog ≥1000 | — | HALTED | `QUEUE_BACKLOG_CRIT:<queue>:<n>` |
|
||||||
|
| Nicht-kritische Unhealthy (Analytics/Notification) | DEGRADED | PAUSED | `CRITICAL_SERVICE_DOWN`/`NO_CONSUMER_…` |
|
||||||
|
| Queue-Backlog ≥100 / Warning | DEGRADED | PAUSED | `QUEUE_BACKLOG:<queue>:<n>` |
|
||||||
|
| Alles healthy | HEALTHY | ENABLED | — |
|
||||||
|
|
||||||
|
**FAIL-CLOSED-Hinweis:** `UNKNOWN` Trading-Status wird in der API auf `HALTED` gemappt (nie ENABLED aus unklarem Zustand).
|
||||||
|
|
||||||
|
## Events (market.control Exchange, keine Event-Schleife)
|
||||||
|
Events werden **nur bei Status-Änderung** publiziert (Idempotenz) — nie bei jedem Poll:
|
||||||
|
|
||||||
|
| Event-Typ | Routing-Key | Anlass |
|
||||||
|
|---|---|---|
|
||||||
|
| `system.degraded` | `system.degraded` | System → DEGRADED |
|
||||||
|
| `system.halted` | `system.halted` | System → UNHEALTHY (HALT) |
|
||||||
|
| `system.recovered` | `system.recovered` | System → HEALTHY |
|
||||||
|
| `trading.state` | `trading.state` | Trading-Status-Änderung |
|
||||||
|
| `service.unhealthy` | `service.unhealthy` | Einzelner Service UNHEALTHY |
|
||||||
|
|
||||||
|
## Persistenz / Audit (PostgreSQL Modul-01)
|
||||||
|
- `system_health` — append-only Zeile pro Beobachtung (aktueller Status + reason_codes).
|
||||||
|
- `service_health` — Upsert pro `service_id` (aktueller Zustand jedes Services/Queue).
|
||||||
|
- `control_event` — **append-only Audit-Trail** jeder Status-/Trading-Änderung (`STATE_CHANGE`) und manuellen Control-Aktion (`MANUAL_*`). Idempotent — eine Änderung wird **genau einmal** auditiert.
|
||||||
|
|
||||||
|
## Manuelle Control (PAUSE/HALT/RESUME)
|
||||||
|
- **Nur wenn `CONTROL_API_ENABLED=true`** (Default `false` → manuelle Control über API deaktiviert, nur Regel-getrieben).
|
||||||
|
- **RESUME wird verweigert** (`RESUME_BLOCKED_CRITICAL_CAUSE`), solange eine kritische Ursache besteht.
|
||||||
|
- Jede manuelle Aktion wird append-only in `control_event` auditiert.
|
||||||
|
|
||||||
|
## Sicherheit
|
||||||
|
- **Keine Secrets in DB/Logs/Doku/Commits** — Zugangsdaten ausschließlich via Environment (Compose).
|
||||||
|
- **Kein Docker-Socket**, **keine Shell-/Docker-Control-Rechte** (CMD=`python main.py`).
|
||||||
|
- Port 55015 nur intern (`expose`), `restart: unless-stopped`, non-root (uid 1001).
|
||||||
|
- Env: `POSTGRES_*`, `RABBITMQ_*`, `CONTROL_API_ENABLED`.
|
||||||
|
|
||||||
|
## Compose / Betrieb
|
||||||
|
- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL`, RabbitMQ `Modul-02-RabbitMQ`.
|
||||||
|
- Env per Compose, Zugangsdaten nur via ENV (`POSTGRES_*`, `RABBITMQ_*`), nie hart kodiert.
|
||||||
|
|
||||||
|
## Unit-Tests
|
||||||
|
- `tests/test_monitoring.py`: **19/19 grün** — deterministische Rules, FAIL-CLOSED (PG/RabbitMQ/Critical), stale, Queue-Consumer, Idempotenz, Audit genau einmal, Recovery, Reconnect, keine Secrets, `state_version`-Inkrementierung.
|
||||||
|
|
||||||
|
## E2E-Verifikation (VPS, 20.08.2026) — 17 Szenarien
|
||||||
|
Alle 17 verifiziert (System-/Trading-Status via Loop, API `/control/trading-state` + `/monitoring/status` konsistent):
|
||||||
|
1. **Normalzustand:** `HEALTHY/ENABLED` — API, State-Modell, Loop identisch.
|
||||||
|
2. **PG-Ausfall:** `HALTED`, `POSTGRESQL_UNREACHABLE`+`PERSISTENCE_FAILURE`, Audit genau einmal.
|
||||||
|
3. **RabbitMQ-Ausfall:** `HALTED`, `RABBITMQ_UNREACHABLE`.
|
||||||
|
4. **Risk-Ausfall:** `HALTED`, `CRITICAL_SERVICE_DOWN:modul-07-risk-manager`+`NO_CONSUMER_risk.input`.
|
||||||
|
5. **Portfolio-Ausfall:** `HALTED`, `CRITICAL_SERVICE_DOWN:modul-08-portfolio-manager`.
|
||||||
|
6. **Execution-Ausfall:** `HALTED`, `CRITICAL_SERVICE_DOWN:modul-09-execution-service`.
|
||||||
|
7. **Analytics-Ausfall:** `DEGRADED` + `PAUSED` (Trading nicht vollständig kill).
|
||||||
|
8. **Notification-Ausfall:** `DEGRADED` + `PAUSED`.
|
||||||
|
9. **Market Data stale:** `HALTED`, `MARKET_DATA_STALE`; Recovery nach frischen Daten.
|
||||||
|
10. **Kritischer Consumer fehlt:** HALT-Severity korrekt (NO_CONSUMER auf kritischer Queue).
|
||||||
|
11. **Queue-Backlog:** Warning/HALT gemäß Schwelle (100/1000, Unit-getestet).
|
||||||
|
12. **Recovery:** Ursache erneut geprüft, kein Auto-RESUME solange Ursache, Status konsistent, kein Event-Spam.
|
||||||
|
13. **Manuelle Controls:** mit `CONTROL_API_ENABLED=false` sauber abgelehnt (`control_api_disabled`); Trading unangetastet.
|
||||||
|
14. **Idempotenz/Event-Flut:** stabiler Zustand → keine wiederholten SYSTEM_HALTED/SERVICE_UNHEALTHY (nur 1 Audit je Änderung).
|
||||||
|
15. **RabbitMQ-Reconnect:** automatisch (alle Recovery-Zyklen), keine Zombies, keine Eventverluste.
|
||||||
|
16. **Audit:** `control_event`/`system_health`/`service_health` vollständig + zeitlich nachvollziehbar.
|
||||||
|
17. **Security:** kein öffentlicher Port (PortBindings `{}`), kein Docker-Socket, keine Secrets im Compose/Logs.
|
||||||
|
|
||||||
|
## Bekannte Design-Hinweise (für separaten M15→M09-Vorschlag)
|
||||||
|
- **M03–M06 (Market-Data-Pipeline) sind in `CRITICAL_SERVICES`** klassifiziert. Ausfall eines einzelnen Daten-Moduls führt so zu `HALTED` (über `CRITICAL_SERVICE_DOWN`), nicht ausschließlich über `MARKET_DATA_STALE`. Das ist fachlich konservativ aber zu klären (nur Data-Pipeline vs. Execution-Pfad).
|
||||||
|
- **DEGRADED → `PAUSED`** (nicht `ENABLED`): bei Analytics/Notification-Ausfall wird Trading vorsichtig gestoppt, nicht vollständig killed. Design-Entscheidung, im Vorschlag benannt.
|
||||||
|
- **PG-Ausfall ⇒ `PERSISTENCE_FAILURE`**: ohne PG kann `control_event`-Audit nicht geschrieben werden (fachlich korrekt FAIL-CLOSED), aber Audit-Lücke im Fehlerfenster.
|
||||||
|
|
||||||
|
## Freigabe
|
||||||
|
- **FREIGEGEBEN** (20.08.2026, Nutzer-Bestätigung). Implementierung + E2E (17 Szenarien) grün.
|
||||||
|
- M09-Anbindung **umgesetzt** (Variante C, Event-Cache + HTTP-Final-Check) — siehe `modul-15-m09-anbindung-implementierung.md`, FREIGEGEBEN.
|
||||||
|
|
||||||
|
## Nächste Module
|
||||||
|
- **Modul-16 ff.** — noch Platzhalter (alpine), NICHT gestartet.
|
||||||
|
|
||||||
|
---
|
||||||
|
## Geändert
|
||||||
|
```
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: Modul-15-Monitoring-Control dokumentiert — implementiert + E2E-verifiziert (17 Szenarien grün),
|
||||||
|
Rules/API/Events/Audit/Security dokumentiert, NICHT FREIGEGEBEN.
|
||||||
|
```
|
||||||
|
```
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: Modul-15-Monitoring-Control auf FREIGEGEBEN gesetzt (Nutzer-Bestätigung 20.08.2026).
|
||||||
|
M15→M09-Anbindung (Variante C) als umgesetzt/FREIGEGEBEN vermerkt; Unit-Tests 19/19.
|
||||||
|
```
|
||||||
128
modul-18-position-manager.md
Normal file
128
modul-18-position-manager.md
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
# Modul-18: Position-Manager
|
||||||
|
|
||||||
|
**Status: FREIGEGEBEN (21.08.2026)** · Container `Modul-18-Position-Manager` · Port `55018` (nur intern/expose)
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
Modul-18 verwaltet **bereits offene Positionen** deterministisch: Es beobachtet offene Trades und gibt auf Basis fester
|
||||||
|
Exit-Regeln (Stop-Loss, Take-Profit, Break-even, Trailing, Time-Exit) gezielte **Exit-Aktionen** an Modul-09.
|
||||||
|
|
||||||
|
**Kritische Grenze:** M18 ist ein **reiner Exit-/Positions-Manager**, KEIN Signal-/Einstiegsmodul.
|
||||||
|
- ✅ M18 darf NUR `REDUCE` / `CLOSE` / `CANCEL` an M09 geben. **NIE `OPEN` / `INCREASE`.**
|
||||||
|
- ❌ Kein eigenmächtiges Nachkaufen/Pyramiding. Keine KI/ML. Keine direkte Broker-Anbindung.
|
||||||
|
- Jede Order-Aktion läuft ausschließlich über **M09 Execution** via RabbitMQ `trade.approved`.
|
||||||
|
- **FAIL-CLOSED:** stale/unbekannter Zustand → nie blind senden; Restart/Reconnect erzeugt keine Doppelaktion; keine Zombies.
|
||||||
|
|
||||||
|
## Architektur / Datenfluss
|
||||||
|
```
|
||||||
|
Modul-03-Market-Data (Preise) ──┐
|
||||||
|
▼
|
||||||
|
Modul-10-Trade-Journal (OPEN) ──► Modul-18-Position-Manager (Port 55018, NUR intern)
|
||||||
|
Modul-15 (Trading-State, readonly) │
|
||||||
|
├── Engine-Loop (deterministische Exit-Regeln)
|
||||||
|
├── Publisher → RabbitMQ market.portfolio / trade.approved → M09
|
||||||
|
└── Consumer ← RabbitMQ market.execution / order.filled (Reconciliation)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Modul-09-Execution-Service (PaperBroker: broker=paper) → order.filled
|
||||||
|
```
|
||||||
|
|
||||||
|
**App-Struktur (`app/`):**
|
||||||
|
- `core/engine.py` — Exit-Regel-Engine (Regel-Priorität deterministisch)
|
||||||
|
- `core/orchestrator.py` — Hydrate-State, wendet Regeln an, idempotent
|
||||||
|
- `publisher/publisher.py` — publiziert Aktionen an M09 (`trade.approved`, je Aktion eine eindeutige `action_id`)
|
||||||
|
- `consumer/consumer.py` — konsumiert M09-Execution-Events (`order.filled` etc.) für Reconciliation
|
||||||
|
- `pricing/client.py` — liest Preise von M03, inkl. **Stale-Guard**
|
||||||
|
- `providers/journal.py` — Quelle offener Positionen (Trade-Journal, `status=OPEN`)
|
||||||
|
- `storage/storage.py` — Persistenz (`position_state`/`position_action`/`position_event`)
|
||||||
|
- `api/main.py` — REST-API (`/health`, `/readiness`), Port 55018 nur intern
|
||||||
|
|
||||||
|
**Eigene Tabellen (`migrations/001_position.sql`):**
|
||||||
|
- `position_state` — aktueller verwalteter Zustand je Position
|
||||||
|
- `position_action` — jede an M09 gegebene Order-Aktion (REDUCE/CLOSE/CANCEL)
|
||||||
|
- `position_event` — append-only Audit-Event-Historie
|
||||||
|
|
||||||
|
## Exit-Regeln (deterministisch)
|
||||||
|
| Regel | Trigger | Aktion |
|
||||||
|
|-------|---------|--------|
|
||||||
|
| Stop-Loss | Preis ≤ stop_loss | CLOSE |
|
||||||
|
| Take-Profit | Preis ≥ target | CLOSE |
|
||||||
|
| Break-even | Preis günstig, SL auf Entry gesetzt | CLOSE bei BE-SL |
|
||||||
|
| Trailing | Preis weiter günstig (best_price_seen) | CLOSE bei Trailing-SL |
|
||||||
|
| Time-Exit | max. Haltedauer erreicht | CLOSE |
|
||||||
|
| Partial-Fill / Duplikat | idempotente Aktion | exakt 1 Order, keine Doppelaktion |
|
||||||
|
| HALTED (M15) | Trading-State HALTED | CLOSE weiterhin erlaubt (Exit) |
|
||||||
|
| stale Preis | Kerze älter als `stale_price_max_age_seconds` (3600s) | **FAIL-CLOSED: keine Aktion** |
|
||||||
|
|
||||||
|
Regeln laufen mit fester Priorität; jede Exit-Aktion wird als eindeutige `position_action` (action_id) persistiert.
|
||||||
|
|
||||||
|
## M09-CLOSE-Pfad (Reduktion → Execution)
|
||||||
|
```
|
||||||
|
M18 position_action (action_id, CLOSE)
|
||||||
|
→ Publisher → RabbitMQ exchange "market.portfolio" routing "trade.approved"
|
||||||
|
→ M09 Execution (control/client bypass für REDUCE/CLOSE/CANCEL) → PaperBroker (broker=paper)
|
||||||
|
→ execution_order (source_portfolio_decision_id = M18 action_id, status FILLED)
|
||||||
|
→ RabbitMQ "market.execution" routing "order.filled"
|
||||||
|
→ M18-Consumer → Reconciliation (position_state → CLOSED, qty=0, Audit-Event)
|
||||||
|
```
|
||||||
|
**Verifikation:** Für jede Exit-Aktion existiert **exakt 1** execution_order mit `broker=paper`, `status=FILLED`,
|
||||||
|
`source_portfolio_decision_id` = M18 `action_id`. M09 speichert die M18-`action_id` als
|
||||||
|
`source_portfolio_decision_id` (top-level + Payload). Keine Doppelaktionen je action_id (HAVING count>1 = 0).
|
||||||
|
|
||||||
|
## Reconciliation über order.filled
|
||||||
|
M18 konsumiert M09-Execution-Events (`order.filled`, `order.partially_filled`, `order.rejected`, `order.failed`)
|
||||||
|
und gleicht den eigenen `position_state` ab: FILLED → Position als `CLOSED` markieren, `quantity=0`, Audit-Event
|
||||||
|
`ORDER_EXECUTED`/`ORDER_FILLED`. Damit sind M10 (Journal) und M18 (Manager) konsistent, auch wenn M10 CLOSE
|
||||||
|
nicht automatisch verknüpft.
|
||||||
|
|
||||||
|
## Thread-Local-DB-Fix (Root Cause „connection pointer is NULL")
|
||||||
|
**Symptom:** Reconciliation scheiterte sporadisch mit `connection pointer is NULL`.
|
||||||
|
**Root Cause:** `PositionStorage._conn` war eine **geteilte Singleton-Verbindung** zwischen Engine-Loop-Thread und
|
||||||
|
ExecutionConsumer-Thread. psycopg2 ist **nicht thread-safe**: ein Thread schloss die Verbindung
|
||||||
|
(`resolve_position_key`/`has_pending_exit`), der andere Thread lief auf totem Cursor.
|
||||||
|
**Fix:** **Thread-Local-Verbindung** — `self._tl = threading.local()`; jeder Thread erhält eine eigene
|
||||||
|
psycopg2-Verbindung; kein Cross-Thread-`.close()`. `close()`/`rollback()` (ensure_schema) auf lokale Verbindung umgestellt.
|
||||||
|
**Verifikation:** Unit-Tests **13/13 grün**, E2E **47/47 PASS**.
|
||||||
|
|
||||||
|
## Testdesign-Lessons (E2E)
|
||||||
|
1. **action_id statt Journal-pd_id:** M09 speichert die M18-`action_id` als `source_portfolio_decision_id` —
|
||||||
|
Verifikation muss `wait_execution_for_action(action_id)` nutzen, nicht Journal-`pd_id`.
|
||||||
|
2. **Audit-Eventname:** Consumer schreibt `ORDER_EXECUTED` (bzw. `ORDER_FILLED`); Test akzeptiert beide.
|
||||||
|
3. **Szenario 10 RA/RB:** Verifikation über `action_id`, nicht `pd_id`.
|
||||||
|
4. **Stale-Szenario:** `stale_price_max_age_seconds=3600`; eine 5-min-Kerze ist NICHT stale. M03 akzeptiert
|
||||||
|
bis 30 Tage alte Kerzen, persistiert injiziertes `ts` aber nicht (vergibt frisches ts). Fix: 2h-alte Kerze
|
||||||
|
(>1h < 30Tage) injizieren + **pro-Lauf eindeutiges Symbol** (M03-Speicher hat keinen Delete-Endpoint).
|
||||||
|
|
||||||
|
## Safety (User-Vorgabe, kritisch)
|
||||||
|
- **NUR REDUCE/CLOSE/CANCEL** — niemals OPEN/INCREASE. Kein Nachkaufen/Pyramiding.
|
||||||
|
- Idempotent + race-safe + **FAIL-CLOSED**. Restart/Reconnect keine Doppelaktion. Keine Zombies.
|
||||||
|
- Broker/Execution unklar → nie blind erneut senden.
|
||||||
|
- Vollständiger Audit-Trail (`position_event` append-only).
|
||||||
|
- Port 55018 nur Docker-intern (`expose`), kein öffentliches Mapping. Netz `trading-modules`.
|
||||||
|
- Keine Secrets in Logs/API/DB.
|
||||||
|
|
||||||
|
## Tests & E2E-Verifikation (VPS, 21.08.2026)
|
||||||
|
- **Unit-Tests** `tests/test_engine.py`: **13/13 grün** (Thread-Local-Fix).
|
||||||
|
- **E2E** `tests/e2e_m18_full2.py`: **47/47 PASS** (EXIT=0), deterministischer Lauf 0–15:
|
||||||
|
HOLD · SL→CLOSE · TP→CLOSE · Break-even · Trailing · Time-Exit · Partial-Fill · bereits CLOSED · Duplikat ·
|
||||||
|
parallel/race-safe (RA/RB) · M09 down · M15 HALTED · **stale Preis (FAIL-CLOSED)** · Restart · Reconciliation order.filled.
|
||||||
|
- Für jede Exit-Aktion: exakt 1 Execution je action_id, `broker=paper`, `status=FILLED`, keine Doppelaktionen,
|
||||||
|
keine offenen Zombie-Testpositionen.
|
||||||
|
- **Regression M03→M09:** M18 erzeugt **NIE OPEN/INCREASE** (nur CLOSE). M09 execution_order:
|
||||||
|
nur `CLOSE`, alle `FILLED`, `broker=paper`, keine Doppel-Order je action_id.
|
||||||
|
- **Health/Security:** `/health` + `/readiness` 200; Port 55018 nur intern; keine Secrets in
|
||||||
|
Logs/API/DB; keine Tracebacks seit finalem Lauf; Consumer/RMQ-Reconnect sauber.
|
||||||
|
|
||||||
|
## Compose / Betrieb
|
||||||
|
- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL` (Modul-01), RabbitMQ `Modul-02-RabbitMQ`.
|
||||||
|
- Port 55018 nur `expose` (Docker-intern), kein Host-Mapping. `restart: unless-stopped`.
|
||||||
|
- Keine Secrets im Compose-Env.
|
||||||
|
|
||||||
|
---
|
||||||
|
## Geändert
|
||||||
|
```
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 21.08.2026
|
||||||
|
Grund: Modul-18-Position-Manager formal abgeschlossen + FREIGEGEBEN. E2E 47/47 PASS, Regression M03→M09 gruen
|
||||||
|
(nur CLOSE, nie OPEN/INCREASE), Health/Security gruen (Port 55018 intern, keine Secrets/Tracebacks).
|
||||||
|
Thread-Local-DB-Fix (psycopg2 "connection pointer is NULL" geteiltes Singleton → Thread-Local). Doku angelegt.
|
||||||
|
```
|
||||||
59
ports-reference.md
Normal file
59
ports-reference.md
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Ports & Service-Referenz — Trading-System VPS
|
||||||
|
|
||||||
|
> Stand: 20.08.2026 · VPS: `187.124.31.123` · SSH: Public-Key-Auth (nur)
|
||||||
|
|
||||||
|
## Netzwerk & Core-Infrastruktur
|
||||||
|
- **Netzwerk:** `trading-modules` (bridge) — Kommunikation über Docker-interne Service-Hostnamen, keine festen IPs.
|
||||||
|
- **Host:** Hostinger-VPS, 300 GB Disk, ~31 GiB RAM.
|
||||||
|
|
||||||
|
## Feste Port-Zuordnung (Schema `55NNN` nach Modulnummer)
|
||||||
|
| Modul | Container | Service | Host-Port | Öffentlich? |
|
||||||
|
|-------|-----------|---------|-----------|-------------|
|
||||||
|
| 01 | Modul-01-PostgreSQL | PostgreSQL | **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) |
|
||||||
|
| 03 | Modul-03-Market-Data | FastAPI | 55003 (intern, `expose`) | **nein** (nur intern) |
|
||||||
|
| 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–12 | Modul-06…12 | FastAPI | 55006–55012 (intern, expose) | nein (nur intern) |
|
||||||
|
| 13 | Modul-13-Optimization | FastAPI | 55013 (intern, expose) | nein (nur intern) |
|
||||||
|
| 14–17 | … | — | 55014–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.
|
||||||
|
|
||||||
|
## Weitere Dienste (Host)
|
||||||
|
| Dienst | Container | Port |
|
||||||
|
|--------|-----------|------|
|
||||||
|
| Forgejo | forgejo-c4u8yyi1eaz1gepn3pqmr5fb | 3000 (HTTP) / 22222 (SSH) |
|
||||||
|
| Tolaria (Second Brain) | tolaria | 5173 |
|
||||||
|
| OpenClaw Alice | openclaw-suqw-openclaw-1 | 55163 |
|
||||||
|
| OpenClaw Matt | openclaw-3sgu-openclaw-1 | 54524 |
|
||||||
|
| Hermes Rain | hermes-workspace-e6un… | 32776 (UI) |
|
||||||
|
| Ollama | ollama-nb6d-ollama-1 | 11434 (intern) |
|
||||||
|
| n8n | n8n | (Coolify-managed) |
|
||||||
|
| Traefik | traefik | 80/443 |
|
||||||
|
|
||||||
|
## Docker-interne Hostnamen (wichtig für Container-Kommunikation)
|
||||||
|
- PostgreSQL: `Modul-01-PostgreSQL:5432`
|
||||||
|
- RabbitMQ: `Modul-02-RabbitMQ:5672` (vhost `trading`)
|
||||||
|
- Ollama: `ollama-nb6d-ollama-1:11434`
|
||||||
|
|
||||||
|
## Sicherheits-Notizen
|
||||||
|
- **Kein Modul-01/02/03-Port ist öffentlich** — alle nur im Docker-Netzwerk erreichbar (via `expose`, nicht `ports`). Verifiziert: `curl` auf 55432/55672/15672/55003 schlägt von außen fehl, während Modul-03 intern weiterhin PostgreSQL & RabbitMQ erreicht.
|
||||||
|
- Zugangsdaten ausschließlich als Env-Variablen/Secrets, nie im Code.
|
||||||
|
- Öffentliche Ports nur, wo nötig (Admin/Debug/UI).
|
||||||
|
|
||||||
|
---
|
||||||
|
```
|
||||||
|
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.
|
||||||
|
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: ports-reference aktualisiert — Modul-06–12 als real (FastAPI, intern) und Modul-13 Optimization (55013 intern) eingetragen.
|
||||||
115
trend-pullback-v1.1-spec.md
Normal file
115
trend-pullback-v1.1-spec.md
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
# trend_pullback_v1.1 — Spezifikation
|
||||||
|
|
||||||
|
> Status: **IMPLEMENTIERT + DEPLOYT** (`backtesting:0.1.2`, Modul-12), V1-Kompatibilitätsmodus bitgenau.
|
||||||
|
> Erstellt: 20.08.2026 (Rain Ocampo) · Aktualisiert: 20.08.2026
|
||||||
|
|
||||||
|
## 1. Motivation
|
||||||
|
|
||||||
|
Die Produktions-Strategie `trend_pullback_v1` (Modul-05/12) ist unter den
|
||||||
|
**unveränderten** produktiven Regime-Schwellen fachlich widersprüchlich
|
||||||
|
(messbar): Ein TREND_UP-Regime verlangt Volatilität, der Pullback
|
||||||
|
`close < EMA20` verlangt ein flaches Plateau. Im 60-Bars-Produktionsfenster
|
||||||
|
(`regime_lookback=60`) schneiden sich `TREND_UP` und `close<EMA20` nur in
|
||||||
|
**1–4 von 1620** Kerzen (m13_evidence.py, Messwerte 20.08.2026). Die
|
||||||
|
Vol-Override (`HIGH_VOL`/`LOW_VOL`) kippt das Regime bei jedem sinnvollen
|
||||||
|
Pullback (>95% der `close<EMA20`-Kerzen werden `LOW_VOL`).
|
||||||
|
|
||||||
|
**V1.1 trennt Richtung und Pullback-Timing.** Richtung wird aus einem
|
||||||
|
strategie-internen, längeren Fenster berechnet (nicht durch Vol-Override
|
||||||
|
kippbar); Volatilität fließt nur noch ins Pullback-Timing (ATR-Band um EMA20)
|
||||||
|
ein.
|
||||||
|
|
||||||
|
## 2. Kernentscheidungen
|
||||||
|
|
||||||
|
1. **V1.1 bleibt zustandslos.**
|
||||||
|
Trendrichtung wird **jede Bar neu** aus dem aktuellen Fenster berechnet.
|
||||||
|
**Kein Sticky-State.** Kein Hidden-State. Begründung: einfacher, transparenter
|
||||||
|
und reproduzierbarer (deterministisch nach Seed/Replay).
|
||||||
|
|
||||||
|
2. **Kein kurzfristiger Vol-Override auf die Richtung.**
|
||||||
|
`regime_vol_override=false` (V1.1-Default): Die Trend-Richtung wird
|
||||||
|
strategie-intern berechnet und **nicht** durch `HIGH_/LOW_VOLATILITY`
|
||||||
|
überschrieben. Volatilität beeinflusst nur das Pullback-Timing (Band).
|
||||||
|
|
||||||
|
3. **Optimierungsraum für V1.1 bewusst klein.** Erste Iteration optimiert
|
||||||
|
ausschließlich `pullback_band_atr`, `trend_regime_window`, `rr_multiplier`.
|
||||||
|
ADX-/EMA-Perioden und `atr_period` bleiben **fix** auf produktiven V1-Werten.
|
||||||
|
|
||||||
|
## 3. Pseudo-Regel (Variante A, lookahead-frei)
|
||||||
|
|
||||||
|
Strategie-intern, jede Bar, aus geordneten `prev_*`-Serien (kein Lookahead,
|
||||||
|
kein versteckter Zustand):
|
||||||
|
|
||||||
|
```
|
||||||
|
# --- Trend-Richtung (Fenster = trend_regime_window) ---
|
||||||
|
ema_fast_l = EMA(prev_closes, ema_fast_period, lookback=trend_regime_window)
|
||||||
|
ema_slow_l = EMA(prev_closes, ema_slow_period, lookback=trend_regime_window)
|
||||||
|
adx_l = ADX(prev_highs, prev_lows, prev_closes, adx_period, lookback=trend_regime_window)
|
||||||
|
|
||||||
|
trend_dir = UP wenn (ema_fast_l > ema_slow_l) UND adx_l >= adx_trend_threshold
|
||||||
|
trend_dir = DOWN wenn (ema_fast_l < ema_slow_l) UND adx_l >= adx_trend_threshold
|
||||||
|
sonst: kein Trade # KEIN atr_ratio-Check für die Richtung
|
||||||
|
|
||||||
|
# --- Pullback-Timing (ATR-Band um EMA20) ---
|
||||||
|
atr_prev = ATR(prev_highs, prev_lows, prev_closes, atr_period)
|
||||||
|
ema20 = EMA20(prev_closes)
|
||||||
|
pullback_limit = ema20 + pullback_band_atr * atr_prev # LONG
|
||||||
|
pullback_limit = ema20 - pullback_band_atr * atr_prev # SHORT
|
||||||
|
|
||||||
|
LONG wenn trend_dir==UP UND last_close>sma200 UND last_close < pullback_limit
|
||||||
|
UND last_close>prev_close UND swing_low<last_close UND rr>=min_risk_reward
|
||||||
|
SHORT wenn trend_dir==DOWN UND last_close<sma200 UND last_close > pullback_limit
|
||||||
|
UND last_close<prev_close UND swing_high>last_close UND rr>=min_risk_reward
|
||||||
|
```
|
||||||
|
|
||||||
|
- `break_high` entfällt (Befund: `prev_high ≥ prev_close` ⇒ `break_high ⇒ close_up`;
|
||||||
|
bleibt als `close_up`/`close_down` erhalten — nicht restriktiv).
|
||||||
|
- Deterministisch: reine Listen-Berechnung, kein Zufall, kein Hidden-State.
|
||||||
|
|
||||||
|
## 4. Parameter-Tabelle
|
||||||
|
|
||||||
|
| Parameter | V1 produktiv (Code) | V1.1 Default | Min | Max | Zweck | V1.1 Optimierbar? |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| `pullback_band_atr` | — (strikt `< EMA20`) | **0.35** | 0.0 | 1.0 | ATR-Band um EMA20. `0.0`=strikt V1. | **Ja** |
|
||||||
|
| `atr_period` | 14 | **14** | 14 (fix) | 14 (fix) | ATR-Fenster für Pullback-Band | Nein (fix) |
|
||||||
|
| `trend_regime_window` | 60 (geteilter 60er) | **200** | 60 | 500 | Fenster für strategie-internes Trend-Regime | **Ja** |
|
||||||
|
| `regime_vol_override` | true (geteilt) | **false** | — | — | false=V1.1 strategie-eigene Richtung | Nein (fix) |
|
||||||
|
| `rr_multiplier` | 2.0 | **2.0** | 1.0 | 4.0 | Target = Entry + rr_multiplier×Risiko | **Ja** |
|
||||||
|
| `min_risk_reward` | 1.0 | **1.0** | 1.0 (min) | 3.0 | Mindest-RR; keine Kandidaten mit Chance/Risiko <1 | Nein (fix) |
|
||||||
|
|
||||||
|
**Zusätzliche Trend-Parameter (produktiv, V1.1 fix — NICHT von Modul-13 optimieren):**
|
||||||
|
|
||||||
|
| Parameter | V1 produktiv (Code) | V1.1 | Zweck |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `adx_trend_threshold` | **20.0** (config.py:54) | fix 20.0 | ADX-Schwelle für echten Trend |
|
||||||
|
| `adx_period` | **14** (config.py:50) | fix 14 | ADX-Indikatorfenster |
|
||||||
|
| `ema_fast_period` | **20** (config.py:36) | fix 20 | Fast-EMA (Strategie-Pullback-Basis) |
|
||||||
|
| `ema_slow_period` | **30** (config.py:47) | fix 30 | Slow-EMA (Regime-Flanke) |
|
||||||
|
|
||||||
|
## 5. V1-Kompatibilitätsmodus (bitgenau V1)
|
||||||
|
|
||||||
|
```
|
||||||
|
pullback_band_atr = 0.0
|
||||||
|
regime_vol_override = true
|
||||||
|
trend_regime_window = 60
|
||||||
|
+ alle übrigen V1-Parameter auf produktiven Defaults
|
||||||
|
```
|
||||||
|
⇒ muss V1 **bitgenau** reproduzieren (Rückwärtskompatibilität; Backtest ohne
|
||||||
|
`strategy_params` bleibt fachlich identisch). **E2E-verifiziert**: V1-Kompatmodus
|
||||||
|
LONG+SHORT bitgenau (net 200.0, target r=2.0).
|
||||||
|
|
||||||
|
## 6. Abgrenzung / No-Go
|
||||||
|
|
||||||
|
- **Keine KI/LLM** in V1.1, keine Live-Orders.
|
||||||
|
- Produktions-Regime-Schwellen (`adx_trend_threshold=20`, `trend_min_ema_gap=0.02`,
|
||||||
|
`high_vol_atr_ratio=0.03`, `low_vol_atr_ratio=0.008`) werden **nicht verändert**.
|
||||||
|
- `atr_period`, ADX-/EMA-Perioden, `min_risk_reward` werden in dieser ersten
|
||||||
|
Optimierungsrunde **nicht** über Modul-13 optimiert.
|
||||||
|
|
||||||
|
---
|
||||||
|
## Geändert
|
||||||
|
```
|
||||||
|
Geändert von: Rain Ocampo
|
||||||
|
Datum: 20.08.2026
|
||||||
|
Grund: V1.1-Spec von ENTWURF auf DEPLOYT aktualisiert (backtesting:0.1.2); V1-Kompatmodus bitgenau E2E-verifiziert.
|
||||||
|
```
|
||||||
Loading…
Reference in a new issue