""" 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) - 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 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) # === REFACTOR 2026-07-20 (Mavis) — Rettangolo passa per il webhook 5580 === # Cosi' le regole Charter (P006 SL -3%, P007 EMA50 regime, sizing 1500, round_qty) si applicano # anche a Rettangolo. Niente piu' bybit.create_market_order diretto. WEBHOOK_URL = "http://127.0.0.1:5580/webhook" WEBHOOK_SECRET = "TV_2026_MATTIA_DEMO" # Path monitor per leggere l'ultimo range daily visualizzato (no, lo calcoliamo noi) LOG_FILE = r"/opt/charter-live/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 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: # leverage already at target 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 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] # P010 Charter fix 2026-07-18: valuta SEMPRE candela chiusa [-2], MAI [-1] (in formazione). # Bybit API restituisce candele fino a quella in formazione; intraday[-1] e' viva, intraday[-2] e' chiusa. if len(intraday) < 3: log(f" {sym}: klines insufficienti per valutare candela chiusa (intra={len(intraday)})") continue idx_closed = len(intraday) - 2 # Pattern mode: "strict" = classico, "relaxed" = rilassato MARTELLO+SPINTA v3.7 # Backtest 14gg (20/07): relaxed vince su SOL/ETH/ZEC, strict vince su BTC/WIF pattern_mode = asset.get("pattern_mode", "relaxed") allow_martello_spinta = (pattern_mode == "relaxed") log(f" {sym}: evaluating CLOSED candle idx={idx_closed} ts={intraday[idx_closed].get('time', '?')} (current live candle skipped) 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 # niente segnale, silenzio log(f" {sym}: SEGNALE {sig['signal']} entry={sig['entry']:.4f} sl={sig['sl']:.4f} tp={sig['tp']:.4f}") # 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 (regole Charter applicate: P006 SL -3%, P007 EMA50 regime, # sizing 1500 USDT, round_qty Bybit). bypass del bybit diretto. bybit_side = "Buy" if sig["signal"] == "LONG" else "Sell" try: payload = { "secret": WEBHOOK_SECRET, "symbol": sym, "side": bybit_side, "qty": 1, # dummy: webhook farà sizing 1500 USDT Charter "price": current_price, "strategy": "rettangolo", "leverage": LEVERAGE, "timeframe": asset.get("timeframe", "120"), "comment": f"rettangolo auto {sig['signal']}", "sl_ref": sig.get("sl_ref"), # Mavis 2026-07-20 21:42: pattern bottom/top per ZONA-MID check 1.5% "tp": sig.get("tp"), # TP al mid range (info aggiuntiva) } 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}") 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 # 8. SL/TP setting rimosso: ora lo fa il webhook (Charter P006 -3% + P005 +5%) 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}) ===") while True: try: # Ricarica asset list da CSV ad ogni iterazione (modifiche riflesse in 60s) 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()