# POSITION-SOURCE-OF-TRUTH · Read-only Analyse 2026-05-18 Frage: **Welche Datenquelle ist authoritative für welchen Aspekt einer Bot-Position?** Diese Analyse ist Voraussetzung für `STATE-EXCHANGE-RECONCILE-LOOP` und `STUCK-POSITION-ALERT-PATH`. Keine Code-Änderung, keine DB-Abfrage durchgeführt (postgres-Container nicht angetastet). --- ## Datenquellen-Inventar | # | Quelle | Pfad / Tabelle | Schreib-Owner | Update-Frequenz | |---|---|---|---|---| | 1 | `live_portfolio.json` `positions` | volume `clawbot-bot-data` | Bot main.py (paper_trade/live_trade) | jede scan_cycle (~2-3 min) | | 2 | `live_portfolio.json` `closed_trades` | dito | dito, bei SELL ok | bei jedem Close | | 3 | `dca_log.json` | dito | dca_manager | bei DCA Init / DCA Buy / Position Close | | 4 | `position_snapshots` Tabelle | postgres | bot via `emit_position_snapshot` | per scan_cycle (main.py:448) und bei BUY/SELL (main.py:408) | | 5 | `trade_logs` Tabelle | postgres | bot via `emit_trade` (main.py:363) | nur bei realisiertem Close | | 6 | Binance Testnet Account | exchange | exchange | real-time | | 7 | `command_audit` (Worker) | postgres | command_worker bei Manual-Close | bei CommandBus close_position | --- ## Authoritative-Mapping | Aspekt der Position | Authoritative Source | Begründung | |---|---|---| | **gibt es die Position überhaupt?** | Exchange (`fetch_balance`) | Single Source of Truth bei realer Ausführung. Bot-State kann durch BUY-Logging-Race oder testnet-Idiosyncrasie falsch sein (siehe SHIB) | | Quantity-on-bot-view | `live_portfolio.json.positions[X].quantity` | bot betreibt sein Trading auf dieser Zahl | | **Quantity-on-truth** | `exchange.fetch_balance(X)['free']` + `['used']` | nur das zählt für SELL-Ausführung | | **initial entry price** | bot_stdout.log `TESTNET BUY` line (kein State-Feld dafür!) | Average überschreibt initial in `live_portfolio.json.entry_price` nach erstem DCA → P1-C Befund | | average entry price | `live_portfolio.json.entry_price` *nach DCA* = `dca_log.json.avg_price` | beide sollten gleich sein; aktuell nur durch happy-path-Schreib-Pfad garantiert | | stop_loss / take_profit (current) | `live_portfolio.json.positions[X].stop_loss / .take_profit` | wird vom paper_trade.position_manager bei trailing / break_even / news-adjust mutiert | | stop_loss / take_profit (Genesis) | trade_logs.stop_loss / .take_profit (bei Close) — fehlt für offene Positionen | initial SL/TP nicht in dauerhafter Quelle für open positions | | DCA-Tranchen-Historie | `dca_log.json.positions[X].tranches[]` + bot_stdout.log Event-Linien | beide nötig: dca_log ist primary, log ist forensic-fallback | | DCA-Count / Pyramid-Count | `dca_log.json.dca_count / .pyramid_count` | wird von dca_manager geführt | | Realized-PnL (closed) | `live_portfolio.json.closed_trades[]` + `trade_logs` Tabelle | beide sollten 1:1 sein (DATA-LINK-1 erweitert) | | Open-PnL (current) | derived: `current_price * quantity - cost - fees` | nirgendwo persistiert; nur GUI-Rendering | | Position Lifecycle (open/closed/stuck) | `live_portfolio.json.positions` (membership) | aktuell binär: ist drin = open, sonst closed/stuck — `STUCK`-State fehlt | | Stuck-Detection | `_track_sell_failure` in live_trade.py | aktuell internes Counter ohne sichtbare Heilung — siehe P0-B | | Manual Close vs Auto Close | `live_portfolio.json.closed_trades[].reason` + `command_audit` | reason==`manual_close` ↔ command_audit hat passende Row | --- ## Bekannte Inkonsistenzen / Gaps ### G1 — Initial-Entry-Price nicht persistiert `paper_trade._new_pos()` schreibt `entry_price` ins `_pos`-Dict. Bei DCA-Rescue wird derselbe Slot mit dem neuen Average überschrieben (siehe `paper_trade.execute_dca`-Pfad, in dieser Session nicht geöffnet aber log-belegt durch „Neuer Avg: 0.1083 / 0.1077 / …"). Die initiale Buy-Preis-Information ist nach erstem DCA **nur noch im log** rekonstruierbar. **Impact:** Operator sieht im Dashboard nicht mehr, zu welchem Preis er „eingestiegen" ist. **Folge-Plan:** `INITIAL-ENTRY-PERSIST P2` — neuer Slot `_pos['initial_entry_price']`, einmalig beim ersten Buy gesetzt, nie überschrieben. ### G2 — Exchange-Reality nicht kontinuierlich reconciled Bot vertraut darauf, dass `TESTNET BUY` → Position existiert. Bei testnet-Idiosyncrasie / Minimum-Quantity-Filter / Settlement-Delay kann die Exchange-Realität abweichen. SHIB hat das demonstriert (22h Loop, weil Exchange `insufficient_funds` zurückgab, Bot-State ignorierte das). **Aktuell:** keine periodische Reconcile-Schleife. **Folge-Plan:** `STATE-EXCHANGE-RECONCILE-LOOP` (Roadmap-ID bereits gepinnt, commit `faa86b0`). ### G3 — DCA-Log Cleanup nur auf paper_trade-Pfad `🗑️ DCA Log für X bereinigt`-Hook nur in `paper_trade.execute_sell` (basierend auf Log-Verteilung). - CommandBus Manual-Close → kein Cleanup - State-Drift Reconcile → kein Cleanup - Pre-DATA-LINK-1-Closes → kein Cleanup **Impact:** 25/26 dca_log-Einträge sind aktuell Orphans (P1-B). **Folge-Plan:** `DCA-LOG-ORPHAN-REAPER P2`. ### G4 — Snapshot-Emit lässt Identifier-Felder leer Plan `PLAN_SNAPSHOT_EMIT_COMPLETENESS.md` ist bereits dokumentiert: per-scan-cycle `emit_position_snapshot` (main.py:448) gibt nur volatile Felder weiter, lässt `decision_id`, `opened_at`, `strategy_id`, `metadata` leer. **Impact:** position_snapshots-Tabelle ist nicht als Open-Position- Lebenszyklus-Quelle nutzbar (Vorbedingung für Dashboard / Analyse). **Folge-Plan:** existiert (`PLAN_SNAPSHOT_EMIT_COMPLETENESS.md`). Tests-Entwurf erstellbar ohne Bot-Touch (siehe TODO Punkt 4 weiter unten). ### G5 — `trade_logs.entry_price` ist Average, nicht Initial Verifikation siehe ALERTS P1-C. Gleiche Quelle wie G1. ### G6 — kein `STUCK_NO_EXCHANGE_BALANCE`-Lifecycle-State Position-Lifecycle ist heute binär: in `positions[]` (open) oder in `closed_trades[]` (closed). Es fehlt ein dritter Zustand für „exchange sagt: gibt's nicht". P0-A. --- ## Empfohlene Authoritative-Hierarchie (Soll-Zustand) Für eine spätere Reconcile-Implementierung: ``` Lifecycle Status: 1. Exchange.fetch_balance(symbol)['total'] > 0 → AKZEPTIERE state.positions 2. Exchange.fetch_balance(symbol)['total'] = 0 → STUCK (alert + freeze SL-emit) 3. state hat keinen Eintrag → CLOSED Quantity authoritative bei aktivem SELL: • exchange.['free'] (clamped) Average price authoritative: • dca_log.json.positions[X].avg_price • live_portfolio.json.positions[X].entry_price (sollte = avg) • bei Drift > 0.1% → ALERT Initial entry price authoritative: • (zukünftig) live_portfolio.json.positions[X].initial_entry_price • Fallback: bot_stdout.log TESTNET BUY line mit gleichem position_id Realized PnL authoritative: • trade_logs (postgres) • mirror: live_portfolio.json.closed_trades Manual-vs-Auto-Close: • command_audit (postgres) → wenn vorhanden = manual • sonst = auto (mit reason) ``` --- ## Was diese Analyse NICHT geprüft hat - DB `position_snapshots` und `trade_logs` direkte Cross-Reads (postgres-Container nicht angetastet — DB-Boundary respektiert). - `command_audit`-Inhalte für die KITE/SUI manual_closes. - Worker `live_trader` state-shadow (separater Reload-Pfad). - Bot-Watchdog / Notifier-Telegram-Pfad. Diese gehören in eine eigene Phase mit Operator-GO.