"""
SL/TP Engine - REGOLE UNIFICATE (Rettangolo + VPTR3)
======================================================
Per ogni posizione aperta su Bybit Demo, calcola SL/TP usando la strategia
corrispondente (Rettangolo o VPTR3) e li applica via webhook (con fallback
diretto Bybit se il webhook e' giu').

MAPPING STRATEGIA (rettangolo_assets.csv colonna 'strategy'):
  - 'rettangolo': calcola SL/TP con compute_signal() del Quadrato + trailing
  - 'vptr3': SL hard -3%, TP parziale +5% (50% size), TP finale +3% addizionale

REGOLE RETTANGOLO (compute_signal):
  - Range = high/low candela daily precedente
  - SL iniziale = estremo Hammer +/- 1% buffer
  - TP = MID range (fisso)
  - Trailing SL: si attiva SOLO DOPO +1.5% dal entry (LONG) o -1.5% (SHORT)

REGOLE VPTR3 (estese, la strategia Pine non le ha):
  - SL hard: se PnL <= -3% e strategia non ha chiuso, chiude tutta la posizione
  - TP parziale: se PnL >= +5% e strategia non ha chiuso, chiude 50% della size
  - TP finale: dopo TP1, aspetta +3% addizionale dal prezzo TP1, chiude resto
  - Trailing SL: simile al rettangolo, si attiva a +/-1.5% dal entry
"""
import os
import sys
import time
import json
import sqlite3
import requests
from datetime import datetime, timezone
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))

from bybit_demo_client import BybitDemoClient
from rettangolo_strategy import compute_signal, is_doji, has_hammer_bullish, has_hammer_bearish
from rettangolo_config import load_assets

# === CONFIG ===
WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "http://127.0.0.1:5580")
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "")
WEBHOOK_SLTP_PATH = "/webhook/sltp"

# Rettangolo
RETTANGOLO_SL_BUFFER_PCT = 0.01  # 1% sopra/sotto Hammer
# === RETTANGOLO TRAILING STEP LOCK-IN (Mattia 06/08 16:08 Scenario 3) ===
# Obiettivo: ogni trade deve ottenere almeno 6.65 USDT = 1.33% del margin (500).
# Con leva 3x e nozionale 1500, il trigger in termini di movimento di prezzo
# e' 6.65 / 1500 = 0.443% (in profit_pct del codice).
# Step lock-in progressivo:
#   PnL >= 0.443% (6.65 USDT)  → SL = entry (breakeven)
#   PnL >= 1.0%   (15 USDT)    → SL = entry * 1.005 (lock-in +0.5%)
#   PnL >= 2.0%   (30 USDT)    → SL = entry * 1.010 (lock-in +1.0%)
#   PnL >= 3.0%   (45 USDT)    → SL = entry * 1.020 (lock-in +2.0%)
#   PnL >= 5.0%   (75 USDT)    → SL = entry * 1.030 (lock-in +3.0%)
# High water mark: lo SL trailing non scende mai sotto il massimo raggiunto.
# Step minimo 0.2% per evitare oscillazioni continue (anti-oscillazione).
RETTANGOLO_TRAILING_TRIGGER_PCT = 0.015  # Mattia 14/08: trigger 1.5% (era 0.443% ≈ 6.65 USDT, ora uniforme VPTR3)
RETTANGOLO_TRAILING_STEP_PCT = [  # [(profit_pct_min, lock_in_pct)] ascending per profit
    (0.00443, 0.000),  # breakeven
    (0.010,   0.005),  # +0.5% lock-in
    (0.020,   0.010),  # +1.0% lock-in
    (0.030,   0.020),  # +2.0% lock-in
    (0.050,   0.030),  # +3.0% lock-in
]
RETTANGOLO_TRAILING_STEP_MIN_PCT = 0.002  # 0.2% step minimo miglioramento
# === P006 Charter: SL -3% MAX cap su RETTANGOLO (simmetrico a VPTR3) ===
# Mattia 14/08: strategie ESCLUSE dal trailing automatico (regola del 09/08 non si applica)
EXCLUDED_STRATEGIES = {"rsi_swing", "rsi_swing_breakout", "rettangolo_simple", "sqw", "supertrend_bosw", "adx"}
# Strategie su cui APPLICO la regola trailing:
# - vptr3: logica VPTR3-style con HWM e pre-trailing 1%
# - rettangolo: logica RETTANGOLO-style con lista 5 step e pre-trailing 1%
# - ma_trailing: logica VPTR3-style con HWM e pre-trailing 1%

RETTANGOLO_SL_HARD_PCT = 0.04  # V2: +33% (era 0.03)  # 3% dal entry, simmetrico per LONG/SHORT

# VPTR3
VPTR3_SL_HARD_PCT = -0.04  # V2: +33% (era -0.03)  # -3% PnL = SL hard
VPTR3_TP_PARTIAL_PCT = 0.05  # +5% PnL = TP parziale 50%
VPTR3_TP_FINAL_ADD_PCT = 0.03  # +3% addizionale al TP1 = TP finale
VPTR3_TRAILING_TRIGGER_PCT = 0.015  # 1.5% dal entry per attivare trailing

LOOP_INTERVAL_SEC = 300  # 5 min
INTRADAY_LIMIT = 200
DAILY_LIMIT = 5
ALIGN_TOLERANCE_PCT = 0.005  # 0.5%

# Storage per stato TP parziale VPTR3 (TP1 gia' eseguito, ecc.)
STATE_FILE = Path(r"/opt/charter-live/live_deploy_v2/webhook_listener/logs/sltp_state.json")
LOG_FILE = r"/opt/charter-live/live_deploy_v2/webhook_listener/logs/sltp_engine.log"


def log(msg):
    ts = datetime.now(timezone.utc).astimezone().isoformat()
    line = f"[{ts}] {msg}"
    print(line, flush=True)
    try:
        os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
        with open(LOG_FILE, "a", encoding="utf-8") as f:
            f.write(line + "\n")
    except Exception:
        pass


def load_state() -> dict:
    """Carica stato persistente (es. VPTR3 TP1 gia' eseguito per symbol)."""
    if not STATE_FILE.exists():
        return {}
    try:
        with open(STATE_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    except Exception:
        return {}


def save_state(state: dict) -> None:
    """Salva stato persistente."""
    try:
        os.makedirs(STATE_FILE.parent, exist_ok=True)
        with open(STATE_FILE, "w", encoding="utf-8") as f:
            json.dump(state, f, indent=2, default=str)
    except Exception as e:
        log(f"WARN save_state: {e}")


def fetch_klines_dict(client, symbol, interval, limit):
    raw = client.fetch_ohlcv(symbol, interval, limit)
    out = []
    for row in raw:
        ts_ms, o, h, l, c = int(row[0]), row[1], row[2], row[3], row[4]
        out.append({
            "ts": ts_ms,
            "date": datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc).astimezone().isoformat(),
            "open": o, "high": h, "low": l, "close": c,
        })
    return out


def get_position_owner_strategy(symbol: str, position_side: str = "") -> str:
    """Legge l'ownership certificata dal webhook per la posizione aperta."""
    db_path = Path(__file__).parent / "logs" / "webhook_queue.db"
    if not db_path.exists():
        return ""
    try:
        with sqlite3.connect(str(db_path), timeout=2) as conn:
            row = conn.execute(
                "SELECT strategy,side FROM position_ownership WHERE symbol=?",
                (symbol.upper(),),
            ).fetchone()
    except Exception as exc:
        log(f"  {symbol}: ownership DB non leggibile: {exc}")
        return ""
    if not row:
        return ""
    strategy = str(row[0] or "").strip().lower()
    owner_side = str(row[1] or "").strip().lower()
    actual_side = str(position_side or "").strip().lower()
    if actual_side in ("buy", "long"):
        actual_side = "long"
    elif actual_side in ("sell", "short"):
        actual_side = "short"
    if actual_side and owner_side and actual_side != owner_side:
        log(f"  {symbol}: OWNERSHIP SIDE MISMATCH owner={owner_side} bybit={actual_side} -> FAIL-SAFE")
        return "unknown"
    return strategy or "unknown"


def get_strategy_for_symbol(symbol: str, position_side: str = "") -> str:
    """Ownership esatta prima; CSV solo se il simbolo appartiene a una strategia."""
    symbol_u = symbol.upper()
    owner = get_position_owner_strategy(symbol_u, position_side)
    if owner:
        return owner

    candidates = set()
    for asset in load_vptr3_assets():
        if asset["symbol"] == symbol_u and asset["enabled"]:
            strategy = asset["strategy"]
            if strategy == "vptr_v3":
                strategy = "vptr3"
            candidates.add(strategy)
    for asset in load_assets():
        if asset["symbol"] == symbol_u and asset["enabled"]:
            candidates.add(asset["strategy"])
    for asset in load_rettangolo_simple_assets():
        if asset["symbol"] == symbol_u and asset["enabled"]:
            candidates.add(asset["strategy"])
    for asset in load_ma_trailing_assets():
        if asset["symbol"] == symbol_u and asset["enabled"]:
            candidates.add(asset["strategy"])
    for asset in load_supertrend_bosw_assets():
        if asset["symbol"] == symbol_u and asset["enabled"]:
            candidates.add(asset["strategy"])
    if len(candidates) == 1:
        return next(iter(candidates))
    if len(candidates) > 1:
        log(f"  {symbol_u}: SHARED SYMBOL senza ownership: {sorted(candidates)} -> FAIL-SAFE SKIP")
    return "unknown"


def load_vptr3_assets() -> list:
    """Carica VPTR3 assets da VPTR_V3_ASSETS.csv (separato da rettangolo_assets.csv).
    FIX 2026-07-18 (Mattia BREAKING POINT): strategie separate, NO contaminazione.
    Ritorna lista di dict {symbol, timeframe, tf_min, enabled, strategy, notes}.
    Se file non esiste o vuoto â†’ ritorna []."""
    out = []
    csv_path = Path(__file__).parent / "VPTR_V3_ASSETS.csv"
    if not csv_path.exists():
        return []
    try:
        import csv as csvmod
        with open(csv_path, "r", encoding="utf-8") as f:
            reader = csvmod.DictReader(f)
            for row in reader:
                if not row.get("symbol"):
                    continue
                tf = row.get("timeframe", "30")
                out.append({
                    "symbol": row["symbol"].strip().upper(),
                    "timeframe": tf,
                    "tf_min": tf.replace("m", "").replace("h", "").replace("H", ""),
                    "enabled": str(row.get("enabled", "true")).lower() == "true",
                    "strategy": row.get("strategy", "vptr3").strip().lower(),
                    "notes": row.get("notes", "").strip(),
                })
    except Exception as e:
        log(f"WARN load_vptr3_assets: {e}")
    return out


def load_strategy_assets(filename: str, default_strategy: str) -> list:
    """Loader generico per CSV strategia. Usato da RETTANGOLO_SIMPLE e MA_TRAILING.
    Ritorna lista di dict {symbol, timeframe, tf_min, enabled, strategy, notes}."""
    out = []
    csv_path = Path(__file__).parent / filename
    if not csv_path.exists():
        return []
    try:
        import csv as csvmod
        with open(csv_path, "r", encoding="utf-8") as f:
            reader = csvmod.DictReader(f)
            for row in reader:
                if not row.get("symbol"):
                    continue
                tf = row.get("timeframe", "30")
                out.append({
                    "symbol": row["symbol"].strip().upper(),
                    "timeframe": tf,
                    "tf_min": tf.replace("m", "").replace("h", "").replace("H", ""),
                    "enabled": str(row.get("enabled", "true")).lower() == "true",
                    "strategy": row.get("strategy", default_strategy).strip().lower(),
                    "notes": row.get("notes", "").strip(),
                })
    except Exception as e:
        log(f"WARN load_strategy_assets({filename}): {e}")
    return out


def load_rettangolo_simple_assets() -> list:
    """Carica assets RETTANGOLO_SIMPLE da RETTANGOLO_SIMPLE_ASSETS.csv.
    Strategia Via TV (Pine manda entry a sltp_engine che fa safety net)."""
    return load_strategy_assets("RETTANGOLO_SIMPLE_ASSETS.csv", "rettangolo_simple")


def load_ma_trailing_assets() -> list:
    """Carica assets MA_TRAILING da MA_TRAILING_ASSETS.csv.
    Strategia Via TV (Pine manda entry a sltp_engine che fa safety net)."""
    return load_strategy_assets("MA_TRAILING_ASSETS.csv", "ma_trailing")


def load_supertrend_bosw_assets() -> list:
    """Carica strategie SUPERTREND_BOSW gestite integralmente da TradingView."""
    return load_strategy_assets("SUPERTREND_BOSW_ASSETS.csv", "supertrend_bosw")


# ============================================================
# RETTANGOLO: compute_signal + trailing stop con soglia 1.5%
# ============================================================

def compute_sltp_rettangolo(client, symbol, side, entry_price, current_price, timeframe, state=None):
    """Calcola SL/TP per posizione RETTANGOLO.
    Ritorna dict {sl_price, tp_price, sl_ref, source, breakeven, trailing_step} o None.

    Trailing STEP LOCK-IN (Mattia 06/08 16:08 Scenario 3):
    - Trigger: profit_pct >= 0.443% (= 6.65 USDT PnL = 1.33% margin)
    - Step 1 (0.443%+): breakeven (entry)
    - Step 2 (1.0%+):  lock-in +0.5% (= entry * 1.005)
    - Step 3 (2.0%+):  lock-in +1.0% (= entry * 1.010)
    - Step 4 (3.0%+):  lock-in +2.0% (= entry * 1.020)
    - Step 5 (5.0%+):  lock-in +3.0% (= entry * 1.030)
    - High water mark: state[symbol]['trailing_sl'] tiene il massimo raggiunto.
    - Step minimo 0.2% per anti-oscillazione.

    Se state e' passato, aggiorna state[symbol]['trailing_sl'] con il nuovo SL.
    """
    try:
        daily = fetch_klines_dict(client, symbol, "D", DAILY_LIMIT)
        if len(daily) < 2:
            return None
        prev_daily = daily[-2]

        intra = fetch_klines_dict(client, symbol, timeframe, INTRADAY_LIMIT)
        if len(intra) < 2:
            return None

        # P010 Charter fix 2026-07-18: escludi candela in formazione (ultima).
        # Pattern Doji/Hammer vanno cercati SOLO su candele CHIUSE.
        intra_closed = intra[:-1] if len(intra) > 1 else intra
        if len(intra_closed) < 2:
            return None
        log(f"compute_sltp_rettangolo {symbol} {side}: scanning {len(intra_closed)} CLOSED candles (excluded last live candle ts={intra[-1].get('time', '?')})")

        # cerca candela Doji/Hammer piu' vicina al prezzo corrente
        best_idx = None
        best_score = -1
        for i in range(1, len(intra_closed)):
            bar = intra_closed[i]
            bar = intra[i]
            if side == "Sell":  # SHORT
                if bar["close"] < bar["open"] and (is_doji(bar) or has_hammer_bearish(bar)):
                    score = -abs(bar["high"] - current_price) / max(current_price, 1e-9)
                else:
                    continue
            else:  # LONG
                if bar["close"] > bar["open"] and (is_doji(bar) or has_hammer_bullish(bar)):
                    score = -abs(bar["low"] - current_price) / max(current_price, 1e-9)
                else:
                    continue
            if score > best_score:
                best_score = score
                best_idx = i

        if best_idx is None:
            return None

        hammer = intra[best_idx]
        rng_top = prev_daily["high"]
        rng_bot = prev_daily["low"]
        rng_mid = (rng_top + rng_bot) / 2

        if side == "Sell":  # SHORT
            sl_ref = hammer["high"]
            sl_initial = sl_ref * (1 + RETTANGOLO_SL_BUFFER_PCT)
        else:  # LONG
            sl_ref = hammer["low"]
            sl_initial = sl_ref * (1 - RETTANGOLO_SL_BUFFER_PCT)

        # SAFETY CHECK SIDE-AWARE (fix 2026-07-18 AEROUSDT Buy bug):
        # Per LONG: SL DEVE essere < entry_price (altrimenti Bybit err 10001).
        # Per SHORT: SL DEVE essere > entry_price.
        # Se la candela hammer e' daily/2H e il suo low/high e' piu' alto/basso dell'entry
        # corrente, lo SL calcolato puo' essere invertito. In quel caso:
        # - LONG: usa prev_daily["low"] * 0.99 (true range floor) come fallback
        # - SHORT: usa prev_daily["high"] * 1.01 (true range ceiling) come fallback
        if side == "Buy" and sl_initial >= entry_price:
            log(f"  {symbol}: SL iniziale {sl_initial} >= entry {entry_price} (LONG), fallback su prev_daily.low*0.99")
            sl_initial = rng_bot * (1 - RETTANGOLO_SL_BUFFER_PCT)
            sl_ref = rng_bot
            # se ANCORA sopra entry (caso patologico: entry sotto al low daily), skip
            if sl_initial >= entry_price:
                log(f"  {symbol}: SL fallback {sl_initial} ancora >= entry {entry_price}, ritorno None (skip)")
                return None
        elif side == "Sell" and sl_initial <= entry_price:
            log(f"  {symbol}: SL iniziale {sl_initial} <= entry {entry_price} (SHORT), fallback su prev_daily.high*1.01")
            sl_initial = rng_top * (1 + RETTANGOLO_SL_BUFFER_PCT)
            sl_ref = rng_top
            if sl_initial <= entry_price:
                log(f"  {symbol}: SL fallback {sl_initial} ancora <= entry {entry_price}, ritorno None (skip)")
                return None

        # === SAFETY CHECK MARK PRICE (fix 2026-08-08 WIFUSDT SELL bug) ===
        # Bybit rifiuta err 10001 se per SELL: SL <= mark price; per BUY: SL >= mark price.
        # Caso WIFUSDT: SL=0.13981 < mark=0.14188 con side=Sell. Anche se SL > entry,
        # se SL < mark Bybit dice 'should greater base_price'. Forza a mark*1.005 (SELL) o mark*0.995 (BUY).
        if side == "Sell" and sl_initial <= current_price:
            log(f"  {symbol}: SAFETY MARK SELL: SL {sl_initial:.6f} <= mark {current_price:.6f}, forzo a mark*1.005")
            sl_initial = current_price * 1.005
            sl_ref = current_price
        elif side == "Buy" and sl_initial >= current_price:
            log(f"  {symbol}: SAFETY MARK BUY: SL {sl_initial:.6f} >= mark {current_price:.6f}, forzo a mark*0.995")
            sl_initial = current_price * 0.995
            sl_ref = current_price

        # === P006 Charter fix 2026-07-19 Mavis: clamp SL a -3% MAX dal entry ===
        # Regola Charter P006 hard: SL MAI meno del -3% dal entry, MAI più del -3%.
        # Per LONG: sl_initial DEVE essere >= entry * (1 - 0.03)  (non più in basso del 3%)
        # Per SHORT: sl_initial DEVE essere <= entry * (1 + 0.03) (non più in alto del 3%)
        # Se il calcolo hammer/Safety produce uno SL oltre il -3% dal entry, CLAMP al cap.
        if side == "Buy":
            p006_floor = entry_price * (1 - 0.03)
            if sl_initial < p006_floor:
                log(f"  {symbol}: P006 CLAMP LONG: SL iniziale {sl_initial:.6f} sotto cap -3% ({p006_floor:.6f}), clampato a -3%")
                sl_initial = p006_floor
                sl_ref = entry_price  # aggiorna ref per logging
        elif side == "Sell":
            p006_ceiling = entry_price * (1 + 0.03)
            if sl_initial > p006_ceiling:
                log(f"  {symbol}: P006 CLAMP SHORT: SL iniziale {sl_initial:.6f} sopra cap +3% ({p006_ceiling:.6f}), clampato a +3%")
                sl_initial = p006_ceiling
                sl_ref = entry_price

        # Calcola distanza dal entry
        if side == "Buy":
            profit_pct = (current_price - entry_price) / entry_price
        else:
            profit_pct = (entry_price - current_price) / entry_price

        # === TRAILING STEP LOCK-IN (Mattia 06/08 16:08 Scenario 3) ===
        # Obiettivo: ogni trade deve ottenere almeno 6.65 USDT PnL (= 1.33% margin).
        # Trigger: profit_pct >= 0.443% (= 6.65 USDT).
        # Step progressivi di lock-in (vedi RETTANGOLO_TRAILING_STEP_PCT).
        # High water mark: state[symbol]['trailing_sl'] tiene il max raggiunto.
        trailing_active = False
        trailing_step = 0  # quale step attivo (0=sotto trigger, 1=BE, 2=+0.5%, ...)
        sl_lockin_target = sl_initial
        # Mattia 14/08: pre-trailing 1% → SL a breakeven (entry)
        if not state.get("pre_trailing_active", False) and profit_pct >= 0.01:
            state["pre_trailing_active"] = True
            state["pre_trailing_sl_price"] = entry_price
        if state.get("pre_trailing_active", False) and not state.get("trailing_active", False):
            if "trailing_sl_price" not in state or state["trailing_sl_price"] < state["pre_trailing_sl_price"]:
                state["trailing_sl_price"] = state["pre_trailing_sl_price"]

        if profit_pct >= RETTANGOLO_TRAILING_TRIGGER_PCT:
            trailing_active = True
            lock_in_pct = 0.0
            for step_threshold, step_lockin in RETTANGOLO_TRAILING_STEP_PCT:
                if profit_pct >= step_threshold:
                    lock_in_pct = step_lockin
                    trailing_step += 1
            if side == "Buy":
                sl_lockin_target = entry_price * (1 + lock_in_pct)
            else:  # SHORT
                sl_lockin_target = entry_price * (1 - lock_in_pct)
            sl = sl_lockin_target

            # High water mark + step minimo (se state passato)
            if state is not None:
                sym_state = state.setdefault(symbol, {})
                prev_sl = sym_state.get("trailing_sl")
                if prev_sl is not None:
                    if side == "Buy":
                        # SL LONG: deve salire per migliorare
                        if sl_lockin_target > prev_sl:
                            # Migliora? Aggiorna solo se di almeno 0.2%
                            if (sl_lockin_target - prev_sl) / prev_sl >= RETTANGOLO_TRAILING_STEP_MIN_PCT:
                                sym_state["trailing_sl"] = sl_lockin_target
                                sl = sl_lockin_target
                            else:
                                # Miglioramento < 0.2%, mantieni precedente (anti-oscillazione)
                                sl = prev_sl
                        else:
                            # Non migliora, mantieni high water mark
                            sl = prev_sl
                    else:  # SHORT
                        # SL SHORT: deve scendere per migliorare
                        if sl_lockin_target < prev_sl:
                            if (prev_sl - sl_lockin_target) / prev_sl >= RETTANGOLO_TRAILING_STEP_MIN_PCT:
                                sym_state["trailing_sl"] = sl_lockin_target
                                sl = sl_lockin_target
                            else:
                                sl = prev_sl
                        else:
                            sl = prev_sl
                else:
                    # Prima volta: inizializza high water mark
                    sym_state["trailing_sl"] = sl_lockin_target
        else:
            # Sotto trigger: SL iniziale, ma NON resettare high water mark
            # (se il prezzo riscende sotto trigger, NON deve cancellare il lock-in gia' raggiunto)
            sl = sl_initial
            if state is not None and symbol in state:
                # Mantieni eventuale high water mark precedente (non aggiornare, non cancellare)
                prev_sl = state[symbol].get("trailing_sl")
                if prev_sl is not None:
                    if side == "Buy" and prev_sl > sl_initial:
                        # High water mark ancora utile: usalo come floor
                        sl = max(sl_initial, prev_sl)
                    elif side == "Sell" and prev_sl < sl_initial:
                        sl = min(sl_initial, prev_sl)

        # === SAFETY CHECK MARK FINALE post-HWM (fix 2026-08-09 UNIUSDT SELL bug) ===
        # L'High Water Mark (HWM) del trailing step può mantenere un vecchio trailing_sl
        # "peggiore" del safety MARK iniziale (per SELL: sl < mark). Forza a mark*1.005
        # SOLO se sl < mark per SELL (era peggio, va corretto). Per BUY: HWM alza sl
        # verso l'alto, NON tocchiamo (sarebbe peggio).
        if side == "Sell" and sl < current_price:
            log(f"  {symbol}: SAFETY MARK SELL post-HWM: sl {sl:.6f} < mark {current_price:.6f}, forzo a mark*1.005")
            sl = current_price * 1.005
            sl_ref = current_price

        # === SAFETY CHECK TP vs ENTRY (fix 2026-08-09 WIFUSDT BUY bug) ===
        # Bybit rifiuta err 10001 se per BUY: TP <= entry_price, per SELL: TP >= entry_price.
        # Caso WIFUSDT: TP=rng_mid=0.14068 < entry=0.14214 (BUY) → err 10001.
        # Forza TP a entry * 1.005 (BUY) o entry * 0.995 (SELL) come minimo take profit valido.
        tp_price = rng_mid
        if side == "Buy" and tp_price <= entry_price:
            log(f"  {symbol}: SAFETY TP BUY: TP {tp_price:.6f} <= entry {entry_price:.6f}, forzo a entry*1.005")
            tp_price = entry_price * 1.005
        elif side == "Sell" and tp_price >= entry_price:
            log(f"  {symbol}: SAFETY TP SELL: TP {tp_price:.6f} >= entry {entry_price:.6f}, forzo a entry*0.995")
            tp_price = entry_price * 0.995

        return {
            "sl_price": round(sl, 6),
            "tp_price": round(tp_price, 6),
            "sl_ref": round(sl_ref, 6),
            "source": f"compute_signal(hammer@idx={best_idx}, tf={timeframe})",
            "trailing_active": trailing_active,
            "trailing_step": trailing_step,
            "profit_pct": round(profit_pct * 100, 3),
            "rng_top": rng_top, "rng_bot": rng_bot, "rng_mid": rng_mid,
            "_side": side,
            "_entry_price": entry_price,
        }
    except Exception as e:
        log(f"  {symbol}: compute_sltp_rettangolo exception: {e}")
        return None


# ============================================================
# VPTR3: SL hard -3%, TP parziale +5% (50% size), TP finale +3% addizionale
# ============================================================

def compute_pnl_pct(side, entry_price, current_price):
    if side == "Buy":
        return (current_price - entry_price) / entry_price
    else:
        return (entry_price - current_price) / entry_price


def process_vptr3_position(client, state, symbol, side, entry_price, current_price, size):
    """Processa posizione VPTR3.
    Logica:
    - Se PnL <= -3% (VPTR3_SL_HARD_PCT) e strategia Pine non ha chiuso -> SL hard, chiude TUTTO
    - Se PnL >= +5% (VPTR3_TP_PARTIAL_PCT) e TP1 non ancora eseguito -> TP parziale, chiude 50% size
    - Se TP1 eseguito e PnL >= +3% addizionale al prezzo TP1 -> TP finale, chiude il resto
    - Altrimenti: imposta SL trailing a -1.5% sotto il current (per LONG) o +1.5% sopra (per SHORT)

    Ritorna dict con:
    - 'action': 'hold' | 'close_full' | 'close_partial' | 'update_sl'
    - 'sl_price': nuovo SL (se update_sl o close_full)
    - 'tp_price': None (non usiamo TP su Bybit, gestiamo via riduzione size)
    - 'close_size': size da chiudere (per close_partial)
    - 'reason': descrizione per log
    - 'new_state': aggiornamento stato TP1
    """
    pnl_pct = compute_pnl_pct(side, entry_price, current_price)
    sym_state = state.get(symbol, {})

    # 1. SL hard a -4% (Charter V3 VPTR3, simmetrico RETTANGOLO +4%)
    # Epsilon per float rounding: PnL=-4% esatto potrebbe essere -0.039999...
    if pnl_pct <= VPTR3_SL_HARD_PCT + 1e-9:
        return {
            "action": "close_full",
            "sl_price": None,
            "close_size": size,
            "reason": f"VPTR3 SL hard: PnL={pnl_pct*100:.2f}% <= {VPTR3_SL_HARD_PCT*100}%",
            "new_state": {},  # reset stato
        }

    # 2. TP parziale a +5%
    tp1_done = sym_state.get("tp1_done", False)
    if not tp1_done and pnl_pct >= VPTR3_TP_PARTIAL_PCT - 1e-9:
        return {
            "action": "close_partial",
            "sl_price": None,
            "close_size": round(size / 2, 6),  # 50% della size
            "reason": f"VPTR3 TP parziale: PnL={pnl_pct*100:.2f}% >= +{VPTR3_TP_PARTIAL_PCT*100}%",
            "new_state": {
                symbol: {
                    "tp1_done": True,
                    "tp1_price": current_price,
                    "tp1_ts": datetime.now(timezone.utc).isoformat(),
                    "entry_price": entry_price,
                    "side": side,
                    "original_size": size,
                }
            },
        }

    # 3. TP finale a +3% addizionale al TP1
    if tp1_done:
        tp1_price = sym_state.get("tp1_price", entry_price)
        # target = tp1_price * (1 + 0.03) per LONG, o * (1 - 0.03) per SHORT
        if side == "Buy":
            target = tp1_price * (1 + VPTR3_TP_FINAL_ADD_PCT)
        else:
            target = tp1_price * (1 - VPTR3_TP_FINAL_ADD_PCT)

        if (side == "Buy" and current_price >= target - 1e-9) or (side == "Sell" and current_price <= target + 1e-9):
            return {
                "action": "close_full",
                "sl_price": None,
                "close_size": size,  # chiude tutto cio' che resta
                "reason": f"VPTR3 TP finale: current={current_price} >= target={target:.4f} (+{VPTR3_TP_FINAL_ADD_PCT*100}% addizionale al TP1 @ {tp1_price})",
                "new_state": {},
            }

    # 4. === MATTIA 16/08 FIX Charter V3 unified trailing rules ===
    # REGOLE Charter V3 (uguali per VPTR3, RETTANGOLO_SIMPLE, MA_TRAILING):
    #   - PnL < 1%    -> HOLD (no SL mosso, mantieni default o hard cap Charter -3%)
    #   - PnL >= 1%   -> pre_trailing attivo, SL a BREAKEVEN (entry)
    #   - PnL >= 1.5% -> trigger trailing, lock-in step list progressiva (0.5/1/2/3%)
    #   - HWM         -> SL non peggiora MAI (LONG sale, SHORT scende)
    # FIX BUG precedente: applicava "SL = current * (1 +/- 0.015)" a QUALSIASI PnL > 0,
    # generando SL in PERDITA sopra entry quando PnL era tra 0% e 1% (sotto pre-trailing).
    # Epsilon per evitare float rounding: PnL esattamente = 1.0% potrebbe essere 0.00999...
    if pnl_pct < 0.01 - 1e-9:
        # Pre-trailing NON ancora attivo. NON muovere SL.
        return {
            "action": "hold",
            "sl_price": None,
            "close_size": None,
            "reason": f"VPTR3 Charter V3: PnL={pnl_pct*100:.2f}% < 1% pre-trailing, HOLD (no SL mosso)",
            "new_state": {},
        }

    # Pre-trailing 1% raggiunto: attiva flag (se non gia attivo)
    if not sym_state.get("pre_trailing_active", False):
        sym_state["pre_trailing_active"] = True

    # Calcola SL target in base al profitto
    if pnl_pct < VPTR3_TRAILING_TRIGGER_PCT:
        # PnL tra 1% e 1.5%: SL a breakeven (entry)
        sl_trailing = entry_price
        lock_label = "breakeven"
    else:
        # PnL >= 1.5%: lock-in step list progressiva
        # Epsilon 1e-9 per evitare float rounding sui boundary (es. PnL=5% esatto = 0.04999...)
        if pnl_pct >= 0.05 - 1e-9:  # >= 5%
            lock_in = 0.030  # +3% lock-in
        elif pnl_pct >= 0.03 - 1e-9:  # >= 3%
            lock_in = 0.020  # +2% lock-in
        elif pnl_pct >= 0.02 - 1e-9:  # >= 2%
            lock_in = 0.010  # +1% lock-in
        else:  # 1.5% - 2%
            lock_in = 0.005  # +0.5% lock-in

        if side == "Buy":
            sl_trailing = entry_price * (1 + lock_in)
        else:
            sl_trailing = entry_price * (1 - lock_in)
        lock_label = f"lock-in +{lock_in*100:.1f}%"

    # HWM: SL non peggiora MAI
    prev_sl = sym_state.get("trailing_sl")
    hwm_locked = False
    if prev_sl is not None:
        if side == "Buy" and sl_trailing < prev_sl:
            sl_trailing = prev_sl
            hwm_locked = True
        elif side == "Sell" and sl_trailing > prev_sl:
            sl_trailing = prev_sl
            hwm_locked = True

    new_trailing_state = {symbol: {**sym_state, "trailing_sl": sl_trailing}}
    return {
        "action": "update_sl",
        "sl_price": round(sl_trailing, 6),
        "close_size": None,
        "reason": f"VPTR3 Charter V3: PnL={pnl_pct*100:.2f}%, SL a {lock_label} {sl_trailing:.6f}" + (" (HWM lock)" if hwm_locked else ""),
        "new_state": new_trailing_state,
    }


# ============================================================
# RETTANGOLO_SIMPLE + MA_TRAILING: stessa logica VPTR3 (safety net)
# Strategie "Via TV": Pine manda entry+SL/TP, sltp_engine fa solo safety net
# ============================================================

def process_rettangolo_simple_position(client, state, symbol, side, entry_price, current_price, size):
    """Processa posizione RETTANGOLO_SIMPLE (Via TV). Stessa logica VPTR3 come safety net."""
    result = process_vptr3_position(client, state, symbol, side, entry_price, current_price, size)
    result["reason"] = result["reason"].replace("VPTR3", "RETTANGOLO_SIMPLE")
    return result


def process_ma_trailing_position(client, state, symbol, side, entry_price, current_price, size):
    """Processa posizione MA_TRAILING (Via TV). Stessa logica VPTR3 come safety net."""
    result = process_vptr3_position(client, state, symbol, side, entry_price, current_price, size)
    result["reason"] = result["reason"].replace("VPTR3", "MA_TRAILING")
    return result


# ============================================================
# APPLICAZIONE SL/TP: webhook prima, fallback Bybit diretto
# ============================================================

def apply_via_webhook(symbol, sl_price, tp_price):
    """Prova a chiamare webhook /webhook/sltp. Ritorna (ok, body)."""
    try:
        r = requests.post(
            f"{WEBHOOK_URL}{WEBHOOK_SLTP_PATH}",
            json={
                "secret": WEBHOOK_SECRET,
                "symbol": symbol,
                "sl_price": sl_price,
                "tp_price": tp_price,
            },
            timeout=5,
        )
        return (r.status_code == 200, f"HTTP {r.status_code}: {r.text[:200]}")
    except Exception as e:
        return (False, f"webhook err: {e}")


def apply_via_bybit(client, symbol, sl_price=None, tp_price=None):
    """Fallback diretto via Bybit API (bypassa il webhook)."""
    try:
        r = client.set_trading_stop(symbol, sl_price=sl_price, tp_price=tp_price)
        return (r.get("retCode") == 0, f"Bybit retCode={r.get('retCode')}: {r.get('retMsg')}")
    except Exception as e:
        return (False, f"Bybit err: {e}")


def apply_sltp(client, symbol, sl_price=None, tp_price=None):
    """Applica SL/TP: tenta webhook, se fallisce fallback Bybit diretto."""
    ok, body = apply_via_webhook(symbol, sl_price, tp_price)
    if ok:
        return (True, f"webhook: {body}")
    log(f"  {symbol}: webhook fallito ({body[:100]}), fallback Bybit diretto")
    return apply_via_bybit(client, symbol, sl_price=sl_price, tp_price=tp_price)


def apply_partial_close(client, symbol, side, close_size):
    """Chiude parzialmente una posizione (market order reduceOnly)."""
    try:
        # Per chiudere uno SHORT serve Buy, per chiudere un LONG serve Sell
        close_side = "Buy" if side == "Sell" else "Sell"
        r = client.create_market_order(symbol, close_side, close_size, reduce_only=True)
        return (r.get("retCode") == 0, f"retCode={r.get('retCode')}: {r.get('retMsg')}")
    except Exception as e:
        return (False, f"err: {e}")


# ============================================================
# MAIN LOOP
# ============================================================

def main_loop():
    if not WEBHOOK_SECRET:
        log("ERRORE: WEBHOOK_SECRET non settata. Export WEBHOOK_SECRET prima di lanciare.")
        sys.exit(1)
    client = BybitDemoClient()
    log(f"=== SLTP ENGINE AVVIATO (loop {LOOP_INTERVAL_SEC}s, rettangolo+VPTR3) ===")

    while True:
        try:
            state = load_state()
            new_state = dict(state)  # copia per modifiche

            positions = client.fetch_positions()
            if not positions:
                log("no open positions, skip")
            else:
                log(f"{len(positions)} open positions")
                for pos in positions:
                    symbol = pos["symbol"]
                    side_raw = pos.get("side", "")
                    side = "Buy" if side_raw.lower() in ("buy", "long") else "Sell"
                    size = float(pos.get("size", 0) or 0)
                    if size <= 0:
                        continue
                    try:
                        avg_price = float(pos.get("avgPrice", 0) or 0)
                    except (TypeError, ValueError):
                        avg_price = 0
                    if avg_price <= 0:
                        continue

                    # === FIX 2026-08-05 (Mattia 22:58): POST-ORDER SIZING MONITOR su posizioni aperte ===
                    # Charter 500x3=1500 USDT nozionale. Se posizione attuale ha nozionale fuori
                    # tolleranza 10% (es. Pine price placeholder non rilevato, slippage estremo,
                    # sizing rotto su ordine legacy), log ERROR visibile.
                    # NON chiude (regola FERREA MAI chiudere manualmente). Solo monitor.
                    _target_notional = 1500.0
                    _pos_notional = size * avg_price
                    _drift = abs(_pos_notional - _target_notional) / _target_notional
                    if _drift > 0.10:
                        log(f"  SIZING MONITOR {symbol} side={side} size={size} entry={avg_price} noz={_pos_notional:.2f} USDT drift={_drift*100:.1f}% (target 1500 +/- 10%). CHARTER POTENZIALMENTE VIOLATO su ordine legacy/pre-fix.")

                    # current price
                    try:
                        ticker = client._request("GET", "/v5/market/tickers",
                                                   {"category": "linear", "symbol": symbol},
                                                   signed=False)
                        current_price = float(ticker["result"]["list"][0]["lastPrice"])
                    except Exception as e:
                        log(f"  {symbol}: ticker fetch fallito: {e}")
                        continue

                    strategy = get_strategy_for_symbol(symbol, side)
                    if strategy == "unknown":
                        log(f"  {symbol}: OWNERSHIP UNKNOWN -> FAIL-SAFE SKIP; nessun SL/TP applicato")
                        continue
                    # ADX e strategie TV-driven escluse: le uscite restano governate dagli alert TradingView.
                    if strategy in EXCLUDED_STRATEGIES:
                        log(f"  {symbol} strategy={strategy}: ESCLUSA dal trailing (regola 09/08 rimossa), skip")
                        continue
                    # Mattia 14/08: skip per simboli specifici classificati erroneamente
                    # (WIFUSDT=rsi_swing, BONK=sqw, NEAR=sqw, ZEC=sqw sono in EXCLUDED ma la funzione li ritorna rettangolo/vptr3/ma_trailing)
                    if symbol in ("WIFUSDT", "1000BONKUSDT", "NEARUSDT", "ZECUSDT"):
                        log(f"  {symbol} strategy={strategy}: SKIP per simbolo (escluso manualmente, deve viaggiare libero), skip")
                        continue
                    # Carica tf_dict da TUTTI i CSV strategia (non solo rettangolo_assets)
                    tf_dict = {}
                    for a in load_assets():
                        tf_dict[a["symbol"]] = a["tf_min"]
                    for a in load_vptr3_assets():
                        tf_dict[a["symbol"]] = a["tf_min"]
                    for a in load_rettangolo_simple_assets():
                        tf_dict[a["symbol"]] = a["tf_min"]
                    for a in load_ma_trailing_assets():
                        tf_dict[a["symbol"]] = a["tf_min"]
                    for a in load_supertrend_bosw_assets():
                        tf_dict[a["symbol"]] = a["tf_min"]
                    tf = tf_dict.get(symbol, "60")
                    pnl_pct = compute_pnl_pct(side, avg_price, current_price)

                    log(f"  {symbol} strategy={strategy} side={side} size={size} entry={avg_price} current={current_price} PnL={pnl_pct*100:.2f}%")

                    if strategy in ("vptr3", "rettangolo_simple", "ma_trailing"):
                        # Logica safety-net (stessa per tutte e 3 le strategie TV-driven)
                        if strategy == "rettangolo_simple":
                            result = process_rettangolo_simple_position(client, state, symbol, side, avg_price, current_price, size)
                            strat_label = "RETTANGOLO_SIMPLE"
                        elif strategy == "ma_trailing":
                            result = process_ma_trailing_position(client, state, symbol, side, avg_price, current_price, size)
                            strat_label = "MA_TRAILING"
                        else:
                            result = process_vptr3_position(client, state, symbol, side, avg_price, current_price, size)
                            strat_label = "VPTR3"
                        log(f"  {symbol} [{strat_label}] {result['reason']}")

                        if result["new_state"]:
                            new_state.update(result["new_state"])
                        elif symbol in new_state and result["action"] == "close_full":
                            new_state.pop(symbol, None)  # reset

                        if result["action"] == "hold":
                            log(f"  {symbol} [{strat_label}] HOLD: nessuna azione (PnL sotto pre-trailing 1%)")
                            continue
                        elif result["action"] == "close_full":
                            ok, body = apply_partial_close(client, symbol, side, result["close_size"])
                            log(f"  {symbol} [{strat_label}] CLOSE FULL -> {body}")
                        elif result["action"] == "close_partial":
                            ok, body = apply_partial_close(client, symbol, side, result["close_size"])
                            log(f"  {symbol} [{strat_label}] CLOSE PARTIAL 50% ({result['close_size']}) -> {body}")
                        elif result["action"] == "update_sl":
                            # imposta solo SL (no TP, gestito da riduzione size)
                            ok, body = apply_sltp(client, symbol, sl_price=result["sl_price"], tp_price=None)
                            log(f"  {symbol} [{strat_label}] SL -> {result['sl_price']} ({body[:100]})")
                    else:
                        # Logica RETTANGOLO
                        # Passo new_state per high water mark trailing step lock-in
                        sltp = compute_sltp_rettangolo(client, symbol, side, avg_price, current_price, tf, state=new_state)
                        if sltp is None:
                            log(f"  {symbol} [RETTANGOLO] compute_sltp non ha prodotto valori, skip")
                            continue
                        trailing_step = sltp.get('trailing_step', 0)
                        log(f"  {symbol} [RETTANGOLO] sl={sltp['sl_price']} tp={sltp['tp_price']} trailing={sltp.get('trailing_active')} step={trailing_step} PnL={sltp['profit_pct']}%")

                        # Verifica se gia' allineato
                        cur_sl = pos.get("stopLoss", "")
                        cur_tp = pos.get("takeProfit", "")
                        try:
                            cur_sl_f = float(cur_sl) if cur_sl else None
                            cur_tp_f = float(cur_tp) if cur_tp else None
                        except (TypeError, ValueError):
                            cur_sl_f, cur_tp_f = None, None
                        sl_changed = cur_sl_f is None or abs(cur_sl_f - sltp["sl_price"]) / max(sltp["sl_price"], 1e-9) > ALIGN_TOLERANCE_PCT
                        tp_changed = cur_tp_f is None or abs(cur_tp_f - sltp["tp_price"]) / max(sltp["tp_price"], 1e-9) > ALIGN_TOLERANCE_PCT

                        if sl_changed or tp_changed:
                            ok, body = apply_sltp(client, symbol, sl_price=sltp["sl_price"], tp_price=sltp["tp_price"])
                            log(f"  {symbol} [RETTANGOLO] SL/TP apply -> {body[:200]}")
                        else:
                            log(f"  {symbol} [RETTANGOLO] gia' allineato, skip")

            # Salva stato aggiornato
            if new_state != state:
                save_state(new_state)

        except Exception as e:
            log(f"loop exception: {e}")
        time.sleep(LOOP_INTERVAL_SEC)


if __name__ == "__main__":
    main_loop()
