#!/usr/bin/env python3 """DOLPHIN GREEN live status — v1 WHITE-on-DARK theme. Reads GREEN-specific HZ maps + shared infrastructure maps. Parses /tmp/green_launch.log for live engine state (LATENCY scan lines). Data source mapping: GREEN-unique : DOLPHIN_STATE_GREEN, DOLPHIN_PNL_GREEN, /tmp/green_launch.log Shared : DOLPHIN_SAFETY, DOLPHIN_META_HEALTH, DOLPHIN_FEATURES, DOLPHIN_HEARTBEAT Run: source /home/dolphin/siloqy_env/bin/activate && python dolphin_status_green.py Quit: Ctrl-C """ import json, math, os, re, threading, time, sys, urllib.request, urllib.parse from collections import deque from datetime import datetime, timezone from pathlib import Path import hazelcast # ── GREEN configuration ───────────────────────────────────────────────────────── STRATEGY = "green" STATE_MAP = "DOLPHIN_STATE_GREEN" PNL_MAP = "DOLPHIN_PNL_GREEN" SAFETY_MAP = "DOLPHIN_SAFETY" # shared META_MAP = "DOLPHIN_META_HEALTH" # shared FEAT_MAP = "DOLPHIN_FEATURES" # shared HB_MAP = "DOLPHIN_HEARTBEAT" # shared (BLUE heartbeat — reference only) GREEN_LOG = Path("/tmp/green_launch.log") # ── ClickHouse fire-and-forget write ───────────────────────────────────────── _CH_URL = "http://localhost:8123" _CH_USER = "dolphin" _CH_PASS = "dolphin_ch_2026" _CH_Q: deque = deque(maxlen=500) def _ch_worker(): while True: time.sleep(2) rows = [] while _CH_Q: try: rows.append(_CH_Q.popleft()) except IndexError: break if not rows: continue body = "\n".join(json.dumps(r) for r in rows).encode() url = f"{_CH_URL}/?database=dolphin&query=INSERT+INTO+status_snapshots+FORMAT+JSONEachRow" req = urllib.request.Request(url, data=body, method="POST") req.add_header("X-ClickHouse-User", _CH_USER) req.add_header("X-ClickHouse-Key", _CH_PASS) req.add_header("Content-Type", "application/octet-stream") try: urllib.request.urlopen(req, timeout=4) except Exception: pass threading.Thread(target=_ch_worker, daemon=True, name="ch-status-green").start() def ch_put(row: dict): _CH_Q.append(row) # ── ANSI theme ────────────────────────────────────────────────────────────────── CLEAR = "\033[2J\033[H" BOLD = "\033[1m"; DIM = "\033[2m"; RST = "\033[0m" # GREEN TUI theme — bright white primary on dark green header BW = "\033[97m" # bright white (replaces CYAN from BLUE TUI) DG = "\033[38;5;46m" # bright green accent BG_HDR = "\033[48;5;22m" # dark green background for header banner LG = "\033[38;5;82m" # light green GREEN = "\033[32m"; YELLOW = "\033[33m"; RED = "\033[31m"; CYAN = "\033[36m" ORANGE = "\033[38;5;208m" PC = {"APEX": GREEN, "STALKER": YELLOW, "TURTLE": ORANGE, "HIBERNATE": RED} SC = {"GREEN": GREEN, "DEGRADED": YELLOW, "CRITICAL": ORANGE, "DEAD": RED} # Thresholds (same algorithm — shared with BLUE) VEL_DIV_THRESHOLD = -0.020 VEL_DIV_EXTREME = -0.050 VEL_DIV_WARN = -0.010 VEL_DIV_CLOSE = -0.015 VOL_P60 = 0.00026414 BTC_VOL_WINDOW = 50 FIXED_TP_PCT = 0.0095 MAX_HOLD_BARS = 250 OB_IMBALANCE_BIAS = -0.09 START_CAP = None CAP_PEAK = None # ── Log parsing ───────────────────────────────────────────────────────────────── _RE_ANSI = re.compile(r'\x1b\[[0-9;]*m') _RE_LATENCY = re.compile( r"LATENCY scan #(\d+): bar=(\d+) vel_div=([-\d.]+) step_bar=([\d.]+)ms vol_ok=(\w+)" ) _RE_TS = re.compile(r"^(\S+Z)\s+") _RE_ENTRY = re.compile(r"ENTRY: (\{.+?\})(?:\s*\[.*\])?$") _RE_EXIT = re.compile(r"EXIT: (\{.+?\})(?:\s*\[.*\])?$") def _strip_ansi(s: str) -> str: return _RE_ANSI.sub('', s) def _parse_log_dict(raw: str) -> dict: import ast cleaned = raw.replace(": nan", ": null").replace(": inf", ": null").replace(": -inf", ": null") try: return ast.literal_eval(raw) except Exception: pass try: return json.loads(cleaned.replace("'", '"')) except Exception: raise ValueError(f"unparseable: {raw[:80]}") def _parse_green_state(n_lines=600): """Parse last N lines of GREEN log for live engine state + trade history.""" try: raw_lines = GREEN_LOG.read_text(errors="replace").splitlines()[-n_lines:] except Exception: return {}, [], [] lines = [_strip_ansi(l) for l in raw_lines] state = {} for line in lines: m = _RE_LATENCY.search(line) if m: ts_m = _RE_TS.match(line) state = { "last_scan_number": int(m.group(1)), "bar_idx": int(m.group(2)), "last_vel_div": float(m.group(3)), "step_bar_ms": float(m.group(4)), "vol_ok": m.group(5) == "True", "last_ts": ts_m.group(1) if ts_m else "", } entries = {} trades = [] for line in lines: m = _RE_ENTRY.search(line) if m: ts_m = _RE_TS.match(line) try: d = _parse_log_dict(m.group(1)) entries[d["trade_id"]] = {"ts": ts_m.group(1) if ts_m else "", **d} except Exception: pass m = _RE_EXIT.search(line) if m: ts_m = _RE_TS.match(line) try: d = _parse_log_dict(m.group(1)) tid = d.get("trade_id") if tid and tid in entries: e = entries.pop(tid) trades.append({**e, "exit_ts": ts_m.group(1) if ts_m else "", "reason": d.get("reason", "?"), "pnl_pct": d.get("pnl_pct", 0), "net_pnl": d.get("net_pnl", 0), "bars_held": d.get("bars_held", 0)}) except Exception: pass errors = [l for l in lines if re.search(r'\[ERROR\]|\[WARNING\]|\[STALE', l)][-5:] return state, trades[-5:], errors[-3:] # ── Helpers ───────────────────────────────────────────────────────────────────── def _age(ts): if not ts: return "?" if isinstance(ts, str): try: dt = datetime.fromisoformat(ts.replace("Z", "+00:00")) s = time.time() - dt.timestamp() except Exception: return "?" else: s = time.time() - ts if s < 0: return "0s" if s < 60: return f"{s:.0f}s" if s < 3600: return f"{s/60:.0f}m" return f"{s/3600:.1f}h" def _age_seconds(ts_str): try: dt = datetime.fromisoformat(ts_str.replace("Z", "+00:00")) return time.time() - dt.timestamp() except Exception: return 9999 def _bar(v, w=20): v = max(0.0, min(1.0, v)) return "\u2588" * round(v * w) + "\u2591" * (w - round(v * w)) def _get(hz, map_name, key): try: raw = hz.get_map(map_name).blocking().get(key) return json.loads(raw) if raw else {} except Exception: return {} def _item(label, color, val=""): dot = f"{color}\u25cf{RST}" v = f":{val}" if val else "" return f"{dot}{DIM}{label}{v}{RST}" def _vel_item(vel_div): v = f"{vel_div:+.4f}" if vel_div <= VEL_DIV_EXTREME: return _item("vel_div", GREEN, v) elif vel_div <= VEL_DIV_THRESHOLD: return _item("vel_div", GREEN, v) elif vel_div <= VEL_DIV_CLOSE: return _item("vel_div", YELLOW, v) elif vel_div <= VEL_DIV_WARN: return _item("vel_div", ORANGE, v) elif vel_div < 0: return _item("vel_div", RED, v) else: return _item("vel_div", RED, v) def signal_fired(vel_div, vol_ok, posture, acb_ready, exf_ok, halt): return ( vel_div <= VEL_DIV_THRESHOLD and vol_ok and posture not in ("HIBERNATE", "TURTLE") and acb_ready and exf_ok and not halt ) def trade_can_execute(open_count, lev, abs_cap, daily_loss_ok, boost): return ( open_count == 0 and lev < abs_cap and daily_loss_ok and boost > 0 ) def _best_fill_candidate(obf_universe): candidates = [] for k, v in obf_universe.items(): if not isinstance(v, dict) or "fill_probability" not in v: continue candidates.append((k, v)) if not candidates: return None, {} def score(item): sym, a = item imb = float(a.get("imbalance", 0)) fp = float(a.get("fill_probability", 0)) sp = float(a.get("spread_bps", 99)) dq = float(a.get("depth_quality", 0)) imb_bonus = max(0.0, -imb) return fp * (1 + imb_bonus) * dq / max(0.1, sp) candidates.sort(key=score, reverse=True) return candidates[0] # ── Gear rows ────────────────────────────────────────────────────────────────── def gear_rows(gstate, safe, acb, exf, hb, obf_universe=None): vel_div = float(gstate.get("last_vel_div", 0) or 0) vol_ok = bool(gstate.get("vol_ok", False)) posture = safe.get("posture") or gstate.get("posture") or "?" halt = posture in ("HIBERNATE", "TURTLE") acb_boost_val = float(acb.get("boost", acb.get("cut", 0)) or 0) acb_ready = acb_boost_val > 0 exf_ok_count = int(exf.get("_ok_count", 0) if exf else 0) exf_ok = exf_ok_count >= 3 hb_ts = hb.get("ts") hb_ok = bool(hb_ts and (time.time() - hb_ts) < 30) # ── SIGNAL ROW ──────────────────────────────────────────────────────────── s_items = [] btc_vol_str = "\u2014" if exf: dvol_raw = exf.get("dvol_btc") or exf.get("dvol") fng_raw = exf.get("fng") if dvol_raw: btc_vol_str = f"dV:{float(dvol_raw):.0f}" if fng_raw: btc_vol_str += f" FnG:{float(fng_raw):.0f}" vol_label = f"vol_ok({btc_vol_str})" s_items.append(_item(vol_label, GREEN if vol_ok else RED, "\u2713" if vol_ok else "\u2717 BLOCKED")) s_items.append(_vel_item(vel_div)) pc = PC.get(posture, DIM) s_items.append(_item("posture", GREEN if posture == "APEX" else (YELLOW if posture == "STALKER" else RED), posture)) s_items.append(_item("acb", GREEN if acb_ready else (ORANGE if acb_boost_val > 0 else RED), f"{acb_boost_val:.2f}")) s_items.append(_item("exf", GREEN if exf_ok else (YELLOW if exf_ok_count >= 1 else RED), f"{exf_ok_count}/5")) s_items.append(_item("no_halt", GREEN if not halt else RED, "\u2713" if not halt else "HALT")) s_items.append(_item("hb", GREEN if hb_ok else RED, _age(hb_ts))) all_sig = signal_fired(vel_div, vol_ok, posture, acb_ready, exf_ok, halt) if all_sig: s_items.append(f" {DG}{BOLD}\u25c9 SIGNAL{RST}") # ── TRADE ROW ───────────────────────────────────────────────────────────── t_items = [] t_items.append(_item("paper", DG, "Nautilus")) t_items.append(_item("regime", GREEN if not halt else RED, "free" if not halt else "HALTED")) rm = float(safe.get("Rm", 0) or 0) t_items.append(_item("Rm", GREEN if rm >= 0.90 else (YELLOW if rm >= 0.70 else (ORANGE if rm >= 0.50 else RED)), f"{rm:.3f}")) c5 = float((safe.get("breakdown") or {}).get("Cat5", 1.0) or 1.0) t_items.append(_item("Cat5", GREEN if c5 >= 0.95 else (YELLOW if c5 >= 0.85 else (ORANGE if c5 >= 0.70 else RED)), f"{c5:.3f}")) bar_idx = int(gstate.get("bar_idx", 0) or 0) t_items.append(_item("bar", DIM, str(bar_idx))) all_trade = all_sig and trade_can_execute(0, 0.0, 8.0, c5 > 0.50, acb_boost_val) if all_trade: t_items.append(f" {DG}{BOLD}\u25c9 TRADE{RST}") sig_row = " ".join(s_items) trade_row = " ".join(t_items) # ── FILL ROW ────────────────────────────────────────────────────────────── f_items = [] n_assets = int(obf_universe.get("_n_assets", 0) if obf_universe else 0) n_stale = int(obf_universe.get("_n_stale", 0) if obf_universe else 0) n_fresh = n_assets - n_stale f_items.append(_item("universe", GREEN if n_fresh >= 200 else (YELLOW if n_fresh >= 50 else RED), f"{n_fresh}/{n_assets}")) sym, ab = _best_fill_candidate(obf_universe or {}) if sym: fill_p = float(ab.get("fill_probability", 0)) spread = float(ab.get("spread_bps", 99)) dq = float(ab.get("depth_quality", 0)) imb = float(ab.get("imbalance", 0)) depth = float(ab.get("depth_1pct_usd", 0)) asset_color = GREEN if fill_p >= 0.80 else (YELLOW if fill_p >= 0.50 else RED) f_items.append(_item("best", asset_color, sym[:6])) f_items.append(_item("fill_p", GREEN if fill_p >= 0.85 else (YELLOW if fill_p >= 0.60 else RED), f"{fill_p:.2f}")) f_items.append(_item("spread", GREEN if spread <= 3 else (YELLOW if spread <= 8 else RED), f"{spread:.1f}bps")) f_items.append(_item("depth_q", GREEN if dq >= 0.5 else (YELLOW if dq >= 0.1 else RED), f"{dq:.2f}")) imb_ok = imb < OB_IMBALANCE_BIAS f_items.append(_item("imb", GREEN if imb_ok else YELLOW if imb < 0 else ORANGE if imb < 0.1 else RED, f"{imb:+.2f}")) f_items.append(_item("depth", GREEN if depth >= 50_000 else (YELLOW if depth >= 10_000 else RED), f"${depth/1000:.0f}k")) else: f_items.append(_item("OBF", RED, "no data")) boost = float(acb.get("boost", 1.0) if acb else 1.0) beta = float(acb.get("beta", 0.8) if acb else 0.8) f_items.append(_item("acb_boost", GREEN if boost >= 1.5 else (YELLOW if boost >= 1.0 else ORANGE), f"\u00d7{boost:.2f}")) f_items.append(_item("beta", GREEN if beta >= 0.7 else (YELLOW if beta >= 0.4 else RED), f"{beta:.2f}")) f_items.append(f" {DIM}(paper: Nautilus internal){RST}") fill = " ".join(f_items) return sig_row, trade_row, fill # ── Render ────────────────────────────────────────────────────────────────────── def render(hz): global START_CAP, CAP_PEAK eng = _get(hz, STATE_MAP, "engine_snapshot") cap = _get(hz, STATE_MAP, "capital_checkpoint") safe = _get(hz, SAFETY_MAP, "latest") hb = _get(hz, HB_MAP, "nautilus_flow_heartbeat") mh = _get(hz, META_MAP, "latest") acb = _get(hz, FEAT_MAP, "acb_boost") exf = _get(hz, FEAT_MAP, "exf_latest") obf = _get(hz, FEAT_MAP, "obf_universe_latest") # Log parsing for liveness + trade history (engine_snapshot is primary for state) glog_state, trades_hist, log_errors = _parse_green_state(600) now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") capital = float(eng.get("capital", 0) or cap.get("capital", 0)) posture = safe.get("posture") or eng.get("posture") or "?" rm = float(safe.get("Rm", 0) or 0) hb_ts = hb.get("ts") phase = hb.get("phase", "?") trader_up = hb_ts and (time.time() - hb_ts) < 30 scans = eng.get("scans_processed", "\u2014") bar_idx = eng.get("bar_idx", "\u2014") vel_div = float(eng.get("last_vel_div", 0) or 0) vol_ok = bool(eng.get("vol_ok", False)) trades_ex = int(eng.get("trades_executed", 0) or 0) lev = float(eng.get("current_leverage", 0) or 0) notional= float(eng.get("open_notional", 0) or 0) mhs_st = mh.get("status", "?") rm_meta = float(mh.get("rm_meta", 0) or 0) snap_ts = eng.get("timestamp", "") # Liveness from log (engine_snapshot may lag a few seconds) log_ts_str = glog_state.get("last_ts", "") log_age = _age(log_ts_str) if log_ts_str else "?" green_alive = log_ts_str and _age_seconds(log_ts_str) < 30 step_ms = float(glog_state.get("step_bar_ms", 0) or 0) if capital > 0: if START_CAP is None: START_CAP = capital if CAP_PEAK is None or capital > CAP_PEAK: CAP_PEAK = capital roi = ((capital - START_CAP) / START_CAP * 100) if START_CAP and capital else 0 dd = ((CAP_PEAK - capital) / CAP_PEAK * 100) if CAP_PEAK and capital < CAP_PEAK else 0 pc = PC.get(posture, DIM) sc = SC.get(mhs_st, DIM) tc = DG if green_alive else RED roi_c = GREEN if roi >= 0 else RED dd_c = RED if dd > 15 else (YELLOW if dd > 5 else GREEN) sig_row, trade_row, fill_row_str = gear_rows(eng, safe, acb, exf, hb, obf) svc = mh.get("service_status", {}) hz_ks = mh.get("hz_key_status", {}) L = [] # HEADER — bright white on dark green background L.append( f"{BG_HDR}{BW}{BOLD} \U0001f42c DOLPHIN-GREEN {RST}" f" {BW}{BOLD}v1{RST} {DIM}{now}{RST} {DG}\u25cf STRATEGY=green{RST}" ) L.append("\u2501" * 70) # GREEN PROCESS L.append( f"{BW}{BOLD}GREEN{RST} {tc}{'\u25cf LIVE' if green_alive else '\u25cf DOWN'}{RST}" f" log_age:{log_age}" f" scan:#{scans}" f" step:{step_ms:.1f}ms" ) L.append(f" {DIM}[shared] BLUE hb:{_age(hb_ts)} phase:{phase}{RST}") # SIGNAL → FILL GEARS if not vol_ok: L.append( f" {RED}{BOLD}\u26d4 VOL_OK=FALSE \u2014 engine gate closed," f" NO trades until BTC vol > {VOL_P60:.6f}{RST}" ) L.append(f" {DIM}SIG \u2502{RST} {sig_row}") L.append(f" {DIM}TRD \u2502{RST} {trade_row}") L.append(f" {DIM}FIL \u2502{RST} {fill_row_str}") L.append("") # CAPITAL L.append( f"{BW}{BOLD}CAPITAL{RST} {BW}${capital:,.2f}{RST}" + (f" ROI:{roi_c}{roi:+.2f}%{RST} DD:{dd_c}{dd:.2f}%{RST}" f" start:${START_CAP:,.0f}" if START_CAP else "") ) L.append( f" trades:{trades_ex} scans:{scans} bar:{bar_idx}" f" lev:{lev:.2f}x notional:${notional:,.0f}" ) # Open positions (from engine_snapshot — same structure as BLUE) positions = eng.get("open_positions") or [] if positions: L.append(f" {BW}{BOLD}OPEN:{RST}") for p in positions: sc2 = GREEN if p.get("side") == "LONG" else RED L.append(f" {sc2}{p.get('asset','?')} {p.get('side','?')}{RST}" f" qty:{p.get('quantity',0):.4f}" f" entry:{p.get('entry_price',0):.2f}" f" upnl:{p.get('unrealized_pnl',0):+.2f}") else: L.append(f" {DIM}no open positions{RST}") L.append("") # POSTURE (shared map) bd = safe.get("breakdown") or {} L.append( f"{BW}{BOLD}POSTURE{RST} {pc}{posture}{RST}" f" Rm:{pc}{_bar(rm,20)}{RST} {rm:.4f} {DIM}[shared]{RST}" ) cats = " ".join(f"C{i}:{float(bd.get(f'Cat{i}',0)):.2f}" for i in range(1,6)) L.append( f" {cats} f_env:{float(bd.get('f_env',0)):.3f}" f" f_exe:{float(bd.get('f_exe',0)):.3f}" ) L.append("") # SYS HEALTH (shared map) L.append( f"{BW}{BOLD}SYS HEALTH{RST} {sc}{mhs_st}{RST}" f" rm_meta:{rm_meta:.3f} {DIM}[shared]{RST}" ) for m in ("m1_data_infra","m1_trader","m2_heartbeat", "m3_data_freshness","m4_control_plane","m5_coherence"): v = float(mh.get(m, 0) or 0) c = GREEN if v >= 0.9 else (YELLOW if v >= 0.5 else RED) L.append(f" {c}{m}:{v:.3f}{RST}") L.append(f" {DIM}services:{RST} " + " ".join( f"{'\u25cf' if st=='RUNNING' else f'{RED}\u25cf{RST}'}{DIM}{n.split(':')[-1]}{RST}" if st == "RUNNING" else f"{RED}\u25cf{DIM}{n.split(':')[-1]}{RST}" for n, st in sorted(svc.items()))) L.append(f" {DIM}hz_keys:{RST} " + " ".join( f"{GREEN if float(i.get('score',0))>=0.9 else (YELLOW if float(i.get('score',0))>=0.5 else RED)}\u25cf{RST}{DIM}{k}{RST}" for k, i in sorted(hz_ks.items()))) # LAST TRADES (from GREEN log) if trades_hist: L.append("") L.append(f"{BW}{BOLD}LAST TRADES{RST} {DIM}(from GREEN log){RST}") for t in trades_hist: pnl = float(t.get("net_pnl", 0)) _not = float(t.get("notional", 0)) pct = (pnl / _not * 100) if _not else float(t.get("pnl_pct", 0)) * 100 lev = float(t.get("leverage", 0)) ep = float(t.get("entry_price", 0)) reason = t.get("reason", "?") asset = t.get("asset", "?") bars = t.get("bars_held", 0) ts_raw = t.get("ts", "")[:16].replace("T", " ") pc2 = GREEN if pnl >= 0 else RED L.append( f" {pc2}{'\u25b2' if pnl>=0 else '\u25bc'}{RST}" f" {asset:<12} " f"ep:{ep:.4g} " f"lev:{lev:.2f}x " f"pnl:{pc2}{pnl:+.2f}({pct:+.2f}%){RST} " f"exit:{reason} bars:{bars} {DIM}{ts_raw}{RST}" ) else: L.append(f" {DIM}no completed trades yet (GREEN paper mode){RST}") # LOG ERRORS if log_errors: L.append("") L.append(f"{RED}{BOLD}LOG WARNINGS{RST}") for e in log_errors: L.append(f" {RED}{e[-120:]}{RST}") # PNL HISTORY try: pnl_map = hz.get_map(PNL_MAP).blocking() pnl_keys = sorted(pnl_map.key_set()) if pnl_keys: L.append("") L.append(f"{BW}{BOLD}PNL HISTORY{RST} {DIM}({PNL_MAP}){RST}") for k in pnl_keys[-7:]: v_raw = pnl_map.get(k) if v_raw: v = json.loads(v_raw) ec = float(v.get("engine_capital", 0) or 0) L.append(f" {DIM}{k}{RST} capital:${ec:,.2f}") except Exception: pass L.append("") L.append(f"{DIM}GREEN v1 \u2022 0.5s poll \u2022 CH\u2192status_snapshots \u2022 Ctrl-C quit{RST}") # CH persistence if int(time.time() * 2) % 2 == 0: ch_put({ "ts": int(time.time() * 1000), "strategy": STRATEGY, "capital": capital, "roi_pct": round(roi, 4), "dd_pct": round(dd, 4), "trades_executed": trades_ex, "scans_processed": int(eng.get("scans_processed", 0) or 0), "bar_idx": int(eng.get("bar_idx", 0) or 0), "posture": posture, "rm": round(rm, 6), "vel_div": round(vel_div, 6), "vol_ok": 1 if vol_ok else 0, "phase": phase, "mhs_status": mhs_st, "boost": round(float(acb.get("boost", 1.0) if acb else 1.0), 4), "cat5": round(float((safe.get("breakdown") or {}).get("Cat5", 1.0) or 1.0), 6), "step_bar_ms": round(step_ms, 2), }) return "\n".join(L) def main(): print("Connecting to HZ...") hz = hazelcast.HazelcastClient( cluster_name="dolphin", cluster_members=["localhost:5701"], connection_timeout=5.0) print("Connected.\n") try: while True: try: sys.stdout.write(CLEAR + render(hz) + "\n") sys.stdout.flush() except Exception as e: sys.stdout.write(f"\n{RED}render error: {e}{RST}\n") sys.stdout.flush() time.sleep(0.5) except KeyboardInterrupt: print(f"\n{DIM}Bye.{RST}") finally: hz.shutdown() if __name__ == "__main__": main()