Files
DOLPHIN/Observability/trade_audit.py

169 lines
7.0 KiB
Python
Raw Normal View History

#!/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()