"""
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 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_TRIGGER_PCT = 0.015  # 1.5% dal entry per attivare trailing
# === P006 Charter: SL -3% MAX cap su RETTANGOLO (simmetrico a VPTR3) ===
RETTANGOLO_SL_HARD_PCT = 0.03  # 3% dal entry, simmetrico per LONG/SHORT

# VPTR3
VPTR3_SL_HARD_PCT = -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/webhook_listener/logs/sltp_state.json")
LOG_FILE = r"/opt/charter-live/live_deploy/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_strategy_for_symbol(symbol: str) -> str:
    """Ritorna 'rettangolo' o 'vptr3' per il symbol.
    FIX 2026-07-18 (Mattia BREAKING POINT): VPTR3 ha file dedicato VPTR_V3_ASSETS.csv
    (NO contaminazione con rettangolo_assets.csv). Legge PRIMA vptr3, POI rettangolo.
    Rispetta campo 'enabled': se disabled in vptr3, salta e cerca in rettangolo.
    Se symbol NON in nessun file â†’ 'rettangolo' (default legacy)."""
    symbol_u = symbol.upper()
    # 1. Controlla VPTR_V3_ASSETS.csv (Pine webhook, separato da rettangolo)
    #    Solo se ENABLED (altrimenti SOLUSDT attivo in rettangolo viene erroneamente letto come vptr3)
    for asset in load_vptr3_assets():
        if asset["symbol"] == symbol_u and asset["enabled"]:
            strategy = asset["strategy"]
            # Normalizza vptr_v3 (Mattia) → vptr3 (interno)
            if strategy == "vptr_v3":
                strategy = "vptr3"
            return strategy
    # 2. Controlla rettangolo_assets.csv (rettangolo + square)
    for asset in load_assets():
        if asset["symbol"] == symbol_u and asset["enabled"]:
            return asset["strategy"]
    return "rettangolo"  # default legacy


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


# ============================================================
# RETTANGOLO: compute_signal + trailing stop con soglia 1.5%
# ============================================================

def compute_sltp_rettangolo(client, symbol, side, entry_price, current_price, timeframe):
    """Calcola SL/TP per posizione RETTANGOLO.
    Ritorna dict {sl_price, tp_price, sl_ref, source, breakeven} o None.
    Trailing: attivato SOLO se current_price in profitto di >= 1.5% dal entry.
    Una volta attivato, SL segue il prezzo (high per SHORT, low per LONG)."""
    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

        # === 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: si attiva SOLO se in profitto di >= 1.5%
        if profit_pct >= RETTANGOLO_TRAILING_TRIGGER_PCT:
            # SL trailing: per LONG, segue il low piu' alto dal entry; per SHORT, segue l'high piu' basso
            # Per semplicita': SL trailing = entry_price (breakeven) + 50% del profitto
            if side == "Buy":
                sl_trailing = entry_price + (current_price - entry_price) * 0.5
                # pero' deve essere < current_price
                sl_trailing = min(sl_trailing, current_price * 0.999)
            else:
                sl_trailing = entry_price - (entry_price - current_price) * 0.5
                sl_trailing = max(sl_trailing, current_price * 1.001)
            sl = sl_trailing
            trailing_active = True
        else:
            sl = sl_initial
            trailing_active = False

        return {
            "sl_price": round(sl, 6),
            "tp_price": round(rng_mid, 6),
            "sl_ref": round(sl_ref, 6),
            "source": f"compute_signal(hammer@idx={best_idx}, tf={timeframe})",
            "trailing_active": trailing_active,
            "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 -3%
    if pnl_pct <= VPTR3_SL_HARD_PCT:
        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:
        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) or (side == "Sell" and current_price <= target):
            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. Altrimenti: imposta SL trailing a -1.5% dal current
    if side == "Buy":
        sl_trailing = current_price * (1 - VPTR3_TRAILING_TRIGGER_PCT)
    else:
        sl_trailing = current_price * (1 + VPTR3_TRAILING_TRIGGER_PCT)
    return {
        "action": "update_sl",
        "sl_price": round(sl_trailing, 6),
        "close_size": None,
        "reason": f"VPTR3 trailing: PnL={pnl_pct*100:.2f}%, SL a -1.5% dal current",
        "new_state": {},
    }


# ============================================================
# 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)
        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

                    # 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)
                    tf_dict = {a["symbol"]: a["tf_min"] for a in load_assets()}
                    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 == "vptr3":
                        # Logica VPTR3
                        result = process_vptr3_position(client, state, symbol, side, avg_price, current_price, size)
                        log(f"  {symbol} [VPTR3] {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"] == "close_full":
                            ok, body = apply_partial_close(client, symbol, side, result["close_size"])
                            log(f"  {symbol} [VPTR3] CLOSE FULL -> {body}")
                        elif result["action"] == "close_partial":
                            ok, body = apply_partial_close(client, symbol, side, result["close_size"])
                            log(f"  {symbol} [VPTR3] 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} [VPTR3] SL -> {result['sl_price']} ({body[:100]})")
                    else:
                        # Logica RETTANGOLO
                        sltp = compute_sltp_rettangolo(client, symbol, side, avg_price, current_price, tf)
                        if sltp is None:
                            log(f"  {symbol} [RETTANGOLO] compute_sltp non ha prodotto valori, skip")
                            continue
                        log(f"  {symbol} [RETTANGOLO] sl={sltp['sl_price']} tp={sltp['tp_price']} trailing={sltp.get('trailing_active')}")

                        # 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()
