""" Rettangolo Runner - Esecuzione live della strategia RETTANGOLO. Loop ogni 60s: 1. Per ogni symbol in rettangolo_assets.csv (strategy=rettangolo) 2. Calcola compute_signal() (rettangolo_strategy.py) 3. Se c'è segnale LONG o SHORT: - Safety cap MAX_OPEN_POSITIONS (10 globale, già in rettangolo_config) - FIX 2026-07-21 Mattia 16:35: ANTI-DOPPIA LOCALE via signal_state.json (cooldown 4h) - Se gia' posizione aperta su quel symbol: skip - Altrimenti: set leverage 3x, market order, set SL/TP via set_trading_stop 4. Logga tutto in rettangolo_runner.log NON tocca file di config (rettangolo_assets.csv, rettangolo_config.py). NON modifica posizioni esistenti (time-stop lo fa Pine via webhook). """ import os import sys import time import json import requests from datetime import datetime, timezone from pathlib import Path from zoneinfo import ZoneInfo sys.path.insert(0, str(Path(__file__).parent)) from bybit_demo_client import BybitDemoClient from rettangolo_strategy import compute_signal from rettangolo_config import ( load_assets, ORDER_VALUE_USD, LEVERAGE, MAX_OPEN_POSITIONS, max_qty_for_order_value, ) # === CONFIG === LOOP_INTERVAL_SEC = 60 INTRADAY_LIMIT = 200 DAILY_LIMIT = 5 TIMEFRAME = "120" # 2H (hardcoded, matches rettangolo_assets.csv) # FIX 2026-07-21 Mattia 16:35: ANTI-DOPPIA LOCALE # Bug: rettangolo_runner ha inviato 933+415=1348 webhook ETH/WIF SELL in 15 ore perche # fetch_positions(sym) ritornava vuoto dopo errore Bybit "ab not enough" (saldo demo finito). # Workaround: file JSON che traccia ultimo_segnale per symbol+side. Cooldown 4h. SIGNAL_COOLDOWN_SEC = 14400 # 4 ore SIGNAL_STATE_FILE = r"G:\AI TRADING ENGINE\live_deploy\logs\rettangolo_signal_state.json" # === REFACTOR 2026-07-20 (Mavis) — Rettangolo passa per il webhook 5580 === WEBHOOK_URL = "http://127.0.0.1:5580/webhook" WEBHOOK_SECRET = "TV_2026_MATTIA_DEMO" LOG_FILE = r"G:\AI TRADING ENGINE\live_deploy\webhook_listener\logs\rettangolo_runner.log" LOCAL_TZ = ZoneInfo("Europe/Rome") def log(msg): ts = datetime.now(timezone.utc).astimezone(LOCAL_TZ).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_signal_state(): try: if os.path.exists(SIGNAL_STATE_FILE): with open(SIGNAL_STATE_FILE, "r", encoding="utf-8") as f: return json.load(f) except Exception as e: log(f" load_signal_state err: {e}") return {} def _save_signal_state(state): try: os.makedirs(os.path.dirname(SIGNAL_STATE_FILE), exist_ok=True) with open(SIGNAL_STATE_FILE, "w", encoding="utf-8") as f: json.dump(state, f, indent=2) except Exception as e: log(f" save_signal_state err: {e}") def _is_recent_signal(state, symbol, side, now_epoch): """Ritorna True se stesso symbol+side ha un segnale nelle ultime SIGNAL_COOLDOWN_SEC.""" key = f"{symbol}_{side}" rec = state.get(key) if not rec: return False elapsed = now_epoch - float(rec.get("ts", 0)) return elapsed < SIGNAL_COOLDOWN_SEC def fetch_klines(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(LOCAL_TZ).isoformat(), "open": o, "high": h, "low": l, "close": c, }) return out def set_leverage_safe(client, symbol, leverage=3): try: r = client.set_leverage(symbol, leverage) return True, f"retCode={r.get('retCode')}" except Exception as e: msg = str(e) if "110043" in msg: return True, "leverage already at target (skip)" return False, f"err: {e}" def count_open_positions(client): try: positions = client.fetch_positions() return sum(1 for p in positions if float(p.get("size", 0) or 0) > 0) except Exception: return -1 def get_ticker_price(client, symbol): try: d = client._request("GET", "/v5/market/tickers", {"category": "linear", "symbol": symbol}, signed=False) lst = d.get("result", {}).get("list", []) if lst: return float(lst[0].get("lastPrice", 0) or 0) except Exception: pass return 0.0 def run_once(client, rettangolo_assets): """Processa tutti gli asset rettangolo. Ritorna numero di trade aperti in questo giro.""" opened = 0 signal_state = _load_signal_state() state_dirty = False now_epoch = time.time() for asset in rettangolo_assets: sym = asset["symbol"] tf_min = asset.get("tf_min", "120") try: tf_int = int(tf_min) except (TypeError, ValueError): tf_int = 120 # 1. fetch candele try: daily = fetch_klines(client, sym, "D", DAILY_LIMIT) intraday = fetch_klines(client, sym, str(tf_int), INTRADAY_LIMIT) except Exception as e: log(f" {sym}: fetch klines err: {e}") continue if len(daily) < 2 or len(intraday) < 2: log(f" {sym}: klines insufficienti (daily={len(daily)}, intra={len(intraday)})") continue prev_daily = daily[-2] if len(intraday) < 3: log(f" {sym}: klines insufficienti per valutare candela chiusa (intra={len(inttraday)})") continue idx_closed = len(intraday) - 2 pattern_mode = asset.get("pattern_mode", "relaxed") allow_martello_spinta = (pattern_mode == "relaxed") log(f" {sym}: evaluating CLOSED candle idx={idx_closed} pattern_mode={pattern_mode}") sig = compute_signal(prev_daily, intraday, idx_closed, sl_buffer_pct=0.01, require_two_candles=True, allow_martello_spinta=allow_martello_spinta) if sig is None: continue bybit_side = "Buy" if sig["signal"] == "LONG" else "Sell" log(f" {sym}: SEGNALE {sig['signal']} entry={sig['entry']:.4f} sl={sig['sl']:.4f} tp={sig['tp']:.4f}") # 2. FIX 2026-07-21 16:35 Mattia — ANTI-DOPPIA LOCALE # Se stesso symbol+side ha un segnale nelle ultime SIGNAL_COOLDOWN_SEC (4h), SKIP. # Workaround per fetch_positions che ritorna vuoto dopo errore Bybit "ab not enough". if _is_recent_signal(signal_state, sym, bybit_side, now_epoch): key = f"{sym}_{bybit_side}" elapsed_h = (now_epoch - float(signal_state[key].get("ts", 0))) / 3600 log(f" ANTI-DOPPIA LOCALE: {sym} {bybit_side} ultimo segnale {elapsed_h:.1f}h fa (cooldown {SIGNAL_COOLDOWN_SEC/3600:.0f}h), skip") continue # 3. safety cap n_open = count_open_positions(client) if n_open < 0: log(f" err count positions, skip {sym}") continue if n_open >= MAX_OPEN_POSITIONS: log(f" safety cap {n_open}/{MAX_OPEN_POSITIONS}, skip {sym}") continue # 4. verifica posizione gia' aperta su questo symbol try: existing = client.fetch_positions(sym) if existing: log(f" posizione gia' aperta su {sym} ({existing[0]['side']} size={existing[0]['size']}), skip") continue except Exception as e: log(f" err fetch position {sym}: {e}, skip") continue # 5. set leverage 3x ok, msg = set_leverage_safe(client, sym, LEVERAGE) if not ok: log(f" set_leverage FAIL: {msg}, skip") continue log(f" leverage: {msg}") # 6. calcola current price per logging e per il payload webhook current_price = get_ticker_price(client, sym) if current_price <= 0: log(f" ticker fail per {sym}, skip") continue log(f" current_price: {current_price} (webhook applichera' sizing Charter 1500 e round_qty Bybit)") # 7. apri posizione VIA WEBHOOK try: payload = { "secret": WEBHOOK_SECRET, "symbol": sym, "side": bybit_side, "qty": 1, "price": current_price, "strategy": "rettangolo", "leverage": LEVERAGE, "timeframe": asset.get("timeframe", "120"), "comment": f"rettangolo auto {sig['signal']}", "sl_ref": sig.get("sl_ref"), "tp": sig.get("tp"), } r = requests.post(WEBHOOK_URL, json=payload, timeout=10) try: result = r.json() except Exception: result = {} if r.status_code == 200 and result.get("ok"): req_id = (result.get("request_id") or "?")[:8] log(f" WEBHOOK OK: request_id={req_id}... queued for {bybit_side} {sym}") # Aggiorna stato segnale (anti-doppia locale) signal_state[f"{sym}_{bybit_side}"] = { "side": bybit_side, "ts": now_epoch, "entry": float(sig["entry"]), "request_id": req_id, } state_dirty = True opened += 1 else: err_msg = (result.get("error") or r.text or "")[:200] log(f" WEBHOOK FAIL: status={r.status_code} err={err_msg}, skip") continue except Exception as e: log(f" webhook exception: {e}, skip") continue # Salva stato segnali se modificato if state_dirty: _save_signal_state(signal_state) log(f" signal_state saved: {len(signal_state)} entries") return opened def main_loop(): client = BybitDemoClient() log(f"=== RETTANGOLO RUNNER AVVIATO (loop {LOOP_INTERVAL_SEC}s, MANIFESTO nozionale={ORDER_VALUE_USD}, leva={LEVERAGE}, cap={MAX_OPEN_POSITIONS}, ANTI_DUP_COOLDOWN={SIGNAL_COOLDOWN_SEC}s) ===") while True: try: assets = [a for a in load_assets() if a.get("strategy", "rettangolo") == "rettangolo" and a.get("enabled", True)] if not assets: log("nessun asset rettangolo enabled nel CSV, skip") else: log(f"processo {len(assets)} asset rettangolo: {','.join(a['symbol'] for a in assets)}") run_once(client, assets) except Exception as e: log(f"loop exception: {e}") time.sleep(LOOP_INTERVAL_SEC) if __name__ == "__main__": if "--once" in sys.argv: client = BybitDemoClient() assets = [a for a in load_assets() if a.get("strategy", "rettangolo") == "rettangolo" and a.get("enabled", True)] log(f"=== RETTANGOLO RUNNER --once ({len(assets)} asset) ===") run_once(client, assets) else: main_loop()