← Back
"""
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}

📜 Git History

c6f6bd5chore: initial commit — version control setup5 weeks ago
Show last diff
Loading...