"""
AI Trading Engine — Live Bybit Demo (custom REST client, bypass ccxt bug)
Setup production-ready (con MIL dynamic sizing):
  - ZEC 4H VPTR3 mask 10001011 — TRIPLA VALIDATA
  - AERO 4H VPTR3 mask 01010101 — 2/3 borderline
  - DASH 4H VPTR3 mask 10000111 — 2/3 borderline
  - PYTH 3H 1M VPTR3 mask 00100000 (DLS)      — SOLVER-validated 11/07 (N=9 +16.3% PF 10.23)
  - PYTH 3H 1M VPTR3 mask 00100010 (CLS+DLS)  — SOLVER-validated 11/07 (N=12 +15.6% PF 3.43)
  - PYTH 3H 1M VPTR3 mask 01100000 (DLS+DSL)  — SOLVER-validated 11/07 (N=11 +12.2% PF 3.28)

CHARTER ORIGINALI Mattia (ripristinati 12/07 post-bug analisi):
  - TP1: 50% size a +3%   (reduceOnly limit)
  - TP2: 50% size a +5%   (reduceOnly limit)
  - SL:  ATR 2x clampato a -3% max  (NO trailing di default, trailing opzionale)
  - LEVA TARGET 3x per TUTTI gli asset
  - NO media-up / NO double-buy (anti-double-buy hard rule)
  - Entry anchor al PRIMO fill (TP ricalcolati solo se Charter apre per primo)

Architettura Bybit V5:
  - 1 entry market (qty piena)
  - 2 reduceOnly limit order: qty/2 a +3% e qty/2 a +5%
  - 1 SL via /v5/position/trading-stop
  - State persistente in state_live.json (entry originale, size, side)
MIL integration: legge mil_report_latest.json e applica size suggerito + verdict check.

CLI:
  python live_engine.py --once                  # 1 loop (DRY-RUN di default)
  python live_engine.py --once --confirm-live   # LIVE Bybit demo (serve OK Mattia)
  python live_engine.py --status                # stato posizioni aperte
  python live_engine.py --setup ZEC_4H           # solo uno specifico setup
"""
import os
import sys
import json
import argparse
from pathlib import Path
from datetime import datetime, timezone
import pandas as pd
import numpy as np
from bybit_demo_client import BybitDemoClient

MIL_REPORT_PATH = Path(r"C:\Users\Mattia\.mavis\sessions\mvs_7410904bba534d318a61faac8876cd4c\workspace\market_intelligence\mil_report_latest.json")

# ============== SETUP CONFIG (production-ready) ==============
# PYTH 3H 1M validation: SOLVER Mattia 11/07 (1M 30gg, 2K USDT, fee 0.1%, max_exit 3)
#   mask 00100000 (DLS)    : N=9  Net +16.3%  WR 77.8%  PF 10.23  MAX DD 1.4%
#   mask 00100010 (CLS+DLS): N=12 Net +15.6%  WR 75.0%  PF 3.43   MAX DD 6.9%
#   mask 01100000 (DLS+DSL): N=11 Net +12.2%  WR 72.7%  PF 3.28   MAX DD 4.0%
# Tutte 3 SHORT-biased: regime filter Pine-faithful obbligatorio (no controtendenza EMA50+ADX)
# ============== CHARTER TARGET LEVA ==============
# REGOLE Mattia 12/07: TARGET LEVA = 3x per TUTTI gli asset.
# Logica set_leverage in bybit_demo_client:
#   1. Chiama get_leverage() PRIMA per leggere leva attuale
#   2. Se leva attuale == 3x: BYPASSA, NON chiamare set_leverage (noop)
#   3. Altrimenti: chiama set_leverage(3)
# Bybit gestisce 110043 "leverage not modified" come OK se gia' a 3x.
# ============== SET LEVA 3x PER TUTTI ==============

SETUPS = [
    {
        "name": "ZEC_4H", "symbol_bybit": "ZECUSDT", "symbol_ccxt": "ZEC/USDT:USDT",
        "tf": "4h", "interval": 240, "mask": "10001011",
        "bb_length": 55, "bb_mult": 1.0, "roc_length": 36, "atr_length": 16,
        "margin_usdt": 500.0,
        "tp1_pct": 0.03, "tp2_pct": 0.05,
        "sl_atr_mult": 2.0, "sl_clamp_min": -0.03, "sl_clamp_max": -0.03,
        "trailing_stop_pct": 0.0,  # 0 = no trailing, >0 = attiva trailing dopo TP1
        "max_bars": 5, "min_bars_between": 4, "leverage": 3,
        "is_primary": True,  # Charter primari: AI Enhancer NON riduce size
    },
    {
        "name": "AERO_4H", "symbol_bybit": "AEROUSDT", "symbol_ccxt": "AERO/USDT:USDT",
        "tf": "4h", "interval": 240, "mask": "01010101",
        "bb_length": 55, "bb_mult": 1.0, "roc_length": 36, "atr_length": 16,
        "margin_usdt": 250.0,
        "tp1_pct": 0.03, "tp2_pct": 0.05,
        "sl_atr_mult": 2.0, "sl_clamp_min": -0.03, "sl_clamp_max": -0.03,
        "trailing_stop_pct": 0.0,
        "max_bars": 5, "min_bars_between": 4, "leverage": 3,
        "is_primary": True,
    },
    {
        "name": "DASH_4H", "symbol_bybit": "DASHUSDT", "symbol_ccxt": "DASH/USDT:USDT",
        "tf": "4h", "interval": 240, "mask": "10000111",
        "bb_length": 55, "bb_mult": 1.0, "roc_length": 36, "atr_length": 16,
        "margin_usdt": 250.0,
        "tp1_pct": 0.03, "tp2_pct": 0.05,
        "sl_atr_mult": 2.0, "sl_clamp_min": -0.03, "sl_clamp_max": -0.03,
        "trailing_stop_pct": 0.0,
        "max_bars": 5, "min_bars_between": 4, "leverage": 3,
        "is_primary": True,
    },
]


def load_mil_sizes():
    """Carica sizing suggerito da MIL report. Ritorna dict {name: {size, verdict}}."""
    if not MIL_REPORT_PATH.exists():
        return {s["name"]: {"size": s["margin_usdt"], "verdict": "GO (MIL missing)"} for s in SETUPS}
    try:
        report = json.loads(MIL_REPORT_PATH.read_text(encoding="utf-8"))
        sizes = {}
        for entry in report:
            if "error" in entry:
                continue
            name_map = {"ZEC_4H_PRIMARY": "ZEC_4H",
                        "AERO_4H_SECONDARY": "AERO_4H",
                        "DASH_4H_SECONDARY": "DASH_4H"}
            name = name_map.get(entry.get("label"))
            if name:
                sizes[name] = {
                    "size": entry.get("suggested_size_usdt", SETUPS_BY_NAME[name]["margin_usdt"]),
                    "verdict": entry.get("verdict", "GO"),
                    "confidence": entry.get("global_confidence", 0.5),
                }
        return sizes
    except Exception as e:
        print(f"[WARN] MIL load fallita: {e}")
        return {s["name"]: {"size": s["margin_usdt"], "verdict": "GO (MIL err)"} for s in SETUPS}


SETUPS_BY_NAME = {s["name"]: s for s in SETUPS}


def get_setup_size(setup_name):
    """Ritorna size suggerito da MIL per il setup."""
    mil = load_mil_sizes()
    s = mil.get(setup_name, {})
    return s.get("size", SETUPS_BY_NAME[setup_name]["margin_usdt"]), s.get("verdict", "GO")

MAX_OPEN_TRADES_PORTFOLIO = 5
STATE_FILE = Path(__file__).parent / "state_live.json"
LOG_FILE = Path(__file__).parent / "trades_log_live.csv"


# ============== STATE ==============
def load_state():
    if STATE_FILE.exists():
        try:
            return json.loads(STATE_FILE.read_text(encoding="utf-8"))
        except Exception as e:
            print(f"[WARN] state load: {e}")
    return {"last_check_ts": None, "open_positions": {}, "last_signal_bar": {}}


def save_state(state):
    STATE_FILE.write_text(json.dumps(state, indent=2, default=str), encoding="utf-8")


# ============== INDICATORS ==============
def compute_indicators(df, bb_len, factor, roc_len, atr_len):
    df = df.copy()
    df.columns = [c.strip().lower() for c in df.columns]
    close = df["close"].astype(float)
    high = df["high"].astype(float)
    low = df["low"].astype(float)
    vol = df["volume"].astype(float)
    n = len(close)
    sma = close.rolling(bb_len, min_periods=bb_len).mean()
    sd = close.rolling(bb_len, min_periods=bb_len).std(ddof=0)
    df["bb_plus"] = sma + factor * sd
    df["bb_minus"] = sma - factor * sd
    prev = close.shift(roc_len)
    df["roc"] = (close - prev) / prev * 100.0
    tr = pd.concat([high - low, (high - low.shift(1)).abs(), (low - high.shift(1)).abs()], axis=1).max(axis=1)
    df["atr"] = tr.ewm(alpha=1.0/atr_len, adjust=False).mean()  # Wilder RMA
    # EMA50 per regime filter Pine-faithful
    df["ema50"] = close.ewm(span=50, adjust=False).mean()
    # ADX semplificato (DI+ - DI- smoothed) per forza trend
    up_move = high.diff()
    down_move = -low.diff()
    plus_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0.0)
    minus_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0.0)
    tr_safe = tr.replace(0, np.nan)
    plus_di = 100 * pd.Series(plus_dm, index=df.index).ewm(alpha=1.0/14, adjust=False).mean() / tr_safe
    minus_di = 100 * pd.Series(minus_dm, index=df.index).ewm(alpha=1.0/14, adjust=False).mean() / tr_safe
    dx = 100 * (plus_di - minus_di).abs() / (plus_di + minus_di).replace(0, np.nan)
    df["adx"] = dx.ewm(alpha=1.0/14, adjust=False).mean()
    # CD (candle direction)
    sp = np.where(close > df["open"].values, 1, np.where(close < df["open"].values, -1, 0))
    sv = np.where(vol > np.roll(vol.values, 1), 1, np.where(vol < np.roll(vol.values, 1), -1, 0))
    sr = np.where(df["roc"].values > 0, 1, np.where(df["roc"].values < 0, -1, 0))
    cd_idx = np.full(n, -1, dtype=np.int8)
    valid = (sp != 0) & (sv != 0) & (sr != 0) & ~np.isnan(df["roc"].values) & ~np.isnan(df["bb_plus"].values)
    m1 = valid & (sp == 1) & (sv == 1); cd_idx[m1 & (sr > 0)] = 0; cd_idx[m1 & (sr < 0)] = 1
    m2 = valid & (sp == -1) & (sv == -1); cd_idx[m2 & (sr > 0)] = 2; cd_idx[m2 & (sr < 0)] = 3
    m3 = valid & (sp == 1) & (sv == -1); cd_idx[m3 & (sr > 0)] = 4; cd_idx[m3 & (sr < 0)] = 5
    m4 = valid & (sp == -1) & (sv == 1); cd_idx[m4 & (sr > 0)] = 6; cd_idx[m4 & (sr < 0)] = 7
    df["cd_idx"] = cd_idx
    return df


def detect_signal(df, mask_str):
    if len(df) < 100:
        return None, None, None, None
    # PINE-FAITHFUL: usa SEMPRE la candela CHIUSA [-2], MAI [-1] (candela in formazione)
    last = df.iloc[-2]
    if pd.isna(last["bb_plus"]) or pd.isna(last["roc"]) or pd.isna(last["atr"]):
        return None, None, None, None
    mask_padded = mask_str.zfill(8)
    cd_active = [int(c) for c in mask_padded]
    cd = int(last["cd_idx"])
    close = float(last["close"])
    atr = float(last["atr"])
    ts = last["timestamp"]
    if cd < 0 or not cd_active[cd]:
        return None, close, atr, ts
    if close > float(last["bb_plus"]) and last["roc"] > 0:
        return "LONG", close, atr, ts
    if close < float(last["bb_minus"]) and last["roc"] < 0:
        return "SHORT", close, atr, ts
    return None, close, atr, ts


def check_regime_filter(df, side):
    """Pine-faithful regime filter: LONG solo se close[-2] > EMA50[-2] E ADX>20.
    SHORT solo se close[-2] < EMA50[-2] E ADX>20. ADX<20 → SKIP.
    Ritorna (True, "ok") o (False, "motivo")."""
    if len(df) < 60:
        return False, "regime: dati insufficienti"
    last = df.iloc[-2]
    if pd.isna(last["ema50"]) or pd.isna(last["adx"]):
        return False, "regime: EMA50/ADX NaN"
    close = float(last["close"])
    ema50 = float(last["ema50"])
    adx = float(last["adx"])
    if adx < 20:
        return False, f"regime: ADX={adx:.1f}<20 (no trend)"
    if side == "LONG" and close <= ema50:
        return False, f"regime: LONG vs EMA50 bearish (close={close:.4f} <= EMA50={ema50:.4f})"
    if side == "SHORT" and close >= ema50:
        return False, f"regime: SHORT vs EMA50 bullish (close={close:.4f} >= EMA50={ema50:.4f})"
    return True, f"regime OK (ADX={adx:.1f}, EMA50={'above' if close>ema50 else 'below'})"


# ============== ENTRY / TP / SL ==============
def place_entry_with_tpsl(client, setup, side, entry_price, atr, dry_run=True, size_usdt=None,
                          original_entry=None):
    """Pine-faithful Charter ORIGINALI Mattia (ripristinati 12/07 post-bug analisi):
      - TP1: 50% size a +3%  (limit reduceOnly)
      - TP2: 50% size a +5%  (limit reduceOnly)
      - SL:  ATR 2x clampato a -3% max  (/v5/position/trading-stop nativo)
      - LEVA: target 3x (bypass se gia' a 3x)
      - Entry anchor: se original_entry e' passato, TP calcolati su QUELLO, non su entry_price
        (gestione casi di media-up o di apertura parziale pre-esistente)

    Architettura Bybit V5:
      - 1 entry market (qty piena, no frazionamento)
      - 2 reduceOnly limit order: floor(qty/2) a +3% e (qty - floor(qty/2)) a +5%
      - 1 SL via /v5/position/trading-stop (mark price trigger, nativo exchange)
      - NIENTE TP nativo sulla posizione (per non creare conflitto con i 2 limit)
      - Cleanup pre-entry: cancella eventuali reduceOnly pendenti vecchi (anti-conflict)

    size_usdt: se None usa setup['margin_usdt'], altrimenti usa MIL dynamic size.
    original_entry: se passato, usa questo come base per calcolare TP (anti-avg-drift).
    """
    if size_usdt is None:
        size_usdt = setup["margin_usdt"]
    # Base per calcolo TP: se Mattia ha gia' aperto manualmente e c'e' un entry originale,
    # usa quello. Altrimenti usa l'entry corrente.
    tp_base = float(original_entry) if original_entry else float(entry_price)
    if side == "LONG":
        tp1 = tp_base * (1 + setup["tp1_pct"])     # +3% dal PRIMO entry
        tp2 = tp_base * (1 + setup["tp2_pct"])     # +5% dal PRIMO entry
        sl_pct = max(-setup["sl_atr_mult"] * atr / entry_price, setup["sl_clamp_min"])
        sl_pct = min(sl_pct, setup["sl_clamp_max"])
        sl = entry_price * (1 + sl_pct)
    else:
        tp1 = tp_base * (1 - setup["tp1_pct"])     # SHORT: -3% dal PRIMO entry
        tp2 = tp_base * (1 - setup["tp2_pct"])     # SHORT: -5% dal PRIMO entry
        sl_pct = min(setup["sl_atr_mult"] * atr / entry_price, -setup["sl_clamp_min"])
        sl_pct = max(sl_pct, -setup["sl_clamp_max"])
        sl = entry_price * (1 + sl_pct)
    qty = (size_usdt * setup["leverage"]) / entry_price
    qty = client.round_qty(setup["symbol_bybit"], qty)
    # Split 50/50: se qty dispari, TP1 prende floor, TP2 prende il resto
    step = client.get_qty_step(setup["symbol_bybit"])
    qty_tp1 = float(int(qty / 2 / step) * step) if step >= 1 else round((qty / 2) - ((qty / 2) % step), len(str(step).rstrip('0').split('.')[-1]) if '.' in str(step) else 0)
    qty_tp2 = qty - qty_tp1
    if qty_tp1 <= 0 or qty_tp2 <= 0:
        # Safety: se la divisione perde troppo (qty=1, step=1), fallback su TP singolo a TP1
        qty_tp1 = qty
        qty_tp2 = 0.0
    if dry_run:
        anchor_note = f" (anchor originale={original_entry})" if original_entry else ""
        print(f"  [DRY-RUN] ENTRY {setup['name']} {side} qty={qty} @ {entry_price} (size={size_usdt}$ lev={setup['leverage']}x){anchor_note}")
        print(f"             Charter ORIGINALI 50/50:")
        print(f"               TP1 +{setup['tp1_pct']*100:.1f}% = {tp1:.4f} reduceOnly qty={qty_tp1}")
        print(f"               TP2 +{setup['tp2_pct']*100:.1f}% = {tp2:.4f} reduceOnly qty={qty_tp2}")
        print(f"               SL  ATR 2x clamp {sl_pct*100:.2f}% = {sl:.4f} (nativo Bybit)")
        return {"entry": entry_price, "tp1": tp1, "tp2": tp2, "sl": sl, "qty": qty,
                "qty_tp1": qty_tp1, "qty_tp2": qty_tp2, "side": side,
                "original_entry": tp_base}
    try:
        # BUG_004 fix: set_leverage() PRIMA di create_market_order.
        try:
            lev_resp = client.set_leverage(setup["symbol_bybit"], setup["leverage"], side)
            print(f"  [LIVE] set_leverage {setup['symbol_bybit']} {setup['leverage']}x OK (bypassed={lev_resp.get('bypassed', False)})")
        except Exception as lev_err:
            print(f"  [ERR] set_leverage {setup['symbol_bybit']} {setup['leverage']}x FALLITO: {lev_err}")
            return None

        # BUG_005 FIX 3: cleanup automatico reduceOnly pre-entry (evita conflitti con ordini vecchi)
        cancelled = client.cancel_reduce_orders(setup["symbol_bybit"])
        if cancelled > 0:
            print(f"  [CLEANUP] {cancelled} ordini reduceOnly vecchi cancellati per {setup['symbol_bybit']}")

        # 1) Entry market (qty piena)
        o = client.create_market_order(setup["symbol_bybit"], side.lower(), qty)
        order_id = o.get("result", {}).get("orderId", "N/A")
        print(f"  [LIVE] Entry {side} qty={qty} @ {entry_price} :: id={order_id}")

        # 2) Charter ORIGINALI 50/50: 2 limit reduceOnly
        side_close = "sell" if side == "LONG" else "buy"
        o_tp1 = client.create_limit_order(setup["symbol_bybit"], side_close, qty_tp1, tp1, reduce_only=True)
        tp1_id = o_tp1.get("result", {}).get("orderId", "N/A")
        print(f"  [LIVE] TP1 Charter: {side_close} {qty_tp1} @ {tp1:.4f} (+{setup['tp1_pct']*100:.1f}%) reduceOnly :: id={tp1_id}")
        if qty_tp2 > 0:
            o_tp2 = client.create_limit_order(setup["symbol_bybit"], side_close, qty_tp2, tp2, reduce_only=True)
            tp2_id = o_tp2.get("result", {}).get("orderId", "N/A")
            print(f"  [LIVE] TP2 Charter: {side_close} {qty_tp2} @ {tp2:.4f} (+{setup['tp2_pct']*100:.1f}%) reduceOnly :: id={tp2_id}")
        else:
            tp2_id = None
            print(f"  [LIVE] TP2 saltato: qty totale={qty} non divisibile, solo TP1 riduce 100%")

        # 3) SL NATIVO Bybit V5 (mark price trigger) — niente TP nativo per non
        # creare conflitto coi 2 reduceOnly limit.
        client.set_trading_stop(setup["symbol_bybit"], sl_price=sl)
        print(f"  [LIVE] SL Charter: {sl:.4f} ({sl_pct*100:.2f}%) [ATR 2x clamp -3% max, nativo Bybit]")

        return {"entry": entry_price, "tp1": tp1, "tp2": tp2, "sl": sl, "qty": qty,
                "qty_tp1": qty_tp1, "qty_tp2": qty_tp2, "side": side,
                "order_id": order_id, "tp1_id": tp1_id, "tp2_id": tp2_id,
                "original_entry": tp_base}
    except Exception as e:
        print(f"  [ERR] place_entry: {e}")
        return None


# ============== PROCESS SINGLE SETUP ==============
def process_setup(setup, client, dry_run):
    print(f"\n--- {setup['name']} ({setup['symbol_bybit']} {setup['tf']} mask={setup['mask']}) ---")
    try:
        ohlcv = client.fetch_ohlcv(setup["symbol_bybit"], setup["interval"], 200)
    except Exception as e:
        print(f"  [ERR] fetch_ohlcv: {e}")
        return None
    df = pd.DataFrame(ohlcv, columns=["timestamp", "open", "high", "low", "close", "volume"])
    df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
    df = compute_indicators(df, setup["bb_length"], setup["bb_mult"], setup["roc_length"], setup["atr_length"])
    sig, close, atr, ts = detect_signal(df, setup["mask"])
    print(f"  Last close={close} | ATR={atr} | Signal={sig}")

    # CHECK 1: candela già processata (anti-doppia entrata stessa candela)
    state = load_state()
    last_bar = state.get("last_signal_bar", {}).get(setup["name"])
    if last_bar and str(last_bar) == str(ts):
        print(f"  [SKIP-DUP] {setup['name']} candela {ts} già processata")
        return None

    # CHECK 2: posizione già aperta su Bybit — fonte primaria fetch_positions
    pos = None
    if not dry_run:
        try:
            positions = client.fetch_positions(setup["symbol_bybit"])
            for p in positions:
                amt = float(p.get("size", 0) or 0)
                if amt > 0:
                    side_pos = "LONG" if p.get("side") == "Buy" else "SHORT"
                    pos = {"side": side_pos, "size": amt,
                           "entry_price": float(p.get("avgPrice", 0) or 0),
                           "created_time": p.get("createdTime", "")}
                    break
        except Exception as e:
            print(f"  [WARN] fetch_positions: {e}")
    # Fallback state se Bybit fallisce
    if not pos:
        state_pos = state.get("open_positions", {}).get(setup["name"])
        if state_pos and float(state_pos.get("size", 0)) > 0:
            pos = state_pos
            print(f"  [STATE-OPEN] {setup['name']} {pos['side']} {pos['size']} @ {pos['entry_price']}")

    # ============================================================
    # ANTI-DOUBLE-BUY HARD RULE (ripristinata 12/07 post-bug analisi)
    # ============================================================
    # Se ESISTE GIA' una posizione aperta su Bybit per questo simbolo:
    #   1) NON aprire una nuova entry (niente media-up, niente aggiunte)
    #   2) NON toccare i TP/SL esistenti (Mattia potrebbe averli messi a mano)
    #   3) Il bot si limita a MONITORARE la posizione (read-only)
    # Questo evita naked position da ristrutturazione e entry drift.
    if pos:
        print(f"  [OPEN-EXISTING] {pos['side']} size={pos['size']} entry={pos['entry_price']}")
        print(f"  [ANTI-DOUBLE-BUY] posizione gia' aperta, bot in MONITOR mode (no entry, no TP/SL touch)")
        # Update state con info posizione corrente (mantieni original_entry se gia' noto)
        state.setdefault("open_positions", {})[setup["name"]] = {
            "side": pos["side"], "size": pos["size"],
            "entry_price": pos["entry_price"],
            "original_entry": state.get("open_positions", {}).get(setup["name"], {}).get("original_entry", pos["entry_price"]),
            "source": "bybit",
            "last_seen_ts": datetime.now(timezone.utc).isoformat(),
        }
        return {"name": setup["name"], "position": state["open_positions"][setup["name"]], "signal": sig}

    if sig and not pos:
        # CHECK 3: regime filter Pine-faithful (EMA50[-2] + ADX>20)
        regime_ok, regime_msg = check_regime_filter(df, sig)
        if not regime_ok:
            print(f"  [SKIP-REGIME] {setup['name']} {regime_msg}")
            return None
        print(f"  [REGIME] {regime_msg}")

        # CHECK 4: NO paralleli stesso simbolo (Charter compliance) — doppio check
        # anche se pos e' None, puo' esserci una posizione in state ancora non flushata.
        if not dry_run:
            try:
                all_pos = client.fetch_positions()
                same_symbol_count = sum(1 for p in all_pos
                                       if p.get("symbol") == setup["symbol_bybit"]
                                       and float(p.get("size", 0) or 0) > 0)
                if same_symbol_count > 0:
                    print(f"  [SKIP-SAME-SYM] {setup['symbol_bybit']} ha gia' {same_symbol_count} posizioni aperte su Bybit")
                    return None
            except Exception as e:
                print(f"  [WARN] count same-sym: {e}")

        # CHECK 5: Rivalidazione regime PRE-EXEC (paletto 10/07) — Pine-faithful compliance
        regime_ok2, regime_msg2 = check_regime_filter(df, sig)
        if not regime_ok2:
            print(f"  [SKIP-PREEXEC] {setup['name']} regime cambiato pre-exec: {regime_msg2}")
            return None

        # Charter primari: usa size piena setup.margin_usdt, ignora MIL
        if setup.get("is_primary", False):
            mil_size = setup["margin_usdt"]
            mil_verdict = "PRIMARY (size piena Charter)"
            print(f"  [PRIMARY-CHARTER] {setup['name']} size piena {mil_size} USDT (ignora MIL)")
        else:
            mil_size, mil_verdict = get_setup_size(setup["name"])
            if "SKIP" in mil_verdict.upper() and not dry_run:
                print(f"  [SKIP-MIL] {setup['name']} verdict={mil_verdict}")
                return None
            if mil_size <= 0:
                print(f"  [SKIP-MIL] {setup['name']} size=0 (verdict={mil_verdict})")
                return None

        # AI PREDICTOR ENHANCER (solo per setup secondari; primari Charter NON ridotti)
        if not setup.get("is_primary", False):
            try:
                import sys as _sys
                mi_path = Path(r"C:\Users\Mattia\.mavis\sessions\mvs_7410904bba534d318a61faac8876cd4c\workspace\market_intelligence")
                if str(mi_path) not in _sys.path:
                    _sys.path.insert(0, str(mi_path))
                from signal_enhancer import enhance_signal
                enh = enhance_signal(sig, setup["symbol_bybit"], setup["tf"], base_size_usdt=mil_size)
                action = enh.get("action", "HOLD")
                if action == "SKIP":
                    print(f"  [SKIP-AI] {setup['name']} {enh.get('reason','')}")
                    return None
                if action == "CAUTELA":
                    mil_size = mil_size * enh.get("size_pct", 50) / 100.0
                    print(f"  [CAUTELA-AI] {setup['name']} size {mil_size:.0f} USDT — {enh.get('reason','')}")
                elif action == "GO":
                    print(f"  [GO-AI] {setup['name']} {enh.get('reason','')}")
            except Exception as e:
                print(f"  [WARN] AI enhancer fail: {e}")

        if not dry_run:
            try:
                all_pos = client.fetch_positions()
                n_open = sum(1 for p in all_pos if float(p.get("size", 0) or 0) > 0)
                if n_open >= MAX_OPEN_TRADES_PORTFOLIO:
                    print(f"  [SKIP] MAX_OPEN_TRADES_PORTFOLIO={n_open}")
                    return None
            except Exception as e:
                print(f"  [WARN] count open: {e}")

        # ============================================================
        # ENTRY ANCHOR: se per qualche ragione c'e' un original_entry in state
        # (es. Mattia ha aperto manualmente prima del loop), usalo come base TP.
        # Altrimenti, close stesso (entry BOT = primo).
        # ============================================================
        original_entry = state.get("open_positions", {}).get(setup["name"], {}).get("original_entry", close)

        res = place_entry_with_tpsl(client, setup, sig, close, atr,
                                    dry_run=dry_run, size_usdt=mil_size,
                                    original_entry=original_entry)
        if res:
            # Salva in state con original_entry = entry_price (entry BOT = primo)
            state.setdefault("open_positions", {})[setup["name"]] = {
                "side": res["side"], "size": res["qty"],
                "entry_price": res["entry"],
                "original_entry": res["original_entry"],
                "tp1": res["tp1"], "tp2": res.get("tp2"), "sl": res["sl"],
                "qty_tp1": res.get("qty_tp1"), "qty_tp2": res.get("qty_tp2"),
                "order_id": res.get("order_id"),
                "tp1_id": res.get("tp1_id"), "tp2_id": res.get("tp2_id"),
                "source": "bot",
                "opened_ts": datetime.now(timezone.utc).isoformat(),
            }
            return {"name": setup["name"], "signal": sig, "entry": res,
                    "mil_size": mil_size, "mil_verdict": mil_verdict,
                    "position": state["open_positions"][setup["name"]]}
    else:
        print(f"  [IDLE]")
    return None


# ============== MAIN ==============
def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--once", action="store_true")
    ap.add_argument("--confirm-live", action="store_true")
    ap.add_argument("--status", action="store_true")
    ap.add_argument("--setup", type=str)
    args = ap.parse_args()

    dry_run = not args.confirm_live
    if not dry_run:
        print("⚠️  MODALITÀ LIVE Bybit demo attiva.")
    else:
        print("[DRY-RUN] Nessun ordine sarà inviato. Usa --confirm-live per LIVE.")

    try:
        client = BybitDemoClient()
    except Exception as e:
        print(f"[ERR] Connessione Bybit demo fallita: {e}")
        return

    if args.status:
        if dry_run:
            print("[DRY-RUN] Stato posizioni: skip (no LIVE)")
            return
        for s in SETUPS:
            positions = client.fetch_positions(s["symbol_bybit"])
            print(f"  {s['name']} ({s['symbol_bybit']}): {positions if positions else 'NESSUNA'}")
        all_pos = client.fetch_positions()
        n = sum(1 for p in all_pos if float(p.get("size", 0) or 0) > 0)
        print(f"\nTotale trade aperti: {n}/{MAX_OPEN_TRADES_PORTFOLIO}")
        return

    state = load_state()
    print(f"State loaded: last_check_ts={state.get('last_check_ts')}")

    setups_to_process = SETUPS
    if args.setup:
        setups_to_process = [s for s in SETUPS if s["name"] == args.setup]
        if not setups_to_process:
            print(f"[ERR] setup '{args.setup}' non trovato.")
            return

    results = []
    for setup in setups_to_process:
        r = process_setup(setup, client, dry_run)
        if r:
            results.append(r)

    state["last_check_ts"] = datetime.now(timezone.utc).isoformat()
    # SYNC STATE: process_setup ora aggiorna state['open_positions'] durante l'esecuzione
    # (sia per posizioni esistenti MONITOR che per nuove entry BOT).
    # Ricarichiamo per assicurarci di non sovrascrivere aggiornamenti.
    fresh_state = load_state()
    state["open_positions"] = fresh_state.get("open_positions", {})
    # Aggiorna last_signal_bar per anti-doppia candela
    for setup in setups_to_process:
        # Rileggi l'ultima candela processata per ogni setup
        try:
            ohlcv = client.fetch_ohlcv(setup["symbol_bybit"], setup["interval"], 5)
            if ohlcv:
                last_ts = pd.to_datetime(ohlcv[-1][0], unit="ms", utc=True)
                state.setdefault("last_signal_bar", {})[setup["name"]] = str(last_ts)
        except Exception:
            pass
    save_state(state)

    print(f"\n[OK] Loop completato. DRY-RUN={dry_run} | Setups processati: {len(setups_to_process)}")


if __name__ == "__main__":
    main()
