"""
v2_stats_dashboard.py — Stats Dashboard 2° account (port 5511, Basic Auth)
NO template engine: tutto render server-side in Python (niente {{}} / {% %} nel template).
"""
import os
import sys
import json
import math
import sqlite3
import hmac
import secrets
import string
from datetime import datetime, timezone
from pathlib import Path
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse
from collections import defaultdict

# === CONFIG ===
ROOT = Path("/opt/charter-live/live_deploy_v2")
DB_PATH = ROOT / "logs" / "webhook_queue.db"
PORT = int(os.environ.get("V2_STATS_PORT", "5511"))
LISTEN_HOST = "0.0.0.0"
LOG_DIR = ROOT / "logs"
LOG_FILE = LOG_DIR / "v2_stats_dashboard.log"

# === AUTH (caricata da .v2_stats_creds via systemd EnvironmentFile) ===
def _load_auth_from_envfile():
    creds_file = ROOT / ".v2_stats_creds"
    if creds_file.exists():
        try:
            for line in creds_file.read_text().splitlines():
                line = line.strip()
                if line.startswith("#") or "=" not in line:
                    continue
                k, v = line.split("=", 1)
                k = k.strip()
                v = v.strip().strip('"').strip("'")
                if k == "V2_STATS_USER" and not os.environ.get("V2_STATS_USER"):
                    os.environ["V2_STATS_USER"] = v
                elif k == "V2_STATS_PASS" and not os.environ.get("V2_STATS_PASS"):
                    os.environ["V2_STATS_PASS"] = v
        except Exception:
            pass

_load_auth_from_envfile()
AUTH_USER = os.environ.get("V2_STATS_USER", "mattia_v2_stats")
AUTH_PASS = os.environ.get("V2_STATS_PASS", "")


def _parse_bybit_ts(ts):
    """Bybit V5 closed-pnl restituisce createdAt/updatedTime in MILLISECONDI come stringa
    (es. "1785856148609"). Accetta anche ISO 8601 ("2026-08-05T20:39:31Z") o None/"".
    Ritorna datetime (UTC) oppure None se non parsabile.
    """
    if ts is None:
        return None
    s = str(ts).strip()
    if not s:
        return None
    # Millisecondi: tutti digits e lunghezza >= 10
    if s.isdigit() and len(s) >= 10:
        try:
            ms = int(s)
            return datetime.fromtimestamp(ms / 1000.0, tz=timezone.utc)
        except Exception:
            return None
    # ISO 8601 fallback
    try:
        return datetime.fromisoformat(s.replace("Z", "+00:00"))
    except Exception:
        return None


def _fmt_bybit_ts(ts, fmt="%Y-%m-%d %H:%M:%S"):
    """Ritorna una stringa formattata (UTC) per ts Bybit (ms o ISO). None se non parsabile."""
    dt = _parse_bybit_ts(ts)
    if dt is None:
        return ""
    return dt.strftime(fmt)


def log(msg):
    ts = datetime.now(timezone.utc).astimezone().isoformat()
    line = f"[{ts}] {msg}"
    print(line, flush=True)
    try:
        LOG_DIR.mkdir(parents=True, exist_ok=True)
        with open(LOG_FILE, "a", encoding="utf-8") as f:
            f.write(line + "\n")
    except Exception:
        pass


# === DATA LOADERS ===
def get_bybit_positions():
    try:
        sys.path.insert(0, str(ROOT))
        from bybit_demo_client import BybitDemoClient
        c = BybitDemoClient()
        return c.fetch_positions()
    except Exception as e:
        log(f"bybit positions err: {e}")
        return []


def get_bybit_balance():
    try:
        sys.path.insert(0, str(ROOT))
        from bybit_demo_client import BybitDemoClient
        c = BybitDemoClient()
        b = c._request("GET", "/v5/account/wallet-balance",
                       {"accountType": "UNIFIED"}, signed=True)
        coin = b["result"]["list"][0]["coin"][0]
        return float(coin["walletBalance"])
    except Exception as e:
        log(f"bybit balance err: {e}")
        return 0.0


# Lista dei trade/posizioni di MIO TEST Pine-style da escludere dalla dashboard
MY_TEST_FILTER = {
    "symbols_qty": {
        # (symbol, qty) -> True se e' MIO test
        ("VIRTUALUSDT", 1500.0): True,   # VIRTUAL placeholder 1500 (mio test Pine-style)
        ("NEARUSDT", 599.9): True,         # NEAR mio test ma_trailing
    },
    "symbols": {
        "VIRTUALUSDT",  # symbol sospetto placeholder price=1.0
    },
}

# Data di partenza: 1° agosto 2026
SINCE_DATE_MS = int(__import__("datetime").datetime(2026, 8, 1, 0, 0, 0, tzinfo=__import__("datetime").timezone.utc).timestamp() * 1000)


def is_mio_test_trade(trade):
    """True se il trade chiuso Bybit corrisponde a un MIO test Pine-style."""
    sym = trade.get("symbol", "")
    qty = float(trade.get("closedSize", 0) or 0)
    if (sym, qty) in MY_TEST_FILTER["symbols_qty"] and (sym, qty) in MY_TEST_FILTER["symbols_qty"]:
        return True
    avg_entry = float(trade.get("avgEntryPrice", 0) or 0)
    # VIRTUAL placeholder Pine: avgEntry ~ 1.0 (placeholder Pine) o avgEntry 0.5711 (eseguito Bybit mio test)
    if sym == "VIRTUALUSDT" and abs(avg_entry - 1.0) < 1e-6 and abs(qty - 1500.0) < 0.01:
        return True
    if sym == "NEARUSDT" and abs(qty - 599.9) < 0.01:
        return True
    if sym == "BTCUSDT" and abs(qty - 0.023) < 1e-6:
        return True
    return False


def is_mio_test_position(pos):
    """True se la posizione aperta e' un MIO test Pine-style.
    FIX 2026-08-10 (Mattia): rimosso BTCUSDT 0.023 dal filtro perche' era una posizione reale
    del 2° account, non un test. Tenuto NEARUSDT 599.9 e VIRTUALUSDT 1500 (quei SI sono test)."""
    sym = pos.get("symbol", "")
    try:
        size = float(pos.get("size", 0) or 0)
    except (TypeError, ValueError):
        return False
    if sym == "NEARUSDT" and abs(size - 599.9) < 0.01:
        return True
    if sym == "VIRTUALUSDT" and abs(size - 1500.0) < 0.01:
        return True
    return False


def get_closed_trades_bybit():
    """Legge TUTTI i trade chiusi da Bybit API (closed-pnl endpoint), filtra dal 1° agosto,
    esclude MIEI test Pine-style.
    FIX 08/08 (Mattia): NO cursor-based paging perche' bybit_demo_client._request calcola
    firma sul cursor RAW ma `requests.get` ri-encoda il `%3A` -> err 10004.
    Uso finestre di 1 giorno: ~5 trade/giorno, no rischio limite 50."""
    out = []
    SYMBOL_STRATEGY_2ACC = {
        "BTCUSDT": "rettangolo",
        "ZECUSDT": "rettangolo",
        "WIFUSDT": "rettangolo",
        "RENDERUSDT": "rettangolo",
        "UNIUSDT": "rettangolo",
        "VIRTUALUSDT": "vptr3",
        "AXSUSDT": "vptr3",
        "ARUSDT": "rettangolo_simple",
        "NEARUSDT": "ma_trailing",
        "BEATUSDT": "ma_trailing",
    }
    # FIX 2026-08-09 (Mattia): simboli da ESCLUDERE dal calcolo KPI/stats (es. trade di test,
    # trade chiusi per errore, asset disabilitati). Aggiungere qui per escluderli completamente.
    EXCLUDED_SYMBOLS_STATS = {
        "NEARUSDT",  # 09/08: chiuso per errore da test live del fix close_intent, NON conta come trade reale
    }
    # FIX 2026-08-10 (Mattia): trade SPECIFICI da escludere (match su symbol+entry+qty).
    # Usare per escludere singoli trade in perdita anomala senza escludere l'asset intero.
    EXCLUDED_SPECIFIC_TRADES = [
        # AR LONG 850.5 @ 1.8147 aperto 09/08 18:50 per errore (alert Pine "Exit Short" interpretato male
        # dal sistema), chiuso 09/08 23:22 in perdita -22.78 USDT. Escluso per pulizia stats.
        {"symbol": "ARUSDT", "entry": 1.8147, "qty": 850.5},
    ]
    try:
        sys.path.insert(0, str(ROOT))
        from bybit_demo_client import BybitDemoClient
        c = BybitDemoClient()
        # Endpoint Bybit V5: GET /v5/position/closed-pnl
        now_ms = int(__import__("datetime").datetime.now(__import__("datetime").timezone.utc).timestamp() * 1000)
        # FIX 08/08: finestre 1gg (no cursor, no err 10004 doppio encoding)
        WINDOW_MS = 1 * 24 * 60 * 60 * 1000
        current_start = SINCE_DATE_MS
        while current_start < now_ms:
            window_end = min(current_start + WINDOW_MS, now_ms)
            params = {
                "category": "linear",
                "startTime": current_start,
                "endTime": window_end,
                "limit": 50,
            }
            r = c._request("GET", "/v5/position/closed-pnl", params, signed=True)
            items = (r.get("result") or {}).get("list", []) or []
            for it in items:
                if is_mio_test_trade(it):
                    continue
                sym_raw = it.get("symbol", "")
                if sym_raw in EXCLUDED_SYMBOLS_STATS:
                    continue
                # Check trade specifici esclusi (per danni anomali o test)
                qty_it = float(it.get("closedSize", 0) or 0)
                entry_it = float(it.get("avgEntryPrice", 0) or 0)
                if any(t["symbol"] == sym_raw and abs(t["entry"] - entry_it) < 1e-6 and abs(t["qty"] - qty_it) < 1e-3
                       for t in EXCLUDED_SPECIFIC_TRADES):
                    continue
                strat = SYMBOL_STRATEGY_2ACC.get(sym_raw, "vptr3")
                qty_t = float(it.get("closedSize", 0) or 0)
                entry_t = float(it.get("avgEntryPrice", 0) or 0)
                exit_t = float(it.get("avgExitPrice", 0) or 0)
                net_pnl = float(it.get("closedPnl", 0) or 0)
                # Fee: Bybit V5 fornisce openFee + closeFee (somma = totalFee)
                open_fee = float(it.get("openFee", 0) or 0)
                close_fee = float(it.get("closeFee", 0) or 0)
                # Se Bybit non restituisce fee separate, prova totalFee
                total_fee = float(it.get("totalFee", 0) or 0) or (open_fee + close_fee)
                # Gross PnL = Net PnL + Total Fee (fee detratte dal lordo)
                gross_pnl = net_pnl + total_fee
                # PnL % basato su nozionale entry
                notional_entry = abs(entry_t * qty_t)
                pnl_pct_gross = (gross_pnl / notional_entry * 100) if notional_entry > 0 else 0
                pnl_pct_net = (net_pnl / notional_entry * 100) if notional_entry > 0 else 0
                out.append({
                    "symbol": sym_raw,
                    "side": it.get("side", ""),
                    "qty": qty_t,
                    "entry_price": entry_t,
                    "exit_price": exit_t,
                    "pnl": net_pnl,
                    "pnl_net": net_pnl,
                    "pnl_gross": gross_pnl,
                    "fee": total_fee,
                    "pnl_pct_gross": pnl_pct_gross,
                    "pnl_pct_net": pnl_pct_net,
                    "strategy": strat,
                    "created_at": it.get("createdAt", ""),
                    "updated_at": it.get("updatedTime", ""),
                })
            current_start = window_end + 1
        return out
    except Exception as e:
        log(f"bybit closed trades err: {e}")
        return out


def get_open_positions_bybit_no_test():
    """Legge posizioni aperte da Bybit, ESCLUDE MIEI test Pine-style."""
    raw = get_bybit_positions()
    return [p for p in raw if not is_mio_test_position(p)]


def get_orders(limit=2000, since_date="2026-08-01"):
    """Carica ordini dal DB, filtrati per data >= since_date (default 1° agosto 2026)."""
    out = []
    if not DB_PATH.exists():
        return out
    try:
        conn = sqlite3.connect(DB_PATH)
        conn.row_factory = sqlite3.Row
        cur = conn.cursor()
        cur.execute(
            "SELECT order_id, symbol, side, qty, price, notional, strategy, created_at "
            "FROM orders WHERE created_at >= ? ORDER BY id DESC LIMIT ?",
            (since_date, limit)
        )
        for row in cur.fetchall():
            out.append(dict(row))
        conn.close()
    except Exception as e:
        log(f"orders load err: {e}")
    return out


def get_queue_stats():
    if not DB_PATH.exists():
        return {"pending": 0, "completed": 0, "failed": 0, "total": 0}
    try:
        conn = sqlite3.connect(DB_PATH)
        cur = conn.cursor()
        cur.execute("SELECT status, COUNT(*) FROM queue GROUP BY status")
        stats = {"pending": 0, "completed": 0, "failed": 0, "total": 0}
        for status, count in cur.fetchall():
            stats[status] = count
            stats["total"] += count
        conn.close()
        return stats
    except Exception as e:
        log(f"queue stats err: {e}")
        return {"pending": 0, "completed": 0, "failed": 0, "total": 0}


MY_TEST_ORDER_IDS = {
    "31a9c2fd-540e-45a5-b44f-fe2f8fdb4495",  # VIRTUAL 1500 buy placeholder (mio test)
    "4cbb1304-8020-42bd-bcad-8e5d0f19caed",  # NEAR 599.9 buy (mio test)
    "5c53a9ef-dcab-469e-bbd1-3057fe2746d9",  # BTC 0.023 buy (mio test)
    "8a53e7b1-c7d4-4e5a-b67b-4e4fbf4c0977",  # VIRTUAL 2496 buy (mio test)
    "2d2e625b-d3b2-4d87-931a-9b378cf8db93",  # VIRTUAL 2679 buy (mio test)
    "514f5553-6aa3-4148-8e1a-82961c47310b",  # VIRTUAL 2712 buy (mio test)
    "b426dc94-b72f-427b-b2f2-7e021f5da985",  # VIRTUAL 2496 buy (mio test)
    "4c8a4478-377e-4b8c-b371-44c64090e26d",  # VIRTUAL buy qty=0 (mio test)
    "f79ab621-dc17-488e-950e-e984fc0d9e43",  # VIRTUAL sell qty=0 (mio test)
    "2cd6e92c-b82b-4814-808e-e23579f1a533",  # VIRTUAL sell Max exit (chiude mio test)
    "e45f561b-c4d8-4045-a2fe-581140fadac1",  # VIRTUAL sell Max exit (chiude mio test 1500)
    "c60d098a-1ec5-4101-9e77-966f67f4f393",  # VIRTUAL sell Pine reale (Max exit di VIRTUAL 2632 Pine reale)
    "2d2e625b-d3b2-4d87-931a-9b378cf8db93",
    "79969acc-3e4b-4101-91bc-69eede1ef6aa",  # ZEC rettangolo (non mio)
    "44e4a3ad-23ec-4b7f-b6f8-35778858e5bf",  # BTC rettangolo (non mio)
    "7ed5cc82-3a99-4978-8ae3-c295e82f4609",  # BTC rettangolo (non mio)
}


def is_mio_test(o):
    """True se l'ordine e' un MIO test Pine-style (placeholder price=1.0, NEAR 599.9, o order_id in MY_TEST_ORDER_IDS)."""
    oid = str(o.get("order_id", ""))
    if oid in MY_TEST_ORDER_IDS:
        return True
    sym = o.get("symbol", "")
    try:
        qty = float(o.get("qty", 0) or 0)
        price = float(o.get("price", 0) or 0)
    except (TypeError, ValueError):
        return False
    # VIRTUAL Pine placeholder (price=1.0, qty variabile)
    if sym == "VIRTUALUSDT" and abs(price - 1.0) < 1e-9:
        return True
    # NEAR mio test (qty=600 placeholder Pine 2.5, oppure 599.9)
    if sym == "NEARUSDT" and qty >= 500:
        return True
    # BTC mio test (qty=0.023)
    if sym == "BTCUSDT" and abs(qty - 0.023) < 1e-6:
        return True
    return False


def compute_trade_pnls(orders):
    """Matcha buy+sell FIFO per symbol. Esclude trade che coinvolgono MIEI test Pine-style
    (placeholder price=1.0 o order_id in MY_TEST_ORDER_IDS)."""
    by_symbol = defaultdict(list)
    for o in sorted(orders, key=lambda x: x.get("created_at", "")):
        sym = o.get("symbol", "")
        if not sym:
            continue
        oid = o.get("order_id", "")
        if oid in ("no_position", "anti_dup_skip", "", None):
            continue
        if o.get("qty", 0) <= 0:
            continue
        # Filtra MIEI test
        if is_mio_test(o):
            continue
        by_symbol[sym].append(o)
    trades = []
    for sym, ords in by_symbol.items():
        open_buys = []
        for o in ords:
            side = o.get("side", "").lower()
            qty = float(o.get("qty", 0) or 0)
            price = float(o.get("price", 0) or 0)
            strat = o.get("strategy", "")
            ts = o.get("created_at", "")
            if side == "buy":
                placeholder = abs(price - 1.0) < 1e-9
                open_buys.append({"qty": qty, "price": price, "strategy": strat, "ts": ts, "placeholder": placeholder})
            elif side == "sell" and open_buys:
                qty_to_close = qty
                while qty_to_close > 0 and open_buys:
                    buy = open_buys[0]
                    matched = min(qty_to_close, buy["qty"])
                    pnl = (price - buy["price"]) * matched
                    pnl_pct = ((price - buy["price"]) / buy["price"]) * 100 if buy["price"] > 0 else 0
                    # Warning se PnL% estremo (>30%) o buy era placeholder Pine
                    warn = buy.get("placeholder", False) or abs(pnl_pct) > 30
                    trades.append({
                        "symbol": sym, "side": "long",
                        "qty": matched, "entry_price": buy["price"], "exit_price": price,
                        "pnl": pnl, "pnl_pct": pnl_pct,
                        "strategy": buy["strategy"], "entry_ts": buy["ts"], "exit_ts": ts,
                        "warn": warn,
                    })
                    qty_to_close -= matched
                    buy["qty"] -= matched
                    if buy["qty"] <= 1e-9:
                        open_buys.pop(0)
    return trades


def compute_kpis_from_bybit(closed_trades, positions, balance):
    """Calcola KPI da trade chiusi Bybit (gia filtrati per data e test) + posizioni aperte (gia filtrate)."""
    n = len(closed_trades)
    if n == 0:
        return {
            "n_trades": 0, "wins": 0, "losses": 0, "winrate": 0.0,
            "total_pnl": 0.0, "total_pnl_net": 0.0,
            "total_pnl_gross": 0.0, "total_fees": 0.0, "total_notional": 0.0,
            "total_losses": 0.0, "pnl_realized": 0.0,
            "avg_pnl": 0.0, "avg_pnl_gross": 0.0,
            "best_trade": 0.0, "worst_trade": 0.0,
            "open_positions": len(positions), "unrealized_pnl": 0.0,
            "balance": balance, "total_pnl_with_unrealized": 0.0,
            "total_pnl_gross_with_unrealized": 0.0,
            "trading_days": 0, "profit_factor": 0.0, "first_trade_date": "",
            "sharpe_ratio": 0.0, "sortino_ratio": 0.0,
        }
    wins = [t for t in closed_trades if t.get("pnl_net", t.get("pnl", 0)) > 0]
    losses = [t for t in closed_trades if t.get("pnl_net", t.get("pnl", 0)) <= 0]
    total_pnl_net = sum(t.get("pnl_net", t.get("pnl", 0)) for t in closed_trades)
    total_pnl_gross = sum(t.get("pnl_gross", 0) for t in closed_trades)
    total_fees = sum(t.get("fee", 0) for t in closed_trades)
    # FIX 07/08 (Mattia 13:51): nozionale Bybit totale (avgEntryPrice * closedSize) di tutti i trade chiusi
    total_notional = sum(abs(float(t.get("entry_price", 0) or 0) * float(t.get("qty", 0) or 0)) for t in closed_trades)
    # FIX 07/08 (Mattia): somma TUTTE le singole loss chiuse (in valore assoluto, positivo).
    # Serve per la formula: P&L REALIZED = GROSS - FEE - LOSS
    total_losses = abs(sum(t.get("pnl_net", t.get("pnl", 0)) for t in losses))
    unrealized = 0.0
    for p in positions:
        try:
            unrealized += float(p.get("unrealisedPnl", 0) or 0)
        except Exception:
            pass
    # P&L REALIZED = GROSS - FEE (formula Mattia 07/08 v3 - LOSS escluso dalla formula Net)
    pnl_realized = total_pnl_gross - total_fees  # = chiusi netti (senza togliere LOSS)

    # === Trading days: dal PRIMO trade chiuso a oggi (UTC) ===
    first_trade_dt = None
    for t in closed_trades:
        ts = _parse_bybit_ts(t.get("updated_at") or t.get("created_at"))
        if ts is not None:
            if first_trade_dt is None or ts < first_trade_dt:
                first_trade_dt = ts
    if first_trade_dt is not None:
        # Converti now a UTC
        from datetime import datetime as _dt, timezone as _tz
        now_utc = _dt.now(_tz.utc)
        first_trade_date = first_trade_dt.astimezone(_tz.utc).strftime("%Y-%m-%d")
        delta = now_utc - first_trade_dt
        # Conta giorni inclusivo (se primo trade oggi, = 1 giorno; se 5gg fa, = 5)
        trading_days = max(1, delta.days + 1)
    else:
        trading_days = 0
        first_trade_date = ""

    # === Profit Factor: somma(win PnL) / |somma(loss PnL)| ===
    # Convenzione: PF > 1.5 buono, > 2.0 ottimo, < 1.0 in perdita.
    total_win_pnl = sum(t.get("pnl_net", t.get("pnl", 0)) for t in wins)
    total_loss_pnl = sum(t.get("pnl_net", t.get("pnl", 0)) for t in losses)  # <= 0
    if total_loss_pnl < 0:
        profit_factor = total_win_pnl / abs(total_loss_pnl)
    elif total_win_pnl > 0:
        # Solo vincite, nessuna perdita: PF "infinito" → mostriamo 99.99
        profit_factor = 99.99
    else:
        profit_factor = 0.0

    # === Sharpe & Sortino globali RIMOSSI 09/08 (Mattia): meglio per strategia ===
    sharpe_ratio = 0.0
    sortino_ratio = 0.0

    return {
        "n_trades": n,
        "wins": len(wins),
        "losses": len(losses),
        "winrate": (len(wins) / n * 100) if n else 0.0,
        "total_pnl": total_pnl_net,            # alias per compatibilita
        "total_pnl_net": total_pnl_net,        # NET (dopo fee) = chiusi netti
        "total_pnl_gross": total_pnl_gross,    # GROSS (lordo chiusi, prima delle fee)
        "total_fees": total_fees,              # TOTALE FEE
        "total_notional": total_notional,      # FIX 07/08 13:51 Mattia: nozionale Bybit totale
        "total_losses": total_losses,          # FIX 07/08 Mattia: somma singole loss chiuse (valore assoluto)
        "pnl_realized": pnl_realized,          # FIX 07/08 Mattia: P&L REALIZED = GROSS - FEE - LOSS
        "avg_pnl": total_pnl_net / n if n else 0.0,
        "avg_pnl_gross": total_pnl_gross / n if n else 0.0,
        "best_trade": max(t.get("pnl_net", t.get("pnl", 0)) for t in closed_trades) if closed_trades else 0.0,
        "worst_trade": min(t.get("pnl_net", t.get("pnl", 0)) for t in closed_trades) if closed_trades else 0.0,
        "open_positions": len(positions),
        "unrealized_pnl": unrealized,
        "balance": balance,
        "total_pnl_with_unrealized": total_pnl_net + unrealized,
        "total_pnl_gross_with_unrealized": total_pnl_gross + unrealized,
        "trading_days": trading_days,
        "first_trade_date": first_trade_date,
        "profit_factor": profit_factor,
        "sharpe_ratio": sharpe_ratio,
        "sortino_ratio": sortino_ratio,
    }


def compute_equity_curve_from_bybit(closed_trades, balance):
    """Calcola equity curve da trade chiusi Bybit (gia filtrati).
    Ritorna anche max_dd_usdt e max_dd_pct (FIX 07/08 Mattia: drawdown real come Dash 5511).
    """
    if not closed_trades:
        return {"dates": [], "equity": [], "trades_count": 0, "max_dd_usdt": 0.0, "max_dd_pct": 0.0}
    closed_trades.sort(key=lambda t: t.get("updated_at") or t.get("created_at") or "")
    initial_equity = max(balance - sum(t["pnl"] for t in closed_trades), 0)
    dates, equity, cum = [], [], 0
    for t in closed_trades:
        cum += t["pnl"]
        ts = t.get("updated_at") or t.get("created_at") or ""
        dates.append(ts)
        equity.append(initial_equity + cum)
    # FIX 07/08 (Mattia): max drawdown real (come Dash 5511)
    peak = equity[0] if equity else 0
    max_dd_usdt = 0.0
    max_dd_pct = 0.0
    for v in equity:
        if v > peak:
            peak = v
        dd = peak - v
        if dd > max_dd_usdt:
            max_dd_usdt = dd
            max_dd_pct = (dd / peak * 100) if peak > 0 else 0
    return {
        "dates": dates, "equity": equity, "trades_count": len(closed_trades),
        "max_dd_usdt": max_dd_usdt, "max_dd_pct": max_dd_pct,
    }


def compute_by_strategy_from_bybit(closed_trades):
    by_strat = defaultdict(lambda: {"n": 0, "wins": 0, "pnl": 0.0, "pnl_gross": 0.0, "fees": 0.0, "pnls": []})
    for t in closed_trades:
        s = t.get("strategy", "unknown")
        pnl_n = t.get("pnl_net", t.get("pnl", 0))
        by_strat[s]["n"] += 1
        if pnl_n > 0:
            by_strat[s]["wins"] += 1
        by_strat[s]["pnl"] += pnl_n
        by_strat[s]["pnl_gross"] += t.get("pnl_gross", 0)
        by_strat[s]["fees"] += t.get("fee", 0)
        by_strat[s]["pnls"].append(pnl_n)
    out = []
    for s, d in by_strat.items():
        wr = (d["wins"] / d["n"] * 100) if d["n"] else 0
        # Sharpe/Sortino per strategia (09/08 Mattia): per-trade, rf=0
        n = d["n"]
        pnls = d["pnls"]
        if n >= 2:
            mean = sum(pnls) / n
            var = sum((x - mean) ** 2 for x in pnls) / (n - 1)
            std = math.sqrt(var) if var > 0 else 0.0
            sharpe = mean / std if std > 0 else 0.0
            ddsq = sum(min(0.0, x) ** 2 for x in pnls)
            dd = math.sqrt(ddsq / n) if ddsq > 0 else 0.0
            sortino = mean / dd if dd > 0 else 0.0
        else:
            sharpe = sortino = 0.0
        out.append({"strategy": s, "n": n, "wins": d["wins"], "winrate": wr,
                    "pnl": d["pnl"], "pnl_gross": d["pnl_gross"], "fees": d["fees"],
                    "sharpe": sharpe, "sortino": sortino})
    out.sort(key=lambda x: x["pnl"], reverse=True)
    return out


def compute_by_asset_from_bybit(closed_trades):
    by_asset = defaultdict(lambda: {"n": 0, "wins": 0, "pnl": 0.0, "pnl_gross": 0.0, "fees": 0.0, "volume": 0.0})
    for t in closed_trades:
        a = t.get("symbol", "")
        if not a:
            continue
        pnl_n = t.get("pnl_net", t.get("pnl", 0))
        by_asset[a]["n"] += 1
        if pnl_n > 0:
            by_asset[a]["wins"] += 1
        by_asset[a]["pnl"] += pnl_n
        by_asset[a]["pnl_gross"] += t.get("pnl_gross", 0)
        by_asset[a]["fees"] += t.get("fee", 0)
        by_asset[a]["volume"] += t.get("qty", 0) * t.get("entry_price", 0)
    out = []
    for a, d in by_asset.items():
        wr = (d["wins"] / d["n"] * 100) if d["n"] else 0
        out.append({"symbol": a, "n": d["n"], "wins": d["wins"], "winrate": wr,
                    "pnl": d["pnl"], "pnl_gross": d["pnl_gross"], "fees": d["fees"], "volume": d["volume"]})
    out.sort(key=lambda x: x["pnl"], reverse=True)
    return out


def compute_by_asset_strategy_from_bybit(closed_trades):
    """FIX 11/08 (Mattia): aggrega per (symbol, strategy) per evitare abbinamenti errati
    tipo AEO RSI swing classificato come VPTR3, oppure WIF RSI swing classificato come RETTANGOLO.
    Ogni entry: {symbol, strategy, n, wins, winrate, pnl, pnl_gross, fees}."""
    by_as = defaultdict(lambda: {"n": 0, "wins": 0, "pnl": 0.0, "pnl_gross": 0.0, "fees": 0.0})
    for t in closed_trades:
        a = t.get("symbol", "")
        s = t.get("strategy", "unknown")
        if not a:
            continue
        pnl_n = t.get("pnl_net", t.get("pnl", 0))
        key = (a, s)
        by_as[key]["n"] += 1
        if pnl_n > 0:
            by_as[key]["wins"] += 1
        by_as[key]["pnl"] += pnl_n
        by_as[key]["pnl_gross"] += t.get("pnl_gross", 0)
        by_as[key]["fees"] += t.get("fee", 0)
    out = []
    for (a, s), d in by_as.items():
        wr = (d["wins"] / d["n"] * 100) if d["n"] else 0
        out.append({"symbol": a, "strategy": s, "n": d["n"], "wins": d["wins"], "winrate": wr,
                    "pnl": d["pnl"], "pnl_gross": d["pnl_gross"], "fees": d["fees"]})
    out.sort(key=lambda x: (x["pnl"], x["n"]), reverse=True)
    return out


def compute_heatmap_from_bybit(closed_trades):
    """Heatmap WIN/LOSS per giorno x ora. Ritorna matrix_w (vittorie) e matrix_l (loss).
    FIX 07/08: prima la heatmap mostrava solo il conteggio totale senza distinguere
    W da L. Adesso separa W (verde) e L (rosso) cosi' VIRTUAL che ha 1 loss -25.47
    a Ven 02 appare in rosso, non in verde come tutti gli altri.
    """
    matrix_w = [[0] * 24 for _ in range(7)]
    matrix_l = [[0] * 24 for _ in range(7)]
    days = ["Lun", "Mar", "Mer", "Gio", "Ven", "Sab", "Dom"]
    for t in closed_trades:
        dt = _parse_bybit_ts(t.get("updated_at") or t.get("created_at"))
        if dt is None:
            continue
        # Converti in local time per heatmap (Europa)
        local_dt = dt.astimezone()
        pnl = t.get("pnl_net", t.get("pnl", 0))
        if pnl > 0:
            matrix_w[local_dt.weekday()][local_dt.hour] += 1
        elif pnl < 0:
            matrix_l[local_dt.weekday()][local_dt.hour] += 1
        else:
            # pnl == 0: conta come win (no loss)
            matrix_w[local_dt.weekday()][local_dt.hour] += 1
    return {"days": days, "hours": list(range(24)), "matrix_w": matrix_w, "matrix_l": matrix_l}


def compute_trades_by_day(closed_trades):
    """Raggruppa trade chiusi per giorno, calcola Gross/Fee/Net per ogni giorno."""
    by_day = defaultdict(lambda: {"n": 0, "wins": 0, "pnl_gross": 0.0, "fees": 0.0, "pnl_net": 0.0, "symbols": []})
    for t in closed_trades:
        dt = _parse_bybit_ts(t.get("updated_at") or t.get("created_at"))
        if dt is None:
            continue
        day_key = dt.astimezone().strftime("%Y-%m-%d")
        d = by_day[day_key]
        d["n"] += 1
        pnl_n = t.get("pnl_net", t.get("pnl", 0))
        if pnl_n > 0:
            d["wins"] += 1
        d["pnl_gross"] += t.get("pnl_gross", 0)
        d["fees"] += t.get("fee", 0)
        d["pnl_net"] += pnl_n
        sym = t.get("symbol", "")
        if sym and sym not in d["symbols"]:
            d["symbols"].append(sym)
    out = []
    for day_key in sorted(by_day.keys(), reverse=True):  # piu recenti prima
        d = by_day[day_key]
        out.append({
            "date": day_key,
            "n": d["n"],
            "wins": d["wins"],
            "winrate": (d["wins"] / d["n"] * 100) if d["n"] else 0,
            "pnl_gross": d["pnl_gross"],
            "fees": d["fees"],
            "pnl_net": d["pnl_net"],
            "symbols": ", ".join(d["symbols"]),
        })
    return out


def compute_entries_by_day(orders, since_date="2026-08-01"):
    """FIX 07/08 v3: conta le APERTURE (entry Buy) per giorno, NON le posizioni ancora
    aperte. Un ordine Buy = un'apertura, anche se poi chiusa in piu' tranche.
    Esclude ordini 'no_pos' e 'anti_dup_skip' (qty=0 o price=0) E MIEI test (is_mio_test).
    FIX 07/08 v4 Mattia 14:40: escludi NEARUSDT 600 (test Pine) e popola con 0 i giorni
    dal since_date a oggi che non hanno entry (trasparenza).
    """
    from datetime import datetime, timezone, timedelta
    by_day = defaultdict(list)
    for o in orders:
        if is_mio_test(o):
            continue
        sym = o.get("symbol", "")
        side = (o.get("side", "") or "").lower()
        try:
            qty = float(o.get("qty", 0) or 0)
            price = float(o.get("price", 0) or 0)
        except (TypeError, ValueError):
            continue
        if qty <= 0 or price <= 0:
            continue
        if side != "buy":
            continue
        ts = o.get("created_at", "")
        if not ts:
            continue
        day = ts[:10]  # YYYY-MM-DD
        by_day[day].append({"symbol": sym, "qty": qty, "price": price, "ts": ts})
    # 0-padding: inserisci giorni dal since_date a oggi che mancano
    try:
        start = datetime.fromisoformat(since_date).date()
    except Exception:
        start = datetime(2026, 8, 1).date()
    today = datetime.now(timezone.utc).date()
    cur = start
    while cur <= today:
        d = cur.isoformat()
        if d not in by_day:
            by_day[d] = []  # giorno vuoto
        cur += timedelta(days=1)
    out = []
    for day in sorted(by_day.keys(), reverse=True):
        entries = by_day[day]
        symbols_str = ", ".join([f"{e['symbol']} ({e['qty']:,.0f}@{e['price']:.4f})" for e in entries]) if entries else "—"
        out.append({
            "date": day,
            "n": len(entries),
            "symbols": symbols_str,
        })
    return out


def compute_open_positions_by_entry_day(bybit_positions, db_orders, since_date="2026-08-01"):
    """DEPRECATED dopo v3: contava le posizioni ANCORA APERTE per giorno di entry.
    Usava FIFO su DB orders, ma non vedeva le chiusure automatiche di Bybit TP/SL
    (es. WIF 10765 chiusa da TP ma DB diceva ancora aperto). Sostituita da
    compute_entries_by_day (aperture totali per giorno) che è piu' utile per Mattia.
    Mantenuta per retrocompatibilita' se Mattia la vuole di nuovo.
    """
    buys_by_symbol = defaultdict(list)
    for o in db_orders:
        sym = o.get("symbol", "")
        side = (o.get("side", "") or "").lower()
        try:
            qty = float(o.get("qty", 0) or 0)
            price = float(o.get("price", 0) or 0)
        except (TypeError, ValueError):
            continue
        if qty <= 0 or price <= 0:
            continue
        if side == "buy":
            buys_by_symbol[sym].append({
                "qty": qty, "price": price,
                "ts": o.get("created_at", ""),
            })
    by_day = defaultdict(lambda: {"n": 0, "symbols": []})
    for pos in bybit_positions:
        sym = pos.get("symbol", "")
        size = _safe_float(pos.get("size"))
        avg_price = _safe_float(pos.get("avgPrice"))
        if not sym or size <= 0:
            continue
        buys = buys_by_symbol.get(sym, [])
        if not buys:
            day = "N/D"
        else:
            last_buy = max(buys, key=lambda x: x.get("ts", ""))
            day = (last_buy.get("ts", "") or "")[:10]
            if not day:
                day = "N/D"
        by_day[day]["n"] += 1
        by_day[day]["symbols"].append(f"{sym} ({size:,.0f}@{avg_price:.4f})")
    out = []
    for day in sorted(by_day.keys(), reverse=True):
        d = by_day[day]
        out.append({
            "date": day,
            "n": d["n"],
            "symbols": ", ".join(d["symbols"]),
        })
    return out


def compute_top_bad_from_bybit(closed_trades, top_n=5):
    """Top N best (asset con pnl > 0, ordinati per pnl desc) + Top N worst
    (asset che hanno ALMENO 1 singola loss, ordinati per worst_single_loss asc).

    FIX 07/08: prima il 'bad' richiedeva len(by_asset) > top_n, quindi con <= 5 asset
    il worst restava vuoto anche se c'erano loser.
    FIX 07/08 (2): il 'best' filtra solo asset con pnl > 0.
    FIX 07/08 (3): il 'worst' ora include asset che hanno almeno 1 singola loss,
    indipendentemente dal pnl aggregato. Cosi' VIRTUAL (pnl aggregato +18.47) compare
    perche' ha una loss singola da -25.47. Ordinato per worst_single_loss ascendente.
    """
    by_asset = compute_by_asset_from_bybit(closed_trades)
    # Top: SOLO asset in profitto, ordinati per pnl desc
    winners = [a for a in by_asset if a.get("pnl", 0) > 0]
    winners.sort(key=lambda x: x.get("pnl", 0), reverse=True)
    top = winners[:top_n]

    # Worst: asset con ALMENO 1 singola loss, ordinati per worst_single_loss ascendente
    # Calcola worst_single_loss per symbol
    asset_worst_loss = {}
    for t in closed_trades:
        sym = t.get("symbol", "")
        pnl = t.get("pnl_net", t.get("pnl", 0))
        if pnl < 0:
            if sym not in asset_worst_loss or pnl < asset_worst_loss[sym]:
                asset_worst_loss[sym] = pnl

    worst_list = []
    for a in by_asset:
        if a.get("symbol") in asset_worst_loss:
            a2 = dict(a)
            a2["worst_single_loss"] = asset_worst_loss[a["symbol"]]
            worst_list.append(a2)
    worst_list.sort(key=lambda x: x.get("worst_single_loss", 0))
    bad = worst_list[:top_n]
    return {"top": top, "bad": bad}


def compute_top_single_losses(closed_trades, top_n=5):
    """Top N peggiori trade SINGOLI (per pnl_net ascendente).
    FIX 07/08: serve perche' VIRTUAL aggregato e' +18.47 ma ha una singola loss -25.47
    che non comparirebbe mai in 'by_asset' worst. Mostra le singole chiusure peggiori.
    """
    losers = [t for t in closed_trades if t.get("pnl_net", t.get("pnl", 0)) < 0]
    losers.sort(key=lambda x: x.get("pnl_net", x.get("pnl", 0)))  # ascendente = peggiore prima
    out = []
    for t in losers[:top_n]:
        dt = _parse_bybit_ts(t.get("updated_at") or t.get("created_at"))
        out.append({
            "symbol": t.get("symbol", ""),
            "side": t.get("side", ""),
            "qty": t.get("qty", 0),
            "entry_price": t.get("entry_price", 0),
            "exit_price": t.get("exit_price", 0),
            "pnl_net": t.get("pnl_net", t.get("pnl", 0)),
            "pnl_gross": t.get("pnl_gross", 0),
            "fee": t.get("fee", 0),
            "date_fmt": dt.strftime("%Y-%m-%d %H:%M") if dt else "",
        })
    return out


def get_strategy_for_symbol(symbol):
    """Replica la logica di sltp_engine.get_strategy_for_symbol per le 4 strategie v2.
    FIX 11/08 (Mattia): scorre TUTTI i CSV e ritorna lo strategy con priorita' piu' alta.
    Priorita': rsi_swing_breakout > rettangolo_simple > rettangolo > ma_trailing > vptr3.
    Necessario perche' WIFUSDT e' sia in rettangolo_assets.csv (strategy=rettangolo) che
    in RETTANGOLO_SIMPLE_ASSETS.csv (strategy=rsi_swing_breakout), e la versione specifica
    RSI swing deve avere priorita' sulla generica rettangolo."""
    sym = symbol.upper()
    vptr3_csv = ROOT / "VPTR_V3_ASSETS.csv"
    rett_csv = ROOT / "rettangolo_assets.csv"
    rett_simple_csv = ROOT / "RETTANGOLO_SIMPLE_ASSETS.csv"
    ma_csv = ROOT / "MA_TRAILING_ASSETS.csv"
    import csv as csvmod
    # Priorita' strategie: piu' alto = piu' specifico
    PRIORITY = {
        "rsi_swing_breakout": 50,
        "rettangolo_simple": 40,
        "rettangolo": 30,
        "ma_trailing": 20,
        "vptr3": 10,
    }
    candidates = []  # lista di (priority, strategy)
    for csv_path in (vptr3_csv, rett_csv, rett_simple_csv, ma_csv):
        if not csv_path.exists():
            continue
        try:
            with open(csv_path, "r", encoding="utf-8") as f:
                reader = csvmod.DictReader(f)
                for row in reader:
                    if not row.get("symbol"):
                        continue
                    if row["symbol"].strip().upper() == sym:
                        if str(row.get("enabled", "true")).lower() == "true":
                            s = row.get("strategy", "").strip().lower()
                            if s == "vptr_v3":
                                s = "vptr3"
                            prio = PRIORITY.get(s, 0)
                            candidates.append((prio, s))
        except Exception:
            pass
    if not candidates:
        return "rettangolo"  # default legacy
    # Prendi il match con priorita' piu' alta
    candidates.sort(key=lambda x: x[0], reverse=True)
    return candidates[0][1]


def _fmt_usdt(v, decimals=2):
    try:
        return f"{float(v):,.{decimals}f}"
    except Exception:
        return "0.00"


def _fmt_pct(v, decimals=1):
    try:
        return f"{float(v):.{decimals}f}"
    except Exception:
        return "0.0"


def _safe_float(v, default=0.0):
    try:
        return float(v) if v is not None else default
    except Exception:
        return default


# === HTML RENDER (server-side, niente template engine) ===
def render_dashboard(data):
    kpis = data["kpis"]
    equity = data["equity"]
    by_strategy = data["by_strategy"]
    by_asset = data["by_asset"]
    top = data["top_bad"]["top"]
    bad = data["top_bad"]["bad"]
    heatmap = data["heatmap"]
    positions = data["positions"]
    recent = data["recent_orders"]
    generated_at = data["generated_at"]

    # KPI
    pnl_class = "good" if kpis["total_pnl_net"] >= 0 else "bad"
    pgross_class = "good" if kpis["total_pnl_gross"] >= 0 else "bad"
    fees_class = "warn" if kpis["total_fees"] > 0 else ""
    upnl_class = "good" if kpis["unrealized_pnl"] >= 0 else "bad"
    tot_class = "good" if kpis["total_pnl_with_unrealized"] >= 0 else "bad"
    wr_class = "good" if kpis["winrate"] >= 50 else ("bad" if kpis["winrate"] < 40 else "")
    avg_class = "good" if kpis["avg_pnl"] >= 0 else "bad"
    # Profit Factor: >2 ottimo, >1.5 buono, <1 in perdita
    pf = kpis.get("profit_factor", 0.0)
    if pf >= 2.0:
        pf_class = "good"
    elif pf >= 1.5:
        pf_class = "good"
    elif pf >= 1.0:
        pf_class = ""
    else:
        pf_class = "bad"
    pf_display = f"{pf:.2f}" if pf < 99 else "∞"
    first_date = kpis.get("first_trade_date", "")
    trading_days = kpis.get("trading_days", 0)
    days_label = f"Trading days (dal {first_date})" if first_date else "Trading days"

    # === Sharpe & Sortino GLOBALI RIMOSSI 09/08 (Mattia): ora per strategia ===
    sharpe_v = 0.0
    sortino_v = 0.0
    sharpe_class = ""
    sortino_class = ""
    sharpe_display = ""
    sortino_display = ""

    # FIX 07/08 (Mattia): composizione del Net P&L
    # GROSS = lordo chiusi - FEE - LOSS (somma singole loss) = P&L REALIZED
    # P&L REALIZED + UNREALIZED = NET P&L (= PnL Totale)
    gross_base = kpis["total_pnl_gross"]  # GROSS base (lordo chiusi) - non usato per display
    fees = kpis["total_fees"]
    fees_class = "warn"
    losses = kpis["total_losses"]  # LOSS = somma singole loss (valore assoluto)
    losses_class = "bad"
    pnl_realized = kpis["pnl_realized"]  # P&L REALIZED = GROSS - FEE - LOSS
    pnl_realized_class = "good" if pnl_realized >= 0 else "bad"
    # FIX 07/08 (Mattia v4): GROSS mostrato = P&L Realized + LOSS + FEE (formula inversa)
    gross = pnl_realized + losses + fees
    gross_class = "good" if gross >= 0 else "bad"
    # FIX 07/08 (Mattia): Win to Loss ratio (W/L) = wins / losses
    n_wins = kpis.get("wins", 0)
    n_losses_count = kpis.get("losses", 0)
    if n_losses_count > 0:
        wtl = n_wins / n_losses_count
        wtl_display = f"{wtl:.2f}"
        if wtl >= 2.0:
            wtl_class = "good"
        elif wtl >= 1.0:
            wtl_class = ""
        else:
            wtl_class = "bad"
    else:
        wtl_display = "∞"  # nessuna loss
        wtl_class = "good"
    unrealized = kpis["unrealized_pnl"]
    unrealized_class = "good" if unrealized >= 0 else "bad"
    # NET P&L = P&L REALIZED + UNREALIZED
    net_pnl = pnl_realized + unrealized
    net_class = "good" if net_pnl >= 0 else "bad"


    # === FIX 07/08 (Mattia 13:51): 4 box DI FIANCO a "Posizioni aperte" ===
    trading_days = kpis.get("trading_days", 0) or 0
    td = max(trading_days, 1)
    # Box 1: Trade al giorno
    trades_per_day = round(kpis.get("n_trades", 0) / td, 2) if kpis.get("n_trades", 0) else 0
    # Box 2: Nozionale Bybit medio/giorno
    notional_per_day = kpis.get("total_notional", 0) / td
    # Box 4: Day Profit in valore assoluto (Net P&L / N° giorni)
    day_profit_abs = net_pnl / td
    day_profit_class = "good" if day_profit_abs >= 0 else "bad"
    # Box 3: Day Profit % su capitale FISSO 4000 USDT (NON balance) = (Net P&L / 4000) * 100
    # FIX 07/08 14:11 (Mattia): era (day_profit_abs / 4000) ma Mattia vuole (Net P&L / 4000)
    DAY_PROFIT_CAPITALE = 4000.0
    day_profit_pct_4000 = (net_pnl / DAY_PROFIT_CAPITALE) * 100
    day_profit_pct_class = "good" if day_profit_pct_4000 >= 0 else "bad"

    kpi_html = f"""
  <div class="kpi"><div class="kpi-label">Balance</div><div class="kpi-value">{_fmt_usdt(kpis['balance'])} USDT</div></div>
  <div class="kpi"><div class="kpi-label">Trade chiusi</div><div class="kpi-value">{kpis['n_trades']}</div></div>
  <div class="kpi"><div class="kpi-label">{days_label}</div><div class="kpi-value">{trading_days}</div></div>
  <div class="kpi"><div class="kpi-label">Win rate</div><div class="kpi-value {wr_class}">{_fmt_pct(kpis['winrate'])}%<br><small style="color:#8b949e;font-size:11px;font-weight:400;">{kpis['wins']}W / {kpis['losses']}L</small></div></div>
  <div class="kpi"><div class="kpi-label">Win to Loss</div><div class="kpi-value {wtl_class}">{wtl_display}</div></div>
  <div class="kpi"><div class="kpi-label">Profit Factor</div><div class="kpi-value {pf_class}">{pf_display}</div></div>
  <div class="kpi"><div class="kpi-label">Gross PnL (chiusi)</div><div class="kpi-value {gross_class}">{_fmt_usdt(gross)} USDT</div></div>
  <div class="kpi"><div class="kpi-label">Fee totali</div><div class="kpi-value {fees_class}">-{_fmt_usdt(fees)} USDT</div></div>
  <div class="kpi"><div class="kpi-label">Loss chiuse</div><div class="kpi-value bad">{_fmt_usdt(losses)} USDT</div></div>
  <div class="kpi"><div class="kpi-label">P&L Realized (Gross-Loss-Fee)</div><div class="kpi-value {pnl_realized_class}">{_fmt_usdt(pnl_realized)} USDT</div></div>
  <div class="kpi"><div class="kpi-label">Unrealized PnL</div><div class="kpi-value {unrealized_class}">{_fmt_usdt(unrealized)} USDT</div></div>
  <div class="kpi"><div class="kpi-label">Net P&L (Realized-Unrealized)</div><div class="kpi-value {net_class}">{_fmt_usdt(net_pnl)} USDT</div></div>
  <div class="kpi"><div class="kpi-label">Avg trade</div><div class="kpi-value {avg_class}">{_fmt_usdt(kpis['avg_pnl'])} USDT</div></div>
  <div class="kpi"><div class="kpi-label">Best trade</div><div class="kpi-value good">{_fmt_usdt(kpis['best_trade'])} USDT</div></div>
  <div class="kpi"><div class="kpi-label">Posizioni aperte</div><div class="kpi-value">{kpis['open_positions']}</div></div>
  <div class="kpi"><div class="kpi-label">Trade al giorno</div><div class="kpi-value">{trades_per_day}</div></div>
  <div class="kpi"><div class="kpi-label">Nozionale Bybit/giorno</div><div class="kpi-value">{_fmt_usdt(notional_per_day)} USDT</div></div>
  <div class="kpi"><div class="kpi-label">Day Profit %</div><div class="kpi-value {day_profit_pct_class}">{day_profit_pct_4000:.2f}%/gg</div></div>
  <div class="kpi"><div class="kpi-label">Day Profit</div><div class="kpi-value {day_profit_class}">{_fmt_usdt(day_profit_abs)} USDT/gg</div></div>
  <div class="kpi"><div class="kpi-label">Drawdown real</div><div class="kpi-value bad">{_fmt_usdt(equity["max_dd_usdt"])} USDT<br>{equity["max_dd_pct"]:.2f}%</div></div>
"""

    # Equity chart
    if equity["trades_count"] > 0:
        # SVG inline (no plotly): polyline semplice
        eq_pts = equity["equity"]
        eq_dates = equity["dates"]
        if len(eq_pts) >= 2:
            min_v, max_v = min(eq_pts), max(eq_pts)
            rng = max(max_v - min_v, 0.01)
            # Normalizza in 0-100 per SVG
            w, h = 800, 200
            pad = 10
            points = []
            for i, v in enumerate(eq_pts):
                x = pad + (i / (len(eq_pts) - 1)) * (w - 2 * pad)
                y = h - pad - ((v - min_v) / rng) * (h - 2 * pad)
                points.append(f"{x:.1f},{y:.1f}")
            svg_line = " ".join(points)
            last_v = eq_pts[-1]
            last_change = last_v - eq_pts[0]
            change_pct = (last_change / eq_pts[0] * 100) if eq_pts[0] else 0
            color = "#3fb950" if last_change >= 0 else "#f85149"
            # Genera marker hover per ogni trade (tooltip nativi browser via <title>)
            hover_points = ""
            n_eq = len(eq_pts)
            for i_p, (pt_xy, v_p) in enumerate(zip(points, eq_pts)):
                cx_p, cy_p = pt_xy.split(",")
                dt_raw = eq_dates[i_p] if i_p < len(eq_dates) else ""
                # Converti unix seconds in data leggibile (formato italiano)
                dt_label = f"#{i_p+1}"
                if dt_raw:
                    try:
                        ts_val = float(dt_raw)
                        if ts_val > 1e12:  # millisecondi
                            ts_val = ts_val / 1000.0
                        from datetime import datetime as _dt, timezone as _tz
                        dt_obj = _dt.fromtimestamp(ts_val, tz=_tz.utc)
                        dt_label = dt_obj.strftime("%d/%m %H:%M")
                    except Exception:
                        dt_label = dt_raw[:16] if dt_raw else f"#{i_p+1}"
                delta_v = v_p - eq_pts[0]
                delta_pct = (delta_v / eq_pts[0] * 100) if eq_pts[0] else 0
                # Marker visible (raggio 4) + tooltip custom JS al hover (no delay browser)
                # Usa singoli apici per la data (escape eventuali apostrofi) - sicuro in HTML attr
                dt_js = "'" + dt_label.replace("\\", "\\\\").replace("'", "\\'") + "'"
                hover_points += (
                    f'<circle class="eq-pt" cx="{cx_p}" cy="{cy_p}" r="4" fill="#fff" fill-opacity="0.95" stroke="{color}" stroke-width="1.5" '
                    f'style="cursor:pointer" '
                    f'onmouseover="eqShowTip(event, {i_p+1}, {dt_js}, {v_p:.2f}, {delta_v:.2f}, {delta_pct:.2f})" '
                    f'onmousemove="eqMoveTip(event)" '
                    f'onmouseout="eqHideTip()">'
                    f'<title>Trade #{i_p+1} | {dt_label} | Equity: {v_p:.2f} USDT</title>'
                    f'</circle>\n    '
                )
            # Label Y (5 tacche: min, q1, mid, q3, max)
            y_labels = []
            for frac in [0.0, 0.25, 0.5, 0.75, 1.0]:
                v_label = min_v + frac * rng
                y_pos = h - pad - frac * (h - 2 * pad)
                y_labels.append((v_label, y_pos))
            y_label_svg = ""
            grid_h_svg = ""
            for v_label, y_pos in y_labels:
                grid_h_svg += f'<line x1="{pad}" y1="{y_pos:.1f}" x2="{w-pad}" y2="{y_pos:.1f}" stroke="#21262d" stroke-width="1"/>\n    '
                y_label_svg += f'<text x="{pad-2}" y="{y_pos+3:.1f}" text-anchor="end" font-size="11" fill="#c9d1d9" font-family="monospace" font-weight="600">{v_label:.2f}</text>\n    '
            # Label X (data primo, 1/3, 2/3, ultimo)
            n = len(eq_pts)
            x_idx = [0, n//3, 2*n//3, n-1] if n >= 4 else list(range(n))
            x_label_svg = ""
            grid_v_svg = ""
            for idx in x_idx:
                x = pad + (idx / max(n - 1, 1)) * (w - 2 * pad)
                dt_raw = eq_dates[idx] if idx < len(eq_dates) else ""
                # Converti unix seconds/ms in data leggibile
                dt_label = ""
                if dt_raw:
                    try:
                        ts_v = float(dt_raw)
                        if ts_v > 1e12:
                            ts_v = ts_v / 1000.0
                        from datetime import datetime as _dt2, timezone as _tz2
                        dt_label = _dt2.fromtimestamp(ts_v, tz=_tz2.utc).strftime("%d/%m %H:%M")
                    except Exception:
                        dt_label = str(dt_raw)[:10]
                grid_v_svg += f'<line x1="{x:.1f}" y1="{pad}" x2="{x:.1f}" y2="{h-pad}" stroke="#21262d" stroke-width="1" stroke-dasharray="2,3"/>\n    '
                anchor = "start" if idx == 0 else ("end" if idx == n-1 else "middle")
                tx = x + 2 if anchor == "start" else (x - 2 if anchor == "end" else x)
                x_label_svg += f'<text x="{tx:.1f}" y="{h+12}" text-anchor="{anchor}" font-size="11" fill="#c9d1d9" font-family="monospace" font-weight="600">{dt_label}</text>\n    '
            # Fill area sotto la curva (gradiente)
            fill_pts = svg_line + f" {points[-1].split(chr(44))[0]},{h-pad} {pad},{h-pad}"
            equity_html = f'''<div style="background:#0d1117;padding:10px;border-radius:6px;">
  <svg viewBox="0 0 {w} {h+18}" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:{h+18}px;">
    {grid_h_svg}{grid_v_svg}<polygon points="{fill_pts}" fill="{color}" fill-opacity="0.12"/>
    <polyline points="{svg_line}" fill="none" stroke="{color}" stroke-width="2"/>
    {hover_points}
    <circle cx="{points[-1].split(chr(44))[0]}" cy="{points[-1].split(chr(44))[1]}" r="4" fill="{color}"/>
    {y_label_svg}{x_label_svg}
  </svg>
  <div style="display:flex;justify-content:space-between;font-size:11px;color:#8b949e;margin-top:4px;">
    <span>Initial: {eq_pts[0]:.2f} USDT</span>
    <span style="color:{color};font-weight:600;">Final: {last_v:.2f} USDT ({change_pct:+.2f}%)</span>
  </div>
  <div style="font-size:12px;color:#8b949e;margin-top:4px;"><span>{len(eq_pts)} trade chiusi</span></div>
  <script>
  (function() {{
    if (window._eqTipLoaded) return;
    window._eqTipLoaded = true;
    var t = document.createElement('div');
    t.id = 'eq-tooltip';
    t.style.cssText = 'display:none;position:absolute;background:#161b22;border:1px solid #30363d;color:#fff;padding:10px 14px;border-radius:6px;font-size:13px;pointer-events:none;z-index:10000;box-shadow:0 4px 12px rgba(0,0,0,0.5);font-family:monospace;line-height:1.5;';
    document.body.appendChild(t);
  }})();
  function eqShowTip(e, n, date, eq, delta, deltaPct) {{
    var t = document.getElementById('eq-tooltip');
    if (!t) return;
    var sign = delta >= 0 ? '+' : '';
    var pctSign = deltaPct >= 0 ? '+' : '';
    var color = delta >= 0 ? '#3fb950' : '#f85149';
    t.innerHTML = '<div style="font-weight:700;color:#fff;margin-bottom:4px;">Trade #' + n + '</div>' +
      '<div style="color:#8b949e;">Data: <span style="color:#c9d1d9;">' + date + '</span></div>' +
      '<div style="color:#8b949e;">Equity: <span style="color:#fff;font-weight:600;">' + eq.toFixed(2) + ' USDT</span></div>' +
      '<div style="color:#8b949e;">Delta: <span style="color:' + color + ';font-weight:600;">' + sign + delta.toFixed(2) + ' USDT (' + pctSign + deltaPct.toFixed(2) + '%)</span></div>';
    t.style.display = 'block';
    eqMoveTip(e);
  }}
  function eqMoveTip(e) {{
    var t = document.getElementById('eq-tooltip');
    if (t && t.style.display === 'block') {{
      t.style.left = (e.pageX + 14) + 'px';
      t.style.top = (e.pageY + 14) + 'px';
    }}
  }}
  function eqHideTip() {{
    var t = document.getElementById('eq-tooltip');
    if (t) t.style.display = 'none';
  }}
  </script>
</div>'''
        else:
            # 1 solo trade
            v = eq_pts[0]
            equity_html = f'<div style="background:#0d1117;padding:20px;border-radius:6px;text-align:center;color:#8b949e;">1 trade chiuso: equity = {v:.2f} USDT</div>'
        equity_json = json.dumps(equity)
    else:
        equity_html = '<div class="empty">Nessun trade chiuso dal 1° agosto. Aspetto chiusure Pine (Max exit bars).</div>'
        equity_json = json.dumps({"dates": [], "equity": [], "max_dd_usdt": 0.0, "max_dd_pct": 0.0})

    # Per strategia (tabella aggregata)
    if by_strategy:
        rows = []
        for s in by_strategy:
            cls = "good" if s["pnl"] >= 0 else "bad"
            rows.append(f"<tr><td>{s['strategy']}</td><td>{s['n']} ({s['wins']}W)</td><td>{_fmt_pct(s['winrate'])}%</td><td class='pnl good'>{_fmt_usdt(s['pnl_gross'])}</td><td class='pnl warn'>-{_fmt_usdt(s['fees'])}</td><td class='pnl {cls}'>{_fmt_usdt(s['pnl'])} USDT</td></tr>")
        strategy_html = "<table><tr><th>Strategia</th><th>Trades</th><th>WR</th><th>Gross</th><th>Fee</th><th>Net</th></tr>" + "".join(rows) + "</table>"
    else:
        strategy_html = '<div class="empty">Nessun trade chiuso ancora. Aspetto chiusure Pine (Max exit bars).</div>'

    # Sharpe / Sortino per strategia (09/08 Mattia): box per ogni strategia con entrambi i valori
    if by_strategy:
        boxes = []
        for s in by_strategy:
            n = s["n"]
            sh = s.get("sharpe", 0.0)
            so = s.get("sortino", 0.0)
            # Nome strategia "pretty"
            name_raw = s["strategy"]
            name_disp = name_raw.replace("_", " ").title() if name_raw != "unknown" else "Unknown"
            if n >= 2:
                sh_cls = "good" if sh >= 1.0 else ("bad" if sh < 0 else "")
                so_cls = "good" if so >= 1.5 else ("bad" if so < 0 else "")
                sh_disp = f"{sh:.3f}"
                so_disp = f"{so:.3f}"
                title = (f"Strategia: {name_raw}\nTrade: {n} ({s['wins']}W/{n - s['wins']}L)\n"
                         f"WR: {s['winrate']:.1f}%\nNet: {s['pnl']:.2f} USDT\n"
                         f"Sharpe: mean/std (rf=0) - &gt;=1 buono, &lt;0 in perdita\n"
                         f"Sortino: mean/downside_dev (rf=0) - &gt;=1.5 buono, &lt;0 in perdita")
            else:
                sh_cls = so_cls = ""
                sh_disp = so_disp = "n/a"
                title = f"Strategia: {name_raw} ({n} trade: servono almeno 2 trade per calcolare Sharpe/Sortino)"
            boxes.append(
                f'<div class="kpi" title="{title}">'
                f'<div class="kpi-label">{name_disp} ({n}t)</div>'
                f'<div class="kpi-value {sh_cls}" style="font-size:18px;">Sh: {sh_disp}</div>'
                f'<div class="kpi-value {so_cls}" style="font-size:13px;margin-top:2px;">So: {so_disp}</div>'
                f'</div>'
            )
        strategy_sharpe_html = f'<div class="kpi-grid">{"".join(boxes)}</div>'
    else:
        strategy_sharpe_html = '<div class="empty">Nessun trade chiuso ancora. Servono almeno 2 trade per strategia per calcolare Sharpe/Sortino.</div>'

    # Per asset
    if by_asset:
        rows = []
        for a in by_asset:
            cls = "good" if a["pnl"] >= 0 else "bad"
            rows.append(f"<tr><td>{a['symbol']}</td><td>{a['n']} ({a['wins']}W)</td><td>{_fmt_pct(a['winrate'])}%</td><td class='pnl good'>{_fmt_usdt(a['pnl_gross'])}</td><td class='pnl warn'>-{_fmt_usdt(a['fees'])}</td><td class='pnl {cls}'>{_fmt_usdt(a['pnl'])} USDT</td></tr>")
        asset_html = "<table><tr><th>Symbol</th><th>Trades</th><th>WR</th><th>Gross</th><th>Fee</th><th>Net</th></tr>" + "".join(rows) + "</table>"
    else:
        asset_html = '<div class="empty">Nessun trade chiuso ancora</div>'

    # Top 5 / Bad 5
    def _top_table(items, cls_color):
        """FIX 07/08: colore PnL DINAMICO basato sul segno (verde se >0, rosso se <0).
        cls_color indica sezione ('best' o 'worst'). Per Worst, se l'item ha
        'worst_single_loss', mostra quella al posto del pnl aggregato (perche'
        un asset puo' avere pnl aggregato positivo ma loss singola grossa).
        """
        if not items:
            return '<div class="empty">Nessun trade chiuso ancora</div>'
        rows = []
        for a in items:
            p = a.get("pnl", 0)
            # Per Worst: se worst_single_loss presente, mostriamo quella
            wsl = a.get("worst_single_loss")
            if cls_color == "bad" and wsl is not None:
                # Worst: mostra worst single loss (sempre rosso) + pnl aggregato in tooltip
                wsl_cls = "bad"
                title = f"title=\"Worst single: {_fmt_usdt(wsl)} USDT | PnL aggregato: {_fmt_usdt(p)} USDT\""
                pnl_cell = f'<td class="pnl {wsl_cls}" {title}>{_fmt_usdt(wsl)} USDT</td>'
            else:
                # Best o worst senza worst_single_loss
                row_cls = "good" if p > 0 else ("bad" if p < 0 else "")
                pnl_cell = f'<td class="pnl {row_cls}">{_fmt_usdt(p)} USDT</td>'
            rows.append(f"<tr><td>{a['symbol']}</td><td>{a['n']}</td><td>{_fmt_pct(a['winrate'])}%</td>{pnl_cell}</tr>")
        return "<table><tr><th>Symbol</th><th>Trades</th><th>WR</th><th>PnL</th></tr>" + "".join(rows) + "</table>"

    top_html = _top_table(top, "good")
    bad_html = _top_table(bad, "bad")

    # Top 5 Single Losses (singoli trade peggiori) - FIX 07/08
    single_losses = data.get("single_losses", [])
    if single_losses:
        rows = []
        for t in single_losses:
            rows.append(f'<tr><td>{t.get("date_fmt", "")}</td><td>{t.get("symbol", "")}</td><td>{t.get("side", "")}</td><td>{t.get("qty", 0):,.2f}</td><td>{t.get("entry_price", 0):,.5f}</td><td>{t.get("exit_price", 0):,.5f}</td><td class="pnl bad">{t.get("pnl_net", 0):.2f} USDT</td><td class="pnl warn">-{t.get("fee", 0):.2f}</td></tr>')
        single_losses_html = "<table><tr><th>Data</th><th>Symbol</th><th>Side</th><th>Qty</th><th>Entry</th><th>Exit</th><th>Net PnL</th><th>Fee</th></tr>" + "".join(rows) + "</table>"
    else:
        single_losses_html = '<div class="empty">Nessuna singola loss. Vai tranquillo.</div>'

    # APERTURE (entry Buy) per Giorno - FIX 07/08 v3
    open_by_day = data.get("open_by_day", [])
    if open_by_day:
        rows = []
        total_n = 0
        for d in open_by_day:
            total_n += d["n"]
            rows.append(f'<tr><td>{d["date"]}</td><td><b>{d["n"]}</b></td><td><small>{d["symbols"]}</small></td></tr>')
        # Riga totale
        rows.append(f'<tr style="border-top:2px solid #58a6ff;font-weight:600;"><td>TOTALE</td><td>{total_n}</td><td><small>aperture totali (entry Buy) dal 1° agosto</small></td></tr>')
        open_by_day_html = "<table><tr><th>Data Entry</th><th>N. Aperture</th><th>Symbols (qty@price)</th></tr>" + "".join(rows) + "</table>"
    else:
        open_by_day_html = '<div class="empty">Nessuna apertura registrata.</div>'

    # Heatmap (HTML puro, no Plotly CDN) - FIX 07/08: WIN/LOSS separati
    matrix_w = heatmap.get("matrix_w", [[0]*24 for _ in range(7)])
    matrix_l = heatmap.get("matrix_l", [[0]*24 for _ in range(7)])
    matrix_tot = [[matrix_w[d][h] + matrix_l[d][h] for h in range(24)] for d in range(7)]
    if any(sum(row) > 0 for row in matrix_tot):
        # max_count basato sul totale per la scala di intensita'
        max_count = max(max(row) for row in matrix_tot) if matrix_tot else 1
        max_count = max(max_count, 1)
        heatmap_rows = []
        # Header (ore)
        cells = ["<td class='hm-corner'>Giorno \\\\ Ora</td>"]
        for h in heatmap["hours"]:
            cells.append(f"<td class='hm-h'>{h}</td>")
        heatmap_rows.append("<tr>" + "".join(cells) + "</tr>")
        for d_idx, day in enumerate(heatmap["days"]):
            cells = [f"<td class='hm-d'>{day}</td>"]
            for h in range(24):
                w = matrix_w[d_idx][h]
                l = matrix_l[d_idx][h]
                if w == 0 and l == 0:
                    cells.append('<td class="hm-c" style="background:#0d1117;"></td>')
                else:
                    # Cella divisa: meta' sinistra W (verde), meta' destra L (rosso)
                    # Intensita' in base al totale nella cella
                    intensity_w = w / max_count if max_count > 0 else 0
                    intensity_l = l / max_count if max_count > 0 else 0
                    # W: da scuro (#0d1117) a verde (#3fb950)
                    rw = int(13 + (63 - 13) * intensity_w)
                    gw = int(17 + (185 - 17) * intensity_w)
                    bw = int(23 + (80 - 23) * intensity_w)
                    color_w = f"rgb({rw},{gw},{bw})"
                    # L: da scuro (#0d1117) a rosso (#f85149)
                    rl = int(13 + (248 - 13) * intensity_l)
                    gl = int(17 + (81 - 17) * intensity_l)
                    bl = int(23 + (73 - 23) * intensity_l)
                    color_l = f"rgb({rl},{gl},{bl})"
                    # Solo W: cella verde piena con numero W
                    if l == 0:
                        cells.append(f'<td class="hm-c" style="background:{color_w};" title="W:{w} L:{l}">{w}</td>')
                    # Solo L: cella rossa piena con numero L
                    elif w == 0:
                        cells.append(f'<td class="hm-c" style="background:{color_l};" title="W:{w} L:{l}">{l}</td>')
                    # Miste: gradiente orizzontale W|L
                    else:
                        bg = f"linear-gradient(to right, {color_w} 0%, {color_w} 50%, {color_l} 50%, {color_l} 100%)"
                        cells.append(f'<td class="hm-c" style="background:{bg};" title="W:{w} L:{l}"><span style="color:#fff;font-weight:600;">{w}|{l}</span></td>')
            heatmap_rows.append("<tr>" + "".join(cells) + "</tr>")
        # Legenda
        legenda = ('<div style="margin-top:8px;font-size:11px;color:#8b949e;">'
                   '<span style="display:inline-block;width:12px;height:12px;background:rgb(63,185,80);margin-right:4px;"></span>Win '
                   '<span style="display:inline-block;width:12px;height:12px;background:rgb(248,81,73);margin:0 4px 0 12px;"></span>Loss '
                   '<span style="margin-left:12px;">Celle miste: W|L</span></div>')
        heatmap_html = "<table class='hm'>" + "".join(heatmap_rows) + "</table>" + legenda
    else:
        heatmap_html = '<div class="empty">Nessun dato heatmap. Aspetto chiusure Pine.</div>'

    # Trade per Giorno
    _trades_by_day = data.get("trades_by_day", [])
    if _trades_by_day:
        rows = []
        for d in _trades_by_day:
            cls = "good" if d["pnl_net"] >= 0 else "bad"
            rows.append(f"<tr><td>{d['date']}</td><td>{d['n']} ({d['wins']}W)</td><td>{d['winrate']:.1f}%</td><td class='pnl good'>{_fmt_usdt(d['pnl_gross'])}</td><td class='pnl warn'>-{_fmt_usdt(d['fees'])}</td><td class='pnl {cls}'>{_fmt_usdt(d['pnl_net'])} USDT</td><td><small>{d['symbols']}</small></td></tr>")
        # Totale
        tot_gross = sum(d['pnl_gross'] for d in _trades_by_day)
        tot_fees = sum(d['fees'] for d in _trades_by_day)
        tot_net = sum(d['pnl_net'] for d in _trades_by_day)
        tot_n = sum(d['n'] for d in _trades_by_day)
        tot_wins = sum(d['wins'] for d in _trades_by_day)
        tot_wr = (tot_wins / tot_n * 100) if tot_n else 0
        tot_cls = "good" if tot_net >= 0 else "bad"
        rows.append(f"<tr style='border-top:2px solid #58a6ff;font-weight:600;'><td>TOTALE</td><td>{tot_n} ({tot_wins}W)</td><td>{tot_wr:.1f}%</td><td class='pnl good'>{_fmt_usdt(tot_gross)}</td><td class='pnl warn'>-{_fmt_usdt(tot_fees)}</td><td class='pnl {tot_cls}'>{_fmt_usdt(tot_net)} USDT</td><td></td></tr>")
        trades_by_day_html = "<table><tr><th>Data</th><th>Trades</th><th>WR</th><th>Gross</th><th>Fee</th><th>Net</th><th>Simboli</th></tr>" + "".join(rows) + "</table>"
    else:
        trades_by_day_html = '<div class="empty">Nessun trade chiuso dal 1° agosto.</div>'

    # Posizioni aperte
    if positions:
        rows = []
        for p in positions:
            cls = "good" if p["pnl"] >= 0 else "bad"
            rows.append(f"<tr><td>{p['symbol']}</td><td><b>{p['strategy']}</b></td><td><span class='badge {p['side'].lower()}'>{p['side']}</span></td><td>{p['size_fmt']}</td><td>{p['entry_fmt']}</td><td>{p['mark_fmt']}</td><td>{p['sl_fmt']}</td><td class='pnl {cls}'>{p['pnl_fmt']} USDT</td><td>{p['lev']}x</td></tr>")
        positions_html = "<table><tr><th>Symbol</th><th>Strategia</th><th>Side</th><th>Size</th><th>Entry</th><th>Mark</th><th>SL</th><th>PnL</th><th>Lev</th></tr>" + "".join(rows) + "</table>"
    else:
        positions_html = '<div class="empty">Nessuna posizione aperta</div>'

    # Ordini recenti (trade chiusi Bybit, no miei test, con Gross/Fee/Net per riga)
    if recent:
        rows = []
        for o in recent:
            warn = " ⚠️placeholder" if o.get("placeholder") else ""
            pnl_gross = o.get("pnl_gross", 0)
            fee = o.get("fee", 0)
            pnl_net = o.get("pnl_net", o.get("pnl", 0))
            pnl_net_class = "good" if pnl_net > 0 else "bad" if pnl_net < 0 else ""
            rows.append(f"<tr><td>{o['created_at_fmt']}</td><td>{o['symbol']}</td><td><span class='badge {o['side']}'>{o['side']}</span></td><td>{o['qty_fmt']}</td><td>{o['price_fmt']}{warn}</td><td>{o['notional_fmt']}</td><td>{o['strategy']}</td><td class='pnl good'>{_fmt_usdt(pnl_gross)}</td><td class='pnl warn'>-{_fmt_usdt(fee)}</td><td class='pnl {pnl_net_class}'>{_fmt_usdt(pnl_net)}</td></tr>")
        if rows:
            orders_html = f"<table><tr><th>Quando</th><th>Symbol</th><th>Side</th><th>Qty</th><th>Price</th><th>Notional</th><th>Strategy</th><th>Gross</th><th>Fee</th><th>Net</th></tr><tr><td colspan='10' style='text-align:center;color:#6e7681;font-size:11px;padding:4px;'>Trade chiusi Bybit (no miei test). {len(rows)} trade mostrati.</td></tr>" + "".join(rows) + "</table>"
        else:
            orders_html = '<div class="empty">Nessun trade chiuso reale (solo miei test)</div>'
    else:
        orders_html = '<div class="empty">Nessun trade chiuso</div>'

    # HTML finale
    html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>v2 Stats Dashboard — 2° Account</title>
<meta http-equiv="refresh" content="30">
<!-- Plotly rimosso: tutto render server-side -->
<style>
* {{ box-sizing: border-box; }}
body {{ background: #0d1117; color: #c9d1d9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 0; padding: 20px; }}
h1 {{ color: #58a6ff; margin: 0 0 8px 0; }}
h2 {{ color: #58a6ff; margin: 20px 0 10px 0; font-size: 16px; }}
.kpi-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-bottom: 20px; }}
.kpi {{ background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 12px 14px; }}
.kpi-label {{ color: #8b949e; font-size: 11px; text-transform: uppercase; margin-bottom: 4px; }}
.kpi-value {{ color: #f0f6fc; font-size: 22px; font-weight: 600; }}
.kpi-value.good {{ color: #3fb950; }}
.kpi-value.bad {{ color: #f85149; }}
.section {{ background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px; margin-bottom: 20px; }}
table {{ width: 100%; border-collapse: collapse; }}
th, td {{ text-align: left; padding: 8px 12px; border-bottom: 1px solid #21262d; }}
th {{ color: #8b949e; font-size: 11px; text-transform: uppercase; font-weight: 600; }}
tr.good td.pnl {{ color: #3fb950; }}
tr.bad td.pnl {{ color: #f85149; }}
.badge {{ display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }}
.badge.buy {{ background: rgba(63, 185, 80, 0.2); color: #3fb950; }}
.badge.sell {{ background: rgba(248, 81, 73, 0.2); color: #f85149; }}
.empty {{ color: #6e7681; text-align: center; padding: 30px; font-style: italic; }}
.footer {{ text-align: center; color: #6e7681; font-size: 12px; margin-top: 30px; }}
.grid-2 {{ display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }}
table.hm {{ border-collapse: collapse; font-size: 10px; width: 100%; }}
table.hm td {{ padding: 2px; text-align: center; min-width: 18px; height: 18px; color: #fff; }}
table.hm td.hm-corner, table.hm td.hm-h, table.hm td.hm-d {{ background: #161b22; color: #8b949e; font-weight: 600; }}
table.hm td.hm-c {{ color: #0d1117; font-weight: 600; }}
</style>
</head>
<body>
<h1>📊 v2 Stats Dashboard — 2° Account <span style="font-size: 14px; color: #6e7681;">(U05gSwYlB5)</span></h1>
<div class="footer">Auto-refresh 30s • Generato: {generated_at} • <a href="/json" style="color:#58a6ff;">/json</a> per API</div>
<div class="kpi-grid">{kpi_html}</div>
<div class="section">
  <h2>📈 Equity Curve</h2>
  {equity_html}
</div>
<div class="section">
  <h2>🎯 Sharpe / Sortino per Strategia (per-trade, rf=0)</h2>
  {strategy_sharpe_html}
</div>
<div class="grid-2">
  <div class="section"><h2>📊 Per Strategia</h2>{strategy_html}</div>
  <div class="section"><h2>💎 Per Asset</h2>{asset_html}</div>
</div>
<div class="grid-2">
  <div class="section"><h2>🏆 Top 5 Best</h2>{top_html}</div>
  <div class="section"><h2>💀 Top 5 Worst</h2>{bad_html}</div>
</div>
<div class="section">
  <h2>🎯 Top 5 Single Losses (singoli trade peggiori)</h2>
  {single_losses_html}
</div>
<div class="section">
  <h2>📂 Aperture (entry Buy) per Giorno</h2>
  {open_by_day_html}
</div>
<div class="section">
  <h2>🔥 Heatmap Giorno × Ora (chiusure trade)</h2>
  {heatmap_html}
</div>

<div class="section">
  <h2>📅 Trade per Giorno</h2>
  {trades_by_day_html}
</div>
<div class="section">
  <h2>📋 Posizioni Aperte (live Bybit)</h2>
  {positions_html}
</div>
<div class="section">
  <h2>📜 Ordini Recenti (ultimi 20)</h2>
  {orders_html}
</div>
<div class="footer">v2_stats_dashboard.py — 2° account Bybit (U05gSwYlB5)</div>
<script>
// Equity: render server-side in HTML (vedi sopra)
// Heatmap e Equity: render server-side in HTML (no Plotly CDN necessario)
</script>
</body>
</html>"""
    return html


# === DATA ASSEMBLY ===
def assemble_data():
    """Dati SOLO da Bybit (no webhook DB orders). Trade chiusi + posizioni aperte,
    filtrati dal 1° agosto ed escludendo MIEI test Pine-style."""
    closed_trades = get_closed_trades_bybit()
    positions = get_open_positions_bybit_no_test()
    balance = get_bybit_balance()
    queue_stats = get_queue_stats()
    # Carica anche ordini dal DB per la statistica 'posizioni aperte per giorno di entry'
    db_orders = get_orders(limit=2000, since_date="2026-08-01")

    kpis = compute_kpis_from_bybit(closed_trades, positions, balance)
    equity = compute_equity_curve_from_bybit(closed_trades, balance)
    by_strategy = compute_by_strategy_from_bybit(closed_trades)
    by_asset = compute_by_asset_from_bybit(closed_trades)
    by_asset_strategy = compute_by_asset_strategy_from_bybit(closed_trades)
    top_bad = compute_top_bad_from_bybit(closed_trades, top_n=5)
    heatmap = compute_heatmap_from_bybit(closed_trades)
    trades_by_day = compute_trades_by_day(closed_trades)
    single_losses = compute_top_single_losses(closed_trades, top_n=5)
    open_by_day = compute_entries_by_day(db_orders)

    positions_fmt = []
    for p in positions:
        size = _safe_float(p.get("size"))
        entry = _safe_float(p.get("avgPrice"))
        mark = _safe_float(p.get("markPrice"))
        sl = _safe_float(p.get("stopLoss"))
        pnl = _safe_float(p.get("unrealisedPnl"))
        lev = _safe_float(p.get("leverage"), 1)
        sym = p.get("symbol", "")
        strat = get_strategy_for_symbol(sym)
        strat_labels = {
            "vptr3": "VPTR3", "rettangolo": "RETTANGOLO",
            "rettangolo_simple": "RETT_SIMPLE", "ma_trailing": "MA_TRAILING",
        }
        strat_label = strat_labels.get(strat, strat.upper())
        positions_fmt.append({
            "symbol": sym,
            "strategy": strat_label,
            "side": p.get("side", ""),
            "size": size, "size_fmt": f"{size:,.4f}",
            "entry": entry, "entry_fmt": f"{entry:,.5f}",
            "mark": mark, "mark_fmt": f"{mark:,.5f}",
            "sl": sl, "sl_fmt": f"{sl:,.5f}" if sl > 0 else "—",
            "pnl": pnl, "pnl_fmt": f"{pnl:,.2f}",
            "lev": f"{lev:.0f}" if lev > 0 else "—",
        })

    # Trade chiusi recenti (gia' filtrati per data e test) - max 20
    # FIX 2026-08-10 (Mattia): ordina per updated_at DESC, altrimenti closed_trades[:20] prende
    # i primi che Bybit restituisce (casuali), non i piu' recenti.
    recent = []
    closed_trades_sorted_recent = sorted(
        closed_trades,
        key=lambda t: t.get("updated_at") or t.get("created_at") or "",
        reverse=True,
    )
    for t in closed_trades_sorted_recent[:20]:
        _ts = _parse_bybit_ts(t.get("updated_at") or t.get("created_at"))
        recent.append({
            "created_at_fmt": _ts.strftime("%Y-%m-%d %H:%M") if _ts else "",
            "symbol": t.get("symbol", ""),
            "side": t.get("side", ""),
            "qty_fmt": f"{t.get('qty', 0):,.4f}",
            "price_fmt": f"{t.get('entry_price', 0):,.5f}",
            "notional_fmt": f"{t.get('qty', 0) * t.get('entry_price', 0):,.2f}",
            "strategy": t.get("strategy", ""),
            "pnl_gross": t.get("pnl_gross", 0),
            "fee": t.get("fee", 0),
            "pnl_net": t.get("pnl_net", t.get("pnl", 0)),
            "pnl": t.get("pnl_net", t.get("pnl", 0)),  # alias
            "placeholder": False,
            "is_real": True,
            "is_test": False,
        })

    return {
        "kpis": kpis,
        "equity": equity,
        "by_strategy": by_strategy,
        "by_asset": by_asset,
        "by_asset_strategy": by_asset_strategy,
        "top_bad": top_bad,
        "heatmap": heatmap,
        "trades_by_day": trades_by_day,
        "single_losses": single_losses,
        "open_by_day": open_by_day,
        "positions": positions_fmt,
        "recent_orders": recent,
        "queue_stats": queue_stats,
        "generated_at": datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S"),
    }


# === HTTP HANDLER ===
class StatsHandler(BaseHTTPRequestHandler):
    def log_message(self, fmt, *args):
        pass

    def _check_auth(self):
        if not AUTH_PASS:
            return True
        auth = self.headers.get("Authorization", "")
        if not auth.startswith("Basic "):
            return False
        try:
            import base64
            decoded = base64.b64decode(auth[6:]).decode("utf-8", errors="ignore")
            if ":" not in decoded:
                return False
            u, p = decoded.split(":", 1)
            return hmac.compare_digest(u, AUTH_USER) and hmac.compare_digest(p, AUTH_PASS)
        except Exception:
            return False

    def _send_unauthorized(self):
        self.send_response(401)
        self.send_header("WWW-Authenticate", 'Basic realm="v2-stats"')
        self.send_header("Content-Type", "text/plain")
        self.end_headers()
        self.wfile.write(b"401 Unauthorized")

    def _send_json(self, code, data):
        body = json.dumps(data, default=str, ensure_ascii=False).encode("utf-8")
        self.send_response(code)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(body)

    def _send_html(self, code, body):
        body_bytes = body.encode("utf-8")
        self.send_response(code)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(body_bytes)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(body_bytes)

    def do_GET(self):
        if not self._check_auth():
            return self._send_unauthorized()
        path = urlparse(self.path).path.rstrip("/")
        try:
            data = assemble_data()
        except Exception as e:
            import traceback
            return self._send_html(500, f"<h1>500</h1><pre>{traceback.format_exc()}</pre>")
        if path in ("", "/", "/index"):
            return self._send_html(200, render_dashboard(data))
        elif path == "/json":
            return self._send_json(200, data)
        elif path == "/healthz":
            return self._send_json(200, {"status": "ok", "service": "v2-stats-dashboard"})
        else:
            self.send_response(404)
            self.end_headers()
            self.wfile.write(b"404")


def main():
    log(f"v2 stats dashboard avviato su {LISTEN_HOST}:{PORT}")
    log(f"DB: {DB_PATH}")
    log(f"Auth: {'abilitata' if AUTH_PASS else 'disabilitata (no password settata)'}")
    httpd = HTTPServer((LISTEN_HOST, PORT), StatsHandler)
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass
    finally:
        httpd.server_close()


if __name__ == "__main__":
    main()
