"""
Monitor visuale INTRADAY 30m per la strategia RETTANGOLO (breakout + retest).
- Range: high/low candela daily precedente (rettangolo blu)
- Candele: 2H intraday (ultimi 7 giorni)
- Segnale live: candela 2H corrente, se c'è breakout precedente nelle ultime 10 candele
  e la corrente tocca il livello rotto con pattern di inversione -> entry
- Auto-refresh: rigenera ogni 60s
"""
import sys
import time
import json
import urllib.request
import urllib.parse
from datetime import datetime, timezone
from pathlib import Path
from zoneinfo import ZoneInfo

import plotly.graph_objects as go
from plotly.subplots import make_subplots

from rettangolo_strategy import compute_signal, scan_signals
from rettangolo_config import load_symbols

# Fuso orario di default per visualizzazione candele: Europe/Rome (CEST/CET auto)
LOCAL_TZ = ZoneInfo("Europe/Rome")


OUT_HTML = Path(r"G:\AI TRADING ENGINE\live_deploy\bt_rettangolo\monitor.html")
ASSETS = load_symbols()  # letti da rettangolo_assets.csv (centralizzato)


def fetch_klines(symbol, interval, days):
    end_ms = int(time.time() * 1000)
    start_ms = end_ms - days * 24 * 3600 * 1000
    params = {"category": "linear", "symbol": symbol, "interval": interval, "start": start_ms, "end": end_ms, "limit": 200}
    qs = urllib.parse.urlencode(params)
    url = f"https://api.bybit.com/v5/market/kline?{qs}"
    with urllib.request.urlopen(url, timeout=15) as r:
        data = json.loads(r.read().decode("utf-8"))
    rows = data.get("result", {}).get("list", [])
    rows.sort(key=lambda x: int(x[0]))
    klines = []
    for r in rows:
        klines.append({
            "ts": int(r[0]),
            "date": datetime.fromtimestamp(int(r[0])/1000, tz=LOCAL_TZ).strftime("%Y-%m-%d %H:%M"),
            "open": float(r[1]),
            "high": float(r[2]),
            "low": float(r[3]),
            "close": float(r[4]),
            "volume": float(r[5])
        })
    return klines


def load_real_trades(symbol, n=5):
    """Carica gli ultimi N trade REALI eseguiti su Bybit per il symbol specifico.
    Ritorna lista di dict con: ts, ts_str, side, entry_price, exit_price, pnl_pct, reason.
    Source: webhook_listener/logs/trades.csv (generato da webhook_receiver.py)."""
    import csv as _csv
    trades_csv = Path(r"G:\AI TRADING ENGINE\live_deploy\webhook_listener\logs\trades.csv")
    if not trades_csv.exists():
        return []
    rows = []
    try:
        with open(trades_csv, "r", encoding="utf-8") as f:
            reader = _csv.DictReader(f)
            for row in reader:
                if row.get("symbol") != symbol:
                    continue
                try:
                    ts_iso = row.get("timestamp", "")
                    ts = datetime.fromisoformat(ts_iso).timestamp() * 1000
                    entry_str = row.get("entry_price", "").strip()
                    exit_str = row.get("exit_price", "").strip()
                    pnl_str = row.get("pnl_pct", "").strip()
                    rows.append({
                        "ts": ts,
                        "ts_str": datetime.fromtimestamp(ts/1000, tz=LOCAL_TZ).strftime("%Y-%m-%d %H:%M"),
                        "side": row.get("side", "Buy"),
                        "entry_price": float(entry_str) if entry_str else None,
                        "exit_price": float(exit_str) if exit_str else None,
                        "pnl_pct": float(pnl_str) if pnl_str else None,
                        "reason": row.get("reason", ""),
                    })
                except Exception:
                    continue
        rows.sort(key=lambda r: r["ts"], reverse=True)
        return rows[:n]
    except Exception:
        return []


def detect_position_strategy(symbol, side, created_unix_ms):
    """Ritorna la strategy che ha aperto la posizione: 'PINE' | 'RETT' | 'AUDIT' | 'UNKNOWN'.
    Match tramite orders.log (PINE/audit) e rettangolo_runner.log (RETT).
    Ritorna sempre UPPERCASE per match con pnl_summary keys."""
    # 1. Cerca in orders.log (PINE / audit_test / manual) - case insensitive
    orders_log = Path(r"G:\AI TRADING ENGINE\live_deploy\logs\orders.log")
    if orders_log.exists():
        try:
            with open(orders_log, "r", encoding="utf-8") as f:
                for line in f:
                    line_l = line.lower()
                    if symbol.lower() not in line_l or side.lower() not in line_l:
                        continue
                    if "strategy=vptr3" in line_l:
                        return "PINE"
                    if "strategy=audit_test" in line_l:
                        return "AUDIT"
        except Exception:
            pass
    # 2. Cerca in rettangolo_runner.log (RETTANGOLO)
    rett_log = Path(r"G:\AI TRADING ENGINE\live_deploy\webhook_listener\logs\rettangolo_runner.log")
    if rett_log.exists():
        try:
            # createdTime e' in ms, timestamp del log e' ISO locale Europe/Rome
            from datetime import datetime
            from zoneinfo import ZoneInfo
            tz = ZoneInfo("Europe/Rome")
            target_local = datetime.fromtimestamp(created_unix_ms / 1000, tz=tz)
            target_str_a = target_local.strftime("%Y-%m-%dT%H:%M")
            target_str_b = target_local.strftime("%Y-%m-%dT%H:%M:%S")
            with open(rett_log, "r", encoding="utf-8") as f:
                for line in f:
                    if "ORDER APERTA" not in line or symbol not in line or side.capitalize() not in line:
                        continue
                    if target_str_a in line or target_str_b in line:
                        return "RETT"
        except Exception:
            pass
    return "UNKNOWN"


def build_figure(symbol, intraday_klines, daily_klines):
    if len(intraday_klines) < 5 or len(daily_klines) < 2:
        return None
    # FIX 2026-07-17 (Verifier): filtro per SESSIONE LONDON 9-9 (non Bybit daily K-line mezzanotte UTC)
    cur_day = daily_klines[-1]
    prev_day = daily_klines[-2]
    # Calcola finestra sessione London 9-9
    _now_london = datetime.now(ZoneInfo("Europe/London"))
    _session_open_today_london = _now_london.replace(hour=9, minute=0, second=0, microsecond=0)
    _session_open_today_unix_ms = int(_session_open_today_london.timestamp()) * 1000
    _session_open_yesterday_unix_ms = _session_open_today_unix_ms - 86400 * 1000
    filtered = [k for k in intraday_klines if _session_open_yesterday_unix_ms <= k["ts"] <= _session_open_today_unix_ms]
    if len(filtered) < 3:
        filtered = intraday_klines  # fallback se filtro troppo aggressivo
    # Box CUSTOM aggregato da candele 2H filtrate (NON Bybit daily K-line)
    if filtered:
        custom_high = max(k["high"] for k in filtered)
        custom_low = min(k["low"] for k in filtered)
        custom_date = datetime.fromtimestamp(_session_open_yesterday_unix_ms / 1000, tz=LOCAL_TZ).strftime("%Y-%m-%d")
    else:
        custom_high = prev_day["high"]
        custom_low = prev_day["low"]
        custom_date = prev_day["date"][:10]
    prev_daily = {"date": custom_date, "ts": _session_open_yesterday_unix_ms,
                  "open": custom_low, "high": custom_high, "low": custom_low, "close": custom_low}
    # RANGE dal box custom (24h London 9-9)
    rng_top = custom_high
    rng_bot = custom_low
    rng_mid = (rng_top + rng_bot) / 2
    rng_pct = (rng_top - rng_bot) / rng_bot * 100
    # FIX 2026-07-18 (Verifier): 3 rettangoli daily K-line stacked + 1 IERI/OGGI split
    # 3 rettangoli daily (l'altro ieri, 2gg fa, 3gg fa) da daily_klines per visualizzazione contesto
    # Strategia breakout+retest invariata (calcolata su filtered = ieri+oggi candele 2H)
    # FIX 2026-07-18 (Verifier): calcola yesterday/today PRIMA dell'uso in dates/opens/...
    # filtered finisce a 10:00 Rome (ieri+1h), escludeva candele odierne. today = filtered[idx_today:] era vuoto.
    yesterday = [k for k in intraday_klines if _session_open_yesterday_unix_ms <= k["ts"] < _session_open_today_unix_ms]
    today = [k for k in intraday_klines if k["ts"] >= _session_open_today_unix_ms]
    # FIX 2026-07-18 (Verifier): dates/opens/highs/lows/closes includono ieri+oggi (no solo ieri)
    dates = [k["date"] for k in yesterday] + [k["date"] for k in today]
    opens = [k["open"] for k in yesterday] + [k["open"] for k in today]
    highs = [k["high"] for k in yesterday] + [k["high"] for k in today]
    lows = [k["low"] for k in yesterday] + [k["low"] for k in today]
    closes = [k["close"] for k in yesterday] + [k["close"] for k in today]
    # Sep date per separatori verticali a 9:00 London (ieri/oggi)
    sep_date = datetime.fromtimestamp(_session_open_today_unix_ms / 1000, tz=LOCAL_TZ).strftime("%Y-%m-%d %H:%M")
    # 3 daily K-line rettangoli: daily_klines[-2]=ieri gia' usato per strategia, daily_klines[-3..-5] = 3 rettangoli stacked
    # Bybit V5 daily K-line: 0:00 UTC -> 0:00 UTC. Per visualizzazione daily=ieri
    # Per coerenza con strategia (9-9 London), sep visivi a 9:00 London (BST = UTC+1)
    daily_klines_for_viz = daily_klines[-5:] if len(daily_klines) >= 5 else daily_klines
    # segnale live
    sig = compute_signal(prev_daily, filtered, len(filtered) - 1)
    sig_text = "NESSUN SETUP - in attesa breakout + retest"
    sig_color = "gray"
    if sig:
        sig_text = ">>> " + sig["signal"] + " ENTRY @ " + format(sig["entry"], ".4f") + " | SL=" + format(sig["sl"], ".4f") + " | TP=" + format(sig["tp"], ".4f") + " <<<"
        sig_color = "#4caf50" if sig["signal"] == "LONG" else "#ef5350"
    # label DIREZIONE compatta per chart (paper coords, in alto a destra)
    if sig:
        dir_text = sig["signal"] + " @" + format(sig["entry"], ".4f")
        dir_color = "#4caf50" if sig["signal"] == "LONG" else "#ef5350"
        dir_bg = "rgba(76,175,80,0.15)" if sig["signal"] == "LONG" else "rgba(239,83,80,0.15)"
    else:
        dir_text = "NESSUN SETUP"
        dir_color = "#888"
        dir_bg = "rgba(136,136,136,0.10)"
    historical = scan_signals(prev_daily, filtered)
    historical = historical[-5:]
    # figura
    fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.05, row_heights=[0.75, 0.25])
    # FIX 2026-07-18 (Verifier): 3 rettangoli daily stacked CON CANDELLE 2H INTERNE
    # Trace "passato" (3gg fa, 2gg fa, l'altro ieri) - candele 2H colori naturali (verde/rosso) per contesto visivo
    past_klines = [k for k in intraday_klines if k["ts"] < _session_open_yesterday_unix_ms]
    if past_klines:
        fig.add_trace(go.Candlestick(
            x=[k["date"] for k in past_klines],
            open=[k["open"] for k in past_klines],
            high=[k["high"] for k in past_klines],
            low=[k["low"] for k in past_klines],
            close=[k["close"] for k in past_klines],
            name=symbol + " 30m PASSATO (3gg fa, 2gg fa, l'altro ieri)",
            increasing_line_color="#26a69a", decreasing_line_color="#ef5350",
            line=dict(width=1)
        ), row=1, col=1)
    # ieri: candele piene, colori saturi
    fig.add_trace(go.Candlestick(
        x=[k["date"] for k in yesterday], open=[k["open"] for k in yesterday],
        high=[k["high"] for k in yesterday], low=[k["low"] for k in yesterday],
        close=[k["close"] for k in yesterday],
        name=symbol + " 30m IERI (chiuso)", increasing_line_color="#26a69a", decreasing_line_color="#ef5350",
        line=dict(width=1.5)
    ), row=1, col=1)
    # oggi: candele in formazione - colori pastello PIENI (no opacity) + outline 2px
    # niente opacity=0.5 sul trace: rende il fill trasparente e l'outline "dashed-style" su dark bg
    fig.add_trace(go.Candlestick(
        x=[k["date"] for k in today], open=[k["open"] for k in today],
        high=[k["high"] for k in today], low=[k["low"] for k in today],
        close=[k["close"] for k in today],
        name=symbol + " 30m OGGI (live)", increasing_line_color="#4dd0c1", decreasing_line_color="#e57373",
        line=dict(width=2)
    ), row=1, col=1)
    # sfondo leggermente più scuro sulla zona "oggi" per separazione visiva
    if today:
        today_first = today[0]["date"]
        today_last = today[-1]["date"]
        # estendi la zona di 2h a sinistra (fino al confine di ieri) e 2h a destra
        fig.add_shape(
            type="rect",
            x0=today_first, x1=today_last,
            y0=rng_bot - (rng_top - rng_bot) * 0.5, y1=rng_top + (rng_top - rng_bot) * 0.5,
            line=dict(width=0),
            fillcolor="rgba(255,255,255,0.04)",
            layer="below"
        )
    # FIX 2026-07-17 (Verifier): separatore verticale a 9:00 London (= 10:00 Italy estate), non mezzanotte UTC
    if today and yesterday:
        sep_x = sep_date
        fig.add_shape(
            type="line", x0=sep_x, x1=sep_x,
            y0=rng_bot - (rng_top - rng_bot) * 0.5, y1=rng_top + (rng_top - rng_bot) * 0.5,
            line=dict(color="rgba(255,255,255,0.65)", width=1.5, dash="dash")
        )
        # label "OGGI (live)" in alto a destra (paper coords su ENTRAMBI gli assi) - non overlappa con candele
        fig.add_annotation(
            xref="paper", yref="paper", x=0.95, y=1.02,
            text="<b>OGGI (live) →</b>", showarrow=False,
            xanchor="right", yanchor="bottom",
            font=dict(color="#4dd0c1", size=12, family="monospace"),
            bgcolor="rgba(77,208,193,0.15)", bordercolor="#4dd0c1", borderwidth=1, borderpad=4
        )
        # label DIREZIONE (SHORT/LONG/NESSUN SETUP) di fianco a OGGI, leggermente distanziata
        fig.add_annotation(
            xref="paper", yref="paper", x=0.71, y=1.02,
            text="<b>" + dir_text + "</b>", showarrow=False,
            xanchor="right", yanchor="bottom",
            font=dict(color=dir_color, size=12, family="monospace"),
            bgcolor=dir_bg, bordercolor=dir_color, borderwidth=1, borderpad=4
        )
    # FIX 2026-07-18 (Verifier): 3 rettangoli daily K-line stacked + 1 OGGI tratteggiato
    # Strategia breakout+retest invariata (calcolata su filtered = candele 2H ieri+oggi London 9-9)
    # Revert fix 4: x0=dates[0] per IERI/OGGI (era sep_date_2gg_fa che dava errore)
    # 3 rettangoli daily passati da daily_klines (l'altro ieri, 2gg fa, 3gg fa) per contesto
    # Ogni daily K-line: 0:00 UTC -> 0:00 UTC (shift 1h vs London 9-9, OK per visualizzazione)
    # daily_klines ordinato ASC per ts: [-5]=3gg fa, [-4]=2gg fa, [-3]=l'altro ieri, [-2]=ieri, [-1]=oggi
    fig.add_shape(
        type="rect", x0=dates[0], x1=sep_date, y0=rng_bot, y1=rng_top,
        line=dict(color="rgba(33,150,243,0.7)", width=1.5), fillcolor="rgba(33,150,243,0.06)",
        name="Range " + prev_daily["date"][:10] + " (IERI - chiuso)"
    )
    fig.add_shape(
        type="rect", x0=sep_date, x1=dates[-1], y0=rng_bot, y1=rng_top,
        line=dict(color="rgba(33,150,243,0.7)", width=1.5, dash="dash"), fillcolor="rgba(33,150,243,0.03)",
        name="Range " + prev_daily["date"][:10] + " (OGGI - live, in formazione)"
    )
    # 3 rettangoli daily CON CANDELLE 2H INTERNE (l'altro ieri, 2gg fa, 3gg fa)
    # Calcolati da candele 2H filtrate per finestra London 9-9 di ogni giorno
    day_boundaries = [
        (_session_open_today_unix_ms - 3 * 86400 * 1000, _session_open_today_unix_ms - 2 * 86400 * 1000, "3gg fa"),
        (_session_open_today_unix_ms - 2 * 86400 * 1000, _session_open_yesterday_unix_ms, "2gg fa"),
        (_session_open_yesterday_unix_ms, _session_open_today_unix_ms, "l'altro ieri"),
    ]
    for start_ms, end_ms, label in day_boundaries:
        day_klines = [k for k in intraday_klines if start_ms <= k["ts"] < end_ms]
        if not day_klines:
            continue
        day_top = max(k["high"] for k in day_klines)
        day_bot = min(k["low"] for k in day_klines)
        start_str = datetime.fromtimestamp(start_ms / 1000, tz=LOCAL_TZ).strftime("%Y-%m-%d %H:%M")
        end_str = datetime.fromtimestamp(end_ms / 1000, tz=LOCAL_TZ).strftime("%Y-%m-%d %H:%M")
        # Rettangolo daily stacked (violetto/lilla #9c27b0 per distinguerlo dal blu IERI/OGGI)
        fig.add_shape(
            type="rect", x0=start_str, x1=end_str, y0=day_bot, y1=day_top,
            line=dict(color="rgba(156,39,176,0.8)", width=1.5, dash="dot"), fillcolor="rgba(156,39,176,0.08)",
            name=label + " " + start_str[:10] + " range"
        )
        # Sep visivo a 9:00 London di inizio giorno (violetto)
        fig.add_shape(
            type="line", x0=end_str, x1=end_str,
            y0=day_bot - (day_top - day_bot) * 0.3, y1=day_top + (day_top - day_bot) * 0.3,
            line=dict(color="rgba(156,39,176,0.6)", width=1.2, dash="dot")
        )
    # LINEE TOP/BOTTOM/MID strategia (IERI/OGGI, come fix 2)
    fig.add_shape(
        type="line", x0=dates[0], x1=sep_date, y0=rng_top, y1=rng_top,
        line=dict(color="rgba(33,150,243,1.0)", width=2.5),
        name="TOP " + format(rng_top, ".4f") + " (IERI)"
    )
    fig.add_shape(
        type="line", x0=sep_date, x1=dates[-1], y0=rng_top, y1=rng_top,
        line=dict(color="rgba(33,150,243,1.0)", width=2.5, dash="dash"),
        name="TOP " + format(rng_top, ".4f") + " (OGGI)"
    )
    fig.add_shape(
        type="line", x0=dates[0], x1=sep_date, y0=rng_bot, y1=rng_bot,
        line=dict(color="rgba(33,150,243,1.0)", width=2.5),
        name="BOTTOM " + format(rng_bot, ".4f") + " (IERI)"
    )
    fig.add_shape(
        type="line", x0=sep_date, x1=dates[-1], y0=rng_bot, y1=rng_bot,
        line=dict(color="rgba(33,150,243,1.0)", width=2.5, dash="dash"),
        name="BOTTOM " + format(rng_bot, ".4f") + " (OGGI)"
    )
    fig.add_shape(
        type="line", x0=dates[0], x1=sep_date, y0=rng_mid, y1=rng_mid,
        line=dict(color="rgba(255,152,0,1.0)", width=2),
        name="MID " + format(rng_mid, ".4f") + " (IERI)"
    )
    fig.add_shape(
        type="line", x0=sep_date, x1=dates[-1], y0=rng_mid, y1=rng_mid,
        line=dict(color="rgba(255,152,0,1.0)", width=2, dash="dot"),
        name="MID " + format(rng_mid, ".4f") + " (OGGI)"
    )
    for k in filtered:
        if k["close"] > rng_top:
            fig.add_trace(go.Scatter(
                x=[k["date"]], y=[k["close"]], mode="markers",
                marker=dict(symbol="triangle-up", size=8, color="#4caf50", opacity=0.6),
                showlegend=False, hoverinfo="skip"
            ), row=1, col=1)
        elif k["close"] < rng_bot:
            fig.add_trace(go.Scatter(
                x=[k["date"]], y=[k["close"]], mode="markers",
                marker=dict(symbol="triangle-down", size=8, color="#ef5350", opacity=0.6),
                showlegend=False, hoverinfo="skip"
            ), row=1, col=1)
    # etichette prezzi lato DESTRO - FIX 2026-07-19 (Coder, richiesta Mattia): spostate a destra
    # (x=1.0 + xanchor="right" con margin.r=240 cosi' le label sono nel margin destro, non coprono le candele)
    fig.add_annotation(
        xref="paper", x=1.0, y=rng_top, text="SELL " + format(rng_top, ".4f"),
        showarrow=False, xanchor="right", yanchor="middle",
        font=dict(color="#ef5350", size=14, family="monospace"),
        bgcolor="rgba(239,83,80,0.15)", bordercolor="#ef5350", borderwidth=1, borderpad=4
    )
    fig.add_annotation(
        xref="paper", x=1.0, y=rng_bot, text="BUY " + format(rng_bot, ".4f"),
        showarrow=False, xanchor="right", yanchor="middle",
        font=dict(color="#4caf50", size=14, family="monospace"),
        bgcolor="rgba(76,175,80,0.15)", bordercolor="#4caf50", borderwidth=1, borderpad=4
    )
    fig.add_annotation(
        xref="paper", x=1.0, y=rng_mid, text="TP " + format(rng_mid, ".4f"),
        showarrow=False, xanchor="right", yanchor="middle",
        font=dict(color="#ff9800", size=14, family="monospace"),
        bgcolor="rgba(255,152,0,0.15)", bordercolor="#ff9800", borderwidth=1, borderpad=4
    )
    for s in historical:
        color = "#4caf50" if s["signal"] == "LONG" else "#ef5350"
        symbol_arrow = "triangle-up" if s["signal"] == "LONG" else "triangle-down"
        fig.add_trace(go.Scatter(
            x=[s["date"]], y=[s["entry"]], mode="markers",
            marker=dict(symbol=symbol_arrow, size=12, color=color, line=dict(width=2, color="white")),
            showlegend=False, hoverinfo="text",
            hovertext=s["signal"] + " @ " + format(s["entry"], ".4f")
        ), row=1, col=1)
    colors = ["#26a69a" if closes[i] >= opens[i] else "#ef5350" for i in range(len(closes))]
    fig.add_trace(go.Bar(x=dates, y=[k["volume"] for k in filtered], marker_color=colors, name="Volume", showlegend=False), row=2, col=1)
    # TRADE PASSATI: triangolino alla data di entry (verde=win, rosso=loss)
    # Tolto le linee/label PnL per grafico piu' pulito
    real_trades = load_real_trades(symbol, n=5)
    for t in real_trades:
        if t.get("reason") != "close" or t.get("exit_price") is None:
            continue
        ts_str = t["ts_str"]
        pnl = t.get("pnl_pct", 0) or 0
        side = t.get("side", "Buy")
        is_long = side.lower() in ("buy", "long")
        is_win = pnl > 0
        # triangolino entry: WIN verde scuro, LOSS rosso scuro
        tri_color = "#2e7d32" if is_win else "#c62828"
        tri_symbol = "triangle-up" if is_long else "triangle-down"
        # piu' piccolo e opaco di quelli delle posizioni aperte
        fig.add_trace(go.Scatter(
            x=[ts_str], y=[t["entry_price"]],
            mode="markers",
            marker=dict(symbol=tri_symbol, size=8, color=tri_color,
                        opacity=0.5, line=dict(color=tri_color, width=0.5)),
            name="Trade",
            showlegend=False, hoverinfo="text",
            hovertext=f"CHIUSO {side} @ {t['entry_price']:.4f} -> {t['exit_price']:.4f} ({pnl:+.2f}%)"
        ), row=1, col=1)
    # POSIZIONI ATTUALMENTE APERTE SU BYBIT (marker con strategy detection)
    # PINE (VPTR3) = quadrato blu, RETT = triangolo verde/rosso, OLD = triangolo grigio
    try:
        from bybit_demo_client import BybitDemoClient
        _bdc = BybitDemoClient()
        _open_pos = _bdc.fetch_positions(symbol)
        if _open_pos:
            _now_str = datetime.now().astimezone().isoformat()
            for _p in _open_pos:
                _avg = float(_p.get("avgPrice", 0) or 0)
                _side_raw = _p.get("side", "")
                _created_ms = int(_p.get("createdTime", 0) or 0)
                if _avg <= 0:
                    continue
                _is_long = _side_raw.lower() in ("buy", "long")
                _strategy = detect_position_strategy(symbol, _side_raw, _created_ms)
                # Marker shape + color per strategy
                if _strategy == "PINE":
                    _marker_symbol = "square"
                    _marker_color = "#2196f3"  # blu PINE
                    _label_text = "PINE"
                elif _strategy == "RETT":
                    _marker_symbol = "triangle-up" if _is_long else "triangle-down"
                    _marker_color = "#3fb950" if _is_long else "#ef5350"
                    _label_text = "RETT"
                elif _strategy == "AUDIT":
                    _marker_symbol = "diamond"
                    _marker_color = "#9c27b0"  # viola audit
                    _label_text = "AUDIT"
                else:  # UNKNOWN / OLD
                    _marker_symbol = "triangle-up" if _is_long else "triangle-down"
                    _marker_color = "#888888"  # grigio OLD
                    _label_text = "OLD"
                fig.add_trace(go.Scatter(
                    x=[_now_str], y=[_avg],
                    mode="markers",
                    marker=dict(symbol=_marker_symbol, size=14, color=_marker_color,
                                line=dict(color="white", width=1.5)),
                    name="Open",
                    showlegend=False, hoverinfo="text",
                    hovertext=f"APERTA {_strategy} {_side_raw} {_p.get('size','')} @ {_avg}"
                ), row=1, col=1)
                # label "OPEN + strategy" accanto al marker
                _label_y = _avg + (rng_top - rng_bot) * 0.04 if _is_long else _avg - (rng_top - rng_bot) * 0.04
                fig.add_annotation(
                    x=_now_str, y=_label_y,
                    text=f"<b>OPEN {_label_text}</b> {_p.get('size','')} @ {_avg}",
                    showarrow=False, xanchor="left", yanchor="bottom" if _is_long else "top",
                    font=dict(color=_marker_color, size=10, family="monospace"),
                    bgcolor="rgba(0,0,0,0.6)", borderpad=2
                )
    except Exception:
        pass
    fig.update_layout(
        title="<b>" + symbol + "</b>  •  Range " + prev_daily["date"][:10] + ": <b>" + format(rng_bot, ".4f") + " — " + format(rng_top, ".4f") + "</b>  (" + format(rng_pct, ".2f") + "%)<br>"
              "<span style='font-size:15px;color:" + sig_color + "'><b>" + sig_text + "</b></span>",
        template="plotly_dark", height=700, width=1500,
        xaxis=dict(dtick=3600000, tickformat="%H:%M\n%b %d"),
        xaxis2=dict(dtick=3600000, tickformat="%H:%M\n%b %d"),
        xaxis_rangeslider_visible=False,
        xaxis2_rangeslider_visible=False,
        # legenda spostata in basso sotto il volume per non sovrapporsi alle label DIREZIONE/OGGI
        legend=dict(orientation="h", y=-0.15, x=0.5, xanchor="center", font=dict(size=10)),
        margin=dict(t=80, b=80, l=80, r=400),  # FIX 2026-07-19 v2: r=400 per spazio visibile tra rettangolo e label SELL/BUY/TP (ESTERNE al rettangolo)
    )
    return fig


def build_html(per_symbol_data, generated_at, open_symbols=None, pnl_summary=None):
    figs = {}
    for sym, data in per_symbol_data.items():
        fig = build_figure(sym, data["intraday"], data["daily"])
        if fig:
            figs[sym] = fig
    if not figs:
        return "<h1>Nessun dato disponibile</h1>"
    # Riordina le tab: prima i symbol con posizione aperta (Mattia: in evidenza)
    if open_symbols:
        ordered = sorted(figs.items(), key=lambda kv: 0 if kv[0] in open_symbols else 1)
    else:
        ordered = list(figs.items())
    tabs_html = ""
    divs_html = ""
    for i, (sym, fig) in enumerate(ordered):
        active = " active" if i == 0 else ""
        tabs_html += '<button class="tablinks' + active + '" onclick="openTab(event, \'' + sym + '\')">' + sym + '</button>\n'
        div_inner = fig.to_html(full_html=False, include_plotlyjs="cdn", div_id="plot_" + sym)
        divs_html += '<div id="' + sym + '" class="tabcontent' + active + '">\n' + div_inner + '\n</div>\n'
    # PnL summary per strategy (mini-pannello in fondo alla pagina)
    pnl_html = ""
    if pnl_summary:
        strategy_colors = {"PINE": "#2196f3", "RETT": "#4caf50", "AUDIT": "#9c27b0", "OLD": "#888"}
        rows = ""
        for strat in ["PINE", "RETT", "AUDIT", "OLD"]:
            positions = pnl_summary.get(strat, [])
            if not positions:
                continue
            total_pnl_usd = sum(p["pnl_usd"] for p in positions)
            color = strategy_colors.get(strat, "#fff")
            # Header strategy
            rows += '<div style="margin-top:6px; padding:6px; background:rgba(255,255,255,0.04); border-left:4px solid ' + color + ';">'
            rows += '<b style="color:' + color + ';">' + strat + ' (' + str(len(positions)) + ' pos)</b> '
            rows += '<span style="color:' + ("#4caf50" if total_pnl_usd >= 0 else "#ef5350") + ';">PnL tot: ' + ("{:+.2f}".format(total_pnl_usd)) + ' USDT</span></div>'
            # Singole posizioni
            for p in positions:
                pnl_color = "#4caf50" if p["pnl_usd"] >= 0 else "#ef5350"
                rows += '<div style="padding:2px 12px; font-size:12px; color:#ccc;">'
                rows += "  " + p["symbol"] + " " + p["side"] + " size=" + "{:.4f}".format(p["size"]) + " "
                rows += "@ " + "{:.4f}".format(p["avg"]) + " -> mark " + "{:.4f}".format(p["mark"]) + " "
                rows += '<span style="color:' + pnl_color + ';">' + ("{:+.2f}%".format(p["pnl_pct"])) + ' (' + ("{:+.2f}".format(p["pnl_usd"])) + ' USDT)</span>'
                rows += '</div>'
        if rows:
            pnl_html = '<div class="pnl-panel" style="margin-top:20px; padding:12px; border:1px solid #444; background:rgba(0,0,0,0.3);">'
            pnl_html += '<h3 style="margin:0 0 8px 0; color:#ff9800;">[PnL] PnL LIVE per Strategy</h3>'
            pnl_html += rows
            pnl_html += '</div>'
    css = """
body { background: #1a1a1a; color: #fff; font-family: monospace; margin: 20px; }
h1 { color: #2196f3; }
.tab { overflow: hidden; border-bottom: 1px solid #555; }
.tab button { background: #222; color: #fff; border: none; padding: 12px 20px; cursor: pointer; font-size: 14px; font-weight: bold; }
.tab button:hover { background: #333; }
.tab button.active { background: #2196f3; color: #fff; }
.tabcontent { display: none; padding: 10px 0; }
.tabcontent.active { display: block; }
.footer { margin-top: 30px; color: #888; font-size: 12px; text-align: center; }
.refresh { color: #4caf50; }
"""
    js = """
function openTab(evt, sym) {
    var i, tabcontent, tablinks;
    tabcontent = document.getElementsByClassName("tabcontent");
    for (i = 0; i < tabcontent.length; i++) { tabcontent[i].className = tabcontent[i].className.replace(" active", ""); }
    tablinks = document.getElementsByClassName("tablinks");
    for (i = 0; i < tablinks.length; i++) { tablinks[i].className = tablinks[i].className.replace(" active", ""); }
    document.getElementById(sym).className += " active";
    evt.currentTarget.className += " active";
    var plotDiv = document.querySelector('#' + sym + ' .plotly');
    if (plotDiv && window.Plotly) {
        window.Plotly.Plots.resize(plotDiv);
    }
}
"""
    html = """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="60">
<title>Rettangolo Monitor 30m - """ + generated_at + """</title>
<style>""" + css + """</style>
</head>
<body>
<h1>📊 Rettangolo Monitor INTRADAY 30m — Strategia Charter (Breakout + Retest)</h1>
<p>Generato: <b>""" + generated_at + """</b> | Auto-refresh: <span class="refresh">ogni 60s</span></p>
<p><b>Range blu</b> = high/low candela daily precedente • <b>Linea arancione</b> = MID (target TP) • <b>Rettangoli viola</b> = daily contesto (3gg fa, 2gg fa, l'altro ieri)</p>
<p><b>Marker posizioni</b>: 🟦 quadrato=PINE (VPTR3) | 🔺/🔻 triangolo=RETT | 💎 diamante=AUDIT | 🔘 grigio=OLD</p>
<div class="tab">
""" + tabs_html + """</div>
""" + divs_html + """
""" + pnl_html + """
<div class="footer">
Live Bybit Demo • Charter: TP al MID del range, SL 1% oltre entry • Margin $500, Leva 3x
</div>
<script>""" + js + """</script>
</body>
</html>
"""
    return html


def run_once():
    global ASSETS
    # Ricarica la lista asset da CSV ad ogni iterazione (così modifiche al CSV
    # sono riflesse entro 60s senza riavvio del processo)
    ASSETS = load_symbols()
    generated_at = datetime.now(timezone.utc).astimezone(LOCAL_TZ).strftime("%Y-%m-%d %H:%M:%S %Z")
    print("[" + generated_at + f"] rigenero monitor ({len(ASSETS)} asset: {','.join(ASSETS)})...")
    per_symbol_data = {}
    # Per ogni asset, leggi il tf_min dal CSV
    from rettangolo_config import load_assets
    assets_full = load_assets()
    tf_map = {a["symbol"]: a["tf_min"] for a in assets_full}
    for sym in ASSETS:
        try:
            tf_min = tf_map.get(sym, "120")  # default 2H se non mappato
            intraday = fetch_klines(sym, tf_min, days=4)
            daily = fetch_klines(sym, "D", days=10)
            if intraday and daily:
                per_symbol_data[sym] = {"intraday": intraday, "daily": daily}
        except Exception as e:
            print("  err " + sym + ": " + str(e))
    # Identifica symbol con posizione aperta (per riordinare le tab)
    open_symbols = set()
    # PnL summary per strategy (PINE / RETT / AUDIT / OLD)
    pnl_summary = {"PINE": [], "RETT": [], "AUDIT": [], "OLD": []}
    try:
        from bybit_demo_client import BybitDemoClient
        for p in BybitDemoClient().fetch_positions():
            sym = p.get("symbol")
            size = float(p.get("size", 0) or 0)
            if sym and size > 0:
                open_symbols.add(sym)
                avg = float(p.get("avgPrice", 0) or 0)
                mark = float(p.get("markPrice", 0) or 0)
                side = p.get("side", "")
                pnl_usd = float(p.get("unrealisedPnl", 0) or 0)
                created_ms = int(p.get("createdTime", 0) or 0)
                if avg > 0:
                    pnl_pct = (mark - avg) / avg * 100
                else:
                    pnl_pct = 0.0
                strat = detect_position_strategy(sym, side, created_ms)
                pnl_summary[strat.upper()].append({
                    "symbol": sym, "side": side, "size": size,
                    "avg": avg, "mark": mark,
                    "pnl_pct": pnl_pct, "pnl_usd": pnl_usd
                })
    except Exception as e:
        print("  warn fetch open positions: " + str(e))
    if open_symbols:
        print("  posizioni aperte (tab prime): " + ",".join(sorted(open_symbols)))
    print("  PnL per strategy: " + ", ".join(
        s + ":" + str(len(pnl_summary[s])) for s in pnl_summary if pnl_summary[s]
    ))

    html = build_html(per_symbol_data, generated_at, open_symbols=open_symbols, pnl_summary=pnl_summary)
    OUT_HTML.parent.mkdir(parents=True, exist_ok=True)
    # forza no-cache (meta refresh tenuto, vedi sopra)
    html = html.replace(
        '<meta http-equiv="refresh" content="60">',
        '<meta http-equiv="refresh" content="60">\n<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">\n<meta http-equiv="Pragma" content="no-cache">'
    )
    OUT_HTML.write_text(html, encoding="utf-8")
    print("  -> " + str(OUT_HTML) + " (" + str(len(html)) + " chars)")


if __name__ == "__main__":
    if "--once" in sys.argv:
        run_once()
    else:
        print("Loop infinito, rigenero ogni 60s. Ctrl+C per uscire.")
        while True:
            try:
                run_once()
            except Exception as e:
                print("ERR: " + str(e))
            time.sleep(60)





