1014 lines
No EOL
443 KiB
JSON
1014 lines
No EOL
443 KiB
JSON
{
|
||
"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-03–17=`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 03–18\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 06–12 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-04–17 = 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=Entry−2×(Stop−Entry)` = 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` 2–3) | `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 0–100, 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 0–100)\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 0–100, 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 0–100; **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 04–08):** `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-04–08 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| 1–7 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 A–J Legacy-Hash `8a5760ae…`, M13 Shared A–J, 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→9–10, 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→8–10 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 05–10 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 1–7, 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 1–7 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. **M03–M06-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 M03–M14 (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```\nM03–M14 (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 (M03–M09) | 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- **M03–M06 (Market-Data-Pipeline) sind in `CRITICAL_SERVICES`** klassifiziert. Ausfall eines einzelnen Daten-Moduls führt so zu `HALTED` (über `CRITICAL_SERVICE_DOWN`), nicht ausschließlich über `MARKET_DATA_STALE`. Das ist fachlich konservativ aber zu klären (nur Data-Pipeline vs. Execution-Pfad).\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 0–15:\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 01–15, 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 A–J + 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\n−7 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~6–7 € (≈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-03–17=`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–17 | … | ⬜ Platzhalter (alpine) | 55016–55017 |\n\nDetails: siehe `modul-03-market-data.md` … `modul-15-monitoring-control.md` (Module 03–15\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 06–12 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 A–J 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 M03–M19; keine IG-Calls/Orders; kein Jahresbackfill; M20–M25 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 A–J ✅ (test_shared_repository)\n- Phase 5 20/20 ✅\n- Phase 6 14/14 ✅\n- Phase 7 A–J ✅\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-04–17 = 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=Entry−2×(Stop−Entry)` = 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` 2–3) | `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 0–100, 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 0–100)\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 0–100, 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 0–100; **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 04–08):** `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-04–08 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| 1–7 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 A–J Legacy-Hash `8a5760ae…`, M13 Shared A–J, 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→9–10, 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→8–10 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 05–10 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 1–7, 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 1–7 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. **M03–M06-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 M03–M14 (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```\nM03–M14 (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 (M03–M09) | 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- **M03–M06 (Market-Data-Pipeline) sind in `CRITICAL_SERVICES`** klassifiziert. Ausfall eines einzelnen Daten-Moduls führt so zu `HALTED` (über `CRITICAL_SERVICE_DOWN`), nicht ausschließlich über `MARKET_DATA_STALE`. Das ist fachlich konservativ aber zu klären (nur Data-Pipeline vs. Execution-Pfad).\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-03–15 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 (M03–15) 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 M03–16 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- **M03–M15 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: A–J (Legacy-Hash `8a5760…` unverändert)\n- M13 shared A–J\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 5–10d + 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 A–J): 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: A–J 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 A–N\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| 06–12 | Modul-06…12 | FastAPI | 55006–55012 (intern, expose) | nein (nur intern) |\n| 13 | Modul-13-Optimization | FastAPI | 55013 (intern, expose) | nein (nur intern) |\n| 14–17 | … | — | 55014–55017 | (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-06–12 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**1–4 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| 06–12 | Modul-06…12 | FastAPI | 55006–55012 (intern, expose) | nein (nur intern) |\n| 13 | Modul-13-Optimization | FastAPI | 55013 (intern, expose) | nein (nur intern) |\n| 14–17 | … | — | 55014–55017 | (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-06–12 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**1–4 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"
|
||
}
|
||
]
|
||
} |