trading-system-docs/tolaria/c4b-search-service/index_source.json

1014 lines
No EOL
443 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"head": "35446c03dfab5b0d8acfb6d5b89da5194a29491a",
"count": 84,
"objects": [
{
"path": "README.md",
"title": "README",
"id": null,
"type": null,
"role": null,
"representation": null,
"state": null,
"knowledge_schema": null,
"content_hash": "af7f5429c5001e690dd240cfb6498cd25d7a264318d10c79b669d2f041d87634",
"body": "# Trading-System — Modulare Architektur\n\nZentrales, modulares automatisiertes Trading-System, betrieben auf einem Hostinger-VPS (`187.124.31.123`).\n\n**Repository-Inhalt:** Live-Dokumentation der Infrastruktur, der Module und des aktuellen Betriebszustands.\nJede Änderung folgt dem **Notation-Format**: Autor (`Alice` / `Rain Ocampo`) + Zeitstempel (`DD.MM.YYYY HH:MM`) + Grund.\n\n---\n\n## Architektur-Überblick\n\n- **19 Module** (`Modul-01-PostgreSQL` … `Modul-18-Position-Manager`, `Modul-26-Telegram-Gateway`), orchestriert über `docker-compose.yml` unter `/opt/trading-modules/`.\n- **Netzwerk:** `trading-modules` (bridge). Kommunikation ausschließlich über **Docker-interne Service-Hostnamen** (keine festen IPs).\n- **Feste Host-Ports:** Modul-01=`55432`, Modul-02=`55672`+`15672`, Modul-0317=`55003``55017`.\n- **Zugangsdaten:** nur über Environment Variables / Docker-Secrets (Defaults in Compose mit `${VAR:-default}`).\n- **Kernprinzip:** bestehende Container/Volumes/Daten nie löschen, keine unnötigen öffentlichen Ports öffnen, Ist-Zustand vor Änderung prüfen.\n\n### Multi-Agent-Setup (Resident-Evil-Theme)\n| Agent | Rolle | Port |\n|-------|-------|------|\n| **Alice** (OpenClaw) | Primary | 55163 |\n| **Matt Addison** (OpenClaw) | Backup | 54524 |\n| **Rain Ocampo** (Hermes) | Technischer Support | 32776 (UI) |\n\n---\n\n## Module\n\n| Modul | Name | Status | Port |\n|-------|------|--------|------|\n| 01 | PostgreSQL | ✅ healthy (postgres:16-alpine) | 55432 |\n| 02 | RabbitMQ | ✅ healthy (rabbitmq:3-management) | 55672 / 15672 |\n| 03 | Market-Data | ✅ healthy (market-data:0.1.0) | 55003 (intern) |\n| 04 | Market-Regime | ✅ healthy (market-regime:1.0.0) | 55004 (intern) |\n| 05 | Strategy-Engine | ✅ healthy (strategy-engine:0.1.0) | 55005 (intern) |\n| 06 | Signal-Ranking | ✅ FREIGEGEBEN (signal-ranking:0.1.0) | 55006 (intern) |\n| 07 | Risk-Manager | ✅ FREIGEGEBEN (risk-manager:0.1.0) | 55007 (intern) |\n| 08 | Portfolio-Manager | ✅ FREIGEGEBEN (portfolio-manager:0.1.0) | 55008 (intern) |\n| 09 | Execution-Service | ✅ FREIGEGEBEN (execution-service:0.1.0) | 55009 (intern) |\n| 10 | Trade-Journal | ✅ FREIGEGEBEN (trade-journal:0.1.0) | 55010 (intern) |\n| 11 | Analytics | ✅ FREIGEGEBEN (analytics-service:0.1.0) | 55011 (intern) |\n| 12 | Backtesting | ✅ FREIGEGEBEN (backtesting:0.1.2) | 55012 (intern) |\n| 13 | Optimization | ✅ FREIGEGEBEN (optimization:0.1.0) | 55013 (intern) |\n| 14 | Notification | ✅ FREIGEGEBEN (notification:0.1.0) | 55014 (intern) |\n| 15 | Monitoring-Control | ✅ FREIGEGEBEN (monitoring-control:0.1.0) | 55015 (intern) |\n| 16 | Paperclip | ✅ FREIGEGEBEN (paperclip:0.1.0) | 55016 (intern) |\n| 17 | Hermes-Agent | ✅ FREIGEGEBEN (hermes-agent:0.1.0) | 55017 (intern) |\n| 18 | Position-Manager | ✅ FREIGEGEBEN (position-manager:0.1.0) | 55018 (intern) |\n| 26 | Telegram-Gateway | ✅ FREIGEGEBEN (telegram-gateway:0.1.0) | 55026 (intern) |\n\nDetails: siehe `modul-03-market-data.md` … `modul-18-position-manager.md` (Module 0318\nvollständig implementiert und freigegeben) + `modul-26-telegram-gateway.md` (Telegram-Gateway).\n\n---\n\n## Notation / Audit-Trail\n\nJede Änderung wird mit folgender Zeile dokumentiert:\n\n```\nGeändert von: [Alice | Rain Ocampo]\nDatum: DD.MM.YYYY HH:MM\nGrund: <Kurzbeschreibung>\n```\n\n- **Alice** = Änderungen durch OpenClaw\n- **Rain Ocampo** = Änderungen durch Hermes\n\n---\n\n## Geändert\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Initiale Repository-Struktur und Modul-03-Dokumentation angelegt.\n```\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: README aktualisiert — Modul-04 und Modul-05 als healthy/freigegeben eingetragen (Ports intern), Details-Link ergänzt.\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: README-Modultabelle aktualisiert — Module 0612 als freigegeben eingetragen; Modul-12-Backtesting als FREIGEGEBEN markiert.\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: README-Modultabelle aktualisiert — Modul-12 auf backtesting:0.1.2 (V1.1), Modul-13 Optimization als E2E-VERIFIZIERT eingetragen.\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: README-Modultabelle aktualisiert — Modul-13 Optimization als FREIGEGEBEN markiert (Freigabe durch Nutzer 20.08.2026).\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: README-Modultabelle aktualisiert — Modul-14 Notification als FREIGEGEBEN eingetragen; Details-Link auf modul-14-notification.md erweitert.\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: README-Modultabelle aktualisiert — Modul-15 Monitoring-Control als FREIGEGEBEN eingetragen; Details-Link auf modul-15-monitoring-control.md erweitert.\n\nGeändert von: Rain Ocampo\nDatum: 21.08.2026\nGrund: 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.\n\nGeändert von: Rain Ocampo\nDatum: 21.08.2026\nGrund: README-Modultabelle aktualisiert — Modul-26 Telegram-Gateway als FREIGEGEBEN eingetragen (E2E 20/20, Prompt-/UX-Fix); Modulanzahl auf 19 erweitert.\n"
},
{
"path": "infrastructure-handbook.md",
"title": "infrastructure-handbook",
"id": "object/f4e559f5-403c-f241-be50-0962d8174644",
"type": "arch",
"role": "reference",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "b1d57fa36d39ce5da066af0412585b0a7f197bef0927112396fe04228be16d0b",
"body": "# Infrastruktur & Betriebs-Handbuch — Trading-System VPS\n\n> Stand: 20.08.2026 · VPS: `187.124.31.123`\n\n## Multi-Agent-Architektur (Resident-Evil-Theme)\n| Agent | Plattform | Rolle | Port |\n|-------|-----------|-------|------|\n| **Alice** | OpenClaw (primary) | Hauptagent, Protagonistin | 55163 |\n| **Matt Addison** | OpenClaw (backup) | Backup/Support | 54524 |\n| **Rain Ocampo** | Hermes (support) | Technischer Support | 32776 (UI) |\n\nNotation in Notion/Forgejo bei JEDER Änderung:\n```\nGeändert von: [Alice | Rain Ocampo]\nDatum: DD.MM.YYYY HH:MM\nGrund: …\n```\n\n## Container-Verwaltung (via SSH)\n```bash\nssh root@187.124.31.123\ndocker compose -f /opt/trading-modules/docker-compose.yml ps\ndocker compose -f /opt/trading-modules/docker-compose.yml up -d <service>\ndocker compose -f /opt/trading-modules/docker-compose.yml build <service>\ndocker compose -f /opt/trading-modules/docker-compose.yml down # nur bei explizitem Wunsch\n```\n\n## Trading-Module (17 Container)\n- Compose: `/opt/trading-modules/docker-compose.yml`\n- Container-Namen exakt: `Modul-01-PostgreSQL` … `Modul-17-Hermes-Agent`\n- Netzwerk: `trading-modules`\n- Modul-01 = PostgreSQL 16-alpine (Host 55432)\n- Modul-02 = RabbitMQ 3-management (Host 55672/15672, vhost `trading`)\n- Modul-03 = Market-Data (FastAPI, Port 55003 intern, **nicht öffentlich**)\n- Modul-0417 = alpine-Platzhalter (bis ausgebaut)\n\n## Wichtige Hinweise\n- **Coolify überschreibt Container-Configs bei Neustart.** Config-Änderungen an OpenClaw ausschließlich über Config-Dateien (nicht UI).\n- **Hot-Reload** OpenClaw: `kill -HUP 1` im Container.\n- **Ollama** braucht `OLLAMA_API_KEY`; interner Hostname `ollama-nb6d-ollama-1:11434`.\n- VPS-Authentifizierung: **nur Public-Key-Auth**.\n\n## Deleted Resources (VPS-Aufräumung 20.08.2026)\nFolgendes 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.\n\n---\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Infrastruktur-Handbuch für das Trading-System dokumentiert.\n```\n"
},
{
"path": "modul-03-market-data.md",
"title": "modul-03-market-data",
"id": "object/1761a726-55af-f97b-d509-84aa4577f102",
"type": "arch",
"role": "module",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "1f6749ad8dc03903452a265f56c512d2d78381a2e2300904378420354a519329",
"body": "# Modul-03-Market-Data — Betriebsdokumentation\n\n> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ In Betrieb (healthy)\n\n## Zweck\nZentrale Marktdatenquelle des Trading-Systems. Pipeline:\n`Marktdaten → Validierung → Normalisierung → PostgreSQL → RabbitMQ-Event`\nKeine Strategie, keine Orders, keine KI — nur zuverlässige Marktdaten.\n\n## Container\n| Attribut | Wert |\n|----------|------|\n| Name | `Modul-03-Market-Data` |\n| Image | `market-data:0.1.0` (lokal gebaut) |\n| Port | **55003** — **NUR intern** (`expose`, nicht öffentlich) |\n| Netzwerk | `trading-modules` (bridge) |\n| Build-Context | `/opt/trading-modules/modul03-market-data/` |\n| Restart | `unless-stopped` |\n| Healthcheck | ✅ `healthy` |\n\n## Deployment\n```bash\ncd /opt/trading-modules\ndocker compose build modul-03-market-data\ndocker compose up -d modul-03-market-data\n```\n\n## Environment Variables (Compose)\n| Variable | Wert (Default) | Zweck |\n|----------|---------------|-------|\n| `PG_HOST` | `Modul-01-PostgreSQL` | Docker-interner Servicename |\n| `PG_PORT` | `5432` | intern (Host: 55432) |\n| `PG_USER/PASSWORD/DB` | `trading` | aus `.env`-Defaults |\n| `RABBITMQ_HOST` | `Modul-02-RabbitMQ` | Docker-interner Servicename |\n| `RABBITMQ_PORT` | `5672` | intern (Host: 55672) |\n| `RABBITMQ_USER/PASSWORD/VHOST` | `trading` | **vhost `trading`** |\n| `DATA_PROVIDER` | `noop` | `noop` / `demo` / später Broker |\n| `LOG_LEVEL` | `INFO` | Strukturiertes Logging |\n\nKeine festen IPs — nur Docker-interne Hostnamen. Creds via Compose env + `${VAR:-default}`.\n\n## Datenbank (Modul-01-PostgreSQL)\nTabelle `public.ohlcv`:\n```sql\nsymbol TEXT, asset_class TEXT, provider TEXT, timeframe TEXT,\nts TIMESTAMPTZ, open/high/low/close DOUBLE PRECISION, volume DOUBLE PRECISION,\ncreated_at TIMESTAMPTZ DEFAULT now(), id BIGSERIAL PRIMARY KEY\n```\n\n### Finale Constraints & Indizes (Stand 20.08.2026)\n| Index | Typ |\n|-------|-----|\n| `uq_ohlcv_provider_symbol_tf_ts` | **UNIQUE** `(provider, symbol, timeframe, ts)` |\n| `ohlcv_pkey` | UNIQUE `(id)` |\n| `idx_ohlcv_symbol_tf` | `(symbol, timeframe)` |\n| `idx_ohlcv_symbol_tf_ts` | `(symbol, timeframe, ts DESC)` |\n| `idx_ohlcv_provider_symbol_tf_ts` | `(provider, symbol, timeframe, ts DESC)` |\n\nDer Unique-Index inkludiert den **Provider** — langfristig werden mehrere Provider/Broker unterstützt\n(gleiche Symbol+Timeframe+Timestamp können von verschiedenen Quellen kommen).\n\n**Migrationen:** `/app/migrations/001_ohlcv.sql` + `002_unique_provider.sql`\n(idempotent, löschen nichts; automatisch via `ensure_schema()`/glob angewendet).\n\n## RabbitMQ (Modul-02)\n- **Exchange:** `market.data` (topic, durable)\n- **vhost:** `trading` (wichtig!)\n- **Routing-Keys:**\n\n| Routing-Key | Event-Typ | Wann |\n|-------------|-----------|------|\n| `market.data.ready` | `MARKET_DATA_READY` | **Batch/Import** — EIN Event pro Batch, `candle_count` + `batch:true` |\n| `market.data.candle.closed` | `MARKET_CANDLE_CLOSED` | **Live** — pro abgeschlossener Kerze EIN Event, OHLCV im payload |\n\n- Eventschema v1.0: `event_id, event_type, event_version, timestamp, symbol, asset_class, timeframe, provider` + payload.\n\n## Interne API\n| Endpoint | Zweck |\n|----------|-------|\n| `GET /health` | Liveness (200 immer) + Komponentenstatus im Body |\n| `GET /health/ready` | Readiness (503 wenn PG/RabbitMQ down) |\n| `POST /ingest` | Kerzen einspeisen (JSON: `{\"candles\":[...]}`) |\n| `GET /prices/{symbol}` | Letzte Kurse |\n| `GET /history/{symbol}?timeframe=` | Historische OHLCV |\n\n## Provider-Adapter\n`app/providers/providers.py`:\n- `DataProvider` (ABC) — abstrakte Schnittstelle `fetch_ohlcv()`, `health()`\n- `NoopProvider` — keine Datenquelle konfiguriert (Default)\n- `DemoProvider` — synthetische OHLCV-Daten für Tests\n- Neue Broker = neue Klasse, umschalten via `DATA_PROVIDER` env → keine harte Anbieter-Kopplung\n\n## Validierung (`app/validation/validator.py`)\n- Timestamp gültig (UTC, nicht Zukunft, nicht zu alt/stale)\n- OHLC-Werte > 0\n- High ≥ Low, High ≥ Open/Close, Low ≤ Open/Close\n- Duplikat-Erkennung (Storage + DB-Unique-Index)\n\n## End-to-End-Test (20.08.2026, nach Provider-Constraint-Upgrade) ✅\n- **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.\n- **Live-Pfad:** 1 AMZN-Candle → `MARKET_CANDLE_CLOSED` (routing `market.data.candle.closed`, OHLCV im payload).\n- **Provider-Duplikat:** identische NVDA-Kerze erneut → `duplicate`, kein Insert, kein Event.\n- **Port-Sicherheit:** 55003 von außen (`http://187.124.31.123:55003/health`) → **nicht erreichbar** ✅\n- Datenbestand final: AAPL 5, GOOG 4, NVDA 3, EURUSD 3, TSLA 4, MSFT 1, AMZN 1.\n\n## Offene Punkte\n- Echter Broker-/Datenprovider-Adapter (Interface bereit, noop/demo Defaults)\n- Zusätzliche Daten (Bid/Ask/Spread, Ticks, Fundamentaldaten) — vorbereitet\n- Consumer für `market.data.ready` / `market.data.candle.closed` (Modul-04 ff.)\n\n---\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-03-Dokumentation angelegt (Provider-Constraint, Event-Trennung, Port-Nicht-Exposition).\n```\n"
},
{
"path": "modul-04-market-regime.md",
"title": "modul-04-market-regime",
"id": "object/6fca3e3a-70b2-0927-d7fb-e650a8f0e7f8",
"type": "arch",
"role": "module",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "8c5616c357449d446d6b16d657201fe1fee205f6e5e42a6722ac4a1b5f87843f",
"body": "# Modul-04-Market-Regime — Betriebsdokumentation\n\n> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ Freigegeben (E2E bestanden)\n\n## Zweck\nErster **Consumer** der Market-Data-Events von Modul-03. Pipeline:\n`MARKET_DATA_READY / MARKET_CANDLE_CLOSED (Modul-03) → Regime-Berechnung → PostgreSQL (market_regime) → MARKET_REGIME_READY`\n**Deterministische, regelbasierte Engine — bewusst OHNE KI/ML.**\n\n## Container\n| Attribut | Wert |\n|----------|------|\n| Name | `Modul-04-Market-Regime` |\n| Image | `market-regime:1.0.0` (lokal gebaut) |\n| Port | **55004** — **NUR intern** (`expose`, nicht öffentlich) |\n| Netzwerk | `trading-modules` (bridge) |\n| Build-Context | `/opt/trading-modules/modul04-market-regime/` |\n| Restart | `unless-stopped` |\n| Healthcheck | ✅ `healthy` |\n\n## Deployment\n```bash\ncd /opt/trading-modules\ndocker compose build modul-04-market-regime\ndocker compose up -d --no-deps --force-recreate modul-04-market-regime\n```\n\n## Environment Variables (Compose)\n| Variable | Wert (Default) | Zweck |\n|----------|---------------|-------|\n| `PG_HOST` | `Modul-01-PostgreSQL` | Docker-interner Servicename |\n| `PG_PORT` | `5432` | intern (Host: 55432) |\n| `PG_USER/PASSWORD/DB` | `trading` | aus `.env`-Defaults |\n| `RABBITMQ_HOST` | `Modul-02-RabbitMQ` | Docker-interner Servicename |\n| `RABBITMQ_PORT` | `5672` | intern (Host: 55672) |\n| `RABBITMQ_USER/PASSWORD/VHOST` | `trading` | **vhost `trading`** |\n| `LOG_LEVEL` | `INFO` | Strukturiertes Logging |\n\nKeine festen IPs — nur Docker-interne Hostnamen. Creds via Compose env + Defaults.\n\n## Architektur\n```\nModul-03 ──market.data.ready / market.data.candle.closed──▶ RegimeConsumer\n │ (bindet beide Routing-Keys)\n ▼\n RegimeEngine (deterministisch)\n EMA / ADX / ATR / Slope / Preisstruktur\n │\n ┌───────────┴───────────┐\n ▼ ▼\n market_regime (PG) MARKET_REGIME_READY\n (16 Spalten) → market.regime.ready\n```\n\n- **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).\n- **Engine** (`app/regime/engine.py`): deterministisch, ohne KI.\n- **Storage** (`app/storage/storage.py`): idempotent via partiellen Unique-Index.\n- **Publisher** (`app/publisher/publisher.py`): publiziert `MARKET_REGIME_READY` auf `market.regime` (Routing `market.regime.ready`).\n- **History-Client** (`app/marketdata/client.py`): liest OHLCV über interne FastAPI Modul-03 (`http://Modul-03-Market-Data:55003/history/{symbol}`).\n\n## Regime-Engine (`app/regime/engine.py`)\nDeterministische Regel-Engine (Version `1.0.0`), 7 Regime:\n`TREND_UP, TREND_DOWN, RANGE, HIGH_VOLATILITY, LOW_VOLATILITY, TRANSITION, UNKNOWN`\n\n**Indikatoren & Metriken:**\n| Indikator | Fenster/Param | Zweck |\n|-----------|---------------|-------|\n| EMA fast/slow | 10 / 30 | Trendrichtung (EMA-Flanken-Differenz) |\n| ADX | 14 | Trendstärke (≥20 = echter Trend) |\n| ATR | 14 | Volatilität (absolut + Ratio + Perzentil) |\n| Slope | 20 | normierte Steigung der Close-Linie |\n| Preisstruktur | — | higher_highs / lower_lows / range |\n\n**Zentrale Schwellenwerte** (`app/config.py`, env-overridable):\n| Parameter | Default | Bedeutung |\n|-----------|---------|-----------|\n| `min_candles_required` | 30 | UNKNOWN, wenn weniger Daten |\n| `regime_lookback` | 60 | max. Kerzen für Berechnung |\n| `trend_min_ema_gap` | 0.02 | |EMA_fast-EMA_slow|/close ≥ → Trend |\n| `adx_trend_threshold` | 20.0 | ADX ≥ → echter Trend |\n| `slope_up/down_threshold` | 0.05 / -0.05 | normierte Steigung |\n| `range_atr_ratio` | 0.02 | ATR/close darunter = Range |\n| `atr_high_vol_multiplier` | 1.5 | ATR jetzt > hist_mean × → HIGH_VOL |\n| `atr_low_vol_multiplier` | 0.6 | ATR jetzt < hist_mean × → LOW_VOL |\n| `high_vol_atr_ratio` | 0.03 | ATR/close ≥ → starke Vol |\n| `low_vol_atr_ratio` | 0.008 | ATR/close ≤ → geringe Vol |\n| `slope_threshold` | 0.01 | |Slope| darunter = seitwärts |\n| `transition_min_events` | 3 | Events für TRANSITION |\n\n## Datenbank (Modul-01-PostgreSQL)\nTabelle `public.market_regime` (16 Spalten, eigene Tabelle — bestehende unangetastet):\n```sql\nsymbol TEXT, asset_class TEXT, timeframe TEXT, provider TEXT,\nregime TEXT, confidence INTEGER (0-100),\ntrend_strength DOUBLE PRECISION, volatility_state TEXT,\ntimestamp TIMESTAMPTZ, indicators_json JSONB,\ncandles_used INTEGER, version TEXT,\nsource_event_id TEXT, correlation_id TEXT,\ndata_ts TIMESTAMPTZ, data_ts_end TIMESTAMPTZ\n```\n- **Unique (partiell):** `uq_market_regime_src` auf `(symbol, timeframe, source_event_id)` **WHERE source_event_id IS NOT NULL** → Idempotenz.\n- Migration: `migrations/001_market_regime.sql` (idempotent, löscht nichts).\n\n## RabbitMQ (Modul-02)\n| Exchange | Typ | Routing | Event |\n|----------|-----|---------|-------|\n| `market.data` | topic | `market.data.ready` (eingang) | MARKET_DATA_READY |\n| `market.data` | topic | `market.data.candle.closed` (eingang) | MARKET_CANDLE_CLOSED |\n| `market.regime` | topic | `market.regime.ready` (**ausgang**) | MARKET_REGIME_READY |\n\n## Interne API\n| Endpoint | Zweck |\n|----------|-------|\n| `GET /health` | Liveness (200 immer) + Komponentenstatus im Body |\n| `GET /health/ready` | Readiness (503 wenn PG/RabbitMQ/Modul-03 down) |\n| `GET /regime/{symbol}` | Regime-Einträge abfragen |\n| `GET /regime/latest/{symbol}` | Letztes Regime eines Symbols |\n\n## End-to-End-Test (20.08.2026, final, nach Rebuild) ✅\nKette verifiziert: Modul-03 Ingest → `market.data.ready` → Modul-04 Consumer → RegimeEngine → `market_regime` → `MARKET_REGIME_READY` auf `market.regime.ready`.\n\n| Fall | Regime | Conf | candles | version | Event | DB |\n|------|--------|------|---------|---------|-------|----|\n| M4TREND_UP (40) | TREND_UP | 100 | 40 | 1.0.0 | genau 1 | ✅ |\n| M4TREND_DN (40) | TREND_DOWN | 100 | 40 | 1.0.0 | genau 1 | ✅ |\n| M4RANGE (40) | LOW_VOLATILITY (Range) | 60 | 40 | 1.0.0 | genau 1 | ✅ |\n| M4HIGHVOL (40) | HIGH_VOLATILITY | 75 | 40 | 1.0.0 | genau 1 | ✅ |\n| M4UNKNOWN (5) | UNKNOWN | 20 | 5 | 1.0.0 | genau 1 | ✅ |\n| M4IDEMPOT (40) | TREND_UP | 100 | 40 | 1.0.0 | genau 1 | ✅ |\n\n**Idempotenz:** dasselbe Quell-Event (`source_event_id`) erneut → **kein zweiter Datensatz**, kein Doppel-Event. ✅\n**Logs:** keine Errors/Tracebacks. Health `{postgresql:true, rabbitmq:true, market_data_ready:true}`. ✅\n\n## Bugs behoben während E2E (20.08.2026)\n| Bug | Fix |\n|-----|-----|\n| `can't adapt type 'dict'` (JSONB) | `json.dumps(ind.model_dump(mode=\"json\"))` |\n| `tuple index out of range` (16/15) | `version` in INSERT-VALUES ergänzt |\n| `ON CONFLICT` + partieller Index Fehler | `WHERE source_event_id IS NOT NULL` in Klausel |\n| `model_dump(default=...)` TypeError | `default`-Kwarg entfernt (`model_dump(mode=\"json\")`) |\n| Consumer-Verbindungs-Leak | `conn.close()` bei Reconnect → kein Message-Leak |\n\n## Offene Punkte\n- Consumer-Downstream für `market.regime.ready` (Modul-05+)\n- Bestätigte TRANSITION-Detektion mit echten Folgedaten\n\n---\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-04-Dokumentation angelegt (Regime-Engine, market_regime-Schema, Events, E2E freigegeben).\n```\n"
},
{
"path": "modul-05-strategy-engine.md",
"title": "modul-05-strategy-engine",
"id": "object/0aef122a-790b-b17a-9176-cc32df98c5a1",
"type": "arch",
"role": "module",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "b8b6574fac45df82c5e7585a095746b5d28c66496aba831f780dde32f4a01502",
"body": "# Modul-05-Strategy-Engine — Betriebsdokumentation\n\n> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ Freigegeben (E2E bestanden)\n\n## Zweck\n**Strategie-Engine** — konsumiert `MARKET_REGIME_READY` (Modul-04) + OHLCV (Modul-03), berechnet deterministische Handelssignale und persistiert sie.\nPipeline: `MARKET_REGIME_READY (Modul-04) → Strategie-Engine (trend_pullback_v1) → PostgreSQL (strategy_signal) → SIGNAL_DETECTED`\n**Deterministische, regelbasierte Engine — bewusst OHNE KI/ML, ohne Ranking, ohne Risk Management, ohne Order-Ausführung.**\n\n## Container\n| Attribut | Wert |\n|----------|------|\n| Name | `Modul-05-Strategy-Engine` |\n| Image | `strategy-engine:0.1.0` (lokal gebaut) |\n| Port | **55005** — **NUR intern** (`expose`, nicht öffentlich) |\n| Netzwerk | `trading-modules` (bridge) |\n| Build-Context | `/opt/trading-modules/modul05-strategy-engine/` |\n| Restart | `unless-stopped` |\n| Healthcheck | ✅ `healthy` |\n\n## Deployment\n```bash\ncd /opt/trading-modules\ndocker compose build modul-05-strategy-engine\ndocker compose up -d --no-deps --force-recreate modul-05-strategy-engine\n```\n\n## Environment Variables (Compose)\n| Variable | Wert (Default) | Zweck |\n|----------|---------------|-------|\n| `PG_HOST` | `Modul-01-PostgreSQL` | Docker-interner Servicename |\n| `PG_PORT` | `5432` | intern |\n| `PG_USER/PASSWORD/DB` | `trading` | aus `.env`-Defaults |\n| `RABBITMQ_HOST` | `Modul-02-RabbitMQ` | Docker-interner Servicename |\n| `RABBITMQ_PORT` | `5672` | intern |\n| `RABBITMQ_USER/PASSWORD/VHOST` | `trading` | **vhost `trading`** |\n| `LOG_LEVEL` | `INFO` | Strukturiertes Logging |\n\nKeine festen IPs — nur Docker-interne Hostnamen. Creds via Compose env + Defaults.\n\n## Architektur\n```\nModul-04 ──market.regime.ready──▶ StrategyConsumer (strategy.input)\n │\n ▼\n OHLCV (Modul-03, intern 55003/history/{symbol})\n │\n ▼\n StrategyEngine (deterministisch)\n trend_pullback_v1 (LONG/SHORT)\n │\n ┌───────────┴───────────┐\n ▼ ▼\n strategy_signal (PG) SIGNAL_DETECTED\n (eigene Tabelle) → market.signals / strategy.signal.detected\n```\n\n- **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).\n- **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`.\n- **Engine/Register** (`app/engine.py`): modular — Strategien via `.name/.version/.evaluate()` registriert.\n- **Storage** (`app/storage/storage.py`): idempotent via partiellen Unique-Index.\n- **Publisher** (`app/publisher/publisher.py`): **frische Verbindung je Publish** + `conn.close()` im finally (verhindert `ConnectionResetError` durch RabbitMQ-Closed-Verbindungen).\n- **Service** (`app/core/service.py`): Pipeline Event → OHLCV → Strategie → speichern + publizieren; robuste Payload-Extraktion.\n\n## Strategie V1 — `trend_pullback_v1` (`app/strategies/trend_pullback_v1.py`)\nDeterministische Pullback-Strategie. **Nur abgeschlossene Candles, kein Lookahead-Bias.**\n\n**LONG-Bedingungen (Regime TREND_UP):**\n1. Close > steigender SMA200 (Trendfilter)\n2. Pullback: Close < EMA20 (Zug zurück in den Trend)\n3. Bestätigung: Close > prev Close ODER Break prev High\n4. Entry = Close; Stop unter Swing-Low; Target = 2R (R:R = 2.0)\n\n**SHORT-Bedingungen (Regime TREND_DOWN):** spiegelbildlich\n1. Close < fallender SMA200\n2. Pullback: Close > EMA20\n3. Bestätigung: Close < prev Close ODER Break prev Low\n4. Stop über Swing-High; Target = 2R\n\n**Mathematik (vom E2E verifiziert):**\n- **LONG:** `Target = Entry + 2 × (Entry - Stop)`; `R:R = 2.0`; Stop < Entry\n- **SHORT:** `Target = Entry - 2 × (Stop - Entry)`; `R:R = 2.0`; Stop > Entry\n\n**Zentrale Schwellenwerte** (`app/config.py`, env-overridable):\n| Parameter | Default | Bedeutung |\n|-----------|---------|-----------|\n| `lookback` | 300 | max. Kerzen zur Berechnung |\n| `min_candles_required` | 220 | UNKNOWN, wenn < 220 (SMA200 braucht 200) |\n| `sma_period` | 200 | Trendfilter SMA |\n| `ema_period` | 20 | Pullback-EMA |\n| `risk_reward` | 2.0 | Target-Multiplikator (2R) |\n| `swing_lookback` | 10 | Swing-Low/High-Fenster für Stop |\n\nKein gültiges Setup → **kein Event** publiziert.\n\n## Datenbank (Modul-01-PostgreSQL)\nEigene Tabelle `public.strategy_signal` (bestehende unangetastet):\n```sql\nsignal_id UUID, source_event_id TEXT, correlation_id TEXT,\ntimestamp TIMESTAMPTZ, symbol TEXT, asset_class TEXT, provider TEXT,\ntimeframe TEXT, strategy_name TEXT, strategy_version TEXT,\ndirection TEXT (LONG/SHORT), regime TEXT,\nentry DOUBLE PRECISION, stop_loss DOUBLE PRECISION, target DOUBLE PRECISION,\nrisk_reward DOUBLE PRECISION, setup_metrics JSONB, trigger_reason TEXT\n```\n- **Unique (partiell):** `uq_strategy_signal_src` auf `(symbol, timeframe, source_event_id)` **WHERE source_event_id IS NOT NULL** → Idempotenz.\n- Migration: `migrations/001_strategy_signal.sql` (idempotent, löscht nichts).\n\n## RabbitMQ (Modul-02)\n| Exchange | Typ | Routing | Event |\n|----------|-----|---------|-------|\n| `market.regime` | topic | `market.regime.ready` (eingang) | MARKET_REGIME_READY |\n| `market.signals` | topic | `strategy.signal.detected` (ausgang) | SIGNAL_DETECTED |\n\n## Interne API\n| Endpoint | Zweck |\n|----------|-------|\n| `GET /health` | Liveness (200) + Komponentenstatus im Body |\n| `GET /health/ready` | Readiness (503 wenn PG/RabbitMQ/Modul-03 down) |\n\n## End-to-End-Test (20.08.2026, final, nach Publisher-Fixes) ✅\nKette verifiziert: Modul-03 Ingest → `market.data.ready` → Modul-04 → `market.regime.ready` → Modul-05 Consumer → `trend_pullback_v1` → `strategy_signal` → `SIGNAL_DETECTED`.\n\n| Fall | Signal | Entry | Stop | Target | R:R | DB | Event |\n|------|--------|-------|------|--------|-----|----|-------|\n| M5LONG (TREND_UP) | **LONG** | 164.20 | 163.4764 | 165.6473 | **2.00** | ✅ 1 | ✅ 1 |\n| M5SHORT (TREND_DOWN) | **SHORT** | 135.80 | 136.4964 | 134.4073 | **2.00** | ✅ 1 | ✅ 1 |\n| M5NOPULL (kein Pullback) | keins | — | — | — | — | ✅ 0 | ✅ 0 |\n| M5RANGE (Range) | keins | — | — | — | — | ✅ 0 | ✅ 0 |\n\n**Mathematik verifiziert:** LONG `Target=Entry+2×(Entry-Stop)` = 164.20 + 2×0.72364 = **165.6473** ✓; SHORT `Target=Entry2×(StopEntry)` = 135.80 2×0.69636 = **134.4073** ✓.\n**Idempotenz:** identisches `source_event_id` erneut → **kein zweiter DB-Eintrag, kein Doppel-Event** (count=1). ✅\n**RabbitMQ-Reconnect:** kontrollierter Neustart → Modul-04+05 verbinden automatisch (Backoff 1s→2s→4s→8s), **je exakt 1 Consumer, keine Zombies, keine verlorenen Events.** ✅\n**Logs:** keine `ConnectionResetError`, keine unbehandelten Tracebacks. Health `{postgresql:true, rabbitmq:true, market_data_ready:true}`. ✅\n\n## Bugs behoben während E2E (20.08.2026)\n| Bug | Fix |\n|-----|-----|\n| `ConnectionResetError` beim Publish | Publisher: **frische Verbindung je Publish** + `conn.close()` (RabbitMQ schließt ungenutzte Verbindung) — **gleicher Bug in Modul-03, -04, -05** |\n| Modul-03 OHLCV-Feld `ts` | Normalisierung `ts`→`timestamp` im MarketData-Client |\n| Zombie-Consumer (`strategy.input` 23) | `queue_delete` beim Consumer-Start; manuelles Cleanup via `rabbitmqctl delete_queue` |\n| Keine Events nach Recreate | OHLCV/Regime-Daten waren noch in DB (Duplikat) → E2E bereinigt `ohlcv`+`market_regime`+`strategy_signal` |\n| M5SHORT ging verloren | Zombie-Consumer verschluckte Event (Round-Robin) → beseitigt |\n\n## Offene Punkte\n- Downstream-Consumer für `strategy.signal.detected` (Modul-06+)\n- Weitere Strategien über Engine-Register hinzufügbar\n\n---\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-05-Dokumentation angelegt (Strategy-Engine trend_pullback_v1, strategy_signal-Schema, Events, E2E freigegeben).\n```\n"
},
{
"path": "modul-06-signal-ranking.md",
"title": "modul-06-signal-ranking",
"id": "object/2b4c4170-b5b2-8a2b-fdee-8ea5eff191b5",
"type": "arch",
"role": "module",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "edc2bfeba911261721a72e9d6be8ba40c5b97c03b92c2bedcc1d1845e13d23ac",
"body": "# Modul-06-Signal-Ranking — Betriebsdokumentation\n\n> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ **Freigegeben** (E2E+Idempotenz+Reconnect+Doku grün)\n\n## Zweck\n**Signal-Ranking** — konsumiert `SIGNAL_DETECTED` (Modul-05) + OHLCV (Modul-03), bewertet die Signalqualität deterministisch (Score 0100, vollständig nachvollziehbar und konfigurierbar), persistiert das Ergebnis und publiziert `SIGNAL_RANKED`.\nPipeline: `SIGNAL_DETECTED (Modul-05) → Signal-Ranking (signal_quality_v1) → PostgreSQL (signal_ranking) → SIGNAL_RANKED`\n**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.\n\n## Container\n| Attribut | Wert |\n|----------|------|\n| Name | `Modul-06-Signal-Ranking` |\n| Image | `signal-ranking:0.1.0` (lokal gebaut) |\n| Port | **55006** — **NUR intern** (`expose`, nicht öffentlich) |\n| Netzwerk | `trading-modules` (bridge) |\n| Build-Context | `/opt/trading-modules/modul06-signal-ranking/` |\n| Restart | `unless-stopped` |\n| Healthcheck | ✅ `healthy` |\n\n## Deployment\n```bash\ncd /opt/trading-modules\ndocker compose build modul-06-signal-ranking\ndocker compose up -d --no-deps --force-recreate modul-06-signal-ranking\n```\n\n## Environment Variables (Compose)\n| Variable | Wert (Default) | Zweck |\n|----------|---------------|-------|\n| `PG_HOST` | `Modul-01-PostgreSQL` | Docker-interner Servicename |\n| `PG_PORT` | `5432` | intern |\n| `PG_USER/PASSWORD/DB` | `trading` | aus `.env`-Defaults |\n| `RABBITMQ_HOST` | `Modul-02-RabbitMQ` | Docker-interner Servicename |\n| `RABBITMQ_PORT` | `5672` | intern |\n| `RABBITMQ_USER/PASSWORD/VHOST` | `trading` | **vhost `trading`** |\n| `LOG_LEVEL` | `INFO` | Strukturiertes Logging |\n\nKeine festen IPs — nur Docker-interne Hostnamen. Creds via Compose env + Defaults.\n\n## Architektur\n```\nModul-05 ──signal.detected──▶ RankingConsumer (ranking.input)\n │\n ▼\n OHLCV (Modul-03, intern 55003/history/{symbol})\n │\n ▼\n RankingEngine (deterministisch, Registry)\n signal_quality_v1 (9 Komponenten, Score 0100)\n │\n ┌───────────┴───────────┐\n ▼ ▼\n signal_ranking (PG) SIGNAL_RANKED\n (eigene Tabelle) → market.rankings / signal.ranked\n```\n\n- **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).\n- **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.\n- **Engine/Register** (`app/ranking/engine.py`): modular — Ranking-Engines via `.name/.version/rank()` registriert; KI/ML später ergänzbar.\n- **Storage** (`app/storage/storage.py`): idempotent via partiellen Unique-Index auf `source_signal_id`.\n- **Publisher** (`app/publisher/publisher.py`): **frische Verbindung je Publish** + `conn.close()` im finally (verhindert `ConnectionResetError` durch RabbitMQ-Closed-Verbindungen).\n- **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.\n\n## Ranking-Engine V1 — `signal_quality_v1` (`app/ranking/signal_quality_v1.py`)\nDeterministische Qualitätsbewertung. **9 Komponenten, Score 0100, vollständig nachvollziehbar und konfigurierbar.** Jede Komponente liefert 0..max; Summe = `total_score`. Komponenten werden einzeln in der DB gespeichert (`comp_*`).\n\n| Komponente | Max | Bewertet |\n|-----------|-----|----------|\n| `regime` | 20 | Market-Regime-Qualität / Confidence |\n| `trend` | 15 | Trendstärke |\n| `pullback` | 15 | Pullback-Qualität |\n| `trigger` | 10 | Entry-Bestätigung |\n| `rr` | 10 | Chance/Risiko-Verhältnis |\n| `volatility` | 10 | Volatilitätszustand |\n| `distance` | 10 | Distanz Entry→Stop |\n| `data_quality` | 5 | Datenqualität / Candle-Anzahl |\n| `consistency` | 5 | Setup-Konsistenz |\n\n**Klassifizierung:** `total_score >= eligible_threshold` → `eligible`; darunter `weak` (wird trotzdem gespeichert).\n\n**Zentrale Schwellenwerte** (`app/config.py`, env-overridable):\n| Parameter | Default | Bedeutung |\n|-----------|---------|-----------|\n| `max_score` | 100 | Score-Obergrenze |\n| `eligible_threshold` | 70 | Score ≥ 70 → `eligible` |\n| `ranking_lookback` | 500 | max. Kerzen für Trend/Volatilität (Modul-03 liefert die N ältesten, daher hoch) |\n| `min_candles_required` | 60 | unter dieser Zahl → Datenqualität-Punktabzug |\n| `full_candles_target` | 250 | ab dieser Zahl → volle Datenqualität |\n| `pullback_depth_min/max` | 0.5 / 4.0 | Abstand Entry zu Trend-Referenz in ATR |\n| `vol_ideal_lo/hi` | 0.005 / 0.030 | ATR %-Fenster für \"ideale\" Volatilität |\n\nKeine Trade-Freigabe — nur Qualitätsbewertung.\n\n## Datenbank (Modul-01-PostgreSQL)\nEigene Tabelle `public.signal_ranking` (bestehende unangetastet):\n```sql\nranking_id UUID, source_signal_id TEXT, source_event_id TEXT, correlation_id TEXT,\ntimestamp TIMESTAMPTZ, symbol TEXT, asset_class TEXT, provider TEXT, timeframe TEXT,\nranking_name TEXT, ranking_version TEXT, ranking_class TEXT (eligible/weak),\ntotal_score DOUBLE PRECISION, comp_regime/comp_trend/comp_pullback/comp_trigger/comp_rr/\ncomp_volatility/comp_distance/comp_data_quality/comp_consistency DOUBLE PRECISION,\ndetails JSONB, reasons JSONB\n```\n- **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).\n- Migration: `migrations/001_signal_ranking.sql` (idempotent, löscht nichts).\n\n## RabbitMQ (Modul-02)\n| Exchange | Typ | Routing | Event |\n|----------|-----|---------|-------|\n| `market.signals` | topic | `strategy.signal.detected` (eingang) | SIGNAL_DETECTED |\n| `market.rankings` | topic | `signal.ranked` (ausgang) | SIGNAL_RANKED |\n\n## Interne API\n| Endpoint | Zweck |\n|----------|-------|\n| `GET /health` | Liveness (200) + Komponentenstatus im Body |\n| `GET /health/ready` | Readiness (503 wenn PG/RabbitMQ/Modul-03 down) |\n| `GET /rankings/{symbol}` | Rankings eines Symbols |\n| `GET /rankings/latest/{symbol}` | Neuestes Ranking eines Symbols |\n| `GET /ranking-engines` | Registrierte Ranking-Engines |\n\n## End-to-End-Test (20.08.2026, final) ✅\nKette 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`.\n\n| Fall | Ranking | Score | Class | DB | SIGNAL_RANKED |\n|------|---------|-------|-------|----|---------------|\n| M5LONG (TREND_UP) | **eligible** | **72.2** | eligible | ✅ 1 | ✅ 1 |\n| M5SHORT (TREND_DOWN) | **eligible** | **73.6** | eligible | ✅ 1 | ✅ 1 |\n| M5NOPULL (kein Pullback) | — | — | — | ✅ 0 | ✅ 0 |\n| M5RANGE (Range) | — | — | — | ✅ 0 | ✅ 0 |\n\n**Verifizierte Eigenschaften:**\n- Gültiger LONG/SHORT → **genau 1** `SIGNAL_RANKED`; NOPULL/RANGE → **0** Ranking + 0 Event. ✅\n- DB-Eintrag je gültigem Signal vorhanden; `source_signal_id` korrekt übernommen (z.B. `effd782c...`, `42dd2042...`). ✅\n- `total_score` immer 0100; **Summe aller Komponenten == total_score**. ✅\n- `eligible`/`weak` korrekt gemäß Threshold (72.2/73.6 ≥ 70 → eligible). ✅\n- **Idempotenz:** identisches `SIGNAL_DETECTED` erneut → **kein zweiter DB-Eintrag, kein zweites SIGNAL_RANKED** (count=1). ✅\n- **RabbitMQ-Reconnect:** kontrollierter Neustart → Modul-04/05/06 verbinden automatisch (Backoff 1s→2s→4s→8s), **je exakt 1 Consumer, keine Zombies, keine verlorenen Events.** ✅\n- **Logs:** keine unbehandelten Tracebacks nach Reconnect. Health/Readiness `{postgresql:true, rabbitmq:true, market_data:true, consumer_ready:true}`. ✅\n\n## Bugs behoben während E2E (20.08.2026)\n| Bug | Fix |\n|-----|-----|\n| `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 |\n| 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) |\n| `direction`-Spalte existierte nicht | E2E-Query auf `details`/Modell angepasst |\n\n## Offene Punkte\n- Downstream-Consumer für `signal.ranked` (Modul-07+ — noch NICHT begonnen)\n- KI/ML-Ranking-Engine über Registry ergänzbar\n- Trade-Freigabe später durch Risk Manager (noch nicht gebaut)\n\n---\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-06-Dokumentation angelegt (Signal-Ranking signal_quality_v1, signal_ranking-Schema, Events, E2E final grün).\n```\n"
},
{
"path": "modul-07-risk-manager.md",
"title": "modul-07-risk-manager",
"id": "object/645f8ff7-5a1a-bcf3-8dfe-0a8cfdf15dbc",
"type": "arch",
"role": "module",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "9288284d15d3c2bfa6d557b068bd074cf2d9dd22100efccaae102060dccece3d",
"body": "# Modul-07: Risk-Manager\n\n**Status:** ✅ **Freigegeben** (20.08.2026)\n**Version:** 0.1.0\n**Port (intern):** 55007\n**Netzwerk:** `trading-modules-trading-modules`\n\n---\n\n## Zweck\n\nDeterministischer Risk-Manager in der Trading-Kette. Konsumiert `SIGNAL_RANKED`\n(aus Modul-06), führt eine **harte, deterministische Risikoprüfung** durch, berechnet\ndie Positionsgröße und publiziert `RISK_APPROVED` oder `RISK_REJECTED`.\n\n**Prinzipien:**\n- **KEINE KI/ML** — rein deterministische Regeln.\n- **KEINE Broker-Order** — nur Risiko-Entscheidung + Positionsgröße.\n- **FAIL-CLOSED** — Fehler oder fehlende Daten führen **niemals** zu APPROVED,\n sondern immer zu REJECTED.\n\n---\n\n## Architektur\n\n```\nSIGNAL_RANKED (market.rankings / signal.ranked)\n │\n ▼\n┌─────────────────────────────────────────────┐\n│ Modul-07 Risk-Manager │\n│ Consumer (risk.input) │\n│ → RiskEngine (Registry) │\n│ → risk_v1 (deterministische Prüfung) │\n│ → Storage (risk_decision, idempotent) │\n│ → Publisher (market.risk) │\n└─────────────────────────────────────────────┘\n │\n ├── RISK_APPROVED (risk.approved)\n └── RISK_REJECTED (risk.rejected)\n```\n\n### Komponenten\n- `app/consumer/consumer.py` — RabbitMQ-Consumer `SIGNAL_RANKED`, manuelles ACK,\n Zombie-Schutz (`queue_delete` beim Start), Reconnect-Backoff 1s→2s→4s→8s.\n- `app/risk/engine.py` — Registry für Risiko-Verfahren (V1, zukünftig V2).\n- `app/risk/risk_v1.py` — deterministische Kernlogik + Positionsgrößen-Mathematik.\n- `app/instruments/registry.py` — instrument-agnostische Werte (Contract Size,\n Lot Size, Tick Size/Value, Min/Max Quantity, Quantity Step, Währung/FX).\n- `app/storage/storage.py` — idempotente Persistenz (unique auf `source_ranking_id`).\n- `app/publisher/publisher.py` — frische Verbindung je Publish + `conn.close()`\n im `finally` (RabbitMQ-Bug-Fix aus Modul-03/04/05/06).\n- `app/api/main.py` — FastAPI, Port 55007 intern, `/health`, `/health/ready`.\n\n---\n\n## V1-Risikoregeln (zentral konfigurierbar in `config.py`, env-overridable)\n\n| Regel | Default | Beschreibung |\n|---|---|---|\n| `account_equity` | 10000 | Account-Equity |\n| `risk_percent` | 0.005 | max. Risiko pro Trade (0,5 %) |\n| `min_score` | 70 | Mindest-Ranking-Score |\n| `min_ranking_class` | eligible | Mindest-Ranking-Klasse |\n| `max_positions` | 5 | max. Anzahl Positionen (nur bei vorhandenen Daten) |\n| `max_open_risk` | 0.02 | max. gesamtes offenes Risiko (nur bei vorhandenen Daten) |\n| `daily_loss_limit` | 0.03 | tägliches Verlustlimit (nur bei vorhandenen Daten) |\n| `max_drawdown` | 0.20 | maximales Drawdown-Limit (nur bei vorhandenen Daten) |\n\n> **Hinweis:** Globale Portfolio-Limits (offene Positionen, Tagesverlust, Drawdown)\n> werden **nur geprüft, soweit Daten vorhanden sind**. Die eigentliche\n> Portfolio-Logik gehört zu **Modul-08**. Fehlende Portfolio-Daten sind **kein**\n> REJECT-Grund.\n\n### Positionsgröße\n```\nrisk_amount = equity × risk_percent\nrisk_per_unit = abs(entry stop)\nraw_size = risk_amount / risk_per_unit\nquantity = floor(raw_size / quantity_step) × quantity_step # ABRUNDEN\n```\nAbrunden auf `quantity_step` (nie aufrunden), damit das erlaubte Risiko nie\nüberschritten wird (FAIL-CLOSED-Sicherheit).\n\n---\n\n## Output (RiskDecision)\n\nNachvollziehbar gespeichert in `risk_decision`:\n`risk_decision_id`, `source_ranking_id`, `symbol`, `asset_class`, `direction`,\n`entry`, `stop_loss`, `target`, `equity`, `risk_percent`, `max_risk_amount`,\n`calculated_position_size`, `actual_risk_amount`, `decision` (APPROVED/REJECTED),\n`reason_codes`, `rule_version`, `timestamp`.\n\n---\n\n## RabbitMQ\n\n| Rolle | Exchange | Routing Key | Queue |\n|---|---|---|---|\n| Consumer | `market.rankings` | `signal.ranked` | `risk.input` |\n| Publisher | `market.risk` | `risk.approved` | — |\n| Publisher | `market.risk` | `risk.rejected` | — |\n\n- Manuelles ACK erst nach erfolgreicher Verarbeitung.\n- Idempotenz: gleiches `source_ranking_id` → keine Doppelentscheidung/Event.\n- Reconnect ohne Zombies (Backoff 1s→2s→4s→8s, `queue_delete` beim Start).\n\n---\n\n## Tests\n\n### Unit-Tests (`test_risk.py`) — 10/10 grün\n1. gültiger LONG → APPROVED + korrekte Größe\n2. gültiger SHORT → APPROVED\n3. Score unter Threshold → REJECTED\n4. falscher Stop → REJECTED\n5. fehlende/ungültige Daten → REJECTED\n6. Positionsgröße/Risiko mathematisch korrekt\n7. Quantity-Step/Min/Max korrekt\n8. Daily-Loss/Drawdown-Limit → REJECTED\n9. Idempotenz\n10. RabbitMQ-Reconnect\n\n### E2E (03→04→05→06→07) — ALLE CHECKS BESTANDEN\n- **A:** Kette 03→07, Decision in DB (LONG & SHORT) ✓\n- **B:** exakt 1 Risiko-Event je Symbol (APPROVED/REJECTED) ✓\n- **C:** Pflichtfelder in Decision vorhanden ✓\n- **D:** identisches Ranking → kein Doppel-Decision ✓\n- **E:** Score<70 / falscher Stop / fehlende Daten → REJECTED + source_ranking_id ✓\n\n### Mathematik-Verifikation (aus DB)\n| Symbol | Direction | Entry | Stop | equity | risk% | max_risk | qty | actual_risk |\n|---|---|---|---|---|---|---|---|---|\n| M5LONG | LONG | 164.2 | 163.47636 | 10000 | 0.005 | 50 | 69 | 49.93 |\n| M5SHORT | SHORT | 135.8 | 136.49636 | 10000 | 0.005 | 50 | 71 | 49.44 |\n\nBeide `actual_risk_amount ≤ max_risk_amount` ✓\n\n### Reconnect-Test\nRabbitMQ kontrolliert neu gestartet → alle 4 Queues (`market-regime.input`,\n`strategy.input`, `ranking.input`, `risk.input`) mit **exakt 1 Consumer**,\nkeine Zombies, keine verlorenen Events, `risk_decision` stabil.\n\n---\n\n## Health/Readiness\n\n- `/health` → `{\"status\":\"ok\", \"postgresql\":true, \"rabbitmq\":true, \"consumer_ready\":true}`\n- `/health/ready` → 200\n- Container: `Modul-07-Risk-Manager` (healthy)\n\n---\n\n## Technischer offener Punkt: Modul-03 `/history?limit=N`\n\n**Modul-03 `/history?limit=N` liefert aktuell die N ältesten Candles (aufsteigend).**\nDas ist langfristig zu korrigieren (sollte die N **neuesten** Candles liefern).\nDer Workaround in Modul-06 (Lookback 500) ist **nicht als API-Vertrag** zu\nbehandeln — er ist ein temporärer Workaround, kein garantiertes Verhalten.\n\n---\n\n## Deployment\n\n- Compose: `/opt/trading-modules/docker-compose.yml` (Block `modul-07-risk-manager`)\n- Nur `expose: 55007` (kein öffentlicher Port)\n- `restart: unless-stopped`\n- Image: `risk-manager:0.1.0`\n"
},
{
"path": "modul-08-portfolio-manager.md",
"title": "modul-08-portfolio-manager",
"id": "object/603e6b87-115a-e258-5bfb-7d94d6d78c97",
"type": "arch",
"role": "module",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "40027abbffd1babb3b400dfeab833aaac18e28d6443fe67999617ac4d6d11e59",
"body": "# Modul-08: Portfolio-Manager\n\n**Status: FREIGEGEBEN** (20.08.2026)\n\nDeterministischer Portfolio-Manager. Konsumiert `RISK_APPROVED` aus Modul-07,\nprüft das Gesamtportfolio gegen zentrale Limits und publiziert\n`TRADE_APPROVED` oder `PORTFOLIO_REJECTED`.\n\n**Keine KI/ML. Keine Broker-Order. FAIL-CLOSED.**\n\n---\n\n## 1. Rolle in der Kette\n\n```\nModul-03 Ingest → 04 Regime → 05 Signal → 06 Ranking\n → SIGNAL_RANKED (market.rankings/signal.ranked)\n → Modul-07 Risk-Check → RISK_APPROVED (market.risk/risk.approved)\n → Modul-08 Portfolio-Check → portfolio_decision-Tabelle\n → TRADE_APPROVED / PORTFOLIO_REJECTED (market.portfolio)\n```\n\nModul-08 prüft **NICHT erneut** die Positionsgrößenlogik aus Modul-07,\nsondern ausschließlich **Portfolio-Risiken**.\n\n---\n\n## 2. Events\n\n| Richtung | Exchange | Routing-Key | Event |\n|----------|----------|-------------|-------|\n| Input (Consumer) | `market.risk` | `risk.approved` | `RISK_APPROVED` |\n| Output (Publisher) | `market.portfolio` | `trade.approved` | `TRADE_APPROVED` |\n| Output (Publisher) | `market.portfolio` | `portfolio.rejected` | `PORTFOLIO_REJECTED` |\n\nConsumer-Queue: `portfolio.input` (durable, manuelles Ack, prefetch=1).\n\n---\n\n## 3. V1 Portfolio-Regeln (zentral konfigurierbar, env-overridable)\n\n| Regel | Config-Feld | Default |\n|-------|-------------|---------|\n| max. Anzahl offener Positionen | `max_positions` | 5 |\n| max. gesamtes Open Risk % | `max_open_risk_percent` | 0.05 (5%) |\n| max. Exposure pro Position % | `max_exposure_per_position_percent` | 0.20 |\n| max. Exposure pro Symbol % | `max_exposure_per_symbol_percent` | 0.30 |\n| max. Exposure pro Assetklasse % | `max_exposure_per_asset_class_percent` | 0.50 |\n| max. Long-Exposure % | `max_long_exposure_percent` | 0.60 |\n| max. Short-Exposure % | `max_short_exposure_percent` | 0.60 |\n| Duplikat Symbol/Strategie erlauben | `allow_duplicate_symbol_strategy` | false |\n| Korrelation anwenden | `apply_correlation` | false (vorbereitet) |\n| Account Equity | `account_equity` | 10000.0 |\n\nAlle Limits sind Prozentwerte des `account_equity`. Bei `account_equity <= 0`\n→ FAIL-CLOSED `PORTFOLIO_REJECTED`.\n\n**Später geplant** (V2): Sector/Country/Currency-Exposure, Korrelation\n(erst wenn verlässliche Daten vorhanden).\n\n---\n\n## 4. FAIL-CLOSED\n\nJede Exception, fehlende oder ungültige kritische Eingabe führt zu\n`PORTFOLIO_REJECTED`, **niemals** zu `TRADE_APPROVED`. Fehlende kritische\nFelder (quantity, risk, entry) → `PORTFOLIO_REJECTED` mit Reason-Codes.\n\n---\n\n## 5. Portfoliozustand (autoritative Quelle)\n\nDer Portfoliozustand wird **NICHT nur aus Events angenommen**. Die\nArchitektur trennt die Quelle des Zustands vom Entscheidungs-Code:\n\n- `app/portfolio/state.py` definiert `PortfolioStateProvider` (Interface)\n und `PortfolioState` / `OpenPosition`.\n- V1-Provider liest offene Positionen aus der eigenen `portfolio_decision`-\n Tabelle (alle `TRADE_APPROVED`-Entscheidungen).\n- **Später** kann ein Broker-/Execution-Provider als autoritative Quelle\n ergänzt werden, ohne den Entscheidungs-Code zu ändern.\n\n---\n\n## 6. Race-Condition-Schutz (atomar)\n\nZwei nahezu gleichzeitige `RISK_APPROVED` dürfen **nicht** beide auf demselben\nalten Portfoliozustand genehmigt werden und dadurch Limits überschreiten.\n\nLösung (`app/storage/storage.py` → `evaluate_and_save_atomic`):\n1. **Advisory Lock** (`pg_advisory_xact_lock`) serialisiert parallele\n Portfolio-Entscheidungen.\n2. Innerhalb **einer Transaktion**: Zustand lesen → Engine bewerten →\n speichern → Commit.\n3. Der zweite Prozess wartet auf den Lock und bewertet auf dem **aktualisierten**\n Zustand (inkl. der ersten Entscheidung).\n\n---\n\n## 7. Idempotenz\n\n- Unique-Index `uq_portfolio_decision_src` auf `source_risk_decision_id`\n (partial, nur wo nicht NULL).\n- Gleiches `source_risk_decision_id` → keine Doppelentscheidung/Event.\n- Zusätzlich prüft der Service vor der Verarbeitung, ob die Quelle bereits\n entschieden wurde (Skip + Ack).\n\n---\n\n## 8. DB-Tabelle `portfolio_decision`\n\nJede Entscheidung wird gespeichert — **auch `PORTFOLIO_REJECTED`**.\n\n| Spalte | Typ | Beschreibung |\n|--------|-----|--------------|\n| `id` | BIGSERIAL PK | |\n| `portfolio_decision_id` | TEXT | eigene Entscheidungs-ID |\n| `source_risk_decision_id` | TEXT | id des RISK_APPROVED (Modul-07) |\n| `source_ranking_id` | TEXT | durchgereichtes Ranking (Modul-06) |\n| `source_signal_id` | TEXT | durchgereichtes Signal (Modul-05) |\n| `source_event_id` | TEXT | |\n| `correlation_id` | TEXT | |\n| `timestamp` | TIMESTAMPTZ | Entscheidungszeitpunkt (UTC) |\n| `symbol` | TEXT | |\n| `asset_class` | TEXT | |\n| `strategy` | TEXT | |\n| `direction` | TEXT | LONG / SHORT |\n| `timeframe` | TEXT | |\n| `proposed_quantity` | DOUBLE | aus RISK_APPROVED |\n| `proposed_risk` | DOUBLE | actual_risk_amount |\n| `proposed_exposure` | DOUBLE | qty × entry |\n| `current_portfolio_risk` | DOUBLE | offenes Risiko VOR |\n| `new_portfolio_risk` | DOUBLE | offenes Risiko NACH |\n| `exposure_before` | DOUBLE | Gesamt-Exposure VOR |\n| `exposure_after` | DOUBLE | Gesamt-Exposure NACH |\n| `decision` | TEXT | TRADE_APPROVED / PORTFOLIO_REJECTED |\n| `reason_codes` | TEXT[] | z. B. `{OK}` oder `{EXPOSURE_PER_POSITION_EXCEEDED,...}` |\n| `rule_version` | TEXT | `portfolio_v1@1.0.0` |\n| `details` | JSONB | Begründung + Zwischenwerte |\n| `portfolio_snapshot` | JSONB | Portfoliozustand zum Zeitpunkt |\n| `created_at` | TIMESTAMPTZ | |\n\n---\n\n## 9. Reason-Codes\n\n| Code | Bedeutung |\n|------|-----------|\n| `OK` | alle Limits eingehalten → TRADE_APPROVED |\n| `MAX_POSITIONS_EXCEEDED` | max. offene Positionen erreicht |\n| `OPEN_RISK_EXCEEDED` | gesamtes Open Risk überschritten |\n| `EXPOSURE_PER_POSITION_EXCEEDED` | Exposure pro Position überschritten |\n| `EXPOSURE_PER_SYMBOL_EXCEEDED` | Exposure pro Symbol überschritten |\n| `EXPOSURE_PER_ASSET_CLASS_EXCEEDED` | Exposure pro Assetklasse überschritten |\n| `LONG_EXPOSURE_EXCEEDED` | Long-Exposure überschritten |\n| `SHORT_EXPOSURE_EXCEEDED` | Short-Exposure überschritten |\n| `DUPLICATE_SYMBOL_STRATEGY` | doppelte Position Symbol/Strategie |\n| `QUANTITY_MISSING` / `RISK_AMOUNT_MISSING` / `ENTRY_MISSING` | fehlende kritische Daten (FAIL-CLOSED) |\n| `EQUITY_MISSING` | account_equity fehlt/ungültig (FAIL-CLOSED) |\n\n---\n\n## 10. Architektur\n\n```\napp/\n config.py # zentrale Konfiguration (env-overridable)\n core/\n models.py # PortfolioDecision + Event-Modelle\n service.py # Orchestrator (Consumer→Engine→Storage→Publisher)\n portfolio/\n engine.py # Registry der Portfolio-Verfahren\n portfolio_v1.py # deterministische V1-Prüfung (FAIL-CLOSED)\n state.py # PortfolioState-Provider (autoritative Quelle)\n storage/\n storage.py # idempotent + atomare Race-Condition-Lösung\n publisher/\n publisher.py # frische Verbindung je Publish + close\n consumer/\n consumer.py # manuelles Ack, Reconnect-Backoff, Zombie-Schutz\n api/\n main.py # FastAPI (Health/Readiness/Decisions/State)\nmigrations/\n 001_portfolio_decision.sql\nDockerfile\nrequirements.txt\ntest_portfolio.py # 11 Unit-Tests (Engine-Logik)\n```\n\n---\n\n## 11. Tests\n\n### Unit-Tests (`test_portfolio.py`, 11/11 grün)\n1. erstes gültiges Portfolio-Trade → TRADE_APPROVED\n2. max Positions überschritten → PORTFOLIO_REJECTED\n3. max Open Risk überschritten → PORTFOLIO_REJECTED\n4. Symbol-Exposure überschritten → PORTFOLIO_REJECTED\n5. Long/Short-Exposure-Limit → PORTFOLIO_REJECTED\n6. Duplicate Symbol/Strategie → PORTFOLIO_REJECTED\n7. fehlender Portfoliozustand → FAIL-CLOSED\n8. Idempotenz (deterministisch)\n9. Assetklassen-Exposure → PORTFOLIO_REJECTED\n10. Exposure pro Position → PORTFOLIO_REJECTED\n11. Short-Exposure → PORTFOLIO_REJECTED\n\n### E2E (Kette 03→04→05→06→07→08, alle Checks grün)\n- **A**: Kette 03→08, Portfolio-Decision in DB (LONG & SHORT)\n- **B**: exakt 1 Portfolio-Event je Symbol (TRADE_APPROVED/PORTFOLIO_REJECTED)\n- **C**: Pflichtfelder in Portfolio-Decision vorhanden\n- **D**: Idempotenz (identisches RISK_APPROVED → kein Doppel-Decision)\n- **E**: parallele RISK_APPROVED → beide verarbeitet, keine Race-Verlust\n- **F**: erstes gültiges Portfolio-Trade → TRADE_APPROVED\n\n### Reconnect-Test\nRabbitMQ gestoppt → Consumer erkennt Ausfall, Reconnect-Backoff (1s→2s→4s→8s).\nRabbitMQ gestartet → Consumer verbindet sich neu, alle 5 Queues haben exakt\n1 Consumer, Health/Readiness 200.\n\n---\n\n## 12. Health / Readiness\n\n- `GET /health` → Liveness (immer 200, Komponentenstatus im Body)\n- `GET /health/ready` → Readiness (200 nur wenn PG + RabbitMQ erreichbar)\n- `GET /decisions/{symbol}` → gespeicherte Entscheidungen (inkl. REJECTED)\n- `GET /portfolio-rules` → verfügbare Portfolio-Regeln\n- `GET /portfolio/state` → aktueller Portfoliozustand\n\nPort: **55008** (nur Docker-intern, kein öffentlicher Host-Port).\n\n---\n\n## 13. Deploy\n\n- Compose-Service: `modul-08-portfolio-manager`\n- Image: `portfolio-manager:0.1.0`\n- Container: `Modul-08-Portfolio-Manager`\n- Netz: `trading-modules`\n- `restart: unless-stopped`\n- Env: PG + RabbitMQ-Zugangsdaten (aus Compose)\n\n---\n\n## 14. Wichtige Fixes (20.08.2026)\n\n**Consumer-Bug (alle Module 0408):** `process_data_events(time_limit=None)`\nverarbeitete nur EIN Event und baute danach die Verbindung neu auf. Der\nerneute `_connect()` führte `queue_delete` aus und löschte wartende Events\n(z. B. ein zweites, nahezu gleichzeitiges RISK_APPROVED) unwiederbringlich.\nFix: innere Schleife `while not stop: process_data_events(time_limit=1.0)`\nhält die Verbindung am Leben. Betroffen und gefixt: Modul-04, 05, 06, 07, 08.\n"
},
{
"path": "modul-09-execution-service.md",
"title": "modul-09-execution-service",
"id": "object/48bd264f-607b-15f1-5f73-3e922af9b19d",
"type": "arch",
"role": "module",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "c185dd59f21f8fed84eca4831242fb5566615fdabd53c5d9638f746ddc8d0b6b",
"body": "# Modul-09: Execution-Service\n\n**Status: FREIGEGEBEN** (20.08.2026)\n\nDeterministischer Execution-Service. Konsumiert `TRADE_APPROVED` aus Modul-08,\nvalidiert die Order hart (FAIL-CLOSED), übergibt sie an einen Broker-Adapter\n(V1: PaperBrokerAdapter) und überwacht den Status. Ergebnis wird idempotent\nin eigenen DB-Tabellen gespeichert und als RabbitMQ-Events publiziert.\n\n**Sicherheit: KEIN unkontrolliertes Live-Trading.**\n- Default `EXECUTION_MODE=PAPER` (bzw. DRY_RUN)\n- `TRADING_ENABLED=false` als sicherer Default für LIVE\n- LIVE nur über explizite Konfiguration aktivierbar, FAIL-CLOSED\n- Bei Timeout nach Order-Senden NIE blind erneut senden — erst über\n `broker_order_id`/`client_order_id` bzw. Brokerstatus klären\n\n## Architektur\n\n```\nTRADE_APPROVED (market.portfolio/trade.approved, Modul-08)\n │\n ▼\nExecutionConsumer (execution.input, Consumer-Fix von Anfang an)\n │\n ▼\nOrder-Validierung (hart, FAIL-CLOSED)\n │ source_portfolio_decision_id gültig, decision=APPROVED,\n │ symbol/direction/quantity vorhanden, quantity>0,\n │ entry/stop/target plausibel, Kette nachvollziehbar,\n │ kein bereits ausgeführter identischer Auftrag, Kill-Switch\n ▼\nBrokerAdapter-Interface (modular, austauschbar)\n │ V1: PaperBrokerAdapter (realistischer Lifecycle)\n ▼\nExecutionStorage (idempotent, Advisory Lock + Transaktion)\n │ execution_order + execution_event\n ▼\nRabbitMQPublisher (market.execution)\n ORDER_SUBMITTED / ORDER_FILLED / ORDER_REJECTED / ORDER_FAILED\n```\n\n## BrokerAdapter-Interface\n\n- `BrokerAdapter` (app/broker/base.py): `name()`, `is_configured()`,\n `submit_order()`, `get_order_status()`\n- `PaperBrokerAdapter` (app/broker/paper.py): V1, simuliert realistischen\n Lifecycle (SUBMITTED → FILLED, Partial Fill, Reject, Timeout) für Tests\n- `IgDemoAdapter` (app/broker/ig_demo.py): IG-Markets-Demo-Broker (IG_DEMO-Modus),\n echter externer Broker-Adapter. Liest/schreibt über IG REST API (demo-api.ig.com).\n- `BrokerRegistry` (app/broker/registry.py): wählt Adapter anhand\n `DEFAULT_BROKER` (V1: paper). Echte Broker-Adapter später austauschbar,\n keine Brokerlogik im Core-Code.\n\n## IG-Demo-Adapter (IG_DEMO-Modus, 21.08.2026)\n\n**Status: FREIGEGEBEN / PRODUKTIV VERIFIZIERT** — erster echter externer Broker-Adapter.\n\n- **Eigener Modus `IG_DEMO`** (nicht LIVE+Gate); PAPER/IG_DEMO/LIVE strikt getrennt.\n- **Demo-Basis-URL erzwungen** (`demo-api.ig.com`), LIVE-Endpunkt (`api.ig.com`)\n technisch blockiert, kein IG_DEMO→LIVE-Autowechsel.\n- **Credentials ausschließlich VPS-Secret/ENV** (`.env.ig`, chmod 600/root-only);\n nie in Code/Forgejo/Tolaria/DB/Logs.\n- **Mengen instrumentabhängig** über `broker_mapping` (PostgreSQL); kein globales 1:1.\n- **Epic-Mapping produktiv in PostgreSQL `broker_mapping`**; Forgejo nur Schema/Doku/Beispiele.\n\n### Order-Aktions-Auswertung (CLOSE-Pfad-Fix, 21.08.2026)\n`BrokerOrderRequest` trägt `action_type` (Default `OPEN`) + `broker_order_id`.\nDer Service reicht beides durch; der Adapter wertet `action_type` aus:\n\n| action_type | IG-Pfad |\n|-------------|---------|\n| `OPEN` / `INCREASE` | `POST /positions/otc` (create_position) |\n| `CLOSE` / `REDUCE` | `POST /positions/otc` mit `_method:DELETE` (close_position) |\n| unbekannt/ungültig | **FAIL-CLOSED** (`IG_UNSUPPORTED_ACTION`) |\n\n**CLOSE-Direction korrekt umkehren:** CLOSE einer LONG/BUY-Position muss mit\n**SELL** erfolgen (Gegenseite), nicht mit der Positionsrichtung. `_close_direction()`:\nLONG→SELL, SHORT→BUY.\n\n**broker_order_id/dealId sauber durchreichen:** `broker_order_id` wird vom Service\nin die Order übernommen und an den Adapter durchgereicht; IG `dealId` wird als\n`broker_order_id` persistiert.\n\n**Unbekannte/ungültige Aktionen FAIL-CLOSED:** `_derive_action_type()` setzt\nunbekannte Aktionen NICHT mehr auf OPEN zurück, sondern reicht sie durch →\nControl blockt mit `UNKNOWN_ACTION_TYPE`, Adapter mit `IG_UNSUPPORTED_ACTION`.\n\n### IG-Adapter-Tests (6/6 grün)\n1. `test_unsupported_action_fail_closed` — unbekannte Aktion → FAIL-CLOSED\n2. `test_close_direction_inverted` — CLOSE einer BUY-Position → SELL\n3. `test_open_uses_create_position` — OPEN → create_position\n4. `test_close_uses_close_position` — CLOSE → close_position mit `_method:DELETE`\n5. `test_broker_order_id_passthrough` — broker_order_id/dealId durchgereicht\n6. `test_fail_closed_without_credentials` — ohne Credentials → nicht konfiguriert\n\n### Erster erfolgreicher IG-DEMO OPEN→CLOSE-E2E (21.08.2026)\n- **OPEN** `IGE2E-US500-OPEN-1787292734` → dealId `DIAAAAYB46KMNAE`, size 1.0, BUY,\n openLevel 7652.58 → **FILLED**\n- **CLOSE-Bug gefunden + gefixt:** erster CLOSE sendete fälschlich als zweite OPEN\n (action_type ignoriert) + falsche direction (BUY statt SELL)\n- **CLOSE2** `IGE2E-US500-CLOSE2-1787293768` für `DIAAAAYB463S4AB` → **FILLED**\n (filled_quantity 1, avg_fill_price 7652.19)\n- **IG GET /positions: Anzahl 0** — alle Positionen geschlossen\n- **Safety-Reset:** M09 zurück auf PAPER (`EXECUTION_MODE=PAPER`,\n `TRADING_ENABLED=false`, `DEFAULT_BROKER=paper`)\n\n## Idempotenz\n\n- Unique-Index `uq_execution_order_src` auf `source_portfolio_decision_id`\n → gleiches TRADE_APPROVED erzeugt NIE eine Doppelorder\n- Unique-Index `uq_execution_order_client` auf `client_order_id`\n- Client Order ID deterministisch aus Execution-ID abgeleitet\n (`EXEC-<execution_id[:32]>`)\n- Advisory Lock + Transaktion: parallele identische Events → exakt eine\n Execution (Race-Condition-Schutz, wie Modul-08)\n- ACK eines TRADE_APPROVED erst, wenn die Order dauerhaft in der DB gespeichert ist\n\n## Order-Status\n\n`PENDING → SUBMITTED → PARTIALLY_FILLED → FILLED`\nsowie `REJECTED`, `CANCELLED`, `FAILED`.\n\n## DB-Tabellen\n\n- `execution_order`: execution_id, source_portfolio_decision_id, broker, mode,\n symbol, asset_class, direction, quantity, requested_price, stop_loss, target,\n broker_order_id, client_order_id, status, filled_quantity, avg_fill_price,\n timestamps, error/reason_codes, execution_version\n- `execution_event`: append-only Log der Status-Übergänge\n\n## RabbitMQ\n\n- Exchange `market.execution` (topic, durable)\n- Routing: `order.submitted`, `order.filled`, `order.rejected`, `order.failed`\n- Queue `execution.input` (durable), gebunden an `market.portfolio`/`trade.approved`\n- Consumer-Fix aus Modul-0408 von Anfang an: dauerhafte Verbindung,\n `process_data_events(time_limit=1.0)` in innerer Schleife, KEIN queue_delete\n bei normalen Reconnects, Backoff 1s→2s→4s→8s→16s→30s\n\n## Tests\n\n- **31/31 Unit-Tests** (test_execution.py + test_ig_demo.py + test_control.py):\n gültige Order → PAPER FILLED, Idempotenz, ungültige Quantity → REJECTED,\n Kill-Switch, fehlende Broker-Credentials → FAIL-CLOSED, Broker-Reject,\n Timeout → keine blinde Doppelorder, Partial Fill, Reconnect-Backoff,\n parallele identische Events, Control-Gate (M15), IG-Adapter (6/6)\n- **E2E 6/6 Checks** (Kette 03→04→05→06→07→08→09): Execution-Order in DB,\n exakt 1 Paper-Order, Pflichtfelder, Idempotenz, ORDER_SUBMITTED+ORDER_FILLED,\n gültiger Trade → PAPER FILLED\n- **Reconnect-Test**: RabbitMQ gestoppt → Backoff → neu verbunden, 1 Consumer,\n kein queue_delete bei normalen Reconnects\n\n## Deploy\n\n- Container `Modul-09-Execution-Service`, Port 55009 nur Docker-intern\n (kein öffentlicher Host-Port)\n- Health/Readiness 200, PG + RabbitMQ erreichbar, Consumer bereit\n- Env: `EXECUTION_MODE=PAPER`, `TRADING_ENABLED=false`, `DEFAULT_BROKER=paper`\n\n## Geändert\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-09-Doku um final freigegebenen Control-Gate-Status (M15→M09, Variante C) ergänzt.\n Kernregel, Einbau-Punkt, Audit-Felder, Parameter, Live-E2E-Ergebnisse dokumentiert.\n```\n\n## Control-Gate (M15→M09, FREIGEGEBEN 20.08.2026)\n\n**Status: FREIGEGEBEN / PRODUKTIV VERIFIZIERT** — Variante C (Event-Cache-Vorfilter + synchroner HTTP-Final-Check).\n\nM15 (`Modul-15-Monitoring-Control`) ist die **autoritative Safety-/Trading-Control-Instanz**.\nM09 sendet neue Orders (OPEN/INCREASE) **nur**, wenn M15 eindeutig `ENABLED` + frisch bestätigt.\n\n### Kernregel\n| State | OPEN/INCREASE | REDUCE/CLOSE/CANCEL |\n|-------|---------------|---------------------|\n| `ENABLED` | **ERLAUBT** | erlaubt |\n| `PAUSED` | **VERBOTEN** | erlaubt |\n| `HALTED` | **VERBOTEN** | erlaubt |\n| `UNKNOWN` / M15-down / stale | **VERBOTEN (FAIL-CLOSED)** | erlaubt |\n\n### Einbau-Punkt\nControl-Check zwischen Schritt 4 (Order atomar in DB anlegen) und Schritt 5 (`_submit_and_track`).\nBei OPEN/INCREASE ohne Erlaubnis → Order als `FAILED`/`REJECTED` speichern, **nicht** senden.\n`TRADING_ENABLED`-Flag bleibt unverändert (blockiert nur LIVE, nicht PAPER).\n\n### Audit-Felder je Order\n`action_type`, `control_status`, `control_state_version`, `control_expires_at`,\n`control_audit_id`, `control_check_source` (`http`|`cache`|`bypass`|`none`).\n\n### Parameter\n- Final-Check-HTTP-Timeout: **500 ms**, Retry: **max 1** sofort.\n- `expires_at`-TTL (M15): **500 ms**; Cache-TTL (M09): **5 s**.\n- Kein Auto-Resume; kein \"Senden ohne Check\".\n\n### Live-E2E (VPS, 20.08.2026) — alle Szenarien grün\nS1 ENABLED 9/9 · S2 HALTED 9/9 · S3 PAUSED 4/4 · S4 Risikoabbau 4/4 · S5 M15-down 7/7 ·\nS6 Cache-vs-Final 2/2 · S7 Race/TTL 2/2 · S8 Event-Cache verifiziert · S9 Recovery 3/3 ·\nS10 Health/Security verifiziert · S11 Regression bestanden.\n\nDetails: siehe `modul-15-m09-anbindung-implementierung.md` (FREIGEGEBEN).\n"
},
{
"path": "modul-10-trade-journal.md",
"title": "modul-10-trade-journal",
"id": "object/0b393153-53e5-99fe-1098-e8bb4e0b6c7c",
"type": "journal",
"role": "module",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "67f1e591aa9ef4747c5d3f02f664570f5bdf447a95f8f656db32a3779a81c8db",
"body": "# Modul-10: Trade-Journal\n\n**Status: FREIGEGEBEN** (20.08.2026)\n\nDeterministisches, append-only Trade-Journal. Konsumiert die\nExecution-Events (`ORDER_SUBMITTED`/`ORDER_FILLED`/`ORDER_REJECTED`/`ORDER_FAILED`)\naus Modul-09, validiert die Herkunftskette hart (FAIL-CLOSED) und dokumentiert\njeden Trade in einer Audit-Trail-konformen, idempotenten Struktur.\nErgebnis wird als `TRADE_RECORDED`-Event publiziert.\n\n**Sicherheit & Audit:**\n- **Keine KI/ML, keine Orderausführung, keine Risikoberechnung** — reines Dokumentieren\n- **Append-only**: `trade_event_history` ist unveränderlich (nur INSERT, kein UPDATE/DELETE)\n- **Herkunftskette wird validiert** (Signal → Ranking → Risk → Portfolio → Execution)\n- Bei inkonsistenter Kette → Trade als `INCONSISTENT` markiert (FAIL-CLOSED), nicht stumm verworfen\n- `TRADE_RECORDED` optional: ACK nach persistenter Speicherung\n\n## Architektur\n\n```\nORDER_* (market.execution, Modul-09)\n │\n ▼\nJournalConsumer (journal.input, Consumer-Fix von Anfang an)\n │\n ▼\nHerkunfts-Validierung (FAIL-CLOSED)\n │ Kette 05→06→07→08→09 nachvollziehbar?\n │ provenance_consistent in metadata\n ▼\nJournalService\n │ erstes Event → Trade anlegen + TRADE_RECORDED\n │ Folge-Event → append-only Event + Status-Update (bestehenden Trade)\n ▼\nJournalStorage (idempotent, Unique-Index)\n │ trade_journal + trade_event_history\n ▼\nRabbitMQPublisher (market.journal)\n TRADE_RECORDED (trade.recorded)\n```\n\n## Service-/Storage-Grenze (vereinheitlicht)\n\n**Storage gibt IMMER `TradeJournal` zurück** — nie ein `dict`.\n- Einzige Konvertierungsstelle `_row_to_trade` (DB-Zeile → Modell)\n- `get_trade_by_execution()` → `Optional[TradeJournal]`\n- Der Service arbeitet ausschließlich mit Modellen (kein dict/Model-Mix je Codepfad)\n- Dadurch ist `trade_id` auf jedem Rückgabewert garantiert verfügbar\n- (Fix gegen `'dict' object has no attribute 'trade_id'` → NACK/Requeue)\n\n## Verarbeitung\n\n- **Erstes Event (ORDER_SUBMITTED)** → Trade anlegen, `TRADE_RECORDED` publizieren\n- **Folge-Event (ORDER_FILLED u.a.)** → bestehenden Trade als Status-Update\n verarbeiten: append-only Event + Status-Update, KEIN neuer Trade,\n **kein zweites TRADE_RECORDED**\n- Bei FILLED: `entry_filled`/`filled_at`/Status aktualisiert (Trade öffnen/aktualisieren)\n- Eingehende Events werden nicht doppelt verarbeitet (idempotent, Unique-Index)\n\n## Idempotenz\n\n- Unique-Index auf `(execution_id, event_id)` → jedes Event exakt einmal\n- `provenance_consistent` in `metadata` (jsonb), nicht als Spalte\n- `TRADE_RECORDED` nur beim ersten Event pro Trade\n- Kein Doppel-Journal bei Re-Publish desselben Events\n\n## DB-Tabellen\n\n- `trade_journal`: trade_id, execution_id, source_portfolio_decision_id,\n Herkunftsketten-IDs (signal/ranking/risk/portfolio), symbol, asset_class,\n strategy, direction, timeframe, entry_requested, entry_filled, stop_loss,\n target, quantity, risk_amount, execution_mode, broker_order_id, status,\n provenance_consistent (in metadata), created_at, updated_at\n- `trade_event_history`: execution_id, event_id, event_type, payload, created_at\n (append-only, unveränderlich)\n\n## RabbitMQ\n\n- Exchange `market.journal` (topic, durable)\n- Routing: `trade.recorded`\n- Queue `journal.input` (durable), gebunden an `market.execution`/`order.*`\n- Consumer-Fix von Anfang an: dauerhafte Verbindung,\n `process_data_events(time_limit=1.0)` in innerer Schleife, KEIN queue_delete\n bei normalen Reconnects, Backoff 1s→2s→4s→8s→16s→30s\n- ACK erst nach persistenter Speicherung; bei Fehler NACK/Requeue\n\n## Tests\n\n- **39/39 Unit-Tests** (test_journal.py): Anlage, Status-Update, Idempotenz,\n FAIL-CLOSED-Herkunftskette, append-only, exakt 1 TRADE_RECORDED, Reconnect\n- **Gezielter Fix-Test 9/9** (test_fix_update.py): SUBMITTED→Journal,\n FILLED→Update, Rückgabe ist TradeJournal (kein dict), `trade_id` vorhanden,\n genau 1 Trade, append-only (2 Events), idempotent, kein Doppel-Journal\n- **E2E 7/7 Checks** (Kette 03→10): Journal-Eintrag in DB, exakt 1 Trade,\n Pflichtfelder + Herkunftskette konsistent, append-only Event-Historie\n (SUBMITTED+FILLED), exakt 1 TRADE_RECORDED, Idempotenz nach Re-Publish\n- **Reconnect-Test**: RabbitMQ gestoppt → Backoff 1→2→4→8→16s → wieder\n verbunden, `journal.input` 1 Consumer, 0 Messages, kein Zombie\n- **Log-Check**: 0 ERROR, 0 Traceback, ACK auf ORDER_SUBMITTED + ORDER_FILLED\n\n## Deploy\n\n- Container `Modul-10-Trade-Journal`, Port 55010 nur Docker-intern\n (kein öffentlicher Host-Port)\n- Health/Readiness 200, PG + RabbitMQ erreichbar, Consumer bereit\n- Env: `EXECUTION_MODE=PAPER`, `TRADING_ENABLED=false`\n"
},
{
"path": "modul-11-analytics.md",
"title": "modul-11-analytics",
"id": "object/31cf1506-0ab6-5f7a-cecc-0e3a7a2e6572",
"type": "arch",
"role": "module",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "0651f72a1bbe416d8f11b6b5ea403185a7dccc5582f93978c73d43fb7f6bc6ee",
"body": "# Modul-11: Analytics\n\n**Status: FREIGEGEBEN** · Container `Modul-11-Analytics` · Image `analytics-service:0.1.0`\n\n## Zweck\nAuswertung 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.**\n\n## Architektur / Datenfluss\n\n```\nTRADE_RECORDED (market.journal / trade.recorded)\n │\n ▼\nModul-11-Analytics ── konsumiert über Queue `analytics.input`\n │\n ├── liest read-only → trade_journal (Modul-10, PostgreSQL)\n ├── schreibt → analytics_snapshot, strategy_performance, daily_performance\n └── publiziert (opt.)→ ANALYTICS_UPDATED (market.analytics / analytics.updated)\n\nAPI (Docker-intern, Port 55011):\n /health, /health/ready\n /analytics/portfolio Gesamt-/Portfolio-Performance\n /analytics/strategy je Strategie+Version\n /analytics/regime je Market Regime\n /analytics/period je Monat (ab optionalem from_date)\n /analytics/rebuild (POST) deterministischer Rebuild aus DB\n```\n\nPrimä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).\n\n## Datenquelle & Realisationslogik\n- **Nur `status = 'CLOSED'` UND `realized_pnl IS NOT NULL`** fließen in realisierte Performance ein.\n- **Offene Trades** (`OPEN`) werden separat ausgewiesen (`open_trade_count`) und zählen NICHT als realisierte Trades.\n- Gruppierungsfelder aus `trade_journal`: `strategy`, `strategy_version`, `symbol`, `asset_class`, `direction` (LONG/SHORT), `regime`, `signal_score`.\n- Zeitraum-Gruppierung nach `exit_time` (realisierter Trades), optional ab `from_date`.\n- **Keine Lookahead-/Survivorship-Tricks**: nur tatsächlich geschlossene Trades, deterministische Equity-Kurve.\n\n## Kennzahlen (V1)\n- Anzahl Trades (realisiert + offen getrennt)\n- Winrate / Lossrate\n- durchschnittlicher Gewinn / Verlust\n- durchschnittliches R (realized_r_multiple)\n- Expectancy (Währung + R)\n- Profit Factor (gross_profit / gross_loss)\n- Netto-PnL\n- Brutto-Gewinn / Brutto-Verlust\n- Max Drawdown (aus Equity-Kurve der realisierten PnL)\n- Durchschnittliche Haltedauer (entry_time→exit_time, Stunden)\n- Bester / schlechtester Trade\n\n## Eigene Tabellen (Migration `migrations/001_analytics.sql`)\n- `analytics_snapshot` — stabiler Snapshot der Gesamt-Performance (jeder Rebuild = neue Zeile, chronologisch append).\n- `strategy_performance` — Kennzahlen je Strategie+Version.\n- `daily_performance` — Tageskennzahlen (Backtesting/Reporting).\n\nDiese fassen KEINE bestehenden Tabellen an (read-only auf `trade_journal`).\n\n## RabbitMQ\n- **Queue** `analytics.input` (durable, topic `market.journal`, Routing `trade.recorded`), manuelles ACK, Reconnect mit Backoff (1→2→4→8→16s), kein Zombie.\n- **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.\n\n## Verifikation (20.08.2026)\n| Test | Ergebnis |\n|---|---|\n| 17 Pure Metriken (Gewinn/Verlust, Winrate, PF, Expectancy, Max DD, Gruppierung, offene≠realisierte) | **OK** |\n| 8 Idempotenz (identisches Event → keine Doppelzählung) | **OK** |\n| 9 Rebuild aus DB → identische Kennzahlen | **OK** |\n| 10 RabbitMQ-Reconnect (Backoff 4→8→16s, erneuter Connect, 1 Consumer) | **OK** |\n| E2E 03→11 (Seed realisierter Trades, Portfolio/Strategie/Regime, DB + Idempotenz) | **OK** |\n\n- Health/Ready: `200` Port 55011 (PostgreSQL ✓, RabbitMQ ✓, Consumer ready ✓).\n- Endzustand nach Freigabe: 2 echte OPEN-Trades im Journal, 0 realisierte → Snapshot zeigt 0/2.\n\n## Betriebshinweise\n- Kein öffentlicher Port (nur `expose: 55011`).\n- `restart: unless-stopped`.\n- Compose-Pfad VPS: `/opt/trading-modules/docker-compose.yml`.\n- Forgejo-Doku: `nexo312/trading-system-docs`, Commit-ID siehe unten.\n\n---\n*Geändert von: Rain Ocampo (Hermes)*\n*Datum: 20.08.2026* — Modul-11-Analytics, Status FREIGABEGEBEN.\n"
},
{
"path": "modul-12-backtesting.md",
"title": "modul-12-backtesting",
"id": "object/302e9929-e186-c930-2406-ad4a8f17c6fd",
"type": "arch",
"role": "module",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "92a009cada99012b6a087b73a4ab805bb379877c969646716b76fba369cc6586",
"body": "# Modul-12: Backtesting\n\n**Status: FREIGEGEBEN** · Container `Modul-12-Backtesting` · Image `backtesting:0.1.2` (V1 + V1.1 parallel)\n\n## Zweck\nDeterministische, reproduzierbare **historische Strategietests** auf echten\nOHLCV-Daten aus **Modul-03** (PostgreSQL). Gleiche Strategie-Logik wie\n**Modul-05** (`trend_pullback_v1`) und identische Regime-Engine wie **Modul-04** —\nder Backtest entscheidet exakt so wie Live/Paper. **V1 ohne KI/ML, ohne Live-Orders,\nohne Broker-Ausführung.** Modular erweiterbar für weitere Strategien (Breakout, ORB,\nMean-Reversion) über gemeinsame `Strategy`-Schnittstelle + Registry.\n\n## Architektur / Datenfluss\n\n```\nOHLCV (Modul-01 PostgreSQL, Tabelle ohlcv)\n │ synchron, direkt nach Timestamp/Zeitraum (KEIN /history?limit)\n ▼\nModul-12-Backtesting ── POST /backtest\n │\n ├── core/engine.py deterministische Backtest-Loop (kein Lookahead)\n ├── strategies/ trend_pullback_v1 (1:1 zu Modul-05)\n ├── regime/engine.py Regime-Engine (1:1 zu Modul-04)\n ├── storage/storage.py Persistenz backtest_run/trade/equity\n └── api/main.py REST-API (intern, Port 55012)\n\nAPI (Docker-intern, Port 55012):\n /health, /health/ready\n POST /backtest Backtest starten (synchron, returns Ergebnis)\n GET /backtest/{run_id} Run-Metadaten\n GET /backtest/{run_id}/status Status + run_hash/data_hash\n GET /backtest/{run_id}/trades gespeicherte Trades\n GET /backtest/{run_id}/equity Equity-Kurve\n GET /backtest alle Runs\n POST /backtest/{run_id}/rebuild (idempotenter Rebuild aus DB)\n GET /strategies registrierte Strategien + Versionen\n```\n\nKein RabbitMQ: Backtest ist ein synchroner, request/response-Aufruf. Port `55012`\nist **nur intern** (`expose:`), kein öffentliches Port-Mapping.\n\n## Datenquelle\n- OHLCV direkt aus PostgreSQL (`ohlcv`), Spalten `provider, symbol, timeframe, ts,\n open, high, low, close, volume`.\n- Abfrage **nach Timestamp/Zeitraum** (`start_date`..`end_date`), nicht via\n `/history?limit=N` — vollständige, deterministische Datenbasis.\n- Mindestanzahl Candles: `min_candles_required` (default 220), sonst Fehler.\n\n## Bias-Schutz / Realismus (kein Lookahead)\n- **Entry am OPEN der Folgewandle** nach dem Signal (nie zum bekannten Signal-Close).\n- **Regime & Strategie** werden nur auf **abgeschlossenen** Candles (bis Candle i)\n ausgewertet — keine zukünftige Information.\n- **Intrabar Stop/Target konservativ**: Stop wird VOR Target geprüft (kein Lookahead\n durch Intrabar-Reihenfolge). Falls beide in einer Candle getroffen, gewinnt Stop\n (Worst-Case).\n- **Gap-Handling**: Überspringt der Open den Stop/Target, Fill zum Gap-Open\n (realistisch schlechter).\n- **Gebühren/Spread/Slippage** werden auf Entry UND Exit angewendet (verschlechtern\n den Fill).\n- **R-Multiple** bezieht sich auf das **Geldrisiko** `qty * |entry stop|` (nicht\n Preisdifferenz) — `expectancy_r` daher korrekt (vorheriger Bug: 31431 → korrekt 2.0).\n\n## Eingaben (POST /backtest)\n- Symbol, Asset-Klasse, Provider, Timeframe, Start-/Enddatum\n- Strategy + Version (`trend_pullback_v1`, default `1.0.0`)\n- Initiales Kapital, Risk % (Risk-basierte Positionsgröße)\n- Gebühren (fix + %), Spread (Preispunkte), Slippage (Preispunkte)\n- Optionale Limits: `max_positions`, `max_qty`\n\n## Eigene Tabellen (Migration `migrations/001_backtest.sql`)\n- `backtest_run` — Run-Metadaten: run_id, strategy+version, symbol, timeframe,\n status (COMPLETED/…), data_hash, run_hash, Parameter (JSON), Datenzeitraum.\n- `backtest_trade` — einzelne Trades: direction (LONG/SHORT), qty, entry/exit-Preis,\n entry/exit_ts, signal_ts, reason (target/stop/force_close), regime, gross_pnl,\n fee, pnl, r (R-Multiple), strategy_version.\n- `backtest_equity` — Equity-Kurve (Zeitstempel, Equity, offene Positionen).\n\nJede Tabelle FK auf `backtest_run(run_id) ON DELETE CASCADE`.\n\n## Determinismus / Reproduzierbarkeit\n- Eindeutige `run_id` + deterministischer `run_hash` + `data_hash`.\n- `run_hash`: Hash über Strategy+Version+Parameter+Datenzeitraum+data_hash.\n- `data_hash`: Hash über die geladenen OHLCV-Daten.\n- **Gleiche Daten + gleiche Version + gleiche Parameter → identische Trades, Equity,\n Kennzahlen und identischer run_hash** (E2E-verifiziert).\n\n## Tests\n- `tests/test_backtest.py`: 12/12 grün (deterministisch, LONG/SHORT, fees, slippage,\n lookahead, stop/target, gaps, unzureichende Daten, Strategy-Version/Parameter,\n Rebuild-Identität).\n- `tests/fixtures.py`: deterministische Fixture-Factory.\n- `tests/m12_fixtures.py`: erzeugt E2E-Fixtures (BT_PULL LONG, BT_PULL_S SHORT) für\n die VPS-ohlcv-Tabelle.\n- `tests/determinism_check.py` / `determinism_diff.py`: E2E-Determinismus-Nachweis\n (identische Trades/Equity/Kennzahlen/Hashes).\n\n## E2E-Verifikation (VPS, 20.08.2026)\n- **LONG** (BT_PULL, TREND_UP): 1 Trade, winrate 1.0, expectancy_r 1.94, net_pnl\n 196.59, max_drawdown 0.0001, r=1.94, reason=target.\n- **SHORT** (BT_PULL_S, TREND_DOWN): 1 Trade, short_count 1, net_pnl 194.96.\n- `signal_ts 15:00 → entry_ts 16:00` (Einstieg am Open der Folgewandle — Lookahead\n praktisch verifiziert).\n- run_hash + data_hash gesetzt (Beispiel: `bc6e2553…` / `d76a7549…`).\n- **Determinismus**: zwei identische Backtests → identischer run_hash, data_hash,\n Metrics, Trades (normalisiert), Equity-Curve.\n- **Stop/Target/Gap**: Engine-`_evaluate_exit` geprüft — Gap-Down unter Stop → Fill\n zum Gap-Open; Target bei high≥target; Stop vor Target (konservativ); SHORT Gap-Up\n → Fill zum Gap-Open.\n- Health `/health` 200, `/health/ready` 200; keine öffentlichen Ports.\n\n## Compose / Betrieb\n- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL` (Modul-01), Port 55012\n nur intern (`expose`), `restart: unless-stopped`. Kein öffentliches Port-Mapping.\n\n## Nächste Module\n- **Modul-13: Optimization** — Image `optimization:0.1.0`, E2E-verifiziert (12/12 Punkte), **FREIGEGEBEN** (20.08.2026). Details: `modul-13-optimization.md`.\n- **V1.1-Strategie**: `trend_pullback_v1.1` (Image `backtesting:0.1.2`) — Spezifikation und Parameter: `trend-pullback-v1.1-spec.md`.\n\n---\n## Phase 10e — Intrabar Execution + Gap Execution Realism (23.08.2026)\n\nVersionierung der Gap-/Intrabar-Semantik (KEIN separates `gap_execution_version`):\n- Träger: `intrabar_policy` + `intrabar_policy_version = \"intrabar_policy_v1\"` im\n `ExecutionContext` (`shared/historical/execution_context.py`). Dadurch ist die neue\n Semantik eindeutig versioniert und ändert den V2-`run_hash` bei Policy-Wechsel\n (PESSIMISTIC ≠ OPTIMISTIC → anderer run_hash).\n- Legacy-Pfad bleibt unangetastet (PESSIMISTIC-Default, identisches Verhalten).\n\nIntrabar-Policy (nur V2/BID_ASK): `PESSIMISTIC` | `OPTIMISTIC` | `STOP_FIRST` |\n`TARGET_FIRST` | `TICK_RESOLUTION` | `UNKNOWN`. **Fail-closed** §18: `UNKNOWN`/\n`TICK_RESOLUTION`/`BOGUS` → `ValueError`/`NotImplementedError`, kein Auto-Default.\n\nGap-Semantik (deterministisch, kein Lookahead):\n- LONG: Stop/Target/Exit auf **BID**; SHORT auf **ASK**.\n- Gap-Stop → Fill zum Gap-Open (schlechter, NICHT auf Stop-Level geclampt).\n- Target-Gap → Fill zum besseren Gap-Open (nicht geclampt auf Target).\n- Intrabar-Target füllt auf `target` (nicht low/high); Intrabar-Ambiguity (Stop UND\n Target in einer Bar) per Policy (PESSIMISTIC→Stop, OPTIMISTIC→Target).\n- Slippage wird adversial auf den tatsächlichen Fill (Trigger ≠ Market ≠ Final Fill\n bei Gap + Slippage); Commission auf finalem Fill (rate × qty × fill pro Seite).\n- **Kein Doppelspread/Slippage/Commission** (Vier-Ebenen-Trennung 10b/10c/10d/10e).\n\nAudit (18 Felder, forensisch rekonstruierbar): `reason`, `intrabar_policy`,\n`trigger_price`, `market_execution_price`, `exit_fill_price`, `gap_execution`,\n`gap_open_price`, `execution_model`, `price_basis`, `spread_model`, `slippage_model`,\n`slippage_value`, `cost_model`, `entry_fee`, `exit_fee`, `gross_pnl`, `net_pnl`\n(verwendete vorhandene Feldnamen, keine künstlichen).\n\nVerifikation 23.08.2026:\n- Unit-Suite `test_phase10e_intrabar_gap.py`: 40/40 grün (Intrabar, Gap, Slippage\n nach Gap, Commission auf Final Fill, BID/ASK-Seiten, Doppelzählung, Audit-18,\n pathologische Fälle, 4 numerische PnL-E2E-Beispiele).\n- Fixture-E2E (`test_phase10e_fixture_e2e.py`): 7/7 grün (A LONG Stop-Gap, B SHORT\n Stop-Gap, C Intrabar PESSIMISTIC, D Intrabar OPTIMISTIC run_hash-Differenz,\n E LONG Target-Gap, F SHORT Target-Gap, ENV-RESET); numerische Kette\n Trigger → Gap-Open → Slippage → Final Fill → Commission → Gross → Net nachgewiesen.\n- Regression Lauf 1 + Lauf 2: vollständig grün (M12 169 inkl. 10e, Phase5 20,\n Phase6 14, Phase7 AJ Legacy-Hash `8a5760ae…`, M13 Shared AJ, M13 Compat 5/5).\n- Legacy Production Smoke 2×: A==B==VORHER (run_hash `bc6e2553…`, data_hash\n `d76a7549…`, net_pnl 196.586062, 1× LONG target). DB-Wahrheit verifiziert\n (backtest_run/backtest_trade konsistent). Legacy unverändert durch Phase 10e.\n- Production Gates M12+M13: `HISTORICAL_DATA_SOURCE`/`ALLOW_FIXTURE_DATA` UNSET.\n- Deployt: `core/engine.py`, `core/service.py`, `api/schemas.py` (Container-Hashes\n aktualisiert); `shared/historical/execution_context.py` + `pricing.py` unverändert.\n\n---\n## Geändert\n```\nGeändert von: Rain Ocampo\nDatum: 23.08.2026\nGrund: Modul-12-Doku um Phase 10e (Intrabar Execution + Gap Realism) ergänzt — Versionierung, Intrabar-Policy, Gap-Semantik, Audit-18, Verifikation, Deploy, Smoke, Production Gates.\n```\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-12-Backtesting als FREIGEGEBEN markiert (deployt + E2E verifiziert: deterministisch, kein Lookahead, Stop/Target/Gap, LONG/SHORT, run_hash+data_hash).\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-12-Doku aktualisiert — Image auf backtesting:0.1.2 (V1.1), V1-Kompatmodus bitgenau, Referenz auf V1.1-Spec und Modul-13.\n"
},
{
"path": "modul-13-optimization.md",
"title": "modul-13-optimization",
"id": "object/bde121b1-121f-590d-2b54-fedc13b02173",
"type": "arch",
"role": "module",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "0c77c9bb81fd13f394d072a9b64f2f4161ca01e3a0ffee7266be09d4c41522be",
"body": "# Modul-13: Optimization\n\n**Status: FREIGEGEBEN ✅** · Container `Modul-13-Optimization` · Image `optimization:0.1.0`\n\n## Zweck\nDeterministische, reproduzierbare **Parameteroptimierung** für die V1.1-Strategie\n(`trend_pullback_v1.1`, Modul-12). Findet robuste Parameter im erlaubten Suchraum\n**ohne KI/LLM und ohne Live-Orders** — Ergebnis ist nur **CANDIDATE/RECOMMENDATION**,\nes gibt keine automatische Parameterübernahme in Modul-05.\n\n## Kernprinzipien\n- **Deterministisch**: gleiche Eingaben → gleiche Kandidaten, Reihenfolge, run_hash (kein Zufall außer explizit via Seed).\n- **Kein Lookahead**: IS/OOS strikt zeitlich getrennt; **OOS wird nie zur Selektion verwendet**.\n- **IS/OOS strikt**: `is_ratio` (z.B. 0.7) teilt die Daten; OOS ausschließlich zur Validierung.\n- **Robuste Bereiche > Einzelmax**: best = Kandidat mit bester kombinierter Score (nicht max net_pnl).\n- **Reject-/Overfit-Logik**: zu wenige Trades, DD-Limit, IS/OOS-Degradation → Kandidat wird `rejected`.\n- **Parametergrenzen zwingend**: Suchraum begrenzt auf `pullback_band_atr`, `trend_regime_window`, `rr_multiplier`.\n- **Score-Gewichte**: Expectancy/R 0.30, PF 0.25, Max-Drawdown 0.20, Trades 0.10, Stabilität 0.15.\n- **Synchron**: Request/Response über API (kein RabbitMQ).\n\n## Architektur / Datenfluss\n```\nOHLCV (Modul-01 PostgreSQL, Tabelle ohlcv)\n │ synchron, direkt nach Timestamp/Zeitraum\n ▼\nModul-13-Optimization ── POST /optimization\n │\n ├── core/split.py IS/OOS + Walk-Forward-Folds (kein Leakage)\n ├── core/optimizer.py Grid/Random-Suche, ScoreEngine, WF, Reject\n ├── core/scoring.py Multi-Metrik-Score (Gewichte siehe oben)\n ├── storage/storage.py Persistenz optimization_run/candidate/walk_forward\n ├── backtest_client.py synchroner Aufruf Modul-12 (POST /backtest)\n └── api/main.py REST-API (intern, Port 55013)\n```\n\nAPI (Docker-intern, Port 55013, kein öffentliches Port-Mapping):\n```\nGET /health, /health/ready\nPOST /optimization Start (Grid/Random), synchron\nGET /optimization Liste aller Runs\nGET /optimization/{id} Run-Metadaten + Status\nGET /optimization/{id}/candidates Kandidaten + Multi-Metrik-Ranking\nGET /optimization/{id}/best bester ROBUSTER Kandidat (RECOMMENDATION/NO_RECOMMENDATION)\nGET /optimization/{id}/walk-forward Walk-Forward-Folds\n```\n\n## Suchraum (V1.1 — NUR 3 optimierbare Parameter)\n| Parameter | Suchraum (E2E) | Zweck |\n|-----------|----------------|-------|\n| `pullback_band_atr` | {0.2, 0.5, 0.8, 1.0} | ATR-Band um EMA20 (Pullback-Timing) |\n| `trend_regime_window` | {60, 200, 300} | Fenster für strategie-internes Trend-Regime |\n| `rr_multiplier` | {1.0, 2.0, 3.0} | Target = Entry + rr×Risiko |\n\nFix (nicht optimiert): `atr_period=14`, `regime_vol_override=false`, `min_risk_reward=1.0`,\nADX-/EMA-Perioden (14/20/30), `adx_trend_threshold=20`.\n\n## Profile\n| Profil | min_trades_is | min_trades_oos | max_drawdown_limit | max_oos_degradation | Zweck |\n|--------|---------------|----------------|--------------------|--------------------|-------|\n| **produktion** (Default) | 3 | 1 | 0.30 | 0.5 | Anti-Overfit, Produktion |\n| **test** (`profile=test`) | 1 | 1 | — | — | nur Mindest-Trade-Schwellen gesenkt für technischen E2E; sonst identisch |\n\nDas TEST-PROFIL senkt **ausschließlich** die Mindest-Trade-Schwellen (damit ein\nSweep über die kalibrierte synthetische Fixture genügend Kandidaten akzeptiert).\nProduktive Anti-Overfit-Defaults bleiben unverändert. `profile` wird per API-Feld\nübergeben; explizite Request-Werte (`min_trades_is/oos`) haben Vorrang.\n\n## Eigene Tabellen (Migration)\n- `optimization_run` — Run-Metadaten: optimization_id, strategy+version, symbol, timeframe,\n optimizer_method, optimizer_version, param_space, seed, status, result (JSON), created/completed_at.\n- `optimization_candidate` — Kandidaten: rank, params, backtest_run_id_is/oos, is_metrics,\n oos_metrics, score, score_components, rejected, reject_reasons, flags, stability, robustness.\n- `walk_forward_result` — WF-Folds: fold, params, is_start/end, oos_start/end, is/oos_metrics,\n score, backtest_run_id_is/oos.\n\n## Reject-/Overfit- und Recommendation-Verhalten\n- Kandidat wird `rejected` wenn: zu wenige Trades (min_trades_is/oos), Drawdown über Limit,\n OOS-Degradation zu hoch (Overfit), fachlich unbrauchbare Metriken (z.B. PF<=0/kein Trades).\n- `/best` liefert:\n - `recommendation=RECOMMENDATION` + `best_candidate` wenn ein fachlich brauchbarer robuster Bester existiert.\n - `recommendation=NO_RECOMMENDATION` (statt leerem `best_candidate`), wenn das Kandidatenfeld\n fachlich unbrauchbar ist — eine produktive Empfehlung wird nie aus unbrauchbaren Daten erzeugt.\n\n## Tests\n- `tests/test_optimization.py`: 12/12 grün (Grid deterministisch, Random-Seed, IS/OOS-Leak,\n Walk-Forward korrekt + JSON-serialisierbar, Multi-Metrik-Ranking, Reject/Overfit, Robustheit,\n Suchraum=3 Params, Reproduzierbarkeit, NO_RECOMMENDATION, TEST-PROFIL).\n\n## E2E-Verifikation (VPS, 20.08.2026)\n- **Suchraum-Differenzierung** (kalibrierte Fixture `M13_E2E`, 1314 Bars, alle 3 Params unterscheidbar):\n - `pullback_band_atr`: 0.2→8 Trades, 0.5→910, 0.8/1.0→1 Trade.\n - `trend_regime_window`: bei pba=0.5: 60→9 Trades vs 200/300→10 Trades.\n - `rr_multiplier`: 1.0→810 Trades (net +828…+1046), 2.0/3.0→1 Trade.\n- **12/12 E2E-Punkte über API bestätigt** (e2e_full.py):\n Grid deterministisch, Random-Seed identisch, IS/OOS-Leak ausgeschlossen, WF-Folds zeitlich\n korrekt + DB-persistiert (3 Folds), Multi-Metrik-Ranking, Reject-Logik (36 Kandidaten,\n 30 rejected / 6 accepted), best=robust + RECOMMENDATION, DB-Run/Candidates vollständig,\n API list_runs, Reproduzierbarkeit (status COMPLETED), NO_RECOMMENDATION bei nur-1-Trade\n Konfiguration (produktion-Profil).\n- **Bugfix C**: Walk-Forward-Fold-Ergebnisse enthalten `datetime`-Timestamps → beim\n `json.dumps` in `complete_run` nicht serialisierbar (422). Fix: ISO-String-Kopie im\n Run-Result, datetime bleibt für `storage.insert_wf`. Verifiziert (3 WF-Folds in DB).\n- Health `/health/ready` → `{\"status\":\"ready\",\"db\":\"ok\"}` (M13 und M12). Keine Tracebacks.\n- Port 55013 nur intern (`expose`), kein Host-Port-Binding (verifiziert).\n\n## Freigabe\n- **20.08.2026: Modul-13-Optimization-Service vom Nutzer FREIGEGEBEN ✅**\n (nach vollständiger E2E-Verifikation: 12/12 API-Punkte, Suchraum-Differenzierung für alle\n 3 Params, TEST-PROFIL, NO_RECOMMENDATION, Bugfixes A/B/C deployt, Tests 12/12 grün).\n- Keine weiteren technischen Änderungen an Modul-13.\n\n## Compose / Betrieb\n- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL` (Modul-01), Backtest via\n `Modul-12-Backtesting` (intern). Port 55013 nur intern (`expose`), `restart: unless-stopped`.\n\n## Nächste Module\n- **Modul-14 ff.** — noch Platzhalter (alpine), NICHT gestartet.\n\n---\n## Geändert\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: 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).\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-13 vom Nutzer FREIGEGEBEN — Status auf FREIGEGEBEN gesetzt, Freigabe-Sektion ergänzt. Keine technischen Änderungen.\n```\n"
},
{
"path": "modul-14-notification.md",
"title": "modul-14-notification",
"id": "object/0a0c9ca4-1cf4-aee1-3072-e1c3e8cfd29e",
"type": "arch",
"role": "module",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "28c30ad6844e31f71b49687f72e09bc010e1620665f4a6bcc4feaf950d7daceb",
"body": "# Modul-14: Notification-Service\n\n**Status: FREIGEGEBEN ✅** · Container `Modul-14-Notification` · Image `notification:0.1.0`\n\n## Zweck\nZentrale **Benachrichtigungen für Trading- und Systemereignisse**. Konsumiert relevante\nEvents aus den RabbitMQ-Exchanges der Module 0510 und liefert kompakte, strukturierte\nMeldungen — **V1 primär über Telegram** (`TelegramProvider`), provider-neutral angelegt.\n\n**Restriktionen (Nutzer-Vorgabe):**\n- **keine KI/ML**, **keine Trading-Entscheidungen**, **keine Orders**.\n- **FAIL-SAFE:** Ein Notification-Ausfall darf die Trading-Pipeline **NIEMALS blockieren**.\n\n## Architektur / Datenfluss\n```\nRabbitMQ (market.signals/.rankings/.risk/.portfolio/.execution/.journal)\n │ NotificationConsumer (notification.input)\n ▼\nModul-14-Notification ── NotificationService ──> RulesEngine + DedupGuard\n │ │ Spam-Schutz\n ├── providers/ NotificationProvider-ABC → Telegram / Fake\n ├── rules/ RulesEngine (Regeln) + DedupGuard (Dedup/Cooldown/Rate-Limit)\n ├── storage/ Persistenz notification_log (PostgreSQL, Modul-01)\n ├── core/ formatter (kompakte Nachricht) + service (Retry/Backoff)\n └── api/main.py REST-API (intern, Port 55014)\n\nEvent → RabbitMQ → Modul-14 → notification_log → Provider (Telegram) → Chat\n```\n\n## API (Docker-intern, Port 55014, kein öffentliches Port-Mapping)\n```\nGET /health liveness\nGET /health/ready readiness (postgresql, consumer_ready, provider_configured)\nGET /notifications/recent letzte Notifications\nGET /notifications/{id} einzelne Notification\nPOST /notifications/test Test-Sendung (nur konfigurierte Destinationen)\n```\n\n## Konsumierte Events & Routing-Keys\n| Event-Typ | Exchange | Routing-Key | Default-Severity | Default aktiv |\n|-----------|----------|-------------|------------------|---------------|\n| `SIGNAL_DETECTED` | `market.signals` | `strategy.signal.detected` | INFO | ❌ (deaktiviert) |\n| `SIGNAL_RANKED` | `market.rankings` | `signal.ranked` | INFO | ❌ (deaktiviert) |\n| `RISK_APPROVED` | `market.risk` | `risk.approved` | INFO | ❌ (deaktiviert) |\n| `RISK_REJECTED` | `market.risk` | `risk.rejected` | WARNING | ❌ (deaktiviert) |\n| `TRADE_APPROVED` | `market.portfolio` | `trade.approved` | INFO | ✅ |\n| `PORTFOLIO_REJECTED` | `market.portfolio` | `portfolio.rejected` | WARNING | ✅ |\n| `ORDER_SUBMITTED` | `market.execution` | `order.submitted` | INFO | ❌ (deaktiviert) |\n| `ORDER_FILLED` | `market.execution` | `order.filled` | INFO | ✅ |\n| `ORDER_REJECTED` | `market.execution` | `order.rejected` | CRITICAL | ✅ |\n| `ORDER_FAILED` | `market.execution` | `order.failed` | CRITICAL | ✅ |\n| `TRADE_RECORDED` | `market.journal` | `trade.recorded` | INFO | ✅ |\n\nDie Regeln sind **pro Event-Typ konfigurierbar** (aktiv/inaktiv, Severity, Provider,\nDestination, Cooldown, Rate-Limit). **Unbekannte Event-Typen werden sicher ignoriert**\n(kein Crash). V1 sind die rauschigen Events (SIGNAL_*, RISK_*, ORDER_SUBMITTED)\nstandardmäßig deaktiviert.\n\n## Severity-Hierarchie\n```\nINFO < WARNING < CRITICAL\n```\nRegel-Severity überschreibt den Event-Standard. V1: alle über Telegram.\n\n## Spam-Schutz (DedupGuard)\nMehrstufig, thread-sicher (Lock):\n1. **Idempotenz** — gleiche `event_id`/`source_event_id` erzeugt nie eine zweite Notification (DB-unique über `source_event_id` + `event_processed`-Check).\n2. **Dedup** — identische Meldungen (gleicher Text/Event-Typ) in kurzem Fenster werden zusammengefasst.\n3. **Cooldown** — pro Event-Typ ein Mindestabstand (`cooldown_seconds`, Default 60s).\n4. **Rate-Limit** — maximale Notifications pro Minute (`rate_limit_per_minute`, Default 30).\n5. **Burst-Schutz** — Ansturm vieler Events wird aufs Limit begrenzt.\n\n## Retry / Dead (FAIL-SAFE)\n- Provider nicht erreichbar → `RETRY_PENDING` mit **begrenzten Retries** (exponentielles Backoff).\n- `max_attempts` (Default 5) erreicht → **DEAD** (keine Endlosschleife).\n- **Provider nicht konfiguriert** (fehlender Token/Chat-ID) → **sofort DEAD** mit\n `error_code=NOT_CONFIGURED` (keine sinnlosen Retries).\n- Nach Provider-Recovery werden **neue Events wieder normal gesendet** (Retry betrifft nur die jeweilige Notification).\n\n## Eigene Tabelle (Migration)\n`notification_log`:\n`notification_id` (PK), `source_event_id`, `event_type`, `severity`, `provider`,\n`destination`, `status` (`SENT`/`FAILED`/`RETRY_PENDING`/`DEAD`), `attempts`,\n`created_at`, `sent_at`, `error_code`, `error_message`.\n\n## Sicherheit\n- **KEINE Secrets in DB/Logs/Doku/Commits.** Telegram-Bot-Token/Chat-ID ausschließlich\n über Environment (Compose `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`), nie hart kodiert.\n- `/notifications/test` sendet **nur an konfigurierte Destinationen**.\n\n## Tests\n- `tests/test_notification.py`: **14/14 grün** (Event→Notification, Idempotenz, Dedup,\n Cooldown, Rate-Limit, Telegram-Ausfall blockiert nicht, Retry+Backoff, Max→DEAD,\n keine Secrets, unbekannter Typ ignoriert, Severity/Regeln, FakeProvider, Reconnect).\n\n## E2E-Verifikation (VPS, 20.08.2026) — alle 7 Punkte grün\n1. **Fake-Telegram-E2E** (Modul-14 auf `http://fake-telegram:55000`, Test-Token/Chat-ID\n nur als ENV): echtes `TRADE_RECORDED` publiziert → **Event → RabbitMQ → Modul-14 →\n notification_log → Fake-Telegram** komplett durchlaufen. DB: `status=SENT`,\n `sent_at` gesetzt, `attempts=0`, **genau 1 Zustellung, kein Duplikat**. Fake-Telegram\n RAW: `{\"chat_id\": \"E2E_TEST_CHAT_114\", \"text\": \"TRADE RECORDED | NVDA | LONG | OPEN\"}`.\n2. **NO_CONFIG-Fix**: Container ohne Token → Event → `status=DEAD, attempts=1,\n error_code=NOT_CONFIGURED` (sofortiges DEAD, keine 5 sinnlosen Retries).\n3. **Fehlerfall-E2E**: Fake-Telegram gestoppt → Notification `RETRY_PENDING` mit\n Backoff (attempts 2→4, `NETWORK_ERROR`), **Trading-Pipeline unbeeinflusst**,\n Max-Retries (`attempts=5`) → `DEAD`. Nach Fake-Telegram-Recovery → neues Event\n wieder `SENT`.\n4. **RabbitMQ-Reconnect**: kontrollierter Restart → Modul-14 reconnectet automatisch\n (Backoff 4s→8s→16s, „Consumer verbunden\"), **genau 1 Consumer** an `notification.input`,\n keine Zombies, keine verlorenen/duplizierten Notifications (nach Reconnect: 1 Zustellung, 0 Duplikate).\n5. **Spam-/Sicherheitschecks** (praktisch): unbekannter Typ `UNKNOWN_EVENT_XYZ` → ignoriert;\n gleiche `event_id` 2× → nur 1 Notification (Idempotenz); gleicher Event-Typ 2×\n → Cooldown greift; deaktivierter `SIGNAL_DETECTED` → keine Notification. **Keine\n Tokens/Chat-IDs in Logs/API/Doku/DB** (0 Token-Leaks).\n6. **Health/Readiness**: `/health` + `/health/ready` → 200\n (`postgresql=true, consumer_ready=true, provider_configured=…`). Keine\n Applikations-Tracebacks im Normalbetrieb (nur erwartete Reconnect-Logs beim Test).\n Port 55014 nur intern (`expose`, kein Host-Port).\n7. **Forgejo-Doku + Commit-ID** (siehe Freigabe unten), temporäre Test-Credentials\n entfernt, Remote token-/key-frei.\n\n## Freigabe\n- **20.08.2026: Modul-14-Notification-Service vom Nutzer FREIGEGEBEN ✅**\n (nach vollständiger E2E-Verifikation der Punkte 17, alle grün).\n- Keine weiteren technischen Änderungen an Modul-14.\n\n## Compose / Betrieb\n- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL`, RabbitMQ `Modul-02-RabbitMQ`.\n- Port 55014 nur intern (`expose`), `restart: unless-stopped`, non-root (uid 1001).\n- Token/Chat-ID via ENV (`TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`, `TELEGRAM_API_BASE`).\n\n## Nächste Module\n- **Modul-15 ff.** — noch Platzhalter (alpine), NICHT gestartet.\n\n---\n## Geändert\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-14-Notification-Service dokumentiert — E2E-verifiziert (Punkte 17 grün),\n API/Events/Severity/Retry-Dead/Spam-Schutz/Fake-E2E dokumentiert, Status FREIGEGEBEN.\n```\n"
},
{
"path": "modul-15-m09-anbindung-design.md",
"title": "modul-15-m09-anbindung-design",
"id": "object/1d6c2323-a31a-8b50-fcdd-3c9268175a69",
"type": "design",
"role": "design",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "d03fc2d29f63de8e6b91c2fbe2dba16caa0047487f41e4e1b3600cddfa20a275",
"body": "# Design-Vorschlag: Modul-15 → Modul-09 Anbindung (Safety-/Trading-Control)\n\n**Status: NUR DESIGN-VORSCHLAG — keine Implementierung.**\n**Datum:** 20.08.2026 · **Autor:** Rain Ocampo (Hermes)\n**Gilt für:** Modul-09-Execution-Service (FREIGEGEBEN, Order-Pfad) ↔ Modul-15-Monitoring-Control (IMPLEMENTIERT + E2E-verifiziert, **noch nicht freigegeben**)\n\n> **Scope-Regeln (verbindlich):**\n> - M09 wird **nicht** verändert, bis dieser Vorschlag vom Nutzer freigegeben wurde.\n> - M15 bleibt **nicht freigegeben** bis Nutzer-Freigabe.\n> - M16 wird **nicht** begonnen.\n> - Dieser Vorschlag ist **Architektur/Pseudoflow**, kein Code.\n\n---\n\n## 1. Ziel\n\n**Modul-15** wird die **autoritative Safety-/Trading-Control-Instanz** für den Order-Durchstich.\n**Modul-09** darf neue Orders **nur senden, wenn M15 dies eindeutig erlaubt**.\n\nKernauftrag: 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).\n\n---\n\n## 2. Ist-Zustand in M09 (Kontext für den Vorschlag)\n\nDer Order-Pfad in `app/core/service.py::_on_event` ist heute:\n\n```\nTRADE_APPROVED (Modul-08)\n → 1) Idempotenz (already_processed)\n → 2) Harte Validierung validate_trade_approved (FAIL-CLOSED)\n → 3) Broker-Bereitschaft validate_broker_ready (FAIL-CLOSED)\n → 4) Order atomar in DB anlegen (Advisory Lock + Transaktion, PENDING)\n → 5) _submit_and_track → BrokerAdapter.submit_order\n```\n\n**Heutiger Kill-Switch:** rein konfigurativ `TRADING_ENABLED=false` (Default, sicher für LIVE).\nEs existiert **keine** externe Echtzeit-Control-Schnittstelle im Order-Pfad.\n\n**Einbau-Punkt (Vorschlag):** zwischen Schritt 4 (Order angelegt) und Schritt 5 (Broker-Submit)\n— d. h. **unmittelbar vor `submit_order`**. Alternativ als zusätzlicher Check in Schritt 2.\nEmpfehlung: als eigener Schritt zwischen 4 und 5, weil dort die Order bereits idempotent in der DB\nliegt und wir den Final-Check „direkt vor dem Senden\" platzieren (Race-Window minimal).\n\n---\n\n## 3. Trading-States & Regelwerk\n\n### 3.1 Globale Trading-States (autoritativ: M15)\n\n| State | Bedeutung | neue OPEN/INCREASE | REDUCE | CLOSE/CANCEL |\n|-------|-----------|--------------------|--------|--------------|\n| `ENABLED` | Alle krit. Dienste gesund, aktuelle Market-Data, Trading erlaubt | **ERLAUBT** | erlaubt | erlaubt |\n| `PAUSED` | Nicht-krit. Störung (Analytics/Notification), Risiko-umfeld unklar | **VERBOTEN** | **ERLAUBT** | **ERLAUBT** |\n| `HALTED` | Krit. Ausfall (PG/RMQ/Execution/Market-Data/UNKNOWN) | **VERBOTEN** | **ERLAUBT** | **ERLAUBT** |\n| `UNKNOWN` / M15 nicht erreichbar / Status veraltet | Unklarheit | **VERBOTEN (FAIL-CLOSED)** | **ERLAUBT** (konservativ) | **ERLAUBT** |\n\n**Kernregel:** Nur `ENABLED` erlaubt neue Positionen (OPEN) oder Aufstockung (INCREASE).\n**Alles andere** (PAUSED/HALTED/UNKNOWN/nicht erreichbar/veraltet) blockiert OPEN/INCREASE.\n**REDUCE/CLOSE** (Risikoreduktion/Position schließen) sind **immer** erlaubt — unabhängig vom State.\n\n### 3.2 Order-Kategorien (Einordnung)\n\n| Aktion | Bedeutung | Regel bei PAUSED/HALTED/UNKNOWN |\n|---|---|---|\n| `OPEN` | Neue Position eröffnen | **BLOCKIEREN** |\n| `INCREASE` | Bestehende Position aufstocken | **BLOCKIEREN** (Neue-Position-Logik, Risiko wächst) |\n| `REDUCE` | Position verkleinern | **ERLAUBT** (Risiko sinkt) |\n| `CLOSE` | Position schließen | **ERLAUBT** (Risiko eliminieren) |\n| `CANCEL` | Offene/geplante Order stornieren | **ERLAUBT** (kein neues Risiko) |\n\n> Begründung INCREASE blockiert: Aufstocken fügt neues Marktrisiko hinzu — bei HALT/Unklarheit\n> unerlaubt. REDUCE/CLOSE/CANCEL sind per Definition risikoreduzierend → immer frei.\n\n---\n\n## 4. Vergleich der Anbindungs-Varianten\n\n### Variante A — synchroner HTTP-Check vor jeder Order\n\nM09 ruft vor jedem Submit `GET /control/trading-state` auf M15 (Docker-intern) auf.\n\n| Kriterium | Bewertung |\n|---|---|\n| **Sicherheit** | Sehr hoch — liest immer den frischesten autoritativen Zustand. FAIL-CLOSED bei Timeout. |\n| **Latenz** | Pro Order +1 Round-Trip (ms-Bereich, Docker-Netz). Bei vielen Orders spürbar, aber Orders sind selten. |\n| **Ausfallsicherheit** | M15-down → HTTP-Time out → FAIL-CLOSED (blockieren). Sicher, aber **M15 wird Single Point of Failure für OPEN**. |\n| **Race Conditions** | M15 könnte direkt nach Antwort auf HALTED wechseln → Race trotz Check (siehe §7). Final-Check nur minimal schmaler. |\n| **Single Point of Failure** | **Ja** — M15 ist für OPEN/INCREASE zwingend. Bei M15-down wird der Execution-Service sonst blind blockiert. |\n| **Bei RabbitMQ-Ausfall** | HTTP bleibt funktionsfähig (separater Kanal) → Check ok; aber M15 meldet RABBITMQ_UNREACHABLE→HALTED → korrekt blockiert. |\n| **Bei M15-Ausfall** | Timeout → FAIL-CLOSED → OPEN/INCREASE blockiert (gewünscht). REDUCE/CLOSE müssen **ausgenommen** werden, sonst kann Position nicht geschlossen werden. |\n| **Komplexität** | Gering — ein HTTP-Call, kein Cache, kein Subscription. |\n| **Nachvollziehbarkeit/Audit** | M15 protokolliert jede Anfrage; M09 loggt Check + Ergebnis. Gut, aber hochfrequent (jede Order). |\n\n### Variante B — Event-basierter Status-Cache (publish/subscribe)\n\nM15 publiziert Statusänderungen auf `market.control`; M09 subscribt und hält ein lokales State-Modell.\n\n| Kriterium | Bewertung |\n|---|---|\n| **Sicherheit** | Mittel — Status nur so frisch wie das letzte Event + Cache-TTL; **riskant ohne TTL/Herzschlag**. |\n| **Latenz** | Sehr gering — kein HTTP im kritischen Pfad, Cache-Lookp. |\n| **Ausfallsicherheit** | Schlecht — RabbitMQ-Ausfall ⇒ keine Events mehr; Cache wird **stale**. Ohne TTL würde M09 mit veraltetem ENABLED weiter traden. **Gefahr.** |\n| **Race Conditions** | Stale-Cache ist die größte Race-Quelle. Event-Reihenfolge/Lag. |\n| **Single Point of Failure** | RabbitMQ als Event-Bus ist der SPOF für den Status-Transport. |\n| **Bei RabbitMQ-Ausfall** | Events stoppen → Cache veraltet → **genau das Risiko**, das wir vermeiden wollen. Muss über Cache-TTL + Fail-CLOSED gelöst werden. |\n| **Bei M15-Ausfall** | Keine neuen Events → Cache veraltet. Ohne TTL gefährlich (weiter OPEN). |\n| **Komplexität** | Mittel — Subscription, Cache-Update, TTL-Reinigung, RabbitMQ-Reconnect im M09. |\n| **Nachvollziehbarkeit/Audit** | Gut — Event-Stream ist Append-Log; aber Cache-Echo im M09 ist ein zweites Modell (Konsistenzpflege). |\n\n### Variante C — Kombination: Event-Cache + synchroner Final-Check (EMPFEHLUNG)\n\n**Event-Cache als schnelle Vorfilterung**, **synchroner HTTP-Final-Check unmittelbar vor dem\nBroker-Submit** als harte Bestätigung. Nur wer beide klar \"ENABLED\" liefert, darf OPEN/INCREASE.\n\n| Kriterium | Bewertung |\n|---|---|\n| **Sicherheit** | **Sehr hoch** — zweistufig. Cache als Bauraus/Performance, HTTP-Final als Autorität. |\n| **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. |\n| **Ausfallsicherheit** | Beste — Cache kann \"PAUSED/HALTED/UNKNOWN\" sofort blockieren; HTTP- Ausfall ⇒ FAIL-CLOSED. Zwei unabhängige Quellen. |\n| **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. |\n| **Single Point of Failure** | M15 bleibt SPOF für OPEN/INCREASE (unvermeidbar, da autoritativ). REDUCE/CLOSE ausgenommen. |\n| **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). |\n| **Bei M15-Ausfall** | HTTP-Final-Check schlägt fehl → FAIL-CLOSED, OPEN/INCREASE blockiert. REDUCE/CLOSE frei. |\n| **Komplexität** | Höher (beides). Aber gut beherrschbar; Cache als reiner nicht-kritischer Vorfilter, Autorität liegt klar beim HTTP. |\n| **Nachvollziehbarkeit/Audit** | Exzellent — Final-Check liefert `state_version` + `timestamp` + `audit_id` pro Order; Cache-Zustand separat auditierbar. |\n\n### Zusammenfassung der Varianten\n\n| | A (HTTP) | B (Event-Cache) | C (Kombi) |\n|---|---|---|---|\n| Sicherheit | hoch | mittel | **sehr hoch** |\n| Latenz | +HTTP | **sehr gering** | gering (HTTP-Final) |\n| Ausfallsicherheit | ok (SPOF M15) | **schwach** (RMQ SPOF) | **beste** |\n| Race-Risiko | minimiert (kurz) | **hoch (stale)** | **minimiert** |\n| RMQ-Ausfall | korrekt (HALTED) | **risiko (stale)** | korrekt |\n| M15-Ausfall | FAIL-CLOSED | **risiko (stale)** | FAIL-CLOSED |\n| Komplexität | gering | mittel | **höher** |\n| Audit | gut | zweites Modell | **exzellent** |\n\n**Empfehlung: Variante C** (Event-Cache als Vorfilter + synchroner Final-Check).\n\n---\n\n## 5. Race Condition & Lösung\n\n**Problem:** M09 prüft `ENABLED`, M15 wechselt sofort danach auf `HALTED` — bevor M09 die Order gesendet hat.\n\n**Lösung (Kombination):**\n1. **Final-Check unmittelbar vor `submit_order`** — minimales Zeitfenster (ms) zwischen Check und Send.\n2. **TTL auf der HTTP-Antwort:** `expires_at` (z. B. `now + 500 ms`). Ist die Antwort beim Senden abgelaufen\n (Fenster überschritten), NICHT senden → Order nach `FAILED/REJECTED` mit `CONTROL_STATE_STALE` markieren.\n3. **`state_version`** (Monoton aufsteigend, von M15 je Status-Änderung). M09 übernimmt die im Final-Check\n gesehene Version. Beim Broker-Submit trägt die Order die `control_version`; die Audit-Kette verbindet\n genau die Version, die gegolten hat, mit dem Versand.\n4. **Bestätigtes Einverständnis:** Der Check liefert `trading_status=ENABLED` **und** `state_version` **und**\n `expires_at`. Nur wenn beides vorliegt und innerhalb der TTL ist, darf OPEN/INCREASE durchgehen.\n5. **Audit-ID / Correlation:** M09 erzeugt `control_audit_id`, M15 protokolliert die Bestätigung. Damit ist\n der „wir haben im ENABLED-Zustand gesendet\" Moment exakt nachvollziehbar.\n\n**Kein \"Senden dann hoffen\":** Ist das Fenster überschritten oder Status unklar → `FAILED` mit\n`CONTROL_STATE_UNKNOWN/CONTROL_STATE_STALE`, **niemals** blind erneut senden (konsistent zum bestehenden\nM09-Time-out-Handling: erst Status beim Broker klären, dann entscheiden).\n\n---\n\n## 6. API-Vertrag M15 → M09\n\n### 6.1 `GET /control/trading-state` (von M09 im Final-Check)\n\n**Request:** keiner (oder optional `?action=OPEN` / `?for_action=OPEN` zur Kontext-Auditierung)\n\n**Response 200 (ENABLED):**\n```json\n{\n \"trading_status\": \"ENABLED\", // ENABLED|PAUSED|HALTED|UNKNOWN\n \"system_status\": \"HEALTHY\", // Kontext\n \"state_version\": 42, // monoton, von M15 je Änderung\n \"timestamp\": \"2026-08-20T15:30:00Z\", // Erzeugzeit (UTC, ISO 8601)\n \"expires_at\": \"2026-08-20T15:30:00.5Z\", // TTL-Horizont (Server), z. B. now+500ms\n \"ttl_seconds\": 0.5,\n \"audit_id\": \"ctl-...\" // M15-interne Audit-Kennung der Bestätigung\n}\n```\n**Nur** dieses Objekt mit `trading_status=ENABLED` und `expires_at` in der Zukunft gilt als \"eindeutige Erlaubnis\".\n\n**Response (blockiert):** gleiches Schema, aber `trading_status` = `PAUSED`/`HALTED`/`UNKNOWN`.\n\n**Fehler/Timeouts:**\n- HTTP 503 → M15 selbst nicht Ready/unbekannt → **FAIL-CLOSED** (als HALTED/unbekannt behandeln).\n- Timeout/5xx/netz → **FAIL-CLOSED**, als `UNKNOWN` behandeln.\n- Alle nicht-`ENABLED`-Antworten → OPEN/INCREASE **verweigert**.\n\n### 6.2 Empfehlung der aktuellen API-Form\n\nM15 hat aktuell `GET /control/trading-state` (liefert `trading_status` + `system_status`). Für M09-Anbindung\nempfehle ich die obige **Erweiterung** um `state_version`, `timestamp`, `expires_at`, `audit_id` — dies ist ein\n**additiver API-Vertrag** (kein Breaking Change). Das ist im Vorschlag berücksichtigt; die konkrete\nUmsetzung erfolgt erst nach Freigabe.\n\n---\n\n## 7. Event-Schema für Status-Änderungen (market.control)\n\nM15 publiziert auf Exchange `market.control`, Routing `trading.state` — **nur bei Status-Änderung** (keine Schleife).\n\n```json\n{\n \"event_id\": \"evt-...\",\n \"event_type\": \"trading.state\",\n \"timestamp\": \"2026-08-20T17:30:00Z\",\n \"version\": 1,\n \"payload\": {\n \"trading_status\": \"HALTED\", // ENABLED|PAUSED|HALTED|UNKNOWN\n \"system_status\": \"UNHEALTHY\",\n \"state_version\": 43,\n \"reason_codes\": [\"POSTGRESQL_UNREACHABLE\", \"PERSISTENCE_FAILURE\"],\n \"changed_at\": \"2026-08-20T17:30:00Z\"\n }\n}\n```\n\n**Im Cache (Variante C):**\n- M09 hält `last_state` (trading_status + state_version + received_at).\n- **TTL (z. B. 5 s):** ist `received_at` älter als TTL → Cache behandelt als `UNKNOWN/veraltet` → OPEN/INCREASE\n **blockiert** (auch wenn letztes Event `ENABLED` war).\n- Ein Event `state_version` älter als bereits gesehen → ignorieren (kein Regress).\n\n---\n\n## 8. Verhalten nach Recovery\n\n- **Kein automatisches RESUME:** M15 nimmt nach Behebung der Ursache Status selbst neu (z. B. HALTED→HEALTHY→ENABLED)\n **nur durch den regulären Monitoring-Zyklus** (Ursache wirklich erneut geprüft). Es gibt kein verstecktes Auto-Resume.\n- M09 wartet darauf: erst wenn ein **neues Event mit `trading_status=ENABLED` und neuer `state_version`** oder\n ein **HTTP-Final-Check mit `ENABLED`** vorliegt, sind OPEN/INCREASE wieder möglich.\n- Ein `PAUSED→ENABLED`-Übergang verlangt, dass M15 nachweislich die früheren HALT-Gründe entfernt hat\n (keine Reason-Codes mehr). Dies wird durch die bestehende FAIL-CLOSED-Regel + idempotentes Event sichergestellt.\n- **Cache-TTL-Disziplin:** Nach einem HALTED/PAUSED-Ereignis bleibt M09 vorsichtig: Es traut dem Cache erst\n wieder, wenn ein neues `ENABLED`-Event (hohe Version) eingegangen ist — niemals basierend auf Zeitablauf der\n Sperre, sondern auf **bestätigter Freigabe**.\n\n---\n\n## 9. Audit in M09 (zusätzlich zu M15-Audit)\n\nFür jede Order-Entscheidung (im M09-Storage bzw. `execution_event`):\n- `control_status`: Ergebnis des Final-Checks (`ENABLED`/`PAUSED`/...)\n- `control_state_version`: Version, die beim Send gegolten hat\n- `control_expires_at` / `control_ttl`: TTL-Horizont\n- `control_audit_id`: vom Final-Check zugeordnet\n- `control_check_source`: `final_http` (und optional `cache`)\n\nDamit ist jede Order exakt dem autoritativen Zustand zum Sendzeitpunkt zugeordnet (Audit-Kette vollständig).\n\n---\n\n## 10. Zusammenfassung Architektur-Empfehlung\n\n**Variante C**: Event-Cache (mark-to-control, TTL, state_version) als **nicht-kritischen Vorfilter** im M09 +\n**synchronier HTTP-Final-Check** (`GET /control/trading-state`, mit `expires_at`+`state_version`+`audit_id`)\nunmittelbar vor `submit_order`. Beide müssen `ENABLED` und frisch sein; sonst OPEN/INCREASE blockiert\n(FAIL-CLOSED). REDUCE/CLOSE/CANCEL sind von der Control-Regel ausgenommen und laufen immer.\n\n- **Zustand, der OPEN/INCREASE erlaubt:** Cache == `ENABLED` (frisch) UND Final-Check == `ENABLED` (frisch).\n- **Sonst alles:** blockiert OPEN/INCREASE; REDUCE/CLOSE/CANCEL frei.\n- **FAIL-CLOSED:** M15 nicht erreichbar, Timeout, Cache veraltet, `UNKNOWN` → OPEN/INCREASE verweigert.\n- **M15 bleibt SPOF nur für OPEN/INCREASE** (unvermeidbar, autoritativ). Exit/Close bleibt immer möglich.\n\n---\n\n## 11. Pseudoflow (Order-Durchstich M09)\n\n```\nTRADE_APPROVED (Modul-08)\n │\n ▼\n1. Kategorisiere Aktion: OPEN | INCREASE | REDUCE | CLOSE | CANCEL\n │\n ├─ action ∈ {REDUCE, CLOSE, CANCEL} → ERLANG DURCHSTICH (kein Control-Check)\n │ └─→ weiter zum bestehenden Submit-Pfad\n │\n └─ action ∈ {OPEN, INCREASE}\n │\n ▼\n2. [Cache-Vorfilter] local_state = cache.get()\n ├─ local_state ist stale (age > TTL) → → zum Final-Check (unten) aber NICHT aus Cache ableiten\n └─ local_state.trading_status != ENABLED → BLOCKIERT (markiere FAILED/REJECTED, CONTROL_STATE_*)\n │\n ▼\n3. [Final-Check] resp = M15 GET /control/trading-state (Time out: 800ms, Retry: 1×)\n ├─ Timeout/5xx → FAIL-CLOSED → BLOCKIERT (FAILED, CONTROL_STATE_UNKNOWN)\n └─ resp.trading_status != ENABLED → BLOCKIERT (FAILED, CONTROL_STATE_*)\n └─ resp.expires_at <= now → BLOCKIERT (FAILED, CONTROL_STATE_STALE)\n ▼\n4. Zustand grün (ENABLED, frisch, version=resp.state_version)\n → Order in DB (PENDING, CONTROL_FIELDS: version, audit_id, expires)\n ▼\n5. unmittelbar danach: broker.submit_order(request) ← nur zwischen 4 und 5 minimales Fenster\n ▼\n6. Ergebnis verarbeiten (FILLED/REJECTED/FAILED), Audit-Event mit control_* schreiben\n```\n\n### Zeit-/Retry-Parameter (Vorschlag)\n- Final-Check-HTTP-Timeout: **500 ms** (Docker-intern, schnell); Retry: **max 1** sofort.\n- `expires_at`-TTL (M15): **500 ms** (Server-seitig gesetzt).\n- Nach Timeout/unklar: **nicht senden**; Order als `FAILED`/`REJECTED` mit `CONTROL_STATE_UNKNOWN/STALE`.\n- **Kein Auto-Resume**, kein \"dann schicken wir eben ohne Check\".\n\n### Warum REDUCE/CLOSE/CANCEL durchlaufen\n- Ziel des Control ist **Risiko-Management**. Ein HALT darf Risiko niemals einfrieren — im Gegenteil,\n muss das System bei Problemen schneller in der Lage sein, Positionen abzubauen.\n- `REDUCE`/`CLOSE`/`CANCEL` senken/eliminieren Risiko → **immer erlaubt**, unabhängig von M15-Status.\n\n---\n\n## 12. Offene Punkte für die Nutzer-Freigabe\n\n1. **API-Vertrag erweitern** (`state_version`, `expires_at`, `ttl_seconds`, `audit_id`) in M15 (additiv).\n2. **M09-Check-Position:** zwischen Schritt 4 (Order in DB) und 5 (Broker-Submit) — bestätigen.\n3. **TTL-Wert** (`expires_at`-Fenster) festschreiben (Vorschlag 500 ms).\n4. **Kategorisierung** der Order-Aktion in M09 (OPEN/INCREASE/REDUCE/CLOSE/CANCEL) ableiten — M09 muss\n aus dem TRADE_APPROVED die Aktion bestimmen (Vorschlag: über `direction`/Kanten-Art der Entscheidung).\n5. **M03M06-Klassifikation** (Market-Data-Pipeline): ob Einzel-Ausfall dieser Module wirklich HALTED\n auslösen soll, oder nur deren Daten-Staleness — zu finalisieren, beeinflusst WEN oft HALTED greift.\n6. **SPOF-Akzeptanz:** M15 ist für OPEN/INCREASE zwingend (autoritativ). Exit-Pfad bleibt frei.\n7. M15 weiterhin **nicht freigeben**, M16 **nicht beginnen** bis Nutzer-Entscheid.\n\n---\n\n*Dies ist ein Design-Vorschlag. Es wurde kein Code geändert. M15 nicht freigegeben. M16 nicht begonnen.*\n"
},
{
"path": "modul-15-m09-anbindung-implementierung.md",
"title": "modul-15-m09-anbindung-implementierung",
"id": "object/a3cffdc1-41fd-ab84-c211-7b586f153e8d",
"type": "arch",
"role": "implementation",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "3272916c79ce01758f2be7627dea86225d9202b704fe2a4b8b4cf179bdef6442",
"body": "# Modul-15 → Modul-09 Control-Anbindung — Implementierung & Live-E2E\n\n**Status: FREIGEGEBEN / PRODUKTIV VERIFIZIERT** (20.08.2026, Nutzer-Bestätigung).\n**Datum:** 20.08.2026 · **Autor:** Rain Ocampo (Hermes)\n**Gilt für:** Modul-09-Execution-Service ↔ Modul-15-Monitoring-Control\n\n> **Freigabe-Hinweis:** Diese Doku dokumentiert den Implementierungs- und E2E-Stand.\n> Die **finale Freigabe** (M15/M09 als FREIGEGEBEN markieren) erfolgte am **20.08.2026**\n> durch Nutzer-Bestätigung. M16 wird **nicht** begonnen.\n\n---\n\n## 1. Umgesetzte Architektur (Variante C)\n\n**Event-Cache als nicht-kritischer Vorfilter + synchroner HTTP-Final-Check als Autorität.**\n\n- **M15** = autoritative Safety-/Trading-Control-Instanz.\n- **M09** sendet neue Orders (OPEN/INCREASE) **nur**, wenn M15 eindeutig `ENABLED` + frisch bestätigt.\n- **REDUCE/CLOSE/CANCEL** (Risikoabbau) sind **immer** erlaubt — unabhängig vom M15-Status.\n\n### Kernregel\n| State | OPEN/INCREASE | REDUCE/CLOSE/CANCEL |\n|-------|---------------|---------------------|\n| `ENABLED` | **ERLAUBT** | erlaubt |\n| `PAUSED` | **VERBOTEN** | erlaubt |\n| `HALTED` | **VERBOTEN** | erlaubt |\n| `UNKNOWN` / M15-down / stale | **VERBOTEN (FAIL-CLOSED)** | erlaubt |\n\n---\n\n## 2. API-Vertrag M15 → M09 (additiv, kein Breaking Change)\n\n### `GET /control/trading-state`\n```json\n{\n \"trading_status\": \"ENABLED\", // ENABLED|PAUSED|HALTED|UNKNOWN\n \"system_status\": \"HEALTHY\",\n \"state_version\": 3, // monoton, je Status-Änderung inkrementiert\n \"timestamp\": \"2026-08-20T15:30:00Z\",\n \"expires_at\": \"2026-08-20T15:30:00.5Z\", // TTL-Horizont (now + 500ms)\n \"ttl_seconds\": 0.5,\n \"audit_id\": \"ctl-...\" // M15-interne Audit-Kennung\n}\n```\nNur `trading_status=ENABLED` **und** `expires_at` in der Zukunft = eindeutige Erlaubnis.\nTimeout/5xx/503 → **FAIL-CLOSED** (als UNKNOWN behandeln).\n\n### Event-Kanal (Event-Cache)\n- Exchange `market.control`, Routing-Key `trading.state` (nur bei Status-Änderung).\n- M09-Consumer `ControlCacheConsumer` speist den Event-Cache-Vorfilter.\n- Cache-TTL: 5 s. Stale-Cache → als UNKNOWN behandeln → OPEN/INCREASE blockiert.\n\n---\n\n## 3. M09-Implementierung\n\n### Neue Dateien\n- `app/control/client.py` — **ControlClient** (Event-Cache-Vorfilter + HTTP-Final-Check, FAIL-CLOSED)\n- `app/control/__init__.py`\n- `app/consumer/control_consumer.py` — **ControlCacheConsumer** (`market.control`/`trading.state`)\n\n### Geänderte Dateien\n- `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\n- `app/core/models.py` — `ExecutionOrder` + Audit-Felder\n- `app/config.py` — M15-URL/Timeout/TTL/Cache-TTL + Control-Exchange/Routing-Key\n- `app/storage/storage.py` — INSERT/UPDATE um control_*-Felder; `update_control_audit`\n- `migrations/001_execution.sql` — Audit-Spalten (idempotent via `ADD COLUMN IF NOT EXISTS`)\n\n### Audit-Felder je Order\n`action_type`, `control_status`, `control_state_version`, `control_expires_at`,\n`control_audit_id`, `control_check_source` (`http`|`cache`|`bypass`|`none`).\n\n### Parameter\n- Final-Check-HTTP-Timeout: **500 ms**, Retry: **max 1** sofort.\n- `expires_at`-TTL (M15): **500 ms**.\n- Cache-TTL (M09): **5 s**.\n- Kein Auto-Resume; kein \"Senden ohne Check\".\n\n---\n\n## 4. Live-E2E-Ergebnisse (VPS, 20.08.2026)\n\nAlle Szenarien auf dem VPS (187.124.31.123) live ausgeführt. **Alle grün.**\n\n| # | Szenario | Ergebnis |\n|---|----------|----------|\n| S1 | **ENABLED-Pfad**: OPEN-Order durch, Auditfelder in DB | **9/9 PASS** |\n| S2 | **HALTED-Pfad**: M03 gestoppt → OPEN blockiert, kein Broker-Send, reason `CONTROL_HALTED` | **9/9 PASS** |\n| S3 | **PAUSED-Pfad**: nicht-krit. Service down → OPEN blockiert, reason `CONTROL_PAUSED` | **4/4 PASS** |\n| S4 | **Risikoabbau bei HALTED**: REDUCE/CLOSE/CANCEL erlaubt (bypass) | **4/4 PASS** |\n| S5 | **M15-down**: OPEN FAIL-CLOSED (`CONTROL_UNREACHABLE`), REDUCE/CLOSE/CANCEL erlaubt | **7/7 PASS** |\n| S6 | **Cache-vs-Final-Check**: HTTP HALTED gewinnt über Cache | **2/2 PASS** |\n| S7 | **Race/TTL**: Doppel-Publish → keine Doppelorder (Idempotenz) | **2/2 PASS** |\n| S8 | **Event-Cache**: 1 Consumer, 0 Backlog, Reconnect nach RabbitMQ-Restart | **verifiziert** |\n| S9 | **Recovery**: nach Behebung wieder ENABLED, OPEN erlaubt | **3/3 PASS** |\n| S10 | **Health/Security**: health/ready 200, keine öffentl. Ports, keine Secrets, keine echten Tracebacks | **verifiziert** |\n| S11 | **Regression**: bestehender M09-Paper-E2E (Kette 03→09) | **ALLE CHECKS BESTANDEN** |\n\n### S1-Detail (ENABLED)\n```\ncontrol_status=ENABLED\ncontrol_state_version=1\ncontrol_expires_at=1787241066.98\ncontrol_audit_id=ctl-2073194e651040db\ncontrol_check_source=http\naction_type=OPEN\nbroker_order_id=PAPER-EXEC-... (Broker-Send erfolgt)\nstatus=FILLED\n```\n\n### S2-Detail (HALTED)\n```\ncontrol_status=HALTED\ncontrol_state_version=2\ncontrol_audit_id=ctl-b2cd6303f8754328\ncontrol_check_source=http\naction_type=OPEN\nbroker_order_id=None (KEIN Broker-Send)\nreason_codes=['CONTROL_HALTED']\nstatus=FAILED\n```\n\n### S5-Detail (M15-down)\n```\nreason_codes=['CONTROL_UNREACHABLE']\nbroker_order_id=None (FAIL-CLOSED, kein Broker-Send)\nREDUCE/CLOSE/CANCEL → control_check_source=bypass (erlaubt)\n```\n\n---\n\n## 5. Verifikation Health/Security\n\n- M09 + M15 `health` und `health/ready` → **200**.\n- Keine öffentlichen Ports (nur Docker-intern via `expose:`).\n- Keine Secrets in Logs/API/DB.\n- Tracebacks in Logs: ausschließlich pika `AMQPConnectionError` während des\n kontrollierten RabbitMQ-Restarts (erwartete Reconnect-Logs, keine echten Fehler).\n\n---\n\n## 6. Freigabe-Status\n\n- **FREIGEGEBEN / PRODUKTIV VERIFIZIERT** (20.08.2026, Nutzer-Bestätigung).\n- M15/M09-Control-Anbindung ist **final freigegeben** — keine weiteren technischen Änderungen an M09/M15.\n- M16: **nicht beginnen**.\n- Credential-Cleanup: temporäre Forgejo-Tokens/Deploy-Keys → count=0 (siehe Git-Commit).\n\n---\n\n*Implementierung + Live-E2E abgeschlossen. Finale Freigabe erteilt (20.08.2026).*\n"
},
{
"path": "modul-15-monitoring-control.md",
"title": "modul-15-monitoring-control",
"id": "object/c2641bbf-0612-0737-86fd-622d899109ca",
"type": "arch",
"role": "module",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "0f4e5ae6d701e26a361e2d8d10e1e7a316d698e083ba6bc0e56a5fafbc17746e",
"body": "# Modul-15: Monitoring-Control\n\n**Status: FREIGEGEBEN** (20.08.2026) · Container `Modul-15-Monitoring-Control` · Image `monitoring-control:0.1.0`\n\n## Zweck\nZentrale **technische Überwachung und Sicherheits-/Kontrollebene** des Trading-Systems.\nÜberwacht die Dienste M03M14 (Health/Readiness/Erreichbarkeit) plus RabbitMQ-Queues/Consumer\nund PostgreSQL und leitet daraus deterministisch einen **globalen Trading-Status**\n(`ENABLED` / `PAUSED` / `HALTED` / `UNKNOWN`) ab.\n\n**Restriktionen (Nutzer-Vorgabe):**\n- **Keine KI/ML**, **keine Trading-Entscheidungen**, **keine Brokerorders**, **keine Shell-/Docker-Control-Rechte**.\n- **FAIL-CLOSED:** Bei unklarem/kritischem Zustand wird Trading sicher blockiert (`HALTED`).\n- M15 unerreichbar → M09 behandelt den Zustand als `HALTED` (FAIL-CLOSED, siehe M15→M09-Anbindung, FREIGEGEBEN).\n- **Keine Event-Schleifen:** Events nur bei Status-Änderung, dedupliziert.\n- Container wird **nicht** automatisch neu gestartet/gekillt — M15 beobachtet und entscheidet Status.\n- Port 55015 **nur Docker-intern** (`expose`, kein Host-Port).\n\n## Architektur / Datenfluss\n```\nM03M14 (health/ready) RabbitMQ (Queues) PostgreSQL\n │ │ │\n └──────────┬────────────┴──────────────┘\n ▼\n Modul-15-Monitoring-Control\n ├── probe/ MonitoringProbe (HTTP-health + RMQ passive declare + PG)\n ├── rules/ MonitoringRules (deterministisch, FAIL-CLOSED, versioniert)\n ├── core/ MonitorService (Status-Engine, Single Source of Truth)\n ├── storage/ Persistenz (PostgreSQL: system_health/service_health/control_event)\n ├── publisher/ ControlPublisher (market.control, nur bei Status-Änderung)\n └── api/main.py REST-API (intern, Port 55015)\n```\nStatus → `system_health` (append), Services → `service_health` (upsert), Änderungen → `control_event` (append-only Audit).\n\n## API (Docker-intern, Port 55015, kein öffentliches Port-Mapping)\n```\nGET /health liveness\nGET /health/ready readiness (postgresql)\nGET /monitoring/status aktueller System-Status + Trading-Status (internes State-Modell)\nGET /monitoring/services aktueller Zustand aller überwachten Services/Queues\nGET /control/status Control-Status (manual_override, control_api_enabled)\nGET /control/trading-state autoritativer Trading-Status für M09\nPOST /control manuelle Control PAUSE/HALT/RESUME (nur wenn CONTROL_API_ENABLED=true)\n```\n\n## Deterministische Regeln (Rules-Engine, Version 0.1.0)\nPräzedenz strikt von oben nach unten (FAIL-CLOSED):\n1. **UNKNOWN** — PostgreSQL nicht erreichbar → nicht verlässlich → `HALTED`.\n2. **UNHEALTHY/HALTED** — kritische Infrastruktur (PG/RabbitMQ) oder Execution-Pfad down.\n3. **DEGRADED/PAUSED** — nicht-kritische Dienste down / stale.\n4. **HEALTHY/ENABLED** — alles gut.\n\n| Bedingung | System | Trading | Reason-Code |\n|---|---|---|---|\n| PostgreSQL nicht erreichbar | UNHEALTHY | HALTED | `POSTGRESQL_UNREACHABLE` (+ `PERSISTENCE_FAILURE`) |\n| RabbitMQ nicht erreichbar | UNHEALTHY | HALTED | `RABBITMQ_UNREACHABLE` |\n| Kritischer Service down (M03M09) | UNHEALTHY | HALTED | `CRITICAL_SERVICE_DOWN:<id>` |\n| Market Data stale (> max_age) | UNHEALTHY | HALTED | `MARKET_DATA_STALE` |\n| Kritische Queue Consumer fehlt | — | HALTED | `NO_CONSUMER_<queue>` |\n| Kritischer Queue-Backlog ≥1000 | — | HALTED | `QUEUE_BACKLOG_CRIT:<queue>:<n>` |\n| Nicht-kritische Unhealthy (Analytics/Notification) | DEGRADED | PAUSED | `CRITICAL_SERVICE_DOWN`/`NO_CONSUMER_…` |\n| Queue-Backlog ≥100 / Warning | DEGRADED | PAUSED | `QUEUE_BACKLOG:<queue>:<n>` |\n| Alles healthy | HEALTHY | ENABLED | — |\n\n**FAIL-CLOSED-Hinweis:** `UNKNOWN` Trading-Status wird in der API auf `HALTED` gemappt (nie ENABLED aus unklarem Zustand).\n\n## Events (market.control Exchange, keine Event-Schleife)\nEvents werden **nur bei Status-Änderung** publiziert (Idempotenz) — nie bei jedem Poll:\n\n| Event-Typ | Routing-Key | Anlass |\n|---|---|---|\n| `system.degraded` | `system.degraded` | System → DEGRADED |\n| `system.halted` | `system.halted` | System → UNHEALTHY (HALT) |\n| `system.recovered` | `system.recovered` | System → HEALTHY |\n| `trading.state` | `trading.state` | Trading-Status-Änderung |\n| `service.unhealthy` | `service.unhealthy` | Einzelner Service UNHEALTHY |\n\n## Persistenz / Audit (PostgreSQL Modul-01)\n- `system_health` — append-only Zeile pro Beobachtung (aktueller Status + reason_codes).\n- `service_health` — Upsert pro `service_id` (aktueller Zustand jedes Services/Queue).\n- `control_event` — **append-only Audit-Trail** jeder Status-/Trading-Änderung (`STATE_CHANGE`) und manuellen Control-Aktion (`MANUAL_*`). Idempotent — eine Änderung wird **genau einmal** auditiert.\n\n## Manuelle Control (PAUSE/HALT/RESUME)\n- **Nur wenn `CONTROL_API_ENABLED=true`** (Default `false` → manuelle Control über API deaktiviert, nur Regel-getrieben).\n- **RESUME wird verweigert** (`RESUME_BLOCKED_CRITICAL_CAUSE`), solange eine kritische Ursache besteht.\n- Jede manuelle Aktion wird append-only in `control_event` auditiert.\n\n## Sicherheit\n- **Keine Secrets in DB/Logs/Doku/Commits** — Zugangsdaten ausschließlich via Environment (Compose).\n- **Kein Docker-Socket**, **keine Shell-/Docker-Control-Rechte** (CMD=`python main.py`).\n- Port 55015 nur intern (`expose`), `restart: unless-stopped`, non-root (uid 1001).\n- Env: `POSTGRES_*`, `RABBITMQ_*`, `CONTROL_API_ENABLED`.\n\n## Compose / Betrieb\n- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL`, RabbitMQ `Modul-02-RabbitMQ`.\n- Env per Compose, Zugangsdaten nur via ENV (`POSTGRES_*`, `RABBITMQ_*`), nie hart kodiert.\n\n## Unit-Tests\n- `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.\n\n## E2E-Verifikation (VPS, 20.08.2026) — 17 Szenarien\nAlle 17 verifiziert (System-/Trading-Status via Loop, API `/control/trading-state` + `/monitoring/status` konsistent):\n1. **Normalzustand:** `HEALTHY/ENABLED` — API, State-Modell, Loop identisch.\n2. **PG-Ausfall:** `HALTED`, `POSTGRESQL_UNREACHABLE`+`PERSISTENCE_FAILURE`, Audit genau einmal.\n3. **RabbitMQ-Ausfall:** `HALTED`, `RABBITMQ_UNREACHABLE`.\n4. **Risk-Ausfall:** `HALTED`, `CRITICAL_SERVICE_DOWN:modul-07-risk-manager`+`NO_CONSUMER_risk.input`.\n5. **Portfolio-Ausfall:** `HALTED`, `CRITICAL_SERVICE_DOWN:modul-08-portfolio-manager`.\n6. **Execution-Ausfall:** `HALTED`, `CRITICAL_SERVICE_DOWN:modul-09-execution-service`.\n7. **Analytics-Ausfall:** `DEGRADED` + `PAUSED` (Trading nicht vollständig kill).\n8. **Notification-Ausfall:** `DEGRADED` + `PAUSED`.\n9. **Market Data stale:** `HALTED`, `MARKET_DATA_STALE`; Recovery nach frischen Daten.\n10. **Kritischer Consumer fehlt:** HALT-Severity korrekt (NO_CONSUMER auf kritischer Queue).\n11. **Queue-Backlog:** Warning/HALT gemäß Schwelle (100/1000, Unit-getestet).\n12. **Recovery:** Ursache erneut geprüft, kein Auto-RESUME solange Ursache, Status konsistent, kein Event-Spam.\n13. **Manuelle Controls:** mit `CONTROL_API_ENABLED=false` sauber abgelehnt (`control_api_disabled`); Trading unangetastet.\n14. **Idempotenz/Event-Flut:** stabiler Zustand → keine wiederholten SYSTEM_HALTED/SERVICE_UNHEALTHY (nur 1 Audit je Änderung).\n15. **RabbitMQ-Reconnect:** automatisch (alle Recovery-Zyklen), keine Zombies, keine Eventverluste.\n16. **Audit:** `control_event`/`system_health`/`service_health` vollständig + zeitlich nachvollziehbar.\n17. **Security:** kein öffentlicher Port (PortBindings `{}`), kein Docker-Socket, keine Secrets im Compose/Logs.\n\n## Bekannte Design-Hinweise (für separaten M15→M09-Vorschlag)\n- **M03M06 (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).\n- **DEGRADED → `PAUSED`** (nicht `ENABLED`): bei Analytics/Notification-Ausfall wird Trading vorsichtig gestoppt, nicht vollständig killed. Design-Entscheidung, im Vorschlag benannt.\n- **PG-Ausfall ⇒ `PERSISTENCE_FAILURE`**: ohne PG kann `control_event`-Audit nicht geschrieben werden (fachlich korrekt FAIL-CLOSED), aber Audit-Lücke im Fehlerfenster.\n\n## Freigabe\n- **FREIGEGEBEN** (20.08.2026, Nutzer-Bestätigung). Implementierung + E2E (17 Szenarien) grün.\n- M09-Anbindung **umgesetzt** (Variante C, Event-Cache + HTTP-Final-Check) — siehe `modul-15-m09-anbindung-implementierung.md`, FREIGEGEBEN.\n\n## Nächste Module\n- **Modul-16 ff.** — noch Platzhalter (alpine), NICHT gestartet.\n\n---\n## Geändert\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-15-Monitoring-Control dokumentiert — implementiert + E2E-verifiziert (17 Szenarien grün),\n Rules/API/Events/Audit/Security dokumentiert, NICHT FREIGEGEBEN.\n```\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-15-Monitoring-Control auf FREIGEGEBEN gesetzt (Nutzer-Bestätigung 20.08.2026).\n M15→M09-Anbindung (Variante C) als umgesetzt/FREIGEGEBEN vermerkt; Unit-Tests 19/19.\n```\n"
},
{
"path": "modul-18-position-manager.md",
"title": "modul-18-position-manager",
"id": "object/2ad746ca-67f1-ce90-1304-4a1f4b052eb7",
"type": "arch",
"role": "module",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "837c05d5b8cd5311cb91828e52d40f80b8433117cc1bf5b8ed8b7e27ad0b31ae",
"body": "# Modul-18: Position-Manager\n\n**Status: FREIGEGEBEN (21.08.2026)** · Container `Modul-18-Position-Manager` · Port `55018` (nur intern/expose)\n\n## Zweck\nModul-18 verwaltet **bereits offene Positionen** deterministisch: Es beobachtet offene Trades und gibt auf Basis fester\nExit-Regeln (Stop-Loss, Take-Profit, Break-even, Trailing, Time-Exit) gezielte **Exit-Aktionen** an Modul-09.\n\n**Kritische Grenze:** M18 ist ein **reiner Exit-/Positions-Manager**, KEIN Signal-/Einstiegsmodul.\n- ✅ M18 darf NUR `REDUCE` / `CLOSE` / `CANCEL` an M09 geben. **NIE `OPEN` / `INCREASE`.**\n- ❌ Kein eigenmächtiges Nachkaufen/Pyramiding. Keine KI/ML. Keine direkte Broker-Anbindung.\n- Jede Order-Aktion läuft ausschließlich über **M09 Execution** via RabbitMQ `trade.approved`.\n- **FAIL-CLOSED:** stale/unbekannter Zustand → nie blind senden; Restart/Reconnect erzeugt keine Doppelaktion; keine Zombies.\n\n## Architektur / Datenfluss\n```\nModul-03-Market-Data (Preise) ──┐\n ▼\nModul-10-Trade-Journal (OPEN) ──► Modul-18-Position-Manager (Port 55018, NUR intern)\nModul-15 (Trading-State, readonly) │\n ├── Engine-Loop (deterministische Exit-Regeln)\n ├── Publisher → RabbitMQ market.portfolio / trade.approved → M09\n └── Consumer ← RabbitMQ market.execution / order.filled (Reconciliation)\n │\n ▼\n Modul-09-Execution-Service (PaperBroker: broker=paper) → order.filled\n```\n\n**App-Struktur (`app/`):**\n- `core/engine.py` — Exit-Regel-Engine (Regel-Priorität deterministisch)\n- `core/orchestrator.py` — Hydrate-State, wendet Regeln an, idempotent\n- `publisher/publisher.py` — publiziert Aktionen an M09 (`trade.approved`, je Aktion eine eindeutige `action_id`)\n- `consumer/consumer.py` — konsumiert M09-Execution-Events (`order.filled` etc.) für Reconciliation\n- `pricing/client.py` — liest Preise von M03, inkl. **Stale-Guard**\n- `providers/journal.py` — Quelle offener Positionen (Trade-Journal, `status=OPEN`)\n- `storage/storage.py` — Persistenz (`position_state`/`position_action`/`position_event`)\n- `api/main.py` — REST-API (`/health`, `/readiness`), Port 55018 nur intern\n\n**Eigene Tabellen (`migrations/001_position.sql`):**\n- `position_state` — aktueller verwalteter Zustand je Position\n- `position_action` — jede an M09 gegebene Order-Aktion (REDUCE/CLOSE/CANCEL)\n- `position_event` — append-only Audit-Event-Historie\n\n## Exit-Regeln (deterministisch)\n| Regel | Trigger | Aktion |\n|-------|---------|--------|\n| Stop-Loss | Preis ≤ stop_loss | CLOSE |\n| Take-Profit | Preis ≥ target | CLOSE |\n| Break-even | Preis günstig, SL auf Entry gesetzt | CLOSE bei BE-SL |\n| Trailing | Preis weiter günstig (best_price_seen) | CLOSE bei Trailing-SL |\n| Time-Exit | max. Haltedauer erreicht | CLOSE |\n| Partial-Fill / Duplikat | idempotente Aktion | exakt 1 Order, keine Doppelaktion |\n| HALTED (M15) | Trading-State HALTED | CLOSE weiterhin erlaubt (Exit) |\n| stale Preis | Kerze älter als `stale_price_max_age_seconds` (3600s) | **FAIL-CLOSED: keine Aktion** |\n\nRegeln laufen mit fester Priorität; jede Exit-Aktion wird als eindeutige `position_action` (action_id) persistiert.\n\n## M09-CLOSE-Pfad (Reduktion → Execution)\n```\nM18 position_action (action_id, CLOSE) \n → Publisher → RabbitMQ exchange \"market.portfolio\" routing \"trade.approved\"\n → M09 Execution (control/client bypass für REDUCE/CLOSE/CANCEL) → PaperBroker (broker=paper)\n → execution_order (source_portfolio_decision_id = M18 action_id, status FILLED)\n → RabbitMQ \"market.execution\" routing \"order.filled\"\n → M18-Consumer → Reconciliation (position_state → CLOSED, qty=0, Audit-Event)\n```\n**Verifikation:** Für jede Exit-Aktion existiert **exakt 1** execution_order mit `broker=paper`, `status=FILLED`,\n`source_portfolio_decision_id` = M18 `action_id`. M09 speichert die M18-`action_id` als\n`source_portfolio_decision_id` (top-level + Payload). Keine Doppelaktionen je action_id (HAVING count>1 = 0).\n\n## Reconciliation über order.filled\nM18 konsumiert M09-Execution-Events (`order.filled`, `order.partially_filled`, `order.rejected`, `order.failed`)\nund gleicht den eigenen `position_state` ab: FILLED → Position als `CLOSED` markieren, `quantity=0`, Audit-Event\n`ORDER_EXECUTED`/`ORDER_FILLED`. Damit sind M10 (Journal) und M18 (Manager) konsistent, auch wenn M10 CLOSE\nnicht automatisch verknüpft.\n\n## Thread-Local-DB-Fix (Root Cause „connection pointer is NULL\")\n**Symptom:** Reconciliation scheiterte sporadisch mit `connection pointer is NULL`.\n**Root Cause:** `PositionStorage._conn` war eine **geteilte Singleton-Verbindung** zwischen Engine-Loop-Thread und\nExecutionConsumer-Thread. psycopg2 ist **nicht thread-safe**: ein Thread schloss die Verbindung\n(`resolve_position_key`/`has_pending_exit`), der andere Thread lief auf totem Cursor.\n**Fix:** **Thread-Local-Verbindung** — `self._tl = threading.local()`; jeder Thread erhält eine eigene\npsycopg2-Verbindung; kein Cross-Thread-`.close()`. `close()`/`rollback()` (ensure_schema) auf lokale Verbindung umgestellt.\n**Verifikation:** Unit-Tests **13/13 grün**, E2E **47/47 PASS**.\n\n## Testdesign-Lessons (E2E)\n1. **action_id statt Journal-pd_id:** M09 speichert die M18-`action_id` als `source_portfolio_decision_id` —\n Verifikation muss `wait_execution_for_action(action_id)` nutzen, nicht Journal-`pd_id`.\n2. **Audit-Eventname:** Consumer schreibt `ORDER_EXECUTED` (bzw. `ORDER_FILLED`); Test akzeptiert beide.\n3. **Szenario 10 RA/RB:** Verifikation über `action_id`, nicht `pd_id`.\n4. **Stale-Szenario:** `stale_price_max_age_seconds=3600`; eine 5-min-Kerze ist NICHT stale. M03 akzeptiert\n bis 30 Tage alte Kerzen, persistiert injiziertes `ts` aber nicht (vergibt frisches ts). Fix: 2h-alte Kerze\n (>1h < 30Tage) injizieren + **pro-Lauf eindeutiges Symbol** (M03-Speicher hat keinen Delete-Endpoint).\n\n## Safety (User-Vorgabe, kritisch)\n- **NUR REDUCE/CLOSE/CANCEL** — niemals OPEN/INCREASE. Kein Nachkaufen/Pyramiding.\n- Idempotent + race-safe + **FAIL-CLOSED**. Restart/Reconnect keine Doppelaktion. Keine Zombies.\n- Broker/Execution unklar → nie blind erneut senden.\n- Vollständiger Audit-Trail (`position_event` append-only).\n- Port 55018 nur Docker-intern (`expose`), kein öffentliches Mapping. Netz `trading-modules`.\n- Keine Secrets in Logs/API/DB.\n\n## Tests & E2E-Verifikation (VPS, 21.08.2026)\n- **Unit-Tests** `tests/test_engine.py`: **13/13 grün** (Thread-Local-Fix).\n- **E2E** `tests/e2e_m18_full2.py`: **47/47 PASS** (EXIT=0), deterministischer Lauf 015:\n HOLD · SL→CLOSE · TP→CLOSE · Break-even · Trailing · Time-Exit · Partial-Fill · bereits CLOSED · Duplikat ·\n parallel/race-safe (RA/RB) · M09 down · M15 HALTED · **stale Preis (FAIL-CLOSED)** · Restart · Reconciliation order.filled.\n- Für jede Exit-Aktion: exakt 1 Execution je action_id, `broker=paper`, `status=FILLED`, keine Doppelaktionen,\n keine offenen Zombie-Testpositionen.\n- **Regression M03→M09:** M18 erzeugt **NIE OPEN/INCREASE** (nur CLOSE). M09 execution_order:\n nur `CLOSE`, alle `FILLED`, `broker=paper`, keine Doppel-Order je action_id.\n- **Health/Security:** `/health` + `/readiness` 200; Port 55018 nur intern; keine Secrets in\n Logs/API/DB; keine Tracebacks seit finalem Lauf; Consumer/RMQ-Reconnect sauber.\n\n## Testdaten / E2E-Fixtures\n- Alle M18-E2E-Fixtures tragen ausschließlich das Präfix **`M18E2E2-…`** in `trade_journal`, `position_state`,\n `position_action`, `execution_order` und M03 `ohlcv` (z. B. `M18E2E2-SL`, `M18E2E2-STALE`, …).\n- M03-Testrest-Symbole (`M18E2E2-*` in `ohlcv`) werden **NICHT destruktiv gelöscht** (M03-Speicher hat keinen\n sauberen Delete-Endpoint), sondern sind als **Testdaten** dokumentiert und klar vom Produktiv-Universe getrennt.\n- **Kein Produktiv-Service/Scanner/Universe referenziert `M18E2E2`** (verifiziert: kein VPS-Config/Code-Code greift\n es auf). M18 sendet nur CLOSE/REDUCE/CANCEL an bereits offene Positionen und erzeugt **kein OPEN** → Test-Symbole\n können nie in ein produktives Signal-/Scanner-Universum gelangen.\n- Produktionsdaten (nicht `M18E2E2-*`-Symbole) bleiben unberührt.\n\n## Compose / Betrieb\n- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL` (Modul-01), RabbitMQ `Modul-02-RabbitMQ`.\n- Port 55018 nur `expose` (Docker-intern), kein Host-Mapping. `restart: unless-stopped`.\n- Keine Secrets im Compose-Env.\n\n---\n## Geändert\n```\nGeändert von: Rain Ocampo\nDatum: 21.08.2026\nGrund: Modul-18-Position-Manager formal abgeschlossen + FREIGEGEBEN. E2E 47/47 PASS, Regression M03→M09 gruen\n (nur CLOSE, nie OPEN/INCREASE), Health/Security gruen (Port 55018 intern, keine Secrets/Tracebacks).\n Thread-Local-DB-Fix (psycopg2 \"connection pointer is NULL\" geteiltes Singleton → Thread-Local). Doku angelegt.\n```\n"
},
{
"path": "modul-19-broker-reconciliation.md",
"title": "modul-19-broker-reconciliation",
"id": "object/3f834840-867d-9e9f-c7d2-8f1caf1550a4",
"type": "arch",
"role": "module",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "b6d511c0ab8f325f8b4f79cead238fd0d9c965cf6b2bedff16ca81b2139d8fce",
"body": "# Modul-19: Broker-Reconciliation\n\n**Status: FREIGEGEBEN (21.08.2026)** · Container `Modul-19-Broker-Reconciliation` · Port `55019` (nur intern/expose)\n\n## Zweck\nModul-19 gleicht die **interne Trading-Realität** (M10 Trade-Journal, M18 Position-Manager) gegen die\n**Broker-/Execution-Realität** ab. Es erkennt Diskrepanzen (fehlende/überschüssige Positionen, Mengen-,\nRichtungs- oder Statusabweichungen) und erzeugt daraus **Audit + Alarm**, ohne selbst zu handeln.\n\n**Kritische Grenze:** M19 ist ein **reiner Reconciliation-/Monitoring-Dienst**, KEIN Trade-Ausführer.\n- ✅ M19 erzeugt **NIE** `OPEN` / `INCREASE` (keine automatische Reparatur, kein Ordering).\n- ✅ M19 schreibt ausschließlich in eigene Audit-Tabellen (`reconciliation_*`), liest fremde Tabellen **read-only**.\n- ❌ Keine KI/ML. Keine direkte Broker-Anbindung in V1.\n- **FAIL-CLOSED:** unklare/unerreichbare Broker-Realität → STALE/UNKNOWN/Alarm + Audit, nie blind handeln.\n- Keine Zombie-Konsumenten; Restart/Reconnect erzeugt keine Doppel-Events (idempotent).\n\n## V1-Grenze: „Brokerrealität“ = rekonstruiert aus M09 `execution_order`\n> ⚠️ **WICHTIG (V1-Akzeptanz):** In V1 gibt es **keinen echten Broker-Adapter**. Die „Brokerrealität\"\n> wird **rekonstruiert** aus der von M09 persistierten Tabelle `execution_order` (PaperBroker hält sein\n> Orderbuch nur In-Memory; die einzig nachweisbare Quelle ist die DB-Persistenz).\n>\n> Das ist für V1 **akzeptabel**, aber **NICHT gleichwertig mit echter Broker-Reconciliation**.\n> Echte Broker-Reconciliation (Abruf von Live-Positionen/Fills beim Broker/MT5) folgt **nach M19** über\n> einen **Demo-Broker-Adapter** (Plugin am `BrokerStateProvider`-Interface).\n\n## Architektur / Datenfluss\n```\nM09 Execution (execution_order, status SUBMITTED/PARTIALLY_FILLED/FILLED) ──┐ Broker-Realität (V1)\nM10 Trade-Journal (trade_journal, status=OPEN) ─────────────────────────────┤ interne Realität\nM18 Position-Manager (position_state, position_key=trade_id) ───────────────┘\n │\n ▼\nModul-19-Broker-Reconciliation (Port 55019, NUR intern)\n ├── BrokerStateProvider (Interface, V1=paper) → Broker-Positionen (netto je Symbol)\n ├── InternalStateReader → interne offene Positionen (Journal + M18 state)\n ├── ReconciliationEngine → Klassifikation je Symbol/Position\n ├── Storage → reconciliation_run / reconciliation_result / reconciliation_event (idempotent)\n └── Publisher → RabbitMQ exchange \"reconciliation\" routing \"reconciliation.alert\" (additiv, M15 kann später konsumieren)\n```\n\n**App-Struktur:**\n- `broker/base.py` — `BrokerStateProvider`-Interface (V1=paper, später live/demo-Adapter)\n- `broker/paper.py` — V1-Paper-Provider: rekonstruiert Broker-Positionen aus `execution_order` (read-only)\n- `broker/registry.py` — Provider-Registry\n- `core/internal_state.py` — liest interne offene Positionen (M10 + M18)\n- `core/engine.py` — Reconciliation-Klassifikation (deterministisch)\n- `core/service.py` — Loop-Orchestrator, publiziert kritische Alarme, **Trade-Actions hart deaktiviert**\n- `storage/storage.py` — Persistenz (eigene Tabellen), Thread-Local-DB (psycopg2 nicht thread-safe)\n- `publisher/publisher.py` — Alarm-Publish auf RMQ (additive Schnittstelle)\n- `api/main.py` — REST-API (`/health`, `/health/ready`, `/reconcile`, `/reconcile/runs`, `/reconcile/results`), Port 55019 intern\n\n**Eigene Tabellen (`migrations/001_reconciliation.sql`):**\n- `reconciliation_run` — Lauf (run_id PK, status RUNNING/COMPLETED/FAILED, totals)\n- `reconciliation_result` — Ergebnis je Position/Symbol (idempotent: UNIQUE(run_id, position_key))\n- `reconciliation_event` — append-only Alarm-/Audit-Events\n\n## Klassifikationen (deterministisch, V1)\n| Klassifikation | Bedingung | Konsequenz |\n|----------------|-----------|------------|\n| `MATCH` | intern offen == Broker offen (Symbol, Richtung, Menge) | kein Event |\n| `QUANTITY_MISMATCH` | Broker-Menge ≠ interne Menge | CRITICAL-Event |\n| `DIRECTION_MISMATCH` | Broker-Richtung ≠ interne Richtung | CRITICAL-Event |\n| `MISSING_BROKER` | intern offen, Broker hat Position | CRITICAL-Event |\n| `MISSING_INTERNAL` | Broker offen, intern **kein** Datensatz | CRITICAL-Event |\n| `STATUS_MISMATCH` | intern kennt Position, aber CLOSED/abweichend vs. Broker offen | CRITICAL-Event |\n| `PARTIAL_FILL` | Broker PARTIALLY_FILLED → Menge berücksichtigt | (Konsistenz) |\n| `STALE` / `UNKNOWN` | Broker-Realität unklar/unerreichbar | **FAIL-CLOSED**, kein Trade, Alarm + Audit |\n\n**Hinweis (Design):** `MISSING_INTERNAL` = Broker offen, intern hat gar keinen Datensatz. `STATUS_MISMATCH` =\nintern kennt die Position, aber sie ist CLOSED/abweichend während der Broker offen ist (vorher fälschlich\nMISSING_INTERNAL). Beides sind kritische Abweichungen, die als `reconciliation.alert`-CRITICAL gemeldet werden.\n\n## Interne Positionswahrheit (Kernregel, 21.08.2026)\n> ⚠️ **Kernregel (User, 21.08.2026):** Eine externe Broker-Position darf nur `MATCH` sein, wenn eine\n> **echte interne OPEN-Position** existiert. **Interne Positionswahrheit = `position_state` (M18).**\n> `trade_journal`/`execution_order` allein dürfen **KEINE** interne offene Position vortäuschen.\n\n**Vorher (Bug):** `get_internal_open()` las `trade_journal` (M10) als Basis. Ein `trade_journal`-Eintrag mit\n`status='OPEN'` galt als intern offen, auch **ohne** `position_state` (M18). Folge: IG-Position existierte,\nintern nur `execution_order`/`trade_journal` → M19 klassifizierte fälschlich `MATCH` mit qty 0 (**Fake-MATCH**).\n\n**Nachher (Fix):** `get_internal_open()` liest **primär `position_state` (M18)** als Treiber. Ein\n`trade_journal`-Eintrag **ohne** `position_state` zählt **nicht** als intern offen → Broker-Position wird\n`MISSING_INTERNAL` statt Fake-MATCH. CLOSED `position_state`-Einträge werden mitgeliefert, damit die Engine\n`STATUS_MISMATCH` erkennen kann.\n\n**Erwartete Klassifikation (Kernregel):**\n| Situation | Klassifikation |\n|-----------|----------------|\n| IG OPEN + kein `position_state` | `MISSING_INTERNAL` |\n| IG OPEN + `position_state` OPEN + gleiche qty/direction | `MATCH` |\n| IG OPEN + `position_state` CLOSED | `STATUS_MISMATCH` |\n| IG OPEN + andere qty | `QUANTITY_MISMATCH` |\n| IG OPEN + andere direction | `DIRECTION_MISMATCH` |\n| Intern OPEN + IG flat | `MISSING_BROKER` |\n\n**Wichtig:** `qty=0` darf **niemals** zu `MATCH` führen (Engine: `internal_open` erfordert `qty>0`).\n\n## Safety (hart)\n- `allow_trade_actions` ist **hart `False`** (Konfiguration + API erzwingen; `/reconcile` lehnt ab wenn True).\n- M19 erzeugt **niemals** `OPEN`/`INCREASE`, keine automatische Reparatur, überschreibt Brokerrealität nie.\n- Bei `UNKNOWN`/`STALE`/`UNREACHABLE` → FAIL-CLOSED + Audit + Alarm, nie blind senden.\n- M19 ist reiner **Publisher** auf RMQ (kein Consumer → keine Zombie-Konsumenten).\n\n## Deployment\n- Port `55019` **nur intern** (`expose`, keine Host-Port-Bindings — wie M18).\n- Netz `trading-modules_trading-modules`, `restart: unless-stopped`.\n- RMQ-Host `Modul-02-RabbitMQ`, vhost `trading`. PG-Host `Modul-01-PostgreSQL`.\n- Reconciliation-Loop `loop_interval_seconds=60`, `loop_enabled=True`.\n\n## Tests\n- **Unit:** `tests/test_engine.py` — 16/16 grün (Klassifikation, Provider, Safety `test_never_allows_trade`,\n Kernregel `qty=0` nie MATCH, journal-ohne-position_state → MISSING_INTERNAL).\n- **E2E:** `tests/e2e_m19_full.py` — 14/14 grün (MATCH, QUANTITY, DIRECTION, MISSING_BROKER/INTERNAL,\n PARTIAL_FILL, STATUS_MISMATCH, Duplikat/Idempotenz, parallel/race-safe, Health/Readiness, Event-Dedup,\n Audit-Trail, **S13 Fake-MATCH → MISSING_INTERNAL**).\n- **Infrastruktur (live, VPS):** Broker-DB unreachable → FAIL-CLOSED (broker_reachable:false,\n trade_actions_allowed:false); Restart sauber; RMQ-Down → kein Crash (gehaltene pika-Warnungen);\n RMQ-Reconnect ohne Zombies; M09-Ausfall → M19 arbeitet weiter (Quelle ist PG `execution_order`); Health+Readiness 200; Port nur intern.\n\n## Regression M03→M09→M18\nAlle Kettenglieder healthy nach M19-Deployment (M03/M05/M08/M09/M10/M15/M18 `/health` = ok).\nM19 schreibt ausschließlich in `reconciliation_*`; fremde Tabellen (`execution_order`, `trade_journal`,\n`position_state`) bleiben unberührt. Keine M19-Konsumenten an der Trading-Kette; M19-Exchange `reconciliation`\nist additiv (M15 kann künftig konsumieren). Keine Produktivpositionen — alle Datensätze sind M18E2E2/M19E2E2-Testreste.\n\n## Bugs / Lessons Learned\n1. **RMQ-vhost `/` vs `trading`:** M18 nutzt vhost `trading`; M19 musste auf vhost `trading` korrigiert werden\n (anfänglich `/` → Fehler). Nach Fix sauber.\n2. **Envs-Präfix `M19_`:** pydantic-settings `env_prefix=\"M19_\"` — Envs wie `M19_PG_HOST`, `M19_RABBITMQ_HOST`\n verwenden, NICHT bare `PG_HOST` (Falle beim Isolationstest). Produktiv-Container trägt noch bare `PG_*`/\n `RABBITMQ_*`-Envs (M18-Muster) = tote Envs; M19 greift auf korrekte pydantic-Defaults\n (`Modul-01-PostgreSQL`, `Modul-02-RabbitMQ`, vhost `trading`) zurück. Für saubere Konsistenz künftig Envs\n auf `M19_`-Präfix umstellen.\n3. **psycopg2 nicht thread-safe:** Storage nutzt `threading.local()` pro Thread (Fix aus M18 übernommen).\n4. **`MISSING_INTERNAL` vs `STATUS_MISMATCH`:** sauber getrennt (s. o.), sonst fälschliche Klassifikation.\n5. **Broker-Realität rekonstruiert:** V1 akzeptiert `execution_order` als Broker-Realität; echter Adapter folgt\n nach M19 (Demo-Broker-Adapter), siehe oben.\n6. **Keine Host-Port-Bindings** (nur `expose`) — wie M18, keine öffentliche API.\n7. **Fake-MATCH (Root Cause, 21.08.2026):** `get_internal_open()` las `trade_journal` (M10) als Basis. Ein\n `trade_journal`-Eintrag mit `status='OPEN'` galt als intern offen, auch **ohne** `position_state` (M18).\n Folge: IG-Position existierte, intern nur `execution_order`/`trade_journal` → M19 klassifizierte fälschlich\n `MATCH` mit qty 0. **Fix:** Interne Positionswahrheit primär aus `position_state` (M18); `trade_journal`/\n `execution_order` allein vortäuschen keine interne offene Position → `MISSING_INTERNAL`. `qty=0` nie MATCH.\n"
},
{
"path": "notes/ai-agents/ai-agents.md",
"title": "AI-Agents Übersicht",
"id": "object/fb050d49-41a1-a58b-3baf-e95a1765c7ed",
"type": "arch",
"role": "hub",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "c87919acc9cd6c622b91a14ae305f241535bbda1f96670d24b6941d95574e1ea",
"body": "\n\n# AI-Agents\n\nMulti-Agent-System von Christian List (Resident-Evil-Theme).\n\n## Agenten\n\n- **Alice** — OpenClaw-Primäragent (v2026.4.29, Port 55163)\n- **Matt Addison** — Backup-Agent (v2026.7.1-2, Port 54524)\n- **Rain Ocampo** — Hermes-Support (dieser Agent, [[vps-infrastruktur]])\n\n## Betriebsdetails\n\nSiehe [[alice-betrieb]] für Alices Konfiguration und Betrieb.\n\n## Wichtig\n\n- Notion: Code-Recovery der MT5-EAs, Attribution-Pflicht (`Geändert von: Alice|Rain Ocampo`)\n- Forgejo: Docs-Repo `trading-system-docs`, SSH-Port 22222\n"
},
{
"path": "notes/ai-agents/alice-betrieb.md",
"title": "Alice Betrieb",
"id": "object/6923ab6b-5132-55d7-4fd5-66890048e12b",
"type": "arch",
"role": "note",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "92809fa8876328f125a7032fa4416a786927c9501dee95a68f57f00d1848bc20",
"body": "\n# Alice Betrieb\n\n## Container\n\n- Name: `openclaw-suqw-openclaw-1`\n- Port: 55163 (direkt HTTP 200)\n- Version: 2026.7.1-2\n- Modell: `ollama/deepseek-v4-flash:cloud`\n- contextWindow: 200000, maxTokens: 8192\n\n## Verbindungen\n\n- Telegram: @nexo7V74756_bot\n- Ollama: `http://ollama-nb6d-ollama-1:11434`\n- Domain: `alice.projektaliceimhive.com` (derzeit HTTP 404 — Traefik-Konfig fehlerhaft, `ProjectAlice.yaml` hat duplizierte YAML-Keys)\n\n## Bekannte Probleme\n\n- Context-Overflow bei hoher Session-Größe → Session-Reset nötig (Backup + `sessions.json`-Eintrag entfernen)\n- Telegram-Duplicate-Loop durch stuck Session-State\n- `lossless-claw`-Plugin blockiert (uid-Mismatch), `n8n`-Plugin stale\n\nSiehe auch [[ai-agents]].\n"
},
{
"path": "notes/inbox/inbox.md",
"title": "Inbox",
"id": "object/7e4314d1-6c2c-df92-e40d-c3bc04d88182",
"type": "arch",
"role": "index",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "0c33d495f172f7b959da144e3fc7de971098806d23e4a6490e8007f6441269b4",
"body": "\n# Inbox\n\nSammelort für ungeordnete Ideen, bevor sie in Bereiche sortiert werden.\n\n## Offene Punkte\n\n- [ ] Tolaria-Web-UI: echtes Dateisystem-Zugriff (Mock-Handler) prüfen\n- [ ] Alice-Domain 404: Traefik-`ProjectAlice.yaml`-Fehler separat fixen\n- [ ] Vault mit weiteren Referenz-Notizen befüllen\n"
},
{
"path": "notes/projects/projekte.md",
"title": "Projekte",
"id": "object/2325c9b5-a668-9a04-3cf3-38be914b170c",
"type": "arch",
"role": "index",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "4ef2386c15aff40ecd040a3e91d2e849f001aa1df384ecdffa9163868f3aea15",
"body": "\n# Projekte\n\n## Aktive Projekte\n\n- **MT5-Expert-Advisors** — Module 0115, M15→M09-Control-Anbindung freigegeben\n- **Tolaria als Second Brain** — dieses Vault\n- **OpenClaw-Multi-Agent** — Alice/Matt/Rain betreiben\n\nSiehe [[ai-agents]] und [[trading]].\n"
},
{
"path": "notes/reference/vps-infrastruktur.md",
"title": "VPS Infrastruktur",
"id": "object/9bd5b7db-bb0f-ae22-3a65-9410b77ad3de",
"type": "arch",
"role": "reference",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "10746cd0a4a2f212dd752d5094d055e43ad3fc179d7787e61244678a606c13f8",
"body": "\n\n# VPS-Infrastruktur\n\n## Kern-Container\n\n- **Forgejo** (Git): `forgejo-c4u8yyi1eaz1gepn3pqmr5fb`, API `127.0.0.1:3000`, SSH-Port 22222, Nutzer `nexo312`\n- **Forgejo-Postgres**: `postgresql-c4u8yyi1eaz1gepn3pqmr5fb`, DB `forgejo`\n- **OpenClaw Alice**: `openclaw-suqw-openclaw-1` (Port 55163)\n- **OpenClaw Matt**: `openclaw-3sgu-openclaw-1` (Port 54524)\n- **Ollama**: `ollama-nb6d-ollama-1` (Port 11434)\n\n## Sicherheit\n\n- Secrets nie in Logs/API/DB\n- Forgejo-Credentials: nur user-owned Alice-Keys/Tokens, temporäre gelöscht\n\nSiehe auch [[trading]].\n"
},
{
"path": "notes/start.md",
"title": "Vault-Start",
"id": "object/4dd3d5ea-da35-5265-a501-a2108253464e",
"type": "arch",
"role": "index",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "c441c7fae0da1069fdc98d8db66e8e82ec17cba83fd7ca03546516d1558a5e69",
"body": "\n\n# Vault-Start\n\nWillkommen im Tolaria-Second-Brain von Christian List.\n\nDieses Vault ist ein Markdown-Wissensspeicher mit YAML-Frontmatter.\nTolaria arbeitet Git-first: jede Änderung ist versioniert.\n\n## Bereiche\n\n- [[ai-agents]] — Multi-Agent-System (Alice, Matt, Rain)\n- [[trading]] — MT5-Expert-Advisors und Trading-Systeme\n- [[projekte]] — Laufende Projekte\n- [[reference]] — Referenz-Notizen\n- [[inbox]] — Schnellnotizen vor der Sortierung\n\n## Struktur\n\n- `notes/ai-agents/` — Agent-Konfigurationen, Rollen, Betrieb\n- `notes/trading/` — EAs, Module, Strategien\n- `notes/inbox/` — ungeordnete Ideen\n- `notes/projects/` — Projekt-Snapshots\n- `notes/reference/` — dauerhafte Referenz\n"
},
{
"path": "notes/trading/second-brain/Phase10c_Slippage_Deterministic.md",
"title": "Phase10c_Slippage_Deterministic",
"id": "object/deea2ed6-cd6f-b006-2e4e-949abea60904",
"type": "arch",
"role": "history",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "eee3c074135099536c4035a39531df08969eb701ff73a28e6bc4e01941cb09a7",
"body": "\n\n# Phase 10c — Deterministische Slippage (DETERMINISTIC_FIXED)\n\n**Geändert von:** Rain Ocampo (Hermes)\n**Datum:** 23.08.2026\n**Grund:** Deterministische Slippage-Komponente `DETERMINISTIC_FIXED` als zusätzliche, eigenständige Ausführungskomponente auf Basis von `LEGACY_SINGLE_PRICE` / `REFERENCE_BID_ASK` — formal/verifikativ abgeschlossen nach Deploy.\n\n## Zusammenfassung\n\nPhase 10c führt ein **deterministisches Slippage-Modell** (`slippage_model = DETERMINISTIC_FIXED`, Version `slippage_model_v1`) ein. Slippage ist eine **zusätzliche, eigenständige Komponente** auf dem Fill-Preis — **getrennt vom Spread**. Der Spread bleibt beim Ausführungspfad `REFERENCE_BID_ASK` intrinsisch (`BID_ASK_INTRINSIC`); Slippage wird **erst danach** auf den Fill-Preis aufgeschlagen. Legacy (`LEGACY_SINGLE_PRICE`) bleibt byte-identisch unverändert.\n\n## Modell & Einheit\n\n- **`DETERMINISTIC_FIXED`** + `slippage_model_version = slippage_model_v1`\n- `slippage_unit = PRICE` → absolute Preisdistanz (keine implizite Umrechnung; Audit zeigt Einheit)\n- **Kein** Random/Seed/Volatilität/Liquidität — reproduzierbar\n- `NONE` (Default) vs `FIXED(0)`: **gleiche Fillpreise/PnL**, aber **unterschiedlicher ExecutionContext/run_hash** — gewollt\n\n## Adverse Slippage-Semantik\n\n- **LONG ENTRY** = ASK + slip, **LONG EXIT** = BID slip\n- **SHORT ENTRY** = BID slip, **SHORT EXIT** = ASK + slip\n- Basis: LONG Entry=ASK / Exit=BID, SHORT Entry=BID / Exit=ASK (unverändert)\n- BUY → höher, SELL → tiefer (adversarial)\n\n## Trigger ≠ Fill\n\n- Stop/Target-Trigger laufen gegen **Basis-Preise** (ohne Slippage)\n- Slippage erst auf den **Fill-Preis** — kein Doppel-Aufschlag\n\n## Fail-Closed (15)\n\n- `DETERMINISTIC_FIXED` ohne gültigen Wert → blockiert\n- negativ/NaN/Inf/nichtnumerisch → ungültig\n- unbekannte Einheit → ungültig\n- `NONE`/Default → 0.0, V2-Pfad unverändert\n\n## ExecutionContext / run_hash / Audit\n\n- ExecutionContext Default: `NONE`/0.0; V2-Pfad unverändert, wenn nicht gesetzt\n- `run_hash` inkl. `slippage_value` + `slippage_unit` (18)\n- Audit: `entry/exit_market_price`/`fill_price`/`slippage` (19)\n- gleiche Slippage-Konfig 2× → **identischer** run_hash; 0.10 vs 0.20 → anderer; NONE vs FIXED(0) → anderer\n- Audit exakt = ExecutionContext\n\n## Legacy byte-identisch (21)\n\n- `params[\"slippage\"]` unverändert (Legacy-Slippage-Feld)\n- **kein** V2-Slippage-Audit im Legacy-Pfad\n- **kein** DatasetGate-Block im Legacy-Pfad\n\n## Beweis (Tests)\n\n| Suite | Ergebnis |\n|---|---|\n| Phase10cSlippage | **30/30** grün (inkl. symmetrischer PnL-Beweis: LONG +2.80 / SHORT +2.80, slip 0.10) |\n| Phase10bFixtureE2E | **8/8** grün |\n| Phase10bEngine | **7/7** grün |\n| Phase10a (execution_context) | **15/15** grün |\n| Phase8 | **14/14** grün |\n| Phase6/5 | 14/14 / 20/20 grün |\n| M13Shared AJ + M13Compat | grün |\n| **Gesamtsuite (Runner, nach Deploy)** | **2× alle 12 Suiten grün** |\n\n## Deployment (kontrollierter Minimal-Deploy, Backup + Rollback)\n\n- **Backup:** `/opt/data/m12m13_work/backup_phase10c_deploy_20260823_095549/` (m12 5 Dateien, sha256-verifiziert)\n- **M12** (`Modul-12-Backtesting`): `core/pricing.py`, `core/engine.py`, `core/service.py`, `api/schemas.py`, `shared/historical/execution_context.py`\n- **Verifikation:** Backup+sha256 VORHER, `docker cp`, `chown 1001:1001`, py_compile OK, Import-Smoke OK, Restart; `/health`=200, `/health/ready`=`{\"status\":\"ready\",\"db\":\"ok\"}` (10.0.13.16:55012, intern)\n- **M13**: `shared/execution_context.py` vorhanden, /health+ready grün (10.0.13.9:55013)\n- **KEINE Phase 10d, keine IG-Calls/Orders, keine V2-Produktivaktivierung**\n\n## V2-Runtime-Smoke (nach Deploy)\n\n- `DETERMINISTIC_FIXED` importierbar\n- `SlippageUnit.PRICE` importierbar\n- `BID_ASK_INTRINSIC` weiterhin korrekt\n- `slippage_amount(exec)` liefert erwarteten Wert (0.0001 für FIXED 0.0001)\n- LONG entry +slip / SHORT entry slip (adversarial)\n- Audit/ExecutionContext serialisierbar, keine Importfehler\n\n## Legacy-Produktions-Smoke (nach Deploy, 2 Runs A/B)\n\n- **run_hash: `bc6e25533d22ee102b6921eeeccd8f7b7dff4ec74a3316e381007ec56b0e1b37`** (A=B, deterministisch)\n- **data_hash: `d76a75496453d9c05d5690237f4621534f954f9f3e87b67eee4ae9327c5c6bb9`** (A=B, identisch Phase 8)\n- **net_pnl: 196.586062** (A=B), total_trades 1, LONG 1/SHORT 0\n- Trades/Fills/PnL identisch; **Legacy-Slippage unverändert**; kein V2-Audit; kein Gate-Block\n\n## Produktions-Gate (Final Check)\n\n- M12: `/health`=200, `/health/ready`=200, `HISTORICAL_DATA_SOURCE=UNSET` (legacy), `ALLOW_FIXTURE_DATA=UNSET` (false)\n- M13: `/health`=200, `/health/ready`=200, Gates UNSET\n- Keine V2-Produktivläufe, keine Fixture-Aktivierung, keine verwaisten Testprozesse\n- M13-Optimizer-Parameterraum enthält **kein** `slippage_model/value/unit`; keine Execution-/Cost-Parameter optimierbar\n\n## Bekannte Grenzen\n\n- NONE vs FIXED(0) → unterschiedlicher run_hash (gewollt, kein Fehler)\n- Fixture-E2E nur mit `ALLOW_FIXTURE_DATA=true` (test-only); danach garantiert false/unset\n- M13-Optimizer nutzt das neue Slippage-Modell **nicht** im Parameterraum (Legacy-`slippage`-Feld bleibt)\n- Kein Random-/Volatilitäts-Slippage implementiert (bewusst, Scope-10c)\n"
},
{
"path": "notes/trading/second-brain/Stop_Loss_Fix_2026-08-18.md",
"title": "Stop_Loss_Fix_2026-08-18",
"id": "object/799b279e-9709-3c05-972b-3751d1dc8a8d",
"type": "arch",
"role": "note",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "95ce161492cde9524465d114c22df9e66bfc572d0c351f4addb4a026607a200b",
"body": "\n# Echter Stop-Loss + Risiko-Sizing — Fix 18.08.2026\n\n## Problem\nDer Stop-Loss war bisher **nur ein lokaler Wert** in `trades.json`. Der Monitor prüfte\n**stündlich** und verkaufte dann per Market-Order. Bei volatilen Pennystocks (MXL, ACRS,\nULCC, XRX) rutschte der Kurs in der Zwischenzeit deutlich unter den SL → Verluste von\n7 bis 10 % statt der konfigurierten 8 %. Realisierte Rendite der Periode: **24,33 €**.\n\nZusätzlich war das Sizing fix auf 50 €/Position → bei weitem SL (8 %) ein Verlust von\n~67 € (≈12 % des Positionsbudgets), statt der 2 %-Regel.\n\n## Fix A — Echter Stop-Loss bei T212\n- Neuer Endpunkt: `POST /api/v0/equity/orders/stop` (negative Menge = Sell-Stop)\n- `place_stop()` + `cancel_stop()` in `t212_resilience.py`\n- `ensure_stop()` in `t212_monitor.py`: platziert/aktualisiert den echten Stop bei\n Fill, Breakeven (+3 %), Trailing (+5 %) und bei jedem Monitor-Lauf\n- **Idempotent:** speichert `stop_order_id` + `stop_order_price` in `trades.json` →\n kein Cancel+Neu-Churn bei jedem Lauf\n- **Rate-Limit-Pacing:** 1 Stop-Request / 2s (T212-Limit)\n- **Konservativ bei 429:** leere Live-Stop-Liste → nichts unternehmen (kein Churn)\n\n## Fix B — Risiko-basiertes Sizing\n- `calc_risk()` nutzt jetzt `risk_per_trade_pct (2 %)` × Gesamtkapital statt fix 50 €\n- Verlust pro Trade in € gedeckelt, egal wie weit der SL steht\n- \"Trades atmen lassen\" (weiter SL) → Position wird automatisch kleiner\n\n## Verifiziert (live gegen T212-API, 18.08.2026)\n4 echte Stop-Orders aktiv, keine Duplikate, zweiter Monitor-Lauf = 0 neue Orders:\n\n| Position | Stop | Order-ID |\n|----------|------|----------|\n| LFST | 11,95 € | 55909740706 |\n| TRGP | 271,78 € | 55909740714 |\n| WEAV | 6,72 € | 55909740725 |\n| UMAC | 28,55 € | 55909740772 |\n\n## Backups\n- `t212_monitor.py.bak_*` (mehrere Stände)\n- `t212_resilience.py.bak_*`\n\n## Status\n✅ Aktiv — Daemon liest Dateien bei jedem Lauf frisch, kein Neustart nötig.\n"
},
{
"path": "notes/trading/second-brain/journal/2026-07-20_T212_Recap.md",
"title": "2026-07-20_T212_Recap",
"id": "object/879703ed-5d7c-5446-397a-d2ec7b1a1d22",
"type": "journal",
"role": "journal",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "001cc0942b7021cd8a929b66df9007062f0d154d50f7caf0dbeed7c76b77caf0",
"body": "\n\n# 📊 Tages-Recap — 20.07.2026\n\n**📊 T212 Tages-Recap — 20.07.2026**\n Monday | 22:30 MEZ\n Letzter Scan: 21:00 | 0 Kandidaten\n\n**⚙️ Risiko-Modell**\n 2%-Risiko/Trade | 90%-Kapazität | Max 2/Sektor\n\n**📊 Offene Positionen: 6**\n Gefüllt: 6 | Pending: 0\n Sektoren: Commercial Services(1), Non-Energy Minerals(1), Industrials(1), Electronic Technology(1), Energy Minerals(1), Consumer Durables(1)\n CCO | Entry 2.41€ | ~+0.0% (+0.00€)\n GGB | Entry 4.68€ | ~+0.0% (+0.00€)\n AAL | Entry 15.10€ | ~+0.0% (+0.00€)\n ASX | Entry 38.75€ | ~+0.0% (+0.00€)\n XOM | Entry 148.00€ | ~+0.0% (+0.00€)\n F | Entry 14.20€ | ~+0.0% (+0.00€)\n\n**✅ Heute geschlossen: 103**\n CVE | +0.0% | Grund: Portfolio-Ghost\n OLPX | -0.0% | Grund: Portfolio-Sync (nicht mehr in T212)\n CVX | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte)\n ABEV | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte)\n TEVA | -0.5% | Grund: Auto-Management\n OXY | -1.7% | Grund: Auto-Management\n TSEM | +17.1% | Grund: Auto-Management\n OVV | -0.6% | Grund: Auto-Management\n FCX | +7.3% | Grund: Auto-Management\n GOOG | -2.0% | Grund: Auto-Management\n VG1 | -7.5% | Grund: Auto-Management\n VKp | -3.3% | Grund: Auto-Management\n GOOG | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte)\n TWEKAa | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte)\n BLDP | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte)\n VACQ | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte)\n GLW | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte)\n VKp | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte)\n VKp | -8.7% | Grund: SL-Hit (bereits von T212 geschlossen)\n APAM | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte)\n NVDd | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte)\n FFARMa | -0.4% | Grund: Auto-Management\n S92d | +0.0% | Grund: Auto-Management\n TWEKAa | +0.0% | Grund: Auto-Management (bereits ausgebucht)\n SGLd | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte)\n VRT | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte)\n OLPX | +0.0% | Grund: Duplicate cleanup\n APLD | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte)\n WMB | +0.0% | Grund: Manuell auf T212 geschlossen (nicht mehr im Portfolio)\n CLSK | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte)\n BE | +0.0% | Grund: Auto-Management (phantom - fillPrice fehlte)\n RWEd | +0.7% | Grund: Auto-Management\n IOVA | +7.9% | Grund: Auto-Management\n REPe | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212)\n ALLO | -100.0% | Grund: Auto-Management\n FFARMa | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212)\n GLW | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212)\n MXL | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212)\n FNB | -0.5% | Grund: Auto-Management\n KEY | +4.3% | Grund: Portfolio-Sync (nicht mehr in T212)\n GLW | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212)\n CSCO | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212)\n MO | +4.9% | Grund: Auto-Management\n VPG | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212)\n LRCX | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212)\n IOVA | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212)\n KLAC | -4.7% | Grund: Auto-Management\n KO | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n GOOG | -6.4% | Grund: Auto-Management\n LRCX | -100.0% | Grund: Auto-Management\n AMZN | -100.0% | Grund: Auto-Management\n ANET | +2.6% | Grund: Auto-Management\n GOOG | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212)\n AFL | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n RY | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n SHW | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n MCD | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n MAIN | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n GOOD | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n CNQ | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n SLB | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n PG | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n IBM | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n AGNC | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n NUE | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n RGLD | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n ECL | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n MA | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n CSCO | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n BNS | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n BLK | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n MSFT | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n BMY | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n KMB | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n SYY | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n TROW | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n LOW | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n PNR | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n GD | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n O | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n DUK | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n EMR | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n ADP | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n GWW | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n PPG | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n LTC | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n ROP | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n ITW | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n JNJ | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n JPM | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n CVX | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n C | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n BMO | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n CF | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n CAH | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n PEP | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n TD | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n CB | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n AAPL | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n ADC | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n WMT | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n ABT | +0.0% | Grund: AutoInvest-Pie (nicht API-managed)\n MO | +0.0% | Grund: Portfolio-Sync (nicht mehr in T212)\n\n**🗑️ Heute gecancelt: 16**\n TWEKAa | 24h nicht gefüllt — Cancel\n VRT | 24h nicht gefüllt — Cancel\n CCO | End-of-Day Cleanup\n OLPX | 24h nicht gefüllt — Cancel\n ABEV | End-of-Day Cleanup\n IOVA | 24h nicht gefüllt — Cancel\n ALLO | 24h nicht gefüllt — Cancel\n TSM | 69h nicht gefüllt — Cancel\n BMY | 69h nicht gefüllt — Cancel\n RTPY | Order existiert nicht mehr bei T212 (404)\n KEY | 61h nicht gefüllt — Cancel\n RTPY | End-of-Day Cleanup\n LION | 26h nicht gefüllt — Cancel\n BCRX | 24h nicht gefüllt — Cancel\n CX | 24h nicht gefüllt — Cancel\n BMY | 24h nicht gefüllt — Cancel\n\n💰 **Investiert: +115.98€**\n Unreal. P&L: +0.00€\n\n**🧠 Analyse:**\n ✅ 7 Gewinn-Trades (⌀ 6.4%)\n 🔴 15 Verlust-Trades (⌀ -22.4%)\n - OLPX_US_EQ: -0.0% (Portfolio-Sync (nicht mehr in T212))\n - TEVA_US_EQ: -0.5% (Auto-Management)\n - OXY_US_EQ: -1.7% (Auto-Management)\n - OVV_US_EQ: -0.6% (Auto-Management)\n - GOOG_US_EQ: -2.0% (Auto-Management)\n - VG1_US_EQ: -7.5% (Auto-Management)\n - VKp_EQ: -3.3% (Auto-Management)\n - VKp_EQ: -8.7% (SL-Hit (bereits von T212 geschlossen))\n - FFARMa_EQ: -0.4% (Auto-Management)\n - ALLO_US_EQ: -100.0% (Auto-Management)\n - FNB_US_EQ: -0.5% (Auto-Management)\n - KLAC_US_EQ: -4.7% (Auto-Management)\n - GOOG_US_EQ: -6.4% (Auto-Management)\n - LRCX_US_EQ: -100.0% (Auto-Management)\n - AMZN_US_EQ: -100.0% (Auto-Management)\n ⏰ 16 Orders gecancelt (unfilled/warning)\n\n**💡 Learning für morgen:**\n ❌ OLPX: -0.0% wegen Portfolio-Sync (nicht mehr in T212)\n → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger?\n ❌ TEVA: -0.5% wegen Auto-Management\n → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger?\n ❌ OXY: -1.7% wegen Auto-Management\n → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger?\n ✅ TSEM: +17.1% (gut erkannt)\n → SMA50-Kipp-Erkennung war richtig\n ❌ OVV: -0.6% wegen Auto-Management\n → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger?\n ✅ FCX: +7.3% (gut erkannt)\n → SMA50-Kipp-Erkennung war richtig\n ❌ GOOG: -2.0% wegen Auto-Management\n → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger?\n ❌ VG1: -7.5% wegen Auto-Management\n → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger?\n ❌ VKp: -3.3% wegen Auto-Management\n → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger?\n ❌ VKp: -8.7% wegen SL-Hit (bereits von T212 geschlossen)\n → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger?\n ❌ FFARMa: -0.4% wegen Auto-Management\n → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger?\n ✅ RWEd: +0.7% (gut erkannt)\n → SMA50-Kipp-Erkennung war richtig\n ✅ IOVA: +7.9% (gut erkannt)\n → SMA50-Kipp-Erkennung war richtig\n ❌ ALLO: -100.0% wegen Auto-Management\n → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger?\n ❌ FNB: -0.5% wegen Auto-Management\n → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger?\n ✅ MO: +4.9% (gut erkannt)\n → SMA50-Kipp-Erkennung war richtig\n ❌ KLAC: -4.7% wegen Auto-Management\n → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger?\n ❌ GOOG: -6.4% wegen Auto-Management\n → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger?\n ❌ LRCX: -100.0% wegen Auto-Management\n → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger?\n ❌ AMZN: -100.0% wegen Auto-Management\n → Vorschlag: Beim nächsten Mal früher reagieren? Oder SL enger?\n ✅ ANET: +2.6% (gut erkannt)\n → SMA50-Kipp-Erkennung war richtig\n\n---\n_Automatisch erstellt von Alice_\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-07-21_log.md",
"title": "2026-07-21_log",
"id": "object/99e784de-4e66-c9d3-7d3e-1a62afc91d90",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "9a35ddd726c9149881234dde869439a1f0743e51357fd86645153f807b409495",
"body": "\n# T212 Logs — 21.07.2026 23:45 UTC\n## Offen (4) | Unrealisiert: +0.0%\n- GGB_US_EQ | +0.0% | SL: 4.31€\n- AAL_US_EQ | +0.0% | SL: 13.89€\n- XOM_US_EQ | +0.0% | SL: 136.16€\n- F_US_EQ | +0.0% | SL: 13.06€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-07-22_log.md",
"title": "2026-07-22_log",
"id": "object/d334663d-ec48-f6e7-03a9-e9715571955f",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "6cfa142cde4b0f734d3b60a068783ff011a59febbfd2c278bc5fa9e9e72b71a1",
"body": "\n# T212 Logs — 22.07.2026 23:45 UTC\n## Offen (3) | Unrealisiert: +0.0%\n- GGB_US_EQ | +0.0% | SL: 4.31€\n- F_US_EQ | +0.0% | SL: 13.06€\n- UNHd_EQ | +0.0% | SL: 352.73€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-07-23_log.md",
"title": "2026-07-23_log",
"id": "object/48e725ec-b9cb-43f1-f719-0e977fb11570",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "701e250ba4ad387f8faedb9431069cc2122d9f905834a6019b2938d4600ca433",
"body": "\n# T212 Logs — 23.07.2026 23:45 UTC\n## Offen (1) | Unrealisiert: +0.0%\n- GGB_US_EQ | +0.0% | SL: 4.31€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-07-24_log.md",
"title": "2026-07-24_log",
"id": "object/b8d1e818-00ba-c4de-c12b-819b9bc92c48",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "6eb0b9e70fa7a18f5d09ef504e771386e723b118113bae902a1101872f1d22a3",
"body": "\n# T212 Logs — 24.07.2026 23:45 UTC\n## Offen (1) | Unrealisiert: +0.0%\n- GGB_US_EQ | +0.0% | SL: 4.31€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-07-27_log.md",
"title": "2026-07-27_log",
"id": "object/69dce38b-d44c-8361-ee76-128f4639c165",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "546a5c22aa3c4e7e113af38ab97043501a0781b4d09ce110e8ed53ea6561067b",
"body": "\n# T212 Logs — 27.07.2026 23:45 UTC\n## Offen (3) | Unrealisiert: +0.0%\n- GGB_US_EQ | +0.0% | SL: 4.31€\n- ACRS_US_EQ | +0.0% | SL: 5.04€\n- BRLI_US_EQ | +0.0% | SL: 9.92€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-07-28_log.md",
"title": "2026-07-28_log",
"id": "object/10f3cd5a-aa77-9cfd-cfc0-f4ab7abd0906",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "320df6b36f5e5adaf52aa658fdef21a1c555f23c3612854f6e898ed03c9494c9",
"body": "\n# T212 Logs — 28.07.2026 23:45 UTC\n## Offen (2) | Unrealisiert: +0.0%\n- GGB_US_EQ | +0.0% | SL: 4.31€\n- ACRS_US_EQ | +0.0% | SL: 5.04€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-07-29_log.md",
"title": "2026-07-29_log",
"id": "object/1748b435-164d-5a9b-674c-233b1a0a1cff",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "6283030f7e7d0fb51a4a4492302a323fb1e53929ee7cf75a39eeeca2afb2ae3d",
"body": "\n# T212 Logs — 29.07.2026 23:45 UTC\n## Offen (2) | Unrealisiert: +0.0%\n- GGB_US_EQ | +0.0% | SL: 4.31€\n- ACRS_US_EQ | +0.0% | SL: 5.04€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-07-30_log.md",
"title": "2026-07-30_log",
"id": "object/03b4a75f-71b1-b33f-c960-c12e948560e1",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "720bbb84d2f150fe0b1a8f4a77771e99d338293a0767f29e7c82d5cd682c0807",
"body": "\n# T212 Logs — 30.07.2026 23:45 UTC\n## Offen (3) | Unrealisiert: +0.0%\n- GGB_US_EQ | +0.0% | SL: 4.86€\n- XRX_US_EQ | +0.0% | SL: 3.10€\n- FORM_US_EQ | +0.0% | SL: 95.31€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-07-31_log.md",
"title": "2026-07-31_log",
"id": "object/acf5b5da-0138-6bf8-b470-f80ba8ca7e03",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "c028abb6d77ca4dfe5c83edf84aa9a02183c472874ff7227d0ad004042375780",
"body": "\n# T212 Logs — 31.07.2026 23:45 UTC\n## Offen (2) | Unrealisiert: +0.0%\n- GGB_US_EQ | +0.0% | SL: 4.86€\n- AMCX_US_EQ | +0.0% | SL: 10.34€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-08-03_log.md",
"title": "2026-08-03_log",
"id": "object/730534ed-b44a-46de-0082-268a476003c7",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "5bdd07b2f5157d3157122939cdb07facf1fe38a1520faee6b4173a00cd1d3ac7",
"body": "\n# T212 Logs — 03.08.2026 23:45 UTC\n## Offen (5) | Unrealisiert: +0.0%\n- GGB_US_EQ | +0.0% | SL: 4.86€\n- AXTI_US_EQ | +0.0% | SL: 55.60€\n- ULCC_US_EQ | +0.0% | SL: 6.96€\n- XRX_US_EQ | +0.0% | SL: 2.98€\n- ATKR_US_EQ | +0.0% | SL: 85.99€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-08-04_log.md",
"title": "2026-08-04_log",
"id": "object/1b4b6582-403a-d1d3-6f4c-a8f5ea868ddb",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "0ea236ff89dbd2f37bc9e847d6b19b5f0c93423143eeb2fb1ce1421d343f6045",
"body": "\n# T212 Logs — 04.08.2026 23:45 UTC\n## Offen (5) | Unrealisiert: +0.0%\n- GGB_US_EQ | +0.0% | SL: 4.86€\n- AXTI_US_EQ | +0.0% | SL: 55.60€\n- ULCC_US_EQ | +0.0% | SL: 6.96€\n- XRX_US_EQ | +0.0% | SL: 2.98€\n- ATKR_US_EQ | +0.0% | SL: 85.99€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-08-05_log.md",
"title": "2026-08-05_log",
"id": "object/866ae1ee-7fd7-4175-c6a7-5a5c0e663d8d",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "3edac14a6e627091b61e588480334fa4bbdf52d1d045f459d39642db013571c1",
"body": "\n# T212 Logs — 05.08.2026 23:45 UTC\n## Offen (5) | Unrealisiert: +0.0%\n- GGB_US_EQ | +0.0% | SL: 4.86€\n- ULCC_US_EQ | +0.0% | SL: 6.96€\n- XRX_US_EQ | +0.0% | SL: 2.98€\n- ATKR_US_EQ | +0.0% | SL: 85.99€\n- AXTI_US_EQ | +0.0% | SL: 53.82€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-08-06_log.md",
"title": "2026-08-06_log",
"id": "object/6d67c1dc-044b-9e40-b519-b3dc57a85d3a",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "bed2be7c19c1d4cd5cf850b5b043f1cfcc556cad7cfca000afa817ecf3d7c824",
"body": "\n# T212 Logs — 06.08.2026 23:45 UTC\n## Offen (4) | Unrealisiert: +0.0%\n- GGB_US_EQ | +0.0% | SL: 4.86€\n- ULCC_US_EQ | +0.0% | SL: 6.96€\n- XRX_US_EQ | +0.0% | SL: 2.98€\n- ATKR_US_EQ | +0.0% | SL: 85.99€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-08-07_log.md",
"title": "2026-08-07_log",
"id": "object/286f4f60-ab5d-f872-ae6b-cb997c214dc5",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "744c35a113f0aabc588e107379aaee379844456e6e0d64f8165117acbdd0f1d7",
"body": "\n# T212 Logs — 07.08.2026 23:45 UTC\n## Offen (4) | Unrealisiert: +0.0%\n- GGB_US_EQ | +0.0% | SL: 4.86€\n- ULCC_US_EQ | +0.0% | SL: 6.96€\n- XRX_US_EQ | +0.0% | SL: 2.98€\n- ATKR_US_EQ | +0.0% | SL: 85.99€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-08-10_log.md",
"title": "2026-08-10_log",
"id": "object/73fdf272-78c3-1c55-1198-84181898f9f0",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "48e0a3a069320437dde79b74007d427f2010e9c6c831c98af37cb3ff95755116",
"body": "\n# T212 Logs — 10.08.2026 23:45 UTC\n## Offen (5) | Unrealisiert: +0.0%\n- GGB_US_EQ | +0.0% | SL: 4.86€\n- XRX_US_EQ | +0.0% | SL: 2.98€\n- ATKR_US_EQ | +0.0% | SL: 85.99€\n- NESR_US_EQ | +0.0% | SL: 32.84€\n- LFST_US_EQ | +0.0% | SL: 10.99€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-08-11_log.md",
"title": "2026-08-11_log",
"id": "object/5a6cf599-8023-e788-1d6c-eb72bfa2ca58",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "338f7ac9549c34819882d5a79aae783baebfda9a0bd45060c09eabcf63fc052b",
"body": "\n# T212 Logs — 11.08.2026 23:45 UTC\n## Offen (4) | Unrealisiert: +0.0%\n- XRX_US_EQ | +0.0% | SL: 2.98€\n- ATKR_US_EQ | +0.0% | SL: 85.99€\n- NESR_US_EQ | +0.0% | SL: 32.84€\n- LFST_US_EQ | +0.0% | SL: 10.99€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-08-12_log.md",
"title": "2026-08-12_log",
"id": "object/3fbb2b3c-921b-3db5-73f3-7de26261ef2f",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "39aa3c953a0020423122ff0a89538dec7e22071a338170d17f3259508054ec64",
"body": "\n# T212 Logs — 12.08.2026 23:45 UTC\n## Offen (4) | Unrealisiert: +0.0%\n- XRX_US_EQ | +0.0% | SL: 2.98€\n- ATKR_US_EQ | +0.0% | SL: 85.99€\n- NESR_US_EQ | +0.0% | SL: 32.84€\n- LFST_US_EQ | +0.0% | SL: 10.99€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-08-13_log.md",
"title": "2026-08-13_log",
"id": "object/badaaaea-79fb-3c65-c1d3-f0028040204b",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "8a7fa1c9b5d73413d96114023a5cda0dfb1b43ec34d331fad012eb9187a1524c",
"body": "\n# T212 Logs — 13.08.2026 23:45 UTC\n## Offen (4) | Unrealisiert: +0.0%\n- XRX_US_EQ | +0.0% | SL: 2.98€\n- ATKR_US_EQ | +0.0% | SL: 85.99€\n- NESR_US_EQ | +0.0% | SL: 32.84€\n- LFST_US_EQ | +0.0% | SL: 10.99€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-08-14_log.md",
"title": "2026-08-14_log",
"id": "object/43673951-0706-29e0-12bb-55b652456962",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "6e4ea4f13a26a763e5b6ba9be0486a9c5e5fa66360fe207296303f8f19217eb6",
"body": "\n\n# T212 Logs — 14.08.2026 23:45 UTC\n## Offen (4) | Unrealisiert: +0.0%\n- XRX_US_EQ | +0.0% | SL: 2.98€\n- ATKR_US_EQ | +0.0% | SL: 85.99€\n- NESR_US_EQ | +0.0% | SL: 32.84€\n- LFST_US_EQ | +0.0% | SL: 10.99€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-08-17_log.md",
"title": "2026-08-17_log",
"id": "object/3b9de5f6-784d-a2dd-a9ea-d154450c3586",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "e3d26199a26e5f889a7f062314de3e0f8813a096909b7e167a5905320ae6f6e0",
"body": "\n# T212 Logs — 17.08.2026 23:45 UTC\n## Offen (4) | Unrealisiert: +0.0%\n- NESR_US_EQ | +0.0% | SL: 35.69€\n- LFST_US_EQ | +0.0% | SL: 11.95€\n- AXTI_US_EQ | +0.0% | SL: 90.35€\n- SMTC_US_EQ | +0.0% | SL: 142.33€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-08-18_log.md",
"title": "2026-08-18_log",
"id": "object/5811e85a-191f-c7a3-8f2e-e3768ae15044",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "0e8af976187a14a228d342bd0ada9929a53df64a94ead8138bff31836c52fdc0",
"body": "\n# T212 Logs — 18.08.2026 23:45 UTC\n## Offen (4) | Unrealisiert: +0.0%\n- LFST_US_EQ | +0.0% | SL: 11.95€\n- TRGP_US_EQ | +0.0% | SL: 271.78€\n- WEAV_US_EQ | +0.0% | SL: 6.72€\n- UMAC_US_EQ | +0.0% | SL: 28.55€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/second-brain/t212-logs/2026-08-19_log.md",
"title": "2026-08-19_log",
"id": "object/3479a3df-f762-0cd1-bf71-2e29fff56cca",
"type": "log",
"role": "log",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "2fc4e0274531a5f372cde39b8f6ac8294e648cee180fb6b9bbbf160e89d3f5c4",
"body": "\n# T212 Logs — 19.08.2026 23:45 UTC\n## Offen (4) | Unrealisiert: +0.0%\n- LFST_US_EQ | +0.0% | SL: 11.95€\n- TRGP_US_EQ | +0.0% | SL: 271.78€\n- WEAV_US_EQ | +0.0% | SL: 6.72€\n- UMAC_US_EQ | +0.0% | SL: 28.55€\n\n---\n*Auto-Log*\n"
},
{
"path": "notes/trading/system-docs/README.md",
"title": "README",
"id": null,
"type": "Trading-Modul-System",
"role": null,
"representation": null,
"state": null,
"knowledge_schema": null,
"content_hash": "96e60fa420c2306a6f80d95fb75b20d30a8ec39e2c8e3008eb064ddd8853190f",
"body": "\n# Modul-Trading-System — Modulare Architektur\n\nZentrales, modulares automatisiertes Modul-Trading-System, betrieben auf einem Hostinger-VPS (`187.124.31.123`).\n\n**Repository-Inhalt:** Live-Dokumentation der Infrastruktur, der Module und des aktuellen Betriebszustands.\nJede Änderung folgt dem **Notation-Format**: Autor (`Alice` / `Rain Ocampo`) + Zeitstempel (`DD.MM.YYYY HH:MM`) + Grund.\n\n---\n\n## Architektur-Überblick\n\n- **17 Module** (`Modul-01-PostgreSQL` … `Modul-17-Hermes-Agent`), orchestriert über `docker-compose.yml` unter `/opt/trading-modules/`.\n- **Netzwerk:** `trading-modules` (bridge). Kommunikation ausschließlich über **Docker-interne Service-Hostnamen** (keine festen IPs).\n- **Feste Host-Ports:** Modul-01=`55432`, Modul-02=`55672`+`15672`, Modul-0317=`55003``55017`.\n- **Zugangsdaten:** nur über Environment Variables / Docker-Secrets (Defaults in Compose mit `${VAR:-default}`).\n- **Kernprinzip:** bestehende Container/Volumes/Daten nie löschen, keine unnötigen öffentlichen Ports öffnen, Ist-Zustand vor Änderung prüfen.\n\n### Multi-Agent-Setup (Resident-Evil-Theme)\n| Agent | Rolle | Port |\n|-------|-------|------|\n| **Alice** (OpenClaw) | Primary | 55163 |\n| **Matt Addison** (OpenClaw) | Backup | 54524 |\n| **Rain Ocampo** (Hermes) | Technischer Support | 32776 (UI) |\n\n---\n\n## Module\n\n| Modul | Name | Status | Port |\n|-------|------|--------|------|\n| 01 | PostgreSQL | ✅ healthy (postgres:16-alpine) | 55432 |\n| 02 | RabbitMQ | ✅ healthy (rabbitmq:3-management) | 55672 / 15672 |\n| 03 | Market-Data | ✅ healthy (market-data:0.1.0) | 55003 (intern) |\n| 04 | Market-Regime | ✅ healthy (market-regime:1.0.0) | 55004 (intern) |\n| 05 | Strategy-Engine | ✅ healthy (strategy-engine:0.1.0) | 55005 (intern) |\n| 06 | Signal-Ranking | ✅ FREIGEGEBEN (signal-ranking:0.1.0) | 55006 (intern) |\n| 07 | Risk-Manager | ✅ FREIGEGEBEN (risk-manager:0.1.0) | 55007 (intern) |\n| 08 | Portfolio-Manager | ✅ FREIGEGEBEN (portfolio-manager:0.1.0) | 55008 (intern) |\n| 09 | Execution-Service | ✅ FREIGEGEBEN (execution-service:0.1.0) | 55009 (intern) |\n| 10 | Trade-Journal | ✅ FREIGEGEBEN (trade-journal:0.1.0) | 55010 (intern) |\n| 11 | Analytics | ✅ FREIGEGEBEN (analytics-service:0.1.0) | 55011 (intern) |\n| 12 | Backtesting | ✅ FREIGEGEBEN (backtesting:0.1.2) | 55012 (intern) |\n| 13 | Optimization | ✅ FREIGEGEBEN (optimization:0.1.0) | 55013 (intern) |\n| 14 | Notification | ✅ FREIGEGEBEN (notification:0.1.0) | 55014 (intern) |\n| 15 | Monitoring-Control | ✅ FREIGEGEBEN (monitoring-control:0.1.0) | 55015 (intern) |\n| 1617 | … | ⬜ Platzhalter (alpine) | 5501655017 |\n\nDetails: siehe `modul-03-market-data.md` … `modul-15-monitoring-control.md` (Module 0315\nvollständig implementiert und freigegeben). Modul-16 ff. folgen.\n\n---\n\n## Notation / Audit-Trail\n\nJede Änderung wird mit folgender Zeile dokumentiert:\n\n```\nGeändert von: [Alice | Rain Ocampo]\nDatum: DD.MM.YYYY HH:MM\nGrund: <Kurzbeschreibung>\n```\n\n- **Alice** = Änderungen durch OpenClaw\n- **Rain Ocampo** = Änderungen durch Hermes\n\n---\n\n## Geändert\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Initiale Repository-Struktur und Modul-03-Dokumentation angelegt.\n```\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: README aktualisiert — Modul-04 und Modul-05 als healthy/freigegeben eingetragen (Ports intern), Details-Link ergänzt.\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: README-Modultabelle aktualisiert — Module 0612 als freigegeben eingetragen; Modul-12-Backtesting als FREIGEGEBEN markiert.\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: README-Modultabelle aktualisiert — Modul-12 auf backtesting:0.1.2 (V1.1), Modul-13 Optimization als E2E-VERIFIZIERT eingetragen.\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: README-Modultabelle aktualisiert — Modul-13 Optimization als FREIGEGEBEN markiert (Freigabe durch Nutzer 20.08.2026).\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: README-Modultabelle aktualisiert — Modul-14 Notification als FREIGEGEBEN eingetragen; Details-Link auf modul-14-notification.md erweitert.\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: README-Modultabelle aktualisiert — Modul-15 Monitoring-Control als FREIGEGEBEN eingetragen; Details-Link auf modul-15-monitoring-control.md erweitert.\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Projekt der Module in Modul-Trading-System umbenannt. README-Titel und projektspezifische Erwaehnungen angepasst.\n"
},
{
"path": "notes/trading/system-docs/historical-v2-foundation-milestone.md",
"title": "historical-v2-foundation-milestone",
"id": "object/90957134-8e8b-021b-717c-e9c7ca6cd887",
"type": "arch",
"role": "history",
"representation": "canonical",
"state": "historical",
"knowledge_schema": "1",
"content_hash": "3146d9a18c205f2fab96d309e0916cfab668ae66663c6a6d4201b5e389fe2795",
"body": "\n# Historical Data Foundation V2 + M12/M13 Shared Repository — Meilenstein\n\n> **Meilenstein:** Historical Data Foundation V2 + M12/M13 Shared Repository Schritt 1\n> **Stand:** 22.08.2026 | **Autor:** Rain Ocampo (Hermes)\n> **Status:** ✅ Schritt 1 fachlich abgeschlossen + verifiziert. Phase 2 NICHT begonnen.\n\n---\n\n## 1. Architektur-Überblick\n\nZwei getrennte Systeme, die über eine gemeinsame Repository-Schicht verbunden sind:\n\n- **Historical-Service V2** (`/opt/historical-v2/`, Container `historical-service` + `historical-db`): Dukascopy-basierte historische Marktdaten mit Dataset-Versionierung, Quality-/Session-/Eligibility-Modellen, Fixture-Trennung.\n- **M12/M13** (`Modul-12-Backtesting`, `Modul-13-Optimization`): Backtesting + Optimization, lesen Marktdaten über die gemeinsame `HistoricalRepository`-Schicht.\n\n**Netzwerk-Grenze (aktuell):** historical-db (10.0.8.2, Netz `f2afda69`) und M12/M13 (10.0.13.x, Netz `c17337ef`) liegen in **verschiedenen Docker-Netzwerken** — die Produktiv-Container erreichen die historical-db NICHT. Für Schritt 1 korrekt: Produktiv bleibt auf Legacy. V2 wurde lokal via SSH-Port-Forward gegen die echte DB getestet.\n\n---\n\n## 2. Dataset-Versioning\n\n- `dataset_version`-Tabelle: `dataset_version_id`, `version_label`, `dataset_version_hash`, `feed_type`, `provider_id`, `instrument_id`, `timeframe`, `start_utc`, `end_utc`, `raw_source`, `normalization_version`, `aggregation_version`, `quality_version`, `data_as_of`, `quality_model_version`, `stale_model_version`, `session_model_version`, `calendar_version`, `eligibility_model_version`, `is_fixture`.\n- **`dataset_version_hash`** = deterministischer SHA-256 über die vollständige DatasetSpec (provider, feed, instrument, timeframe, start, end, raw_source, norm_version, quality_version, aggregation_version, raw_or_derived, raw_source_hash, quality_model_version, stale_model_version, session_model_version, calendar_version, eligibility_model_version).\n- Gleiche Spec → identischer Hash; andere Version → anderer Hash. **Hash-Identität** (kryptografisch) und **Auditierbarkeit** (menschenlesbare Versionen als Run-Metadaten) sind getrennt zu betrachten.\n\n---\n\n## 3. Dukascopy-Befunde\n\n- Nur EURUSD, nur Dukascopy, nur Historical-V2-Stack.\n- Bid+Ask-OHLC, H4 = HOUR_4 nativ.\n- Range > ~5000 → 0 (Datenlücke).\n- Historical-Allowance zählt Datenpunkte wöchentlich (~10k).\n\n---\n\n## 4. Session / Quality / Eligibility\n\n- `historical_bar` trägt pro Bar: quality_status, quality_flags, session_state, session_phase, eligibility, exclusion_reason, technical_quality, market_quality.\n- `dataset_version` trägt die Modell-Versionen (quality_model_version, session_model_version, calendar_version, eligibility_model_version, stale_model_version).\n- **Aktuell (Schritt 1):** Diese Felder werden von der Repository-Schicht **noch NICHT** an M12/M13 propagiert (nur OHLCV + Dataset-Metadaten). → Phase-2-Requirement R5.\n\n---\n\n## 5. Fixture-Trennung\n\n- `is_fixture`-Marker auf `dataset_version`-Ebene (Migration 06).\n- Repository-V2-Query filtert hart `AND dv.is_fixture = FALSE`.\n- **Lücke:** Kein explizites Config-Gate `ALLOW_FIXTURE_DATA` (Default `false`). → Phase-2-Requirement R4.\n\n---\n\n## 6. Daily Quality\n\n- Daily-Quality-Report-Pipeline vorhanden (Foundation-Suite).\n- Quality-/Coverage-Status wird pro Bar erfasst, aber noch nicht bis zum Backtest propagiert.\n\n---\n\n## 7. Shared Repository (M12/M13 Schritt 1)\n\n- **Befund:** `historical_repository.py` existiert als **zwei physisch getrennte Kopien** (M12 + M13, identischer Hash `bc64638b…`). Kein gemeinsames Paket/Volume.\n- **Drift-Risiko:** Kein Mechanismus erzwingt Identität → künftige Änderungen können auseinanderlaufen.\n- **Zielstruktur (Phase-2-Requirement R1):** Gemeinsames `shared/historical`-Paket als EINE Quelle, beide Module importieren daraus. Keine neue Kopierlösung.\n- Interface `load_candles(symbol, timeframe, start_date, end_date, provider, asset_class)` unverändert; M12 `data_hash()` erhalten.\n\n---\n\n## 8. Legacy/V2 Gate\n\n- `HISTORICAL_DATA_SOURCE` (env), **Default = `legacy`** (liest `ohlcv` aus trading-DB, Modul-01).\n- `historical_v2` nur explizit aktivierbar. Kein stiller Wechsel.\n- FAIL-CLOSED: unbekannte Quelle → Fehler, kein stiller Fallback.\n\n---\n\n## 9. M13 Hash\n\n- `compute_run_hash` erweitert um `dataset_version_id`, `dataset_version_hash`, `provider`, `feed_type` (nur bei V2).\n- Bei Legacy (Default) → unverändert, keine Cache-Invalidierung bestehender Runs.\n- **Hash-Audit:** `dataset_version_hash` deckt kryptografisch alle Versionen ab (bewiesen über Code + Tests). Entscheidung: **A** — Hash reicht für Identität, Versionen zusätzlich als Run-Metadaten (Phase-2-Requirement R3).\n\n---\n\n## 10. Aktuelle Netzwerkgrenze\n\n- historical-db (10.0.8.2) und M12/M13 (10.0.13.x) in verschiedenen Docker-Netzwerken.\n- Produktiv-Container erreichen historical-db NICHT (timeout, verifiziert).\n- **Empfohlene Service Boundary (Phase-2-Requirement R2):** B — M12/M13 → Historical-Service API → historical-db. Dedizierter Bulk-Bars-Endpoint mit Dataset-Version + Quality/Eligibility-Metadaten. Kein direkter DB-Zugriff aus M12/M13.\n\n---\n\n## 11. Bekannte offene Punkte\n\n- Shared-Repository als EINE Quelle (R1) — noch 2 Kopien.\n- Service-Boundary B (R2) — noch direkte DB-Logik.\n- Run-Metadaten-Persistenz (R3) — noch nicht.\n- Fixture-Safety-Gate (R4) — noch nicht.\n- Quality/Coverage-Propagation (R5) — noch nicht.\n- Bid/Ask-Fill, Spread, Slippage, Kosten — Phase 2.\n\n---\n\n## 12. Phase-2-Plan\n\nPhase 2 wird getrennt behandeln (NICHT implementiert):\n\n| ID | Thema |\n|----|-------|\n| A | Run-Metadaten-Persistenz |\n| B | echte Bid/Ask-Bar-Verarbeitung |\n| C | Fill-Modell |\n| D | variable reale Spreads |\n| E | Slippage-Modell |\n| F | Broker-/Trading-Kosten |\n| G | Intrabar-Ambiguität M1 |\n| H | Feed-Type BROKER vs REFERENCE |\n| I | Fixture-Safety |\n| J | Quality/Coverage-Safety |\n| K | M13-Reproduzierbarkeit |\n| L | Legacy-Kompatibilität |\n\n**Reihenfolge-Vorschlag:** I → J → A → B/C/D/E/F → G → H → K → L.\n\n---\n\n## 13. Status / Freigaben\n\n- **M12/M13 Schritt 1:** ✅ fachlich abgeschlossen + verifiziert (Tests AJ grün, M12 12/12 grün, Foundation-Suite 21/21 zweimal).\n- **Deploy:** Code-Identität lokal = VPS = Container (sha256 identisch), Container-Restart, Smoke-Tests grün.\n- **Phase 2:** ⛔ NICHT begonnen (explizite Freigabe erforderlich).\n- **Grenzen:** nur EURUSD, nur Dukascopy, nur Historical-V2-Stack; keine Produktivmodule M03M19; keine IG-Calls/Orders; kein Jahresbackfill; M20M25 nicht beginnen.\n\n---\n\n## Geändert\n```\nGeändert von: Rain Ocampo\nDatum: 22.08.2026\nGrund: Meilenstein-Doku Historical Data Foundation V2 + M12/M13 Shared Repository Schritt 1 angelegt (Architecture-Closeout).\n```\n"
},
{
"path": "notes/trading/system-docs/historical-v2-phase8-m12-datasetgate.md",
"title": "historical-v2-phase8-m12-datasetgate",
"id": "object/ac1df780-019b-bb11-cb91-4edaa49e1843",
"type": "arch",
"role": "history",
"representation": "canonical",
"state": "historical",
"knowledge_schema": "1",
"content_hash": "9ff229fe8b3049c9c52be8ddae41f79aa4e7833ebf261657297df07b4e2497b2",
"body": "# Phase 8 — M12 Historical-V2 DatasetGate + Run-Audit\n\nAutor: Rain Ocampo (Hermes) | Datum: 2026-08-22 | Status: IMPLEMENTIERT + GETESTET\n\n## Ziel\nM12 akzeptiert Historical-V2-Datasets künftig NUR für Backtests, wenn der\nDatasetContext fachlich backtest-eligible ist. Jeder V2-Run macht seine\nkomplette Datenwahrheit auditierbar. Legacy-Verhalten bleibt unverändert.\n\n## 1. Baseline\n- M12 12/12 grün (vor Änderung, unverändert nach)\n- M12 Backup: `/opt/data/backup_phase8_20260822_214633` (service.py, client.py, storage.py, core/)\n- DATA SOURCE = UNSET (→ legacy) bestätigt\n- ALLOW_FIXTURE_DATA = UNSET (→ false/default) bestätigt\n- /health + /health/ready = 200 OK (im Container via urllib)\n\n## 2. DatasetGate (nur historical_v2, fail-closed)\n`BacktestService._dataset_gate()` greift NUR bei `marketdata.source == \"historical_v2\"`.\nLegacy (source=legacy) ist vollständig unangetastet — kein Gate, kein neuer Fehler.\n\nGate-Regeln (Punkt 2/3):\n- A) backtest_eligible=true → Run darf weiter\n- B) backtest_eligible=false → fail-closed blockiert\n- C) UNKNOWN_CRITICAL_METADATA → NICHT starten\n- D) Fixture + ALLOW_FIXTURE_DATA=false → FIXTURE_DATA_BLOCKED\n- E) fehlender DatasetContext → fail-closed (DATASET_CONTEXT_MISSING)\n- F) fehlende Dataset-Version → fail-closed (DATASET_VERSION_NOT_FOUND)\n- G) kein stiller Fallback auf Legacy\n\n## 3. Block-Codes\n| error_code | Bedingung |\n|---|---|\n| DATASET_NOT_ELIGIBLE | backtest_eligible=false, reason != UNKNOWN_CRITICAL_METADATA |\n| UNKNOWN_CRITICAL_METADATA | backtest_eligible=false + reason=UNKNOWN_CRITICAL_METADATA |\n| FIXTURE_DATA_BLOCKED | is_fixture=true + ALLOW_FIXTURE_DATA=false |\n| DATASET_CONTEXT_MISSING | build_dataset_context wirft / Context None |\n| DATASET_VERSION_NOT_FOUND | dataset_version_id & hash beide None |\n\nEin geblockter Run wird als **auditierbarer BLOCKED-Run persistiert** (status=BLOCKED,\nmetrics.error_code, metrics.block_reason, metrics.policy_version, metrics.timestamp,\nmetrics.audit). KEINE Strategieausführung.\n\n## 4. Run-Audit (Punkt 4)\nVollständiger Audit-Datensatz pro V2-Run (in `metrics.audit` + Antwort `audit`):\ndataset_context_version, data_source, dataset_id, dataset_version_id,\ndataset_version_hash, raw_source_hash, instrument, timeframe, start/end,\nprovider, feed_type, price_basis, data_as_of, data_hash,\nnormalization_version, quality_model_version, stale_model_version,\nsession_model_version, calendar_version, eligibility_model_version,\naggregation_version, eligibility_policy_version, is_fixture,\nquality_summary, effective_eligibility_summary, backtest_eligible, block_reason,\nstrategy, strategy_version, parameters, run_hash.\n\nKeine DB-Migration nötig — sauber im bestehenden `backtest_run.metrics` (JSONB).\n\n## 5. Run-Identität / Reproduzierbarkeit (Punkt 5)\nM12 hatte bereits deterministischen `run_hash` (strategy/version/params/symbol/\ntimeframe/start/end/data_hash). Für historical_v2 wird ZUSÄTZLICH `dataset_version_hash`\n+ `eligibility_policy_version` in den Hash aufgenommen (nur wenn gesetzt).\n- gleiche Daten + Strategie + Parameter + Policy → gleiche Identität\n- andere Dataset-Version → andere Identität\n- andere Policy-Version → andere Identität\n- Legacy (beide None) → exakt alter Hash, unverändert\n\n## 6. Echter EURUSD-V2-Blocktest (POSITIV, Punkt 6)\nGegen historical-DB (Port 5433): DatasetContext liefert aktuell\n`backtest_eligible=False`, `block_reason=NO_DATASET_METADATA`,\n`dataset_version_id=None` (keine Dataset-Version für EURUSD/H4 2024-01).\nGate blockt mit `DATASET_VERSION_NOT_FOUND` (strenger als UNKNOWN).\nKeine Strategieausführung, Run als BLOCKED auditierbar.\n-> PASS (fail-closed, keine Echtorders)\nAnmerkung: Der im Brief erwartete Code `UNKNOWN_CRITICAL_METADATA` trifft hier\nnicht zu, weil keine Dataset-Version existiert. Der UNKNOWN-Code ist separat über\nMock-Test abgedeckt. Beides gültige fail-closed Blöcke.\n\n## 7. Positiver ELIGIBLE-Test (Punkt 7)\nMock/Test-DatasetContext vollständig & backtest_eligible=true, mit\nALLOW_FIXTURE_DATA=true → Gate lässt durch, M12 läuft bis zur bestehenden\nBacktestlogik, Audit vollständig. (Test 3, 7 grün.)\n\n## 8. QualitySummary im Run (Punkt 8)\ntotal/eligible/conditional/excluded/unknown/suspect/partial/stale_bars,\ngap_count, coverage_pct — über `quality_summary` im Audit. None bleibt None\n(nicht 0/100% erfunden), bewiesen durch Test.\n\n## 9. CONDITIONAL Data Policy (Punkt 9)\nKeine finale Handelslogik. Audit kennzeichnet CONDITIONAL sichtbar\n(`effective_eligibility_summary.conditional`). Kein stilles Zählen als FULL.\n\n## 10. Feed Type (Punkt 10)\nHistorical V2 Dukascopy: feed_type=REFERENCE — im Audit sichtbar. Kein Eindruck\nREFERENCE == IG Broker Feed, keine Kosten-Simulation.\n\n## 11. Legacy-Kompatibilität\n- Legacy (source=legacy) → kein Gate, kein Audit, kein neuer Fehler\n- M12 12/12 unverändert grün\n- Legacy run_hash deterministisch unverändert (Phase-7 J, Phase-6 12)\n- kein Hash-/Cache-Break\n- Legacy bleibt Default\n\n## 12. Tests (Punkt 12)\nNeue Suite `m12_app/tests/test_phase8_gate.py` (13 Tests) — alle grün:\n1 Legacy unverändert | 2 V2 Context vollständig | 3 eligible erlaubt\n4 not_eligible blockiert | 5 UNKNOWN blockiert | 6 Fixture default blockiert\n7 Fixture explizit erlaubt | 8 fehlender Context fail-closed\n9 fehlende Version fail-closed | 10 andere dataset_hash → andere ID\n11 andere policy_version → andere ID | 12 gleiche Inputs → gleiche ID\n13 QualitySummary vollständig | 14 CONDITIONAL sichtbar | 15 REFERENCE sichtbar\n\nRegression:\n- M12 12/12 ✅\n- M13 AJ ✅ (test_shared_repository)\n- Phase 5 20/20 ✅\n- Phase 6 14/14 ✅\n- Phase 7 AJ ✅\n\n## 13. Produktionsdeploy\nNUR M12. HISTORICAL_DATA_SOURCE bleibt legacy/unset, ALLOW_FIXTURE_DATA bleibt\nfalse/unset. Keine Netzwerkänderung. (Deploy-Pfad: scp → docker cp → chown)\n## 14. Dokumentation\nDiese Datei. Push zu Forgejo/Tolaria in Phase 8 formalisiert (nur diese Datei, keine Secrets); Obsidian NUR lokal, KEIN Push.\n\n## 15. Legacy-Smoke-Ergebnis (POST /backtest, BT_PULL 1h demo, 2026-08-01→19)\nAusgeführt im Produktions-Container Modul-12-Backtesting (urllib, Port 55012):\n- **Run A**: status=COMPLETED, run_id=ed62f1b3628b4fa29c3e880d83002ed8\n- **Run B**: status=COMPLETED, run_id=73a8cf75034a439cba60e974bd07304b\n- **run_hash**: `bc6e2553…` A == B identisch ✅\n- **data_hash**: `d76a7549…` A == B identisch ✅\n- **error_code**: None / None ✅ (kein Gate-Code)\n- **block_reason**: None / None ✅\n- **audit vorhanden (Legacy)**: False (korrekt minimal, kein Audit-Feld nötig)\n- KEIN DATASET_NOT_ELIGIBLE / DATASET_VERSION_NOT_FOUND / FIXTURE_DATA_BLOCKED\n- **Produktions-DB (trading.backtest_run)**: status=COMPLETED, run_hash bc6e2553…\n- **Health/Ready nach Smoke**: /health 200, /health/ready 200\n- **Production Gate nach Smoke**: HISTORICAL_DATA_SOURCE=UNSET, ALLOW_FIXTURE_DATA=UNSET\n\n**Legacy-Hash-Nachweis (vorher=nachher):**\n- Pre-Phase-8-Hash (Backup) payload = {strategy,version,params,symbol,timeframe,start,end,data_hash}\n- Phase 8 fügt dataset_version_hash/eligibility_policy_version NUR hinzu wenn `is not None`\n → Legacy ruft mit None auf → **byte-identischer payload → identischer Hash.**\n- Empirisch 2 unabhängige Runs nach Deploy → gleicher run_hash bc6e2553…\n- ⇒ Legacy run_hash deterministisch UNVERÄNDERT gegenüber vor Phase 8. ✅\n\n**Safety:** keine M09-Order, keine M07/M08/M15-Control-Auswirkung, keine Netzwerkänderung,\nkeine IG-Calls. Nur M12-Container-Neustart (Prozess, kein Docker-Netz/Config).\n\n## Bekannte offene Punkte\n- EURUSD-Context liefert NO_DATASET_METADATA (keine Dataset-Version hinterlegt).\n- CONDITIONAL-Handelslogik (Punkt 9) NICHT implementiert (nur Kennzeichnung) — bewusst.\n- Noch keine Broker-Kosten-/Feed-Angleichung — bewusst.\n- `verify_phase8_block.py` nutzt FakeStorage für den Gate-Pfad; echtes DB-Persistieren\n der BLOCKED-Runs wird im Container (Deploy) verifiziert.\n"
},
{
"path": "notes/trading/system-docs/infrastructure-handbook.md",
"title": "infrastructure-handbook",
"id": "object/5fcf4885-a361-f49f-2164-f4e10709da34",
"type": "arch",
"role": "reference",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "01df352eeb7af6a9492dffb86b19e8ae56c95463df174442cd7cc414e12d79f7",
"body": "\n\n# Infrastruktur & Betriebs-Handbuch — Trading-System VPS\n\n> Stand: 20.08.2026 · VPS: `187.124.31.123`\n\n## Multi-Agent-Architektur (Resident-Evil-Theme)\n| Agent | Plattform | Rolle | Port |\n|-------|-----------|-------|------|\n| **Alice** | OpenClaw (primary) | Hauptagent, Protagonistin | 55163 |\n| **Matt Addison** | OpenClaw (backup) | Backup/Support | 54524 |\n| **Rain Ocampo** | Hermes (support) | Technischer Support | 32776 (UI) |\n\nNotation in Notion/Forgejo bei JEDER Änderung:\n```\nGeändert von: [Alice | Rain Ocampo]\nDatum: DD.MM.YYYY HH:MM\nGrund: …\n```\n\n## Container-Verwaltung (via SSH)\n```bash\nssh root@187.124.31.123\ndocker compose -f /opt/trading-modules/docker-compose.yml ps\ndocker compose -f /opt/trading-modules/docker-compose.yml up -d <service>\ndocker compose -f /opt/trading-modules/docker-compose.yml build <service>\ndocker compose -f /opt/trading-modules/docker-compose.yml down # nur bei explizitem Wunsch\n```\n\n## Trading-Module (17 Container)\n- Compose: `/opt/trading-modules/docker-compose.yml`\n- Container-Namen exakt: `Modul-01-PostgreSQL` … `Modul-17-Hermes-Agent`\n- Netzwerk: `trading-modules`\n- Modul-01 = PostgreSQL 16-alpine (Host 55432)\n- Modul-02 = RabbitMQ 3-management (Host 55672/15672, vhost `trading`)\n- Modul-03 = Market-Data (FastAPI, Port 55003 intern, **nicht öffentlich**)\n- Modul-0417 = alpine-Platzhalter (bis ausgebaut)\n\n## Wichtige Hinweise\n- **Coolify überschreibt Container-Configs bei Neustart.** Config-Änderungen an OpenClaw ausschließlich über Config-Dateien (nicht UI).\n- **Hot-Reload** OpenClaw: `kill -HUP 1` im Container.\n- **Ollama** braucht `OLLAMA_API_KEY`; interner Hostname `ollama-nb6d-ollama-1:11434`.\n- VPS-Authentifizierung: **nur Public-Key-Auth**.\n\n## Deleted Resources (VPS-Aufräumung 20.08.2026)\nFolgendes 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.\n\n---\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Infrastruktur-Handbuch für das Trading-System dokumentiert.\n```\n"
},
{
"path": "notes/trading/system-docs/modul-01-postgresql.md",
"title": "modul-01-postgresql",
"id": "object/ebdc6b2b-4b30-c839-729b-c240ad433a60",
"type": "arch",
"role": "module",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "7cff67f84e55b6d3ca03920f207c90099af5985c6a88ac456303e325292dae06",
"body": "\n# Modul-01-PostgreSQL — Betriebsdokumentation\n\n> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ In Betrieb (healthy)\n\n## Zweck\nZentrale Datenbank des Modul-Trading-Systems. Persistiert Marktdaten (OHLCV), Journal- und\nBetriebszustände. Keine Strategie, keine Orders — nur zuverlässige Datenspeicherung.\n\n## Container\n| Attribut | Wert |\n|----------|------|\n| Name | `Modul-01-PostgreSQL` |\n| Image | `postgres:16-alpine` (PG 16.13) |\n| Port | **55432** (Host) / **5432** (Docker-intern) |\n| Netzwerk | `trading-modules` (bridge) |\n| IP (intern) | `10.0.13.13` |\n| Restart | `unless-stopped` |\n| Status | ✅ `healthy` |\n\n## Credentials (Compose Env)\n| Variable | Wert (Default) | Zweck |\n|----------|---------------|-------|\n| `POSTGRES_DB` | `trading` | Standard-Datenbank |\n| `POSTGRES_USER` | `trading` | Standard-User |\n| `POSTGRES_PASSWORD` | `trading` | aus `.env`-Defaults `${VAR:-default}` |\n\n## Konfiguration\n- **Image:** `postgres:16-alpine` (schlank, produktionsreif)\n- **PGDATA:** `/var/lib/postgresql/data`\n- **Docker-interner Servicename:** `Modul-01-PostgreSQL` (von anderen Modulen so referenziert, keine feste IP)\n- **Host-Port:** 55432 (für externen DB-Client / DBA-Tooling, nicht für Modul-Kommunikation)\n\n## Schema\nSiehe `modul-03-market-data.md` → Tabelle `public.ohlcv` (Symbol, Timeframe, OHLCV, Timestamps, Provider).\nUnique-Constraint `uq_ohlcv_provider_symbol_tf_ts` inkl. Provider für Multi-Broker-Unterstützung.\n\n## Verbraucher (Module, die schreiben/lesen)\n- **Modul-03-Market-Data** → schreibt `ohlcv` (Hauptschreiber)\n- **Modul-10-Trade-Journal** → Journal-Daten\n- Weitere Module lesen OHLCV für Analyse/Backtesting (Modul-11, 12, 13)\n\n## Sicherheit\n- Creds nur via Environment Variables / Docker-Secrets (Compose `${VAR:-default}`), nie in Doku-Repos hartkodiert\n- Host-Port 55432 nur wenn externer Zugriff nötig — kein unnötig öffentlicher Port\n\n---\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-01-Dokumentation nachgetragen (lief bereits produktiv, fehlte in Repo).\n```\n"
},
{
"path": "notes/trading/system-docs/modul-02-rabbitmq.md",
"title": "modul-02-rabbitmq",
"id": "object/b909ccbc-f7cf-7f71-46ea-817f0b697d35",
"type": "arch",
"role": "module",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "94cb38f3fddc6c0234e7e791bad922e90a2dadc42774e13e151b724be44a3318",
"body": "\n# Modul-02-RabbitMQ — Betriebsdokumentation\n\n> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ In Betrieb (healthy)\n\n## Zweck\nZentrale Event-Bus / Message-Queue des Modul-Trading-Systems. Verteilte Kommunikation zwischen\nden Modulen über Topics/Exchanges. Keine Strategie, keine Orders — nur zuverlässige\nNachrichtenvermittlung.\n\n## Container\n| Attribut | Wert |\n|----------|------|\n| Name | `Modul-02-RabbitMQ` |\n| Image | `rabbitmq:3-management` (RMQ 3.13.7) |\n| Ports | **55672** (AMQP, Host) / **15672** (Management-UI, Host) — beide nur intern |\n| Netzwerk | `trading-modules` (bridge) |\n| IP (intern) | `10.0.13.7` |\n| Restart | `unless-stopped` |\n| Status | ✅ `healthy` |\n\n## Credentials (Compose Env)\n| Variable | Wert (Default) | Zweck |\n|----------|---------------|-------|\n| `RABBITMQ_DEFAULT_VHOST` | `trading` | **Default-vhost `trading`** (wichtig!) |\n| `RABBITMQ_DEFAULT_USER` | `trading` | Default-User |\n| `RABBITMQ_DEFAULT_PASS` | `trading` | aus `.env`-Defaults `${VAR:-default}` |\n\n## Konfiguration\n- **Image:** `rabbitmq:3-management` (inkl. Management-Plugin für UI/Diagnose)\n- **Docker-interner Servicename:** `Modul-02-RabbitMQ` (von anderen Modulen so referenziert, keine feste IP)\n- **AMQP-Port intern:** 5672 (Host: 55672)\n- **Management-Port intern:** 15672 (Host: 15672)\n- **vhost `trading`** — alle Trading-Module nutzen diesen Vhost\n\n## Exchanges & Routing-Keys\nSiehe `modul-03-market-data.md` → Exchange `market.data` (topic, durable).\n\n| Routing-Key | Event-Typ | Wann |\n|-------------|-----------|------|\n| `market.data.ready` | `MARKET_DATA_READY` | Batch/Import — EIN Event pro Batch |\n| `market.data.candle.closed` | `MARKET_CANDLE_CLOSED` | Live — pro abgeschlossener Kerze |\n\n- Eventschema v1.0: `event_id, event_type, event_version, timestamp, symbol, asset_class, timeframe, provider` + payload.\n\n## Verbraucher/Produzenten\n- **Produzent:** Modul-03-Market-Data (publiziert `market.data.*`)\n- **Konsumenten:** Modul-04 ff. (Market Regime, Strategy Engine, Signal Ranking, …) — siehe jeweilige Modul-Doku\n- **Consumer-Regel:** `queue_delete` beim Start gegen Zombies (alte, hängende Consumer-Queues entfernen)\n\n## Sicherheit\n- Creds nur via Environment Variables / Docker-Secrets (Compose `${VAR:-default}`), nie in Doku-Repos hartkodiert\n- Ports 55672/15672 nur intern — kein unnötig öffentlicher Port\n\n---\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-02-Dokumentation nachgetragen (lief bereits produktiv, fehlte in Repo).\n```\n"
},
{
"path": "notes/trading/system-docs/modul-03-market-data.md",
"title": "modul-03-market-data",
"id": "object/cf54abfc-4c8b-7477-1b4f-21e76ca7b055",
"type": "arch",
"role": "module",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "35ea350530abf07a7f453fd34a1ad05a00fc0e348f9fddefcbf1f008bde4458d",
"body": "\n\n# Modul-03-Market-Data — Betriebsdokumentation\n\n> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ In Betrieb (healthy)\n\n## Zweck\nZentrale Marktdatenquelle des Trading-Systems. Pipeline:\n`Marktdaten → Validierung → Normalisierung → PostgreSQL → RabbitMQ-Event`\nKeine Strategie, keine Orders, keine KI — nur zuverlässige Marktdaten.\n\n## Container\n| Attribut | Wert |\n|----------|------|\n| Name | `Modul-03-Market-Data` |\n| Image | `market-data:0.1.0` (lokal gebaut) |\n| Port | **55003** — **NUR intern** (`expose`, nicht öffentlich) |\n| Netzwerk | `trading-modules` (bridge) |\n| Build-Context | `/opt/trading-modules/modul03-market-data/` |\n| Restart | `unless-stopped` |\n| Healthcheck | ✅ `healthy` |\n\n## Deployment\n```bash\ncd /opt/trading-modules\ndocker compose build modul-03-market-data\ndocker compose up -d modul-03-market-data\n```\n\n## Environment Variables (Compose)\n| Variable | Wert (Default) | Zweck |\n|----------|---------------|-------|\n| `PG_HOST` | `Modul-01-PostgreSQL` | Docker-interner Servicename |\n| `PG_PORT` | `5432` | intern (Host: 55432) |\n| `PG_USER/PASSWORD/DB` | `trading` | aus `.env`-Defaults |\n| `RABBITMQ_HOST` | `Modul-02-RabbitMQ` | Docker-interner Servicename |\n| `RABBITMQ_PORT` | `5672` | intern (Host: 55672) |\n| `RABBITMQ_USER/PASSWORD/VHOST` | `trading` | **vhost `trading`** |\n| `DATA_PROVIDER` | `noop` | `noop` / `demo` / später Broker |\n| `LOG_LEVEL` | `INFO` | Strukturiertes Logging |\n\nKeine festen IPs — nur Docker-interne Hostnamen. Creds via Compose env + `${VAR:-default}`.\n\n## Datenbank (Modul-01-PostgreSQL)\nTabelle `public.ohlcv`:\n```sql\nsymbol TEXT, asset_class TEXT, provider TEXT, timeframe TEXT,\nts TIMESTAMPTZ, open/high/low/close DOUBLE PRECISION, volume DOUBLE PRECISION,\ncreated_at TIMESTAMPTZ DEFAULT now(), id BIGSERIAL PRIMARY KEY\n```\n\n### Finale Constraints & Indizes (Stand 20.08.2026)\n| Index | Typ |\n|-------|-----|\n| `uq_ohlcv_provider_symbol_tf_ts` | **UNIQUE** `(provider, symbol, timeframe, ts)` |\n| `ohlcv_pkey` | UNIQUE `(id)` |\n| `idx_ohlcv_symbol_tf` | `(symbol, timeframe)` |\n| `idx_ohlcv_symbol_tf_ts` | `(symbol, timeframe, ts DESC)` |\n| `idx_ohlcv_provider_symbol_tf_ts` | `(provider, symbol, timeframe, ts DESC)` |\n\nDer Unique-Index inkludiert den **Provider** — langfristig werden mehrere Provider/Broker unterstützt\n(gleiche Symbol+Timeframe+Timestamp können von verschiedenen Quellen kommen).\n\n**Migrationen:** `/app/migrations/001_ohlcv.sql` + `002_unique_provider.sql`\n(idempotent, löschen nichts; automatisch via `ensure_schema()`/glob angewendet).\n\n## RabbitMQ (Modul-02)\n- **Exchange:** `market.data` (topic, durable)\n- **vhost:** `trading` (wichtig!)\n- **Routing-Keys:**\n\n| Routing-Key | Event-Typ | Wann |\n|-------------|-----------|------|\n| `market.data.ready` | `MARKET_DATA_READY` | **Batch/Import** — EIN Event pro Batch, `candle_count` + `batch:true` |\n| `market.data.candle.closed` | `MARKET_CANDLE_CLOSED` | **Live** — pro abgeschlossener Kerze EIN Event, OHLCV im payload |\n\n- Eventschema v1.0: `event_id, event_type, event_version, timestamp, symbol, asset_class, timeframe, provider` + payload.\n\n## Interne API\n| Endpoint | Zweck |\n|----------|-------|\n| `GET /health` | Liveness (200 immer) + Komponentenstatus im Body |\n| `GET /health/ready` | Readiness (503 wenn PG/RabbitMQ down) |\n| `POST /ingest` | Kerzen einspeisen (JSON: `{\"candles\":[...]}`) |\n| `GET /prices/{symbol}` | Letzte Kurse |\n| `GET /history/{symbol}?timeframe=` | Historische OHLCV |\n\n## Provider-Adapter\n`app/providers/providers.py`:\n- `DataProvider` (ABC) — abstrakte Schnittstelle `fetch_ohlcv()`, `health()`\n- `NoopProvider` — keine Datenquelle konfiguriert (Default)\n- `DemoProvider` — synthetische OHLCV-Daten für Tests\n- Neue Broker = neue Klasse, umschalten via `DATA_PROVIDER` env → keine harte Anbieter-Kopplung\n\n## Validierung (`app/validation/validator.py`)\n- Timestamp gültig (UTC, nicht Zukunft, nicht zu alt/stale)\n- OHLC-Werte > 0\n- High ≥ Low, High ≥ Open/Close, Low ≤ Open/Close\n- Duplikat-Erkennung (Storage + DB-Unique-Index)\n\n## End-to-End-Test (20.08.2026, nach Provider-Constraint-Upgrade) ✅\n- **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.\n- **Live-Pfad:** 1 AMZN-Candle → `MARKET_CANDLE_CLOSED` (routing `market.data.candle.closed`, OHLCV im payload).\n- **Provider-Duplikat:** identische NVDA-Kerze erneut → `duplicate`, kein Insert, kein Event.\n- **Port-Sicherheit:** 55003 von außen (`http://187.124.31.123:55003/health`) → **nicht erreichbar** ✅\n- Datenbestand final: AAPL 5, GOOG 4, NVDA 3, EURUSD 3, TSLA 4, MSFT 1, AMZN 1.\n\n## Offene Punkte\n- Echter Broker-/Datenprovider-Adapter (Interface bereit, noop/demo Defaults)\n- Zusätzliche Daten (Bid/Ask/Spread, Ticks, Fundamentaldaten) — vorbereitet\n- Consumer für `market.data.ready` / `market.data.candle.closed` (Modul-04 ff.)\n\n---\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-03-Dokumentation angelegt (Provider-Constraint, Event-Trennung, Port-Nicht-Exposition).\n```\n"
},
{
"path": "notes/trading/system-docs/modul-04-market-regime.md",
"title": "modul-04-market-regime",
"id": "object/36d6dc60-1965-ed65-a002-cdb477ccd638",
"type": "arch",
"role": "module",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "ff0b97f9245729cd3657b7364f5d9d82e748b36650fadcfbeb50c62c4a5d6b36",
"body": "\n\n# Modul-04-Market-Regime — Betriebsdokumentation\n\n> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ Freigegeben (E2E bestanden)\n\n## Zweck\nErster **Consumer** der Market-Data-Events von Modul-03. Pipeline:\n`MARKET_DATA_READY / MARKET_CANDLE_CLOSED (Modul-03) → Regime-Berechnung → PostgreSQL (market_regime) → MARKET_REGIME_READY`\n**Deterministische, regelbasierte Engine — bewusst OHNE KI/ML.**\n\n## Container\n| Attribut | Wert |\n|----------|------|\n| Name | `Modul-04-Market-Regime` |\n| Image | `market-regime:1.0.0` (lokal gebaut) |\n| Port | **55004** — **NUR intern** (`expose`, nicht öffentlich) |\n| Netzwerk | `trading-modules` (bridge) |\n| Build-Context | `/opt/trading-modules/modul04-market-regime/` |\n| Restart | `unless-stopped` |\n| Healthcheck | ✅ `healthy` |\n\n## Deployment\n```bash\ncd /opt/trading-modules\ndocker compose build modul-04-market-regime\ndocker compose up -d --no-deps --force-recreate modul-04-market-regime\n```\n\n## Environment Variables (Compose)\n| Variable | Wert (Default) | Zweck |\n|----------|---------------|-------|\n| `PG_HOST` | `Modul-01-PostgreSQL` | Docker-interner Servicename |\n| `PG_PORT` | `5432` | intern (Host: 55432) |\n| `PG_USER/PASSWORD/DB` | `trading` | aus `.env`-Defaults |\n| `RABBITMQ_HOST` | `Modul-02-RabbitMQ` | Docker-interner Servicename |\n| `RABBITMQ_PORT` | `5672` | intern (Host: 55672) |\n| `RABBITMQ_USER/PASSWORD/VHOST` | `trading` | **vhost `trading`** |\n| `LOG_LEVEL` | `INFO` | Strukturiertes Logging |\n\nKeine festen IPs — nur Docker-interne Hostnamen. Creds via Compose env + Defaults.\n\n## Architektur\n```\nModul-03 ──market.data.ready / market.data.candle.closed──▶ RegimeConsumer\n │ (bindet beide Routing-Keys)\n ▼\n RegimeEngine (deterministisch)\n EMA / ADX / ATR / Slope / Preisstruktur\n │\n ┌───────────┴───────────┐\n ▼ ▼\n market_regime (PG) MARKET_REGIME_READY\n (16 Spalten) → market.regime.ready\n```\n\n- **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).\n- **Engine** (`app/regime/engine.py`): deterministisch, ohne KI.\n- **Storage** (`app/storage/storage.py`): idempotent via partiellen Unique-Index.\n- **Publisher** (`app/publisher/publisher.py`): publiziert `MARKET_REGIME_READY` auf `market.regime` (Routing `market.regime.ready`).\n- **History-Client** (`app/marketdata/client.py`): liest OHLCV über interne FastAPI Modul-03 (`http://Modul-03-Market-Data:55003/history/{symbol}`).\n\n## Regime-Engine (`app/regime/engine.py`)\nDeterministische Regel-Engine (Version `1.0.0`), 7 Regime:\n`TREND_UP, TREND_DOWN, RANGE, HIGH_VOLATILITY, LOW_VOLATILITY, TRANSITION, UNKNOWN`\n\n**Indikatoren & Metriken:**\n| Indikator | Fenster/Param | Zweck |\n|-----------|---------------|-------|\n| EMA fast/slow | 10 / 30 | Trendrichtung (EMA-Flanken-Differenz) |\n| ADX | 14 | Trendstärke (≥20 = echter Trend) |\n| ATR | 14 | Volatilität (absolut + Ratio + Perzentil) |\n| Slope | 20 | normierte Steigung der Close-Linie |\n| Preisstruktur | — | higher_highs / lower_lows / range |\n\n**Zentrale Schwellenwerte** (`app/config.py`, env-overridable):\n| Parameter | Default | Bedeutung |\n|-----------|---------|-----------|\n| `min_candles_required` | 30 | UNKNOWN, wenn weniger Daten |\n| `regime_lookback` | 60 | max. Kerzen für Berechnung |\n| `trend_min_ema_gap` | 0.02 | |EMA_fast-EMA_slow|/close ≥ → Trend |\n| `adx_trend_threshold` | 20.0 | ADX ≥ → echter Trend |\n| `slope_up/down_threshold` | 0.05 / -0.05 | normierte Steigung |\n| `range_atr_ratio` | 0.02 | ATR/close darunter = Range |\n| `atr_high_vol_multiplier` | 1.5 | ATR jetzt > hist_mean × → HIGH_VOL |\n| `atr_low_vol_multiplier` | 0.6 | ATR jetzt < hist_mean × → LOW_VOL |\n| `high_vol_atr_ratio` | 0.03 | ATR/close ≥ → starke Vol |\n| `low_vol_atr_ratio` | 0.008 | ATR/close ≤ → geringe Vol |\n| `slope_threshold` | 0.01 | |Slope| darunter = seitwärts |\n| `transition_min_events` | 3 | Events für TRANSITION |\n\n## Datenbank (Modul-01-PostgreSQL)\nTabelle `public.market_regime` (16 Spalten, eigene Tabelle — bestehende unangetastet):\n```sql\nsymbol TEXT, asset_class TEXT, timeframe TEXT, provider TEXT,\nregime TEXT, confidence INTEGER (0-100),\ntrend_strength DOUBLE PRECISION, volatility_state TEXT,\ntimestamp TIMESTAMPTZ, indicators_json JSONB,\ncandles_used INTEGER, version TEXT,\nsource_event_id TEXT, correlation_id TEXT,\ndata_ts TIMESTAMPTZ, data_ts_end TIMESTAMPTZ\n```\n- **Unique (partiell):** `uq_market_regime_src` auf `(symbol, timeframe, source_event_id)` **WHERE source_event_id IS NOT NULL** → Idempotenz.\n- Migration: `migrations/001_market_regime.sql` (idempotent, löscht nichts).\n\n## RabbitMQ (Modul-02)\n| Exchange | Typ | Routing | Event |\n|----------|-----|---------|-------|\n| `market.data` | topic | `market.data.ready` (eingang) | MARKET_DATA_READY |\n| `market.data` | topic | `market.data.candle.closed` (eingang) | MARKET_CANDLE_CLOSED |\n| `market.regime` | topic | `market.regime.ready` (**ausgang**) | MARKET_REGIME_READY |\n\n## Interne API\n| Endpoint | Zweck |\n|----------|-------|\n| `GET /health` | Liveness (200 immer) + Komponentenstatus im Body |\n| `GET /health/ready` | Readiness (503 wenn PG/RabbitMQ/Modul-03 down) |\n| `GET /regime/{symbol}` | Regime-Einträge abfragen |\n| `GET /regime/latest/{symbol}` | Letztes Regime eines Symbols |\n\n## End-to-End-Test (20.08.2026, final, nach Rebuild) ✅\nKette verifiziert: Modul-03 Ingest → `market.data.ready` → Modul-04 Consumer → RegimeEngine → `market_regime` → `MARKET_REGIME_READY` auf `market.regime.ready`.\n\n| Fall | Regime | Conf | candles | version | Event | DB |\n|------|--------|------|---------|---------|-------|----|\n| M4TREND_UP (40) | TREND_UP | 100 | 40 | 1.0.0 | genau 1 | ✅ |\n| M4TREND_DN (40) | TREND_DOWN | 100 | 40 | 1.0.0 | genau 1 | ✅ |\n| M4RANGE (40) | LOW_VOLATILITY (Range) | 60 | 40 | 1.0.0 | genau 1 | ✅ |\n| M4HIGHVOL (40) | HIGH_VOLATILITY | 75 | 40 | 1.0.0 | genau 1 | ✅ |\n| M4UNKNOWN (5) | UNKNOWN | 20 | 5 | 1.0.0 | genau 1 | ✅ |\n| M4IDEMPOT (40) | TREND_UP | 100 | 40 | 1.0.0 | genau 1 | ✅ |\n\n**Idempotenz:** dasselbe Quell-Event (`source_event_id`) erneut → **kein zweiter Datensatz**, kein Doppel-Event. ✅\n**Logs:** keine Errors/Tracebacks. Health `{postgresql:true, rabbitmq:true, market_data_ready:true}`. ✅\n\n## Bugs behoben während E2E (20.08.2026)\n| Bug | Fix |\n|-----|-----|\n| `can't adapt type 'dict'` (JSONB) | `json.dumps(ind.model_dump(mode=\"json\"))` |\n| `tuple index out of range` (16/15) | `version` in INSERT-VALUES ergänzt |\n| `ON CONFLICT` + partieller Index Fehler | `WHERE source_event_id IS NOT NULL` in Klausel |\n| `model_dump(default=...)` TypeError | `default`-Kwarg entfernt (`model_dump(mode=\"json\")`) |\n| Consumer-Verbindungs-Leak | `conn.close()` bei Reconnect → kein Message-Leak |\n\n## Offene Punkte\n- Consumer-Downstream für `market.regime.ready` (Modul-05+)\n- Bestätigte TRANSITION-Detektion mit echten Folgedaten\n\n---\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-04-Dokumentation angelegt (Regime-Engine, market_regime-Schema, Events, E2E freigegeben).\n```\n"
},
{
"path": "notes/trading/system-docs/modul-05-strategy-engine.md",
"title": "modul-05-strategy-engine",
"id": "object/58f1bc40-5b12-87a0-e95f-5288b91ebc87",
"type": "arch",
"role": "module",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "d7701db394982fb9609d201da53797b8da5e7bb7b506d4da29d284d64fb0d413",
"body": "\n\n# Modul-05-Strategy-Engine — Betriebsdokumentation\n\n> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ Freigegeben (E2E bestanden)\n\n## Zweck\n**Strategie-Engine** — konsumiert `MARKET_REGIME_READY` (Modul-04) + OHLCV (Modul-03), berechnet deterministische Handelssignale und persistiert sie.\nPipeline: `MARKET_REGIME_READY (Modul-04) → Strategie-Engine (trend_pullback_v1) → PostgreSQL (strategy_signal) → SIGNAL_DETECTED`\n**Deterministische, regelbasierte Engine — bewusst OHNE KI/ML, ohne Ranking, ohne Risk Management, ohne Order-Ausführung.**\n\n## Container\n| Attribut | Wert |\n|----------|------|\n| Name | `Modul-05-Strategy-Engine` |\n| Image | `strategy-engine:0.1.0` (lokal gebaut) |\n| Port | **55005** — **NUR intern** (`expose`, nicht öffentlich) |\n| Netzwerk | `trading-modules` (bridge) |\n| Build-Context | `/opt/trading-modules/modul05-strategy-engine/` |\n| Restart | `unless-stopped` |\n| Healthcheck | ✅ `healthy` |\n\n## Deployment\n```bash\ncd /opt/trading-modules\ndocker compose build modul-05-strategy-engine\ndocker compose up -d --no-deps --force-recreate modul-05-strategy-engine\n```\n\n## Environment Variables (Compose)\n| Variable | Wert (Default) | Zweck |\n|----------|---------------|-------|\n| `PG_HOST` | `Modul-01-PostgreSQL` | Docker-interner Servicename |\n| `PG_PORT` | `5432` | intern |\n| `PG_USER/PASSWORD/DB` | `trading` | aus `.env`-Defaults |\n| `RABBITMQ_HOST` | `Modul-02-RabbitMQ` | Docker-interner Servicename |\n| `RABBITMQ_PORT` | `5672` | intern |\n| `RABBITMQ_USER/PASSWORD/VHOST` | `trading` | **vhost `trading`** |\n| `LOG_LEVEL` | `INFO` | Strukturiertes Logging |\n\nKeine festen IPs — nur Docker-interne Hostnamen. Creds via Compose env + Defaults.\n\n## Architektur\n```\nModul-04 ──market.regime.ready──▶ StrategyConsumer (strategy.input)\n │\n ▼\n OHLCV (Modul-03, intern 55003/history/{symbol})\n │\n ▼\n StrategyEngine (deterministisch)\n trend_pullback_v1 (LONG/SHORT)\n │\n ┌───────────┴───────────┐\n ▼ ▼\n strategy_signal (PG) SIGNAL_DETECTED\n (eigene Tabelle) → market.signals / strategy.signal.detected\n```\n\n- **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).\n- **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`.\n- **Engine/Register** (`app/engine.py`): modular — Strategien via `.name/.version/.evaluate()` registriert.\n- **Storage** (`app/storage/storage.py`): idempotent via partiellen Unique-Index.\n- **Publisher** (`app/publisher/publisher.py`): **frische Verbindung je Publish** + `conn.close()` im finally (verhindert `ConnectionResetError` durch RabbitMQ-Closed-Verbindungen).\n- **Service** (`app/core/service.py`): Pipeline Event → OHLCV → Strategie → speichern + publizieren; robuste Payload-Extraktion.\n\n## Strategie V1 — `trend_pullback_v1` (`app/strategies/trend_pullback_v1.py`)\nDeterministische Pullback-Strategie. **Nur abgeschlossene Candles, kein Lookahead-Bias.**\n\n**LONG-Bedingungen (Regime TREND_UP):**\n1. Close > steigender SMA200 (Trendfilter)\n2. Pullback: Close < EMA20 (Zug zurück in den Trend)\n3. Bestätigung: Close > prev Close ODER Break prev High\n4. Entry = Close; Stop unter Swing-Low; Target = 2R (R:R = 2.0)\n\n**SHORT-Bedingungen (Regime TREND_DOWN):** spiegelbildlich\n1. Close < fallender SMA200\n2. Pullback: Close > EMA20\n3. Bestätigung: Close < prev Close ODER Break prev Low\n4. Stop über Swing-High; Target = 2R\n\n**Mathematik (vom E2E verifiziert):**\n- **LONG:** `Target = Entry + 2 × (Entry - Stop)`; `R:R = 2.0`; Stop < Entry\n- **SHORT:** `Target = Entry - 2 × (Stop - Entry)`; `R:R = 2.0`; Stop > Entry\n\n**Zentrale Schwellenwerte** (`app/config.py`, env-overridable):\n| Parameter | Default | Bedeutung |\n|-----------|---------|-----------|\n| `lookback` | 300 | max. Kerzen zur Berechnung |\n| `min_candles_required` | 220 | UNKNOWN, wenn < 220 (SMA200 braucht 200) |\n| `sma_period` | 200 | Trendfilter SMA |\n| `ema_period` | 20 | Pullback-EMA |\n| `risk_reward` | 2.0 | Target-Multiplikator (2R) |\n| `swing_lookback` | 10 | Swing-Low/High-Fenster für Stop |\n\nKein gültiges Setup → **kein Event** publiziert.\n\n## Datenbank (Modul-01-PostgreSQL)\nEigene Tabelle `public.strategy_signal` (bestehende unangetastet):\n```sql\nsignal_id UUID, source_event_id TEXT, correlation_id TEXT,\ntimestamp TIMESTAMPTZ, symbol TEXT, asset_class TEXT, provider TEXT,\ntimeframe TEXT, strategy_name TEXT, strategy_version TEXT,\ndirection TEXT (LONG/SHORT), regime TEXT,\nentry DOUBLE PRECISION, stop_loss DOUBLE PRECISION, target DOUBLE PRECISION,\nrisk_reward DOUBLE PRECISION, setup_metrics JSONB, trigger_reason TEXT\n```\n- **Unique (partiell):** `uq_strategy_signal_src` auf `(symbol, timeframe, source_event_id)` **WHERE source_event_id IS NOT NULL** → Idempotenz.\n- Migration: `migrations/001_strategy_signal.sql` (idempotent, löscht nichts).\n\n## RabbitMQ (Modul-02)\n| Exchange | Typ | Routing | Event |\n|----------|-----|---------|-------|\n| `market.regime` | topic | `market.regime.ready` (eingang) | MARKET_REGIME_READY |\n| `market.signals` | topic | `strategy.signal.detected` (ausgang) | SIGNAL_DETECTED |\n\n## Interne API\n| Endpoint | Zweck |\n|----------|-------|\n| `GET /health` | Liveness (200) + Komponentenstatus im Body |\n| `GET /health/ready` | Readiness (503 wenn PG/RabbitMQ/Modul-03 down) |\n\n## End-to-End-Test (20.08.2026, final, nach Publisher-Fixes) ✅\nKette verifiziert: Modul-03 Ingest → `market.data.ready` → Modul-04 → `market.regime.ready` → Modul-05 Consumer → `trend_pullback_v1` → `strategy_signal` → `SIGNAL_DETECTED`.\n\n| Fall | Signal | Entry | Stop | Target | R:R | DB | Event |\n|------|--------|-------|------|--------|-----|----|-------|\n| M5LONG (TREND_UP) | **LONG** | 164.20 | 163.4764 | 165.6473 | **2.00** | ✅ 1 | ✅ 1 |\n| M5SHORT (TREND_DOWN) | **SHORT** | 135.80 | 136.4964 | 134.4073 | **2.00** | ✅ 1 | ✅ 1 |\n| M5NOPULL (kein Pullback) | keins | — | — | — | — | ✅ 0 | ✅ 0 |\n| M5RANGE (Range) | keins | — | — | — | — | ✅ 0 | ✅ 0 |\n\n**Mathematik verifiziert:** LONG `Target=Entry+2×(Entry-Stop)` = 164.20 + 2×0.72364 = **165.6473** ✓; SHORT `Target=Entry2×(StopEntry)` = 135.80 2×0.69636 = **134.4073** ✓.\n**Idempotenz:** identisches `source_event_id` erneut → **kein zweiter DB-Eintrag, kein Doppel-Event** (count=1). ✅\n**RabbitMQ-Reconnect:** kontrollierter Neustart → Modul-04+05 verbinden automatisch (Backoff 1s→2s→4s→8s), **je exakt 1 Consumer, keine Zombies, keine verlorenen Events.** ✅\n**Logs:** keine `ConnectionResetError`, keine unbehandelten Tracebacks. Health `{postgresql:true, rabbitmq:true, market_data_ready:true}`. ✅\n\n## Bugs behoben während E2E (20.08.2026)\n| Bug | Fix |\n|-----|-----|\n| `ConnectionResetError` beim Publish | Publisher: **frische Verbindung je Publish** + `conn.close()` (RabbitMQ schließt ungenutzte Verbindung) — **gleicher Bug in Modul-03, -04, -05** |\n| Modul-03 OHLCV-Feld `ts` | Normalisierung `ts`→`timestamp` im MarketData-Client |\n| Zombie-Consumer (`strategy.input` 23) | `queue_delete` beim Consumer-Start; manuelles Cleanup via `rabbitmqctl delete_queue` |\n| Keine Events nach Recreate | OHLCV/Regime-Daten waren noch in DB (Duplikat) → E2E bereinigt `ohlcv`+`market_regime`+`strategy_signal` |\n| M5SHORT ging verloren | Zombie-Consumer verschluckte Event (Round-Robin) → beseitigt |\n\n## Offene Punkte\n- Downstream-Consumer für `strategy.signal.detected` (Modul-06+)\n- Weitere Strategien über Engine-Register hinzufügbar\n\n---\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-05-Dokumentation angelegt (Strategy-Engine trend_pullback_v1, strategy_signal-Schema, Events, E2E freigegeben).\n```\n"
},
{
"path": "notes/trading/system-docs/modul-06-signal-ranking.md",
"title": "modul-06-signal-ranking",
"id": "object/5c04acf8-baf8-0771-f36b-05bf5cf58138",
"type": "arch",
"role": "module",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "06369ccc8de498ca026c4ea093a1aedb7f84acadacb1af5bbb6ed60110cc1df5",
"body": "\n\n# Modul-06-Signal-Ranking — Betriebsdokumentation\n\n> Erstellt: 20.08.2026 (Rain Ocampo) · Status: ✅ **Freigegeben** (E2E+Idempotenz+Reconnect+Doku grün)\n\n## Zweck\n**Signal-Ranking** — konsumiert `SIGNAL_DETECTED` (Modul-05) + OHLCV (Modul-03), bewertet die Signalqualität deterministisch (Score 0100, vollständig nachvollziehbar und konfigurierbar), persistiert das Ergebnis und publiziert `SIGNAL_RANKED`.\nPipeline: `SIGNAL_DETECTED (Modul-05) → Signal-Ranking (signal_quality_v1) → PostgreSQL (signal_ranking) → SIGNAL_RANKED`\n**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.\n\n## Container\n| Attribut | Wert |\n|----------|------|\n| Name | `Modul-06-Signal-Ranking` |\n| Image | `signal-ranking:0.1.0` (lokal gebaut) |\n| Port | **55006** — **NUR intern** (`expose`, nicht öffentlich) |\n| Netzwerk | `trading-modules` (bridge) |\n| Build-Context | `/opt/trading-modules/modul06-signal-ranking/` |\n| Restart | `unless-stopped` |\n| Healthcheck | ✅ `healthy` |\n\n## Deployment\n```bash\ncd /opt/trading-modules\ndocker compose build modul-06-signal-ranking\ndocker compose up -d --no-deps --force-recreate modul-06-signal-ranking\n```\n\n## Environment Variables (Compose)\n| Variable | Wert (Default) | Zweck |\n|----------|---------------|-------|\n| `PG_HOST` | `Modul-01-PostgreSQL` | Docker-interner Servicename |\n| `PG_PORT` | `5432` | intern |\n| `PG_USER/PASSWORD/DB` | `trading` | aus `.env`-Defaults |\n| `RABBITMQ_HOST` | `Modul-02-RabbitMQ` | Docker-interner Servicename |\n| `RABBITMQ_PORT` | `5672` | intern |\n| `RABBITMQ_USER/PASSWORD/VHOST` | `trading` | **vhost `trading`** |\n| `LOG_LEVEL` | `INFO` | Strukturiertes Logging |\n\nKeine festen IPs — nur Docker-interne Hostnamen. Creds via Compose env + Defaults.\n\n## Architektur\n```\nModul-05 ──signal.detected──▶ RankingConsumer (ranking.input)\n │\n ▼\n OHLCV (Modul-03, intern 55003/history/{symbol})\n │\n ▼\n RankingEngine (deterministisch, Registry)\n signal_quality_v1 (9 Komponenten, Score 0100)\n │\n ┌───────────┴───────────┐\n ▼ ▼\n signal_ranking (PG) SIGNAL_RANKED\n (eigene Tabelle) → market.rankings / signal.ranked\n```\n\n- **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).\n- **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.\n- **Engine/Register** (`app/ranking/engine.py`): modular — Ranking-Engines via `.name/.version/rank()` registriert; KI/ML später ergänzbar.\n- **Storage** (`app/storage/storage.py`): idempotent via partiellen Unique-Index auf `source_signal_id`.\n- **Publisher** (`app/publisher/publisher.py`): **frische Verbindung je Publish** + `conn.close()` im finally (verhindert `ConnectionResetError` durch RabbitMQ-Closed-Verbindungen).\n- **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.\n\n## Ranking-Engine V1 — `signal_quality_v1` (`app/ranking/signal_quality_v1.py`)\nDeterministische Qualitätsbewertung. **9 Komponenten, Score 0100, vollständig nachvollziehbar und konfigurierbar.** Jede Komponente liefert 0..max; Summe = `total_score`. Komponenten werden einzeln in der DB gespeichert (`comp_*`).\n\n| Komponente | Max | Bewertet |\n|-----------|-----|----------|\n| `regime` | 20 | Market-Regime-Qualität / Confidence |\n| `trend` | 15 | Trendstärke |\n| `pullback` | 15 | Pullback-Qualität |\n| `trigger` | 10 | Entry-Bestätigung |\n| `rr` | 10 | Chance/Risiko-Verhältnis |\n| `volatility` | 10 | Volatilitätszustand |\n| `distance` | 10 | Distanz Entry→Stop |\n| `data_quality` | 5 | Datenqualität / Candle-Anzahl |\n| `consistency` | 5 | Setup-Konsistenz |\n\n**Klassifizierung:** `total_score >= eligible_threshold` → `eligible`; darunter `weak` (wird trotzdem gespeichert).\n\n**Zentrale Schwellenwerte** (`app/config.py`, env-overridable):\n| Parameter | Default | Bedeutung |\n|-----------|---------|-----------|\n| `max_score` | 100 | Score-Obergrenze |\n| `eligible_threshold` | 70 | Score ≥ 70 → `eligible` |\n| `ranking_lookback` | 500 | max. Kerzen für Trend/Volatilität (Modul-03 liefert die N ältesten, daher hoch) |\n| `min_candles_required` | 60 | unter dieser Zahl → Datenqualität-Punktabzug |\n| `full_candles_target` | 250 | ab dieser Zahl → volle Datenqualität |\n| `pullback_depth_min/max` | 0.5 / 4.0 | Abstand Entry zu Trend-Referenz in ATR |\n| `vol_ideal_lo/hi` | 0.005 / 0.030 | ATR %-Fenster für \"ideale\" Volatilität |\n\nKeine Trade-Freigabe — nur Qualitätsbewertung.\n\n## Datenbank (Modul-01-PostgreSQL)\nEigene Tabelle `public.signal_ranking` (bestehende unangetastet):\n```sql\nranking_id UUID, source_signal_id TEXT, source_event_id TEXT, correlation_id TEXT,\ntimestamp TIMESTAMPTZ, symbol TEXT, asset_class TEXT, provider TEXT, timeframe TEXT,\nranking_name TEXT, ranking_version TEXT, ranking_class TEXT (eligible/weak),\ntotal_score DOUBLE PRECISION, comp_regime/comp_trend/comp_pullback/comp_trigger/comp_rr/\ncomp_volatility/comp_distance/comp_data_quality/comp_consistency DOUBLE PRECISION,\ndetails JSONB, reasons JSONB\n```\n- **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).\n- Migration: `migrations/001_signal_ranking.sql` (idempotent, löscht nichts).\n\n## RabbitMQ (Modul-02)\n| Exchange | Typ | Routing | Event |\n|----------|-----|---------|-------|\n| `market.signals` | topic | `strategy.signal.detected` (eingang) | SIGNAL_DETECTED |\n| `market.rankings` | topic | `signal.ranked` (ausgang) | SIGNAL_RANKED |\n\n## Interne API\n| Endpoint | Zweck |\n|----------|-------|\n| `GET /health` | Liveness (200) + Komponentenstatus im Body |\n| `GET /health/ready` | Readiness (503 wenn PG/RabbitMQ/Modul-03 down) |\n| `GET /rankings/{symbol}` | Rankings eines Symbols |\n| `GET /rankings/latest/{symbol}` | Neuestes Ranking eines Symbols |\n| `GET /ranking-engines` | Registrierte Ranking-Engines |\n\n## End-to-End-Test (20.08.2026, final) ✅\nKette 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`.\n\n| Fall | Ranking | Score | Class | DB | SIGNAL_RANKED |\n|------|---------|-------|-------|----|---------------|\n| M5LONG (TREND_UP) | **eligible** | **72.2** | eligible | ✅ 1 | ✅ 1 |\n| M5SHORT (TREND_DOWN) | **eligible** | **73.6** | eligible | ✅ 1 | ✅ 1 |\n| M5NOPULL (kein Pullback) | — | — | — | ✅ 0 | ✅ 0 |\n| M5RANGE (Range) | — | — | — | ✅ 0 | ✅ 0 |\n\n**Verifizierte Eigenschaften:**\n- Gültiger LONG/SHORT → **genau 1** `SIGNAL_RANKED`; NOPULL/RANGE → **0** Ranking + 0 Event. ✅\n- DB-Eintrag je gültigem Signal vorhanden; `source_signal_id` korrekt übernommen (z.B. `effd782c...`, `42dd2042...`). ✅\n- `total_score` immer 0100; **Summe aller Komponenten == total_score**. ✅\n- `eligible`/`weak` korrekt gemäß Threshold (72.2/73.6 ≥ 70 → eligible). ✅\n- **Idempotenz:** identisches `SIGNAL_DETECTED` erneut → **kein zweiter DB-Eintrag, kein zweites SIGNAL_RANKED** (count=1). ✅\n- **RabbitMQ-Reconnect:** kontrollierter Neustart → Modul-04/05/06 verbinden automatisch (Backoff 1s→2s→4s→8s), **je exakt 1 Consumer, keine Zombies, keine verlorenen Events.** ✅\n- **Logs:** keine unbehandelten Tracebacks nach Reconnect. Health/Readiness `{postgresql:true, rabbitmq:true, market_data:true, consumer_ready:true}`. ✅\n\n## Bugs behoben während E2E (20.08.2026)\n| Bug | Fix |\n|-----|-----|\n| `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 |\n| 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) |\n| `direction`-Spalte existierte nicht | E2E-Query auf `details`/Modell angepasst |\n\n## Offene Punkte\n- Downstream-Consumer für `signal.ranked` (Modul-07+ — noch NICHT begonnen)\n- KI/ML-Ranking-Engine über Registry ergänzbar\n- Trade-Freigabe später durch Risk Manager (noch nicht gebaut)\n\n---\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-06-Dokumentation angelegt (Signal-Ranking signal_quality_v1, signal_ranking-Schema, Events, E2E final grün).\n```\n"
},
{
"path": "notes/trading/system-docs/modul-07-risk-manager.md",
"title": "modul-07-risk-manager",
"id": "object/d87e2842-ed93-9f5b-e8ea-3b4de31f4bc6",
"type": "arch",
"role": "module",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "bbac776322598fa37d35b7fdac0e995de1147681be473e3f084cd5493971d37d",
"body": "\n\n# Modul-07: Risk-Manager\n\n**Status:** ✅ **Freigegeben** (20.08.2026)\n**Version:** 0.1.0\n**Port (intern):** 55007\n**Netzwerk:** `trading-modules-trading-modules`\n\n---\n\n## Zweck\n\nDeterministischer Risk-Manager in der Trading-Kette. Konsumiert `SIGNAL_RANKED`\n(aus Modul-06), führt eine **harte, deterministische Risikoprüfung** durch, berechnet\ndie Positionsgröße und publiziert `RISK_APPROVED` oder `RISK_REJECTED`.\n\n**Prinzipien:**\n- **KEINE KI/ML** — rein deterministische Regeln.\n- **KEINE Broker-Order** — nur Risiko-Entscheidung + Positionsgröße.\n- **FAIL-CLOSED** — Fehler oder fehlende Daten führen **niemals** zu APPROVED,\n sondern immer zu REJECTED.\n\n---\n\n## Architektur\n\n```\nSIGNAL_RANKED (market.rankings / signal.ranked)\n │\n ▼\n┌─────────────────────────────────────────────┐\n│ Modul-07 Risk-Manager │\n│ Consumer (risk.input) │\n│ → RiskEngine (Registry) │\n│ → risk_v1 (deterministische Prüfung) │\n│ → Storage (risk_decision, idempotent) │\n│ → Publisher (market.risk) │\n└─────────────────────────────────────────────┘\n │\n ├── RISK_APPROVED (risk.approved)\n └── RISK_REJECTED (risk.rejected)\n```\n\n### Komponenten\n- `app/consumer/consumer.py` — RabbitMQ-Consumer `SIGNAL_RANKED`, manuelles ACK,\n Zombie-Schutz (`queue_delete` beim Start), Reconnect-Backoff 1s→2s→4s→8s.\n- `app/risk/engine.py` — Registry für Risiko-Verfahren (V1, zukünftig V2).\n- `app/risk/risk_v1.py` — deterministische Kernlogik + Positionsgrößen-Mathematik.\n- `app/instruments/registry.py` — instrument-agnostische Werte (Contract Size,\n Lot Size, Tick Size/Value, Min/Max Quantity, Quantity Step, Währung/FX).\n- `app/storage/storage.py` — idempotente Persistenz (unique auf `source_ranking_id`).\n- `app/publisher/publisher.py` — frische Verbindung je Publish + `conn.close()`\n im `finally` (RabbitMQ-Bug-Fix aus Modul-03/04/05/06).\n- `app/api/main.py` — FastAPI, Port 55007 intern, `/health`, `/health/ready`.\n\n---\n\n## V1-Risikoregeln (zentral konfigurierbar in `config.py`, env-overridable)\n\n| Regel | Default | Beschreibung |\n|---|---|---|\n| `account_equity` | 10000 | Account-Equity |\n| `risk_percent` | 0.005 | max. Risiko pro Trade (0,5 %) |\n| `min_score` | 70 | Mindest-Ranking-Score |\n| `min_ranking_class` | eligible | Mindest-Ranking-Klasse |\n| `max_positions` | 5 | max. Anzahl Positionen (nur bei vorhandenen Daten) |\n| `max_open_risk` | 0.02 | max. gesamtes offenes Risiko (nur bei vorhandenen Daten) |\n| `daily_loss_limit` | 0.03 | tägliches Verlustlimit (nur bei vorhandenen Daten) |\n| `max_drawdown` | 0.20 | maximales Drawdown-Limit (nur bei vorhandenen Daten) |\n\n> **Hinweis:** Globale Portfolio-Limits (offene Positionen, Tagesverlust, Drawdown)\n> werden **nur geprüft, soweit Daten vorhanden sind**. Die eigentliche\n> Portfolio-Logik gehört zu **Modul-08**. Fehlende Portfolio-Daten sind **kein**\n> REJECT-Grund.\n\n### Positionsgröße\n```\nrisk_amount = equity × risk_percent\nrisk_per_unit = abs(entry stop)\nraw_size = risk_amount / risk_per_unit\nquantity = floor(raw_size / quantity_step) × quantity_step # ABRUNDEN\n```\nAbrunden auf `quantity_step` (nie aufrunden), damit das erlaubte Risiko nie\nüberschritten wird (FAIL-CLOSED-Sicherheit).\n\n---\n\n## Output (RiskDecision)\n\nNachvollziehbar gespeichert in `risk_decision`:\n`risk_decision_id`, `source_ranking_id`, `symbol`, `asset_class`, `direction`,\n`entry`, `stop_loss`, `target`, `equity`, `risk_percent`, `max_risk_amount`,\n`calculated_position_size`, `actual_risk_amount`, `decision` (APPROVED/REJECTED),\n`reason_codes`, `rule_version`, `timestamp`.\n\n---\n\n## RabbitMQ\n\n| Rolle | Exchange | Routing Key | Queue |\n|---|---|---|---|\n| Consumer | `market.rankings` | `signal.ranked` | `risk.input` |\n| Publisher | `market.risk` | `risk.approved` | — |\n| Publisher | `market.risk` | `risk.rejected` | — |\n\n- Manuelles ACK erst nach erfolgreicher Verarbeitung.\n- Idempotenz: gleiches `source_ranking_id` → keine Doppelentscheidung/Event.\n- Reconnect ohne Zombies (Backoff 1s→2s→4s→8s, `queue_delete` beim Start).\n\n---\n\n## Tests\n\n### Unit-Tests (`test_risk.py`) — 10/10 grün\n1. gültiger LONG → APPROVED + korrekte Größe\n2. gültiger SHORT → APPROVED\n3. Score unter Threshold → REJECTED\n4. falscher Stop → REJECTED\n5. fehlende/ungültige Daten → REJECTED\n6. Positionsgröße/Risiko mathematisch korrekt\n7. Quantity-Step/Min/Max korrekt\n8. Daily-Loss/Drawdown-Limit → REJECTED\n9. Idempotenz\n10. RabbitMQ-Reconnect\n\n### E2E (03→04→05→06→07) — ALLE CHECKS BESTANDEN\n- **A:** Kette 03→07, Decision in DB (LONG & SHORT) ✓\n- **B:** exakt 1 Risiko-Event je Symbol (APPROVED/REJECTED) ✓\n- **C:** Pflichtfelder in Decision vorhanden ✓\n- **D:** identisches Ranking → kein Doppel-Decision ✓\n- **E:** Score<70 / falscher Stop / fehlende Daten → REJECTED + source_ranking_id ✓\n\n### Mathematik-Verifikation (aus DB)\n| Symbol | Direction | Entry | Stop | equity | risk% | max_risk | qty | actual_risk |\n|---|---|---|---|---|---|---|---|---|\n| M5LONG | LONG | 164.2 | 163.47636 | 10000 | 0.005 | 50 | 69 | 49.93 |\n| M5SHORT | SHORT | 135.8 | 136.49636 | 10000 | 0.005 | 50 | 71 | 49.44 |\n\nBeide `actual_risk_amount ≤ max_risk_amount` ✓\n\n### Reconnect-Test\nRabbitMQ kontrolliert neu gestartet → alle 4 Queues (`market-regime.input`,\n`strategy.input`, `ranking.input`, `risk.input`) mit **exakt 1 Consumer**,\nkeine Zombies, keine verlorenen Events, `risk_decision` stabil.\n\n---\n\n## Health/Readiness\n\n- `/health` → `{\"status\":\"ok\", \"postgresql\":true, \"rabbitmq\":true, \"consumer_ready\":true}`\n- `/health/ready` → 200\n- Container: `Modul-07-Risk-Manager` (healthy)\n\n---\n\n## Technischer offener Punkt: Modul-03 `/history?limit=N`\n\n**Modul-03 `/history?limit=N` liefert aktuell die N ältesten Candles (aufsteigend).**\nDas ist langfristig zu korrigieren (sollte die N **neuesten** Candles liefern).\nDer Workaround in Modul-06 (Lookback 500) ist **nicht als API-Vertrag** zu\nbehandeln — er ist ein temporärer Workaround, kein garantiertes Verhalten.\n\n---\n\n## Deployment\n\n- Compose: `/opt/trading-modules/docker-compose.yml` (Block `modul-07-risk-manager`)\n- Nur `expose: 55007` (kein öffentlicher Port)\n- `restart: unless-stopped`\n- Image: `risk-manager:0.1.0`\n"
},
{
"path": "notes/trading/system-docs/modul-08-portfolio-manager.md",
"title": "modul-08-portfolio-manager",
"id": "object/7ff050c3-ebaa-d0ae-5aa4-6e9da67d4773",
"type": "arch",
"role": "module",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "b5dc3696fee1e47826f424980ab4d743ff93631000dbe0ed70cc92674f02d865",
"body": "\n\n# Modul-08: Portfolio-Manager\n\n**Status: FREIGEGEBEN** (20.08.2026)\n\nDeterministischer Portfolio-Manager. Konsumiert `RISK_APPROVED` aus Modul-07,\nprüft das Gesamtportfolio gegen zentrale Limits und publiziert\n`TRADE_APPROVED` oder `PORTFOLIO_REJECTED`.\n\n**Keine KI/ML. Keine Broker-Order. FAIL-CLOSED.**\n\n---\n\n## 1. Rolle in der Kette\n\n```\nModul-03 Ingest → 04 Regime → 05 Signal → 06 Ranking\n → SIGNAL_RANKED (market.rankings/signal.ranked)\n → Modul-07 Risk-Check → RISK_APPROVED (market.risk/risk.approved)\n → Modul-08 Portfolio-Check → portfolio_decision-Tabelle\n → TRADE_APPROVED / PORTFOLIO_REJECTED (market.portfolio)\n```\n\nModul-08 prüft **NICHT erneut** die Positionsgrößenlogik aus Modul-07,\nsondern ausschließlich **Portfolio-Risiken**.\n\n---\n\n## 2. Events\n\n| Richtung | Exchange | Routing-Key | Event |\n|----------|----------|-------------|-------|\n| Input (Consumer) | `market.risk` | `risk.approved` | `RISK_APPROVED` |\n| Output (Publisher) | `market.portfolio` | `trade.approved` | `TRADE_APPROVED` |\n| Output (Publisher) | `market.portfolio` | `portfolio.rejected` | `PORTFOLIO_REJECTED` |\n\nConsumer-Queue: `portfolio.input` (durable, manuelles Ack, prefetch=1).\n\n---\n\n## 3. V1 Portfolio-Regeln (zentral konfigurierbar, env-overridable)\n\n| Regel | Config-Feld | Default |\n|-------|-------------|---------|\n| max. Anzahl offener Positionen | `max_positions` | 5 |\n| max. gesamtes Open Risk % | `max_open_risk_percent` | 0.05 (5%) |\n| max. Exposure pro Position % | `max_exposure_per_position_percent` | 0.20 |\n| max. Exposure pro Symbol % | `max_exposure_per_symbol_percent` | 0.30 |\n| max. Exposure pro Assetklasse % | `max_exposure_per_asset_class_percent` | 0.50 |\n| max. Long-Exposure % | `max_long_exposure_percent` | 0.60 |\n| max. Short-Exposure % | `max_short_exposure_percent` | 0.60 |\n| Duplikat Symbol/Strategie erlauben | `allow_duplicate_symbol_strategy` | false |\n| Korrelation anwenden | `apply_correlation` | false (vorbereitet) |\n| Account Equity | `account_equity` | 10000.0 |\n\nAlle Limits sind Prozentwerte des `account_equity`. Bei `account_equity <= 0`\n→ FAIL-CLOSED `PORTFOLIO_REJECTED`.\n\n**Später geplant** (V2): Sector/Country/Currency-Exposure, Korrelation\n(erst wenn verlässliche Daten vorhanden).\n\n---\n\n## 4. FAIL-CLOSED\n\nJede Exception, fehlende oder ungültige kritische Eingabe führt zu\n`PORTFOLIO_REJECTED`, **niemals** zu `TRADE_APPROVED`. Fehlende kritische\nFelder (quantity, risk, entry) → `PORTFOLIO_REJECTED` mit Reason-Codes.\n\n---\n\n## 5. Portfoliozustand (autoritative Quelle)\n\nDer Portfoliozustand wird **NICHT nur aus Events angenommen**. Die\nArchitektur trennt die Quelle des Zustands vom Entscheidungs-Code:\n\n- `app/portfolio/state.py` definiert `PortfolioStateProvider` (Interface)\n und `PortfolioState` / `OpenPosition`.\n- V1-Provider liest offene Positionen aus der eigenen `portfolio_decision`-\n Tabelle (alle `TRADE_APPROVED`-Entscheidungen).\n- **Später** kann ein Broker-/Execution-Provider als autoritative Quelle\n ergänzt werden, ohne den Entscheidungs-Code zu ändern.\n\n---\n\n## 6. Race-Condition-Schutz (atomar)\n\nZwei nahezu gleichzeitige `RISK_APPROVED` dürfen **nicht** beide auf demselben\nalten Portfoliozustand genehmigt werden und dadurch Limits überschreiten.\n\nLösung (`app/storage/storage.py` → `evaluate_and_save_atomic`):\n1. **Advisory Lock** (`pg_advisory_xact_lock`) serialisiert parallele\n Portfolio-Entscheidungen.\n2. Innerhalb **einer Transaktion**: Zustand lesen → Engine bewerten →\n speichern → Commit.\n3. Der zweite Prozess wartet auf den Lock und bewertet auf dem **aktualisierten**\n Zustand (inkl. der ersten Entscheidung).\n\n---\n\n## 7. Idempotenz\n\n- Unique-Index `uq_portfolio_decision_src` auf `source_risk_decision_id`\n (partial, nur wo nicht NULL).\n- Gleiches `source_risk_decision_id` → keine Doppelentscheidung/Event.\n- Zusätzlich prüft der Service vor der Verarbeitung, ob die Quelle bereits\n entschieden wurde (Skip + Ack).\n\n---\n\n## 8. DB-Tabelle `portfolio_decision`\n\nJede Entscheidung wird gespeichert — **auch `PORTFOLIO_REJECTED`**.\n\n| Spalte | Typ | Beschreibung |\n|--------|-----|--------------|\n| `id` | BIGSERIAL PK | |\n| `portfolio_decision_id` | TEXT | eigene Entscheidungs-ID |\n| `source_risk_decision_id` | TEXT | id des RISK_APPROVED (Modul-07) |\n| `source_ranking_id` | TEXT | durchgereichtes Ranking (Modul-06) |\n| `source_signal_id` | TEXT | durchgereichtes Signal (Modul-05) |\n| `source_event_id` | TEXT | |\n| `correlation_id` | TEXT | |\n| `timestamp` | TIMESTAMPTZ | Entscheidungszeitpunkt (UTC) |\n| `symbol` | TEXT | |\n| `asset_class` | TEXT | |\n| `strategy` | TEXT | |\n| `direction` | TEXT | LONG / SHORT |\n| `timeframe` | TEXT | |\n| `proposed_quantity` | DOUBLE | aus RISK_APPROVED |\n| `proposed_risk` | DOUBLE | actual_risk_amount |\n| `proposed_exposure` | DOUBLE | qty × entry |\n| `current_portfolio_risk` | DOUBLE | offenes Risiko VOR |\n| `new_portfolio_risk` | DOUBLE | offenes Risiko NACH |\n| `exposure_before` | DOUBLE | Gesamt-Exposure VOR |\n| `exposure_after` | DOUBLE | Gesamt-Exposure NACH |\n| `decision` | TEXT | TRADE_APPROVED / PORTFOLIO_REJECTED |\n| `reason_codes` | TEXT[] | z. B. `{OK}` oder `{EXPOSURE_PER_POSITION_EXCEEDED,...}` |\n| `rule_version` | TEXT | `portfolio_v1@1.0.0` |\n| `details` | JSONB | Begründung + Zwischenwerte |\n| `portfolio_snapshot` | JSONB | Portfoliozustand zum Zeitpunkt |\n| `created_at` | TIMESTAMPTZ | |\n\n---\n\n## 9. Reason-Codes\n\n| Code | Bedeutung |\n|------|-----------|\n| `OK` | alle Limits eingehalten → TRADE_APPROVED |\n| `MAX_POSITIONS_EXCEEDED` | max. offene Positionen erreicht |\n| `OPEN_RISK_EXCEEDED` | gesamtes Open Risk überschritten |\n| `EXPOSURE_PER_POSITION_EXCEEDED` | Exposure pro Position überschritten |\n| `EXPOSURE_PER_SYMBOL_EXCEEDED` | Exposure pro Symbol überschritten |\n| `EXPOSURE_PER_ASSET_CLASS_EXCEEDED` | Exposure pro Assetklasse überschritten |\n| `LONG_EXPOSURE_EXCEEDED` | Long-Exposure überschritten |\n| `SHORT_EXPOSURE_EXCEEDED` | Short-Exposure überschritten |\n| `DUPLICATE_SYMBOL_STRATEGY` | doppelte Position Symbol/Strategie |\n| `QUANTITY_MISSING` / `RISK_AMOUNT_MISSING` / `ENTRY_MISSING` | fehlende kritische Daten (FAIL-CLOSED) |\n| `EQUITY_MISSING` | account_equity fehlt/ungültig (FAIL-CLOSED) |\n\n---\n\n## 10. Architektur\n\n```\napp/\n config.py # zentrale Konfiguration (env-overridable)\n core/\n models.py # PortfolioDecision + Event-Modelle\n service.py # Orchestrator (Consumer→Engine→Storage→Publisher)\n portfolio/\n engine.py # Registry der Portfolio-Verfahren\n portfolio_v1.py # deterministische V1-Prüfung (FAIL-CLOSED)\n state.py # PortfolioState-Provider (autoritative Quelle)\n storage/\n storage.py # idempotent + atomare Race-Condition-Lösung\n publisher/\n publisher.py # frische Verbindung je Publish + close\n consumer/\n consumer.py # manuelles Ack, Reconnect-Backoff, Zombie-Schutz\n api/\n main.py # FastAPI (Health/Readiness/Decisions/State)\nmigrations/\n 001_portfolio_decision.sql\nDockerfile\nrequirements.txt\ntest_portfolio.py # 11 Unit-Tests (Engine-Logik)\n```\n\n---\n\n## 11. Tests\n\n### Unit-Tests (`test_portfolio.py`, 11/11 grün)\n1. erstes gültiges Portfolio-Trade → TRADE_APPROVED\n2. max Positions überschritten → PORTFOLIO_REJECTED\n3. max Open Risk überschritten → PORTFOLIO_REJECTED\n4. Symbol-Exposure überschritten → PORTFOLIO_REJECTED\n5. Long/Short-Exposure-Limit → PORTFOLIO_REJECTED\n6. Duplicate Symbol/Strategie → PORTFOLIO_REJECTED\n7. fehlender Portfoliozustand → FAIL-CLOSED\n8. Idempotenz (deterministisch)\n9. Assetklassen-Exposure → PORTFOLIO_REJECTED\n10. Exposure pro Position → PORTFOLIO_REJECTED\n11. Short-Exposure → PORTFOLIO_REJECTED\n\n### E2E (Kette 03→04→05→06→07→08, alle Checks grün)\n- **A**: Kette 03→08, Portfolio-Decision in DB (LONG & SHORT)\n- **B**: exakt 1 Portfolio-Event je Symbol (TRADE_APPROVED/PORTFOLIO_REJECTED)\n- **C**: Pflichtfelder in Portfolio-Decision vorhanden\n- **D**: Idempotenz (identisches RISK_APPROVED → kein Doppel-Decision)\n- **E**: parallele RISK_APPROVED → beide verarbeitet, keine Race-Verlust\n- **F**: erstes gültiges Portfolio-Trade → TRADE_APPROVED\n\n### Reconnect-Test\nRabbitMQ gestoppt → Consumer erkennt Ausfall, Reconnect-Backoff (1s→2s→4s→8s).\nRabbitMQ gestartet → Consumer verbindet sich neu, alle 5 Queues haben exakt\n1 Consumer, Health/Readiness 200.\n\n---\n\n## 12. Health / Readiness\n\n- `GET /health` → Liveness (immer 200, Komponentenstatus im Body)\n- `GET /health/ready` → Readiness (200 nur wenn PG + RabbitMQ erreichbar)\n- `GET /decisions/{symbol}` → gespeicherte Entscheidungen (inkl. REJECTED)\n- `GET /portfolio-rules` → verfügbare Portfolio-Regeln\n- `GET /portfolio/state` → aktueller Portfoliozustand\n\nPort: **55008** (nur Docker-intern, kein öffentlicher Host-Port).\n\n---\n\n## 13. Deploy\n\n- Compose-Service: `modul-08-portfolio-manager`\n- Image: `portfolio-manager:0.1.0`\n- Container: `Modul-08-Portfolio-Manager`\n- Netz: `trading-modules`\n- `restart: unless-stopped`\n- Env: PG + RabbitMQ-Zugangsdaten (aus Compose)\n\n---\n\n## 14. Wichtige Fixes (20.08.2026)\n\n**Consumer-Bug (alle Module 0408):** `process_data_events(time_limit=None)`\nverarbeitete nur EIN Event und baute danach die Verbindung neu auf. Der\nerneute `_connect()` führte `queue_delete` aus und löschte wartende Events\n(z. B. ein zweites, nahezu gleichzeitiges RISK_APPROVED) unwiederbringlich.\nFix: innere Schleife `while not stop: process_data_events(time_limit=1.0)`\nhält die Verbindung am Leben. Betroffen und gefixt: Modul-04, 05, 06, 07, 08.\n"
},
{
"path": "notes/trading/system-docs/modul-09-execution-service.md",
"title": "modul-09-execution-service",
"id": "object/ffa8db13-8aa6-d4a4-6756-69eabd0c8d77",
"type": "arch",
"role": "module",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "fcaacfac3ab6ce530f67166034ea6d67713239a3a2244f58e42de49f0118cc5f",
"body": "\n\n# Modul-09: Execution-Service\n\n**Status: FREIGEGEBEN** (20.08.2026)\n\nDeterministischer Execution-Service. Konsumiert `TRADE_APPROVED` aus Modul-08,\nvalidiert die Order hart (FAIL-CLOSED), übergibt sie an einen Broker-Adapter\n(V1: PaperBrokerAdapter) und überwacht den Status. Ergebnis wird idempotent\nin eigenen DB-Tabellen gespeichert und als RabbitMQ-Events publiziert.\n\n**Sicherheit: KEIN unkontrolliertes Live-Trading.**\n- Default `EXECUTION_MODE=PAPER` (bzw. DRY_RUN)\n- `TRADING_ENABLED=false` als sicherer Default für LIVE\n- LIVE nur über explizite Konfiguration aktivierbar, FAIL-CLOSED\n- Bei Timeout nach Order-Senden NIE blind erneut senden — erst über\n `broker_order_id`/`client_order_id` bzw. Brokerstatus klären\n\n## Architektur\n\n```\nTRADE_APPROVED (market.portfolio/trade.approved, Modul-08)\n │\n ▼\nExecutionConsumer (execution.input, Consumer-Fix von Anfang an)\n │\n ▼\nOrder-Validierung (hart, FAIL-CLOSED)\n │ source_portfolio_decision_id gültig, decision=APPROVED,\n │ symbol/direction/quantity vorhanden, quantity>0,\n │ entry/stop/target plausibel, Kette nachvollziehbar,\n │ kein bereits ausgeführter identischer Auftrag, Kill-Switch\n ▼\nBrokerAdapter-Interface (modular, austauschbar)\n │ V1: PaperBrokerAdapter (realistischer Lifecycle)\n ▼\nExecutionStorage (idempotent, Advisory Lock + Transaktion)\n │ execution_order + execution_event\n ▼\nRabbitMQPublisher (market.execution)\n ORDER_SUBMITTED / ORDER_FILLED / ORDER_REJECTED / ORDER_FAILED\n```\n\n## BrokerAdapter-Interface\n\n- `BrokerAdapter` (app/broker/base.py): `name()`, `is_configured()`,\n `submit_order()`, `get_order_status()`\n- `PaperBrokerAdapter` (app/broker/paper.py): V1, simuliert realistischen\n Lifecycle (SUBMITTED → FILLED, Partial Fill, Reject, Timeout) für Tests\n- `IgDemoAdapter` (app/broker/ig_demo.py): IG-Markets-Demo-Broker (IG_DEMO-Modus),\n echter externer Broker-Adapter. Liest/schreibt über IG REST API (demo-api.ig.com).\n- `BrokerRegistry` (app/broker/registry.py): wählt Adapter anhand\n `DEFAULT_BROKER` (V1: paper). Echte Broker-Adapter später austauschbar,\n keine Brokerlogik im Core-Code.\n\n## IG-Demo-Adapter (IG_DEMO-Modus, 21.08.2026)\n\n**Status: FREIGEGEBEN / PRODUKTIV VERIFIZIERT** — erster echter externer Broker-Adapter.\n\n- **Eigener Modus `IG_DEMO`** (nicht LIVE+Gate); PAPER/IG_DEMO/LIVE strikt getrennt.\n- **Demo-Basis-URL erzwungen** (`demo-api.ig.com`), LIVE-Endpunkt (`api.ig.com`)\n technisch blockiert, kein IG_DEMO→LIVE-Autowechsel.\n- **Credentials ausschließlich VPS-Secret/ENV** (`.env.ig`, chmod 600/root-only);\n nie in Code/Forgejo/Tolaria/DB/Logs.\n- **Mengen instrumentabhängig** über `broker_mapping` (PostgreSQL); kein globales 1:1.\n- **Epic-Mapping produktiv in PostgreSQL `broker_mapping`**; Forgejo nur Schema/Doku/Beispiele.\n\n### Order-Aktions-Auswertung (CLOSE-Pfad-Fix, 21.08.2026)\n`BrokerOrderRequest` trägt `action_type` (Default `OPEN`) + `broker_order_id`.\nDer Service reicht beides durch; der Adapter wertet `action_type` aus:\n\n| action_type | IG-Pfad |\n|-------------|---------|\n| `OPEN` / `INCREASE` | `POST /positions/otc` (create_position) |\n| `CLOSE` / `REDUCE` | `POST /positions/otc` mit `_method:DELETE` (close_position) |\n| unbekannt/ungültig | **FAIL-CLOSED** (`IG_UNSUPPORTED_ACTION`) |\n\n**CLOSE-Direction korrekt umkehren:** CLOSE einer LONG/BUY-Position muss mit\n**SELL** erfolgen (Gegenseite), nicht mit der Positionsrichtung. `_close_direction()`:\nLONG→SELL, SHORT→BUY.\n\n**broker_order_id/dealId sauber durchreichen:** `broker_order_id` wird vom Service\nin die Order übernommen und an den Adapter durchgereicht; IG `dealId` wird als\n`broker_order_id` persistiert.\n\n**Unbekannte/ungültige Aktionen FAIL-CLOSED:** `_derive_action_type()` setzt\nunbekannte Aktionen NICHT mehr auf OPEN zurück, sondern reicht sie durch →\nControl blockt mit `UNKNOWN_ACTION_TYPE`, Adapter mit `IG_UNSUPPORTED_ACTION`.\n\n### IG-Adapter-Tests (6/6 grün)\n1. `test_unsupported_action_fail_closed` — unbekannte Aktion → FAIL-CLOSED\n2. `test_close_direction_inverted` — CLOSE einer BUY-Position → SELL\n3. `test_open_uses_create_position` — OPEN → create_position\n4. `test_close_uses_close_position` — CLOSE → close_position mit `_method:DELETE`\n5. `test_broker_order_id_passthrough` — broker_order_id/dealId durchgereicht\n6. `test_fail_closed_without_credentials` — ohne Credentials → nicht konfiguriert\n\n### Erster erfolgreicher IG-DEMO OPEN→CLOSE-E2E (21.08.2026)\n- **OPEN** `IGE2E-US500-OPEN-1787292734` → dealId `DIAAAAYB46KMNAE`, size 1.0, BUY,\n openLevel 7652.58 → **FILLED**\n- **CLOSE-Bug gefunden + gefixt:** erster CLOSE sendete fälschlich als zweite OPEN\n (action_type ignoriert) + falsche direction (BUY statt SELL)\n- **CLOSE2** `IGE2E-US500-CLOSE2-1787293768` für `DIAAAAYB463S4AB` → **FILLED**\n (filled_quantity 1, avg_fill_price 7652.19)\n- **IG GET /positions: Anzahl 0** — alle Positionen geschlossen\n- **Safety-Reset:** M09 zurück auf PAPER (`EXECUTION_MODE=PAPER`,\n `TRADING_ENABLED=false`, `DEFAULT_BROKER=paper`)\n\n## Idempotenz\n\n- Unique-Index `uq_execution_order_src` auf `source_portfolio_decision_id`\n → gleiches TRADE_APPROVED erzeugt NIE eine Doppelorder\n- Unique-Index `uq_execution_order_client` auf `client_order_id`\n- Client Order ID deterministisch aus Execution-ID abgeleitet\n (`EXEC-<execution_id[:32]>`)\n- Advisory Lock + Transaktion: parallele identische Events → exakt eine\n Execution (Race-Condition-Schutz, wie Modul-08)\n- ACK eines TRADE_APPROVED erst, wenn die Order dauerhaft in der DB gespeichert ist\n\n## Order-Status\n\n`PENDING → SUBMITTED → PARTIALLY_FILLED → FILLED`\nsowie `REJECTED`, `CANCELLED`, `FAILED`.\n\n## DB-Tabellen\n\n- `execution_order`: execution_id, source_portfolio_decision_id, broker, mode,\n symbol, asset_class, direction, quantity, requested_price, stop_loss, target,\n broker_order_id, client_order_id, status, filled_quantity, avg_fill_price,\n timestamps, error/reason_codes, execution_version\n- `execution_event`: append-only Log der Status-Übergänge\n\n## RabbitMQ\n\n- Exchange `market.execution` (topic, durable)\n- Routing: `order.submitted`, `order.filled`, `order.rejected`, `order.failed`\n- Queue `execution.input` (durable), gebunden an `market.portfolio`/`trade.approved`\n- Consumer-Fix aus Modul-0408 von Anfang an: dauerhafte Verbindung,\n `process_data_events(time_limit=1.0)` in innerer Schleife, KEIN queue_delete\n bei normalen Reconnects, Backoff 1s→2s→4s→8s→16s→30s\n\n## Tests\n\n- **31/31 Unit-Tests** (test_execution.py + test_ig_demo.py + test_control.py):\n gültige Order → PAPER FILLED, Idempotenz, ungültige Quantity → REJECTED,\n Kill-Switch, fehlende Broker-Credentials → FAIL-CLOSED, Broker-Reject,\n Timeout → keine blinde Doppelorder, Partial Fill, Reconnect-Backoff,\n parallele identische Events, Control-Gate (M15), IG-Adapter (6/6)\n- **E2E 6/6 Checks** (Kette 03→04→05→06→07→08→09): Execution-Order in DB,\n exakt 1 Paper-Order, Pflichtfelder, Idempotenz, ORDER_SUBMITTED+ORDER_FILLED,\n gültiger Trade → PAPER FILLED\n- **Reconnect-Test**: RabbitMQ gestoppt → Backoff → neu verbunden, 1 Consumer,\n kein queue_delete bei normalen Reconnects\n\n## Deploy\n\n- Container `Modul-09-Execution-Service`, Port 55009 nur Docker-intern\n (kein öffentlicher Host-Port)\n- Health/Readiness 200, PG + RabbitMQ erreichbar, Consumer bereit\n- Env: `EXECUTION_MODE=PAPER`, `TRADING_ENABLED=false`, `DEFAULT_BROKER=paper`\n\n## Geändert\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-09-Doku um final freigegebenen Control-Gate-Status (M15→M09, Variante C) ergänzt.\n Kernregel, Einbau-Punkt, Audit-Felder, Parameter, Live-E2E-Ergebnisse dokumentiert.\n```\n\n## Control-Gate (M15→M09, FREIGEGEBEN 20.08.2026)\n\n**Status: FREIGEGEBEN / PRODUKTIV VERIFIZIERT** — Variante C (Event-Cache-Vorfilter + synchroner HTTP-Final-Check).\n\nM15 (`Modul-15-Monitoring-Control`) ist die **autoritative Safety-/Trading-Control-Instanz**.\nM09 sendet neue Orders (OPEN/INCREASE) **nur**, wenn M15 eindeutig `ENABLED` + frisch bestätigt.\n\n### Kernregel\n| State | OPEN/INCREASE | REDUCE/CLOSE/CANCEL |\n|-------|---------------|---------------------|\n| `ENABLED` | **ERLAUBT** | erlaubt |\n| `PAUSED` | **VERBOTEN** | erlaubt |\n| `HALTED` | **VERBOTEN** | erlaubt |\n| `UNKNOWN` / M15-down / stale | **VERBOTEN (FAIL-CLOSED)** | erlaubt |\n\n### Einbau-Punkt\nControl-Check zwischen Schritt 4 (Order atomar in DB anlegen) und Schritt 5 (`_submit_and_track`).\nBei OPEN/INCREASE ohne Erlaubnis → Order als `FAILED`/`REJECTED` speichern, **nicht** senden.\n`TRADING_ENABLED`-Flag bleibt unverändert (blockiert nur LIVE, nicht PAPER).\n\n### Audit-Felder je Order\n`action_type`, `control_status`, `control_state_version`, `control_expires_at`,\n`control_audit_id`, `control_check_source` (`http`|`cache`|`bypass`|`none`).\n\n### Parameter\n- Final-Check-HTTP-Timeout: **500 ms**, Retry: **max 1** sofort.\n- `expires_at`-TTL (M15): **500 ms**; Cache-TTL (M09): **5 s**.\n- Kein Auto-Resume; kein \"Senden ohne Check\".\n\n### Live-E2E (VPS, 20.08.2026) — alle Szenarien grün\nS1 ENABLED 9/9 · S2 HALTED 9/9 · S3 PAUSED 4/4 · S4 Risikoabbau 4/4 · S5 M15-down 7/7 ·\nS6 Cache-vs-Final 2/2 · S7 Race/TTL 2/2 · S8 Event-Cache verifiziert · S9 Recovery 3/3 ·\nS10 Health/Security verifiziert · S11 Regression bestanden.\n\nDetails: siehe `modul-15-m09-anbindung-implementierung.md` (FREIGEGEBEN).\n"
},
{
"path": "notes/trading/system-docs/modul-10-trade-journal.md",
"title": "modul-10-trade-journal",
"id": "object/e2beb080-49cc-0b05-64fb-291440fe9abb",
"type": "journal",
"role": "module",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "ba4f47aa6f72cc0f0cde5242ba95554997ff7c58d966571dc90f7a8d20fb0758",
"body": "\n\n# Modul-10: Trade-Journal\n\n**Status: FREIGEGEBEN** (20.08.2026)\n\nDeterministisches, append-only Trade-Journal. Konsumiert die\nExecution-Events (`ORDER_SUBMITTED`/`ORDER_FILLED`/`ORDER_REJECTED`/`ORDER_FAILED`)\naus Modul-09, validiert die Herkunftskette hart (FAIL-CLOSED) und dokumentiert\njeden Trade in einer Audit-Trail-konformen, idempotenten Struktur.\nErgebnis wird als `TRADE_RECORDED`-Event publiziert.\n\n**Sicherheit & Audit:**\n- **Keine KI/ML, keine Orderausführung, keine Risikoberechnung** — reines Dokumentieren\n- **Append-only**: `trade_event_history` ist unveränderlich (nur INSERT, kein UPDATE/DELETE)\n- **Herkunftskette wird validiert** (Signal → Ranking → Risk → Portfolio → Execution)\n- Bei inkonsistenter Kette → Trade als `INCONSISTENT` markiert (FAIL-CLOSED), nicht stumm verworfen\n- `TRADE_RECORDED` optional: ACK nach persistenter Speicherung\n\n## Architektur\n\n```\nORDER_* (market.execution, Modul-09)\n │\n ▼\nJournalConsumer (journal.input, Consumer-Fix von Anfang an)\n │\n ▼\nHerkunfts-Validierung (FAIL-CLOSED)\n │ Kette 05→06→07→08→09 nachvollziehbar?\n │ provenance_consistent in metadata\n ▼\nJournalService\n │ erstes Event → Trade anlegen + TRADE_RECORDED\n │ Folge-Event → append-only Event + Status-Update (bestehenden Trade)\n ▼\nJournalStorage (idempotent, Unique-Index)\n │ trade_journal + trade_event_history\n ▼\nRabbitMQPublisher (market.journal)\n TRADE_RECORDED (trade.recorded)\n```\n\n## Service-/Storage-Grenze (vereinheitlicht)\n\n**Storage gibt IMMER `TradeJournal` zurück** — nie ein `dict`.\n- Einzige Konvertierungsstelle `_row_to_trade` (DB-Zeile → Modell)\n- `get_trade_by_execution()` → `Optional[TradeJournal]`\n- Der Service arbeitet ausschließlich mit Modellen (kein dict/Model-Mix je Codepfad)\n- Dadurch ist `trade_id` auf jedem Rückgabewert garantiert verfügbar\n- (Fix gegen `'dict' object has no attribute 'trade_id'` → NACK/Requeue)\n\n## Verarbeitung\n\n- **Erstes Event (ORDER_SUBMITTED)** → Trade anlegen, `TRADE_RECORDED` publizieren\n- **Folge-Event (ORDER_FILLED u.a.)** → bestehenden Trade als Status-Update\n verarbeiten: append-only Event + Status-Update, KEIN neuer Trade,\n **kein zweites TRADE_RECORDED**\n- Bei FILLED: `entry_filled`/`filled_at`/Status aktualisiert (Trade öffnen/aktualisieren)\n- Eingehende Events werden nicht doppelt verarbeitet (idempotent, Unique-Index)\n\n## Idempotenz\n\n- Unique-Index auf `(execution_id, event_id)` → jedes Event exakt einmal\n- `provenance_consistent` in `metadata` (jsonb), nicht als Spalte\n- `TRADE_RECORDED` nur beim ersten Event pro Trade\n- Kein Doppel-Journal bei Re-Publish desselben Events\n\n## DB-Tabellen\n\n- `trade_journal`: trade_id, execution_id, source_portfolio_decision_id,\n Herkunftsketten-IDs (signal/ranking/risk/portfolio), symbol, asset_class,\n strategy, direction, timeframe, entry_requested, entry_filled, stop_loss,\n target, quantity, risk_amount, execution_mode, broker_order_id, status,\n provenance_consistent (in metadata), created_at, updated_at\n- `trade_event_history`: execution_id, event_id, event_type, payload, created_at\n (append-only, unveränderlich)\n\n## RabbitMQ\n\n- Exchange `market.journal` (topic, durable)\n- Routing: `trade.recorded`\n- Queue `journal.input` (durable), gebunden an `market.execution`/`order.*`\n- Consumer-Fix von Anfang an: dauerhafte Verbindung,\n `process_data_events(time_limit=1.0)` in innerer Schleife, KEIN queue_delete\n bei normalen Reconnects, Backoff 1s→2s→4s→8s→16s→30s\n- ACK erst nach persistenter Speicherung; bei Fehler NACK/Requeue\n\n## Tests\n\n- **39/39 Unit-Tests** (test_journal.py): Anlage, Status-Update, Idempotenz,\n FAIL-CLOSED-Herkunftskette, append-only, exakt 1 TRADE_RECORDED, Reconnect\n- **Gezielter Fix-Test 9/9** (test_fix_update.py): SUBMITTED→Journal,\n FILLED→Update, Rückgabe ist TradeJournal (kein dict), `trade_id` vorhanden,\n genau 1 Trade, append-only (2 Events), idempotent, kein Doppel-Journal\n- **E2E 7/7 Checks** (Kette 03→10): Journal-Eintrag in DB, exakt 1 Trade,\n Pflichtfelder + Herkunftskette konsistent, append-only Event-Historie\n (SUBMITTED+FILLED), exakt 1 TRADE_RECORDED, Idempotenz nach Re-Publish\n- **Reconnect-Test**: RabbitMQ gestoppt → Backoff 1→2→4→8→16s → wieder\n verbunden, `journal.input` 1 Consumer, 0 Messages, kein Zombie\n- **Log-Check**: 0 ERROR, 0 Traceback, ACK auf ORDER_SUBMITTED + ORDER_FILLED\n\n## Deploy\n\n- Container `Modul-10-Trade-Journal`, Port 55010 nur Docker-intern\n (kein öffentlicher Host-Port)\n- Health/Readiness 200, PG + RabbitMQ erreichbar, Consumer bereit\n- Env: `EXECUTION_MODE=PAPER`, `TRADING_ENABLED=false`\n"
},
{
"path": "notes/trading/system-docs/modul-11-analytics.md",
"title": "modul-11-analytics",
"id": "object/c86dba29-835c-6b6a-d4f9-ac7cec69b8c4",
"type": "arch",
"role": "module",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "55751a8bc1215a878cb27f9801040ab595acf7b763d8d3c16978c47beb5ffb92",
"body": "\n\n# Modul-11: Analytics\n\n**Status: FREIGEGEBEN** · Container `Modul-11-Analytics` · Image `analytics-service:0.1.0`\n\n## Zweck\nAuswertung 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.**\n\n## Architektur / Datenfluss\n\n```\nTRADE_RECORDED (market.journal / trade.recorded)\n │\n ▼\nModul-11-Analytics ── konsumiert über Queue `analytics.input`\n │\n ├── liest read-only → trade_journal (Modul-10, PostgreSQL)\n ├── schreibt → analytics_snapshot, strategy_performance, daily_performance\n └── publiziert (opt.)→ ANALYTICS_UPDATED (market.analytics / analytics.updated)\n\nAPI (Docker-intern, Port 55011):\n /health, /health/ready\n /analytics/portfolio Gesamt-/Portfolio-Performance\n /analytics/strategy je Strategie+Version\n /analytics/regime je Market Regime\n /analytics/period je Monat (ab optionalem from_date)\n /analytics/rebuild (POST) deterministischer Rebuild aus DB\n```\n\nPrimä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).\n\n## Datenquelle & Realisationslogik\n- **Nur `status = 'CLOSED'` UND `realized_pnl IS NOT NULL`** fließen in realisierte Performance ein.\n- **Offene Trades** (`OPEN`) werden separat ausgewiesen (`open_trade_count`) und zählen NICHT als realisierte Trades.\n- Gruppierungsfelder aus `trade_journal`: `strategy`, `strategy_version`, `symbol`, `asset_class`, `direction` (LONG/SHORT), `regime`, `signal_score`.\n- Zeitraum-Gruppierung nach `exit_time` (realisierter Trades), optional ab `from_date`.\n- **Keine Lookahead-/Survivorship-Tricks**: nur tatsächlich geschlossene Trades, deterministische Equity-Kurve.\n\n## Kennzahlen (V1)\n- Anzahl Trades (realisiert + offen getrennt)\n- Winrate / Lossrate\n- durchschnittlicher Gewinn / Verlust\n- durchschnittliches R (realized_r_multiple)\n- Expectancy (Währung + R)\n- Profit Factor (gross_profit / gross_loss)\n- Netto-PnL\n- Brutto-Gewinn / Brutto-Verlust\n- Max Drawdown (aus Equity-Kurve der realisierten PnL)\n- Durchschnittliche Haltedauer (entry_time→exit_time, Stunden)\n- Bester / schlechtester Trade\n\n## Eigene Tabellen (Migration `migrations/001_analytics.sql`)\n- `analytics_snapshot` — stabiler Snapshot der Gesamt-Performance (jeder Rebuild = neue Zeile, chronologisch append).\n- `strategy_performance` — Kennzahlen je Strategie+Version.\n- `daily_performance` — Tageskennzahlen (Backtesting/Reporting).\n\nDiese fassen KEINE bestehenden Tabellen an (read-only auf `trade_journal`).\n\n## RabbitMQ\n- **Queue** `analytics.input` (durable, topic `market.journal`, Routing `trade.recorded`), manuelles ACK, Reconnect mit Backoff (1→2→4→8→16s), kein Zombie.\n- **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.\n\n## Verifikation (20.08.2026)\n| Test | Ergebnis |\n|---|---|\n| 17 Pure Metriken (Gewinn/Verlust, Winrate, PF, Expectancy, Max DD, Gruppierung, offene≠realisierte) | **OK** |\n| 8 Idempotenz (identisches Event → keine Doppelzählung) | **OK** |\n| 9 Rebuild aus DB → identische Kennzahlen | **OK** |\n| 10 RabbitMQ-Reconnect (Backoff 4→8→16s, erneuter Connect, 1 Consumer) | **OK** |\n| E2E 03→11 (Seed realisierter Trades, Portfolio/Strategie/Regime, DB + Idempotenz) | **OK** |\n\n- Health/Ready: `200` Port 55011 (PostgreSQL ✓, RabbitMQ ✓, Consumer ready ✓).\n- Endzustand nach Freigabe: 2 echte OPEN-Trades im Journal, 0 realisierte → Snapshot zeigt 0/2.\n\n## Betriebshinweise\n- Kein öffentlicher Port (nur `expose: 55011`).\n- `restart: unless-stopped`.\n- Compose-Pfad VPS: `/opt/trading-modules/docker-compose.yml`.\n- Forgejo-Doku: `nexo312/trading-system-docs`, Commit-ID siehe unten.\n\n---\n*Geändert von: Rain Ocampo (Hermes)*\n*Datum: 20.08.2026* — Modul-11-Analytics, Status FREIGABEGEBEN.\n"
},
{
"path": "notes/trading/system-docs/modul-12-backtesting.md",
"title": "modul-12-backtesting",
"id": "object/47b8029c-5874-e73f-0279-d335785b359a",
"type": "arch",
"role": "module",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "3c2fd0d8a45350b8baea60f24b6c81fe95955f1ace37775919461309ec542aea",
"body": "# Modul-12: Backtesting\n\n**Status: FREIGEGEBEN** · Container `Modul-12-Backtesting` · Image `backtesting:0.1.2` (V1 + V1.1 parallel)\n\n## Zweck\nDeterministische, reproduzierbare **historische Strategietests** auf echten\nOHLCV-Daten aus **Modul-03** (PostgreSQL). Gleiche Strategie-Logik wie\n**Modul-05** (`trend_pullback_v1`) und identische Regime-Engine wie **Modul-04** —\nder Backtest entscheidet exakt so wie Live/Paper. **V1 ohne KI/ML, ohne Live-Orders,\nohne Broker-Ausführung.** Modular erweiterbar für weitere Strategien (Breakout, ORB,\nMean-Reversion) über gemeinsame `Strategy`-Schnittstelle + Registry.\n\n## Architektur / Datenfluss\n\n```\nOHLCV (Modul-01 PostgreSQL, Tabelle ohlcv)\n │ synchron, direkt nach Timestamp/Zeitraum (KEIN /history?limit)\n ▼\nModul-12-Backtesting ── POST /backtest\n │\n ├── core/engine.py deterministische Backtest-Loop (kein Lookahead)\n ├── strategies/ trend_pullback_v1 (1:1 zu Modul-05)\n ├── regime/engine.py Regime-Engine (1:1 zu Modul-04)\n ├── storage/storage.py Persistenz backtest_run/trade/equity\n └── api/main.py REST-API (intern, Port 55012)\n\nAPI (Docker-intern, Port 55012):\n /health, /health/ready\n POST /backtest Backtest starten (synchron, returns Ergebnis)\n GET /backtest/{run_id} Run-Metadaten\n GET /backtest/{run_id}/status Status + run_hash/data_hash\n GET /backtest/{run_id}/trades gespeicherte Trades\n GET /backtest/{run_id}/equity Equity-Kurve\n GET /backtest alle Runs\n POST /backtest/{run_id}/rebuild (idempotenter Rebuild aus DB)\n GET /strategies registrierte Strategien + Versionen\n```\n\nKein RabbitMQ: Backtest ist ein synchroner, request/response-Aufruf. Port `55012`\nist **nur intern** (`expose:`), kein öffentliches Port-Mapping.\n\n## Datenquelle\n- OHLCV direkt aus PostgreSQL (`ohlcv`), Spalten `provider, symbol, timeframe, ts,\n open, high, low, close, volume`.\n- Abfrage **nach Timestamp/Zeitraum** (`start_date`..`end_date`), nicht via\n `/history?limit=N` — vollständige, deterministische Datenbasis.\n- Mindestanzahl Candles: `min_candles_required` (default 220), sonst Fehler.\n\n## Bias-Schutz / Realismus (kein Lookahead)\n- **Entry am OPEN der Folgewandle** nach dem Signal (nie zum bekannten Signal-Close).\n- **Regime & Strategie** werden nur auf **abgeschlossenen** Candles (bis Candle i)\n ausgewertet — keine zukünftige Information.\n- **Intrabar Stop/Target konservativ**: Stop wird VOR Target geprüft (kein Lookahead\n durch Intrabar-Reihenfolge). Falls beide in einer Candle getroffen, gewinnt Stop\n (Worst-Case).\n- **Gap-Handling**: Überspringt der Open den Stop/Target, Fill zum Gap-Open\n (realistisch schlechter).\n- **Gebühren/Spread/Slippage** werden auf Entry UND Exit angewendet (verschlechtern\n den Fill).\n- **R-Multiple** bezieht sich auf das **Geldrisiko** `qty * |entry stop|` (nicht\n Preisdifferenz) — `expectancy_r` daher korrekt (vorheriger Bug: 31431 → korrekt 2.0).\n\n## Eingaben (POST /backtest)\n- Symbol, Asset-Klasse, Provider, Timeframe, Start-/Enddatum\n- Strategy + Version (`trend_pullback_v1`, default `1.0.0`)\n- Initiales Kapital, Risk % (Risk-basierte Positionsgröße)\n- Gebühren (fix + %), Spread (Preispunkte), Slippage (Preispunkte)\n- Optionale Limits: `max_positions`, `max_qty`\n\n## Eigene Tabellen (Migration `migrations/001_backtest.sql`)\n- `backtest_run` — Run-Metadaten: run_id, strategy+version, symbol, timeframe,\n status (COMPLETED/…), data_hash, run_hash, Parameter (JSON), Datenzeitraum.\n- `backtest_trade` — einzelne Trades: direction (LONG/SHORT), qty, entry/exit-Preis,\n entry/exit_ts, signal_ts, reason (target/stop/force_close), regime, gross_pnl,\n fee, pnl, r (R-Multiple), strategy_version.\n- `backtest_equity` — Equity-Kurve (Zeitstempel, Equity, offene Positionen).\n\nJede Tabelle FK auf `backtest_run(run_id) ON DELETE CASCADE`.\n\n## Determinismus / Reproduzierbarkeit\n- Eindeutige `run_id` + deterministischer `run_hash` + `data_hash`.\n- `run_hash`: Hash über Strategy+Version+Parameter+Datenzeitraum+data_hash.\n- `data_hash`: Hash über die geladenen OHLCV-Daten.\n- **Gleiche Daten + gleiche Version + gleiche Parameter → identische Trades, Equity,\n Kennzahlen und identischer run_hash** (E2E-verifiziert).\n\n## Tests\n- `tests/test_backtest.py`: 12/12 grün (deterministisch, LONG/SHORT, fees, slippage,\n lookahead, stop/target, gaps, unzureichende Daten, Strategy-Version/Parameter,\n Rebuild-Identität).\n- `tests/fixtures.py`: deterministische Fixture-Factory.\n- `tests/m12_fixtures.py`: erzeugt E2E-Fixtures (BT_PULL LONG, BT_PULL_S SHORT) für\n die VPS-ohlcv-Tabelle.\n- `tests/determinism_check.py` / `determinism_diff.py`: E2E-Determinismus-Nachweis\n (identische Trades/Equity/Kennzahlen/Hashes).\n\n## E2E-Verifikation (VPS, 20.08.2026)\n- **LONG** (BT_PULL, TREND_UP): 1 Trade, winrate 1.0, expectancy_r 1.94, net_pnl\n 196.59, max_drawdown 0.0001, r=1.94, reason=target.\n- **SHORT** (BT_PULL_S, TREND_DOWN): 1 Trade, short_count 1, net_pnl 194.96.\n- `signal_ts 15:00 → entry_ts 16:00` (Einstieg am Open der Folgewandle — Lookahead\n praktisch verifiziert).\n- run_hash + data_hash gesetzt (Beispiel: `bc6e2553…` / `d76a7549…`).\n- **Determinismus**: zwei identische Backtests → identischer run_hash, data_hash,\n Metrics, Trades (normalisiert), Equity-Curve.\n- **Stop/Target/Gap**: Engine-`_evaluate_exit` geprüft — Gap-Down unter Stop → Fill\n zum Gap-Open; Target bei high≥target; Stop vor Target (konservativ); SHORT Gap-Up\n → Fill zum Gap-Open.\n- Health `/health` 200, `/health/ready` 200; keine öffentlichen Ports.\n\n## Compose / Betrieb\n- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL` (Modul-01), Port 55012\n nur intern (`expose`), `restart: unless-stopped`. Kein öffentliches Port-Mapping.\n\n## Nächste Module\n- **Modul-13: Optimization** — Image `optimization:0.1.0`, E2E-verifiziert (12/12 Punkte), **FREIGEGEBEN** (20.08.2026). Details: `modul-13-optimization.md`.\n- **V1.1-Strategie**: `trend_pullback_v1.1` (Image `backtesting:0.1.2`) — Spezifikation und Parameter: `trend-pullback-v1.1-spec.md`.\n\n---\n## Phase 10e — Intrabar Execution + Gap Execution Realism (23.08.2026)\n\nVersionierung der Gap-/Intrabar-Semantik (KEIN separates `gap_execution_version`):\n- Träger: `intrabar_policy` + `intrabar_policy_version = \"intrabar_policy_v1\"` im\n `ExecutionContext` (`shared/historical/execution_context.py`). Dadurch ist die neue\n Semantik eindeutig versioniert und ändert den V2-`run_hash` bei Policy-Wechsel\n (PESSIMISTIC ≠ OPTIMISTIC → anderer run_hash).\n- Legacy-Pfad bleibt unangetastet (PESSIMISTIC-Default, identisches Verhalten).\n\nIntrabar-Policy (nur V2/BID_ASK): `PESSIMISTIC` | `OPTIMISTIC` | `STOP_FIRST` |\n`TARGET_FIRST` | `TICK_RESOLUTION` | `UNKNOWN`. **Fail-closed** §18: `UNKNOWN`/\n`TICK_RESOLUTION`/`BOGUS` → `ValueError`/`NotImplementedError`, kein Auto-Default.\n\nGap-Semantik (deterministisch, kein Lookahead):\n- LONG: Stop/Target/Exit auf **BID**; SHORT auf **ASK**.\n- Gap-Stop → Fill zum Gap-Open (schlechter, NICHT auf Stop-Level geclampt).\n- Target-Gap → Fill zum besseren Gap-Open (nicht geclampt auf Target).\n- Intrabar-Target füllt auf `target` (nicht low/high); Intrabar-Ambiguity (Stop UND\n Target in einer Bar) per Policy (PESSIMISTIC→Stop, OPTIMISTIC→Target).\n- Slippage wird adversial auf den tatsächlichen Fill (Trigger ≠ Market ≠ Final Fill\n bei Gap + Slippage); Commission auf finalem Fill (rate × qty × fill pro Seite).\n- **Kein Doppelspread/Slippage/Commission** (Vier-Ebenen-Trennung 10b/10c/10d/10e).\n\nAudit (18 Felder, forensisch rekonstruierbar): `reason`, `intrabar_policy`,\n`trigger_price`, `market_execution_price`, `exit_fill_price`, `gap_execution`,\n`gap_open_price`, `execution_model`, `price_basis`, `spread_model`, `slippage_model`,\n`slippage_value`, `cost_model`, `entry_fee`, `exit_fee`, `gross_pnl`, `net_pnl`\n(verwendete vorhandene Feldnamen, keine künstlichen).\n\nVerifikation 23.08.2026:\n- Unit-Suite `test_phase10e_intrabar_gap.py`: 40/40 grün (Intrabar, Gap, Slippage\n nach Gap, Commission auf Final Fill, BID/ASK-Seiten, Doppelzählung, Audit-18,\n pathologische Fälle, 4 numerische PnL-E2E-Beispiele).\n- Fixture-E2E (`test_phase10e_fixture_e2e.py`): 7/7 grün (A LONG Stop-Gap, B SHORT\n Stop-Gap, C Intrabar PESSIMISTIC, D Intrabar OPTIMISTIC run_hash-Differenz,\n E LONG Target-Gap, F SHORT Target-Gap, ENV-RESET); numerische Kette\n Trigger → Gap-Open → Slippage → Final Fill → Commission → Gross → Net nachgewiesen.\n- Regression Lauf 1 + Lauf 2: vollständig grün (M12 169 inkl. 10e, Phase5 20,\n Phase6 14, Phase7 AJ Legacy-Hash `8a5760ae…`, M13 Shared AJ, M13 Compat 5/5).\n- Legacy Production Smoke 2×: A==B==VORHER (run_hash `bc6e2553…`, data_hash\n `d76a7549…`, net_pnl 196.586062, 1× LONG target). DB-Wahrheit verifiziert\n (backtest_run/backtest_trade konsistent). Legacy unverändert durch Phase 10e.\n- Production Gates M12+M13: `HISTORICAL_DATA_SOURCE`/`ALLOW_FIXTURE_DATA` UNSET.\n- Deployt: `core/engine.py`, `core/service.py`, `api/schemas.py` (Container-Hashes\n aktualisiert); `shared/historical/execution_context.py` + `pricing.py` unverändert.\n\n---\n## Geändert\n```\nGeändert von: Rain Ocampo\nDatum: 23.08.2026\nGrund: Modul-12-Doku um Phase 10e (Intrabar Execution + Gap Realism) ergänzt — Versionierung, Intrabar-Policy, Gap-Semantik, Audit-18, Verifikation, Deploy, Smoke, Production Gates.\n```\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-12-Backtesting als FREIGEGEBEN markiert (deployt + E2E verifiziert: deterministisch, kein Lookahead, Stop/Target/Gap, LONG/SHORT, run_hash+data_hash).\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-12-Doku aktualisiert — Image auf backtesting:0.1.2 (V1.1), V1-Kompatmodus bitgenau, Referenz auf V1.1-Spec und Modul-13.\n"
},
{
"path": "notes/trading/system-docs/modul-13-optimization.md",
"title": "modul-13-optimization",
"id": "object/2c48489e-0fb9-bdbf-8000-6bbba26771fb",
"type": "arch",
"role": "module",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "d2c3a274ef96ee9923db158735bc71baa8306e0296de27c4fa6c6929e8a5c1fd",
"body": "\n\n# Modul-13: Optimization\n\n**Status: FREIGEGEBEN ✅** · Container `Modul-13-Optimization` · Image `optimization:0.1.0`\n\n## Zweck\nDeterministische, reproduzierbare **Parameteroptimierung** für die V1.1-Strategie\n(`trend_pullback_v1.1`, Modul-12). Findet robuste Parameter im erlaubten Suchraum\n**ohne KI/LLM und ohne Live-Orders** — Ergebnis ist nur **CANDIDATE/RECOMMENDATION**,\nes gibt keine automatische Parameterübernahme in Modul-05.\n\n## Kernprinzipien\n- **Deterministisch**: gleiche Eingaben → gleiche Kandidaten, Reihenfolge, run_hash (kein Zufall außer explizit via Seed).\n- **Kein Lookahead**: IS/OOS strikt zeitlich getrennt; **OOS wird nie zur Selektion verwendet**.\n- **IS/OOS strikt**: `is_ratio` (z.B. 0.7) teilt die Daten; OOS ausschließlich zur Validierung.\n- **Robuste Bereiche > Einzelmax**: best = Kandidat mit bester kombinierter Score (nicht max net_pnl).\n- **Reject-/Overfit-Logik**: zu wenige Trades, DD-Limit, IS/OOS-Degradation → Kandidat wird `rejected`.\n- **Parametergrenzen zwingend**: Suchraum begrenzt auf `pullback_band_atr`, `trend_regime_window`, `rr_multiplier`.\n- **Score-Gewichte**: Expectancy/R 0.30, PF 0.25, Max-Drawdown 0.20, Trades 0.10, Stabilität 0.15.\n- **Synchron**: Request/Response über API (kein RabbitMQ).\n\n## Architektur / Datenfluss\n```\nOHLCV (Modul-01 PostgreSQL, Tabelle ohlcv)\n │ synchron, direkt nach Timestamp/Zeitraum\n ▼\nModul-13-Optimization ── POST /optimization\n │\n ├── core/split.py IS/OOS + Walk-Forward-Folds (kein Leakage)\n ├── core/optimizer.py Grid/Random-Suche, ScoreEngine, WF, Reject\n ├── core/scoring.py Multi-Metrik-Score (Gewichte siehe oben)\n ├── storage/storage.py Persistenz optimization_run/candidate/walk_forward\n ├── backtest_client.py synchroner Aufruf Modul-12 (POST /backtest)\n └── api/main.py REST-API (intern, Port 55013)\n```\n\nAPI (Docker-intern, Port 55013, kein öffentliches Port-Mapping):\n```\nGET /health, /health/ready\nPOST /optimization Start (Grid/Random), synchron\nGET /optimization Liste aller Runs\nGET /optimization/{id} Run-Metadaten + Status\nGET /optimization/{id}/candidates Kandidaten + Multi-Metrik-Ranking\nGET /optimization/{id}/best bester ROBUSTER Kandidat (RECOMMENDATION/NO_RECOMMENDATION)\nGET /optimization/{id}/walk-forward Walk-Forward-Folds\n```\n\n## Suchraum (V1.1 — NUR 3 optimierbare Parameter)\n| Parameter | Suchraum (E2E) | Zweck |\n|-----------|----------------|-------|\n| `pullback_band_atr` | {0.2, 0.5, 0.8, 1.0} | ATR-Band um EMA20 (Pullback-Timing) |\n| `trend_regime_window` | {60, 200, 300} | Fenster für strategie-internes Trend-Regime |\n| `rr_multiplier` | {1.0, 2.0, 3.0} | Target = Entry + rr×Risiko |\n\nFix (nicht optimiert): `atr_period=14`, `regime_vol_override=false`, `min_risk_reward=1.0`,\nADX-/EMA-Perioden (14/20/30), `adx_trend_threshold=20`.\n\n## Profile\n| Profil | min_trades_is | min_trades_oos | max_drawdown_limit | max_oos_degradation | Zweck |\n|--------|---------------|----------------|--------------------|--------------------|-------|\n| **produktion** (Default) | 3 | 1 | 0.30 | 0.5 | Anti-Overfit, Produktion |\n| **test** (`profile=test`) | 1 | 1 | — | — | nur Mindest-Trade-Schwellen gesenkt für technischen E2E; sonst identisch |\n\nDas TEST-PROFIL senkt **ausschließlich** die Mindest-Trade-Schwellen (damit ein\nSweep über die kalibrierte synthetische Fixture genügend Kandidaten akzeptiert).\nProduktive Anti-Overfit-Defaults bleiben unverändert. `profile` wird per API-Feld\nübergeben; explizite Request-Werte (`min_trades_is/oos`) haben Vorrang.\n\n## Eigene Tabellen (Migration)\n- `optimization_run` — Run-Metadaten: optimization_id, strategy+version, symbol, timeframe,\n optimizer_method, optimizer_version, param_space, seed, status, result (JSON), created/completed_at.\n- `optimization_candidate` — Kandidaten: rank, params, backtest_run_id_is/oos, is_metrics,\n oos_metrics, score, score_components, rejected, reject_reasons, flags, stability, robustness.\n- `walk_forward_result` — WF-Folds: fold, params, is_start/end, oos_start/end, is/oos_metrics,\n score, backtest_run_id_is/oos.\n\n## Reject-/Overfit- und Recommendation-Verhalten\n- Kandidat wird `rejected` wenn: zu wenige Trades (min_trades_is/oos), Drawdown über Limit,\n OOS-Degradation zu hoch (Overfit), fachlich unbrauchbare Metriken (z.B. PF<=0/kein Trades).\n- `/best` liefert:\n - `recommendation=RECOMMENDATION` + `best_candidate` wenn ein fachlich brauchbarer robuster Bester existiert.\n - `recommendation=NO_RECOMMENDATION` (statt leerem `best_candidate`), wenn das Kandidatenfeld\n fachlich unbrauchbar ist — eine produktive Empfehlung wird nie aus unbrauchbaren Daten erzeugt.\n\n## Tests\n- `tests/test_optimization.py`: 12/12 grün (Grid deterministisch, Random-Seed, IS/OOS-Leak,\n Walk-Forward korrekt + JSON-serialisierbar, Multi-Metrik-Ranking, Reject/Overfit, Robustheit,\n Suchraum=3 Params, Reproduzierbarkeit, NO_RECOMMENDATION, TEST-PROFIL).\n\n## E2E-Verifikation (VPS, 20.08.2026)\n- **Suchraum-Differenzierung** (kalibrierte Fixture `M13_E2E`, 1314 Bars, alle 3 Params unterscheidbar):\n - `pullback_band_atr`: 0.2→8 Trades, 0.5→910, 0.8/1.0→1 Trade.\n - `trend_regime_window`: bei pba=0.5: 60→9 Trades vs 200/300→10 Trades.\n - `rr_multiplier`: 1.0→810 Trades (net +828…+1046), 2.0/3.0→1 Trade.\n- **12/12 E2E-Punkte über API bestätigt** (e2e_full.py):\n Grid deterministisch, Random-Seed identisch, IS/OOS-Leak ausgeschlossen, WF-Folds zeitlich\n korrekt + DB-persistiert (3 Folds), Multi-Metrik-Ranking, Reject-Logik (36 Kandidaten,\n 30 rejected / 6 accepted), best=robust + RECOMMENDATION, DB-Run/Candidates vollständig,\n API list_runs, Reproduzierbarkeit (status COMPLETED), NO_RECOMMENDATION bei nur-1-Trade\n Konfiguration (produktion-Profil).\n- **Bugfix C**: Walk-Forward-Fold-Ergebnisse enthalten `datetime`-Timestamps → beim\n `json.dumps` in `complete_run` nicht serialisierbar (422). Fix: ISO-String-Kopie im\n Run-Result, datetime bleibt für `storage.insert_wf`. Verifiziert (3 WF-Folds in DB).\n- Health `/health/ready` → `{\"status\":\"ready\",\"db\":\"ok\"}` (M13 und M12). Keine Tracebacks.\n- Port 55013 nur intern (`expose`), kein Host-Port-Binding (verifiziert).\n\n## Freigabe\n- **20.08.2026: Modul-13-Optimization-Service vom Nutzer FREIGEGEBEN ✅**\n (nach vollständiger E2E-Verifikation: 12/12 API-Punkte, Suchraum-Differenzierung für alle\n 3 Params, TEST-PROFIL, NO_RECOMMENDATION, Bugfixes A/B/C deployt, Tests 12/12 grün).\n- Keine weiteren technischen Änderungen an Modul-13.\n\n## Compose / Betrieb\n- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL` (Modul-01), Backtest via\n `Modul-12-Backtesting` (intern). Port 55013 nur intern (`expose`), `restart: unless-stopped`.\n\n## Nächste Module\n- **Modul-14 ff.** — noch Platzhalter (alpine), NICHT gestartet.\n\n---\n## Geändert\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: 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).\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-13 vom Nutzer FREIGEGEBEN — Status auf FREIGEGEBEN gesetzt, Freigabe-Sektion ergänzt. Keine technischen Änderungen.\n```\n"
},
{
"path": "notes/trading/system-docs/modul-14-notification.md",
"title": "modul-14-notification",
"id": "object/ea077314-9e29-6c7f-1c17-008fa8e720ae",
"type": "arch",
"role": "module",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "842832a4f2ff1fce6609e9f7dc92ae1b113ddd1bc1800dcd36d526dcef390a3a",
"body": "\n\n# Modul-14: Notification-Service\n\n**Status: FREIGEGEBEN ✅** · Container `Modul-14-Notification` · Image `notification:0.1.0`\n\n## Zweck\nZentrale **Benachrichtigungen für Trading- und Systemereignisse**. Konsumiert relevante\nEvents aus den RabbitMQ-Exchanges der Module 0510 und liefert kompakte, strukturierte\nMeldungen — **V1 primär über Telegram** (`TelegramProvider`), provider-neutral angelegt.\n\n**Restriktionen (Nutzer-Vorgabe):**\n- **keine KI/ML**, **keine Trading-Entscheidungen**, **keine Orders**.\n- **FAIL-SAFE:** Ein Notification-Ausfall darf die Trading-Pipeline **NIEMALS blockieren**.\n\n## Architektur / Datenfluss\n```\nRabbitMQ (market.signals/.rankings/.risk/.portfolio/.execution/.journal)\n │ NotificationConsumer (notification.input)\n ▼\nModul-14-Notification ── NotificationService ──> RulesEngine + DedupGuard\n │ │ Spam-Schutz\n ├── providers/ NotificationProvider-ABC → Telegram / Fake\n ├── rules/ RulesEngine (Regeln) + DedupGuard (Dedup/Cooldown/Rate-Limit)\n ├── storage/ Persistenz notification_log (PostgreSQL, Modul-01)\n ├── core/ formatter (kompakte Nachricht) + service (Retry/Backoff)\n └── api/main.py REST-API (intern, Port 55014)\n\nEvent → RabbitMQ → Modul-14 → notification_log → Provider (Telegram) → Chat\n```\n\n## API (Docker-intern, Port 55014, kein öffentliches Port-Mapping)\n```\nGET /health liveness\nGET /health/ready readiness (postgresql, consumer_ready, provider_configured)\nGET /notifications/recent letzte Notifications\nGET /notifications/{id} einzelne Notification\nPOST /notifications/test Test-Sendung (nur konfigurierte Destinationen)\n```\n\n## Konsumierte Events & Routing-Keys\n| Event-Typ | Exchange | Routing-Key | Default-Severity | Default aktiv |\n|-----------|----------|-------------|------------------|---------------|\n| `SIGNAL_DETECTED` | `market.signals` | `strategy.signal.detected` | INFO | ❌ (deaktiviert) |\n| `SIGNAL_RANKED` | `market.rankings` | `signal.ranked` | INFO | ❌ (deaktiviert) |\n| `RISK_APPROVED` | `market.risk` | `risk.approved` | INFO | ❌ (deaktiviert) |\n| `RISK_REJECTED` | `market.risk` | `risk.rejected` | WARNING | ❌ (deaktiviert) |\n| `TRADE_APPROVED` | `market.portfolio` | `trade.approved` | INFO | ✅ |\n| `PORTFOLIO_REJECTED` | `market.portfolio` | `portfolio.rejected` | WARNING | ✅ |\n| `ORDER_SUBMITTED` | `market.execution` | `order.submitted` | INFO | ❌ (deaktiviert) |\n| `ORDER_FILLED` | `market.execution` | `order.filled` | INFO | ✅ |\n| `ORDER_REJECTED` | `market.execution` | `order.rejected` | CRITICAL | ✅ |\n| `ORDER_FAILED` | `market.execution` | `order.failed` | CRITICAL | ✅ |\n| `TRADE_RECORDED` | `market.journal` | `trade.recorded` | INFO | ✅ |\n\nDie Regeln sind **pro Event-Typ konfigurierbar** (aktiv/inaktiv, Severity, Provider,\nDestination, Cooldown, Rate-Limit). **Unbekannte Event-Typen werden sicher ignoriert**\n(kein Crash). V1 sind die rauschigen Events (SIGNAL_*, RISK_*, ORDER_SUBMITTED)\nstandardmäßig deaktiviert.\n\n## Severity-Hierarchie\n```\nINFO < WARNING < CRITICAL\n```\nRegel-Severity überschreibt den Event-Standard. V1: alle über Telegram.\n\n## Spam-Schutz (DedupGuard)\nMehrstufig, thread-sicher (Lock):\n1. **Idempotenz** — gleiche `event_id`/`source_event_id` erzeugt nie eine zweite Notification (DB-unique über `source_event_id` + `event_processed`-Check).\n2. **Dedup** — identische Meldungen (gleicher Text/Event-Typ) in kurzem Fenster werden zusammengefasst.\n3. **Cooldown** — pro Event-Typ ein Mindestabstand (`cooldown_seconds`, Default 60s).\n4. **Rate-Limit** — maximale Notifications pro Minute (`rate_limit_per_minute`, Default 30).\n5. **Burst-Schutz** — Ansturm vieler Events wird aufs Limit begrenzt.\n\n## Retry / Dead (FAIL-SAFE)\n- Provider nicht erreichbar → `RETRY_PENDING` mit **begrenzten Retries** (exponentielles Backoff).\n- `max_attempts` (Default 5) erreicht → **DEAD** (keine Endlosschleife).\n- **Provider nicht konfiguriert** (fehlender Token/Chat-ID) → **sofort DEAD** mit\n `error_code=NOT_CONFIGURED` (keine sinnlosen Retries).\n- Nach Provider-Recovery werden **neue Events wieder normal gesendet** (Retry betrifft nur die jeweilige Notification).\n\n## Eigene Tabelle (Migration)\n`notification_log`:\n`notification_id` (PK), `source_event_id`, `event_type`, `severity`, `provider`,\n`destination`, `status` (`SENT`/`FAILED`/`RETRY_PENDING`/`DEAD`), `attempts`,\n`created_at`, `sent_at`, `error_code`, `error_message`.\n\n## Sicherheit\n- **KEINE Secrets in DB/Logs/Doku/Commits.** Telegram-Bot-Token/Chat-ID ausschließlich\n über Environment (Compose `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`), nie hart kodiert.\n- `/notifications/test` sendet **nur an konfigurierte Destinationen**.\n\n## Tests\n- `tests/test_notification.py`: **14/14 grün** (Event→Notification, Idempotenz, Dedup,\n Cooldown, Rate-Limit, Telegram-Ausfall blockiert nicht, Retry+Backoff, Max→DEAD,\n keine Secrets, unbekannter Typ ignoriert, Severity/Regeln, FakeProvider, Reconnect).\n\n## E2E-Verifikation (VPS, 20.08.2026) — alle 7 Punkte grün\n1. **Fake-Telegram-E2E** (Modul-14 auf `http://fake-telegram:55000`, Test-Token/Chat-ID\n nur als ENV): echtes `TRADE_RECORDED` publiziert → **Event → RabbitMQ → Modul-14 →\n notification_log → Fake-Telegram** komplett durchlaufen. DB: `status=SENT`,\n `sent_at` gesetzt, `attempts=0`, **genau 1 Zustellung, kein Duplikat**. Fake-Telegram\n RAW: `{\"chat_id\": \"E2E_TEST_CHAT_114\", \"text\": \"TRADE RECORDED | NVDA | LONG | OPEN\"}`.\n2. **NO_CONFIG-Fix**: Container ohne Token → Event → `status=DEAD, attempts=1,\n error_code=NOT_CONFIGURED` (sofortiges DEAD, keine 5 sinnlosen Retries).\n3. **Fehlerfall-E2E**: Fake-Telegram gestoppt → Notification `RETRY_PENDING` mit\n Backoff (attempts 2→4, `NETWORK_ERROR`), **Trading-Pipeline unbeeinflusst**,\n Max-Retries (`attempts=5`) → `DEAD`. Nach Fake-Telegram-Recovery → neues Event\n wieder `SENT`.\n4. **RabbitMQ-Reconnect**: kontrollierter Restart → Modul-14 reconnectet automatisch\n (Backoff 4s→8s→16s, „Consumer verbunden\"), **genau 1 Consumer** an `notification.input`,\n keine Zombies, keine verlorenen/duplizierten Notifications (nach Reconnect: 1 Zustellung, 0 Duplikate).\n5. **Spam-/Sicherheitschecks** (praktisch): unbekannter Typ `UNKNOWN_EVENT_XYZ` → ignoriert;\n gleiche `event_id` 2× → nur 1 Notification (Idempotenz); gleicher Event-Typ 2×\n → Cooldown greift; deaktivierter `SIGNAL_DETECTED` → keine Notification. **Keine\n Tokens/Chat-IDs in Logs/API/Doku/DB** (0 Token-Leaks).\n6. **Health/Readiness**: `/health` + `/health/ready` → 200\n (`postgresql=true, consumer_ready=true, provider_configured=…`). Keine\n Applikations-Tracebacks im Normalbetrieb (nur erwartete Reconnect-Logs beim Test).\n Port 55014 nur intern (`expose`, kein Host-Port).\n7. **Forgejo-Doku + Commit-ID** (siehe Freigabe unten), temporäre Test-Credentials\n entfernt, Remote token-/key-frei.\n\n## Freigabe\n- **20.08.2026: Modul-14-Notification-Service vom Nutzer FREIGEGEBEN ✅**\n (nach vollständiger E2E-Verifikation der Punkte 17, alle grün).\n- Keine weiteren technischen Änderungen an Modul-14.\n\n## Compose / Betrieb\n- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL`, RabbitMQ `Modul-02-RabbitMQ`.\n- Port 55014 nur intern (`expose`), `restart: unless-stopped`, non-root (uid 1001).\n- Token/Chat-ID via ENV (`TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`, `TELEGRAM_API_BASE`).\n\n## Nächste Module\n- **Modul-15 ff.** — noch Platzhalter (alpine), NICHT gestartet.\n\n---\n## Geändert\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-14-Notification-Service dokumentiert — E2E-verifiziert (Punkte 17 grün),\n API/Events/Severity/Retry-Dead/Spam-Schutz/Fake-E2E dokumentiert, Status FREIGEGEBEN.\n```\n"
},
{
"path": "notes/trading/system-docs/modul-15-m09-anbindung-design.md",
"title": "modul-15-m09-anbindung-design",
"id": "object/5f28e1fd-19f4-e5af-1c92-a17ec80b08f0",
"type": "design",
"role": "design",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "4e4286e95b5306c72371d8ee09c9bed75018b9d29a76b8142383ab47c9418bea",
"body": "\n\n# Design-Vorschlag: Modul-15 → Modul-09 Anbindung (Safety-/Trading-Control)\n\n**Status: NUR DESIGN-VORSCHLAG — keine Implementierung.**\n**Datum:** 20.08.2026 · **Autor:** Rain Ocampo (Hermes)\n**Gilt für:** Modul-09-Execution-Service (FREIGEGEBEN, Order-Pfad) ↔ Modul-15-Monitoring-Control (IMPLEMENTIERT + E2E-verifiziert, **noch nicht freigegeben**)\n\n> **Scope-Regeln (verbindlich):**\n> - M09 wird **nicht** verändert, bis dieser Vorschlag vom Nutzer freigegeben wurde.\n> - M15 bleibt **nicht freigegeben** bis Nutzer-Freigabe.\n> - M16 wird **nicht** begonnen.\n> - Dieser Vorschlag ist **Architektur/Pseudoflow**, kein Code.\n\n---\n\n## 1. Ziel\n\n**Modul-15** wird die **autoritative Safety-/Trading-Control-Instanz** für den Order-Durchstich.\n**Modul-09** darf neue Orders **nur senden, wenn M15 dies eindeutig erlaubt**.\n\nKernauftrag: 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).\n\n---\n\n## 2. Ist-Zustand in M09 (Kontext für den Vorschlag)\n\nDer Order-Pfad in `app/core/service.py::_on_event` ist heute:\n\n```\nTRADE_APPROVED (Modul-08)\n → 1) Idempotenz (already_processed)\n → 2) Harte Validierung validate_trade_approved (FAIL-CLOSED)\n → 3) Broker-Bereitschaft validate_broker_ready (FAIL-CLOSED)\n → 4) Order atomar in DB anlegen (Advisory Lock + Transaktion, PENDING)\n → 5) _submit_and_track → BrokerAdapter.submit_order\n```\n\n**Heutiger Kill-Switch:** rein konfigurativ `TRADING_ENABLED=false` (Default, sicher für LIVE).\nEs existiert **keine** externe Echtzeit-Control-Schnittstelle im Order-Pfad.\n\n**Einbau-Punkt (Vorschlag):** zwischen Schritt 4 (Order angelegt) und Schritt 5 (Broker-Submit)\n— d. h. **unmittelbar vor `submit_order`**. Alternativ als zusätzlicher Check in Schritt 2.\nEmpfehlung: als eigener Schritt zwischen 4 und 5, weil dort die Order bereits idempotent in der DB\nliegt und wir den Final-Check „direkt vor dem Senden\" platzieren (Race-Window minimal).\n\n---\n\n## 3. Trading-States & Regelwerk\n\n### 3.1 Globale Trading-States (autoritativ: M15)\n\n| State | Bedeutung | neue OPEN/INCREASE | REDUCE | CLOSE/CANCEL |\n|-------|-----------|--------------------|--------|--------------|\n| `ENABLED` | Alle krit. Dienste gesund, aktuelle Market-Data, Trading erlaubt | **ERLAUBT** | erlaubt | erlaubt |\n| `PAUSED` | Nicht-krit. Störung (Analytics/Notification), Risiko-umfeld unklar | **VERBOTEN** | **ERLAUBT** | **ERLAUBT** |\n| `HALTED` | Krit. Ausfall (PG/RMQ/Execution/Market-Data/UNKNOWN) | **VERBOTEN** | **ERLAUBT** | **ERLAUBT** |\n| `UNKNOWN` / M15 nicht erreichbar / Status veraltet | Unklarheit | **VERBOTEN (FAIL-CLOSED)** | **ERLAUBT** (konservativ) | **ERLAUBT** |\n\n**Kernregel:** Nur `ENABLED` erlaubt neue Positionen (OPEN) oder Aufstockung (INCREASE).\n**Alles andere** (PAUSED/HALTED/UNKNOWN/nicht erreichbar/veraltet) blockiert OPEN/INCREASE.\n**REDUCE/CLOSE** (Risikoreduktion/Position schließen) sind **immer** erlaubt — unabhängig vom State.\n\n### 3.2 Order-Kategorien (Einordnung)\n\n| Aktion | Bedeutung | Regel bei PAUSED/HALTED/UNKNOWN |\n|---|---|---|\n| `OPEN` | Neue Position eröffnen | **BLOCKIEREN** |\n| `INCREASE` | Bestehende Position aufstocken | **BLOCKIEREN** (Neue-Position-Logik, Risiko wächst) |\n| `REDUCE` | Position verkleinern | **ERLAUBT** (Risiko sinkt) |\n| `CLOSE` | Position schließen | **ERLAUBT** (Risiko eliminieren) |\n| `CANCEL` | Offene/geplante Order stornieren | **ERLAUBT** (kein neues Risiko) |\n\n> Begründung INCREASE blockiert: Aufstocken fügt neues Marktrisiko hinzu — bei HALT/Unklarheit\n> unerlaubt. REDUCE/CLOSE/CANCEL sind per Definition risikoreduzierend → immer frei.\n\n---\n\n## 4. Vergleich der Anbindungs-Varianten\n\n### Variante A — synchroner HTTP-Check vor jeder Order\n\nM09 ruft vor jedem Submit `GET /control/trading-state` auf M15 (Docker-intern) auf.\n\n| Kriterium | Bewertung |\n|---|---|\n| **Sicherheit** | Sehr hoch — liest immer den frischesten autoritativen Zustand. FAIL-CLOSED bei Timeout. |\n| **Latenz** | Pro Order +1 Round-Trip (ms-Bereich, Docker-Netz). Bei vielen Orders spürbar, aber Orders sind selten. |\n| **Ausfallsicherheit** | M15-down → HTTP-Time out → FAIL-CLOSED (blockieren). Sicher, aber **M15 wird Single Point of Failure für OPEN**. |\n| **Race Conditions** | M15 könnte direkt nach Antwort auf HALTED wechseln → Race trotz Check (siehe §7). Final-Check nur minimal schmaler. |\n| **Single Point of Failure** | **Ja** — M15 ist für OPEN/INCREASE zwingend. Bei M15-down wird der Execution-Service sonst blind blockiert. |\n| **Bei RabbitMQ-Ausfall** | HTTP bleibt funktionsfähig (separater Kanal) → Check ok; aber M15 meldet RABBITMQ_UNREACHABLE→HALTED → korrekt blockiert. |\n| **Bei M15-Ausfall** | Timeout → FAIL-CLOSED → OPEN/INCREASE blockiert (gewünscht). REDUCE/CLOSE müssen **ausgenommen** werden, sonst kann Position nicht geschlossen werden. |\n| **Komplexität** | Gering — ein HTTP-Call, kein Cache, kein Subscription. |\n| **Nachvollziehbarkeit/Audit** | M15 protokolliert jede Anfrage; M09 loggt Check + Ergebnis. Gut, aber hochfrequent (jede Order). |\n\n### Variante B — Event-basierter Status-Cache (publish/subscribe)\n\nM15 publiziert Statusänderungen auf `market.control`; M09 subscribt und hält ein lokales State-Modell.\n\n| Kriterium | Bewertung |\n|---|---|\n| **Sicherheit** | Mittel — Status nur so frisch wie das letzte Event + Cache-TTL; **riskant ohne TTL/Herzschlag**. |\n| **Latenz** | Sehr gering — kein HTTP im kritischen Pfad, Cache-Lookp. |\n| **Ausfallsicherheit** | Schlecht — RabbitMQ-Ausfall ⇒ keine Events mehr; Cache wird **stale**. Ohne TTL würde M09 mit veraltetem ENABLED weiter traden. **Gefahr.** |\n| **Race Conditions** | Stale-Cache ist die größte Race-Quelle. Event-Reihenfolge/Lag. |\n| **Single Point of Failure** | RabbitMQ als Event-Bus ist der SPOF für den Status-Transport. |\n| **Bei RabbitMQ-Ausfall** | Events stoppen → Cache veraltet → **genau das Risiko**, das wir vermeiden wollen. Muss über Cache-TTL + Fail-CLOSED gelöst werden. |\n| **Bei M15-Ausfall** | Keine neuen Events → Cache veraltet. Ohne TTL gefährlich (weiter OPEN). |\n| **Komplexität** | Mittel — Subscription, Cache-Update, TTL-Reinigung, RabbitMQ-Reconnect im M09. |\n| **Nachvollziehbarkeit/Audit** | Gut — Event-Stream ist Append-Log; aber Cache-Echo im M09 ist ein zweites Modell (Konsistenzpflege). |\n\n### Variante C — Kombination: Event-Cache + synchroner Final-Check (EMPFEHLUNG)\n\n**Event-Cache als schnelle Vorfilterung**, **synchroner HTTP-Final-Check unmittelbar vor dem\nBroker-Submit** als harte Bestätigung. Nur wer beide klar \"ENABLED\" liefert, darf OPEN/INCREASE.\n\n| Kriterium | Bewertung |\n|---|---|\n| **Sicherheit** | **Sehr hoch** — zweistufig. Cache als Bauraus/Performance, HTTP-Final als Autorität. |\n| **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. |\n| **Ausfallsicherheit** | Beste — Cache kann \"PAUSED/HALTED/UNKNOWN\" sofort blockieren; HTTP- Ausfall ⇒ FAIL-CLOSED. Zwei unabhängige Quellen. |\n| **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. |\n| **Single Point of Failure** | M15 bleibt SPOF für OPEN/INCREASE (unvermeidbar, da autoritativ). REDUCE/CLOSE ausgenommen. |\n| **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). |\n| **Bei M15-Ausfall** | HTTP-Final-Check schlägt fehl → FAIL-CLOSED, OPEN/INCREASE blockiert. REDUCE/CLOSE frei. |\n| **Komplexität** | Höher (beides). Aber gut beherrschbar; Cache als reiner nicht-kritischer Vorfilter, Autorität liegt klar beim HTTP. |\n| **Nachvollziehbarkeit/Audit** | Exzellent — Final-Check liefert `state_version` + `timestamp` + `audit_id` pro Order; Cache-Zustand separat auditierbar. |\n\n### Zusammenfassung der Varianten\n\n| | A (HTTP) | B (Event-Cache) | C (Kombi) |\n|---|---|---|---|\n| Sicherheit | hoch | mittel | **sehr hoch** |\n| Latenz | +HTTP | **sehr gering** | gering (HTTP-Final) |\n| Ausfallsicherheit | ok (SPOF M15) | **schwach** (RMQ SPOF) | **beste** |\n| Race-Risiko | minimiert (kurz) | **hoch (stale)** | **minimiert** |\n| RMQ-Ausfall | korrekt (HALTED) | **risiko (stale)** | korrekt |\n| M15-Ausfall | FAIL-CLOSED | **risiko (stale)** | FAIL-CLOSED |\n| Komplexität | gering | mittel | **höher** |\n| Audit | gut | zweites Modell | **exzellent** |\n\n**Empfehlung: Variante C** (Event-Cache als Vorfilter + synchroner Final-Check).\n\n---\n\n## 5. Race Condition & Lösung\n\n**Problem:** M09 prüft `ENABLED`, M15 wechselt sofort danach auf `HALTED` — bevor M09 die Order gesendet hat.\n\n**Lösung (Kombination):**\n1. **Final-Check unmittelbar vor `submit_order`** — minimales Zeitfenster (ms) zwischen Check und Send.\n2. **TTL auf der HTTP-Antwort:** `expires_at` (z. B. `now + 500 ms`). Ist die Antwort beim Senden abgelaufen\n (Fenster überschritten), NICHT senden → Order nach `FAILED/REJECTED` mit `CONTROL_STATE_STALE` markieren.\n3. **`state_version`** (Monoton aufsteigend, von M15 je Status-Änderung). M09 übernimmt die im Final-Check\n gesehene Version. Beim Broker-Submit trägt die Order die `control_version`; die Audit-Kette verbindet\n genau die Version, die gegolten hat, mit dem Versand.\n4. **Bestätigtes Einverständnis:** Der Check liefert `trading_status=ENABLED` **und** `state_version` **und**\n `expires_at`. Nur wenn beides vorliegt und innerhalb der TTL ist, darf OPEN/INCREASE durchgehen.\n5. **Audit-ID / Correlation:** M09 erzeugt `control_audit_id`, M15 protokolliert die Bestätigung. Damit ist\n der „wir haben im ENABLED-Zustand gesendet\" Moment exakt nachvollziehbar.\n\n**Kein \"Senden dann hoffen\":** Ist das Fenster überschritten oder Status unklar → `FAILED` mit\n`CONTROL_STATE_UNKNOWN/CONTROL_STATE_STALE`, **niemals** blind erneut senden (konsistent zum bestehenden\nM09-Time-out-Handling: erst Status beim Broker klären, dann entscheiden).\n\n---\n\n## 6. API-Vertrag M15 → M09\n\n### 6.1 `GET /control/trading-state` (von M09 im Final-Check)\n\n**Request:** keiner (oder optional `?action=OPEN` / `?for_action=OPEN` zur Kontext-Auditierung)\n\n**Response 200 (ENABLED):**\n```json\n{\n \"trading_status\": \"ENABLED\", // ENABLED|PAUSED|HALTED|UNKNOWN\n \"system_status\": \"HEALTHY\", // Kontext\n \"state_version\": 42, // monoton, von M15 je Änderung\n \"timestamp\": \"2026-08-20T15:30:00Z\", // Erzeugzeit (UTC, ISO 8601)\n \"expires_at\": \"2026-08-20T15:30:00.5Z\", // TTL-Horizont (Server), z. B. now+500ms\n \"ttl_seconds\": 0.5,\n \"audit_id\": \"ctl-...\" // M15-interne Audit-Kennung der Bestätigung\n}\n```\n**Nur** dieses Objekt mit `trading_status=ENABLED` und `expires_at` in der Zukunft gilt als \"eindeutige Erlaubnis\".\n\n**Response (blockiert):** gleiches Schema, aber `trading_status` = `PAUSED`/`HALTED`/`UNKNOWN`.\n\n**Fehler/Timeouts:**\n- HTTP 503 → M15 selbst nicht Ready/unbekannt → **FAIL-CLOSED** (als HALTED/unbekannt behandeln).\n- Timeout/5xx/netz → **FAIL-CLOSED**, als `UNKNOWN` behandeln.\n- Alle nicht-`ENABLED`-Antworten → OPEN/INCREASE **verweigert**.\n\n### 6.2 Empfehlung der aktuellen API-Form\n\nM15 hat aktuell `GET /control/trading-state` (liefert `trading_status` + `system_status`). Für M09-Anbindung\nempfehle ich die obige **Erweiterung** um `state_version`, `timestamp`, `expires_at`, `audit_id` — dies ist ein\n**additiver API-Vertrag** (kein Breaking Change). Das ist im Vorschlag berücksichtigt; die konkrete\nUmsetzung erfolgt erst nach Freigabe.\n\n---\n\n## 7. Event-Schema für Status-Änderungen (market.control)\n\nM15 publiziert auf Exchange `market.control`, Routing `trading.state` — **nur bei Status-Änderung** (keine Schleife).\n\n```json\n{\n \"event_id\": \"evt-...\",\n \"event_type\": \"trading.state\",\n \"timestamp\": \"2026-08-20T17:30:00Z\",\n \"version\": 1,\n \"payload\": {\n \"trading_status\": \"HALTED\", // ENABLED|PAUSED|HALTED|UNKNOWN\n \"system_status\": \"UNHEALTHY\",\n \"state_version\": 43,\n \"reason_codes\": [\"POSTGRESQL_UNREACHABLE\", \"PERSISTENCE_FAILURE\"],\n \"changed_at\": \"2026-08-20T17:30:00Z\"\n }\n}\n```\n\n**Im Cache (Variante C):**\n- M09 hält `last_state` (trading_status + state_version + received_at).\n- **TTL (z. B. 5 s):** ist `received_at` älter als TTL → Cache behandelt als `UNKNOWN/veraltet` → OPEN/INCREASE\n **blockiert** (auch wenn letztes Event `ENABLED` war).\n- Ein Event `state_version` älter als bereits gesehen → ignorieren (kein Regress).\n\n---\n\n## 8. Verhalten nach Recovery\n\n- **Kein automatisches RESUME:** M15 nimmt nach Behebung der Ursache Status selbst neu (z. B. HALTED→HEALTHY→ENABLED)\n **nur durch den regulären Monitoring-Zyklus** (Ursache wirklich erneut geprüft). Es gibt kein verstecktes Auto-Resume.\n- M09 wartet darauf: erst wenn ein **neues Event mit `trading_status=ENABLED` und neuer `state_version`** oder\n ein **HTTP-Final-Check mit `ENABLED`** vorliegt, sind OPEN/INCREASE wieder möglich.\n- Ein `PAUSED→ENABLED`-Übergang verlangt, dass M15 nachweislich die früheren HALT-Gründe entfernt hat\n (keine Reason-Codes mehr). Dies wird durch die bestehende FAIL-CLOSED-Regel + idempotentes Event sichergestellt.\n- **Cache-TTL-Disziplin:** Nach einem HALTED/PAUSED-Ereignis bleibt M09 vorsichtig: Es traut dem Cache erst\n wieder, wenn ein neues `ENABLED`-Event (hohe Version) eingegangen ist — niemals basierend auf Zeitablauf der\n Sperre, sondern auf **bestätigter Freigabe**.\n\n---\n\n## 9. Audit in M09 (zusätzlich zu M15-Audit)\n\nFür jede Order-Entscheidung (im M09-Storage bzw. `execution_event`):\n- `control_status`: Ergebnis des Final-Checks (`ENABLED`/`PAUSED`/...)\n- `control_state_version`: Version, die beim Send gegolten hat\n- `control_expires_at` / `control_ttl`: TTL-Horizont\n- `control_audit_id`: vom Final-Check zugeordnet\n- `control_check_source`: `final_http` (und optional `cache`)\n\nDamit ist jede Order exakt dem autoritativen Zustand zum Sendzeitpunkt zugeordnet (Audit-Kette vollständig).\n\n---\n\n## 10. Zusammenfassung Architektur-Empfehlung\n\n**Variante C**: Event-Cache (mark-to-control, TTL, state_version) als **nicht-kritischen Vorfilter** im M09 +\n**synchronier HTTP-Final-Check** (`GET /control/trading-state`, mit `expires_at`+`state_version`+`audit_id`)\nunmittelbar vor `submit_order`. Beide müssen `ENABLED` und frisch sein; sonst OPEN/INCREASE blockiert\n(FAIL-CLOSED). REDUCE/CLOSE/CANCEL sind von der Control-Regel ausgenommen und laufen immer.\n\n- **Zustand, der OPEN/INCREASE erlaubt:** Cache == `ENABLED` (frisch) UND Final-Check == `ENABLED` (frisch).\n- **Sonst alles:** blockiert OPEN/INCREASE; REDUCE/CLOSE/CANCEL frei.\n- **FAIL-CLOSED:** M15 nicht erreichbar, Timeout, Cache veraltet, `UNKNOWN` → OPEN/INCREASE verweigert.\n- **M15 bleibt SPOF nur für OPEN/INCREASE** (unvermeidbar, autoritativ). Exit/Close bleibt immer möglich.\n\n---\n\n## 11. Pseudoflow (Order-Durchstich M09)\n\n```\nTRADE_APPROVED (Modul-08)\n │\n ▼\n1. Kategorisiere Aktion: OPEN | INCREASE | REDUCE | CLOSE | CANCEL\n │\n ├─ action ∈ {REDUCE, CLOSE, CANCEL} → ERLANG DURCHSTICH (kein Control-Check)\n │ └─→ weiter zum bestehenden Submit-Pfad\n │\n └─ action ∈ {OPEN, INCREASE}\n │\n ▼\n2. [Cache-Vorfilter] local_state = cache.get()\n ├─ local_state ist stale (age > TTL) → → zum Final-Check (unten) aber NICHT aus Cache ableiten\n └─ local_state.trading_status != ENABLED → BLOCKIERT (markiere FAILED/REJECTED, CONTROL_STATE_*)\n │\n ▼\n3. [Final-Check] resp = M15 GET /control/trading-state (Time out: 800ms, Retry: 1×)\n ├─ Timeout/5xx → FAIL-CLOSED → BLOCKIERT (FAILED, CONTROL_STATE_UNKNOWN)\n └─ resp.trading_status != ENABLED → BLOCKIERT (FAILED, CONTROL_STATE_*)\n └─ resp.expires_at <= now → BLOCKIERT (FAILED, CONTROL_STATE_STALE)\n ▼\n4. Zustand grün (ENABLED, frisch, version=resp.state_version)\n → Order in DB (PENDING, CONTROL_FIELDS: version, audit_id, expires)\n ▼\n5. unmittelbar danach: broker.submit_order(request) ← nur zwischen 4 und 5 minimales Fenster\n ▼\n6. Ergebnis verarbeiten (FILLED/REJECTED/FAILED), Audit-Event mit control_* schreiben\n```\n\n### Zeit-/Retry-Parameter (Vorschlag)\n- Final-Check-HTTP-Timeout: **500 ms** (Docker-intern, schnell); Retry: **max 1** sofort.\n- `expires_at`-TTL (M15): **500 ms** (Server-seitig gesetzt).\n- Nach Timeout/unklar: **nicht senden**; Order als `FAILED`/`REJECTED` mit `CONTROL_STATE_UNKNOWN/STALE`.\n- **Kein Auto-Resume**, kein \"dann schicken wir eben ohne Check\".\n\n### Warum REDUCE/CLOSE/CANCEL durchlaufen\n- Ziel des Control ist **Risiko-Management**. Ein HALT darf Risiko niemals einfrieren — im Gegenteil,\n muss das System bei Problemen schneller in der Lage sein, Positionen abzubauen.\n- `REDUCE`/`CLOSE`/`CANCEL` senken/eliminieren Risiko → **immer erlaubt**, unabhängig von M15-Status.\n\n---\n\n## 12. Offene Punkte für die Nutzer-Freigabe\n\n1. **API-Vertrag erweitern** (`state_version`, `expires_at`, `ttl_seconds`, `audit_id`) in M15 (additiv).\n2. **M09-Check-Position:** zwischen Schritt 4 (Order in DB) und 5 (Broker-Submit) — bestätigen.\n3. **TTL-Wert** (`expires_at`-Fenster) festschreiben (Vorschlag 500 ms).\n4. **Kategorisierung** der Order-Aktion in M09 (OPEN/INCREASE/REDUCE/CLOSE/CANCEL) ableiten — M09 muss\n aus dem TRADE_APPROVED die Aktion bestimmen (Vorschlag: über `direction`/Kanten-Art der Entscheidung).\n5. **M03M06-Klassifikation** (Market-Data-Pipeline): ob Einzel-Ausfall dieser Module wirklich HALTED\n auslösen soll, oder nur deren Daten-Staleness — zu finalisieren, beeinflusst WEN oft HALTED greift.\n6. **SPOF-Akzeptanz:** M15 ist für OPEN/INCREASE zwingend (autoritativ). Exit-Pfad bleibt frei.\n7. M15 weiterhin **nicht freigeben**, M16 **nicht beginnen** bis Nutzer-Entscheid.\n\n---\n\n*Dies ist ein Design-Vorschlag. Es wurde kein Code geändert. M15 nicht freigegeben. M16 nicht begonnen.*\n"
},
{
"path": "notes/trading/system-docs/modul-15-m09-anbindung-implementierung.md",
"title": "modul-15-m09-anbindung-implementierung",
"id": "object/42b89fc3-4572-b493-c05e-dfb85a639df2",
"type": "arch",
"role": "implementation",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "ab1af35950f61bc1aa7853d86a2ecb6069e76b2311259d72fdee4fbd4ed55850",
"body": "\n\n# Modul-15 → Modul-09 Control-Anbindung — Implementierung & Live-E2E\n\n**Status: FREIGEGEBEN / PRODUKTIV VERIFIZIERT** (20.08.2026, Nutzer-Bestätigung).\n**Datum:** 20.08.2026 · **Autor:** Rain Ocampo (Hermes)\n**Gilt für:** Modul-09-Execution-Service ↔ Modul-15-Monitoring-Control\n\n> **Freigabe-Hinweis:** Diese Doku dokumentiert den Implementierungs- und E2E-Stand.\n> Die **finale Freigabe** (M15/M09 als FREIGEGEBEN markieren) erfolgte am **20.08.2026**\n> durch Nutzer-Bestätigung. M16 wird **nicht** begonnen.\n\n---\n\n## 1. Umgesetzte Architektur (Variante C)\n\n**Event-Cache als nicht-kritischer Vorfilter + synchroner HTTP-Final-Check als Autorität.**\n\n- **M15** = autoritative Safety-/Trading-Control-Instanz.\n- **M09** sendet neue Orders (OPEN/INCREASE) **nur**, wenn M15 eindeutig `ENABLED` + frisch bestätigt.\n- **REDUCE/CLOSE/CANCEL** (Risikoabbau) sind **immer** erlaubt — unabhängig vom M15-Status.\n\n### Kernregel\n| State | OPEN/INCREASE | REDUCE/CLOSE/CANCEL |\n|-------|---------------|---------------------|\n| `ENABLED` | **ERLAUBT** | erlaubt |\n| `PAUSED` | **VERBOTEN** | erlaubt |\n| `HALTED` | **VERBOTEN** | erlaubt |\n| `UNKNOWN` / M15-down / stale | **VERBOTEN (FAIL-CLOSED)** | erlaubt |\n\n---\n\n## 2. API-Vertrag M15 → M09 (additiv, kein Breaking Change)\n\n### `GET /control/trading-state`\n```json\n{\n \"trading_status\": \"ENABLED\", // ENABLED|PAUSED|HALTED|UNKNOWN\n \"system_status\": \"HEALTHY\",\n \"state_version\": 3, // monoton, je Status-Änderung inkrementiert\n \"timestamp\": \"2026-08-20T15:30:00Z\",\n \"expires_at\": \"2026-08-20T15:30:00.5Z\", // TTL-Horizont (now + 500ms)\n \"ttl_seconds\": 0.5,\n \"audit_id\": \"ctl-...\" // M15-interne Audit-Kennung\n}\n```\nNur `trading_status=ENABLED` **und** `expires_at` in der Zukunft = eindeutige Erlaubnis.\nTimeout/5xx/503 → **FAIL-CLOSED** (als UNKNOWN behandeln).\n\n### Event-Kanal (Event-Cache)\n- Exchange `market.control`, Routing-Key `trading.state` (nur bei Status-Änderung).\n- M09-Consumer `ControlCacheConsumer` speist den Event-Cache-Vorfilter.\n- Cache-TTL: 5 s. Stale-Cache → als UNKNOWN behandeln → OPEN/INCREASE blockiert.\n\n---\n\n## 3. M09-Implementierung\n\n### Neue Dateien\n- `app/control/client.py` — **ControlClient** (Event-Cache-Vorfilter + HTTP-Final-Check, FAIL-CLOSED)\n- `app/control/__init__.py`\n- `app/consumer/control_consumer.py` — **ControlCacheConsumer** (`market.control`/`trading.state`)\n\n### Geänderte Dateien\n- `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\n- `app/core/models.py` — `ExecutionOrder` + Audit-Felder\n- `app/config.py` — M15-URL/Timeout/TTL/Cache-TTL + Control-Exchange/Routing-Key\n- `app/storage/storage.py` — INSERT/UPDATE um control_*-Felder; `update_control_audit`\n- `migrations/001_execution.sql` — Audit-Spalten (idempotent via `ADD COLUMN IF NOT EXISTS`)\n\n### Audit-Felder je Order\n`action_type`, `control_status`, `control_state_version`, `control_expires_at`,\n`control_audit_id`, `control_check_source` (`http`|`cache`|`bypass`|`none`).\n\n### Parameter\n- Final-Check-HTTP-Timeout: **500 ms**, Retry: **max 1** sofort.\n- `expires_at`-TTL (M15): **500 ms**.\n- Cache-TTL (M09): **5 s**.\n- Kein Auto-Resume; kein \"Senden ohne Check\".\n\n---\n\n## 4. Live-E2E-Ergebnisse (VPS, 20.08.2026)\n\nAlle Szenarien auf dem VPS (187.124.31.123) live ausgeführt. **Alle grün.**\n\n| # | Szenario | Ergebnis |\n|---|----------|----------|\n| S1 | **ENABLED-Pfad**: OPEN-Order durch, Auditfelder in DB | **9/9 PASS** |\n| S2 | **HALTED-Pfad**: M03 gestoppt → OPEN blockiert, kein Broker-Send, reason `CONTROL_HALTED` | **9/9 PASS** |\n| S3 | **PAUSED-Pfad**: nicht-krit. Service down → OPEN blockiert, reason `CONTROL_PAUSED` | **4/4 PASS** |\n| S4 | **Risikoabbau bei HALTED**: REDUCE/CLOSE/CANCEL erlaubt (bypass) | **4/4 PASS** |\n| S5 | **M15-down**: OPEN FAIL-CLOSED (`CONTROL_UNREACHABLE`), REDUCE/CLOSE/CANCEL erlaubt | **7/7 PASS** |\n| S6 | **Cache-vs-Final-Check**: HTTP HALTED gewinnt über Cache | **2/2 PASS** |\n| S7 | **Race/TTL**: Doppel-Publish → keine Doppelorder (Idempotenz) | **2/2 PASS** |\n| S8 | **Event-Cache**: 1 Consumer, 0 Backlog, Reconnect nach RabbitMQ-Restart | **verifiziert** |\n| S9 | **Recovery**: nach Behebung wieder ENABLED, OPEN erlaubt | **3/3 PASS** |\n| S10 | **Health/Security**: health/ready 200, keine öffentl. Ports, keine Secrets, keine echten Tracebacks | **verifiziert** |\n| S11 | **Regression**: bestehender M09-Paper-E2E (Kette 03→09) | **ALLE CHECKS BESTANDEN** |\n\n### S1-Detail (ENABLED)\n```\ncontrol_status=ENABLED\ncontrol_state_version=1\ncontrol_expires_at=1787241066.98\ncontrol_audit_id=ctl-2073194e651040db\ncontrol_check_source=http\naction_type=OPEN\nbroker_order_id=PAPER-EXEC-... (Broker-Send erfolgt)\nstatus=FILLED\n```\n\n### S2-Detail (HALTED)\n```\ncontrol_status=HALTED\ncontrol_state_version=2\ncontrol_audit_id=ctl-b2cd6303f8754328\ncontrol_check_source=http\naction_type=OPEN\nbroker_order_id=None (KEIN Broker-Send)\nreason_codes=['CONTROL_HALTED']\nstatus=FAILED\n```\n\n### S5-Detail (M15-down)\n```\nreason_codes=['CONTROL_UNREACHABLE']\nbroker_order_id=None (FAIL-CLOSED, kein Broker-Send)\nREDUCE/CLOSE/CANCEL → control_check_source=bypass (erlaubt)\n```\n\n---\n\n## 5. Verifikation Health/Security\n\n- M09 + M15 `health` und `health/ready` → **200**.\n- Keine öffentlichen Ports (nur Docker-intern via `expose:`).\n- Keine Secrets in Logs/API/DB.\n- Tracebacks in Logs: ausschließlich pika `AMQPConnectionError` während des\n kontrollierten RabbitMQ-Restarts (erwartete Reconnect-Logs, keine echten Fehler).\n\n---\n\n## 6. Freigabe-Status\n\n- **FREIGEGEBEN / PRODUKTIV VERIFIZIERT** (20.08.2026, Nutzer-Bestätigung).\n- M15/M09-Control-Anbindung ist **final freigegeben** — keine weiteren technischen Änderungen an M09/M15.\n- M16: **nicht beginnen**.\n- Credential-Cleanup: temporäre Forgejo-Tokens/Deploy-Keys → count=0 (siehe Git-Commit).\n\n---\n\n*Implementierung + Live-E2E abgeschlossen. Finale Freigabe erteilt (20.08.2026).*\n"
},
{
"path": "notes/trading/system-docs/modul-15-monitoring-control.md",
"title": "modul-15-monitoring-control",
"id": "object/a50e821b-9c56-fe60-7b46-a0b607ef9e00",
"type": "arch",
"role": "module",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "a6502b4789ab5cd2e6610234e73b99229d034ac46af8f3ed2ca2b72d90b5101d",
"body": "\n\n# Modul-15: Monitoring-Control\n\n**Status: FREIGEGEBEN** (20.08.2026) · Container `Modul-15-Monitoring-Control` · Image `monitoring-control:0.1.0`\n\n## Zweck\nZentrale **technische Überwachung und Sicherheits-/Kontrollebene** des Trading-Systems.\nÜberwacht die Dienste M03M14 (Health/Readiness/Erreichbarkeit) plus RabbitMQ-Queues/Consumer\nund PostgreSQL und leitet daraus deterministisch einen **globalen Trading-Status**\n(`ENABLED` / `PAUSED` / `HALTED` / `UNKNOWN`) ab.\n\n**Restriktionen (Nutzer-Vorgabe):**\n- **Keine KI/ML**, **keine Trading-Entscheidungen**, **keine Brokerorders**, **keine Shell-/Docker-Control-Rechte**.\n- **FAIL-CLOSED:** Bei unklarem/kritischem Zustand wird Trading sicher blockiert (`HALTED`).\n- M15 unerreichbar → M09 behandelt den Zustand als `HALTED` (FAIL-CLOSED, siehe M15→M09-Anbindung, FREIGEGEBEN).\n- **Keine Event-Schleifen:** Events nur bei Status-Änderung, dedupliziert.\n- Container wird **nicht** automatisch neu gestartet/gekillt — M15 beobachtet und entscheidet Status.\n- Port 55015 **nur Docker-intern** (`expose`, kein Host-Port).\n\n## Architektur / Datenfluss\n```\nM03M14 (health/ready) RabbitMQ (Queues) PostgreSQL\n │ │ │\n └──────────┬────────────┴──────────────┘\n ▼\n Modul-15-Monitoring-Control\n ├── probe/ MonitoringProbe (HTTP-health + RMQ passive declare + PG)\n ├── rules/ MonitoringRules (deterministisch, FAIL-CLOSED, versioniert)\n ├── core/ MonitorService (Status-Engine, Single Source of Truth)\n ├── storage/ Persistenz (PostgreSQL: system_health/service_health/control_event)\n ├── publisher/ ControlPublisher (market.control, nur bei Status-Änderung)\n └── api/main.py REST-API (intern, Port 55015)\n```\nStatus → `system_health` (append), Services → `service_health` (upsert), Änderungen → `control_event` (append-only Audit).\n\n## API (Docker-intern, Port 55015, kein öffentliches Port-Mapping)\n```\nGET /health liveness\nGET /health/ready readiness (postgresql)\nGET /monitoring/status aktueller System-Status + Trading-Status (internes State-Modell)\nGET /monitoring/services aktueller Zustand aller überwachten Services/Queues\nGET /control/status Control-Status (manual_override, control_api_enabled)\nGET /control/trading-state autoritativer Trading-Status für M09\nPOST /control manuelle Control PAUSE/HALT/RESUME (nur wenn CONTROL_API_ENABLED=true)\n```\n\n## Deterministische Regeln (Rules-Engine, Version 0.1.0)\nPräzedenz strikt von oben nach unten (FAIL-CLOSED):\n1. **UNKNOWN** — PostgreSQL nicht erreichbar → nicht verlässlich → `HALTED`.\n2. **UNHEALTHY/HALTED** — kritische Infrastruktur (PG/RabbitMQ) oder Execution-Pfad down.\n3. **DEGRADED/PAUSED** — nicht-kritische Dienste down / stale.\n4. **HEALTHY/ENABLED** — alles gut.\n\n| Bedingung | System | Trading | Reason-Code |\n|---|---|---|---|\n| PostgreSQL nicht erreichbar | UNHEALTHY | HALTED | `POSTGRESQL_UNREACHABLE` (+ `PERSISTENCE_FAILURE`) |\n| RabbitMQ nicht erreichbar | UNHEALTHY | HALTED | `RABBITMQ_UNREACHABLE` |\n| Kritischer Service down (M03M09) | UNHEALTHY | HALTED | `CRITICAL_SERVICE_DOWN:<id>` |\n| Market Data stale (> max_age) | UNHEALTHY | HALTED | `MARKET_DATA_STALE` |\n| Kritische Queue Consumer fehlt | — | HALTED | `NO_CONSUMER_<queue>` |\n| Kritischer Queue-Backlog ≥1000 | — | HALTED | `QUEUE_BACKLOG_CRIT:<queue>:<n>` |\n| Nicht-kritische Unhealthy (Analytics/Notification) | DEGRADED | PAUSED | `CRITICAL_SERVICE_DOWN`/`NO_CONSUMER_…` |\n| Queue-Backlog ≥100 / Warning | DEGRADED | PAUSED | `QUEUE_BACKLOG:<queue>:<n>` |\n| Alles healthy | HEALTHY | ENABLED | — |\n\n**FAIL-CLOSED-Hinweis:** `UNKNOWN` Trading-Status wird in der API auf `HALTED` gemappt (nie ENABLED aus unklarem Zustand).\n\n## Events (market.control Exchange, keine Event-Schleife)\nEvents werden **nur bei Status-Änderung** publiziert (Idempotenz) — nie bei jedem Poll:\n\n| Event-Typ | Routing-Key | Anlass |\n|---|---|---|\n| `system.degraded` | `system.degraded` | System → DEGRADED |\n| `system.halted` | `system.halted` | System → UNHEALTHY (HALT) |\n| `system.recovered` | `system.recovered` | System → HEALTHY |\n| `trading.state` | `trading.state` | Trading-Status-Änderung |\n| `service.unhealthy` | `service.unhealthy` | Einzelner Service UNHEALTHY |\n\n## Persistenz / Audit (PostgreSQL Modul-01)\n- `system_health` — append-only Zeile pro Beobachtung (aktueller Status + reason_codes).\n- `service_health` — Upsert pro `service_id` (aktueller Zustand jedes Services/Queue).\n- `control_event` — **append-only Audit-Trail** jeder Status-/Trading-Änderung (`STATE_CHANGE`) und manuellen Control-Aktion (`MANUAL_*`). Idempotent — eine Änderung wird **genau einmal** auditiert.\n\n## Manuelle Control (PAUSE/HALT/RESUME)\n- **Nur wenn `CONTROL_API_ENABLED=true`** (Default `false` → manuelle Control über API deaktiviert, nur Regel-getrieben).\n- **RESUME wird verweigert** (`RESUME_BLOCKED_CRITICAL_CAUSE`), solange eine kritische Ursache besteht.\n- Jede manuelle Aktion wird append-only in `control_event` auditiert.\n\n## Sicherheit\n- **Keine Secrets in DB/Logs/Doku/Commits** — Zugangsdaten ausschließlich via Environment (Compose).\n- **Kein Docker-Socket**, **keine Shell-/Docker-Control-Rechte** (CMD=`python main.py`).\n- Port 55015 nur intern (`expose`), `restart: unless-stopped`, non-root (uid 1001).\n- Env: `POSTGRES_*`, `RABBITMQ_*`, `CONTROL_API_ENABLED`.\n\n## Compose / Betrieb\n- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL`, RabbitMQ `Modul-02-RabbitMQ`.\n- Env per Compose, Zugangsdaten nur via ENV (`POSTGRES_*`, `RABBITMQ_*`), nie hart kodiert.\n\n## Unit-Tests\n- `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.\n\n## E2E-Verifikation (VPS, 20.08.2026) — 17 Szenarien\nAlle 17 verifiziert (System-/Trading-Status via Loop, API `/control/trading-state` + `/monitoring/status` konsistent):\n1. **Normalzustand:** `HEALTHY/ENABLED` — API, State-Modell, Loop identisch.\n2. **PG-Ausfall:** `HALTED`, `POSTGRESQL_UNREACHABLE`+`PERSISTENCE_FAILURE`, Audit genau einmal.\n3. **RabbitMQ-Ausfall:** `HALTED`, `RABBITMQ_UNREACHABLE`.\n4. **Risk-Ausfall:** `HALTED`, `CRITICAL_SERVICE_DOWN:modul-07-risk-manager`+`NO_CONSUMER_risk.input`.\n5. **Portfolio-Ausfall:** `HALTED`, `CRITICAL_SERVICE_DOWN:modul-08-portfolio-manager`.\n6. **Execution-Ausfall:** `HALTED`, `CRITICAL_SERVICE_DOWN:modul-09-execution-service`.\n7. **Analytics-Ausfall:** `DEGRADED` + `PAUSED` (Trading nicht vollständig kill).\n8. **Notification-Ausfall:** `DEGRADED` + `PAUSED`.\n9. **Market Data stale:** `HALTED`, `MARKET_DATA_STALE`; Recovery nach frischen Daten.\n10. **Kritischer Consumer fehlt:** HALT-Severity korrekt (NO_CONSUMER auf kritischer Queue).\n11. **Queue-Backlog:** Warning/HALT gemäß Schwelle (100/1000, Unit-getestet).\n12. **Recovery:** Ursache erneut geprüft, kein Auto-RESUME solange Ursache, Status konsistent, kein Event-Spam.\n13. **Manuelle Controls:** mit `CONTROL_API_ENABLED=false` sauber abgelehnt (`control_api_disabled`); Trading unangetastet.\n14. **Idempotenz/Event-Flut:** stabiler Zustand → keine wiederholten SYSTEM_HALTED/SERVICE_UNHEALTHY (nur 1 Audit je Änderung).\n15. **RabbitMQ-Reconnect:** automatisch (alle Recovery-Zyklen), keine Zombies, keine Eventverluste.\n16. **Audit:** `control_event`/`system_health`/`service_health` vollständig + zeitlich nachvollziehbar.\n17. **Security:** kein öffentlicher Port (PortBindings `{}`), kein Docker-Socket, keine Secrets im Compose/Logs.\n\n## Bekannte Design-Hinweise (für separaten M15→M09-Vorschlag)\n- **M03M06 (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).\n- **DEGRADED → `PAUSED`** (nicht `ENABLED`): bei Analytics/Notification-Ausfall wird Trading vorsichtig gestoppt, nicht vollständig killed. Design-Entscheidung, im Vorschlag benannt.\n- **PG-Ausfall ⇒ `PERSISTENCE_FAILURE`**: ohne PG kann `control_event`-Audit nicht geschrieben werden (fachlich korrekt FAIL-CLOSED), aber Audit-Lücke im Fehlerfenster.\n\n## Freigabe\n- **FREIGEGEBEN** (20.08.2026, Nutzer-Bestätigung). Implementierung + E2E (17 Szenarien) grün.\n- M09-Anbindung **umgesetzt** (Variante C, Event-Cache + HTTP-Final-Check) — siehe `modul-15-m09-anbindung-implementierung.md`, FREIGEGEBEN.\n\n## Nächste Module\n- **Modul-16 ff.** — noch Platzhalter (alpine), NICHT gestartet.\n\n---\n## Geändert\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-15-Monitoring-Control dokumentiert — implementiert + E2E-verifiziert (17 Szenarien grün),\n Rules/API/Events/Audit/Security dokumentiert, NICHT FREIGEGEBEN.\n```\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-15-Monitoring-Control auf FREIGEGEBEN gesetzt (Nutzer-Bestätigung 20.08.2026).\n M15→M09-Anbindung (Variante C) als umgesetzt/FREIGEGEBEN vermerkt; Unit-Tests 19/19.\n```\n"
},
{
"path": "notes/trading/system-docs/modul-16-paperclip.md",
"title": "modul-16-paperclip",
"id": "object/eb0ef2f8-1ed9-aaf1-1afb-2fdbdaa15c74",
"type": "arch",
"role": "module",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "a845f400a7c131620493d3220937663f8a23fb4cf3ebdf889cf33237578b6bf2",
"body": "# Modul-16: Paperclip (Agent-Orchestrierungsebene)\n\n**Status: FREIGEGEBEN (21.08.2026)** · Container `Modul-16-Paperclip` · Image `trading-modules-modul16-paperclip:latest`\n\n> ⚠️ **Achtung Namenskollision:** Es existiert ein **Fremdsystem** `paperclip-dn8l-paperclip-1`\n> (Coolify, Hostinger hvps-paperclip) im `coolify`-Netz. Dies ist **NICHT** unser Modul-16.\n> Unser Modul-16 läuft im `trading-modules`-Netz als `Modul-16-Paperclip`. Fremdsystem nie anfassen.\n\n## Zweck\nPaperclip ist die **übergeordnete Agent-/Orchestrierungsebene** des Trading-Systems. Er nimmt\nResearch-, Analytics-, Strategieüberprüfungs-, Backtest-, Optimization- und Monitoring-Aufträge\nentgegen, wählt eine **Agent-Rolle** (Strategy/Research/Analytics/Validation/System) und führt sie\ndeterministisch aus. Später (V2) können daraus Hermes-Agent-Aufgaben entstehen.\n\n**Kritische Grenze:** Paperclip ist **NICHT Teil des kritischen Trading-Pfads.**\n- ✅ Er liest READ-ONLY aus Modul-0315 und startet klar begrenzte, nicht-kritische Writes (Backtest, Optimization).\n- ❌ Er DARF KEINE Broker-Orders senden, Modul-09 nicht zur Orderausführung anweisen, Risk-/Portfolio-Regeln nicht\n überschreiben, M15-Safety nicht überschreiben, Strategien nicht automatisch produktiv aktivieren,\n Optimierungsparameter nicht automatisch deployen.\n- **Trading-Core (M0315) läuft bei komplettem Ausfall von Modul-16 unverändert weiter.**\n\n## Architektur / Datenfluss\n```\nClient/Auftrag\n │ POST /tasks (task_type + payload)\n ▼\nModul-16-Paperclip (Port 55016, NUR intern/expose)\n │\n ├── app/core/orchestrator.py Rollen-Wahl + deterministische Ausführung\n ├── app/core/proposal_gate.py Proposal-Gate (NIE Auto-APPROVED/aktiviert)\n ├── app/storage/storage.py Persistenz agent_task/agent_run/agent_proposal/agent_audit\n ├── app/clients/clients.py Read-Clients + 2 begrenzte Write-Clients\n └── app/api/main.py REST-API (/health, /tasks, /proposals)\n │\n ├── Read-Only (tolerant): Modul-03/04/06 (research), M05+M11 (strategy_review),\n │ M11 (analytics), M15 (monitoring, NUR GET)\n └── Write (begrenzt): Modul-12 POST /backtest, Modul-13 POST /optimization\n```\n\n**API (Docker-intern, Port 55016):**\n- `GET /health` — Liveness (`{\"status\":\"ok\",\"service\":\"paperclip\",\"version\":\"0.1.0\"}`)\n- `GET /health/ready` — Readiness (DB + Schema vorhanden) → 200 `{\"status\":\"ready\"}`\n- `POST /tasks` — Auftrag einreichen (validiert; unbekannte/`execute_order` → 422)\n- `GET /tasks` / `GET /tasks/{id}` — Aufträge + Status/result_refs\n- `POST /tasks/{id}/run` — Aufgabe synchron ausführen (erzeugt `agent_run`)\n- `POST /proposals` — Proposal als DRAFT erzeugen (APPROVED via Paperclip → 422)\n- `GET /proposals` / `GET /proposals/{id}`\n\nKein RabbitMQ: synchroner request/response. Port 55016 ist **nur intern** (`expose:`), kein öffentliches Mapping.\n\n## Agent-Rollen (V1, vorbereitet)\n| Rolle | task_type | liest | schreibt |\n|-------|-----------|-------|----------|\n| `analytics` | `analytics` | Modul-11 (portfolio/strategy/regime) | — |\n| `research` | `research` | Modul-03/04/06 | — |\n| `strategy` | `strategy_review` | Modul-05 (Strategien) + Modul-11 | — |\n| `validation` | `backtest`, `optimization`, `validation` | Modul-12/13 | M12 POST /backtest, M13 POST /optimization |\n| `system` | `monitoring` | Modul-15 (NUR GET) | — |\n\nAlle Rollen sind per `ENABLED_ROLES` registriert; es werden **nicht unnötig viele aktiviert**.\n`TASK_TO_ROLE` mappt `task_type → Rolle`.\n\n## Proposal-Prinzip (ProposalGate)\nWenn Paperclip eine Änderung empfiehlt, wird sie **als Proposal** gespeichert, niemals direkt angewendet:\n\n```\nSTRATEGY_CHANGE_PROPOSAL mit:\n proposal_id, source/agent, strategy, aktuelle Version, vorgeschlagene Änderung (JSON),\n Begründung (rationale), Analytics-/Backtest-Referenzen, Confidence, created_at, status\nStatus: DRAFT / PROPOSED / REVIEWED / APPROVED / REJECTED\n```\n\n- `create_proposal` läuft ausschließlich über `ProposalGate` (nur `status in {DRAFT, PROPOSED}` erlaubt).\n- Paperclip kann `APPROVED` **NIE selbst setzen** (validiert in `proposal_gate.py`; API gibt 422).\n- Proposal wird mit `auto_activated=false` persistiert — **keine automatische Aktivierung**.\n- **Keine automatische Übernahme optimierter Parameter** in Produktion.\n\n## Sicherheit\n- **Keine öffentlichen Ports**: 55016 nur `expose` (Docker-intern), kein Host-Mapping.\n- **Internes Docker-Netzwerk** `trading-modules`.\n- **Secrets nur ENV/Secret** (`PG_PASSWORD` aus Compose `environment`); keine Secrets in Logs/API/DB.\n- **Keine Broker-Credentials.**\n- **Kein Docker-Socket-/Root-Recht, kein Shell-Zugriff auf andere Container.**\n- **M15-Ausfall kann Paperclip nicht dazu bringen, Safety zu umgehen** (Paperclip hat KEINE M15-Control-Write-Methode).\n- **Paperclip-Ausfall stoppt Trading nicht** (eigene DB-Tabellen, kein Einfluss auf Trading-Core).\n\n## Persistenz / Audit (Migration `migrations/001_paperclip.sql`)\n- `agent_task` — Aufträge (id, agent_role, task_type, status, payload, result_refs, error_*).\n- `agent_run` — Ausführungen (run_id, task_id, agent_role, status, result, idempotent).\n- `agent_proposal` — Vorschläge (proposal_id, source, strategy, proposed_change, rationale, references, confidence, status, auto_activated).\n- `agent_audit` — lückenloses Audit-Log (actor, action, detail, task_id, run_id, proposal_id).\n\nJede Agent-Aktion (task_created, run_started, run_succeeded, run_failed, run_rejected, proposal_*) wird in `agent_audit` protokolliert.\n\n## Read-Clients (tolerant)\nEinzelne Quellen-Fehler (404/5xx/Timeout) werden über `_safe_get` als `{\"_error\": code, \"_message\": …}`\nTeil-Ergebnis gekapselt → der Gesamt-Task bleibt SUCCEEDED. Nur wenn ein Service **komplett unreachable**\n(`UNREACHABLE`/`TIMEOUT`) ist, liefert er `None`. Write-Aktionen (Backtest/Optimization) bleiben strikt FAILED bei Fehlern.\n\n**Ausnahme — Monitoring (FAIL-CLOSED):** Der `monitoring`-Task ruft M15 **nicht** über `_safe_get`,\nsondern direkt auf. M15 ist die autoritative Trading-Status-Quelle — ist sie nicht erreichbar, wird der\nMonitoring-Task **FAILED** (kein „ok\"-Status bei ausgefallenem M15). Bei M12/M13-Ausfall → betroffener\nTask (Backtest/Optimization) FAILED, M16 bleibt healthy, Trading-Core unbeeinflusst.\n\n## Tests & E2E-Verifikation (VPS, 21.08.2026)\n- **Unit-Tests** `tests/test_paperclip.py`: 10/10 grün (ProposalGate-Safety, Task-Validierung, Worker, Idempotenz, Auto-Aktivierungs-Verbot).\n- **Backtest-Flow**: M16 → M12 `POST /backtest` → **SUCCEEDED**, `agent_run` + `run_id`/`result_refs` korrekt (`kind:\"backtest\"`).\n- **Optimization-Flow**: M16 → M13 `POST /optimization` → **SUCCEEDED**, `run_id`/`result_refs` korrekt (`kind:\"optimization\"`).\n- **Monitoring-Flow**: M16 → M15 `/monitoring/status` (READ-ONLY) → **SUCCEEDED**, Ergebnis gespeichert.\n- **Research-Flow**: M16 → M03/M04/M06 → **SUCCEEDED** (M04 404 „Kein Regime\" als Teil-Ergebnis gekapselt, kein Crash).\n- **Strategy-Review-Flow**: M16 → M05+M11 → **SUCCEEDED**.\n- **Proposal**: erstellt als `DRAFT`/`auto_activated=false`; `APPROVED` → 422. **NICHT auto-aktiviert.**\n- **Kein M09-Zugriff**: `task_type=execute_order` → 422; kein `ExecutionClient`, keine `/order`-Endpunkte, kein `execution_base_url`.\n- **Kein M15-Override**: Paperclip hat nur GET-Methoden auf M15, keine Control-Write.\n- **Ausfall-Toleranz**: M12 down → backtest FAILED, M13 down → optimization FAILED, M15 down → monitoring\n **FAILED (FAIL-CLOSED)**, M15 up → SUCCEEDED. M16 bleibt in allen Fällen **healthy**, Trading-Core unbeeinflusst,\n Erholung → SUCCEEDED.\n- **Idempotenz**: erneuter `run` erzeugt neuen `agent_run`, überschreibt frühere Runs nicht.\n- **Ungültige Aufgabe**: `task_type=delete_database` → 422.\n- **Keine Secrets in Logs/API**: `docker logs` frei von password/secret/token/api_key.\n- **Health/Ready**: beide 200.\n\n## Bugs / Fixes (modul-16, dokumentiert)\n1. **`references`-SQL-Keyword** → Spalte in Migration + Queries als `\"references\"` gequoted.\n2. **Migration-Commit**: psycopg2 braucht `conn.autocommit = True` (Muster wie M15), sonst bleiben Tabellen\n uncommittet → `relation agent_task does not exist` bei `/health/ready`. Behoben in `_connect`.\n3. **create_task RETURNING-Mismatch**: RETURNING listete 7 Spalten, `_task_row` erwartete 10 →\n `IndexError`. Behoben: vollständige Spaltenliste.\n4. **Research-Einzelfehler-Crash**: Einzelne fehlende Quelle (M04 404) failte den ganzen Research-Task.\n Behoben: `_safe_get` kapselt Einzelquellen-Fehler, Task bleibt SUCCEEDED mit Teil-Ergebnissen.\n5. **Monitoring-FAIL-CLOSED (21.08.2026)**: `monitoring`-Task nutzte `_safe_get`, was M15-`UNREACHABLE`\n zu `None` kapselte → Task blieb SUCCEEDED trotz ausgefallenem M15. Fix: `_monitoring` ruft M15 **direkt**\n auf, damit ein M15-Ausfall als `ClientError` weiterfaellt und der Task **FAILED** wird. Verifiziert:\n M15 down → monitoring FAILED, M15 up → SUCCEEDED.\n\n## Compose / Betrieb\n- Netzwerk `trading-modules`, DB-Host `Modul-01-PostgreSQL` (Modul-01), Port 55016 nur intern (`expose`),\n `restart: unless-stopped`. Kein öffentliches Port-Mapping.\n- **Kein Teil des kritischen Trading-Pfads:** kein M09-, kein M15-Control-Zugriff.\n\n---\n## Geändert\n```\nGeändert von: Rain Ocampo\nDatum: 21.08.2026\nGrund: Modul-16-Paperclip implementiert + E2E verifiziert (Backtest/Optimization/Monitoring/Research/Strategy-Success, Safety 422, ProposalGate, Ausfall-toleranz, Idempotenz). Monitoring-FAIL-CLOSED-Fix: M15-Ausfall -> monitoring-Task FAILED statt SUCCEEDED. Ausfall-Toleranz-E2E (M12/M13/M15 down -> FAILED, healthy) gruen. Image neu gebaut (Fix persistent). FREIGEGEBEN. Modul-17 NICHT begonnen.\n```\n"
},
{
"path": "notes/trading/system-docs/modul-17-hermes-agent.md",
"title": "modul-17-hermes-agent",
"id": "object/3063a680-c35e-a62c-91b4-f382027930a5",
"type": "arch",
"role": "module",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "40731e36ce326e07234872d26bfafc2a345afef6637528c406896be3b85e8c98",
"body": "# Modul-17: Hermes-Agent (autonomer Research-/Analyse-Worker unter Paperclip)\n\n**Status: FREIGEGEBEN (21.08.2026)** · Container `Modul-17-Hermes-Agent` · Image `trading-modules-modul17-hermes-agent:latest`\n\n> ⚠️ **Keine Namenskollision** mit fremden Containern. Hermes läuft im `trading-modules`-Netz\n> als `Modul-17-Hermes-Agent`. Fremdsysteme (`paperclip-dn8l-paperclip-1` u.ä., Coolify) nie anfassen.\n\n## Zweck & Rolle\nHermes ist die **autonome Research-/Analyse-Ausführungsebene unter Paperclip (M16)**. Er nimmt\nvon Paperclip delegierte Tasks (Research, längere Analysen, Strategie-Review, Analytics-Auswertung,\nBacktest-/Optimization-Auswertung, Systemdiagnose READ-ONLY) entgegen und führt sie **ausschließlich\nüber die strikte Tool-Whitelist** aus. Das Ergebnis geht als `agent_run`/`result_refs` an Paperclip\nzurück.\n\n**Kritische Grenze — Hermes ist NICHT Teil des kritischen Trading-Pfads und darf NICHT:**\n- direkt auf Modul-09 Execution zugreifen, Orders erzeugen\n- Risk-/Portfolio-Regeln (M07/M08) überschreiben\n- M15-Control verändern (PAUSE/HALT/RESUME)\n- Strategien automatisch produktiv aktivieren\n- Optimierungsparameter automatisch deployen\n- Broker-Credentials erhalten\n\n**Paperclip bleibt Orchestrator.** Hermes arbeitet NUR über die erlaubten Tools/APIs. Unbekannte\noder unerlaubte Tools → **REJECTED**.\n\n## Architektur / Datenfluss\n```\nPaperclip (M16) ── Task ──▶ Modul-17-Hermes-Agent (Port 55017, NUR intern/expose)\n │\n ├── app/core/tool_whitelist.py strikte Tool-Whitelist (Herzstück)\n ├── app/core/executor.py Hermes-Executor (Tool-Ausführung + Audit + Idempotenz)\n ├── app/storage/storage.py Persistenz hermes_task/run/tool_call/result\n ├── app/clients/clients.py Read-Clients + 2 begrenzte Write-Clients\n └── app/api/main.py REST-API (/health, /tasks, /runs, /tools)\n │\n ├── READ (tolerant): Modul-03 Market Data, M04 Regime, M05 Strategy,\n │ M06 Ranking, M10 Journal, M11 Analytics, M15 Monitoring (GET)\n └── WRITE (begrenzt): Modul-12 POST /backtest, Modul-13 POST /optimization,\n Proposal/Research-Ergebnis speichern\n```\n\n**API (Docker-intern, Port 55017):**\n- `GET /health` — Liveness (`{\"status\":\"ok\",\"service\":\"hermes\",\"version\":\"0.1.0\"}`)\n- `GET /health/ready` — Readiness (DB-Schema vorhanden) → 200 `{\"status\":\"ready\"}`\n- `POST /tasks` — Task einreichen (`task_type` + `payload.tools[]`); Whitelist-Prüfung\n- `GET /tasks` / `GET /tasks/{id}` — Tasks + Status/result_refs\n- `GET /runs/{id}` — einzelner Run mit result_refs\n- `GET /tools` — Tool-Whitelist auflisten\n\nKein RabbitMQ: synchroner request/response. Port 55017 **nur intern** (`expose:`), kein öffentliches Mapping.\n\n## Tool-Whitelist (strikt, `app/core/tool_whitelist.py`)\nJeder Tool-Call wird gegen die Whitelist geprüft. Unbekannte/unerlaubte Tools → REJECTED.\n\n| Tool (namespace.method) | Zielmodul | Richtung | Status |\n|-------------------------|-----------|----------|--------|\n| `market_data.prices` | Modul-03 | READ | erlaubt |\n| `regime.analyze` | Modul-04 | READ | erlaubt |\n| `strategy.list` | Modul-05 | READ | erlaubt |\n| `ranking.current` | Modul-06 | READ | erlaubt |\n| `journal.entries` | Modul-10 | READ | erlaubt |\n| `analytics.portfolio` | Modul-11 | READ | erlaubt |\n| `monitoring.status` | Modul-15 | READ | erlaubt (NUR GET) |\n| `backtest.start` | Modul-12 | WRITE | erlaubt (CONTROLLED) |\n| `optimization.start` | Modul-13 | WRITE | erlaubt (CONTROLLED) |\n| `execution.place_order` | Modul-09 | WRITE | **REJECTED** |\n| `risk.set_limits` | Modul-07 | WRITE | **REJECTED** |\n| `portfolio.rebalance` | Modul-08 | WRITE | **REJECTED** |\n| `monitoring.pause/halt/resume` | Modul-15 | Control | **REJECTED** |\n| alles andere | — | — | **REJECTED** |\n\n**M09/M07/M08 sind strukturell NICHT in den Clients implementiert** (`clients.py`) — ein Verstoß\nist damit zur Laufzeit unmöglich, zusätzlich zur Whitelist-Barriere. M15-Control-Writes fehlen ebenfalls.\n\n## Persistenz / Audit (Migration `migrations/001_hermes.sql`)\nVier Tabellen in Modul-01-PostgreSQL (PostgreSQL-01), NICHT im kritischen Trading-Pfad:\n- `hermes_task` — Aufgaben (id, task_type, agent_role, status, payload, result_refs, error_code)\n- `hermes_run` — Ausführungen (run_id, task_id, status, result, result_refs, idempotent)\n- `hermes_tool_call` — **jeder Tool-Call nachvollziehbar**: task_id, tool, target_module, input_hash,\n status, timestamps, error_code\n- `hermes_result` — gespeicherte Ergebnisse/Reports\n\nJeder Tool-Call wird in `hermes_tool_call` auditier. Keine Secrets/Prompts mit Credentials in Logs.\n\n## Sicherheit\n- **Keine öffentlichen Ports**: 55017 nur `expose` (Docker-intern), kein Host-Mapping (verifiziert: `docker port` leer).\n- **Internes Docker-Netzwerk** `trading-modules`.\n- **Kein Docker-Socket, kein Root-/Shell-Zugriff auf andere Container.**\n- **Secrets nur ENV/Secret** (`PG_PASSWORD` aus Compose `environment`); keine Secrets in Logs/API/DB.\n- **Keine Broker-Credentials** (kein Client, kein ENV dafür).\n- **Hermes-Ausfall beeinflusst M0316 NICHT** (eigene DB-Tabellen, keine Kopplung an Trading-Core).\n- **M15-Ausfall → Monitoring-/Systemdiagnose-Task FAILED** (FAIL-CLOSED, nicht schönreden).\n\n## Read-Clients (tolerant)\nEinzelne Quellen-Fehler (404/5xx/Timeout) werden als Teil-Ergebnis `{\"_error\": code}` gekapselt,\nder Gesamt-Task bleibt SUCCEEDED. Komplett unreachable (`UNREACHABLE`/`TIMEOUT`) → `None`.\n\n**Ausnahme — Monitoring (FAIL-CLOSED):** Der `monitoring.status`-Call ruft M15 direkt auf. Ist M15\nnicht erreichbar → Task **FAILED** (kein \"ok\" bei ausgefallenem M15). Gleiches gilt für M12/M13:\nAusfall → zugehöriger Backtest-/Optimization-Task **FAILED** (`UNREACHABLE`), Hermes bleibt healthy,\nTrading-Core unbeeinflusst.\n\n## Idempotenz\nJeder `task_id` erzeugt genau **einen** `hermes_run` (SUCCEEDED). Wiederholte Einreichung derselben\nTask-Payload → je Task genau 1 Run, kein Doppel-Run. `input_hash` in `hermes_tool_call` identifiziert\nidentische Aufrufe.\n\n## Tests & E2E-Verifikation (VPS, 21.08.2026)\n- **Unit-Tests** `tests/test_hermes.py`: 13/13 grün (Whitelist-Gates, Executor, Idempotenz, REJECTED).\n- **T1 Research-Task** → M03 → **SUCCEEDED**, result_refs mit `tool_output` + `report`.\n- **T2 Analytics-Task** → M11 → **SUCCEEDED**.\n- **T3 Backtest-Task** → M12 `POST /backtest` (BT_PULL, stock, 450 Candles) → **SUCCEEDED**.\n- **T4 Optimization-Task** → M13 `POST /optimization` (BT_PULL) → **SUCCEEDED**.\n- **T5 Monitoring READ-ONLY** → M15 `GET /status` → **SUCCEEDED**.\n- **T6 Execution-Tool** (`execution.place_order`) → **REJECTED** (`TOOL_NOT_ALLOWED`).\n- **T7 M15-Control-Tool** (`monitoring.pause`) → **REJECTED**.\n- **T8 Risk/Portfolio-Write** (`risk.set_limits`) → **REJECTED**.\n- **T9 ungültiges Tool** (`bogus.tool`) → **REJECTED**.\n- **T10 Ausfall**: M15 down → Monitoring FAILED; M12 down → Backtest FAILED; M13 down → Optimization\n FAILED (jeweils `UNREACHABLE`). Erholung → SUCCEEDED. Hermes bleibt healthy.\n- **T11 Idempotenz**: je Task genau 1 Run.\n- **T12 Restart/Reconnect**: nach `docker restart` ready, Daten persistent.\n- **T13 keine Secrets**: `docker logs` frei von password/secret/token/api_key.\n- **T14 Port nur intern**: `ss -ltn` kein Host-Listening auf 55017.\n- **T15 Hermes-Ausfall**: bei gestopptem Hermes sind M03/04/05/11/12/13/15/16 alle healthy\n (`/health` ok) → Trading-Core unbeeinflusst.\n\n**E2E-Gesamt: 15/15 grün.**\n\n## Bugs / Fixes (modul-17, dokumentiert)\n1. **Methodenname `_last_succeeded_run` vs `_run_last_succeeded_run`**: Executor referenzierte\n `_last_succeeded_run`, Methode hieß anders → benannt in `_last_succeeded_run` (Idempotenz-Check).\n2. **Test-Pfad**: `sys.path.insert` musste auf `../app` zeigen (nicht Modul-Root) für `app.*`-Importe.\n3. **M12 422 \"Unzureichende Daten\"**: EURUSD/demo lieferte nur 1 Candlestick (< 220 nötig). Fix im\n E2E: Symbol **`BT_PULL`** (asset_class `stock`, 450 Candles) für Backtest/Optimization.\n4. **M13 erfordert Pflichtfelder `start_date`+`end_date`** (anders als M12): Optimization-Payload\n musste beide Datumsfelder enthalten, sonst HTTP 422.\n5. **E2E-Skript**: BusyBox wget im Alpine-Worker kennt kein `--post-file` → JSON per `docker cp`\n + `--post-data=\"$(cat …)\"`. Funktionsname `mkpayload`→`mk_put` (aufrufender Code nutzte `mk_put`).\n6. **SSH-Quoting**: verschachtelte `$(…)` mit doppelten Anführungszeichen im Inline-Python kappen\n das Kommando → Inline-Echo entfernt, reiner grep-Check.\n\n## Compose / Betrieb\n- Netz `trading-modules`, DB-Host `Modul-01-PostgreSQL` (Modul-01), Port 55017 nur intern (`expose`),\n `restart: unless-stopped`. Kein öffentliches Port-Mapping.\n- **Kein Teil des kritischen Trading-Pfads:** kein M09-, kein M07/M08-, kein M15-Control-Zugriff.\n- Paperclip (M16) bleibt Orchestrator; Hermes ist seine Worker-Ausführungsebene.\n\n---\n\n## LLM-Schicht (KI-/Analyse-Erweiterung, M16 additiv `llm=true`)\nHermes kann Tasks zusätzlich über eine **LLM-Schicht** (Tool-Calling-Loop) ausführen. Die LLM-Schicht\nist **additiv** (M16 setzt `llm=true` im Task-Payload); ohne Flag bleibt das deterministische M17-Verhalten\nunverändert. Die Tool-Whitelist und Task-Gates bleiben **deterministisch außerhalb des LLM** — das LLM\nführt nie selbst Tools aus, sondern liefert `requested_tools[]`, die durch die bestehende Whitelist geprüft werden.\n\n### 2-Stufen-Modellkonzept (final, 21.08.2026)\n| Stufe | Modell | Provider | Verwendung |\n|-------|--------|----------|------------|\n| **STANDARD** | `deepseek-v4-flash:cloud` | `ollama_cloud` | Normale Research-/Analyse-Tasks, voller Tool-Calling-Loop, Structured JSON, vollständiger Audit |\n| **LOKAL** | `llama3.1:8b` | `ollama_local` | NUR expliziter Privacy-/Offline-Modus (`llm_mode=local`), **Analyse-only** (kein Tool-Loop), CPU-/Timeout-Schutz |\n\n**Regeln (hart):**\n- **KEIN automatischer Cloud→Local-Fallback.** Cloud-Ausfall → Task **FAILED** (`LLM_UNAVAILABLE`),\n kein minutenlanger Wechsel auf das lokale Modell.\n- **LOKAL nur explizit** über `llm_mode=local` im Task-Payload (`extra.llm_mode`). Nicht als Standard verwenden.\n- **LOKAL = Analyse-only**: genau EIN Analyse-Call, keine Tool-Anfragen. `llama3.1:8b` auf CPU versteht\n den Tool-Calling-Loop nicht zuverlässig (LLM_MAX_ROUNDS) und ist langsam (~60-90s/Call).\n- **FAIL-CLOSED**: LLM down/Timeout/invalid JSON/Schema/Max-Runden → Task FAILED, kein Trading-Core-Effekt.\n- **Safety unverändert**: kein `execution.place_order`, kein `risk.set_limits`, kein `portfolio.rebalance`,\n kein M15 pause/halt/resume, Proposal maximal DRAFT/PROPOSED.\n\n### LLM-Audit (`hermes_llm_run`, Migration `002_hermes_llm.sql`)\nFünfte Audit-Tabelle. Je LLM-Run: provider, model, config_version, input_context_hash, requested_tools,\nexecuted_tools, rejected_tools, tokens_in/out, latency_ms, structured_result, status, error_code.\n**Vollständig AUCH im FAILED-Pfad** (Timeout/Invalid-JSON/Max-Runden persistieren Metriken). Keine\nvollständigen sensiblen Prompts, keine Secrets.\n\n### LLM-ENV (Compose M17-Block)\n- `LLM_PROVIDER=ollama_cloud` (Standard = Cloud)\n- `LLM_MODEL_CLOUD=deepseek-v4-flash:cloud`\n- `LLM_MODEL_LOCAL=llama3.1:8b`\n- `LLM_BASE_URL=http://10.0.4.2:11434` (Ollama-Netz)\n- `LLM_TIMEOUT_SECONDS=300`\n\n### LLM-E2E-Verifikation (final, 21.08.2026)\n- **Cloud-Standard** (`llm=true`, standard): **SUCCEEDED** (deepseek-v4-flash:cloud, 4 requested/2 executed, Audit vollständig).\n- **Local explizit** (`llm=true`, `llm_mode=local`): **SUCCEEDED** (llama3.1:8b, Analyse-only, Audit vollständig).\n- **Cloud-down** (isoliert, ungültige Base-URL): **FAILED** (`LLM_UNAVAILABLE`), **kein Auto-Fallback** (Provider bleibt Cloud).\n- **llm=false**: **SUCCEEDED** (deterministisches M17, kein LLM-Run).\n- **Audit vollständig**: requested/executed/rejected/tokens/latency/context_hash persistiert, auch im FAILED-Pfad.\n- **M03M15 unbeeinflusst**: alle Module healthy während der LLM-Tests.\n- **Unit-Tests** `tests/test_llm.py`: **20/20 grün** (inkl. 4 REJECTED-Gate-Tests: place_order, monitoring_pause, risk_set_limits, unknown_tool).\n\n### LLM-Bugs / Fixes\n1. **Audit-Metriken leer (Cloud-E2E)**: `finish_llm_run` schrieb nur status/result/error → um\n requested/executed/rejected_tools, tokens_in/out, latency_ms, structured_result, provider/model/config_version erweitert.\n2. **FAILED-Pfad verlor Audit-Metriken**: Max-Runden-Exception trug keine Metriken → Exception trägt jetzt\n Audit-Dict, `execute()` persistiert sie auch bei LLM_MAX_ROUNDS/Timeout/Invalid-JSON.\n3. **NOT NULL-Constraint**: `requested_tools` etc. `NOT NULL DEFAULT '[]'` → `finish_llm_run` koerziert `None`→`[]`.\n4. **Structured Output**: Cloud-Modell ignoriert Schema ohne `format`-Constraint → `format`-Constraint im Payload.\n5. **ToolCall-Format**: ToolCall-Modell erwartet `name`/`args` (nicht `tool`/`params`) → Skript/Executor angepasst.\n6. **Lokales Modell ungeeignet für Tool-Loop**: `llama3.1:8b` auf CPU → LLM_MAX_ROUNDS (fragt endlos Tools an).\n Fix: lokaler Modus = **Analyse-only** (kein Tool-Loop), wie vom User empfohlen.\n\n## Geändert\n```\nGeändert von: Rain Ocampo\nDatum: 21.08.2026\nGrund: LLM-Schicht (2-Stufen-Konzept) ergaenzt: STANDARD=deepseek-v4-flash:cloud (voller Tool-Loop),\nLOKAL=llama3.1:8b (nur explizit llm_mode=local, Analyse-only), KEIN Auto-Fallback, Cloud-down->FAILED\n(LLM_UNAVAILABLE), llm=false deterministisch, Audit hermes_llm_run vollstaendig (auch FAILED-Pfad),\nUnit 20/20, E2E 5/5 Szenarien gruen, M03-M15 unbeeinflusst. FREIGEGEBEN.\nGeändert von: Rain Ocampo\nDatum: 21.08.2026\nGrund: Modul-17-Hermes-Agent implementiert + E2E verifiziert (Research/Analytics/Backtest/Optimization/Monitoring-Success,\nSafety REJECTED 6/7/8/9, M12/M13/M15-Ausfall -> FAILED (FAIL-CLOSED), Idempotenz, Restart, keine Secrets, Port intern,\nHermes-Ausfall beeinflusst M03-16 nicht). Tool-Whitelist strikt, 4 Audit-Tabellen (hermes_task/run/tool_call/result),\n6 API-Endpunkte. Unit 13/13, E2E 15/15 gruen. FREIGEGEBEN. Keine weiteren Module begonnen.\n```\n"
},
{
"path": "notes/trading/system-docs/modul-26-telegram-gateway.md",
"title": "modul-26-telegram-gateway",
"id": "object/3b41fc80-9d60-e640-a19c-1107114d8495",
"type": "arch",
"role": "module",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "65aee1059f8e3d1e3e16785689f97684153c94af3aaecf0404af58d33e04b38f",
"body": "# Modul-26-Telegram-Gateway\n\n**Status:** FREIGEGEBEN (2026-08-21)\n**Port:** 55026 (nur Docker-intern, kein öffentlicher Port)\n**Container:** `Modul-26-Telegram-Gateway`\n**Image:** `telegram-gateway:0.1.0`\n**Netzwerk:** `trading-modules`\n\n## Zweck\n\nDünner, **tool-loser** Adapter zwischen Telegram und M17 Hermes-Agent. Ermöglicht die Bedienung von Hermes vom Handy über einen eigenen Telegram-Bot. **KEINE Trading-Tools**, kein M09/M07/M08/M15-Control-Zugriff. Safety bleibt deterministisch in M17.\n\n## Architektur\n\n```\nTelegram Bot API\n │ Polling (getUpdates) — KEIN Webhook, kein öffentlicher Port\n ▼\nModul-26-Telegram-Gateway (Port 55026 intern)\n │ Auth-Allowlist, Rate-Limit, Audit — NUR HTTP-Calls an M17, KEINE Tools\n ▼\nModul-17-Hermes-Agent (POST /tasks llm=true)\n │ Tool-Whitelist + Task-Gates (deterministisch, UNVERÄNDERT)\n ▼\nLLM (Cloud deepseek-v4-flash:cloud Standard / Local llama3.1:8b explizit)\n```\n\n## Sicherheitsmodell\n\n- **Allowlist:** Nur explizit erlaubte User-/Chat-IDs (`TELEGRAM_ALLOWED_IDS`, kommagetrennt). Unbekannte User werden **hart abgelehnt** (kein Task, Log-Eintrag, Status `REJECTED`/`UNAUTHORIZED`).\n- **Rate-Limit:** Sliding-Window pro User/Chat (Default 10/min). Überschreitung → `REJECTED`/`RATE_LIMITED`.\n- **Idempotenz:** `update_id` als PK in `telegram_message`. Bereits verarbeitete Updates werden übersprungen.\n- **Secrets:** Bot-Token NUR via ENV/Secret (`TELEGRAM_BOT_TOKEN`), nie im Code. Keine Secrets in DB/Logs/Doku.\n- **Prompt-Injection:** M17 behandelt externe Eingaben als untrusted data. Tool-Whitelist + Task-Gates hart im Code.\n- **Doppelte Barriere:** Gateway ist tool-los (nur HTTP-Calls an M17) + M17-Whitelist deterministisch.\n\n## Commands\n\n| Command | M17 task_type | Beschreibung |\n|---------|---------------|--------------|\n| `/status` | — | Hermes-Health (synchron) |\n| `/research` | research | Markt-/Regime-Analyse |\n| `/analytics` | analytics | Portfolio-/Strategie-Analyse |\n| `/backtest` | backtest | Backtest starten |\n| `/optimize` | optimization | Optimierung starten |\n| `/monitoring` | monitoring | Monitoring-Status |\n| `/tasks` | — | Letzte Tasks (synchron) |\n| `/help` | — | Hilfe |\n| Freitext | research | Wird als Research-Analyse behandelt |\n\n## Asynchrone Task-Abwicklung\n\nLange Tasks (Cloud 5-23s, explizit lokale Analyse bis zu Minuten) laufen asynchron:\n1. Sofortige Bestätigung an denselben Chat (\"⏳ *task* gestartet…\").\n2. Task wird in Background-Thread an M17 gesendet (`POST /tasks` mit `llm=true`).\n3. Ergebnis wird später an denselben Chat zurückgesendet.\n\n### Ergebnis-/Fehlerbehandlung\n\n- **SUCCEEDED:** M26 sendet die finale Hermes-Analyse (`analysis`) + kompakte Tool-Liste. Tool-Fehler (z.B. 404/keine Marktdaten) **brechen den Flow nicht ab** — die finale Analyse mit Einschränkungen wird trotzdem verständlich an Telegram gesendet, einzelne Tools als `[FAILED]` gelistet.\n- **FAILED:** M26 sendet eine **klare Fehlermeldung** (`🔴 *task* fehlgeschlagen (Status: FAILED)` + Fehlercode + Details) — NICHT irreführend \"✅ abgeschlossen\".\n\n## Prompt-/UX-Fix (2026-08-21)\n\nHermes (M17 System-Prompt) formuliert **NIEMALS**, dass eine Order „an einen Execution-Agenten delegiert\" oder „zur Ausführung weitergeleitet\" wird — es gibt keinen solchen Ausführungspfad. Stattdessen stellt Hermes klar:\n\n> **„Hermes kann keine Orders ausführen. Es kann nur analysieren, Research durchführen und DRAFT-Vorschläge erstellen.\"**\n\nWenn eine Order gewünscht ist, antwortet Hermes mit dieser Klarstellung + Analyse-/DRAFT-Vorschlag. Keine Safety-Logik wurde geändert (Tool-Whitelist + Task-Gates unverändert).\n\n## Audit\n\nDB-Tabelle `telegram_message` (Migration `001_telegram_gateway.sql`):\n- `update_id` (PK), `chat_id`, `user_id`, `username`, `text`, `command`\n- `task_id`, `run_id`, `status`, `error_code`, `created_at`\n\nNachvollziehbarkeit: **update_id → task_id → run_id → LLM-run → Tool-Calls** (via M17 hermes_llm_run + hermes_tool_call).\n\n## API (Port 55026 intern)\n\n- `GET /health` — Liveness\n- `GET /health/ready` — Readiness (Telegram-Token + Allowlist)\n- `GET /messages/recent` — Letzte Messages (Audit)\n- `GET /messages/{update_id}` — Einzelne Message\n- `POST /messages/test` — Test-Nachricht an konfigurierte Destination\n\n## Konfiguration (ENV)\n\n| Variable | Beschreibung |\n|----------|--------------|\n| `TELEGRAM_BOT_TOKEN` | Bot-Token (Secret, NUR ENV) |\n| `TELEGRAM_ALLOWED_IDS` | Kommagetrennte erlaubte User-/Chat-IDs |\n| `TELEGRAM_API_BASE` | Telegram API-Base (Default `https://api.telegram.org`; für E2E Fake-Server) |\n| `M17_BASE_URL` | M17 Hermes-Agent (Default `http://Modul-17-Hermes-Agent:55017`) |\n| `PG_HOST/PORT/USER/PASSWORD/DB` | PostgreSQL (Modul-01) |\n\n## E2E-Tests\n\n`tests/e2e_m26.py` (20/20 grün, 2026-08-21):\n1. Unknown user reject\n2. /status\n3. /research (+ Ergebnis)\n4. /analytics (+ Ergebnis)\n5. /backtest (+ Ergebnis)\n6. /optimize (+ Ergebnis)\n7. /monitoring (+ Ergebnis)\n8. /tasks\n9. /help\n10. Freitext → research (+ Ergebnis)\n11. Safety: place_order im Prompt → keine Order\n12. Rate-Limit\n13. Audit: update_id → task_id → run_id\n\nTest-Infrastruktur: `tests/fake_telegram.py` (Fake-Telegram Bot API Server mit Steuer-API für E2E).\n\n## Wichtige Hinweise\n\n- **M20 bleibt reserviert für Universe-Scheduler** — M26 ist separat.\n- **M14 NICHT verändern** — M26 nutzt nur das FAIL-SAFE-Muster als Vorlage, koppelt nicht an M14.\n- **M17 Safety-Gates unverändert** — M26 fügt nur einen dünnen Adapter hinzu.\n- **Kein öffentlicher Port** — nur `expose` im Compose.\n"
},
{
"path": "notes/trading/system-docs/phase10a_execution_context.md",
"title": "phase10a_execution_context",
"id": "object/086b7673-c183-6ca2-52fc-6f394107c9cf",
"type": "arch",
"role": "history",
"representation": "canonical",
"state": "historical",
"knowledge_schema": "1",
"content_hash": "84c87ae6a81979280c43bad13e3f37ed656fca015e6490526bbdd704b96e367b",
"body": "# Phase 10a — ExecutionContext-Dataclass + Versionierung + Tests (M12/M13)\n\n> Autoren: Rain Ocampo (Hermes) | Datum: 2026-08-23\n> Status: **ABGESCHLOSSEN + DEPLOYT** (Analyse/Design/Verifikation + Datenmodell/Audit/Run-Hash-Integration, OHNE Fill-Änderungen)\n> Tags: trading, mt5, architecture, historical-v2, m12, m13, phase10a, execution-context\n\n---\n\n## 1. Ziel (aus Phase-9-Freigabe)\n\nPhase 10a = **ExecutionContext-Dataclass + Versionierung + Tests**.\n\n**WICHTIG — NUR Datenmodell/Context/Audit/Run-Hash-Integration vorbereiten.**\nNoch KEINE Bid/Ask-Ausführung, KEINE Spread-/Slippage-/Kostenberechnung.\nDanach STOPP und Bericht.\n\nDie 7 festgelegten Entscheidungen (Christian, 22.08.2026) sind als Modellversionen\nund Defaults in diesem Modul kodiert — aber es wird NOCH NICHTS berechnet.\n\n---\n\n## 2. Modul: `shared/historical/execution_context.py` (NEU)\n\nGemeinsame Quelle für M12 UND M13 (konsistent mit DatasetContext, R1-Prinzip).\n\n### Enums (festgelegte Bezeichnungen, Entscheidung 7)\n- `ExecutionModel`: `LEGACY_SINGLE_PRICE`, `REFERENCE_BID_ASK`, `BROKER_APPROXIMATION`, `BROKER_REPLAY`, `TICK_REPLAY`\n- `SpreadModel`: `NONE`, `BID_ASK_INTRINSIC`, `SYNTHETIC_FIXED` (Entscheidung 1)\n- `SlippageModel`: `NONE`, `DETERMINISTIC_FIXED` (Entscheidung 3: deterministisch V1)\n- `CostModel`: `NONE`, `cost_model_v1_simple` (Entscheidung 4)\n- `IntrabarPolicy`: `PESSIMISTIC`(Default), `OPTIMISTIC`, `STOP_FIRST`, `TARGET_FIRST`, `TICK_RESOLUTION`, `UNKNOWN` (Entscheidung 2)\n\n### Modellversionen (pro Modell unabhängig, künftig bumpbar)\n`execution_context_v1`, `execution_model_v1`, `spread_model_v1`, `slippage_model_v1`, `cost_model_v1`, `intrabar_policy_v1`.\n\n### Dataclass `ExecutionContext`\nFelder: `execution_context_version`, `execution_model`+`_version`, `price_basis`, `spread_model`+`_version`+`spread_points`, `slippage_model`+`_version`+`slippage_points`, `cost_model`+`_version`, `intrabar_policy`+`_version`, `feed_type`, `validation_errors`.\n\nMethoden: `to_dict()`, `is_valid()`, `validate()` (fail-closed bei unbekanntem Modell), Build-Helper `build_execution_context()`.\n\n**Keine Berechnung** — nur Konfiguration + Modellversionen für Hash/Audit.\n\n---\n\n## 3. service.py-Integration (M12 Run-Hash + Audit)\n\n### `_compute_run_hash` — erweitert um `execution`-Parameter\n- **Legacy** (`execution=None`): exakt alter Hash, **unverändert** (diff-verifiziert).\n- **V2** (`execution` gesetzt): fließen zusätzlich ein (Entscheidung J — **Modellwerte + Versionen**):\n - `execution_model`, `execution_model_version`\n - `price_basis`\n - `spread_model`, `spread_model_version`\n - `slippage_model`, `slippage_model_version`\n - `cost_model`, `cost_model_version`\n - `intrabar_policy`, `intrabar_policy_version`\n\n> **Wichtiger Befund (Reproducibility)**: Anfangs wurden nur die Modell-**Versionen** in den Hash genommen. Das war fehlerhaft: ein Wechsel `bid_ask`→`single` ändert das Ausführungsmodell (REFERENCE_BID_ASK vs LEGACY_SINGLE_PRICE), aber die Version blieb `execution_model_v1` → **identischer Hash**. Korrigiert: **Modellwerte UND Versionen** fließen in den Hash. Test `test_andere_execution_andere_hash` + `test_andere_execution_version_andere_hash` decken beides ab.\n\n### Gate/Audit — ExecutionContext im V2-Pfad\n- Im Gate (`ctx is not None`) wird ein ExecutionContext gebaut (aus `price_basis`+`feed_type` des DatasetContext):\n - `price_basis==\"bid_ask\"` → `execution_model=REFERENCE_BID_ASK`\n - sonst → `LEGACY_SINGLE_PRICE`\n- Das Execution-Audit (`exec_ctx.to_dict()`) wird **sowohl für allowed als auch blocked Runs** an `audit[\"execution\"]` angehängt (auditierbar).\n- Der V2-run_hash nutzt dieses Execution-Audit.\n\n---\n\n## 4. Tests: `m12_app/tests/test_phase10a_execution_context.py` (11/11)\n\n| # | Test | Zweck |\n|---|------|-------|\n| 1 | `test_default_legacy_single_price` | Defaults: LEGACY_SINGLE_PRICE, NONE-Modelle, PESSIMISTIC, keine Doppelzählung |\n| 2 | `test_build_reference_bid_ask` | REFERENCE_BID_ASK-Ableitung; kein künstlicher Spread |\n| 3 | `test_validate_fail_closed_unknown_model` | unbekanntes Modell → invalid |\n| 4 | `test_validate_unknown_intrabar` | unbekannte Intrabar-Policy → invalid |\n| 5 | `test_to_dict_roundtrip` | to_dict serialisierbar |\n| 6 | `test_gleiche_execution_gleicher_hash` | gleiche Execution → gleicher Hash |\n| 7 | `test_andere_execution_andere_hash` | bid_ask vs single → anderer Hash + Audit-Model |\n| 8 | `test_andere_execution_version_andere_hash` | Versions-Bump → andere Identität |\n| 9 | `test_legacy_kein_execution_audit` | Legacy: kein Audit, kein Execution-Dict, Hash unverändert |\n| 10 | `test_v2_execution_audit_present` | V2: Audit.execution vorhanden mit Modellversionen |\n| 11 | `test_blocked_execution_audit_present` | Blocked-Run trägt Execution-Audit |\n\n**Regression (alle grün):**\n- Phase 8 Gate: 13/13\n- M12 Backtest: 12/12\n- Phase 5: 20/20, Phase 6: 14/14, Phase 7: AJ (Legacy-Hash `8a5760…` unverändert)\n- M13 shared AJ\n\n---\n\n## 5. Deploy & Verifikation (Produktion)\n\n- **Deploy**: `execution_context.py` + `__init__.py` + `service.py` per `docker cp` in **Modul-12-Backtesting**; `execution_context.py` + `__init__.py` auch in **Modul-13-Optimization** (geteiltes shared/historical — sonst ImportError bei `import shared.historical`).\n- **chown** auf appuser 1001 (Deploy-Pitfall, bekannt aus Phase 8).\n- **Restart** M12 (uvicorn ohne `--reload`).\n- **Health**: `/health` 200, `/health/ready` 200 (via Python urllib im Container).\n- **Legacy-Smoke PASS**: beide Runs COMPLETED, run_hash `bc6e2553…` + data_hash `d76a7549…` **identisch wie Phase 8** → Legacy unverändert.\n- **Rollback-Backup**: `/opt/trading-modules/backup_phase10a_<TS>/` (service.py, shared_init.py, execution_context.py).\n\n---\n\n## 6. Nächster Schritt (Phase 10b, nach Freigabe)\n\n**Bid/Ask-Datenmodell + Fill** — vollständige Propagation der bid/ask-OHLC durch den M12-Engine-/Execution-Pfad (Entscheidung 5), erste echte Bid/Ask-Fills (Long Entry=Ask, Exit=Bid; Short umgekehrt, Entscheidung 1). NOCH NICHT jetzt — STOPP nach dieser Phase.\n\n---\n\n## 7. STOPP\n\nPhase 10a abgeschlossen. **Keine Implementierung von Phase 10b.** Auf Freigabe warten.\n"
},
{
"path": "notes/trading/system-docs/phase10b_semantic_correction.md",
"title": "phase10b_semantic_correction",
"id": "object/da874dfe-edd4-a6ba-1587-a145ae045ddc",
"type": "arch",
"role": "history",
"representation": "canonical",
"state": "historical",
"knowledge_schema": "1",
"content_hash": "f3bd92a79d38d7c4c6af28f4c19578aa42b561f53a5725298e91fdb804381ce7",
"body": "\n# Phase 10b — Semantische Korrektur REFERENCE_BID_ASK → BID_ASK_INTRINSIC\n\n**Geändert von:** Rain Ocampo (Hermes)\n**Datum:** 23.08.2026\n**Grund:** Semantische Korrektur des Spread-Modells für den REFERENCE_BID_ASK-Ausführungspfad + vollständiger Beweis + kontrollierter Minimal-Deploy.\n\n## Zusammenfassung\n\nPhase 10b schließt die semantische Korrektur des Spread-Modells ab: Für den Ausführungspfad `REFERENCE_BID_ASK` ist der Spread **intrinsisch** (ergibt sich aus Entry-/Exit-Seite, KEIN Zusatz-Aufschlag). Deshalb gilt dort standardmäßig `spread_model = BID_ASK_INTRINSIC`, sofern nicht explizit ein anderes Spread-Modell übergeben wird. Legacy (`LEGACY_SINGLE_PRICE`) bleibt unverändert `NONE`.\n\nDamit zeigen **ExecutionContext, Audit und run_hash dieselbe Wahrheit** — es gibt keine Sonderbehandlung nur im Audit.\n\n## Semantische Korrektur (Option 1, User-Entscheidung)\n\n- **REFERENCE_BID_ASK** (Default) → `spread_model = BID_ASK_INTRINSIC` (explizit übersteuerbar)\n- **LEGACY_SINGLE_PRICE** → `spread_model = NONE` (unverändert)\n- **V2-run_hash DARF sich ändern** (ist gewollt)\n- Keine Sonderbehandlung nur im Audit — Context/Audit/Hash konsistent\n\n## Slippage-Wording\n\nPhase 10b verwendet **noch kein Slippage-Modell** (nicht „ignoriert grundsätzlich\"). `slippage_model = NONE` im Audit.\n\n## Fail-Closed (11 Fälle)\n\n- fehlendes/unvollständiges Bid/Ask → `BID_ASK_REQUIRED`\n- `price_basis != bid_ask` → blockiert\n- `REFERENCE_BID_ASK` + Single-Price → blockiert\n- `BROKER_APPROXIMATION` / `BROKER_REPLAY` / `TICK_REPLAY` → blockiert\n- unbekanntes `execution_model` → blockiert\n- **kein Legacy-Fallback**\n\n## Beweis (Tests)\n\n| Suite | Ergebnis |\n|---|---|\n| Phase10a (execution_context) | **15/15** grün |\n| Phase10bFills | **5/5** grün |\n| Phase10bEngineFills | **8/8** grün |\n| Phase10bFixtureE2E | **4/4** grün |\n| M13Compat | **5/5** grün |\n| Gesamtsuite (Runner) | **2× alle 11 Suiten grün** |\n\n### Beweis-Kernpunkte (Phase10a)\n- A) REFERENCE_BID_ASK Default → `spread_model = BID_ASK_INTRINSIC`\n- B) explizites NONE-Override → NONE\n- C) LEGACY → NONE\n- D) NONE vs intrinsic → **verschiedene** V2-run_hash\n- E) gleiche Konfig 2× → **identischer** V2-run_hash\n- F) Legacy unberührt\n\n### Fixture-E2E (4/4)\n- LONG + SHORT COMPLETED\n- Audit: `execution_model=REFERENCE_BID_ASK`, `spread_model=BID_ASK_INTRINSIC`, `slippage_model=NONE`, `is_fixture=true`\n- ohne `ALLOW_FIXTURE_DATA` → `FIXTURE_DATA_BLOCKED`\n- Reset-Beweis (Flag an→COMPLETED, Flag aus→BLOCKED, ENV wiederhergestellt)\n- kein Doppelspread\n\n## Deployment (kontrollierter Minimal-Deploy, Backup + Rollback)\n\n- **Backup:** `/opt/data/backup_phase10b_deploy_20260823_090949/` (m12 + m13, md5-verifiziert)\n- **M12** (`Modul-12-Backtesting`): `/app/core/engine.py`, `/app/core/service.py`, `/app/core/pricing.py` (NEU), `/app/shared/historical/execution_context.py`, `/app/shared/historical/repository.py`\n- **M13** (`Modul-13-Optimization`): `/app/shared/historical/execution_context.py`, `/app/shared/historical/repository.py`\n- **Verifikation:** py_compile OK in beiden Containern; Restart; `/health`=200, `/health/ready`=`{\"status\":\"ready\",\"db\":\"ok\"}` (M12), `/health`=200 (M13)\n- **KEINE Phase 10c, keine anderen Module, keine IG-Calls/Orders**\n\n## Legacy-Produktions-Smoke (nach Deploy)\n\n- **run_hash: `bc6e25533d22ee102b6921eeeccd8f7b7dff4ec74a3316e381007ec56b0e1b37`** (deterministisch, 2 Runs identisch)\n- **data_hash: `d76a75496453d9c05d5690237f4621534f954f9f3e87b67eee4ae9327c5c6bb9`** (identisch Phase 8)\n- **RESULT: PASS** — Legacy bleibt nach 10b-Deploy byte-identisch\n- **Hinweis:** Die frühere Diskrepanz `b3c2553…` vs `bc6e2553…` ist geklärt — der echte Hash aus der Run-Wahrheit ist `bc6e2553…` (`b3c2553…` war ein Tippfehler im Zwischenbericht).\n\n## Produktions-Gate\n\n- M12 + M13: `HISTORICAL_DATA_SOURCE=UNSET`, `ALLOW_FIXTURE_DATA=UNSET`\n- Produktion bleibt auf echtem Datensatz, **nie auf Fixture**\n"
},
{
"path": "notes/trading/system-docs/phase10d_commission_fees.md",
"title": "phase10d_commission_fees",
"id": "object/dd4f96c5-6cc8-28e4-c8d4-7069c949fa3f",
"type": "arch",
"role": "history",
"representation": "canonical",
"state": "historical",
"knowledge_schema": "1",
"content_hash": "bbc274ca07a07c544681f90640823000f9bceed6c44cabad69b5c1cec0d8c288",
"body": "\n# Phase 10d — Commission / Fees (cost_model_v1_simple)\n\n**Geändert von:** Rain Ocampo (Hermes)\n**Datum:** 23.08.2026\n**Grund:** Deterministisches, reproduzierbares und auditierbares Commission-/Fee-Modell für Historical-V2-Backtests (M12), strikt nach Safe-Change-Management.\n\n## Zusammenfassung\n\nPhase 10d führt ein deterministisches Cost-Modell `cost_model_v1_simple` in den Historical-V2-Backtest ein. Es unterstützt zwei kombinierbare Gebührenarten **pro Fill/Seite**:\n\n- **FIXED PER SIDE** (`commission_fixed_per_side`) — pauschal je Entry- und Exit-Fill.\n- **NOTIONAL RATE PER SIDE** (`commission_rate`) — prozentual auf den **tatsächlichen Fill-Notional** (`abs(fill_price * quantity) * rate`).\n\n`CostModel.NONE` lässt Legacy-Fee-Felder (`fee_fixed`/`fee_pct`) exakt unverändert. `CostModel.SIMPLE_V1` ersetzt im V2-Pfad die betreffende Fee-Berechnung und wird **nicht** zusätzlich zur Legacy-Fee berechnet (keine Doppelzählung). Spread (10b) und Slippage (10c) bleiben getrennte Ebenen und werden nicht als Commission erneut berechnet.\n\n## Fee-Reihenfolge (verbindlich)\n\n```\nMARKET PRICE\n→ BID/ASK SIDE (LONG Entry=ASK / Exit=BID; SHORT Entry=BID / Exit=ASK)\n→ SLIPPAGE (adversarial, DETERMINISTIC_FIXED)\n→ FILL PRICE\n→ COMMISSION/FEE (auf Fill, pro Seite)\n→ NET PNL\n```\n\nDie Rate-Commission verwendet den tatsächlichen **Fill NACH Slippage** — nicht den Pre-Slippage-Basispreis.\n\n## Fee-Formel\n\n```\nfee_per_side = fixed_per_side + abs(fill_price * quantity) * rate\n```\n\n- **LONG/SHORT-symmetrisch** — Fees sind immer Kosten, keine Vorzeichenlogik.\n- **Keine FX-/Währungs-Konvertierung** — Fees in derselben PnL-/Quote-Währung.\n- **Entry- und Exit-Fee exakt einmal** pro Trade; **kein Exit → keine Exit-Fee**.\n- Target, Stop und Force-Close verwenden jeweils den tatsächlichen Exit-Fill.\n\n## Gross / Net\n\n```\ngross_pnl (entry_fee + exit_fee) = net_pnl\n```\n\nTrade-Audit trägt: `gross_pnl`, `entry_fee`, `exit_fee`, `total_fees`, `net_pnl`.\n\n## NONE vs SIMPLE(0)\n\n`CostModel.NONE` und `cost_model_v1_simple(fixed=0, rate=0)` erzeugen **gleiche Fillpreise, gleiches gross_pnl, gleiches net_pnl**, aber **unterschiedlichen ExecutionContext und V2-run_hash** — gewollt.\n\n## Fail-Closed\n\nUngültige Werte blockieren: `fixed < 0`, `rate < 0`, `NaN`, `Inf`, nicht-numerisch, unbekanntes `cost_model`. Kein stilles Clamp, kein Fallback auf NONE.\n\n## run_hash\n\nFür historical_v2 fließen in den reproduzierbaren Run-Kontext ein: `cost_model`, `cost_model_version`, `commission_fixed_per_side`, `commission_rate`. Legacy-Hash (`fee_fixed`/`fee_pct`-Pfad) bleibt exakt unverändert.\n\n## Proof: deterministische Beispiele\n\n| Test | Erwartung | Ergebnis |\n|---|---|---|\n| A: FIXED LONG (Fill 100→110, qty 1, fixed 1) | gross +10, fees 2, net +8 | grün |\n| B: FIXED SHORT (Fill 110→100, qty 1, fixed 1) | gross +10, fees 2, net +8 | grün |\n| C: RATE LONG (Fill 100→110, qty 2, 1%) | entry 2.00, exit 2.20, gross 20, fee 4.20, net 15.80 | grün |\n| D: RATE SHORT (Fill 110→100, qty 2, 1%) | entry 2.20, exit 2.00, gross 20, fee 4.20, net 15.80 | grün |\n| E: FIXED + RATE | exakt additiv | grün |\n\n## Testabdeckung\n\n- `test_phase10d_cost_model.py`: **38 Tests** — NONE, SIMPLE zero, fixed/rate LONG+SHORT, fixed+rate, quantity scaling, fill-price basis, slippage-before-fee, kein double spread/slippage, target/stop/force-close fee, gross/net, audit, run_hash-deterministic/-changes, fail-closed (negativ/NaN/Inf/unbekannt), fixture blocked/allowed, Legacy unchanged.\n\n## Regression (Phase 510d + M12/M13, 2× grün)\n\n- pytest M12-Gruppe: **129 passed** × 2\n- Phase 5 (Eligibility): 20/20 ×2 · Phase 6 (DatasetContext): 14/14 ×2\n- Phase 7 (M13 run_hash AJ): grün ×2 (Legacy-Hash unverändert)\n- M13 compat: 5/5 ×2 (inkl. `optimizer_no_exec_params` = M13 optimiert KEINE Cost-Parameter)\n- M13 shared_repository: AJ grün ×2\n\n## Deploy\n\n- READ-ONLY Importkette geprüft; Backup + sha256 VORHER verifiziert.\n- Minimaler Dateisatz (5 Dateien): `execution_context.py`, `pricing.py`, `engine.py`, `service.py`, `api/schemas.py`.\n- `docker cp` (echter Pfad) → `chown 1001:1001` (als root) → `py_compile` OK → Import-Smoke OK → Restart → Health 200.\n- Production Gates: `HISTORICAL_DATA_SOURCE=UNSET`, `ALLOW_FIXTURE_DATA=UNSET`.\n\n## Legacy-Production-Smoke (2×, byte-identisch)\n\n- run_hash `bc6e25533d22ee102b6921eeeccd8f7b7dff4ec74a3316e381007ec56b0e1b37`\n- data_hash `d76a75496453d9c05d5690237f4621534f954f9f3e87b67eee4ae9327c5c6bb9`\n- net_pnl **196.586062**, total_trades **1** — 2× identisch → Legacy unverändert.\n\n## Geänderte Dateien (Phase 10d)\n\n- `shared/historical/execution_context.py` — Cost-Felder + Fail-Closed + `build_execution_context`-Parameter.\n- `m12_app/core/pricing.py` — `commission_for_fill` (FIXED + NOTIONAL, pro Seite).\n- `m12_app/core/engine.py` — `_position_fee`, Entry/Exit-Fee, Trade-Audit-Felder.\n- `m12_app/core/service.py` — run_hash-Felder, `_dataset_gate` explizite Cost-Parameter.\n- `m12_app/api/schemas.py` — Request-Felder.\n\n## Technische Schulden / Hinweise\n\n- **Deploy-Vorfall (dokumentiert):** `docker cp /dev/stdin` erzeugt defekte Symlinks (`/proc/self/fd/0`). Sauberer Weg: scp → Staging-Pfad → `docker cp <echter-pfad>` → `docker exec -u root chown`. Der Vorfall ist vollständig behoben, kein Daten-/PnL-Befund, Produktion valid.\n- Keine FX-Konvertierung; Overnight/Swap/Finanzierung/Borrow sind NICHT Teil von 10d.\n\n## Nächster Schritt\n\nSTOPP nach Phase 10d. Keine automatische Fortsetzung (Finanzierung/Overnight/Swap/Borrow/Phase 10e/Jahresbackfill/Live-Broker-Costs). Auf neue Freigabe warten.\n"
},
{
"path": "notes/trading/system-docs/phase9_readiness_audit.md",
"title": "phase9_readiness_audit",
"id": "object/ca9b3551-53f8-408b-8697-9a21fa3a24d8",
"type": "arch",
"role": "history",
"representation": "canonical",
"state": "historical",
"knowledge_schema": "1",
"content_hash": "32ccc655fb014826d5ce47978b38f85a6896f791322c689a92c7647a8d256690",
"body": "\n# Phase 9 — M12/M13 Phase-2-Readiness-Audit + Implementierungsmatrix AN\n\n> Autor: Rain Ocampo | Datum: 22.08.2026 | Status: ✅ ANALYSE + DESIGN + VERIFIKATION (KEINE Implementierung)\n> **Geändert von: Rain Ocampo, Datum: 22.08.2026, Grund: Phase-9-Audit (read-only, Code-Evidenz)**\n\n## Executive Summary\n\nPhase 2 (realistischer Backtest: Bid/Ask-Fill, Spread, Slippage, Kosten, Intrabar, Gap) ist NICHT implementiert. Das heutige M12 nutzt ein **reines Single-Price-Bid-Modell** (nur Legacy-`ohlcv`-`close`, oder V2-`bid_close` als `close`). Es gibt keine Bid/Ask-Fill-Logik, kein separates Spread-Modell außer einem pauschalen Preispunkte-Abschlag auf Entry+Exit, keine Slippage außer derselben Punkte-Verschlechterung, und keinen Kosten-Breakdown. Der wichtigste Verzerrungs-Punkt: **Der Intrabar-Exit ist symmetrie-verzerrt** — Stop gewinnt immer bei Stop+Target-in-einer-Bar (konservativ, aber pessimistisch für Target-Trades).\n\nDie Erweiterung muss einen **ExecutionContext** einführen, der `execution_model`, `spread_model`, `slippage_model`, `cost_model`, `intrabar_policy`, `price_basis`, `feed_type` transportiert und versioniert — inklusive Run-Hash-Integration, aber **strikt getrennt** von Dataset-Identität und Strategie-Parameter.\n\n---\n\n## A — PRICE MODEL / SINGLE-PRICE AUDIT\n\n### Candle-Datenmodell (M12)\n- **Legacy** (`repository.py:_load_candles_legacy`, Zeile 305-327): `ohlcv`-Tabelle, Keys `timestamp, open, high, low, close, volume`. **Nur Einzelpreis** pro Bar (Mid/Einzelpreis). Kein bid/ask.\n- **V2** (`repository.py:_load_candles_v2`, Zeile 332-371): liest aus `historical_bar` und mappt **`bid_open→open, bid_high→high, bid_low→low, bid_close→close`**. Das `historical_bar`-Schema HAT bid_open..ask_close (verifiziert: 37.480/37.480 bid+ask, mid=0), aber `load_candles` **transportiert sie NICHT** — wirft die ask-Seite weg und liefert nur bid-OHLC.\n- Die Engine bekommt also immer OHLC (nur bid/Einzel) — **niemals ask**.\n\n### Entry / Exit / Stop / Target (engine.py)\n| Feld | Quelle | Zeile |\n|---|---|---|\n| Entry (Signal) | `setup.entry` = **letzter Close der Signal-Candle** (Strategie: `entry = last_close`) | strategy 134/175 |\n| Entry-Fill (Open Pos) | `next_candle[\"open\"]` + Spread + Slippage (verschlechtert) | `_open_position` 192-214 |\n| Stop | `setup.stop_loss` = Swing-Low/High ± buffer | 135, 176 |\n| Target | `setup.target` = Entry ± 2R | 139, 180 |\n| Exit (normales Ende) | `candle[\"close\"]` (OHNE Slippage/Spread auf Stop/Target) | `_close_position` 283-336 |\n| Market-Order-Fill | pauschaler Preispunkte-Abschlag LONG+ / SHORT- | 198-200 |\n| Gap | Open-Fill zum Gap-Open | `_evaluate_exit` 268-280 |\n| Run-Ende | Force-Close zum letzten `close` | 141-153 |\n\n### Aktuelle Annahmen (Single-Price)\n| Annahme | Konsequenz |\n|---|---|\n| `open`/`close` sind handelbarer Preis (Einzel) | Kein Bid/Ask-Spread |\n| LONG Entry = OPEN + spread + slippage | Wird besser (künstlicher Overhead) |\n| LONG Exit = CLOSE (bei normalem Exit) | Kein Exit-Spread/Slippage — nur Entry hat Kosten |\n\n**RISIKO**: V2-Daten sind real `bid_ask`, aber `load_candles` reduziert auf bid — die ask-Seite wird VOR der Engine verworfen. Realisierung als Bid/Ask-Fill ist daher **nicht möglich ohne Datenmodell-Änderung**.\n\n**SOLL**: Engine muss `price_basis` + `bid_*`/`ask_*`/`mid_*` transportieren; Entry/Exit-Getrennt je nach Richtung; Stop/Target auf entsprechende Seite anwenden.\n\n---\n\n## B — BID/ASK READINESS\n\n### Was liefert Historical V2 tatsächlich?\n- `historical_bar` hat **`bid_open/high/low/close` UND `ask_open/high/low/close`** (verifiziert: 37.480/37.480/37.480, mid=0) — bid_ask vorhanden, mid NICHT.\n- `load_bars_with_quality` (Zeile 163-247) transportiert bid+ask+mid+quality-Keys (vollständig).\n- ABER: `load_candles` (der Pfad, den die Engine nutzt) liefert **nur bid-OHLC** — ask wird verworfen.\n\n### Kann M12 es heute transportieren?\n**Nein, nicht über den Engine-Pfad.** Die Engine bekommt `open/high/low/close` als Einzelwerte. `build_dataset_context` bringt `price_basis=\"bid_ask\"` (aus Bar-Metadaten), aber diese Info erreicht die Engine-Fills **nicht** (nur Run-Audit).\n\n### Zielmodell\n```\nLONG: Entry grundsätzlich Ask (Käufer zahlt ask)\n Exit grundsätzlich Bid (Verkäufer erhält bid)\nSHORT: Entry grundsätzlich Bid (Verkäufer verkauft bid)\n Exit grundsätzlich Ask (Käufer erhält ask)\n```\n**Sonderfälle (zu dokumentieren, NICHT implementieren):**\n- **Stop LONG**: Löse aus, wenn bid_low ≤ stop → fill = stop (oder gap-open bid, wenn gap unter)\n- **Stop SHORT**: bid_high ≥ stop → fill = stop (bid/ask?)\n- **Target LONG**: ask_high ≥ target → fill = target\n- **Market**: Entry LONG = ask_open; Entry SHORT = bid_open\n- **Limit**: (Phase 2 nicht eingeführt — kein Limit-Fill)\n- **Gap**: bid/ask-gap-opening überschlägt Stop/Target\n- **Bar Open**: Entry zum nächsten Bar-Open auf der korrekten Seite\n- **End-of-data**: force-close zum letzten bid/ask-close\n\n---\n\n## C — SPREAD AUDIT\n\nHeute (**engine.py** `_open_position` Zeile 198-201, `_close_position` Zeile 297-303):\n- **Spread = ein fester Punktwert** (`params[\"spread\"]`, Default 0.0, M13-Default 0.0).\n- **Einheit: Preispunkte** (absolute Preisdifferenz, nicht %).\n- Anwendung: **sowohl Entry als auch Exit**, je Richtung versetzt — d.h. ein **doppelter Spread pro Trade** (Entry+Exit) statt einmal.\n- **Preisbasis**: unklar — es wird einfach von `open`/`close` abgezogen, ohne bid/ask-Bezug. Da Daten heute bid-single sind, ist es ein \"Abschlag\" auf den einzigen Preis.\n\n**Doppelzählungsrisiko**: Wenn Bid/Ask-Fill eingeführt wird, ist der aktuelle pauschale Spread-Abzug bereits implizit im echten bid/ask-Spread enthalten. Zusätzlich ein künstlicher `spread`-Abzug ⇒ **Doppelzählung**.\n\n**Zielregel**:\n- `price_basis=bid_ask` (echte bid/ask) → Spread NICHT künstlich aufschlagen; nutze den intrinsischen bid/ask.\n- `price_basis=mid`/single → synthetisches Spread-Modell möglich (aufzuschlagen).\n- Beide Modi **strikt unterscheidbar** durch `spread_model` (z.B. `BID_ASK_INTRINSIC` vs `SYNTHETIC_FIXED` vs `SYNTHETIC_POINTS`).\n\n---\n\n## D — SLIPPAGE AUDIT\n\nHeute (engine.py):\n- **Wann**: nur Entry + normaler Exit (nicht Stop/Target, siehe unten).\n- **Einheit**: Preispunkte (`params[\"slippage\"]`, Default 0.0).\n- **Long/Short**: symmetrisch — Long schlägt auf (Entry+Exit), Short zieht ab. Adversarial in beiden Fällen.\n- **Deterministisch**: JA — ein fester Punkte-Wert. Kein Zufall, kein Seed.\n- **Bestandteil run_hash**: JA (steht in `params_snapshot` → `_compute_run_hash` inkludiert `params`). Slippage-Änderung ändert run_hash.\n- **Bestandteil Audit**: indirekt über `parameters` (in `metrics.audit` nicht explizit).\n\n**Zielmodell**: `slippage_model` (`DETERMINISTIC_FIXED` | `SYNTHETIC_POINTS` | `BROKER_REPLAY`...). Kein Zufall im deterministischen Kern; falls stochastisch, Seed fix + in Hash. Jeder Trade trennbar slippage_cost.\n\n---\n\n## E — COST MODEL\n\n| Kosten | Status | Quelle | Heutige Modellierung |\n|---|---|---|---|\n| **Commission** | TEILWEISE | CONFIG (`fee_fixed`+`fee_pct`) | `_fee()` auf Entry+Exit |\n| **Spread** | TEILWEISE | CONFIG (`spread`, Punkte) | pauschaler Preispunkte-Abschlag auf Entry+Exit |\n| **Slippage** | TEILWEISE | CONFIG (`slippage`, Punkte) | pauschaler Preispunkte-Abschlag auf Entry+Exit |\n| **Financing/Overnight/Swap** | NICHT | — | nicht modelliert |\n| **FX conversion** | NICHT | — | nicht modelliert |\n| **Guaranteed Stop Premium** | NICHT | — | nicht modelliert |\n| **sonstige Gebühren** | NICHT | — | nicht modelliert |\n\n**Kosten-Quellen heute**: ausschließlich **CONFIG** (keine Broker-, keine Referenz-Kosten). Keine IG-Gebühren erfunden ✓.\n\n**Doppelzählungsrisiko (Kosten)**: `fee` UND `spread` UND `slippage` werden alle separat abgezogen — kein Broker/Referenz-Double-count, aber `spread` als pauschaler Abzug + künftiges bid/ask würde doppelt zählen (siehe C).\n\n---\n\n## F — INTRABAR AMBIGUITY (engine.py `_evaluate_exit`, Zeile 251-281)\n\n**Aktuelles Verhalten — Code-Evidenz:**\n- **LONG**: Stop wird VOR Target geprüft. Wenn `open<=stop`→Gap-Stop. Dann `if low<=stop`→Stop. Dann `if high>=target`→Target.\n ⇒ **Wenn Stop UND Target in derselben Bar** → **Stop gewinnt IMMER** (Worst-Case konservativ).\n- **SHORT**: spiegelbildlich (Stop zuerst).\n\n**Verzerrung**: M1-OHLC kann reale Intrabar-Reihenfolge nicht bestimmen (bekannt aus POC). Aktuelle Wahl ist deterministisch-pessimistisch (Stop-first), aber **kann Target-Gewinner in realistischen Sequenzen unterschätzen** (Stop-first bei bar mit beiden → Trades als Verluste klassifiziert, die real oft Target-Trades wären).\n\n**Zielmodi (Design, NICHT implementiert)**:\n- `PESSIMISTIC` (= Stop-first, heutiges Verhalten)\n- `OPTIMISTIC` (= Target-first)\n- `STOP_FIRST` / `TARGET_FIRST` (explizite Varianten)\n- `TICK_RESOLUTION` (benötigt Tick-Daten — nur V2 nicht lieferbar)\n- `UNKNOWN/BLOCK` (fail-closed: keine Intrabar-Reihenfolge festgelegt → Run blocken)\n- Optional `DEFAULT=STOP_FIRST` (behält heutiges Verhalten als Baseline; künftig wählbar)\n\n---\n\n## G — GAP EXECUTION (engine.py, Zeile 268-280)\n\n- **LONG Stop-Gap**: wenn `open<=stop` → Fill zum `min(open, stop)` = Gap-Open (schlechter). Bewiesen: ein Gap-Down unter Stop füllt nicht perfekt bei stop, sondern Gap-Open.\n- **LONG Target**: `high>=target`→ fill=target (kein Gap-Fall bei Target gesondert). wenn `open>=target` → wird als `high>=target` abgefangen und zum target gefüllt (das wäre Optimistisch — Gap UP über Target müsste evtl. Gap-Open, nicht target).\n- **SHORT analog**.\n- **RISIKO**: `target`-Fill bei Gap wird zum target (nicht gap-open) — unrealistisch optimistisch für Gap-UP-LONG (sollte bid/ask-open nach gap). Umgekehrt ist der Stop-Gap korrekt pessimistisch.\n- **Zielverhalten**: Gap-Fill-Regel konsistent: bei Gap-Öffnung jenseits Stop/Target → zum **Gap-Open der realen ausführbaren Seite**, nie zum anvisierten Level.\n\n---\n\n## H — REFERENCE VS BROKER FEED\n\n- **Heute**: M12 kennt keinen Feed-Unterschied. Engine ignoriert `feed_type`/`price_basis`. Ergebnis wird als generischer Backtest-Output geliefert (einzig Run-Audit enthält `feed_type`/`price_basis`, aber nur bei V2 + Context).\n- **Zielregel**: REFERENCE-Daten nie als exakte Broker-Ausführung ausgeben. Run muss `execution_model` + `cost_model` + `spread_model` + `feed_type` + `price_basis` sichtbar kennzeichnen.\n- **Execution-Klassen (Phase 2)**:\n - `MARKET_SIMULATION` — nur OHLC, synthetische Modelle (Spread/Slippage/Cost als config)\n - `BROKER_APPROXIMATION` — echte bid/ask OHLC (REFERENCE), realistische Spread/Fill\n - `BROKER_REPLAY` — echte Broker-Feed + Broker-Fill (IG later), fehler bei REFERENCE\n- `BROKER_REPLAY` auf REFERENCE-Daten → **fail-closed** (inconsist).\n\n---\n\n## I — M13 OPTIMIZATION IMPACT\n\nM13 (optimizer.py) ruft M12 mit `spread`/`slippage`/`fee_fixed`/`fee_pct` als **feste Baustein-Argumente** (`_bt_kwargs`, Zeile 326-335, alle Default 0.0). **NICHT Teil des Optimizer-Suchraums** — `param_space` enthält nur Strategie-Parameter. \n\n**Gefahr**: Wenn `spread`/`slippage`/Fees in `param_space` aufgenommen würden, könnte der Optimizer sie MINIMIEREN (um Score zu maximieren) — realistische Kosten würden \"schönoptimiert\". Das ist **verboten**.\n\n**Klassifikation (Phase-2-Design)**:\n| Klasse | Beispiele | M13 optimiert? |\n|---|---|---|\n| STRATEGY_PARAMETER | ema_period, rr_multiplier, stop_buffer | JA |\n| EXECUTION_PARAMETER | intrabar_policy, gap_rule | NEIN |\n| BROKER_PARAMETER | spread, slippage, commission, financing | NEIN |\n| DATA_PARAMETER | data_source, feed, price_basis, timeframe | NEIN |\n| POLICY_PARAMETER | eligibility_policy, quality_rule | NEIN |\n\nM13 darf **standardmäßig nur STRATEGY_PARAMETER** optimieren; alle anderen Klassen bleiben fix aus dem Backtest-Request (nicht optimierbar).\n\n---\n\n## J — RUN HASH / REPRODUCIBILITY\n\nPhase-2-Parameter, die die Run-Identität beeinflussen müssen (Kandidaten):\n- `execution_model_version`\n- `spread_model_version`\n- `slippage_model_version`\n- `cost_model_version`\n- `intrabar_policy_version`\n\n**Entscheidung** (Grundregel: Daten ≠ Ausführungsmodell ≠ Strategieparameter):\n- **Dataset-Hash** (data_hash): rein die OHLC/Bar-Daten (bid/ask). Unverändert durch Execution-Modelle.\n- **Run-Hash**: Dataset-Hash + Strategie+Param + **Execution/Broker/Cost-Modell-Versionen** (weil anderer Execution-Modus → anderes Ergebnis → anderes run_hash, sonst falscher Cache-Hit).\n- **Audit**: alle expliziten Modelle/Versions + breakdown, aber NICHT in den Hash (nur repräsentativ).\n- M13-Optimizer-Hash analog: Execution-Modell-Versionen zusätzlich.\n\n---\n\n## K — RESULT METRICS\n\nHeute (`_compute_metrics`): total_trades, winrate, profit_factor, expectancy_r, avg_r, net_pnl, gross_profit, gross_loss, avg_holding_h, long_count, short_count, max_drawdown, by_regime. **Kein Kosten-Breakdown.**\n\n**Zielmetriken (Phase-2-Design)**:\n- `gross_pnl` (ohne Kosten)\n- `net_pnl` (nach allen Kosten)\n- `spread_cost` / `slippage_cost` / `commission_cost` / `financing_cost` / `total_cost`\n- `ambiguous_bars` (Bars mit Intrabar-Ambiguität)\n- `gap_fills`\n- `conditional_bars_used` (V2-Conditional)\n- `reference_feed_warning`\n\n---\n\n## L — FAIL-CLOSED CONDITIONS (Phase-2)\n\nDesign-Liste, Situationen in denen Phase-2-M12 NICHT rechnen darf:\n| Fall | Error-Code |\n|---|---|\n| Bid/Ask erforderlich aber fehlt | `BID_ASK_REQUIRED_MISSING` |\n| unbekannte price_basis | `UNKNOWN_PRICE_BASIS` |\n| unbekanntes execution_model | `UNKNOWN_EXECUTION_MODEL` |\n| Fixture blockiert | `FIXTURE_DATA_BLOCKED` |\n| Dataset nicht eligible | `DATASET_NOT_ELIGIBLE` / `UNKNOWN_CRITICAL_METADATA` |\n| DatasetContext fehlt | `DATASET_CONTEXT_MISSING` |\n| Kostenmodell verlangt Brokerdaten fehlen | `COST_MODEL_BROKER_DATA_MISSING` |\n| BROKER_REPLAY + REFERENCE-Feed | `BROKER_REPLAY_REFERENCE_INCOMPATIBLE` |\n| Tick-Modus verlangt Tickdaten, hat nur M1 | `TICK_DATA_REQUIRED_MISSING` |\n\n---\n\n## M — MIGRATION / COMPATIBILITY\n\n| Modus | Daten | Execution | Spread | Slippage | Costs | Intrabar | Audit | Reproducibility | Zulässig |\n|---|---|---|---|---|---|---|---|---|---|\n| **LEGACY Single-Price** | ohlcv single | open/close, Stop-first | Config-punkte | Config-punkte | Config-fix/% | Stop-first | Legacy-Hash | run_hash | ✅ |\n| **V2 REFERENCE BID/ASK** | historical bid/ask | bid/ask-Fill | intrinsic (bid/ask) | Config/Determ. | Config-fix/% | wählbar | full-Audit | run_hash + execution_version | ✅ (nach Implementierung) |\n| **BROKER BID/ASK** | Broker bid/ask | broker-Fill | real broker | real broker | real broker | broker | full | Broker-Feed-Audit | (künftig) |\n| **TICK** | tick-data | tick-seq | real | real | real | tick-resolved | full | Tick-Audit | (künftig) |\n\nLegacy bleibt **unverändert** (durch `price_basis`/`execution_model`-Gates). Kein stiller Fallback.\n\n---\n\n## N — IMPLEMENTIERUNGSREIHENFOLGE (begründet)\n\nCode-Audit belegt, dass die Engine rein Single-Price ist. Empfohlene Reihenfolge (jede Phase testbar + rückbaubar):\n\n1. **ExecutionContext** (Datenmodell/Versionierung) — Voraussetzung, trennt alles. LOW\n2. **Bid/Ask-Datenmodell** (load_candles/bars trägt bid/ask; Engine liest nach price_basis) — MEDIUM\n3. **Bid/Ask Fill** (LONG buy ask/sell bid, SHORT bid/ask) — HIGH (Fill-Preis)\n4. **Spread-Modell** (bid_ask intrinsic vs synthetic, keine Doppelzählung) — HIGH\n5. **Slippage** (slippage_model, Entry/Exit-Einheit, Determinismus) — MEDIUM\n6. **Cost-Breakdown** (Commission/Financing getrennt, nicht nur pauschal) — MEDIUM\n7. **Gap-Fills** (konsistentes Gap-Open-Verhalten, Stop+Target) — HIGH\n8. **Intrabar-Policy** (PESSIMISTIC/OPTIMISTIC/STOP_FIRST/TARGET_FIRST/TICK/UNKNOWN-BLOCK) — MEDIUM\n9. **Result-Cost-Breakdown** (gross/net/cost-Metriken) — LOW\n10. **M13-Param-Grenzen** (nur STRATEGY_PARAMETER optimizierbar, Execution-Kosten fix) — MEDIUM\n11. **Reproducibility** (execution_model_version in run_hash, Audit-Vollständigkeit) — MEDIUM\n12. **Regression/Legacy** (unverändert; Gate-Zusammenheit) — LOW\n13. **Acceptance-Tests** (Plan unten)\n\nDie Reihenfolge weicht von der (User)-Suggestions ab: **ExecutionContext-Versionierung zuerst** ist zwingend, sonst werden Modelle ohne Versionierung eingebaut und Reproducibility bricht nachträglich. Bid/Ask-Datenmodell vor Fill zwingend.\n\n---\n\n## RISIKO-MATRIX\n\n| Änderung | Modul | Datei/Fn | Risiko | Regression | Test | Rollback |\n|---|---|---|---|---|---|---|\n| ExecutionContext | M12 | service.py/engine.py | LOW | — | Hash-Vergleich | revert service+engine |\n| Bid/Ask-Datenmodell | M12 | marketdata/client.py, repo | **MEDIUM** | Daten-Format | load-candles-Pfad | revert client/repo |\n| Bid/Ask-Fill | M12 | engine.py | **HIGH** | Fill-Preis | LONG/SHORT Fill-Test | revert engine |\n| Spread-Modell | M12 | engine.py | **HIGH** | Spread, PnL | No-Doppelzählung | revert |\n| Slippage | M12 | engine.py | MEDIUM | slippage-Einheit | Entry/Exit-Slipp | revert |\n| Commission/Costs | M12 | engine.py | MEDIUM | Cost-Breakdown | Fee-Test | revert |\n| Gap-Fills | M12 | engine.py | **HIGH** | Stop/Target, PnL | Gap-Tests | revert |\n| Intrabar-Policy | M12 | engine.py | MEDIUM | Stop/Target | Intrabar-Tests | revert |\n| Result-Cost-Breakdown | M12 | engine.py | **HIGH** | PnL | Metrics | revert |\n| M13-Param-Grenzen | M13 | optimizer.py | MEDIUM | Overfit | Optimizer-Test | revert |\n| Reproducibility | M12/M13 | service.py/optimizer.py | MEDIUM | Run-Hash | Hash-Repro | revert |\n\n---\n\n## ACCEPTANCE PLAN (vor Implementierung — Katalog)\n\n**Fill/Spread:**\n- LONG Entry Ask, LONG Exit Bid (BID-ASK)\n- SHORT Entry Bid, SHORT Exit Ask\n- echte bid/ask-Spread NICHT doppelt (Bid/Ask-Modus)\n- synthetischer Spread korrekt (Single-Price-Modus)\n\n**Slippage/Cost:**\n- Slippage Long/Short adversarial\n- Commission korrekt (fix+%)\n\n**Intrabar/Gap:**\n- Stop-only, Target-only, Stop+Target gleiche Bar\n- Gap über Stop, Gap über Target\n- PESSIMISTIC/OPTIMISTIC unterscheidbar\n\n**Feed/Klassifizierung:**\n- REFERENCE korrekt markiert\n- BROKER_REPLAY + REFERENCE → fail-closed\n\n**Reproduzierbarkeit:**\n- gleicher Input → identisches Ergebnis\n- andere Execution-Version → anderer run_hash\n\n**Regression:**\n- Legacy unverändert\n\n---\n\n## OFFENE ENTSCHEIDUNGEN (für Christian)\n\n> **STAND 22.08.2026: ALLE 7 ENTSCHEIDUNGEN FESTGELEGT** (Christian, 22.08.2026).\n\n1. **Spread → pro Fill/Seite modellieren, KEIN pauschaler Roundtrip-Aufschlag.**\n Bei echten Bid/Ask-Daten entsteht der Spread über Entry-/Exit-Seite. Keine Doppelzählung.\n2. **Intrabar-Default = PESSIMISTIC.** Wenn Stop und Target in derselben Bar liegen und keine\n Tick-Reihenfolge vorhanden ist: konservativ ungünstigeren Ausgang verwenden. Später optional TICK_RESOLUTION.\n3. **Slippage = deterministisch.** Kein Zufall in V1. Später optional stochastisch nur mit festem Seed + eigener Modellversion.\n4. **Cost Model = `cost_model_v1_simple`.** Abdeckung: Spread, Slippage, Commission.\n NOCH KEINE Overnight-/Swap-/Financing-Komplexität in V1 (Financing später eigene Modellversion).\n5. **Bid/Ask = durch den vollständigen M12-Engine-/Execution-Pfad propagieren.**\n Nicht nur im Repository/load_bars. Keine Reduktion zurück auf Single Price im V2-Pfad.\n6. **M13: Execution-/Broker-/Cost-Parameter sind FIX und NICHT optimierbar.** M13 optimiert\n standardmäßig nur echte STRATEGY_PARAMETER. Spread/Slippage/Commission/Brokerparameter dürfen nicht schönoptimiert werden.\n7. **Execution Models (verwenden):**\n - `LEGACY_SINGLE_PRICE`\n - `REFERENCE_BID_ASK`\n - `BROKER_APPROXIMATION`\n - später `BROKER_REPLAY`\n - optional später `TICK_REPLAY`\n Dukascopy Historical V2 = `REFERENCE_BID_ASK`. Nicht als IG-Brokerfeed behandeln.\n\n---\n\n## EXAKTER NÄCHSTER IMPLEMENTIERUNGSSCHRITT\n\nSobald Christian die offenen Entscheidungen freigibt, ist der **1. Schritt von Phase 10**:\n1. **ExecutionContext-Dataclass** (execution_model, spread_model, slippage_model, cost_model, intrabar_policy, price_basis, feed_type + je *_version) — **ausser** DatasetContext, reine Ausführungs-Konfiguration.\n2. Engine lädt als Einzelz: `execution_model` in `BacktestRequest`.\n3. Tests: deterministisch, Hash-Integration, Legacy unverändert.\n\n**NOCH NICHT implementiert** (verboten bis Freigabe): Bid/Ask-Fill, Spread-Änderung, Slippage-Änderung, Kostenmodell-Änderung, Intrabar-Ausführungslogik, Jahresbackfill, IG-Calls, Orders, Docker-Netzwerkänderung.\n"
},
{
"path": "notes/trading/system-docs/ports-reference.md",
"title": "ports-reference",
"id": "object/ebe08190-5cd5-a649-d6ab-38cb8f50f8b4",
"type": "arch",
"role": "reference",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "9698bce8dee9a4924d072fd0d5e3609c5db16ee10cc36d7a3a158331434cf173",
"body": "\n# Ports & Service-Referenz — Trading-System VPS\n\n> Stand: 20.08.2026 · VPS: `187.124.31.123` · SSH: Public-Key-Auth (nur)\n\n## Netzwerk & Core-Infrastruktur\n- **Netzwerk:** `trading-modules` (bridge) — Kommunikation über Docker-interne Service-Hostnamen, keine festen IPs.\n- **Host:** Hostinger-VPS, 300 GB Disk, ~31 GiB RAM.\n\n## Feste Port-Zuordnung (Schema `55NNN` nach Modulnummer)\n| Modul | Container | Service | Host-Port | Öffentlich? |\n|-------|-----------|---------|-----------|-------------|\n| 01 | Modul-01-PostgreSQL | PostgreSQL | **keiner (intern, `expose`)** | **nein** (nur intern) |\n| 02 | Modul-02-RabbitMQ | RabbitMQ AMQP | **keiner (intern, `expose`)** | **nein** (nur intern) |\n| 02 | Modul-02-RabbitMQ | RabbitMQ Management | **keiner (intern, `expose`)** | **nein** (nur intern) |\n| 03 | Modul-03-Market-Data | FastAPI | 55003 (intern, `expose`) | **nein** (nur intern) |\n| 04 | Modul-04-Market-Regime | FastAPI | 55004 (intern, expose) | nein (nur intern) |\n| 05 | Modul-05-Strategy-Engine | FastAPI | 55005 (intern, expose) | nein (nur intern) |\n| 0612 | Modul-06…12 | FastAPI | 5500655012 (intern, expose) | nein (nur intern) |\n| 13 | Modul-13-Optimization | FastAPI | 55013 (intern, expose) | nein (nur intern) |\n| 1417 | … | — | 5501455017 | (Platzhalter) |\n\n> **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.\n\n## Weitere Dienste (Host)\n| Dienst | Container | Port |\n|--------|-----------|------|\n| Forgejo | forgejo-c4u8yyi1eaz1gepn3pqmr5fb | 3000 (HTTP) / 22222 (SSH) |\n| Tolaria (Second Brain) | tolaria | 5173 |\n| OpenClaw Alice | openclaw-suqw-openclaw-1 | 55163 |\n| OpenClaw Matt | openclaw-3sgu-openclaw-1 | 54524 |\n| Hermes Rain | hermes-workspace-e6un… | 32776 (UI) |\n| Ollama | ollama-nb6d-ollama-1 | 11434 (intern) |\n| n8n | n8n | (Coolify-managed) |\n| Traefik | traefik | 80/443 |\n\n## Docker-interne Hostnamen (wichtig für Container-Kommunikation)\n- PostgreSQL: `Modul-01-PostgreSQL:5432`\n- RabbitMQ: `Modul-02-RabbitMQ:5672` (vhost `trading`)\n- Ollama: `ollama-nb6d-ollama-1:11434`\n\n## Sicherheits-Notizen\n- **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.\n- Zugangsdaten ausschließlich als Env-Variablen/Secrets, nie im Code.\n- Öffentliche Ports nur, wo nötig (Admin/Debug/UI).\n\n---\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-01/02 von öffentlichen Ports auf interne `expose`-Bindings umgestellt (Security-Fix), Doku aktualisiert.\n```\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: ports-reference aktualisiert — Modul-04 und Modul-05 von Platzhalter auf real (FastAPI, intern expose, nicht öffentlich) eingetragen.\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: ports-reference aktualisiert — Modul-0612 als real (FastAPI, intern) und Modul-13 Optimization (55013 intern) eingetragen.\n"
},
{
"path": "notes/trading/system-docs/trend-pullback-v1.1-spec.md",
"title": "trend-pullback-v1.1-spec",
"id": "object/31671ffa-fc39-ae9c-9449-e9322c9bb7be",
"type": "design",
"role": "design",
"representation": "canonical",
"state": "current",
"knowledge_schema": "1",
"content_hash": "d771e13cb30516b340d8624e1dd09bf945743f463e6b048fe0a0020b76c6e195",
"body": "\n\n# trend_pullback_v1.1 — Spezifikation\n\n> Status: **IMPLEMENTIERT + DEPLOYT** (`backtesting:0.1.2`, Modul-12), V1-Kompatibilitätsmodus bitgenau.\n> Erstellt: 20.08.2026 (Rain Ocampo) · Aktualisiert: 20.08.2026\n\n## 1. Motivation\n\nDie Produktions-Strategie `trend_pullback_v1` (Modul-05/12) ist unter den\n**unveränderten** produktiven Regime-Schwellen fachlich widersprüchlich\n(messbar): Ein TREND_UP-Regime verlangt Volatilität, der Pullback\n`close < EMA20` verlangt ein flaches Plateau. Im 60-Bars-Produktionsfenster\n(`regime_lookback=60`) schneiden sich `TREND_UP` und `close<EMA20` nur in\n**14 von 1620** Kerzen (m13_evidence.py, Messwerte 20.08.2026). Die\nVol-Override (`HIGH_VOL`/`LOW_VOL`) kippt das Regime bei jedem sinnvollen\nPullback (>95% der `close<EMA20`-Kerzen werden `LOW_VOL`).\n\n**V1.1 trennt Richtung und Pullback-Timing.** Richtung wird aus einem\nstrategie-internen, längeren Fenster berechnet (nicht durch Vol-Override\nkippbar); Volatilität fließt nur noch ins Pullback-Timing (ATR-Band um EMA20)\nein.\n\n## 2. Kernentscheidungen\n\n1. **V1.1 bleibt zustandslos.**\n Trendrichtung wird **jede Bar neu** aus dem aktuellen Fenster berechnet.\n **Kein Sticky-State.** Kein Hidden-State. Begründung: einfacher, transparenter\n und reproduzierbarer (deterministisch nach Seed/Replay).\n\n2. **Kein kurzfristiger Vol-Override auf die Richtung.**\n `regime_vol_override=false` (V1.1-Default): Die Trend-Richtung wird\n strategie-intern berechnet und **nicht** durch `HIGH_/LOW_VOLATILITY`\n überschrieben. Volatilität beeinflusst nur das Pullback-Timing (Band).\n\n3. **Optimierungsraum für V1.1 bewusst klein.** Erste Iteration optimiert\n ausschließlich `pullback_band_atr`, `trend_regime_window`, `rr_multiplier`.\n ADX-/EMA-Perioden und `atr_period` bleiben **fix** auf produktiven V1-Werten.\n\n## 3. Pseudo-Regel (Variante A, lookahead-frei)\n\nStrategie-intern, jede Bar, aus geordneten `prev_*`-Serien (kein Lookahead,\nkein versteckter Zustand):\n\n```\n# --- Trend-Richtung (Fenster = trend_regime_window) ---\nema_fast_l = EMA(prev_closes, ema_fast_period, lookback=trend_regime_window)\nema_slow_l = EMA(prev_closes, ema_slow_period, lookback=trend_regime_window)\nadx_l = ADX(prev_highs, prev_lows, prev_closes, adx_period, lookback=trend_regime_window)\n\ntrend_dir = UP wenn (ema_fast_l > ema_slow_l) UND adx_l >= adx_trend_threshold\ntrend_dir = DOWN wenn (ema_fast_l < ema_slow_l) UND adx_l >= adx_trend_threshold\nsonst: kein Trade # KEIN atr_ratio-Check für die Richtung\n\n# --- Pullback-Timing (ATR-Band um EMA20) ---\natr_prev = ATR(prev_highs, prev_lows, prev_closes, atr_period)\nema20 = EMA20(prev_closes)\npullback_limit = ema20 + pullback_band_atr * atr_prev # LONG\npullback_limit = ema20 - pullback_band_atr * atr_prev # SHORT\n\nLONG wenn trend_dir==UP UND last_close>sma200 UND last_close < pullback_limit\n UND last_close>prev_close UND swing_low<last_close UND rr>=min_risk_reward\nSHORT wenn trend_dir==DOWN UND last_close<sma200 UND last_close > pullback_limit\n UND last_close<prev_close UND swing_high>last_close UND rr>=min_risk_reward\n```\n\n- `break_high` entfällt (Befund: `prev_high ≥ prev_close` ⇒ `break_high ⇒ close_up`;\n bleibt als `close_up`/`close_down` erhalten — nicht restriktiv).\n- Deterministisch: reine Listen-Berechnung, kein Zufall, kein Hidden-State.\n\n## 4. Parameter-Tabelle\n\n| Parameter | V1 produktiv (Code) | V1.1 Default | Min | Max | Zweck | V1.1 Optimierbar? |\n|---|---|---|---|---|---|---|\n| `pullback_band_atr` | — (strikt `< EMA20`) | **0.35** | 0.0 | 1.0 | ATR-Band um EMA20. `0.0`=strikt V1. | **Ja** |\n| `atr_period` | 14 | **14** | 14 (fix) | 14 (fix) | ATR-Fenster für Pullback-Band | Nein (fix) |\n| `trend_regime_window` | 60 (geteilter 60er) | **200** | 60 | 500 | Fenster für strategie-internes Trend-Regime | **Ja** |\n| `regime_vol_override` | true (geteilt) | **false** | — | — | false=V1.1 strategie-eigene Richtung | Nein (fix) |\n| `rr_multiplier` | 2.0 | **2.0** | 1.0 | 4.0 | Target = Entry + rr_multiplier×Risiko | **Ja** |\n| `min_risk_reward` | 1.0 | **1.0** | 1.0 (min) | 3.0 | Mindest-RR; keine Kandidaten mit Chance/Risiko <1 | Nein (fix) |\n\n**Zusätzliche Trend-Parameter (produktiv, V1.1 fix — NICHT von Modul-13 optimieren):**\n\n| Parameter | V1 produktiv (Code) | V1.1 | Zweck |\n|---|---|---|---|\n| `adx_trend_threshold` | **20.0** (config.py:54) | fix 20.0 | ADX-Schwelle für echten Trend |\n| `adx_period` | **14** (config.py:50) | fix 14 | ADX-Indikatorfenster |\n| `ema_fast_period` | **20** (config.py:36) | fix 20 | Fast-EMA (Strategie-Pullback-Basis) |\n| `ema_slow_period` | **30** (config.py:47) | fix 30 | Slow-EMA (Regime-Flanke) |\n\n## 5. V1-Kompatibilitätsmodus (bitgenau V1)\n\n```\npullback_band_atr = 0.0\nregime_vol_override = true\ntrend_regime_window = 60\n+ alle übrigen V1-Parameter auf produktiven Defaults\n```\n⇒ muss V1 **bitgenau** reproduzieren (Rückwärtskompatibilität; Backtest ohne\n`strategy_params` bleibt fachlich identisch). **E2E-verifiziert**: V1-Kompatmodus\nLONG+SHORT bitgenau (net 200.0, target r=2.0).\n\n## 6. Abgrenzung / No-Go\n\n- **Keine KI/LLM** in V1.1, keine Live-Orders.\n- Produktions-Regime-Schwellen (`adx_trend_threshold=20`, `trend_min_ema_gap=0.02`,\n `high_vol_atr_ratio=0.03`, `low_vol_atr_ratio=0.008`) werden **nicht verändert**.\n- `atr_period`, ADX-/EMA-Perioden, `min_risk_reward` werden in dieser ersten\n Optimierungsrunde **nicht** über Modul-13 optimiert.\n\n---\n## Geändert\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: V1.1-Spec von ENTWURF auf DEPLOYT aktualisiert (backtesting:0.1.2); V1-Kompatmodus bitgenau E2E-verifiziert.\n```\n"
},
{
"path": "notes/trading/trading.md",
"title": "Trading Systeme",
"id": "object/8b362849-2a5e-4a07-42f0-557a58e1cf14",
"type": "arch",
"role": "index",
"representation": "standalone",
"state": "current",
"knowledge_schema": "1",
"content_hash": "34c249bc67fe0a35701e825d77aa529bf8f80669166f7c58bc30960d63c3a7f4",
"body": "\n# Trading Systeme\n\nMT5-Expert-Advisors und Infrastruktur von Christian List.\n\n## VPS\n\n- Host: 187.124.31.123\n- Deploy-Ort: `/opt/trading-modules/`\n- Netzwerk: `trading-modules_trading-modules`\n- SSH: Port 22, Key `id_ed25519_deploy`\n\nSiehe [[vps-infrastruktur]].\n\n## Module\n\n- Modul 15: Monitoring & Control (autoritativ für Trading-State)\n- Modul 09: Execution Service (ControlClient, FAIL-CLOSED)\n- M15→M09: Control-Anbindung freigegeben (Commit `d6f09c8`)\n"
},
{
"path": "ports-reference.md",
"title": "ports-reference",
"id": "object/09fd796f-51b6-4104-bc69-5f9dc79db0d6",
"type": "arch",
"role": "reference",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "ddda89a7a6a012fc7d6c36d63e12ba44d49e7a93543d20abbe46cc14b9e670ef",
"body": "# Ports & Service-Referenz — Trading-System VPS\n\n> Stand: 20.08.2026 · VPS: `187.124.31.123` · SSH: Public-Key-Auth (nur)\n\n## Netzwerk & Core-Infrastruktur\n- **Netzwerk:** `trading-modules` (bridge) — Kommunikation über Docker-interne Service-Hostnamen, keine festen IPs.\n- **Host:** Hostinger-VPS, 300 GB Disk, ~31 GiB RAM.\n\n## Feste Port-Zuordnung (Schema `55NNN` nach Modulnummer)\n| Modul | Container | Service | Host-Port | Öffentlich? |\n|-------|-----------|---------|-----------|-------------|\n| 01 | Modul-01-PostgreSQL | PostgreSQL | **keiner (intern, `expose`)** | **nein** (nur intern) |\n| 02 | Modul-02-RabbitMQ | RabbitMQ AMQP | **keiner (intern, `expose`)** | **nein** (nur intern) |\n| 02 | Modul-02-RabbitMQ | RabbitMQ Management | **keiner (intern, `expose`)** | **nein** (nur intern) |\n| 03 | Modul-03-Market-Data | FastAPI | 55003 (intern, `expose`) | **nein** (nur intern) |\n| 04 | Modul-04-Market-Regime | FastAPI | 55004 (intern, expose) | nein (nur intern) |\n| 05 | Modul-05-Strategy-Engine | FastAPI | 55005 (intern, expose) | nein (nur intern) |\n| 0612 | Modul-06…12 | FastAPI | 5500655012 (intern, expose) | nein (nur intern) |\n| 13 | Modul-13-Optimization | FastAPI | 55013 (intern, expose) | nein (nur intern) |\n| 1417 | … | — | 5501455017 | (Platzhalter) |\n\n> **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.\n\n## Weitere Dienste (Host)\n| Dienst | Container | Port |\n|--------|-----------|------|\n| Forgejo | forgejo-c4u8yyi1eaz1gepn3pqmr5fb | 3000 (HTTP) / 22222 (SSH) |\n| Tolaria (Second Brain) | tolaria | 5173 |\n| OpenClaw Alice | openclaw-suqw-openclaw-1 | 55163 |\n| OpenClaw Matt | openclaw-3sgu-openclaw-1 | 54524 |\n| Hermes Rain | hermes-workspace-e6un… | 32776 (UI) |\n| Ollama | ollama-nb6d-ollama-1 | 11434 (intern) |\n| n8n | n8n | (Coolify-managed) |\n| Traefik | traefik | 80/443 |\n\n## Docker-interne Hostnamen (wichtig für Container-Kommunikation)\n- PostgreSQL: `Modul-01-PostgreSQL:5432`\n- RabbitMQ: `Modul-02-RabbitMQ:5672` (vhost `trading`)\n- Ollama: `ollama-nb6d-ollama-1:11434`\n\n## Sicherheits-Notizen\n- **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.\n- Zugangsdaten ausschließlich als Env-Variablen/Secrets, nie im Code.\n- Öffentliche Ports nur, wo nötig (Admin/Debug/UI).\n\n---\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: Modul-01/02 von öffentlichen Ports auf interne `expose`-Bindings umgestellt (Security-Fix), Doku aktualisiert.\n```\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: ports-reference aktualisiert — Modul-04 und Modul-05 von Platzhalter auf real (FastAPI, intern expose, nicht öffentlich) eingetragen.\n\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: ports-reference aktualisiert — Modul-0612 als real (FastAPI, intern) und Modul-13 Optimization (55013 intern) eingetragen.\n"
},
{
"path": "trend-pullback-v1.1-spec.md",
"title": "trend-pullback-v1.1-spec",
"id": "object/91c7786c-cffa-1197-0837-e55a87cdc161",
"type": "design",
"role": "design",
"representation": "source",
"state": "current",
"knowledge_schema": "1",
"content_hash": "9c2acad66c7ef37110742c379737587c48ff44494babe5d044d84b312ede3916",
"body": "# trend_pullback_v1.1 — Spezifikation\n\n> Status: **IMPLEMENTIERT + DEPLOYT** (`backtesting:0.1.2`, Modul-12), V1-Kompatibilitätsmodus bitgenau.\n> Erstellt: 20.08.2026 (Rain Ocampo) · Aktualisiert: 20.08.2026\n\n## 1. Motivation\n\nDie Produktions-Strategie `trend_pullback_v1` (Modul-05/12) ist unter den\n**unveränderten** produktiven Regime-Schwellen fachlich widersprüchlich\n(messbar): Ein TREND_UP-Regime verlangt Volatilität, der Pullback\n`close < EMA20` verlangt ein flaches Plateau. Im 60-Bars-Produktionsfenster\n(`regime_lookback=60`) schneiden sich `TREND_UP` und `close<EMA20` nur in\n**14 von 1620** Kerzen (m13_evidence.py, Messwerte 20.08.2026). Die\nVol-Override (`HIGH_VOL`/`LOW_VOL`) kippt das Regime bei jedem sinnvollen\nPullback (>95% der `close<EMA20`-Kerzen werden `LOW_VOL`).\n\n**V1.1 trennt Richtung und Pullback-Timing.** Richtung wird aus einem\nstrategie-internen, längeren Fenster berechnet (nicht durch Vol-Override\nkippbar); Volatilität fließt nur noch ins Pullback-Timing (ATR-Band um EMA20)\nein.\n\n## 2. Kernentscheidungen\n\n1. **V1.1 bleibt zustandslos.**\n Trendrichtung wird **jede Bar neu** aus dem aktuellen Fenster berechnet.\n **Kein Sticky-State.** Kein Hidden-State. Begründung: einfacher, transparenter\n und reproduzierbarer (deterministisch nach Seed/Replay).\n\n2. **Kein kurzfristiger Vol-Override auf die Richtung.**\n `regime_vol_override=false` (V1.1-Default): Die Trend-Richtung wird\n strategie-intern berechnet und **nicht** durch `HIGH_/LOW_VOLATILITY`\n überschrieben. Volatilität beeinflusst nur das Pullback-Timing (Band).\n\n3. **Optimierungsraum für V1.1 bewusst klein.** Erste Iteration optimiert\n ausschließlich `pullback_band_atr`, `trend_regime_window`, `rr_multiplier`.\n ADX-/EMA-Perioden und `atr_period` bleiben **fix** auf produktiven V1-Werten.\n\n## 3. Pseudo-Regel (Variante A, lookahead-frei)\n\nStrategie-intern, jede Bar, aus geordneten `prev_*`-Serien (kein Lookahead,\nkein versteckter Zustand):\n\n```\n# --- Trend-Richtung (Fenster = trend_regime_window) ---\nema_fast_l = EMA(prev_closes, ema_fast_period, lookback=trend_regime_window)\nema_slow_l = EMA(prev_closes, ema_slow_period, lookback=trend_regime_window)\nadx_l = ADX(prev_highs, prev_lows, prev_closes, adx_period, lookback=trend_regime_window)\n\ntrend_dir = UP wenn (ema_fast_l > ema_slow_l) UND adx_l >= adx_trend_threshold\ntrend_dir = DOWN wenn (ema_fast_l < ema_slow_l) UND adx_l >= adx_trend_threshold\nsonst: kein Trade # KEIN atr_ratio-Check für die Richtung\n\n# --- Pullback-Timing (ATR-Band um EMA20) ---\natr_prev = ATR(prev_highs, prev_lows, prev_closes, atr_period)\nema20 = EMA20(prev_closes)\npullback_limit = ema20 + pullback_band_atr * atr_prev # LONG\npullback_limit = ema20 - pullback_band_atr * atr_prev # SHORT\n\nLONG wenn trend_dir==UP UND last_close>sma200 UND last_close < pullback_limit\n UND last_close>prev_close UND swing_low<last_close UND rr>=min_risk_reward\nSHORT wenn trend_dir==DOWN UND last_close<sma200 UND last_close > pullback_limit\n UND last_close<prev_close UND swing_high>last_close UND rr>=min_risk_reward\n```\n\n- `break_high` entfällt (Befund: `prev_high ≥ prev_close` ⇒ `break_high ⇒ close_up`;\n bleibt als `close_up`/`close_down` erhalten — nicht restriktiv).\n- Deterministisch: reine Listen-Berechnung, kein Zufall, kein Hidden-State.\n\n## 4. Parameter-Tabelle\n\n| Parameter | V1 produktiv (Code) | V1.1 Default | Min | Max | Zweck | V1.1 Optimierbar? |\n|---|---|---|---|---|---|---|\n| `pullback_band_atr` | — (strikt `< EMA20`) | **0.35** | 0.0 | 1.0 | ATR-Band um EMA20. `0.0`=strikt V1. | **Ja** |\n| `atr_period` | 14 | **14** | 14 (fix) | 14 (fix) | ATR-Fenster für Pullback-Band | Nein (fix) |\n| `trend_regime_window` | 60 (geteilter 60er) | **200** | 60 | 500 | Fenster für strategie-internes Trend-Regime | **Ja** |\n| `regime_vol_override` | true (geteilt) | **false** | — | — | false=V1.1 strategie-eigene Richtung | Nein (fix) |\n| `rr_multiplier` | 2.0 | **2.0** | 1.0 | 4.0 | Target = Entry + rr_multiplier×Risiko | **Ja** |\n| `min_risk_reward` | 1.0 | **1.0** | 1.0 (min) | 3.0 | Mindest-RR; keine Kandidaten mit Chance/Risiko <1 | Nein (fix) |\n\n**Zusätzliche Trend-Parameter (produktiv, V1.1 fix — NICHT von Modul-13 optimieren):**\n\n| Parameter | V1 produktiv (Code) | V1.1 | Zweck |\n|---|---|---|---|\n| `adx_trend_threshold` | **20.0** (config.py:54) | fix 20.0 | ADX-Schwelle für echten Trend |\n| `adx_period` | **14** (config.py:50) | fix 14 | ADX-Indikatorfenster |\n| `ema_fast_period` | **20** (config.py:36) | fix 20 | Fast-EMA (Strategie-Pullback-Basis) |\n| `ema_slow_period` | **30** (config.py:47) | fix 30 | Slow-EMA (Regime-Flanke) |\n\n## 5. V1-Kompatibilitätsmodus (bitgenau V1)\n\n```\npullback_band_atr = 0.0\nregime_vol_override = true\ntrend_regime_window = 60\n+ alle übrigen V1-Parameter auf produktiven Defaults\n```\n⇒ muss V1 **bitgenau** reproduzieren (Rückwärtskompatibilität; Backtest ohne\n`strategy_params` bleibt fachlich identisch). **E2E-verifiziert**: V1-Kompatmodus\nLONG+SHORT bitgenau (net 200.0, target r=2.0).\n\n## 6. Abgrenzung / No-Go\n\n- **Keine KI/LLM** in V1.1, keine Live-Orders.\n- Produktions-Regime-Schwellen (`adx_trend_threshold=20`, `trend_min_ema_gap=0.02`,\n `high_vol_atr_ratio=0.03`, `low_vol_atr_ratio=0.008`) werden **nicht verändert**.\n- `atr_period`, ADX-/EMA-Perioden, `min_risk_reward` werden in dieser ersten\n Optimierungsrunde **nicht** über Modul-13 optimiert.\n\n---\n## Geändert\n```\nGeändert von: Rain Ocampo\nDatum: 20.08.2026\nGrund: V1.1-Spec von ENTWURF auf DEPLOYT aktualisiert (backtesting:0.1.2); V1-Kompatmodus bitgenau E2E-verifiziert.\n```\n"
},
{
"path": "vps.md",
"title": "vps",
"id": null,
"type": "Type",
"role": null,
"representation": null,
"state": null,
"knowledge_schema": null,
"content_hash": "efbd2fda29dffc7249d998a5cadd6ea9b047cb0e660744e4c67ca4472f52d0b7",
"body": "\n# VPS\n"
}
]
}