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
|
||||
"""
|
||||
|
||||
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]:
|
||||
|
||||
Reference in New Issue
Block a user