feat(esof): rename NEUTRAL→UNKNOWN + backward-compat alias

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>
This commit is contained in:
hjnormey
2026-04-22 06:07:30 +02:00
parent 0da46d8635
commit af5156f52d
3 changed files with 43 additions and 8 deletions

View File

@@ -291,7 +291,10 @@ def compute_esof(now: datetime = None) -> dict:
if advisory_score > 0.25: advisory_label = "FAVORABLE" if advisory_score > 0.25: advisory_label = "FAVORABLE"
elif advisory_score > 0.05: advisory_label = "MILD_POSITIVE" 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" elif advisory_score > -0.25: advisory_label = "MILD_NEGATIVE"
else: advisory_label = "UNFAVORABLE" else: advisory_label = "UNFAVORABLE"
@@ -394,7 +397,8 @@ CYAN = "\033[36m"; BOLD = "\033[1m"; DIM = "\033[2m"; RST = "\033[0m"
LABEL_COLOR = { LABEL_COLOR = {
"FAVORABLE": GREEN, "FAVORABLE": GREEN,
"MILD_POSITIVE":"\033[92m", "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", "MILD_NEGATIVE":"\033[91m",
"UNFAVORABLE": RED, "UNFAVORABLE": RED,
} }

View File

@@ -104,13 +104,16 @@ S6_MULT: Dict[str, Dict[int, float]] = {
# B0 B1 B2 B3 B4 B5 B6 # 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}, "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}, "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}, "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}, "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}, "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 # Base S6 — UNKNOWN/NEUTRAL rows above are identical (alias)
S6_BASE: Dict[int, float] = S6_MULT["NEUTRAL"] S6_BASE: Dict[int, float] = S6_MULT["UNKNOWN"]
# ── IRP filter threshold tables keyed by advisory_label (Strategy S6_IRP) ───── # ── 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]] = { IRP_PARAMS: Dict[str, Dict[str, float]] = {
"FAVORABLE": {"alignment_min": 0.15, "noise_max": 640.0, "latency_max": 24}, "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}, "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}, "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}, "UNFAVORABLE": {"alignment_min": 0.25, "noise_max": 380.0, "latency_max": 15},
} }
# Gold-spec thresholds (NEUTRAL row) # Gold-spec thresholds (UNKNOWN/NEUTRAL row)
IRP_GOLD: Dict[str, float] = IRP_PARAMS["NEUTRAL"] IRP_GOLD: Dict[str, float] = IRP_PARAMS["UNKNOWN"]
# ── GateResult ───────────────────────────────────────────────────────────────── # ── GateResult ─────────────────────────────────────────────────────────────────
@@ -157,7 +161,8 @@ def strategy_A_lev_scale(adv: dict) -> GateResult:
mult_map = { mult_map = {
"UNFAVORABLE": 0.50, "UNFAVORABLE": 0.50,
"MILD_NEGATIVE": 0.75, "MILD_NEGATIVE": 0.75,
"NEUTRAL": 1.00, "UNKNOWN": 1.00,
"NEUTRAL": 1.00, # alias — historical CH replays
"MILD_POSITIVE": 1.00, "MILD_POSITIVE": 1.00,
"FAVORABLE": 1.00, "FAVORABLE": 1.00,
} }

View File

@@ -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.