2026-04-21 16:58:38 +02:00
|
|
|
|
#!/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},
|
2026-04-22 06:07:30 +02:00
|
|
|
|
# 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},
|
2026-04-21 16:58:38 +02:00
|
|
|
|
"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},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-22 06:07:30 +02:00
|
|
|
|
# Base S6 — UNKNOWN/NEUTRAL rows above are identical (alias)
|
|
|
|
|
|
S6_BASE: Dict[int, float] = S6_MULT["UNKNOWN"]
|
2026-04-21 16:58:38 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 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},
|
2026-04-22 06:07:30 +02:00
|
|
|
|
"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
|
2026-04-21 16:58:38 +02:00
|
|
|
|
"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},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-22 06:07:30 +02:00
|
|
|
|
# Gold-spec thresholds (UNKNOWN/NEUTRAL row)
|
|
|
|
|
|
IRP_GOLD: Dict[str, float] = IRP_PARAMS["UNKNOWN"]
|
2026-04-21 16:58:38 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 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,
|
2026-04-22 06:07:30 +02:00
|
|
|
|
"UNKNOWN": 1.00,
|
|
|
|
|
|
"NEUTRAL": 1.00, # alias — historical CH replays
|
2026-04-21 16:58:38 +02: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))
|