VIOLET V3.4c: ACB boost/beta — keep-prior on stale exf, decision logging, semantics doc

Per operator: VIOLET must keep a "prior" on stale ExF rather than collapse to neutral, and
the ACB's role must be commented-in + logged.

- KEEP-PRIOR on stale/missing exf: BLUE keeps _day_base_boost/_day_beta on the stale branch
  (update_acb_boost isn't called). _source_boost_beta now takes `prior` (boost,beta) and
  returns it on ValueError / no-exf, seeded by the first successful compute. Threaded as
  `prior_boost_beta` through source_live_blue_sizing_factors (caller persists last good).
- LOGGING: violet.live_blue_source logger emits live boost/beta + the full sourced plane.
- SEMANTICS: documents the ACB in effect (inverse v6) and CONFIRMS ACB IS in the sizing layer
  via _day_base_boost/_day_beta → _update_regime_size_mult (esf_alpha_orchestrator.py:771-772,
  898-909). OB Sub-4 beta modulation dormant in live — per operator found NON-PERFORMANT and
  deliberately bypassed; TODO_SOMEDAY in-code to find documentary confirmation.

37 tests. violet-only; no shared-file edits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-16 18:22:29 +02:00
parent fac287d678
commit 262cada664
2 changed files with 99 additions and 21 deletions

View File

@@ -33,6 +33,7 @@ test here (see test_signal_gen_params_match_blue_engine_kwargs / the mc_scale fo
from __future__ import annotations
import json
import logging
import os as _os
import sys
from collections import deque
@@ -65,6 +66,8 @@ from .decision_engine import SizingFactors
from .live_factor_source import esof_score_from_features, posture_from_engine_snapshot
from .live_factors import extract_live_sizing_factors
LOGGER = logging.getLogger("violet.live_blue_source")
# Hazelcast coordinates — MUST equal nautilus_event_trader.py:107-108 (BLUE's live
# cluster). HZOBProvider opens its own connection to these, exactly as BLUE's _wire_obf.
HZ_CLUSTER = _os.environ.get("HZ_CLUSTER", "dolphin")
@@ -155,33 +158,59 @@ def _derive_mc_scale(payload: Any) -> float:
return 0.5 if mc_orange else 1.0
# Inverse-ACB neutral identity — used ONLY at cold start (no prior yet AND no fresh exf).
# In continuous operation exf is warmed, so the first call seeds the prior and this is
# never the steady-state value.
_BOOST_BETA_NEUTRAL = (1.0, 0.0)
def _source_boost_beta(
client: hazelcast.HazelcastClient,
*,
date_str: str,
trade_direction: int,
acb: Optional[AdaptiveCircuitBreaker] = None,
prior: Optional[tuple[float, float]] = None,
) -> tuple[float, float]:
"""Recompute (boost, beta) EXACTLY as BLUE's live trader does — NOT from acb_boost.
BLUE (nautilus_event_trader.py on_exf_update:4769 / rollover prewarm:2710):
acb = AdaptiveCircuitBreaker() # bare, no args (trader 578/585)
info = acb.get_dynamic_boost_from_hz(
date_str=today,
exf_snapshot=DOLPHIN_FEATURES['exf_latest'],
w750_velocity=latest_eigen_scan['w750_velocity'] or None,
direction=trade_direction, # NO ob_engine in the live path
)
boost, beta = info['boost'], info['beta']
WHAT THE ACB IS DOING, IN EFFECT (AdaptiveCircuitBreaker v6, "inverse" mode):
- ``boost`` = a DAILY position-size governor >= 1.0 driven by EXTERNAL FACTORS (ExF:
funding_btc/dvol_btc/fng/taker from DOLPHIN_FEATURES.exf_latest). When stress
signals fire, ``boost = 1 + 0.5*ln(1+signals)``; else 1.0. INVERSE = it leans size
UP under stress, it does not cut (adaptive_circuit_breaker.py:591-593).
- ``beta`` = regime sensitivity in {BETA_HIGH=0.8, BETA_LOW=0.2}, keyed on the w750
eigenvalue-velocity (>= threshold → HIGH). It sets how strongly per-bar signal
strength amplifies size: regime_size_mult = base_boost*(1 + beta*strength^3)*mc_scale.
- Both become the engine's _day_base_boost / _day_beta via update_acb_boost
(esf_alpha_orchestrator.py:771-772), which then feed BLUE's SIZING directly:
_update_regime_size_mult (:898-909) = _day_base_boost * (1 + _day_beta*strength^3)
* _day_mc_scale. So the ACB IS in the sizing layer — confirmed, not incidental.
The published ``DOLPHIN_FEATURES['acb_boost']`` is a SEPARATE daily publish
(acb_processor_service) and is NOT what drives BLUE's sizing, so we never read it.
On stale exf (>12h) get_dynamic_boost_from_hz raises ValueError; BLUE logs
'ACB Stale Data Fallback' and keeps the prior boost/beta — VIOLET has no prior, so it
returns the neutral identity (1.0, 0.0)."""
BLUE's LIVE path passes NO ob_engine (on_exf_update:4769 / rollover prewarm:2710), so the
ACB's OB Sub-4 macro-regime beta modulation (x1.25 stress / x0.85 calm,
adaptive_circuit_breaker.py:613-628) is DORMANT in live — only the NPZ backtest path
get_dynamic_boost_for_date applies it. Per operator recollection (2026-06-16), OB Sub-4
beta modulation was found NON-PERFORMANT and deliberately removed/bypassed — so passing
no ob_engine is INTENTIONAL design, not an oversight. We pass no ob_engine to match BLUE
bit-for-bit either way.
# TODO_SOMEDAY: find the documentary confirmation of the OB Sub-4 non-performant bypass
# (operator: likely SYSTEM_BIBLE v7 lineage or a deep code comment / the in-progress
# INDEX_DOLPHINNG5_PREDICT doc). Behaviour is already bit-identical to live BLUE.
The published DOLPHIN_FEATURES.acb_boost is acb_processor_service's SEPARATE daily value
and is never read here.
STALE / MISSING exf: BLUE keeps the prior _day_base_boost/_day_beta (update_acb_boost
simply isn't called on the stale branch), so VIOLET KEEPS ``prior`` too. ``prior`` is
seeded by the first successful compute — exf is warmed in continuous BLUE operation, so
the steady state always has a prior; only a cold start with no prior AND no fresh exf
falls back to the neutral identity."""
fallback = prior if prior is not None else _BOOST_BETA_NEUTRAL
exf = _jsonish(_read_hz_map(client, "DOLPHIN_FEATURES", "exf_latest"))
if not isinstance(exf, Mapping):
return 1.0, 0.0
LOGGER.debug("ACB: no exf_latest — keeping prior boost/beta=%s", fallback)
return fallback
eigen = _jsonish(_read_hz_map(client, "DOLPHIN_FEATURES", "latest_eigen_scan"))
w750 = _coerce_float(eigen.get("w750_velocity"), None) if isinstance(eigen, Mapping) else None
acb = acb or AdaptiveCircuitBreaker()
@@ -190,13 +219,17 @@ def _source_boost_beta(
date_str=date_str,
exf_snapshot=dict(exf),
w750_velocity=float(w750) if w750 else None, # 0.0 → None, matches BLUE
direction=trade_direction,
direction=trade_direction, # NO ob_engine — matches BLUE live
)
except ValueError:
return 1.0, 0.0 # ACB Stale Data Fallback (BLUE keeps prior; VIOLET neutral)
boost = _coerce_float(info.get("boost"), 1.0) or 1.0
beta = _coerce_float(info.get("beta"), 0.0) or 0.0
return max(0.0, boost), max(0.0, beta)
except ValueError as exc:
# BLUE logs "ACB Stale Data Fallback" and keeps the prior boost/beta.
LOGGER.info("ACB Stale Data Fallback (%s) — keeping prior boost/beta=%s", exc, fallback)
return fallback
boost = max(0.0, _coerce_float(info.get("boost"), 1.0) or 1.0)
beta = max(0.0, _coerce_float(info.get("beta"), 0.0) or 0.0)
LOGGER.debug("ACB live boost=%.6f beta=%.6f (signals=%s w750=%s dir=%s)",
boost, beta, info.get("signals"), w750, trade_direction)
return boost, beta
def _scan_view(payload: Any) -> Mapping[str, Any]:
@@ -349,6 +382,7 @@ def source_live_blue_sizing_factors(
ob_engine: Optional[OBFeatureEngine] = None,
bar_idx: int = 0,
date_str: Optional[str] = None,
prior_boost_beta: Optional[tuple[float, float]] = None,
) -> LiveBlueSourceResult:
"""Read BLUE's live surfaces and RECONSTRUCT the factor plane the way BLUE computes it.
@@ -401,6 +435,7 @@ def source_live_blue_sizing_factors(
# acb_boost). Needs trade_direction, so computed after it.
acb_boost, acb_beta = _source_boost_beta(
client, date_str=today, trade_direction=trade_direction, acb=acb,
prior=prior_boost_beta,
)
candidate_market = scan_history.market_data(selector.lookback)
@@ -425,6 +460,12 @@ def source_live_blue_sizing_factors(
"posture": posture,
}
factors = extract_live_sizing_factors(hz_snapshot=hz_snapshot)
LOGGER.debug(
"live BLUE plane: asset=%s posture=%s boost=%.4f beta=%.4f mc_scale=%.2f "
"esof=%s ob=(%s,%s) dc=%s dir=%s",
selected_asset, posture, acb_boost, acb_beta, mc_scale, esof_score,
ob_median_imbalance, ob_agreement_pct, dc_status, trade_direction,
)
return LiveBlueSourceResult(
factors=factors,
acb_boost=acb_boost,

View File

@@ -130,6 +130,43 @@ def test_boost_beta_neutral_on_stale_exf_valueerror():
assert _source_boost_beta(client, date_str="2026-06-16", trade_direction=-1) == (1.0, 0.0)
def test_boost_beta_keeps_prior_on_stale_exf():
# BLUE keeps _day_base_boost/_day_beta on stale exf; VIOLET keeps the seeded prior.
stale = dict(_EXF, _staleness_s={"funding_btc": 999999.0})
client = _FakeClient({"DOLPHIN_FEATURES": {"exf_latest": json.dumps(stale),
"latest_eigen_scan": json.dumps({"w750_velocity": 0.001})}})
assert _source_boost_beta(client, date_str="2026-06-16", trade_direction=-1,
prior=(1.37, 0.8)) == (1.37, 0.8)
def test_boost_beta_keeps_prior_when_no_exf():
client = _FakeClient({"DOLPHIN_FEATURES": {}})
assert _source_boost_beta(client, date_str="2026-06-16", trade_direction=-1,
prior=(1.5, 0.2)) == (1.5, 0.2)
def test_boost_beta_cold_start_no_prior_is_neutral():
# No prior AND no fresh exf (cold start) → neutral identity, never silently wrong.
client = _FakeClient({"DOLPHIN_FEATURES": {}})
assert _source_boost_beta(client, date_str="2026-06-16", trade_direction=-1, prior=None) == (1.0, 0.0)
def test_source_live_blue_threads_prior_boost_beta_on_stale():
stale = dict(_EXF, _staleness_s={"funding_btc": 999999.0})
client = _FakeClient({
"DOLPHIN_FEATURES": {
"exf_latest": json.dumps(stale),
"mc_forewarner_latest": json.dumps({"catastrophic_prob": 0.02, "envelope_score": 0.9}),
"latest_eigen_scan": json.dumps({"assets": ["BTCUSDT"], "asset_prices": [100.0], "w750_velocity": 0.001}),
},
"DOLPHIN_STATE_BLUE": {"latest_nautilus": json.dumps({"posture": "APEX"})},
})
res = source_live_blue_sizing_factors(
client, assets=["BTCUSDT"], ob_engine=_FakeOBEngine(), prior_boost_beta=(1.42, 0.8),
)
assert res.factors.boost == 1.42 and res.factors.beta == 0.8
def test_boost_beta_does_not_read_published_acb_boost():
# An acb_boost scalar is present but MUST be ignored (BLUE recomputes from exf).
eigen = {"w750_velocity": 0.0012}