diff --git a/Observability/esof_advisor.py b/Observability/esof_advisor.py index 716e8d6..8be31c7 100755 --- a/Observability/esof_advisor.py +++ b/Observability/esof_advisor.py @@ -291,7 +291,10 @@ def compute_esof(now: datetime = None) -> dict: if advisory_score > 0.25: advisory_label = "FAVORABLE" elif advisory_score > 0.05: advisory_label = "MILD_POSITIVE" - elif advisory_score > -0.05: advisory_label = "NEUTRAL" + # 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" @@ -394,7 +397,8 @@ CYAN = "\033[36m"; BOLD = "\033[1m"; DIM = "\033[2m"; RST = "\033[0m" LABEL_COLOR = { "FAVORABLE": GREEN, "MILD_POSITIVE":"\033[92m", - "NEUTRAL": YELLOW, + "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, } diff --git a/Observability/esof_gate.py b/Observability/esof_gate.py index 6941163..5d182e2 100755 --- a/Observability/esof_gate.py +++ b/Observability/esof_gate.py @@ -104,13 +104,16 @@ S6_MULT: Dict[str, Dict[int, float]] = { # B0 B1 B2 B3 B4 B5 B6 "FAVORABLE": {0: 0.65, 1: 0.50, 2: 0.0, 3: 2.0, 4: 0.20, 5: 0.75, 6: 1.5}, "MILD_POSITIVE": {0: 0.50, 1: 0.35, 2: 0.0, 3: 2.0, 4: 0.10, 5: 0.60, 6: 1.5}, + # UNKNOWN replaces NEUTRAL (constituent signals in conflict — empirically the + # worst-ROI state). Keep NEUTRAL as alias so historical CH replays still resolve. + "UNKNOWN": {0: 0.40, 1: 0.30, 2: 0.0, 3: 2.0, 4: 0.0, 5: 0.50, 6: 1.5}, "NEUTRAL": {0: 0.40, 1: 0.30, 2: 0.0, 3: 2.0, 4: 0.0, 5: 0.50, 6: 1.5}, "MILD_NEGATIVE": {0: 0.20, 1: 0.20, 2: 0.0, 3: 1.5, 4: 0.0, 5: 0.30, 6: 1.2}, "UNFAVORABLE": {0: 0.0, 1: 0.0, 2: 0.0, 3: 1.5, 4: 0.0, 5: 0.0, 6: 1.2}, } -# Base S6 (NEUTRAL row above) — exposed for quick reference -S6_BASE: Dict[int, float] = S6_MULT["NEUTRAL"] +# Base S6 — UNKNOWN/NEUTRAL rows above are identical (alias) +S6_BASE: Dict[int, float] = S6_MULT["UNKNOWN"] # ── IRP filter threshold tables keyed by advisory_label (Strategy S6_IRP) ───── @@ -121,13 +124,14 @@ S6_BASE: Dict[int, float] = S6_MULT["NEUTRAL"] IRP_PARAMS: Dict[str, Dict[str, float]] = { "FAVORABLE": {"alignment_min": 0.15, "noise_max": 640.0, "latency_max": 24}, "MILD_POSITIVE": {"alignment_min": 0.17, "noise_max": 560.0, "latency_max": 22}, - "NEUTRAL": {"alignment_min": 0.20, "noise_max": 500.0, "latency_max": 20}, + "UNKNOWN": {"alignment_min": 0.20, "noise_max": 500.0, "latency_max": 20}, + "NEUTRAL": {"alignment_min": 0.20, "noise_max": 500.0, "latency_max": 20}, # alias "MILD_NEGATIVE": {"alignment_min": 0.22, "noise_max": 440.0, "latency_max": 18}, "UNFAVORABLE": {"alignment_min": 0.25, "noise_max": 380.0, "latency_max": 15}, } -# Gold-spec thresholds (NEUTRAL row) -IRP_GOLD: Dict[str, float] = IRP_PARAMS["NEUTRAL"] +# Gold-spec thresholds (UNKNOWN/NEUTRAL row) +IRP_GOLD: Dict[str, float] = IRP_PARAMS["UNKNOWN"] # ── GateResult ───────────────────────────────────────────────────────────────── @@ -157,7 +161,8 @@ def strategy_A_lev_scale(adv: dict) -> GateResult: mult_map = { "UNFAVORABLE": 0.50, "MILD_NEGATIVE": 0.75, - "NEUTRAL": 1.00, + "UNKNOWN": 1.00, + "NEUTRAL": 1.00, # alias — historical CH replays "MILD_POSITIVE": 1.00, "FAVORABLE": 1.00, } diff --git a/prod/docs/ESOF_LABEL_MIGRATION.md b/prod/docs/ESOF_LABEL_MIGRATION.md new file mode 100644 index 0000000..fddce34 --- /dev/null +++ b/prod/docs/ESOF_LABEL_MIGRATION.md @@ -0,0 +1,26 @@ +# EsoF Label Migration — `NEUTRAL` → `UNKNOWN` + +Landed in `exp/green-s6-esof-aem-shadow-2026-04-21` on 2026-04-21. + +## What changed + +`Observability/esof_advisor.py` emits `UNKNOWN` where it previously emitted `NEUTRAL` (mid-band, `-0.05 < advisory_score <= 0.05`). All downstream consumers keep a `NEUTRAL` alias so historical CH rows and replays continue to resolve. + +## Why + +Findings from the 637-trade EsoF retrospective (`prod/docs/EsoF_BLUE_IMPLEMENTATION_CURR_AND_RESEARCH.md`) show the mid-band isn't a benign middle — it's the **worst-ROI regime**, corresponding to states where the constituent liq/session/DoW/slot/cell signals are **in conflict**. "NEUTRAL" connoted "no strong read, probably fine"; the data says the opposite. The orchestrator-top EsoF gate uses `UNKNOWN` → `0.25x` sizer mult to encode "stand mostly aside" instead of "trade normally". + +## Touched files + +- `Observability/esof_advisor.py` — emitter renamed; `LABEL_COLOR` carries both keys. +- `Observability/esof_gate.py` — `S6_MULT`, `IRP_PARAMS`, Strategy A mult map all keyed on `UNKNOWN` with a `NEUTRAL` alias entry. +- `nautilus_dolphin/nautilus_dolphin/nautilus/esf_alpha_orchestrator.py` — new GREEN-only `esof_sizing_table` looks up by label; missing/None label defaults to `UNKNOWN`. + +## CH / HZ + +- CH `dolphin.trade_events`, `dolphin.esof_advisory_log`: no DDL change (labels are strings). Historical rows keep `NEUTRAL`; new rows get `UNKNOWN`. Any dashboard/query filtering on the label needs to `IN ('NEUTRAL','UNKNOWN')` or be migrated. +- HZ `DOLPHIN_FEATURES.esof_advisor_latest`: next advisor tick overwrites with `UNKNOWN`. Consumers reading stale snapshots across the cutover should treat both as equivalent. + +## Rollback + +Revert the three files above. The `NEUTRAL` alias means a partial rollback (advisor only) is safe without cascading breakage.