VIOLET V3.4c review: fix mc_scale BLUE-parity bug + document re-derivation debt
REVIEW of the V3.4c/d/e work (other agents) for BLUE-algo compliance found one
real bug and one architectural concern.
BUG FIXED — mc_scale derivation (live_blue_source.py):
The adapter mapped `mc_forewarner_latest.status == "ORANGE"` → 0.5. But BLUE's sizing
mc_scale is NOT the MC service's `status` label (that label, from mc_forewarner_flow.py,
uses GREEN<0.10/ORANGE<0.30/RED — observability only). The live trader re-derives the
haircut in begin_day (esf_alpha_orchestrator.py:956-962) from the SAME published fields
`catastrophic_prob` + `envelope_score` with DIFFERENT thresholds:
mc_red = cat>0.25 or env<-1.0
mc_orange = (not mc_red) and (env<0 or cat>0.10)
mc_scale = 0.5 if mc_orange else 1.0
The two disagree (e.g. cat=0.05/env=-0.5 → label GREEN→1.0 but BLUE orange→0.5;
cat=0.28 → label ORANGE→0.5 but BLUE red→1.0). Rewrote `_derive_mc_scale` to mirror
begin_day exactly on the source fields. Per operator: there is genuine ambiguity over
which surface is "nominal" — we go with the SOURCE FIELDS + begin_day formula (the path
that actually drives BLUE's sizing); the ambiguity + decision are noted in the docstring.
TESTS — the old fixtures were complicit: they fed `{"status":"ORANGE"}`, a payload shape
BLUE never emits, so they "passed" against fiction. Replaced with BLUE's real payload
(`catastrophic_prob`/`envelope_score`) and added a parametrized formula test covering the
exact divergence cases the status-based code got wrong, plus missing/garbage-field neutral.
15 passed (live-HZ smoke deselected).
ARCHITECTURAL DEBT documented — VIOLET_BLUE_PARITY_STRUCTURAL_DIVERGENCE.md:
VIOLET imitates BLUE's computations in a DIFFERENT module/scope structure. Kernels are
WRAPPED (safe), but orchestration arithmetic (compose/regime/ob/strength/mc_scale) is
HAND-REPLICATED out of the monolithic NDAlphaEngine — making orderly, verifiable BLUE↔VIOLET
parity comparison and refactoring hard. Doc inventories every re-derivation with BLUE's
file:line authority, the drift risk, and mitigations (parity-pin tests; single canonical
surface per ambiguous factor; eventual DITAv2 Rust-backplane convergence). Pointer added to
live_blue_source.py's module docstring.
Also reviewed (no change needed): launcher launch_dolphin_violet.py is DARK-safe
(ObserveOnlyVenue + shadow gated default-OFF, no order path); shadow_live_factors.py glue
is correct; trade_slot_compare.py reimplements no BLUE algo. fb34431's launcher tests —
which the authoring agent never ran on the slow mount — pass (14 green).
violet-only; no shared-file edits.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,18 @@ translates them into ``SizingFactors`` for the shadow path:
|
||||
|
||||
The remaining DC signal is left neutral here for now. It needs the same live
|
||||
signal-history path BLUE uses and should be added as a separate mirror step.
|
||||
|
||||
PARITY DEBT (see prod/docs/VIOLET_BLUE_PARITY_STRUCTURAL_DIVERGENCE.md): this module
|
||||
reconstructs BLUE's factors in a DIFFERENT file/scope structure than BLUE's monolithic
|
||||
NDAlphaEngine (esf_alpha_orchestrator.py). Kernels here are WRAPPED (OBFeatureEngine,
|
||||
AlphaSignalGenerator, VioletAssetSelector — single source of truth), but two derivations
|
||||
are HAND-REPLICATED / surface-substituted and can drift silently from BLUE:
|
||||
- ``_derive_mc_scale`` transcribes begin_day's mc thresholds (no pin to BLUE's fn);
|
||||
- ``AlphaSignalGenerator()`` is built with BARE DEFAULTS, not BLUE's threaded params
|
||||
(esf_alpha_orchestrator.py:180-191) — cosmetic only while dc_leverage_boost==1.0;
|
||||
- boost/beta read the PUBLISHED ``acb_boost`` (acb_processor_service), not the trader's
|
||||
``get_dynamic_boost_from_hz`` recompute — the two surfaces may differ.
|
||||
Any change to BLUE's corresponding formula REQUIRES a matching change here + a test.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -72,13 +84,41 @@ def _read_hz_map(client: hazelcast.HazelcastClient, map_name: str, key: str) ->
|
||||
return None
|
||||
|
||||
|
||||
def _map_status_to_mc_scale(payload: Any) -> float:
|
||||
def _derive_mc_scale(payload: Any) -> float:
|
||||
"""Mirror BLUE ``begin_day``'s ``mc_scale`` EXACTLY (esf_alpha_orchestrator.py:956-962).
|
||||
|
||||
BLUE does NOT use the MC service's published ``status`` label for sizing — that label
|
||||
(mc_forewarner_flow.py: GREEN<0.10 / ORANGE<0.30 / RED) is observability-only and uses
|
||||
DIFFERENT thresholds from the engine. The live trader re-derives the size haircut from
|
||||
the SAME published fields ``catastrophic_prob`` + ``envelope_score`` with the engine's
|
||||
own thresholds:
|
||||
|
||||
mc_red = catastrophic_prob > 0.25 or envelope_score < -1.0
|
||||
mc_orange = (not mc_red) and (envelope_score < 0 or catastrophic_prob > 0.10)
|
||||
mc_scale = 0.5 if mc_orange else 1.0 # RED → 1.0 here; BLUE halts via regime_dd_halt
|
||||
|
||||
Reading ``status`` instead diverges (e.g. cat=0.05/env=-0.5 → publisher GREEN→1.0 but
|
||||
BLUE orange→0.5).
|
||||
|
||||
AMBIGUITY (flagged 2026-06-16): the MC service's ``status`` and the engine's begin_day
|
||||
thresholds genuinely disagree, and from OUTSIDE BLUE there is no way to know which is the
|
||||
"nominal" intent — they are two independent threshold sets over the same numbers. We go
|
||||
with the SOURCE FIELDS + begin_day formula because that is the path that actually drives
|
||||
BLUE's live sizing (the published ``status`` is consumed only by the TUI/observability).
|
||||
If BLUE's begin_day thresholds change, THIS must change with them. Operator confirmation
|
||||
of the canonical surface is still desirable.
|
||||
|
||||
Missing/unparseable fields → neutral 1.0 (no haircut)."""
|
||||
data = _jsonish(payload)
|
||||
if isinstance(data, Mapping):
|
||||
status = str(data.get("status", "")).upper()
|
||||
else:
|
||||
status = str(data).upper()
|
||||
return 0.5 if status == "ORANGE" else 1.0
|
||||
if not isinstance(data, Mapping):
|
||||
return 1.0
|
||||
cat = _coerce_float(data.get("catastrophic_prob"), None)
|
||||
env = _coerce_float(data.get("envelope_score"), None)
|
||||
if cat is None or env is None:
|
||||
return 1.0
|
||||
mc_red = cat > 0.25 or env < -1.0
|
||||
mc_orange = (not mc_red) and (env < 0.0 or cat > 0.10)
|
||||
return 0.5 if mc_orange else 1.0
|
||||
|
||||
|
||||
def _extract_acb(payload: Any) -> tuple[float, float]:
|
||||
@@ -302,7 +342,7 @@ def source_live_blue_sizing_factors(
|
||||
acb_boost, acb_beta = _extract_acb(acb_raw)
|
||||
|
||||
mc_raw = _read_hz_map(client, "DOLPHIN_FEATURES", "mc_forewarner_latest")
|
||||
mc_scale = _map_status_to_mc_scale(mc_raw)
|
||||
mc_scale = _derive_mc_scale(mc_raw)
|
||||
|
||||
scan_raw = _read_hz_map(client, "DOLPHIN_FEATURES", "latest_eigen_scan")
|
||||
scan = scan_history.ingest_scan(scan_raw)
|
||||
|
||||
Reference in New Issue
Block a user