"""
Webhook receiver - TradingView Pine -> Bybit Demo
SCELTA B + close handler: replica fedele della strategy Pine
- Riceve alert (BUY/SELL) -> apre posizione, chiude opposta se c'è
- Riceve alert {action: close} -> chiude posizione aperta (time-stop / segnale opposto)
- NIENTE TP/SL tecnici (li decide Pine via comment "Max exit bars"/"Opposite signal")
- MANIFESTO: ordini limitati a order value 500 USDT con leva 3x (vedi rettangolo_config)
"""
import os
import sys
import json
import hmac
import hashlib
import time
from datetime import datetime, timezone
from flask import Flask, request, jsonify
import requests

# Config centralizzata (MANIFESTO Charter: order value max 500 USDT, leva 3x)
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from rettangolo_config import ORDER_VALUE_USD, LEVERAGE as MANIFESTO_LEVERAGE, max_qty_for_order_value

# --- Config ---
# SICUREZZA: secrets letti da variabili d'ambiente o da API_KEY_BYBIT.env
# (escluso da git). Settare PRIMA di eseguire il receiver live:
#   $env:WEBHOOK_SECRET = '<secret-concordato-con-Pine-Script>'
#   $env:BYBIT_API_KEY = '<la-tua-bybit-key>'
#   $env:BYBIT_API_SECRET = '<il-tuo-bybit-secret>'
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "")
BYBIT_API_KEY = os.environ.get("BYBIT_API_KEY", "")
BYBIT_API_SECRET = os.environ.get("BYBIT_API_SECRET", "")
BYBIT_BASE = "https://api-demo.bybit.com"
LEVERAGE_TARGET = 3

if not WEBHOOK_SECRET or not BYBIT_API_KEY or not BYBIT_API_SECRET:
    raise SystemExit(
        "ERRORE: WEBHOOK_SECRET, BYBIT_API_KEY, BYBIT_API_SECRET devono "
        "essere settate come variabili d'ambiente prima di eseguire il "
        "receiver live.\n"
        "Esempio PowerShell:\n"
        "  $env:WEBHOOK_SECRET='<secret>'\n"
        "  $env:BYBIT_API_KEY='<key>'\n"
        "  $env:BYBIT_API_SECRET='<secret>'\n"
    )

# --- Bybit V5 client ---
def bybit_request(method, path, params=None, signed=True):
    ts = str(int(time.time() * 1000))
    recv_window = "5000"
    if signed:
        param_str = ""
        if method == "GET" and params:
            sorted_items = sorted(params.items())
            param_str = "&".join([f"{k}={v}" for k, v in sorted_items])
            url_path = path
            if param_str:
                url_path = f"{path}?{param_str}"
        elif method == "POST" and params:
            param_str = json.dumps(params, separators=(", ", ": "))
            url_path = path
        else:
            url_path = path
        sign_payload = f"{ts}{BYBIT_API_KEY}{recv_window}{param_str}"
        signature = hmac.new(
            BYBIT_API_SECRET.encode("utf-8"),
            sign_payload.encode("utf-8"),
            hashlib.sha256
        ).hexdigest()
        headers = {
            "X-BAPI-API-KEY": BYBIT_API_KEY,
            "X-BAPI-SIGN": signature,
            "X-BAPI-TIMESTAMP": ts,
            "X-BAPI-RECV-WINDOW": recv_window,
            "Content-Type": "application/json"
        }
        url = f"{BYBIT_BASE}{url_path}"
    else:
        headers = {"Content-Type": "application/json"}
        url = f"{BYBIT_BASE}{path}"

    if method == "GET":
        r = requests.get(url, headers=headers, timeout=10)
    else:
        r = requests.post(url, headers=headers, data=json.dumps(params) if params else None, timeout=10)
    return r.json() if r.text else {}

def set_leverage(symbol, leverage=3):
    try:
        body = {"category": "linear", "symbol": symbol, "buyLeverage": str(leverage), "sellLeverage": str(leverage)}
        r = bybit_request("POST", "/v5/position/set-leverage", body)
        if r.get("retCode") == 0:
            return True, "leverage set"
        if r.get("retCode") == 110043:
            return True, "leverage already at target (skip)"
        return False, f"leverage err: {r.get('retMsg')} (code {r.get('retCode')})"
    except Exception as e:
        return False, f"leverage exception: {e}"

def get_position(symbol):
    try:
        r = bybit_request("GET", "/v5/position/list", {"category": "linear", "symbol": symbol})
        if r.get("retCode") != 0:
            return None
        positions = r.get("result", {}).get("list", [])
        for p in positions:
            size = float(p.get("size", 0) or 0)
            if size > 0:
                return {
                    "side": p.get("side"),
                    "size": size,
                    "avgPrice": p.get("avgPrice"),
                    "positionIdx": int(p.get("positionIdx", 0))
                }
        return None
    except Exception as e:
        return None


def count_open_positions():
    """Ritorna il numero totale di posizioni aperte su Bybit (tutti i symbol linear).
    Usato per il safety cap MAX_OPEN_POSITIONS."""
    try:
        r = bybit_request("GET", "/v5/position/list", {"category": "linear", "settleCoin": "USDT"})
        if r.get("retCode") != 0:
            return -1  # errore, non bloccare
        positions = r.get("result", {}).get("list", [])
        return sum(1 for p in positions if float(p.get("size", 0) or 0) > 0)
    except Exception:
        return -1

def close_position(symbol, side, size, position_idx=0):
    close_side = "Sell" if side == "Buy" else "Buy"
    body = {
        "category": "linear",
        "symbol": symbol,
        "side": close_side,
        "orderType": "Market",
        "qty": str(size),
        "timeInForce": "GTC",
        "reduceOnly": True,
        "positionIdx": position_idx
    }
    return bybit_request("POST", "/v5/order/create", body)

def create_market_order(symbol, side, qty):
    body = {
        "category": "linear",
        "symbol": symbol,
        "side": side,
        "orderType": "Market",
        "qty": str(qty),
        "timeInForce": "GTC",
        "positionIdx": 0
    }
    return bybit_request("POST", "/v5/order/create", body)


def set_trading_stop(symbol, sl_price=None, tp_price=None):
    """Set SL e/o TP sulla posizione aperta del symbol. Bybit V5 /v5/position/trading-stop.
    sl_price / tp_price: trigger mark price. Almeno uno dei due obbligatorio.
    Se la posizione non esiste, Bybit ritorna errore (lo gestiamo come warning, non fatale)."""
    if sl_price is None and tp_price is None:
        return {"retCode": -1, "retMsg": "no sl/tp provided"}
    body = {"category": "linear", "symbol": symbol}
    if sl_price is not None:
        body["stopLoss"] = str(sl_price)
        body["slTriggerBy"] = "MarkPrice"
    if tp_price is not None:
        body["takeProfit"] = str(tp_price)
        body["tpTriggerBy"] = "MarkPrice"
    return bybit_request("POST", "/v5/position/trading-stop", body)

# --- Flask app ---
app = Flask(__name__)

LOG_FILE = r"G:\AI TRADING ENGINE\live_deploy\webhook_listener\logs\orders.log"
TRADES_CSV = r"G:\AI TRADING ENGINE\live_deploy\webhook_listener\logs\trades.csv"


def normalize_symbol(symbol):
    """Bybit V5 usa 'BTCUSDT' (senza .P) per linear perpetual. Pine {{ticker}}
    puo' mandare:
      - 'DASHUSDT.P' (con .P)
      - 'BYBIT:DASHUSDT.P' (prefisso exchange + .P)
      - 'BINANCE:DASHUSDT.P' (prefisso exchange + .P)
      - 'DASHUSDT' (gia' normalizzato)
    Normalizza tutto a 'DASHUSDT'."""
    if not symbol:
        return ""
    s = symbol.strip().upper()
    # rimuovi prefisso exchange "EXCHANGE:" se presente
    if ":" in s:
        s = s.split(":", 1)[1]
    # rimuovi suffisso ".P" (linear perpetual Pine/TradingView)
    if s.endswith(".P"):
        s = s[:-2]
    return s


# Strategia Pine (comment in strategy.close / strategy.close_all) che Pine
# puo' riportare nel payload come `comment` o `action`. Quando uno di questi
# match, il payload va trattato come CLOSE (non come ENTRY).
# Pine: strategy.close("Long", comment="Opposite signal")
# Pine: strategy.close_all(comment="Max exit bars")
CLOSE_KEYWORDS = ("max exit", "opposite signal", "max_exit", "exit_bar", "exit bar")


def is_close_intent(data):
    """Ritorna (is_close, reason) se il payload Pine indica una chiusura.

    Casi gestiti:
      - action: "close"                        (esplicito)
      - action: "max_exit_bar" / "exit_bar"    (varianti Pine)
      - comment contiene "Max exit bars" o "Opposite signal"
        (stringhe esatte usate da strategy.close* in tutti i Pine VPTR3)
    """
    action = str(data.get("action", "")).strip().lower()
    comment = str(data.get("comment", "")).strip().lower()
    if action == "close":
        return True, "action=close"
    if action in ("max_exit_bar", "exit_bar", "maxexit", "exit"):
        return True, f"action={action}"
    for kw in CLOSE_KEYWORDS:
        if kw in action or kw in comment:
            return True, f"matched keyword '{kw}'"
    return False, None


def log_trade(symbol, side, qty, entry_price, exit_price=None, pnl_pct=None, pnl_usd=None, reason="", strategy="vptr3"):
    """Logga un trade in CSV strutturato per monitoraggio statistiche.
    strategy: 'vptr3' (Pine webhook) o 'rettangolo' (Python bot)"""
    try:
        os.makedirs(os.path.dirname(TRADES_CSV), exist_ok=True)
        file_exists = os.path.isfile(TRADES_CSV)
        ts = datetime.now(timezone.utc).astimezone().isoformat()
        with open(TRADES_CSV, "a", encoding="utf-8") as f:
            if not file_exists:
                f.write("timestamp,symbol,side,qty,entry_price,exit_price,pnl_pct,pnl_usd,reason,strategy\n")
            f.write(",".join([
                ts,
                symbol,
                side,
                str(qty),
                str(entry_price) if entry_price else "",
                str(exit_price) if exit_price else "",
                str(pnl_pct) if pnl_pct is not None else "",
                str(pnl_usd) if pnl_usd is not None else "",
                reason.replace(",", ";"),
                strategy
            ]) + "\n")
    except Exception as e:
        log_event(f"  log_trade err: {e}")


# stato delle posizioni aperte: symbol -> dict con entry_price, qty, side, entry_ts
OPEN_POSITIONS = {}

def log_event(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

@app.route("/webhook", methods=["POST"])
def webhook():
    # 1. Parse JSON
    try:
        data = request.get_json(force=True, silent=True) or {}
    except Exception:
        log_event("ERR: payload non JSON")
        return jsonify({"ok": False, "err": "not json"}), 400

    # 1b. Payload vuoto (TV a volte manda ping/health-check senza body)
    if not data or (isinstance(data, dict) and not any(data.values())):
        return jsonify({"ok": True, "result": "empty payload ignored", "ts": int(time.time())}), 200

    # 2. Check secret
    if data.get("secret") != WEBHOOK_SECRET:
        log_event(f"ERR: secret sbagliato da {request.remote_addr}: {data}")
        return jsonify({"ok": False, "err": "bad secret"}), 403

    # 3. Check symbol required
    symbol = data.get("symbol")
    if not symbol:
        log_event(f"ERR: symbol mancante: {data}")
        return jsonify({"ok": False, "err": "missing symbol"}), 400

    # 4. Estrai strategy dal payload (default 'vptr3' = Pine webhook)
    payload_strategy = data.get("strategy", "vptr3")

    # 4. CLOSE INTENT: esplicito (action=close) o implicito via comment Pine
    # Pine emette strategy.close("Long", comment="Opposite signal")
    # e strategy.close_all(comment="Max exit bars") - entrambi vanno gestiti come CLOSE
    is_close, close_reason = is_close_intent(data)
    if is_close:
        symbol = normalize_symbol(symbol)
        log_event(f"WEBHOOK CLOSE: {symbol} (reason: {close_reason})")
        pos = get_position(symbol)
        if not pos:
            log_event(f"  >> Nessuna posizione aperta su {symbol}, niente da chiudere")
            return jsonify({"ok": True, "action": "close", "result": "no position to close", "reason": close_reason}), 200
        log_event(f"  >> Chiudo posizione esistente {pos['side']} size={pos['size']}")
        close_resp = close_position(symbol, pos["side"], pos["size"], pos["positionIdx"])
        log_event(f"  close resp: {close_resp}")
        if close_resp.get("retCode") != 0:
            return jsonify({"ok": False, "err": f"close failed: {close_resp.get('retMsg')}"}), 500
        oid = close_resp.get("result", {}).get("orderId")
        log_event(f"  POSIZIONE CHIUSA: orderId={oid}")
        # calcola PnL usando il fill di entry salvato
        entry_info = OPEN_POSITIONS.pop(symbol, None)
        # reason per il trade CSV: max_exit_bars / opposite_signal / close esplicito
        csv_reason = "max_exit_bars" if "max exit" in close_reason else \
                     "opposite_signal" if "opposite signal" in close_reason else "close"
        if entry_info:
            exit_price = close_resp.get("result", {}).get("avgPrice")
            if not exit_price:
                # fallback: leggi posizione (sarà 0 size) e usa last trade price
                try:
                    ticker = bybit_request("GET", "/v5/market/tickers", {"category": "linear", "symbol": symbol})
                    exit_price = float(ticker.get("result", {}).get("list", [{}])[0].get("lastPrice", 0))
                except Exception:
                    exit_price = 0
            entry_price_raw = entry_info["entry_price"]
            qty_closed = pos["size"]
            try:
                # entry_price da Bybit e' una stringa, serve cast float
                entry_price = float(entry_price_raw) if entry_price_raw else 0.0
                exit_price_f = float(exit_price) if exit_price else 0.0
                if entry_price > 0 and exit_price_f > 0:
                    if pos["side"] == "Buy":
                        pnl_pct = (exit_price_f - entry_price) / entry_price * 100
                    else:
                        pnl_pct = (entry_price - exit_price_f) / entry_price * 100
                    pnl_usd = pnl_pct * entry_price * qty_closed / 100
                else:
                    pnl_pct, pnl_usd = None, None
                log_trade(symbol, pos["side"], qty_closed, entry_price_raw, exit_price, pnl_pct, pnl_usd, reason=csv_reason, strategy=payload_strategy)
                if pnl_pct is not None:
                    log_event(f"  PnL registrato: {pnl_pct:.2f}% (${pnl_usd:.2f})")
                else:
                    log_event("  PnL non calcolabile (entry o exit price mancanti)")
            except Exception as e:
                # non bloccare il 200 OK se il calcolo PnL fallisce — la close su Bybit e' andata
                log_event(f"  WARN PnL calc fallita: {e}")
                log_trade(symbol, pos["side"], qty_closed, entry_price_raw, exit_price, None, None, reason=csv_reason+"_no_pnl", strategy=payload_strategy)
        return jsonify({"ok": True, "action": "close", "reason": close_reason, "orderId": oid, "symbol": symbol, "closedSize": pos["size"]})

    # 5. ENTRY: richiede side + qty
    side = data.get("side")
    qty = data.get("qty")
    if not (side and qty):
        log_event(f"ERR: side/qty mancanti per entry: {data}")
        return jsonify({"ok": False, "err": "missing side or qty for entry"}), 400

    try:
        qty = float(qty)
    except (TypeError, ValueError):
        log_event(f"ERR: qty non numerico: {qty}")
        return jsonify({"ok": False, "err": "qty not numeric"}), 400

    # 5a. MANIFESTO: tronca qty se eccede ORDER_VALUE_USD nozionale con leva 3x.
    # Recupera current_price: prima dal payload ("price" opzionale), poi da Bybit ticker.
    symbol_pre = symbol  # potrebbe avere .P, ma per il ticker va normalizzato dopo
    current_price = None
    payload_price = data.get("price")
    if payload_price is not None:
        try:
            current_price = float(payload_price)
        except (TypeError, ValueError):
            current_price = None
    if current_price is None or current_price <= 0:
        # fallback: Bybit ticker
        try:
            t = bybit_request("GET", "/v5/market/tickers", {"category": "linear", "symbol": normalize_symbol(symbol_pre)})
            current_price = float(t.get("result", {}).get("list", [{}])[0].get("lastPrice", 0))
        except Exception:
            current_price = 0
    if current_price and current_price > 0:
        max_qty = max_qty_for_order_value(current_price)
        if qty > max_qty:
            log_event(f"  MANIFESTO: qty Pine {qty} > max {max_qty:.6f} per order_value ${ORDER_VALUE_USD} @ price {current_price} -> tronco")
            qty = round(max_qty, 3)
        else:
            log_event(f"  MANIFESTO: qty {qty} OK (nozionale ${qty * current_price:.2f} <= ${ORDER_VALUE_USD})")

    # FIX 2026-07-16: Pine `{{strategy.order.action}}` puo' ritornare "buy"/"sell" lowercase
    # (in base al case sensitivity della strategia TV). Normalizziamo per evitare 400 inutili.
    # Episodes: 15/07 20:00, 16/07 02:00, 16/07 05:00 (3 entry Pine rifiutati in 12h).
    if side.lower() in ("buy", "sell"):
        side = side.capitalize()  # "buy" -> "Buy", "BUY" -> "Buy"
    if side not in ("Buy", "Sell"):
        log_event(f"ERR: side non valido: {side}")
        return jsonify({"ok": False, "err": "side must be Buy or Sell"}), 400

    log_event(f"WEBHOOK ENTRY: {side} {qty} {symbol}")
    # normalizza symbol rimuovendo .P (Bybit V5 linear non lo vuole) — DEVE essere PRIMA di set_leverage
    # altrimenti Bybit rifiuta "BTCUSDT.P" con errore 10001 (symbol invalid) e il webhook ritorna 500.
    symbol = normalize_symbol(symbol)
    log_event(f"  normalized: {symbol}")

    # 6. Set leverage 3x
    ok, msg = set_leverage(symbol, LEVERAGE_TARGET)
    log_event(f"  set_leverage: {msg}")
    if not ok:
        return jsonify({"ok": False, "err": msg}), 500

    # 7. Controlla posizione esistente
    pos = get_position(symbol)
    if pos:
        log_event(f"  pos esistente: {pos['side']} size={pos['size']} avg={pos['avgPrice']}")
        if pos["side"] != side:
            # OPPOSTA: chiudi (mirror di strategy.close)
            log_event(f"  >> Chiudo posizione opposta {pos['side']} {pos['size']}")
            close_resp = close_position(symbol, pos["side"], pos["size"], pos["positionIdx"])
            log_event(f"  close resp: {close_resp}")
            if close_resp.get("retCode") != 0:
                return jsonify({"ok": False, "err": f"close failed: {close_resp.get('retMsg')}"}), 500
        else:
            # stessa direzione gia aperta: skip (pyramiding=0)
            log_event(f"  >> Stessa direzione gia' aperta, skip apertura")
            return jsonify({"ok": True, "action": "skipped", "reason": "same side already open"}), 200

    # 7b. SAFETY CAP: se siamo gia' al massimo di posizioni aperte, skippa
    # (Pine riceve 200 OK con "skipped" cosi' non ritrasmette)
    from rettangolo_config import MAX_OPEN_POSITIONS
    n_open = count_open_positions()
    if n_open >= MAX_OPEN_POSITIONS:
        log_event(f"  SAFETY CAP: {n_open} posizioni aperte >= MAX {MAX_OPEN_POSITIONS} -> skip entry {side} {qty} {symbol}")
        return jsonify({"ok": True, "action": "skipped", "reason": f"safety cap: {n_open}/{MAX_OPEN_POSITIONS} posizioni aperte"}), 200

    # 8. Apri nuova posizione
    order = create_market_order(symbol, side, qty)
    log_event(f"  order resp: {order}")
    if order.get("retCode") != 0:
        return jsonify({"ok": False, "err": order.get("retMsg"), "code": order.get("retCode")}), 500

    oid = order.get("result", {}).get("orderId")
    log_event(f"  ORDER APERTA: orderId={oid} {side} {qty} {symbol}")
    # 8b. SL/TP opzionali dal payload (opzione C: BB- / BB mid calcolati da Python lato mittente)
    sltp_result = None
    sl_in = data.get("sl_price")
    tp_in = data.get("tp_price")
    if sl_in is not None or tp_in is not None:
        try:
            sl_f = float(sl_in) if sl_in is not None else None
            tp_f = float(tp_in) if tp_in is not None else None
            sltp_resp = set_trading_stop(symbol, sl_f, tp_f)
            log_event(f"  sltp resp (post-entry): {sltp_resp}")
            if sltp_resp.get("retCode") == 0:
                sltp_result = {"sl": sl_f, "tp": tp_f}
                log_event(f"  SLTP SETTATO post-entry: {symbol} sl={sl_f} tp={tp_f}")
            else:
                log_event(f"  WARN sltp post-entry fallito: {sltp_resp.get('retMsg')}")
        except (TypeError, ValueError) as e:
            log_event(f"  WARN sl/tp dal payload non numerici: sl={sl_in} tp={tp_in} ({e})")
    # Prova a leggere avgPrice direttamente dalla response (Bybit V5 market order fill istantaneo)
    # Fallback a get_position SOLO se avgPrice manca (evita 1 chiamata API extra nel 99% dei casi)
    fill_price = order.get("result", {}).get("avgPrice")
    if not fill_price:
        pos = get_position(symbol)
        fill_price = pos["avgPrice"] if pos else None
    if fill_price:
        # cast a string per consistenza con entry_info
        try:
            fill_price_f = float(fill_price)
        except (TypeError, ValueError):
            fill_price_f = None
        if fill_price_f is not None:
            OPEN_POSITIONS[symbol] = {
                "side": side, "qty": qty, "entry_price": str(fill_price_f),
                "entry_ts": datetime.now(timezone.utc).astimezone().isoformat()
            }
            log_trade(symbol, side, qty, str(fill_price_f), reason="entry", strategy=payload_strategy)
            log_event(f"  fill price registrato: {fill_price_f}")
    if not fill_price:
        log_trade(symbol, side, qty, None, reason="entry_no_fill", strategy=payload_strategy)
    return jsonify({"ok": True, "orderId": oid, "symbol": symbol, "side": side, "qty": qty, "sltp": sltp_result})

@app.route("/health", methods=["GET"])
def health():
    return jsonify({"ok": True, "ts": int(time.time()), "service": "bybit-webhook", "mode": "B-pine-faithful+close+sltp"})

@app.route("/webhook/sltp", methods=["POST"])
def webhook_sltp():
    """Set SL/TP su posizione aperta. Chiamato da script esterni (compute_signal Python,
    rettangolo_monitor, o manualmente) che calcolano BB- / BB mid e li passano qui.
    Payload: {secret, symbol, sl_price?, tp_price?}
    - sl_price / tp_price opzionali ma almeno uno obbligatorio
    - entrambi triggerBy = MarkPrice (scelta coerente con strategy.close Pine)"""
    try:
        data = request.get_json(force=True, silent=True) or {}
    except Exception:
        log_event("ERR sltp: payload non JSON")
        return jsonify({"ok": False, "err": "not json"}), 400
    if not data or (isinstance(data, dict) and not any(data.values())):
        return jsonify({"ok": True, "result": "empty payload ignored", "ts": int(time.time())}), 200
    if data.get("secret") != WEBHOOK_SECRET:
        log_event(f"ERR sltp: secret sbagliato da {request.remote_addr}: {data}")
        return jsonify({"ok": False, "err": "bad secret"}), 403
    symbol = data.get("symbol")
    if not symbol:
        log_event(f"ERR sltp: symbol mancante: {data}")
        return jsonify({"ok": False, "err": "missing symbol"}), 400
    sl_price = data.get("sl_price")
    tp_price = data.get("tp_price")
    if sl_price is None and tp_price is None:
        log_event(f"ERR sltp: almeno uno tra sl_price/tp_price: {data}")
        return jsonify({"ok": False, "err": "sl_price or tp_price required"}), 400
    try:
        sl_f = float(sl_price) if sl_price is not None else None
        tp_f = float(tp_price) if tp_price is not None else None
    except (TypeError, ValueError):
        log_event(f"ERR sltp: sl/tp non numerici: sl={sl_price} tp={tp_price}")
        return jsonify({"ok": False, "err": "sl/tp not numeric"}), 400

    symbol = normalize_symbol(symbol)
    log_event(f"WEBHOOK SLTP: {symbol} sl={sl_f} tp={tp_f}")
    # verifica posizione aperta (Bybit rifiuta trading-stop se non c'è posizione)
    pos = get_position(symbol)
    if not pos:
        log_event(f"  >> Nessuna posizione aperta su {symbol}, skip SLTP")
        return jsonify({"ok": True, "result": "no position, sltp skipped", "symbol": symbol}), 200
    resp = set_trading_stop(symbol, sl_f, tp_f)
    log_event(f"  sltp resp: {resp}")
    if resp.get("retCode") != 0:
        return jsonify({"ok": False, "err": f"sltp failed: {resp.get('retMsg')}", "code": resp.get("retCode")}), 500
    log_event(f"  SLTP SETTATO: {symbol} sl={sl_f} tp={tp_f}")
    return jsonify({"ok": True, "action": "sltp", "symbol": symbol, "sl": sl_f, "tp": tp_f, "orderId": resp.get("result", {}).get("orderId", "")})

@app.route("/", methods=["GET"])
def root():
    return "Bybit webhook receiver (SCELTA B + close handler + SLTP) - POST /webhook or /webhook/sltp"

if __name__ == "__main__":
    log_event("=== WEBHOOK SERVER AVVIATO (SCELTA B + CLOSE + SLTP) ===")
    app.run(host="127.0.0.1", port=5580, debug=False, use_reloader=False)
