"""
Monitor visuale INTRADAY 2H per la strategia RETTANGOLO (breakout + retest).
- Range: high/low candela daily precedente (rettangolo blu)
- Candele: 2H intraday (ultimi 7 giorni)
- Segnale live: candela 2H corrente, se c'è breakout precedente nelle ultime 10 candele
e la corrente tocca il livello rotto con pattern di inversione -> entry
- Auto-refresh: rigenera ogni 60s
"""
import sys
import time
import json
import urllib.request
import urllib.parse
from datetime import datetime, timezone
from pathlib import Path
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from rettangolo_strategy import compute_signal, scan_signals
OUT_HTML = Path(r"G:\AI TRADING ENGINE\live_deploy\bt_rettangolo\monitor.html")
ASSETS = ["SOLUSDT", "BTCUSDT", "ETHUSDT", "WIFUSDT"]
def fetch_klines(symbol, interval, days):
end_ms = int(time.time() * 1000)
start_ms = end_ms - days * 24 * 3600 * 1000
params = {"category": "linear", "symbol": symbol, "interval": interval, "start": start_ms, "end": end_ms, "limit": 200}
qs = urllib.parse.urlencode(params)
url = f"https://api.bybit.com/v5/market/kline?{qs}"
with urllib.request.urlopen(url, timeout=15) as r:
data = json.loads(r.read().decode("utf-8"))
rows = data.get("result", {}).get("list", [])
rows.sort(key=lambda x: int(x[0]))
klines = []
for r in rows:
klines.append({
"ts": int(r[0]),
"date": datetime.fromtimestamp(int(r[0])/1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M"),
"open": float(r[1]),
"high": float(r[2]),
"low": float(r[3]),
"close": float(r[4]),
"volume": float(r[5])
})
return klines
def build_figure(symbol, intraday_klines, daily_klines):
if len(intraday_klines) < 5 or len(daily_klines) < 2:
return None
# Filtra: solo candele 2H del giorno precedente (ieri) e del giorno in osservazione (oggi)
# giorno precedente = candela daily[-2], giorno in osservazione = candela daily[-1]
cur_day = daily_klines[-1]
prev_day = daily_klines[-2]
cur_day_start = cur_day["ts"]
prev_day_start = prev_day["ts"]
filtered = [k for k in intraday_klines if prev_day_start <= k["ts"] <= cur_day_start + 24*3600*1000]
if len(filtered) < 3:
filtered = intraday_klines # fallback se filtro troppo aggressivo
prev_daily = prev_day
# RANGE dal giorno precedente (daily[-2]): high/low di ieri
rng_top = prev_daily["high"]
rng_bot = prev_daily["low"]
rng_mid = (rng_top + rng_bot) / 2
rng_pct = (rng_top - rng_bot) / rng_bot * 100
# candele 2H (solo ieri + oggi)
dates = [k["date"] for k in filtered]
opens = [k["open"] for k in filtered]
highs = [k["high"] for k in filtered]
lows = [k["low"] for k in filtered]
closes = [k["close"] for k in filtered]
# split: ieri (linea continua) vs oggi (linea tratteggiata)
today_start = cur_day["ts"]
idx_today = next((i for i, k in enumerate(filtered) if k["ts"] >= today_start), len(filtered))
yesterday = filtered[:idx_today]
today = filtered[idx_today:]
# segnale live
sig = compute_signal(prev_daily, filtered, len(filtered) - 1)
sig_text = "NESSUN SETUP - in attesa breakout + retest"
sig_color = "gray"
if sig:
sig_text = ">>> " + sig["signal"] + " ENTRY @ " + format(sig["entry"], ".4f") + " | SL=" + format(sig["sl"], ".4f") + " | TP=" + format(sig["tp"], ".4f") + " <<<"
sig_color = "#4caf50" if sig["signal"] == "LONG" else "#ef5350"
historical = scan_signals(prev_daily, filtered)
historical = historical[-5:]
# figura
fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.05, row_heights=[0.75, 0.25])
fig.add_trace(go.Candlestick(
x=[k["date"] for k in yesterday], open=[k["open"] for k in yesterday],
high=[k["high"] for k in yesterday], low=[k["low"] for k in yesterday],
close=[k["close"] for k in yesterday],
name=symbol + " 2H IERI", increasing_line_color="#26a69a", decreasing_line_color="#ef5350"
), row=1, col=1)
fig.add_trace(go.Candlestick(
x=[k["date"] for k in today], open=[k["open"] for k in today],
high=[k["high"] for k in today], low=[k["low"] for k in today],
close=[k["close"] for k in today],
name=symbol + " 2H OGGI (in formazione)", increasing_line_color="#80cbc4", decreasing_line_color="#ef9a9a",
opacity=0.5
), row=1, col=1)
# rettangolo tratteggiato del range daily precedente
fig.add_shape(
type="rect", x0=dates[0], x1=dates[-1], y0=rng_bot, y1=rng_top,
line=dict(color="rgba(33,150,243,0.4)", width=1, dash="dot"), fillcolor="rgba(33,150,243,0.06)",
name="Range " + prev_daily["date"][:10]
)
# LINEE TRATTEGGIATE TOP/BOTTOM: prosecuzione del min/max del giorno precedente, estese su tutto il chart
fig.add_shape(
type="line", x0=dates[0], x1=dates[-1], y0=rng_top, y1=rng_top,
line=dict(color="rgba(33,150,243,1.0)", width=2.5, dash="dash"),
name="TOP " + format(rng_top, ".4f")
)
fig.add_shape(
type="line", x0=dates[0], x1=dates[-1], y0=rng_bot, y1=rng_bot,
line=dict(color="rgba(33,150,243,1.0)", width=2.5, dash="dash"),
name="BOTTOM " + format(rng_bot, ".4f")
)
# linea MID (target TP)
fig.add_shape(
type="line", x0=dates[0], x1=dates[-1], y0=rng_mid, y1=rng_mid,
line=dict(color="rgba(255,152,0,1.0)", width=2, dash="dot"),
name="MID " + format(rng_mid, ".4f")
)
for k in filtered:
if k["close"] > rng_top:
fig.add_trace(go.Scatter(
x=[k["date"]], y=[k["close"]], mode="markers",
marker=dict(symbol="triangle-up", size=8, color="#4caf50", opacity=0.6),
showlegend=False, hoverinfo="skip"
), row=1, col=1)
elif k["close"] < rng_bot:
fig.add_trace(go.Scatter(
x=[k["date"]], y=[k["close"]], mode="markers",
marker=dict(symbol="triangle-down", size=8, color="#ef5350", opacity=0.6),
showlegend=False, hoverinfo="skip"
), row=1, col=1)
# etichette prezzi lato destro
fig.add_annotation(
xref="paper", x=0.985, y=rng_top, text="BUY " + format(rng_top, ".4f"),
showarrow=False, xanchor="left", yanchor="middle",
font=dict(color="#ef5350", size=14, family="monospace"),
bgcolor="rgba(239,83,80,0.15)", bordercolor="#ef5350", borderwidth=1, borderpad=4
)
fig.add_annotation(
xref="paper", x=0.985, y=rng_bot, text="SELL " + format(rng_bot, ".4f"),
showarrow=False, xanchor="left", yanchor="middle",
font=dict(color="#4caf50", size=14, family="monospace"),
bgcolor="rgba(76,175,80,0.15)", bordercolor="#4caf50", borderwidth=1, borderpad=4
)
fig.add_annotation(
xref="paper", x=0.985, y=rng_mid, text="TP " + format(rng_mid, ".4f"),
showarrow=False, xanchor="left", yanchor="middle",
font=dict(color="#ff9800", size=14, family="monospace"),
bgcolor="rgba(255,152,0,0.15)", bordercolor="#ff9800", borderwidth=1, borderpad=4
)
for s in historical:
color = "#4caf50" if s["signal"] == "LONG" else "#ef5350"
symbol_arrow = "triangle-up" if s["signal"] == "LONG" else "triangle-down"
fig.add_trace(go.Scatter(
x=[s["date"]], y=[s["entry"]], mode="markers",
marker=dict(symbol=symbol_arrow, size=12, color=color, line=dict(width=2, color="white")),
showlegend=False, hoverinfo="text",
hovertext=s["signal"] + " @ " + format(s["entry"], ".4f")
), row=1, col=1)
colors = ["#26a69a" if closes[i] >= opens[i] else "#ef5350" for i in range(len(closes))]
fig.add_trace(go.Bar(x=dates, y=[k["volume"] for k in filtered], marker_color=colors, name="Volume", showlegend=False), row=2, col=1)
fig.update_layout(
title="" + symbol + " • Range " + prev_daily["date"][:10] + ": " + format(rng_bot, ".4f") + " — " + format(rng_top, ".4f") + " (" + format(rng_pct, ".2f") + "%)
"
"" + sig_text + "",
template="plotly_dark", height=700, width=1500,
xaxis=dict(dtick=7200000, tickformat="%H:%M\n%b %d"),
xaxis2=dict(dtick=7200000, tickformat="%H:%M\n%b %d"),
xaxis_rangeslider_visible=False,
xaxis2_rangeslider_visible=False,
legend=dict(orientation="h", y=1.08, x=0.5, xanchor="center"),
)
return fig
def build_html(per_symbol_data, generated_at):
figs = {}
for sym, data in per_symbol_data.items():
fig = build_figure(sym, data["intraday"], data["daily"])
if fig:
figs[sym] = fig
if not figs:
return "
Generato: """ + generated_at + """ | Auto-refresh: ogni 60s
Range blu = high/low candela daily precedente • Linea arancione = MID (target TP)
▲ verde / ▼ rosso sui bordi = breakout (candela 2H chiusa oltre range) • Etichette sui tocchi = segnali di retest confermati
LOGICA: breakout candela 2H (close oltre range) → candela successiva tocca livello rotto + Doji/Hammer → entry live