Files
trade-kuns/docs/specs/2026-06-10-live-paper-engine-design.md

128 lines
6.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# trade-kuns — Live-Paper-Engine (Phase 3, Design)
**Datum:** 2026-06-10
**Status:** Umsetzung (User-Entscheidung: bewusster Paper-Probelauf)
**Basis:** Spec `2026-06-09-trade-kuns-design.md` §4.34.6, §6, §8
---
## 1. Kontext & Entscheidung
Das Walk-Forward-Gate wurde von keiner der 7 Varianten bestanden (siehe
`docs/walkforward-ergebnisse-2026-06-09.md`). Beste Variante: **long-only mit
fixen Spec-Default-Parametern** (Donchian 20 / ATR×3 / EMA 200 / ADX 20) —
OOS-PF 1.21, 249 Trades, MaxDD 16 %, Overfitting-Ratio 1.51; einziger
Fail: 11/32 Fenster mit PF < 0.5.
**Entscheidung (User, 2026-06-10):** Live-**Paper**-Engine bauen und diese
Variante im Paper-Probelauf validieren. Das Gate wird nicht aufgeweicht —
der Paper-Lauf *ist* die nächste Validierungsstufe. Kein echtes Geld, keine
Order-Ausführung, keine Schreib-API-Keys (Spec §10 unverändert).
## 2. Abweichungen von der Ursprungs-Spec
| Spec | Jetzt | Grund |
|---|---|---|
| Subdomain `trade.kuns.dev` | **`trading.kuns.dev`** | User-Vorgabe |
| Hono | **`Bun.serve`** | 6 GET-Routen, kein Framework nötig |
| Vue 3 + Vite Dashboard | **statische Single-Page** (Vanilla JS, Canvas-Chart) | kein Build-Step, kein Over-Engineering |
| Zitadel-JWT-Auth | **keine Auth** | API ist read-only (Paper-Daten, nichts Sensibles); Schreib-Endpoints (`toggle`/`reset`/`config`) entfallen in v1 |
| Port 8080 | 8080 | unverändert |
## 3. Architektur
Ein Bun-Prozess (`src/server/index.ts`): HTTP-Server + 5-min-Loop.
```
src/server/
live/
engine.ts LiveEngine: Zyklus-Orchestrierung, Recovery, Persistenz
process-cycle.ts pure Funktion: (candles, state) → actions (Entries/Exits/Decisions/Equity)
api/
server.ts Bun.serve: /health, /api/*, statisches Dashboard
db/schema.ts + positions, paper_trades, decision_logs, bot_state, equity_snapshots
public/index.html Dashboard
```
**Ein Code-Pfad-Prinzip bleibt:** `process-cycle.ts` nutzt exakt dieselben
puren Funktionen wie der Backtest-Runner (`computeIndicators`, `evaluateAt`,
`updateChandelier`, `sizePosition`, `Portfolio`) mit identischer Semantik:
4h-Entries (Close > Donchian-High(20) ∧ Close > EMA-200 ∧ ADX ≥ 20),
15m-Stop-Checks (Low ≤ Stop → Exit, Gap → Open als schlechterer Fill),
Chandelier-Update pro abgeschlossener 4h-Bar, Fees 0.1 % + Slippage 5 bps.
## 4. Zyklus (alle 5 Minuten)
1. **Fetch:** je Pair neueste 15m-Candles von Crypto.com (Lücke seit Cursor,
`end_ts`-Paginierung wie Backfill, max 300/Request), nur abgeschlossene
(`ts + 15m ≤ now`) → `candles` (Dedup via PK).
2. **Load:** je Pair letzte ~6500 15m-Candles (≈ 400 4h-Bars — Warmup für
EMA-200 + Donchian) aus DB.
3. **Process** (pure, deterministisch): alle 15m-Candles mit `ts > cursor`
chronologisch gemergt (Tie-Break PAIRS-Reihenfolge wie Runner):
- neue abgeschlossene 4h-Bars des Pairs: Chandelier-Update → Entry-Evaluation
(jede Evaluation → DecisionLog, inkl. Blockierungsgrund/Sizing-Block)
- 15m-Stop-Check der offenen Position
- Equity-Punkt einmal pro 4h-Bucket
4. **Persist** (eine Transaktion): Positions-Upsert/Delete, Trades,
DecisionLogs, Equity-Snapshots, `bot_state` (cash, cursor).
5. **Outcome-Backfill:** `decision_logs` mit NULL-Outcomes füllen, sobald
Candles 4h/24h/72h später vorliegen (Edge-Frühindikator, Spec §5.5).
**Initialisierung (erster Start):** `bot_state` mit 1000 USDT Cash,
Cursor = neueste abgeschlossene 15m-Candle. Keine historische Replay —
der erste mögliche Entry ist der nächste frische 4h-Close.
**Restart-Recovery (Spec §6):** Positionen + Cash + Cursor aus DB; verpasste
Candles werden nachgeholt und Stops rückwirkend geprüft — identisch zum
Normalzyklus, da der Prozess-Schritt nur vom Cursor abhängt.
**Fehler:** API-Fehler eines Pairs → Pair in diesem Zyklus überspringen,
andere laufen weiter; Fetch-Retry (3×, Backoff) existiert im Client.
DB-Fehler → Zyklus abbrechen, Status rot. Überlappende Zyklen durch
`running`-Flag verhindert. Letzter Zyklus-Status sichtbar in `/api/portfolio`.
## 5. Datenbank (neu)
- `positions` — pair (PK), side, qty, entry_ts, entry_price, entry_cost,
initial_stop, stop, trail_extreme, risk_amount
- `paper_trades` — id, pair, side, entry/exit (ts+price), qty, pnl, r, exit_reason
- `decision_logs` — id, pair, bar_ts (4h), signal, blocked_by, close, atr, adx,
donchian_high, trend_ema, price_after_4h/24h/72h (nullable), unique(pair, bar_ts)
- `bot_state` — id=1, cash, start_capital, cursor_ts, updated_at
- `equity_snapshots` — ts (PK, 4h-Bucket), equity, cash
## 6. API & Dashboard
| Route | Inhalt |
|---|---|
| `GET /health` | `{ok, lastCycle, cycleError}` — Coolify-Healthcheck |
| `GET /api/portfolio` | Equity, Cash, offene Positionen (inkl. Stop, unrealized PnL), Zyklus-Status |
| `GET /api/trades?limit` | abgeschlossene Trades, neueste zuerst |
| `GET /api/decisions?pair&limit` | DecisionLog inkl. Outcomes |
| `GET /api/stats` | PF, WinRate, MaxDD, avgR, Trade-Anzahl, Equity-Kurve |
| `GET /api/candles?pair&tf=15m|4h&limit` | Candles fürs Dashboard |
Dashboard: eine statische Seite, 30-s-Polling. KPI-Leiste (Equity, PnL, PF,
WinRate, MaxDD), Equity-Kurve (Canvas), Tabellen: offene Positionen, Trades,
letzte Decisions je Pair.
## 7. Tests
- `process-cycle`: Determinismus; Entry auf 4h-Close; Stop-Check inkl.
Gap-Fill; Cursor-Idempotenz (zweiter Lauf ohne neue Candles = no-op);
Restart-Äquivalenz (ein Lauf über N Candles ≡ zwei Läufe mit Cut dazwischen).
- Engine-Persistenz gegen Test-DB wird nicht automatisiert (kein CI mit DB);
manuelle Verifikation beim Deploy.
## 8. Deployment
- Dockerfile `oven/bun:1.3`, Start: Migrationen → Server, Port 8080,
Healthcheck `/health`.
- Coolify-App `trade-kuns`, Domain `https://trading.kuns.dev`, Netz `coolify`.
- `DATABASE_URL=postgres://mika:…@l8kogcggsc80sgcgk8kswww4:5432/tradekuns`
(shared-postgres über Coolify-Docker-Netz; vom Host aus weiterhin
`localhost:54320`).
- Backfill läuft weiter vom Host (`bun run backfill`) oder implizit über den
Lücken-Fetch des Loops.