169 lines
7.0 KiB
Python
169 lines
7.0 KiB
Python
|
|
#!/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()
|