The mid-band advisory label (constituent signals in conflict) was called NEUTRAL, implying "benign middle" — but retrospective data (637 trades) shows it is empirically the worst-ROI regime. Renaming to UNKNOWN makes the semantics explicit for regime-gate consumers. - esof_advisor.py: emits UNKNOWN; LABEL_COLOR keeps NEUTRAL alias for historical CH rows / stale HZ snapshots - esof_gate.py: S6_MULT, IRP_PARAMS, Strategy A mult_map all keyed on UNKNOWN with NEUTRAL alias (values identical → replays unaffected) - prod/docs/ESOF_LABEL_MIGRATION.md: migration note, CH/HZ impact, rollback procedure Plan ref: Task 4 — NEUTRAL→UNKNOWN is load-bearing for the EsoF gate in the orchestrator (0.25× sizing vs 1.0× under old label semantics). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
483 lines
22 KiB
Python
Executable File
483 lines
22 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
DOLPHIN EsoF Advisory — v2.0 (2026-04-19)
|
||
==========================================
|
||
Advisory-only (NOT wired into BLUE engine).
|
||
|
||
Computes esoteric/calendar/session factors every 15s and:
|
||
- Writes to HZ DOLPHIN_FEATURES['esof_advisor_latest']
|
||
- Writes to CH dolphin.esof_advisory (fire-and-forget)
|
||
- Stdout: live display (run standalone or import get_advisory())
|
||
|
||
Expectancy tables derived from 637 live trades (2026-03-31 → 2026-04-19).
|
||
Update these tables periodically as more data accumulates.
|
||
|
||
Weighted hours: uses MarketIndicators.get_weighted_times() from
|
||
external_factors/esoteric_factors_service.py (requires astropy).
|
||
Falls back to UTC-based approximation if astropy not available.
|
||
|
||
Run: source /home/dolphin/siloqy_env/bin/activate && python esof_advisor.py
|
||
"""
|
||
import json
|
||
import math
|
||
import sys
|
||
import time
|
||
import threading
|
||
import urllib.request
|
||
from datetime import datetime, timezone
|
||
from pathlib import Path
|
||
|
||
# ── MarketIndicators integration (real weighted hours) ────────────────────────
|
||
_EF_PATH = Path(__file__).parent.parent / "external_factors"
|
||
if str(_EF_PATH) not in sys.path:
|
||
sys.path.insert(0, str(_EF_PATH))
|
||
|
||
try:
|
||
from esoteric_factors_service import MarketIndicators as _MI
|
||
_market_indicators = _MI()
|
||
_WEIGHTED_HOURS_AVAILABLE = True
|
||
except Exception:
|
||
_market_indicators = None
|
||
_WEIGHTED_HOURS_AVAILABLE = False
|
||
|
||
def _get_weighted_hours(now: datetime):
|
||
"""Returns (pop_hour, liq_hour). Falls back to UTC approximation."""
|
||
if _WEIGHTED_HOURS_AVAILABLE:
|
||
return _market_indicators.get_weighted_times(now)
|
||
# Fallback: pop≈UTC+4.2, liq≈UTC+1.0 (empirical offsets)
|
||
h = now.hour + now.minute / 60.0
|
||
return ((h + 4.21) % 24), ((h + 0.98) % 24)
|
||
|
||
# ── Expectancy tables (from CH analysis, 637 trades) ──────────────────────────
|
||
# Key: liq_hour 3h bucket start → (trades, wr_pct, net_pnl, avg_pnl)
|
||
# liq_hour ≈ UTC + 0.98h (liquidity-weighted centroid: Americas 35%, EMEA 30%,
|
||
# East_Asia 20%, Oceania_SEA 10%, South_Asia 5%)
|
||
LIQ_HOUR_STATS = {
|
||
0: ( 70, 51.4, +1466, +20.9), # liq 0-3h ≈ UTC 23-2h (Asia open)
|
||
3: ( 73, 46.6, -1166, -16.0), # liq 3-6h ≈ UTC 2-5h (deep Asia)
|
||
6: ( 62, 41.9, +1026, +16.5), # liq 6-9h ≈ UTC 5-8h (Asia/EMEA handoff)
|
||
9: ( 65, 43.1, +476, +7.3), # liq 9-12h ≈ UTC 8-11h (EMEA morning)
|
||
12: ( 84, 52.4, +3532, +42.0), # liq 12-15h ≈ UTC 11-14h (EMEA pm + US open) ★ BEST
|
||
15: (113, 43.4, -770, -6.8), # liq 15-18h ≈ UTC 14-17h (US morning)
|
||
18: ( 99, 35.4, -2846, -28.8), # liq 18-21h ≈ UTC 17-20h (US afternoon) ✗ WORST
|
||
21: ( 72, 36.1, -1545, -21.5), # liq 21-24h ≈ UTC 20-23h (US close/late)
|
||
}
|
||
|
||
# Key: session name → (trades, wr_pct, net_pnl, avg_pnl)
|
||
SESSION_STATS = {
|
||
"LONDON_MORNING": (111, 47.7, +4132.94, +37.23),
|
||
"ASIA_PACIFIC": (182, 46.7, +1600.04, +8.79),
|
||
"LOW_LIQUIDITY": ( 71, 39.4, -809.19, -11.40),
|
||
"LN_NY_OVERLAP": (147, 45.6, -894.86, -6.09),
|
||
"NY_AFTERNOON": (127, 35.4, -3857.09, -30.37),
|
||
}
|
||
|
||
# Key: dow (0=Mon) → (trades, wr_pct, net_pnl, avg_pnl)
|
||
DOW_STATS = {
|
||
0: (81, 27.2, -1053.91, -13.01), # Mon — worst
|
||
1: (77, 54.5, +3823.81, +49.66), # Tue — best
|
||
2: (98, 43.9, -385.08, -3.93), # Wed
|
||
3: (115, 44.3, -4017.06, -34.93), # Thu — 2nd worst
|
||
4: (106, 39.6, -1968.41, -18.57), # Fri
|
||
5: (82, 43.9, +43.37, +0.53), # Sat
|
||
6: (78, 53.8, +3729.73, +47.82), # Sun — 2nd best
|
||
}
|
||
DOW_NAMES = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
||
|
||
# DoW × Session notable extremes (subset — key cells only)
|
||
# Format: (dow, session) → (trades, wr_pct, net_pnl)
|
||
DOW_SESSION_STATS = {
|
||
(6, "LONDON_MORNING"): (13, 85.0, +2153), # Sun LDN — best cell
|
||
(6, "LN_NY_OVERLAP"): (24, 75.0, +2110), # Sun OVLP — 2nd best
|
||
(1, "ASIA_PACIFIC"): (27, 67.0, +2522), # Tue ASIA — 3rd
|
||
(1, "LN_NY_OVERLAP"): (18, 56.0, +2260), # Tue OVLP — 4th
|
||
(6, "NY_AFTERNOON"): (17, 6.0, -1025), # Sun NY — worst cell
|
||
(0, "ASIA_PACIFIC"): (21, 19.0, -411), # Mon ASIA — bad
|
||
(3, "LN_NY_OVERLAP"): (27, 41.0, -3310), # Thu OVLP — catastrophic
|
||
}
|
||
|
||
# 15m slot stats: slot_key → (trades, wr_pct, net_pnl, avg_pnl)
|
||
# Only slots with n >= 5 included
|
||
SLOT_STATS = {
|
||
"0:00": (7, 57.1, +32.52, +4.65),
|
||
"0:15": (5, 80.0,+103.19, +20.64),
|
||
"0:30": (6, 33.3,-203.12, -33.85),
|
||
"1:00": (7, 42.9,-271.32, -38.76),
|
||
"1:30": (10, 50.0,+1606.66,+160.67),
|
||
"1:45": (5, 80.0, +458.74, +91.75),
|
||
"2:00": (8, 62.5,-214.45, -26.81),
|
||
"2:15": (5, 0.0, -851.56,-170.31),
|
||
"2:30": (7, 57.1,-157.04, -22.43),
|
||
"2:45": (7, 57.1, +83.24, +11.89),
|
||
"3:00": (8, 50.0, +65.0, +8.13),
|
||
"3:30": (7, 14.3,-230.05, -32.86),
|
||
"4:00": (8, 37.5, +38.73, +4.84),
|
||
"4:15": (5, 60.0, +525.75,+105.15),
|
||
"4:30": (6, 50.0, +221.14, +36.86),
|
||
"4:45": (7, 28.6,-777.03,-111.00),
|
||
"5:00": (5, 40.0,-120.47, -24.09),
|
||
"5:15": (4, 50.0, +559.32,+139.83),
|
||
"5:30": (5, 40.0, +345.88, +69.18),
|
||
"5:45": (5, 40.0,-1665.24,-333.05),
|
||
"6:00": (5, 80.0, +635.74,+127.15),
|
||
"6:30": (5, 60.0,-191.66, -38.33),
|
||
"6:45": (8, 37.5, +325.97, +40.75),
|
||
"7:15": (7, 42.9, +763.60,+109.09),
|
||
"7:30": (5, 20.0,-162.27, -32.45),
|
||
"7:45": (6, 66.7, -18.42, -3.07),
|
||
"8:00": (5, 40.0, +10.23, +2.05),
|
||
"8:15": (5, 20.0, -31.26, -6.25),
|
||
"8:30": (5, 40.0, -69.76, -13.95),
|
||
"8:45": (6, 50.0, +302.53, +50.42),
|
||
"9:00": (5, 60.0, -62.44, -12.49),
|
||
"9:15": (6, 66.7, +81.85, +13.64),
|
||
"9:30": (5, 20.0, -23.36, -4.67),
|
||
"9:45": (7, 42.9, -8.20, -1.17),
|
||
"10:15": (8, 62.5, +542.20, +67.77),
|
||
"10:30": (5, 80.0, +37.19, +7.44),
|
||
"10:45": (6, 0.0,-223.62, -37.27),
|
||
"11:00": (9, 44.4, +737.87, +81.99),
|
||
"11:30": (8, 87.5,+1074.52,+134.32),
|
||
"11:45": (5, 60.0, +558.01,+111.60),
|
||
"12:00": (5, 60.0, +660.08,+132.02),
|
||
"12:15": (6, 66.7, +705.15,+117.53),
|
||
"12:30": (6, 33.3, +513.91, +85.65),
|
||
"12:45": (6, 16.7,-1178.07,-196.35),
|
||
"13:00": (7, 14.3, -878.41,-125.49),
|
||
"13:15": (10, 60.0, +419.31, +41.93),
|
||
"13:30": (9, 44.4, -699.33, -77.70),
|
||
"13:45": (10, 70.0,+1082.10,+108.21),
|
||
"14:00": (7, 42.9,-388.03, -55.43),
|
||
"14:15": (9, 55.6, +215.29, +23.92),
|
||
"14:30": (7, 28.6, +413.16, +59.02),
|
||
"14:45": (11, 27.3, -65.79, -5.98),
|
||
"15:00": (10, 70.0,+2265.83,+226.58),
|
||
"15:15": (9, 55.6,-1225.87,-136.21),
|
||
"15:30": (11, 63.6, -65.03, -5.91),
|
||
"15:45": (10, 30.0, +81.01, +8.10),
|
||
"16:00": (5, 60.0, +691.34,+138.27),
|
||
"16:15": (9, 22.2, -78.42, -8.71),
|
||
"16:30": (4, 25.0,-2024.04,-506.01),
|
||
"16:45": (19, 42.1,-637.98, -33.58),
|
||
"17:00": (13, 38.5, +410.17, +31.55),
|
||
"17:15": (15, 46.7,-439.31, -29.29),
|
||
"17:30": (10, 60.0,-157.24, -15.72),
|
||
"18:00": (6, 16.7,-1595.60,-265.93),
|
||
"18:15": (17, 17.6, +60.98, +3.59),
|
||
"18:30": (9, 22.2,-317.64, -35.29),
|
||
"19:00": (8, 50.0,-157.93, -19.74),
|
||
"19:15": (5, 60.0, -95.94, -19.19),
|
||
"19:45": (7, 28.6,-392.53, -56.08),
|
||
"20:00": (5, 60.0, +409.41, +81.88),
|
||
"20:15": (8, 12.5,-1116.49,-139.56),
|
||
"20:45": (9, 44.4,-173.96, -19.33),
|
||
"21:15": (8, 50.0,-653.67, -81.71),
|
||
"21:30": (6, 33.3, +338.33, +56.39),
|
||
"22:00": (8, 25.0,-360.17, -45.02),
|
||
"22:15": (5, 60.0, +73.44, +14.69),
|
||
"22:30": (7, 28.6,-248.96, -35.57),
|
||
"23:00": (8, 62.5, +476.83, +59.60),
|
||
"23:15": (7, 71.4, +82.51, +11.79),
|
||
"23:30": (7, 42.9, -69.24, -9.89),
|
||
}
|
||
|
||
# Baseline: overall WR 43.7% — score is deviation from baseline
|
||
BASELINE_WR = 43.7
|
||
|
||
# ── Session classification ─────────────────────────────────────────────────────
|
||
def get_session(hour_utc: float) -> str:
|
||
if hour_utc < 8: return "ASIA_PACIFIC"
|
||
elif hour_utc < 13: return "LONDON_MORNING"
|
||
elif hour_utc < 17: return "LN_NY_OVERLAP"
|
||
elif hour_utc < 21: return "NY_AFTERNOON"
|
||
else: return "LOW_LIQUIDITY"
|
||
|
||
# ── EsoF computation ───────────────────────────────────────────────────────────
|
||
def compute_esof(now: datetime = None) -> dict:
|
||
"""Compute all EsoF advisory signals for a given UTC datetime."""
|
||
if now is None:
|
||
now = datetime.now(timezone.utc)
|
||
|
||
dow = now.weekday() # 0=Mon
|
||
hour_utc = now.hour + now.minute / 60.0
|
||
min_bucket = (now.minute // 15) * 15
|
||
slot_key = f"{now.hour}:{min_bucket:02d}"
|
||
session = get_session(hour_utc)
|
||
|
||
# ── Weighted hours (real computation via MarketIndicators) ─────────────────
|
||
pop_hour, liq_hour = _get_weighted_hours(now)
|
||
liq_bkt = int(liq_hour // 3) * 3
|
||
|
||
# ── Session expectancy ─────────────────────────────────────────────────────
|
||
sess_data = SESSION_STATS.get(session, (0, BASELINE_WR, 0, 0))
|
||
sess_wr, sess_net = sess_data[1], sess_data[2]
|
||
|
||
# ── Liq_hour expectancy (more granular than session) ───────────────────────
|
||
liq_data = LIQ_HOUR_STATS.get(liq_bkt, (0, BASELINE_WR, 0, 0))
|
||
liq_wr, liq_net = liq_data[1], liq_data[2]
|
||
|
||
# ── DoW expectancy ─────────────────────────────────────────────────────────
|
||
dow_data = DOW_STATS.get(dow, (0, BASELINE_WR, 0, 0))
|
||
dow_wr, dow_net = dow_data[1], dow_data[2]
|
||
|
||
# ── Slot expectancy ────────────────────────────────────────────────────────
|
||
slot_data = SLOT_STATS.get(slot_key, None)
|
||
if slot_data:
|
||
slot_wr, slot_net, slot_avg = slot_data[1], slot_data[2], slot_data[3]
|
||
else:
|
||
slot_wr, slot_net, slot_avg = BASELINE_WR, 0.0, 0.0
|
||
|
||
# ── DoW × Session notable cell ─────────────────────────────────────────────
|
||
cell_data = DOW_SESSION_STATS.get((dow, session), None)
|
||
cell_bonus = 0.0
|
||
if cell_data:
|
||
cell_trades, cell_wr, cell_net = cell_data
|
||
# bonus/penalty proportional to deviation from baseline
|
||
cell_bonus = (cell_wr - BASELINE_WR) / 100.0 * 0.3 # ±0.3 max contribution
|
||
|
||
# ── Fibonacci time ─────────────────────────────────────────────────────────
|
||
mins_passed = now.hour * 60 + now.minute
|
||
fib_seq = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1440]
|
||
closest_fib = min(fib_seq, key=lambda x: abs(x - mins_passed))
|
||
fib_dist = abs(mins_passed - closest_fib)
|
||
fib_strength = 1.0 - min(fib_dist / 30.0, 1.0)
|
||
|
||
# ── Market cycle position (BTC halving: Apr 19 2024) ──────────────────────
|
||
halving = datetime(2024, 4, 19, tzinfo=timezone.utc)
|
||
days_since = (now - halving).days
|
||
cycle_pos = (days_since % 1461) / 1461.0 # 4yr cycle
|
||
|
||
# ── Moon (simple approximation without astropy dependency) ────────────────
|
||
# Synodic period 29.53d; reference new moon 2024-01-11
|
||
ref_new_moon = datetime(2024, 1, 11, tzinfo=timezone.utc)
|
||
days_since_ref = (now - ref_new_moon).days + (now - ref_new_moon).seconds / 86400
|
||
moon_age = days_since_ref % 29.53059
|
||
moon_illumination = (1 - math.cos(2 * math.pi * moon_age / 29.53059)) / 2.0
|
||
if moon_illumination < 0.03: moon_phase = "NEW_MOON"
|
||
elif moon_illumination > 0.97: moon_phase = "FULL_MOON"
|
||
elif moon_age < 14.77:
|
||
moon_phase = "WAXING_CRESCENT" if moon_illumination < 0.5 else "WAXING_GIBBOUS"
|
||
else:
|
||
moon_phase = "WANING_GIBBOUS" if moon_illumination > 0.5 else "WANING_CRESCENT"
|
||
|
||
# Mercury retrograde periods (2025-2026 known dates)
|
||
retro_periods = [
|
||
(datetime(2025, 3, 15, tzinfo=timezone.utc), datetime(2025, 4, 7, tzinfo=timezone.utc)),
|
||
(datetime(2025, 7, 18, tzinfo=timezone.utc), datetime(2025, 8, 11, tzinfo=timezone.utc)),
|
||
(datetime(2025, 11, 10, tzinfo=timezone.utc), datetime(2025, 12, 1, tzinfo=timezone.utc)),
|
||
(datetime(2026, 3, 7, tzinfo=timezone.utc), datetime(2026, 3, 30, tzinfo=timezone.utc)),
|
||
(datetime(2026, 6, 29, tzinfo=timezone.utc), datetime(2026, 7, 23, tzinfo=timezone.utc)),
|
||
]
|
||
mercury_retrograde = any(s <= now <= e for s, e in retro_periods)
|
||
|
||
# ── Composite advisory score ───────────────────────────────────────────────
|
||
# Normalize each component to [-1, +1] relative to baseline WR=43.7%
|
||
# Range ≈ ±20 WR points across all factors
|
||
sess_score = (sess_wr - BASELINE_WR) / 20.0
|
||
liq_score = (liq_wr - BASELINE_WR) / 20.0
|
||
dow_score = (dow_wr - BASELINE_WR) / 20.0
|
||
slot_score = (slot_wr - BASELINE_WR) / 20.0 if slot_data else 0.0
|
||
|
||
# Weights: liq_hour 30%, session 25%, dow 30%, slot 10%, cell 5%
|
||
# liq_hour replaces pure session — it's strictly more granular (continuous)
|
||
advisory_score = (
|
||
liq_score * 0.30 +
|
||
sess_score * 0.25 +
|
||
dow_score * 0.30 +
|
||
slot_score * 0.10 +
|
||
cell_bonus * 0.05
|
||
)
|
||
advisory_score = max(-1.0, min(1.0, advisory_score))
|
||
|
||
if advisory_score > 0.25: advisory_label = "FAVORABLE"
|
||
elif advisory_score > 0.05: advisory_label = "MILD_POSITIVE"
|
||
# UNKNOWN (was NEUTRAL): constituent signals in conflict. Empirically the worst
|
||
# ROI bucket, not a benign mid-state — naming is load-bearing for consumers
|
||
# making "stand aside vs size-down" decisions.
|
||
elif advisory_score > -0.05: advisory_label = "UNKNOWN"
|
||
elif advisory_score > -0.25: advisory_label = "MILD_NEGATIVE"
|
||
else: advisory_label = "UNFAVORABLE"
|
||
|
||
# Mercury retrograde: small penalty
|
||
if mercury_retrograde:
|
||
advisory_score = max(-1.0, advisory_score - 0.05)
|
||
|
||
return {
|
||
"ts": now.isoformat(),
|
||
"_ts": now.timestamp(),
|
||
# Calendar
|
||
"dow": dow,
|
||
"dow_name": DOW_NAMES[dow],
|
||
"hour_utc": now.hour,
|
||
"slot_15m": slot_key,
|
||
"session": session,
|
||
# Weighted hours (real MarketIndicators computation)
|
||
"pop_weighted_hour": round(pop_hour, 2),
|
||
"liq_weighted_hour": round(liq_hour, 2),
|
||
"liq_bucket_3h": liq_bkt,
|
||
# Astro
|
||
"moon_illumination": round(moon_illumination, 3),
|
||
"moon_phase": moon_phase,
|
||
"mercury_retrograde": mercury_retrograde,
|
||
# Cycle / harmonic
|
||
"market_cycle_pos": round(cycle_pos, 4),
|
||
"fib_strength": round(fib_strength, 3),
|
||
# Expectancy (from live trade history)
|
||
"liq_wr_pct": round(liq_wr, 1),
|
||
"liq_net_pnl": round(liq_net, 2),
|
||
"slot_wr_pct": round(slot_wr, 1),
|
||
"slot_net_pnl": round(slot_net, 2),
|
||
"session_wr_pct": round(sess_wr, 1),
|
||
"session_net_pnl": round(sess_net, 2),
|
||
"dow_wr_pct": round(dow_wr, 1),
|
||
"dow_net_pnl": round(dow_net, 2),
|
||
# Composite
|
||
"advisory_score": round(advisory_score, 4),
|
||
"advisory_label": advisory_label,
|
||
# Meta
|
||
"_weighted_hours_real": _WEIGHTED_HOURS_AVAILABLE,
|
||
}
|
||
|
||
# ── CH writer (fire-and-forget) ───────────────────────────────────────────────
|
||
CH_URL = "http://localhost:8123"
|
||
CH_USER = "dolphin"
|
||
CH_PASS = "dolphin_ch_2026"
|
||
|
||
def _ch_write(row: dict):
|
||
ch_row = {
|
||
"ts": row["_ts"] * 1000, # ms for DateTime64(3)
|
||
"dow": row["dow"],
|
||
"dow_name": row["dow_name"],
|
||
"hour_utc": row["hour_utc"],
|
||
"slot_15m": row["slot_15m"],
|
||
"session": row["session"],
|
||
"moon_illumination": row["moon_illumination"],
|
||
"moon_phase": row["moon_phase"],
|
||
"mercury_retrograde": int(row["mercury_retrograde"]),
|
||
"pop_weighted_hour": row.get("pop_weighted_hour", 0.0),
|
||
"liq_weighted_hour": row.get("liq_weighted_hour", 0.0),
|
||
"market_cycle_pos": row["market_cycle_pos"],
|
||
"fib_strength": row["fib_strength"],
|
||
"slot_wr_pct": row["slot_wr_pct"],
|
||
"slot_net_pnl": row["slot_net_pnl"],
|
||
"session_wr_pct": row["session_wr_pct"],
|
||
"session_net_pnl": row["session_net_pnl"],
|
||
"dow_wr_pct": row["dow_wr_pct"],
|
||
"dow_net_pnl": row["dow_net_pnl"],
|
||
"advisory_score": row["advisory_score"],
|
||
"advisory_label": row["advisory_label"],
|
||
}
|
||
body = json.dumps(ch_row).encode()
|
||
url = f"{CH_URL}/?database=dolphin&query=INSERT+INTO+esof_advisory+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)
|
||
try:
|
||
urllib.request.urlopen(req, timeout=3)
|
||
except Exception:
|
||
pass
|
||
|
||
# ── HZ writer ─────────────────────────────────────────────────────────────────
|
||
def _hz_write(data: dict):
|
||
try:
|
||
import hazelcast
|
||
hz = hazelcast.HazelcastClient(
|
||
cluster_name="dolphin", cluster_members=["localhost:5701"],
|
||
connection_timeout=3.0)
|
||
hz.get_map("DOLPHIN_FEATURES").blocking().put(
|
||
"esof_advisor_latest", json.dumps(data))
|
||
hz.shutdown()
|
||
except Exception:
|
||
pass
|
||
|
||
# ── Display ───────────────────────────────────────────────────────────────────
|
||
GREEN = "\033[32m"; RED = "\033[31m"; YELLOW = "\033[33m"
|
||
CYAN = "\033[36m"; BOLD = "\033[1m"; DIM = "\033[2m"; RST = "\033[0m"
|
||
|
||
LABEL_COLOR = {
|
||
"FAVORABLE": GREEN,
|
||
"MILD_POSITIVE":"\033[92m",
|
||
"UNKNOWN": YELLOW, # renamed from NEUTRAL — signals-in-conflict
|
||
"NEUTRAL": YELLOW, # backward-compat for historical CH rows / stale HZ snapshots
|
||
"MILD_NEGATIVE":"\033[91m",
|
||
"UNFAVORABLE": RED,
|
||
}
|
||
|
||
def display(d: dict):
|
||
sc = d["advisory_score"]
|
||
lbl = d["advisory_label"]
|
||
col = LABEL_COLOR.get(lbl, RST)
|
||
bar_len = int(abs(sc) * 20)
|
||
bar = ("▓" * bar_len).ljust(20)
|
||
sign = "+" if sc >= 0 else "-"
|
||
|
||
print(f"\n{BOLD}{CYAN}🐬 DOLPHIN EsoF Advisory{RST} "
|
||
f"{DIM}{d['ts'][:19]} UTC{RST}")
|
||
_lc = GREEN if d['liq_wr_pct'] > BASELINE_WR else RED
|
||
print(f" {BOLD}Liq hour{RST} : {_lc}liq={d['liq_weighted_hour']:.2f}h (pop={d['pop_weighted_hour']:.2f}h){RST} "
|
||
f"bkt:{d['liq_bucket_3h']}-{d['liq_bucket_3h']+3}h "
|
||
f"WR={_lc}{d['liq_wr_pct']:.1f}%{RST} net={d['liq_net_pnl']:+,.0f}"
|
||
+ (f" {DIM}[real]{RST}" if d.get('_weighted_hours_real') else f" {YELLOW}[approx]{RST}"))
|
||
print(f" {BOLD}Session{RST} : {col}{d['session']:<22}{RST} "
|
||
f"WR={col}{d['session_wr_pct']:.1f}%{RST} net={d['session_net_pnl']:+,.0f}")
|
||
print(f" {BOLD}DoW{RST} : {col}{d['dow_name']:<22}{RST} "
|
||
f"WR={col}{d['dow_wr_pct']:.1f}%{RST} net={d['dow_net_pnl']:+,.0f}")
|
||
print(f" {BOLD}Slot 15m{RST} : {d['slot_15m']:<22} "
|
||
f"WR={d['slot_wr_pct']:.1f}% net={d['slot_net_pnl']:+,.0f}")
|
||
print(f" {BOLD}Moon{RST} : {d['moon_phase']:<18} {d['moon_illumination']*100:.0f}% illum")
|
||
print(f" {BOLD}Mercury{RST} : {'⚠ RETROGRADE' if d['mercury_retrograde'] else 'direct'}")
|
||
print(f" {BOLD}Fib{RST} : strength {d['fib_strength']:.2f} "
|
||
f"cycle_pos {d['market_cycle_pos']:.4f}")
|
||
print(f" {BOLD}Advisory{RST} : {col}{bar}{RST} "
|
||
f"{col}{sign}{abs(sc):.3f} {BOLD}{lbl}{RST}")
|
||
print()
|
||
|
||
# ── Daemon ────────────────────────────────────────────────────────────────────
|
||
def run_daemon(interval_s: float = 15.0, write_hz: bool = True,
|
||
write_ch: bool = True, verbose: bool = True):
|
||
"""Loop: compute → HZ → CH → display every interval_s."""
|
||
print(f"{BOLD}EsoF Advisory daemon started (interval={interval_s}s){RST}\n"
|
||
f" HZ={write_hz} CH={write_ch} display={verbose}\n"
|
||
f" Advisory-only — NOT wired into BLUE engine\n")
|
||
|
||
last_ch_write = 0.0 # write CH every 5 min, not every 15s
|
||
|
||
while True:
|
||
try:
|
||
d = compute_esof()
|
||
if verbose:
|
||
display(d)
|
||
if write_hz:
|
||
threading.Thread(target=_hz_write, args=(d,), daemon=True).start()
|
||
if write_ch and time.time() - last_ch_write > 300:
|
||
threading.Thread(target=_ch_write, args=(d,), daemon=True).start()
|
||
last_ch_write = time.time()
|
||
except Exception as e:
|
||
print(f"[EsoF] error: {e}")
|
||
time.sleep(interval_s)
|
||
|
||
# ── Public API for dolphin_status.py import ───────────────────────────────────
|
||
def get_advisory(now: datetime = None) -> dict:
|
||
"""Single-shot advisory computation. Import this into dolphin_status.py."""
|
||
return compute_esof(now)
|
||
|
||
if __name__ == "__main__":
|
||
import argparse
|
||
p = argparse.ArgumentParser()
|
||
p.add_argument("--once", action="store_true", help="Compute once and exit")
|
||
p.add_argument("--interval", type=float, default=15.0)
|
||
p.add_argument("--no-hz", action="store_true")
|
||
p.add_argument("--no-ch", action="store_true")
|
||
args = p.parse_args()
|
||
|
||
if args.once:
|
||
d = compute_esof()
|
||
display(d)
|
||
sys.exit(0)
|
||
|
||
run_daemon(
|
||
interval_s=args.interval,
|
||
write_hz=not args.no_hz,
|
||
write_ch=not args.no_ch,
|
||
)
|