initial: import DOLPHIN baseline 2026-04-21 from dolphinng5_predict working tree

Includes core prod + GREEN/BLUE subsystems:
- prod/ (BLUE harness, configs, scripts, docs)
- nautilus_dolphin/ (GREEN Nautilus-native impl + dvae/ preserved)
- adaptive_exit/ (AEM engine + models/bucket_assignments.pkl)
- Observability/ (EsoF advisor, TUI, dashboards)
- external_factors/ (EsoF producer)
- mc_forewarning_qlabs_fork/ (MC regime/envelope)

Excludes runtime caches, logs, backups, and reproducible artifacts per .gitignore.
This commit is contained in:
hjnormey
2026-04-21 16:58:38 +02:00
commit 01c19662cb
643 changed files with 260241 additions and 0 deletions

299
Observability/esof_gate.py Executable file
View File

@@ -0,0 +1,299 @@
#!/usr/bin/env python3
"""
DOLPHIN EsoF Gate — Advisory-only (NOT wired into BLUE).
Six gating / modulation strategies derived from EsoF advisory data.
All functions are pure (no side effects, no I/O, no HZ/CH deps).
Import-safe; designed to be wired into nautilus_event_trader.py when ready.
Strategies
──────────
A LEV_SCALE Soft leverage reduction on negative advisory score
B HARD_BLOCK Block entries on UNFAVORABLE + worst-session combo / Monday
C DOW_BLOCK Block Monday only (WR=27.2%, n=81, high confidence)
D SESSION_BLOCK Block NY_AFTERNOON only (WR=35.4%, n=127, high confidence)
E COMBINED C + D (Monday OR NY_AFTERNOON)
F S6_BUCKET EsoF-modulated S6 bucket sizing multipliers (main research target)
S6_IRP EsoF-modulated IRP filter thresholds (needs full backtest to evaluate)
S6 Bucket Multiplier Tables
───────────────────────────
Source: prod/docs/CRITICAL_ASSET_PICKING_BRACKETS_VS._ROI_WR_AT_TRADES.md
Base ("NEUTRAL") = Scenario S6 from that doc:
B3 2.0×, B6 1.5×, B5 0.5×, B0 0.4×, B1 0.3×, B4 0×, B2 0×
EsoF modulates these: favorable → widen selection (higher mult on weak buckets,
allow B4 back at reduced sizing); unfavorable → concentrate (S2-like, B3+B6 only).
Theory: In high-WR periods (FAVORABLE), even weaker buckets (B0/B1/B5) contribute
gross alpha. In low-WR periods (UNFAVORABLE), concentrate on the only reliably
profitable buckets (B3, B6) and minimise drag from the rest.
IRP ARS Constitutive Coefficients (S6_IRP, for reference)
──────────────────────────────────────────────────────────
ARS = 0.5×log1p(efficiency) + 0.35×alignment 0.15×noise×1000
Filter thresholds (gold spec): ALIGNMENT_MIN=0.20, NOISE_MAX=500, LATENCY_MAX=20
Source: nautilus_dolphin/nautilus/alpha_asset_selector.py
EsoF modulates the thresholds: favorable → relax (more assets qualify);
unfavorable → tighten (only highest-quality assets pass).
This strategy CANNOT be evaluated against existing CH trades — it changes WHICH
asset is selected, requiring a full IRP replay on historical klines.
Online Calibration Protocol (no feedback loop)
──────────────────────────────────────────────
ALWAYS calibrate EsoF tables from ungated BLUE trades only.
NEVER update EsoF expectancy tables using trades that were gated by EsoF.
Running gated trades through the calibration loop creates a positive/negative
feedback that tightens advisory bands until they lose real-world validity.
The baseline BLUE system (no gate) must always run in shadow to accumulate
out-of-sample calibration data.
"""
from __future__ import annotations
import math
from dataclasses import dataclass, field
from typing import Dict, Optional
# ── Known bucket assignments (from bucket_assignments.pkl / ASSET_BUCKETS.md) ─
# Runtime: prefer loading from pkl; this map is the authoritative fallback.
BUCKET_MAP: Dict[str, int] = {
# B2 — Macro Anchors
"BTCUSDT": 2, "ETHUSDT": 2,
# B4 — Blue-Chip Alts (WORST bucket — net-negative even gross)
"LTCUSDT": 4, "BNBUSDT": 4, "NEOUSDT": 4, "ETCUSDT": 4, "LINKUSDT": 4,
# B0 — Mid-Vol Established Alts (fee-drag losers, gross-positive)
"ONGUSDT": 0, "WANUSDT": 0, "ONTUSDT": 0, "MTLUSDT": 0, "BANDUSDT": 0,
"TFUELUSDT": 0, "ICXUSDT": 0, "QTUMUSDT": 0, "RVNUSDT": 0, "XTZUSDT": 0,
"VETUSDT": 0, "COSUSDT": 0, "HOTUSDT": 0, "STXUSDT": 0,
# B5 — Low-BTC-Relevance Alts (gross-positive, large fee victim)
"TRXUSDT": 5, "IOSTUSDT": 5, "CVCUSDT": 5, "BATUSDT": 5, "ATOMUSDT": 5,
"ANKRUSDT": 5, "IOTAUSDT": 5, "CHZUSDT": 5, "ALGOUSDT": 5, "DUSKUSDT": 5,
# B3 — High-Vol Alts (STAR bucket — only structurally profitable)
"WINUSDT": 3, "ADAUSDT": 3, "ENJUSDT": 3, "ZILUSDT": 3, "DOGEUSDT": 3,
"DENTUSDT": 3, "THETAUSDT": 3, "ONEUSDT": 3,
# B1 — Extreme Low-Corr (marginal, fee-drag)
"DASHUSDT": 1, "XRPUSDT": 1, "XLMUSDT": 1, "CELRUSDT": 1, "ZECUSDT": 1,
"HBARUSDT": 1, "FUNUSDT": 1,
# B6 — Extreme Vol Mid-Corr (good, small sample)
"ZRXUSDT": 6, "FETUSDT": 6,
}
def get_bucket(asset: str, pkl_assignments: Optional[Dict[str, int]] = None) -> int:
"""Resolve bucket_id for asset. Prefers pkl_assignments over built-in map."""
if pkl_assignments and asset in pkl_assignments:
return pkl_assignments[asset]
return BUCKET_MAP.get(asset, 0) # B0 fallback for unknown assets
# ── S6 bucket multiplier tables keyed by advisory_label ───────────────────────
#
# Base (NEUTRAL) = Scenario S6 from CRITICAL_ASSET_PICKING doc:
# B3 2.0× B6 1.5× B5 0.5× B0 0.4× B1 0.3× B4 0× B2 0×
#
# FAVORABLE / MILD_POSITIVE → wider selection: more assets qualify,
# even B4 re-admitted at very low sizing (0.2×) because in high-WR periods
# even B4's 34.8% WR is partially redeemed by signal quality uplift.
#
# MILD_NEGATIVE / UNFAVORABLE → concentrate: pull back to S2-like config
# (B3+B6 only) to minimise drag during periods where signal quality degrades.
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},
"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"]
# ── IRP filter threshold tables keyed by advisory_label (Strategy S6_IRP) ─────
# Gold spec (NEUTRAL): ALIGNMENT_MIN=0.20, NOISE_MAX=500, LATENCY_MAX=20
# Widening during FAVORABLE: more assets pass IRP → wider selection surface
# Tightening during UNFAVORABLE: only highest-quality assets enter
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},
"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"]
# ── GateResult ─────────────────────────────────────────────────────────────────
@dataclass
class GateResult:
action: str # 'ALLOW' | 'BLOCK' | 'SCALE'
lev_mult: float # leverage multiplier: 1.0=no change, 0=block, 0.5=halve
reason: str # human-readable label for logging
s6_mult: Dict[int, float] = field(default_factory=lambda: dict(S6_BASE))
irp_params: Dict[str, float] = field(default_factory=lambda: dict(IRP_GOLD))
@property
def is_blocked(self) -> bool:
return self.action == 'BLOCK' or self.lev_mult == 0.0
# ── Strategy implementations ───────────────────────────────────────────────────
def strategy_A_lev_scale(adv: dict) -> GateResult:
"""
Strategy A — LEV_SCALE
Soft leverage reduction proportional to advisory label.
Never boosts beyond gold spec (no mult > 1.0).
"""
label = adv["advisory_label"]
mult_map = {
"UNFAVORABLE": 0.50,
"MILD_NEGATIVE": 0.75,
"NEUTRAL": 1.00,
"MILD_POSITIVE": 1.00,
"FAVORABLE": 1.00,
}
mult = mult_map.get(label, 1.0)
action = "SCALE" if mult < 1.0 else "ALLOW"
return GateResult(action=action, lev_mult=mult,
reason=f"A_LEV_SCALE({label},{mult:.2f}x)")
def strategy_B_hard_block(adv: dict) -> GateResult:
"""
Strategy B — HARD_BLOCK
Block entry when UNFAVORABLE in the two worst sessions.
Monday: reduce to 60% (WR=27.2%, not blocking entirely to maintain diversity).
"""
label = adv["advisory_label"]
session = adv["session"]
dow = adv["dow"]
BAD_SESSIONS = {"NY_AFTERNOON", "LOW_LIQUIDITY"}
if label == "UNFAVORABLE" and session in BAD_SESSIONS:
return GateResult("BLOCK", 0.0,
f"B_HARD_BLOCK(UNFAVORABLE+{session})")
if dow == 0: # Monday
return GateResult("SCALE", 0.60,
"B_HARD_BLOCK(Monday,0.60x)")
return GateResult("ALLOW", 1.0, "B_HARD_BLOCK(ALLOW)")
def strategy_C_dow_block(adv: dict) -> GateResult:
"""
Strategy C — DOW_BLOCK
Block ALL entries on Monday (WR=27.2%, n=81, most robust single signal).
"""
if adv["dow"] == 0:
return GateResult("BLOCK", 0.0, "C_DOW_BLOCK(Monday)")
return GateResult("ALLOW", 1.0, "C_DOW_BLOCK(ALLOW)")
def strategy_D_session_block(adv: dict) -> GateResult:
"""
Strategy D — SESSION_BLOCK
Block ALL entries during NY_AFTERNOON (WR=35.4%, n=127, net=-$3,857).
"""
if adv["session"] == "NY_AFTERNOON":
return GateResult("BLOCK", 0.0, "D_SESSION_BLOCK(NY_AFTERNOON)")
return GateResult("ALLOW", 1.0, "D_SESSION_BLOCK(ALLOW)")
def strategy_E_combined(adv: dict) -> GateResult:
"""
Strategy E — COMBINED
Block Monday OR NY_AFTERNOON. The two highest-confidence single-factor signals.
Together they cover 208 of 637 trades at WR=32.2% (heavy combined drag).
"""
if adv["dow"] == 0:
return GateResult("BLOCK", 0.0, "E_COMBINED(Monday)")
if adv["session"] == "NY_AFTERNOON":
return GateResult("BLOCK", 0.0, "E_COMBINED(NY_AFTERNOON)")
return GateResult("ALLOW", 1.0, "E_COMBINED(ALLOW)")
def strategy_F_s6_bucket(adv: dict) -> GateResult:
"""
Strategy F — S6_BUCKET_MODULATION (primary research target)
Returns EsoF-modulated S6 bucket multipliers.
The ALLOW action + lev_mult=1.0 means: use the returned s6_mult table
to scale position size per bucket at the routing layer.
During FAVORABLE: widen selection (B4 back at 0.2×, B5/B0/B1 boosted)
During NEUTRAL: base S6 (gold scenario from CRITICAL_ASSET_PICKING doc)
During UNFAVORABLE: concentrate (B3+B6 only, S2-like)
IMPORTANT: This strategy cannot be evaluated against historical PnL alone
because it changes WHICH trades occur (more/fewer assets qualify at the
routing layer). The counterfactual below assumes the SAME trades execute
with scaled sizing — a lower-bound estimate of the real effect.
"""
label = adv["advisory_label"]
mults = S6_MULT.get(label, S6_BASE)
params = IRP_PARAMS.get(label, IRP_GOLD)
return GateResult("ALLOW", 1.0,
f"F_S6_BUCKET({label})",
s6_mult=dict(mults),
irp_params=dict(params))
# ── Unified dispatcher ─────────────────────────────────────────────────────────
STRATEGY_NAMES = {
"A": "A_LEV_SCALE",
"B": "B_HARD_BLOCK",
"C": "C_DOW_BLOCK",
"D": "D_SESSION_BLOCK",
"E": "E_COMBINED",
"F": "F_S6_BUCKET",
}
_STRATEGY_FNS = {
"A": strategy_A_lev_scale,
"B": strategy_B_hard_block,
"C": strategy_C_dow_block,
"D": strategy_D_session_block,
"E": strategy_E_combined,
"F": strategy_F_s6_bucket,
}
def apply_gate(strategy: str, advisory: dict) -> GateResult:
"""
Apply a named gate strategy to an advisory dict.
Args:
strategy: Key from STRATEGY_NAMES ('A'..'F')
advisory: Dict returned by compute_esof() from esof_advisor.py
Returns:
GateResult with action, lev_mult, reason, s6_mult, irp_params.
Raises:
KeyError: if strategy key is unknown.
"""
fn = _STRATEGY_FNS.get(strategy)
if fn is None:
raise KeyError(f"Unknown strategy '{strategy}'. Valid: {list(_STRATEGY_FNS)}")
return fn(advisory)
def get_s6_mult(advisory: dict, bucket_id: int) -> float:
"""Convenience: return S6 bucket multiplier for a specific advisory + bucket."""
label = advisory["advisory_label"]
return S6_MULT.get(label, S6_BASE).get(bucket_id, 0.4)
def get_irp_params(advisory: dict) -> Dict[str, float]:
"""Convenience: return IRP filter params for a specific advisory."""
return dict(IRP_PARAMS.get(advisory["advisory_label"], IRP_GOLD))