feat(s6/esof): orchestrator single-site S6+EsoF+int-leverage gate
All sizing multipliers now applied at one location (esf_alpha_orchestrator.py line 565 region) so there are no hidden side-effects and BLUE parity is trivially preserved by leaving the new kwargs at None/False. - S6 per-bucket notional multiplier via s6_size_table kwarg - EsoF regime gate at _try_entry top: reads _current_esof_label, skips entry if esof_sizing_table maps to 0.0 (UNFAV/MILD_NEG regime) - Integer-leverage gate: use_int_leverage=True → leverage_int=1 (FIXED, pending prod/scripts/analyze_leverage_winrate.py analysis); float leverage_raw preserved in NDPosition + return dict for CH logging - notional <= 0 → return None guard (prevents 0-notional positions) - NDPosition extended: asset_bucket_id, s6_mult, esof_mult, esof_label, leverage_raw fields (BLUE leaves these at defaults) Plan ref: Task 2 — sizer stays pure (its returned notional is discarded by line 565 anyway; adding mults inside would be dead code with hidden side-effects). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ Flow per tick:
|
|||||||
Source: dolphin_vbt_real.py simulate_multi_asset_nb() lines 1715-2250
|
Source: dolphin_vbt_real.py simulate_multi_asset_nb() lines 1715-2250
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
import uuid
|
import uuid
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from numba import njit
|
from numba import njit
|
||||||
@@ -57,6 +58,12 @@ class NDPosition:
|
|||||||
entry_v50_vel: float = 0.0
|
entry_v50_vel: float = 0.0
|
||||||
entry_v750_vel: float = 0.0
|
entry_v750_vel: float = 0.0
|
||||||
current_price: 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
|
@property
|
||||||
def pnl_pct(self) -> float:
|
def pnl_pct(self) -> float:
|
||||||
@@ -134,6 +141,11 @@ class NDAlphaEngine:
|
|||||||
use_dynamic_leverage: bool = True,
|
use_dynamic_leverage: bool = True,
|
||||||
# Absolute leverage ceiling — ACB/MC/EsoF steer within [base, abs_max]; never breach
|
# Absolute leverage ceiling — ACB/MC/EsoF steer within [base, abs_max]; never breach
|
||||||
abs_max_leverage: float = 6.0,
|
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
|
||||||
seed: int = 42,
|
seed: int = 42,
|
||||||
):
|
):
|
||||||
@@ -147,6 +159,18 @@ class NDAlphaEngine:
|
|||||||
self.leverage_convexity = leverage_convexity
|
self.leverage_convexity = leverage_convexity
|
||||||
self.abs_max_leverage = abs_max_leverage
|
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
|
# Fee/slippage model
|
||||||
self.use_sp_fees = use_sp_fees
|
self.use_sp_fees = use_sp_fees
|
||||||
self.use_sp_slippage = use_sp_slippage
|
self.use_sp_slippage = use_sp_slippage
|
||||||
@@ -466,6 +490,18 @@ class NDAlphaEngine:
|
|||||||
if self.capital <= 0:
|
if self.capital <= 0:
|
||||||
return None
|
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
|
trade_direction = self.regime_direction
|
||||||
|
|
||||||
# 1. IRP asset selection (matches dolphin_vbt_real.py lines 2047-2071)
|
# 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 = min(raw_leverage, clamped_max_leverage)
|
||||||
leverage = max(self.bet_sizer.min_leverage, 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)
|
# 5. Entry price — NO entry slippage (dolphin_vbt_real.py uses raw price for PnL calculation baseline)
|
||||||
entry_price = prices.get(trade_asset, 0)
|
entry_price = prices.get(trade_asset, 0)
|
||||||
@@ -577,12 +652,17 @@ class NDAlphaEngine:
|
|||||||
entry_price=entry_price,
|
entry_price=entry_price,
|
||||||
entry_bar=bar_idx,
|
entry_bar=bar_idx,
|
||||||
notional=notional,
|
notional=notional,
|
||||||
leverage=leverage,
|
leverage=effective_leverage,
|
||||||
fraction=size_result["fraction"],
|
fraction=size_result["fraction"],
|
||||||
entry_vel_div=vel_div,
|
entry_vel_div=vel_div,
|
||||||
bucket_idx=size_result["bucket_idx"],
|
bucket_idx=size_result["bucket_idx"],
|
||||||
entry_v50_vel=v50_vel,
|
entry_v50_vel=v50_vel,
|
||||||
entry_v750_vel=v750_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).
|
# Consume pending overrides (set by subclasses before calling super()._try_entry).
|
||||||
@@ -605,10 +685,15 @@ class NDAlphaEngine:
|
|||||||
"trade_id": trade_id,
|
"trade_id": trade_id,
|
||||||
"asset": trade_asset,
|
"asset": trade_asset,
|
||||||
"direction": trade_direction,
|
"direction": trade_direction,
|
||||||
"leverage": leverage,
|
"leverage": effective_leverage,
|
||||||
|
"leverage_raw": leverage_raw,
|
||||||
"notional": notional,
|
"notional": notional,
|
||||||
"vel_div": vel_div,
|
"vel_div": vel_div,
|
||||||
"entry_price": entry_price, # needed by _exec_submit_entry when prices dict is empty
|
"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]:
|
def get_performance_summary(self) -> Dict[str, Any]:
|
||||||
|
|||||||
Reference in New Issue
Block a user