diff --git a/nautilus_dolphin/nautilus_dolphin/nautilus/esf_alpha_orchestrator.py b/nautilus_dolphin/nautilus_dolphin/nautilus/esf_alpha_orchestrator.py index 44fa739..de3f988 100755 --- a/nautilus_dolphin/nautilus_dolphin/nautilus/esf_alpha_orchestrator.py +++ b/nautilus_dolphin/nautilus_dolphin/nautilus/esf_alpha_orchestrator.py @@ -14,6 +14,7 @@ Flow per tick: Source: dolphin_vbt_real.py simulate_multi_asset_nb() lines 1715-2250 """ +import math import uuid import numpy as np from numba import njit @@ -57,6 +58,12 @@ class NDPosition: entry_v50_vel: float = 0.0 entry_v750_vel: float = 0.0 current_price: float = 0.0 + # GREEN-only observability fields (BLUE leaves at defaults). + asset_bucket_id: Optional[int] = None + s6_mult: float = 1.0 + esof_mult: float = 1.0 + esof_label: Optional[str] = None + leverage_raw: Optional[float] = None # pre-int-rounding leverage, for CH analysis @property def pnl_pct(self) -> float: @@ -134,6 +141,11 @@ class NDAlphaEngine: use_dynamic_leverage: bool = True, # Absolute leverage ceiling — ACB/MC/EsoF steer within [base, abs_max]; never breach abs_max_leverage: float = 6.0, + # GREEN-only toggles (BLUE no-op when left at None/False). + s6_size_table: Optional[Dict[int, float]] = None, + esof_sizing_table: Optional[Dict[str, float]] = None, + asset_bucket_data: Optional[Dict[str, Any]] = None, + use_int_leverage: bool = False, # Seed seed: int = 42, ): @@ -147,6 +159,18 @@ class NDAlphaEngine: self.leverage_convexity = leverage_convexity self.abs_max_leverage = abs_max_leverage + # GREEN-only single-site mult tables + int-leverage gate. + # BLUE leaves these at None/False → notional/leverage math unchanged. + self.s6_size_table: Optional[Dict[int, float]] = s6_size_table + self.esof_sizing_table: Optional[Dict[str, float]] = esof_sizing_table + self.asset_bucket_data: Optional[Dict[str, Any]] = asset_bucket_data + self.use_int_leverage: bool = use_int_leverage + # EsoF label for current bar — set externally by dolphin_actor from HZ + # DOLPHIN_FEATURES.esof_advisor_latest each tick. None → treated as UNKNOWN. + self._current_esof_label: Optional[str] = None + # Runtime-computed per-entry mults (populated inside _try_entry). + self._esof_size_mult: float = 1.0 + # Fee/slippage model self.use_sp_fees = use_sp_fees self.use_sp_slippage = use_sp_slippage @@ -466,6 +490,18 @@ class NDAlphaEngine: if self.capital <= 0: return None + # GREEN-only EsoF regime gate. Runs before selector/sizer at the highest-visible level. + # BLUE leaves esof_sizing_table=None → skipped entirely (mult stays 1.0, no gate). + # Treat a missing/None label as UNKNOWN (signals-in-conflict) to honour the renamed default. + esof_label: Optional[str] = None + if self.esof_sizing_table: + esof_label = self._current_esof_label or "UNKNOWN" + self._esof_size_mult = float(self.esof_sizing_table.get(esof_label, 1.0)) + if self._esof_size_mult <= 0.0: + return None # regime-wide skip — no selector/sizer work + else: + self._esof_size_mult = 1.0 + trade_direction = self.regime_direction # 1. IRP asset selection (matches dolphin_vbt_real.py lines 2047-2071) @@ -562,7 +598,46 @@ class NDAlphaEngine: leverage = min(raw_leverage, clamped_max_leverage) leverage = max(self.bet_sizer.min_leverage, leverage) - notional = self.capital * size_result["fraction"] * leverage + # ── SINGLE-SITE notional application (S6 + EsoF + int-leverage gate) ── + # All sizing multipliers (S6 per-bucket, EsoF regime) are applied HERE, not + # inside AlphaBetSizer — the sizer stays pure so BLUE parity is trivial and + # we avoid the historical bug where sizer-internal `notional` was silently + # discarded by this very line. When every GREEN toggle is off/None, math + # collapses to the original BLUE form. + asset_bucket_id: Optional[int] = None + if self.asset_bucket_data is not None: + assignments = self.asset_bucket_data.get("assignments", {}) if isinstance(self.asset_bucket_data, dict) else {} + _bkt = assignments.get(trade_asset) + if _bkt is not None: + asset_bucket_id = int(_bkt) + + s6_mult: float = 1.0 + if self.s6_size_table and asset_bucket_id is not None: + s6_mult = float(self.s6_size_table.get(asset_bucket_id, 1.0)) + + esof_mult: float = self._esof_size_mult + + # INTEGER-LEVERAGE GATE — target exchanges (e.g. Binance) require int leverage. + # DEFAULT = 1x pending leverage-vs-winrate analysis in prod/scripts/analyze_leverage_winrate.py. + # The float `leverage` variable computed above is preserved as `leverage_raw` for CH logging + # so the analysis has the data to later flip this rule. + # + # Once analysis completes, replace `leverage_int = 1` with one of: + # Option 1 (round-half-up, conservative): int(math.floor(leverage + 0.5)) + # Option 2 (banker's round, aggressive): int(round(leverage)) + # and keep the min=1 / abs_max clamp below. + leverage_raw: float = leverage + if self.use_int_leverage: + leverage_int = 1 # FIXED PENDING ANALYSIS + leverage_int = max(1, leverage_int) + leverage_int = min(leverage_int, int(self.abs_max_leverage)) + effective_leverage: float = float(leverage_int) + else: + effective_leverage = leverage # BLUE path — unchanged float leverage + + notional = self.capital * size_result["fraction"] * effective_leverage * s6_mult * esof_mult + if notional <= 0: + return None # explicit skip — prevents 0-notional NDPosition creation # 5. Entry price — NO entry slippage (dolphin_vbt_real.py uses raw price for PnL calculation baseline) entry_price = prices.get(trade_asset, 0) @@ -577,12 +652,17 @@ class NDAlphaEngine: entry_price=entry_price, entry_bar=bar_idx, notional=notional, - leverage=leverage, + leverage=effective_leverage, fraction=size_result["fraction"], entry_vel_div=vel_div, bucket_idx=size_result["bucket_idx"], entry_v50_vel=v50_vel, entry_v750_vel=v750_vel, + asset_bucket_id=asset_bucket_id, + s6_mult=s6_mult, + esof_mult=esof_mult, + esof_label=esof_label, + leverage_raw=leverage_raw, ) # Consume pending overrides (set by subclasses before calling super()._try_entry). @@ -605,10 +685,15 @@ class NDAlphaEngine: "trade_id": trade_id, "asset": trade_asset, "direction": trade_direction, - "leverage": leverage, + "leverage": effective_leverage, + "leverage_raw": leverage_raw, "notional": notional, "vel_div": vel_div, "entry_price": entry_price, # needed by _exec_submit_entry when prices dict is empty + "asset_bucket_id": asset_bucket_id, + "s6_mult": s6_mult, + "esof_mult": esof_mult, + "esof_label": esof_label, } def get_performance_summary(self) -> Dict[str, Any]: