"""
Trade Log — JSON journal of all trading actions.
Records entries, partial closes, SL hits, and calculates PnL.
Strategy-agnostic: event types are prefixed per strategy (e.g. SCALP_ENTRY, FUND_SL_HIT).
"""
import json
import os
import logging
from datetime import datetime, timezone, timedelta
from src.config import DATA_DIR
logger = logging.getLogger(__name__)
TRADE_LOG_FILE = os.path.join(DATA_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."""
os.makedirs(DATA_DIR, exist_ok=True)
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) -> dict:
"""
Append event to trade log.
event_type should be prefixed with strategy name:
e.g. FUND_ENTRY, FUND_TP1_HIT, FUND_SL_HIT, FUND_MANUAL_CLOSE
"""
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', '?')}")
return event
def get_open_trades(prefix: str = "") -> list[dict]:
"""
Get entries that don't have a closing event — for recovery on restart.
Args:
prefix: strategy prefix to filter (e.g. "FUND_"). Empty = all.
"""
log = load_trade_log()
open_symbols = {}
close_suffixes = {"SL_HIT", "TP3_HIT", "MANUAL_CLOSE", "TP", "TIME_STOP"}
for event in log:
symbol = event.get("symbol", "")
evt = event.get("event", "")
if prefix and not evt.startswith(prefix):
continue
if evt.endswith("ENTRY"):
open_symbols[symbol] = event
else:
# Check if this is a close event
for suffix in close_suffixes:
if evt.endswith(suffix) and symbol in open_symbols:
del open_symbols[symbol]
break
return list(open_symbols.values())
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_strategy_summary(prefix: str, period: str = "all") -> dict:
"""
Calculate PnL summary for a specific strategy.
Args:
prefix: strategy event prefix (e.g. "FUND_", "SCALP_")
period: "today", "week", "month", "all"
"""
log = load_trade_log()
filtered = _filter_by_period(log, period)
# Only events for this strategy
events = [e for e in filtered if e.get("event", "").startswith(prefix)]
entries = [e for e in events if e.get("event", "").endswith("ENTRY")]
# Sum up PnL from all close events
total_pnl = 0.0
for e in events:
if "pnl_usdt" in e:
total_pnl += e["pnl_usdt"]
elif "realized_pnl_usdt" in e:
total_pnl += e["realized_pnl_usdt"]
# Count wins/losses by trade_id
trades = {}
for e in events:
tid = e.get("trade_id", "")
if not tid:
continue
if tid not in trades:
trades[tid] = {"won": False, "closed": False}
evt = e.get("event", "")
if "TP" in evt and "HIT" in evt:
trades[tid]["won"] = True
if evt.endswith("TP"):
trades[tid]["won"] = True
trades[tid]["closed"] = True
for suffix in ("SL_HIT", "TP3_HIT", "MANUAL_CLOSE", "TIME_STOP"):
if evt.endswith(suffix):
trades[tid]["closed"] = True
# DCA strategy: DCA_CLOSE with pnl_usdt
if evt.endswith("CLOSE"):
trades[tid]["closed"] = True
pnl = e.get("pnl_usdt", 0)
if pnl > 0:
trades[tid]["won"] = True
closed = {k: v for k, v in trades.items() if v["closed"]}
total_closed = len(closed)
wins = sum(1 for v in closed.values() if v["won"])
losses = total_closed - wins
win_rate = (wins / total_closed * 100) if total_closed > 0 else 0
return {
"strategy": prefix.rstrip("_"),
"total_entries": len(entries),
"total_closed": total_closed,
"wins": wins,
"losses": losses,
"win_rate": round(win_rate, 1),
"total_pnl_usdt": round(total_pnl, 4),
"period": period,
}