#!/usr/bin/env python3 """DOLPHIN trade audit — reconstructs capital from log, compares to live HZ. Run: source /home/dolphin/siloqy_env/bin/activate && python trade_audit.py """ import json, re, sys, time from pathlib import Path from datetime import datetime, timezone TRADER_LOG = Path("/mnt/dolphinng5_predict/prod/supervisor/logs/nautilus_trader.log") INITIAL_CAP = 25_000.0 # from engine config RE_ENTRY = re.compile(r"\[(.+?)\] ENTRY: (.+)") RE_EXIT = re.compile(r"\[(.+?)\] EXIT: (.+)") GREEN = "\033[32m"; RED = "\033[31m"; YELLOW = "\033[33m" CYAN = "\033[36m"; BOLD = "\033[1m"; DIM = "\033[2m"; RST = "\033[0m" def _parse_dict(s): """Parse Python dict repr (single quotes) or JSON.""" try: return json.loads(s) except Exception: try: return json.loads(s.replace("'", '"').replace("nan", "null").replace("True","true").replace("False","false")) except Exception: return {} def parse_trades(log_path): lines = log_path.read_text(errors="replace").splitlines() entries = {} trades = [] for line in lines: m = RE_ENTRY.search(line) if m: d = _parse_dict(m.group(2)) if d.get("trade_id"): entries[d["trade_id"]] = {"entry_ts": m.group(1), **d} m = RE_EXIT.search(line) if m: d = _parse_dict(m.group(2)) tid = d.get("trade_id") if tid and tid in entries: e = entries.pop(tid) trades.append({**e, "exit_ts": m.group(1), "reason": d.get("reason", "?"), "pnl_pct": d.get("pnl_pct"), "net_pnl": d.get("net_pnl"), "bars_held": d.get("bars_held", 0), }) # Open (no exit yet) open_trades = list(entries.values()) return trades, open_trades def hz_capital(): try: import hazelcast hz = hazelcast.HazelcastClient( cluster_name="dolphin", cluster_members=["localhost:5701"], connection_timeout=3.0) raw = hz.get_map("DOLPHIN_STATE_BLUE").blocking().get("engine_snapshot") hz.shutdown() if raw: d = json.loads(raw) return d.get("capital"), d.get("trades_executed") except Exception as e: print(f"{YELLOW}HZ unavailable: {e}{RST}") return None, None def main(): print(f"\n{BOLD}{CYAN}🐬 DOLPHIN TRADE AUDIT{RST} {DIM}{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}{RST}\n") trades, open_trades = parse_trades(TRADER_LOG) print(f"Parsed {len(trades)} completed trades, {len(open_trades)} open.\n") # ── Trade-by-trade reconstruction ──────────────────────────────────────── capital = INITIAL_CAP wins = losses = skipped = 0 total_pnl = 0.0 peak_cap = INITIAL_CAP max_dd = 0.0 consecutive_losses = 0 max_consec_loss = 0 print(f"{'#':>4} {'Date':>16} {'Asset':<12} {'Lev':>5} {'Notional':>10} {'PnL $':>10} {'PnL %':>7} {'Reason':<16} {'Capital':>12}") print("─" * 115) for i, t in enumerate(trades, 1): pnl_pct = t.get("pnl_pct") net_pnl = t.get("net_pnl") notional= t.get("notional") lev = t.get("leverage", 0) or 0 asset = t.get("asset", "?") reason = t.get("reason", "?") ts = t.get("entry_ts", "")[:16].replace("T", " ") # Reconstruct net_pnl if missing (nan from early runs) if net_pnl is None and pnl_pct is not None and notional is not None: net_pnl = pnl_pct * notional if net_pnl is None: skipped += 1 pnl_str = f"{YELLOW}nan{RST}" print(f"{i:>4} {ts:>16} {asset:<12} {lev:>5.2f} {'nan':>10} {pnl_str:>10} {'?':>7} {reason:<16} {capital:>12,.2f} {DIM}(skipped — nan){RST}") continue capital += net_pnl total_pnl += net_pnl peak_cap = max(peak_cap, capital) dd = (peak_cap - capital) / peak_cap * 100 max_dd = max(max_dd, dd) if net_pnl >= 0: wins += 1 consecutive_losses = 0 pc = GREEN else: losses += 1 consecutive_losses += 1 max_consec_loss = max(max_consec_loss, consecutive_losses) pc = RED notional_str = f"${notional:,.0f}" if notional else "?" pnl_pct_val = pnl_pct * 100 if pnl_pct is not None else 0 print(f"{i:>4} {ts:>16} {asset:<12} {lev:>5.2f} {notional_str:>10} " f"{pc}{net_pnl:>+10.2f}{RST} {pc}{pnl_pct_val:>+6.2f}%{RST} " f"{reason:<16} {capital:>12,.2f}") # ── Summary ─────────────────────────────────────────────────────────────── completed = wins + losses wr = wins / completed * 100 if completed else 0 roi = (capital - INITIAL_CAP) / INITIAL_CAP * 100 print("─" * 115) print(f"\n{BOLD}SUMMARY{RST}") print(f" Completed trades : {completed} ({skipped} skipped — pre-fix nan)") print(f" Wins / Losses : {GREEN}{wins}{RST} / {RED}{losses}{RST} → WR: {GREEN if wr>=50 else RED}{wr:.1f}%{RST}") print(f" Total PnL : {GREEN if total_pnl>=0 else RED}{total_pnl:+,.2f}{RST}") print(f" Initial capital : ${INITIAL_CAP:,.2f}") print(f" Audit capital : {GREEN if capital >= INITIAL_CAP else RED}${capital:,.2f}{RST}") print(f" Audit ROI : {GREEN if roi>=0 else RED}{roi:+.3f}%{RST}") print(f" Peak capital : ${peak_cap:,.2f}") print(f" Max drawdown : {RED if max_dd>20 else YELLOW if max_dd>10 else GREEN}{max_dd:.2f}%{RST}") print(f" Max consec.loss : {max_consec_loss}") if open_trades: print(f" {YELLOW}Open positions : {len(open_trades)} (not counted in audit){RST}") for ot in open_trades: print(f" → {ot.get('asset')} lev:{ot.get('leverage',0):.2f}x notional:{ot.get('notional','?')}") # ── HZ comparison ───────────────────────────────────────────────────────── print(f"\n{BOLD}LIVE HZ COMPARISON{RST}") hz_cap, hz_trades = hz_capital() if hz_cap is not None: diff = hz_cap - capital match = abs(diff) < 1.0 mc = GREEN if match else (YELLOW if abs(diff) < 50 else RED) print(f" HZ capital : ${hz_cap:,.2f}") print(f" Audit capital: ${capital:,.2f}") print(f" Difference : {mc}{diff:+,.2f}{RST} {'✓ MATCH' if match else '⚠ MISMATCH'}") print(f" HZ trades : {hz_trades} Audit trades: {completed} completed + {skipped} skipped") else: print(f" {YELLOW}HZ not available — run standalone to compare{RST}") print() if __name__ == "__main__": main()