"""
Trade Log — JSON journal of all trading actions.
Records entries, partial closes, SL hits, and calculates PnL.
"""
import json
import os
import logging
from datetime import datetime, timezone, timedelta
logger = logging.getLogger(__name__)
TRADE_LOG_DIR = os.path.dirname(os.path.abspath(__file__))
TRADE_LOG_FILE = os.path.join(TRADE_LOG_DIR, "trade_log.json")
VANCOUVER_TZ = timezone(timedelta(hours=-7))
def now_van() -> datetime:
return datetime.now(VANCOUVER_TZ)
def load_trade_log() -> list[dict]:
"""Load trade log from file."""
if not os.path.exists(TRADE_LOG_FILE):
return []
try:
with open(TRADE_LOG_FILE, "r") as f:
return json.load(f)
except Exception as e:
logger.error(f"Failed to load trade log: {e}")
return []
def save_trade_log(log: list[dict]):
"""Save trade log to file."""
try:
with open(TRADE_LOG_FILE, "w") as f:
json.dump(log, f, indent=2, ensure_ascii=False)
except Exception as e:
logger.error(f"Failed to save trade log: {e}")
def log_event(event_type: str, data: dict):
"""
Append event to trade log.
event_type: ENTRY, TP1_HIT, TP2_HIT, TP3_HIT, SL_HIT, MANUAL_CLOSE, ERROR
data: symbol, price, quantity, pnl, etc.
"""
now = now_van()
event = {
"timestamp": now.isoformat(),
"timestamp_utc": datetime.now(timezone.utc).isoformat(),
"event": event_type,
**data,
}
log = load_trade_log()
log.append(event)
save_trade_log(log)
logger.info(f"Trade event: {event_type} | {data.get('symbol', '?')} | {data}")
return event
def get_open_trades() -> list[dict]:
"""Get entries that don't have a closing event — for recovery on restart."""
log = load_trade_log()
# Find symbols with ENTRY but no final close (SL_HIT, TP3_HIT, MANUAL_CLOSE)
open_symbols = {}
close_events = {"SL_HIT", "TP3_HIT", "MANUAL_CLOSE"}
for event in log:
symbol = event.get("symbol", "")
evt = event.get("event", "")
if evt == "ENTRY":
open_symbols[symbol] = event
elif evt in close_events and symbol in open_symbols:
del open_symbols[symbol]
return list(open_symbols.values())
def get_tp_state(symbol: str) -> dict:
"""Get TP state for a symbol from log (which TPs were hit)."""
log = load_trade_log()
state = {"tp1_hit": False, "tp2_hit": False, "current_sl_pct": None}
for event in reversed(log):
if event.get("symbol") != symbol:
continue
evt = event.get("event", "")
if evt == "ENTRY":
break # Found the entry, stop looking
if evt == "TP1_HIT":
state["tp1_hit"] = True
if evt == "TP2_HIT":
state["tp2_hit"] = True
if "new_sl_pct" in event:
state["current_sl_pct"] = event["new_sl_pct"]
return state
def _filter_by_period(log: list[dict], period: str) -> list[dict]:
"""Filter trade log events by time period."""
now = now_van()
if period == "today":
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
elif period == "week":
start = now - timedelta(days=7)
elif period == "month":
start = now - timedelta(days=30)
else:
return log
filtered = []
for e in log:
try:
ts = datetime.fromisoformat(e["timestamp"])
if ts >= start:
filtered.append(e)
except Exception:
continue
return filtered
def calculate_pnl_summary(period: str = "all") -> dict:
"""Calculate PnL summary for WT strategy (non-scalp trades)."""
log = load_trade_log()
filtered = _filter_by_period(log, period)
# Exclude scalp events
filtered = [e for e in filtered if not e.get("event", "").startswith("SCALP_")]
entries = [e for e in filtered if e.get("event") == "ENTRY"]
sl_hits = [e for e in filtered if e.get("event") == "SL_HIT"]
tp1_hits = [e for e in filtered if e.get("event") == "TP1_HIT"]
tp2_hits = [e for e in filtered if e.get("event") == "TP2_HIT"]
tp3_hits = [e for e in filtered if e.get("event") == "TP3_HIT"]
manual = [e for e in filtered if e.get("event") == "MANUAL_CLOSE"]
total_pnl = sum(e.get("realized_pnl_usdt", 0) for e in filtered if "realized_pnl_usdt" in e)
# Win/Loss: WIN = at least TP1 was hit before close
# Group by trade_id to determine per-trade outcome
trades = {} # trade_id -> {tp1, tp2, tp3, sl, pnl}
for e in filtered:
tid = e.get("trade_id", "")
if not tid:
continue
if tid not in trades:
trades[tid] = {"tp1": False, "tp2": False, "tp3": False, "sl": False, "closed": False}
evt = e.get("event", "")
if evt == "TP1_HIT":
trades[tid]["tp1"] = True
elif evt == "TP2_HIT":
trades[tid]["tp2"] = True
elif evt == "TP3_HIT":
trades[tid]["tp3"] = True
trades[tid]["closed"] = True
elif evt == "SL_HIT":
trades[tid]["sl"] = True
trades[tid]["closed"] = True
elif evt == "MANUAL_CLOSE":
trades[tid]["closed"] = True
closed_trades = {k: v for k, v in trades.items() if v["closed"]}
total_closed = len(closed_trades)
wins = sum(1 for v in closed_trades.values() if v["tp1"]) # At least TP1 hit = win
losses = total_closed - wins
win_rate = (wins / total_closed * 100) if total_closed > 0 else 0
return {
"total_entries": len(entries),
"total_closed": total_closed,
"wins": wins,
"losses": losses,
"win_rate": round(win_rate, 1),
"tp1_hits": len(tp1_hits),
"tp2_hits": len(tp2_hits),
"tp3_full_hits": len(tp3_hits),
"sl_hits": len(sl_hits),
"total_pnl_usdt": round(total_pnl, 2),
"period": period,
}
def calculate_scalp_summary(period: str = "all") -> dict:
"""Calculate PnL summary for Quick Take scalp strategy."""
log = load_trade_log()
filtered = _filter_by_period(log, period)
# Only scalp events
scalp = [e for e in filtered if e.get("event", "").startswith("SCALP_")]
entries = [e for e in scalp if e.get("event") == "SCALP_ENTRY"]
tp_hits = [e for e in scalp if e.get("event") == "SCALP_TP"]
sl_hits = [e for e in scalp if e.get("event") == "SCALP_SL"]
time_stops = [e for e in scalp if e.get("event") == "SCALP_TIME_STOP"]
manual = [e for e in scalp if e.get("event") == "SCALP_MANUAL"]
total_closed = len(tp_hits) + len(sl_hits) + len(time_stops) + len(manual)
# Wins = TP + positive time stops + positive manual
wins = len(tp_hits)
for e in time_stops + manual:
if e.get("pnl_pct", 0) >= 0:
wins += 1
losses = total_closed - wins
win_rate = (wins / total_closed * 100) if total_closed > 0 else 0
total_pnl = sum(e.get("pnl_usdt", 0) for e in scalp if "pnl_usdt" in e)
# Average trade duration
durations = [e.get("age_minutes", 0) for e in scalp if "age_minutes" in e]
avg_duration = sum(durations) / len(durations) if durations else 0
# Best and worst trade
pnls = [e.get("pnl_usdt", 0) for e in scalp if "pnl_usdt" in e]
best = max(pnls) if pnls else 0
worst = min(pnls) if pnls else 0
return {
"total_entries": len(entries),
"total_closed": total_closed,
"wins": wins,
"losses": losses,
"win_rate": round(win_rate, 1),
"tp_hits": len(tp_hits),
"sl_hits": len(sl_hits),
"time_stops": len(time_stops),
"total_pnl_usdt": round(total_pnl, 4),
"avg_duration_min": round(avg_duration, 1),
"best_trade": round(best, 4),
"worst_trade": round(worst, 4),
"period": period,
}
def calculate_gerchik_summary(period: str = "all") -> dict:
"""Calculate PnL summary for Gerchik Levels strategy."""
log = load_trade_log()
filtered = _filter_by_period(log, period)
# Only GR_ events
gr = [e for e in filtered if e.get("event", "").startswith("GR_")]
entries = [e for e in gr if e.get("event") == "GR_ENTRY"]
sl_hits = [e for e in gr if e.get("event") == "GR_SL_HIT"]
tp1_hits = [e for e in gr if e.get("event") == "GR_TP1_HIT"]
tp3_hits = [e for e in gr if e.get("event") == "GR_TP3_HIT"]
manual = [e for e in gr if e.get("event") == "GR_MANUAL_CLOSE"]
# Total PnL from close events
total_pnl = 0
for e in gr:
if "total_trade_pnl_usdt" in e:
total_pnl += e["total_trade_pnl_usdt"]
elif "total_pnl_usdt" in e:
total_pnl += e["total_pnl_usdt"]
# Win/Loss: WIN = at least TP1 hit
trades = {}
for e in gr:
tid = e.get("trade_id", "")
if not tid:
continue
if tid not in trades:
trades[tid] = {"tp1": False, "closed": False, "model": "?"}
evt = e.get("event", "")
if evt == "GR_ENTRY":
trades[tid]["model"] = e.get("model", "?")
elif evt == "GR_TP1_HIT":
trades[tid]["tp1"] = True
elif evt in ("GR_SL_HIT", "GR_TP3_HIT", "GR_MANUAL_CLOSE"):
trades[tid]["closed"] = True
closed_trades = {k: v for k, v in trades.items() if v["closed"]}
total_closed = len(closed_trades)
wins = sum(1 for v in closed_trades.values() if v["tp1"])
losses = total_closed - wins
win_rate = (wins / total_closed * 100) if total_closed > 0 else 0
# Count by model
model_counts = {"A": 0, "B": 0, "C": 0, "D": 0}
for e in entries:
m = e.get("model", "?")
if m in model_counts:
model_counts[m] += 1
return {
"total_entries": len(entries),
"total_closed": total_closed,
"wins": wins,
"losses": losses,
"win_rate": round(win_rate, 1),
"tp1_hits": len(tp1_hits),
"tp3_hits": len(tp3_hits),
"sl_hits": len(sl_hits),
"total_pnl_usdt": round(total_pnl, 2),
"model_a": model_counts["A"],
"model_b": model_counts["B"],
"model_c": model_counts["C"],
"model_d": model_counts["D"],
"period": period,
}
def calculate_combined_summary(period: str = "all") -> dict:
"""Combined stats for all strategies."""
wt = calculate_pnl_summary(period)
scalp = calculate_scalp_summary(period)
gr = calculate_gerchik_summary(period)
return {"wt": wt, "scalp": scalp, "gerchik": gr}