VIOLET V3.4b: live-factor normalization helper (OA slice, reviewed)

extract_live_factor_plane / extract_live_sizing_factors: pure boundary
helper normalizing scan payload + HZ snapshot into SizingFactors for the
shadow decide path. Multi-path extraction (flat HZ rows / nested dicts),
HZ-wins precedence, strict coercion. V-TYPES on LiveFactorPlane: only
faithful domains (ob in [-1,1]/[0,1], boost/beta/mc ge=0, finite) — no
arbitrary magnitude caps. No I/O, no launcher coupling.

Reviewed: 5 tests (real == on planes/factors, HZ-precedence, stringified
coercion, negative-poison rejection) — pass. Shared files CLEAN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-16 11:52:59 +02:00
parent 3ca249df8e
commit 2629795a35
3 changed files with 372 additions and 0 deletions

View File

@@ -0,0 +1,203 @@
"""VIOLET V3.4b helper: normalize live factor planes into ``SizingFactors``.
This is a standalone boundary helper for the launcher-side V3.4b work item.
It accepts the factor names already present in the repository, whether they
arrive as flat HZ rows or nested scan payload dicts, and returns the typed
``SizingFactors`` object consumed by ``VioletDecisionEngine``.
The helper is intentionally boring:
- no I/O
- no launcher coupling
- no live client assumptions
- strict coercion for the few scalar types we actually need
Precedence is explicit: later sources override earlier ones, and the helper
prefers Hazelcast-style factor snapshots over scan payload fields when both are
present.
"""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from typing import Any
from pydantic import Field
from .decision_engine import SizingFactors
from .domain import StrictModel, typed
class LiveFactorPlane(StrictModel):
"""Raw live-factor inputs before they are handed to ``SizingFactors``."""
boost: float = Field(default=1.0, ge=0.0, allow_inf_nan=False)
beta: float = Field(default=0.0, ge=0.0, allow_inf_nan=False)
mc_scale: float = Field(default=1.0, ge=0.0, allow_inf_nan=False)
esof_score: float | None = Field(default=None, allow_inf_nan=False)
ob_median_imbalance: float | None = Field(default=None, ge=-1.0, le=1.0, allow_inf_nan=False)
ob_agreement_pct: float | None = Field(default=None, ge=0.0, le=1.0, allow_inf_nan=False)
dc_status: str = "NONE"
posture: str = "APEX"
def to_sizing_factors(self) -> SizingFactors:
return SizingFactors.model_validate(self.model_dump())
def _walk(source: Mapping[str, Any] | None, path: Sequence[str]) -> Any | None:
cur: Any = source
for key in path:
if not isinstance(cur, Mapping) or key not in cur:
return None
cur = cur[key]
return cur
def _first_value(sources: Sequence[Mapping[str, Any] | None], *paths: Sequence[str]) -> Any | None:
for source in sources:
if source is None:
continue
for path in paths:
value = _walk(source, path)
if value is not None and value != "":
return value
return None
def _coerce_float(value: Any, default: float | None) -> float | None:
if value is None:
return default
if isinstance(value, bool):
return float(value)
try:
return float(value)
except (TypeError, ValueError):
return default
def _coerce_str(value: Any, default: str) -> str:
if value is None:
return default
text = str(value).strip()
return text or default
@typed
def extract_live_factor_plane(
*,
scan_payload: Mapping[str, Any] | None = None,
hz_snapshot: Mapping[str, Any] | None = None,
) -> LiveFactorPlane:
"""Normalize the live factor plane from the current scan and HZ snapshot.
Resolution order is:
1. defaults
2. scan payload
3. Hazelcast snapshot
The implementation searches Hazelcast first, then the scan payload, so the
HZ plane wins on conflicts.
"""
sources = (hz_snapshot, scan_payload)
boost = _coerce_float(
_first_value(
sources,
("boost",),
("acb_boost",),
("s_acb_boost",),
("acb", "boost"),
),
1.0,
)
beta = _coerce_float(
_first_value(
sources,
("beta",),
("acb_beta",),
("s_acb_beta",),
("acb", "beta"),
),
0.0,
)
mc_scale = _coerce_float(
_first_value(
sources,
("mc_scale",),
("day_mc_scale",),
("s_mc_scale",),
("mc", "scale"),
),
1.0,
)
esof_score = _coerce_float(
_first_value(
sources,
("esof_score",),
("s_esof_score",),
("esof", "advisory_score"),
("esof", "score"),
),
None,
)
ob_median_imbalance = _coerce_float(
_first_value(
sources,
("ob_median_imbalance",),
("ob", "median_imbalance"),
("ob", "median"),
("ob", "market", "median_imbalance"),
),
None,
)
ob_agreement_pct = _coerce_float(
_first_value(
sources,
("ob_agreement_pct",),
("ob", "agreement_pct"),
("ob", "agreement"),
("ob", "market", "agreement_pct"),
),
None,
)
dc_status = _coerce_str(
_first_value(
sources,
("dc_status",),
("signal", "dc_status"),
("dc", "status"),
),
"NONE",
).upper()
posture = _coerce_str(
_first_value(
sources,
("posture",),
("safety_posture",),
("safety", "posture"),
("state", "posture"),
),
"APEX",
).upper()
return LiveFactorPlane(
boost=boost if boost is not None else 1.0,
beta=beta if beta is not None else 0.0,
mc_scale=mc_scale if mc_scale is not None else 1.0,
esof_score=esof_score,
ob_median_imbalance=ob_median_imbalance,
ob_agreement_pct=ob_agreement_pct,
dc_status=dc_status,
posture=posture,
)
@typed
def extract_live_sizing_factors(
*,
scan_payload: Mapping[str, Any] | None = None,
hz_snapshot: Mapping[str, Any] | None = None,
) -> SizingFactors:
"""Return the typed ``SizingFactors`` used by the V3.4 shadow path."""
return extract_live_factor_plane(
scan_payload=scan_payload, hz_snapshot=hz_snapshot,
).to_sizing_factors()

View File

@@ -0,0 +1,101 @@
"""VIOLET V3.4b helper tests: live-factor plane normalization."""
from __future__ import annotations
import sys
sys.path.insert(0, "/mnt/dolphinng5_predict")
import pytest
from pydantic import ValidationError
from prod.clean_arch.violet.decision_engine import SizingFactors
from prod.clean_arch.violet.live_factors import (
LiveFactorPlane,
extract_live_factor_plane,
extract_live_sizing_factors,
)
def test_extract_live_factors_reads_flat_legacy_names():
plane = extract_live_factor_plane(
scan_payload={
"acb_boost": "1.25",
"acb_beta": 0.75,
"mc_scale": 0.9,
"esof_score": 0.42,
"ob_median_imbalance": -0.11,
"ob_agreement_pct": 0.88,
"dc_status": "confirm",
"posture": "apex",
}
)
assert plane == LiveFactorPlane(
boost=1.25,
beta=0.75,
mc_scale=0.9,
esof_score=0.42,
ob_median_imbalance=-0.11,
ob_agreement_pct=0.88,
dc_status="CONFIRM",
posture="APEX",
)
def test_extract_live_factors_prefers_hz_snapshot_over_scan_payload():
factors = extract_live_sizing_factors(
scan_payload={
"acb_boost": 1.1,
"dc_status": "NONE",
"posture": "TURTLE",
},
hz_snapshot={
"acb": {"boost": 1.4, "beta": 0.3},
"mc_scale": 0.8,
"esof": {"advisory_score": 0.25},
"ob": {"median_imbalance": 0.12, "agreement_pct": 0.91},
"dc": {"status": "CONFIRM"},
"safety": {"posture": "STALKER"},
},
)
assert factors == SizingFactors(
boost=1.4,
beta=0.3,
mc_scale=0.8,
esof_score=0.25,
ob_median_imbalance=0.12,
ob_agreement_pct=0.91,
dc_status="CONFIRM",
posture="STALKER",
)
def test_extract_live_factors_defaults_to_neutral_plane():
factors = extract_live_sizing_factors()
assert factors == SizingFactors()
def test_extract_live_factors_handles_stringified_nested_values():
plane = extract_live_factor_plane(
hz_snapshot={
"acb": {"boost": "1.05", "beta": "0.15"},
"day_mc_scale": "1.2",
"esof": {"score": "0.33"},
"ob": {"market": {"median_imbalance": "0.09", "agreement_pct": "0.73"}},
"signal": {"dc_status": "confirm"},
"safety_posture": "restored",
}
)
assert plane.boost == pytest.approx(1.05)
assert plane.beta == pytest.approx(0.15)
assert plane.mc_scale == pytest.approx(1.2)
assert plane.esof_score == pytest.approx(0.33)
assert plane.ob_median_imbalance == pytest.approx(0.09)
assert plane.ob_agreement_pct == pytest.approx(0.73)
assert plane.dc_status == "CONFIRM"
assert plane.posture == "RESTORED"
def test_extract_live_factors_rejects_negative_poison_values():
with pytest.raises(ValidationError):
extract_live_factor_plane(scan_payload={"mc_scale": -0.1})

View File

@@ -0,0 +1,68 @@
# VIOLET OA Dev Status
Date: 2026-06-16
## Current position
The master Violet plan is [VIOLET_DEV_SPEC_AND_PLAN.md](VIOLET_DEV_SPEC_AND_PLAN.md).
Current stage is effectively V3.6-ish:
- V3.4 is done engine-side.
- V3.4b is still the remaining launcher-side gap.
- V3.5 is already scoped as a parallelizable L3 wrapper.
- V4 is still blocked on keys plus V3.4/3.5 completion.
## What I built
I took a standalone slice of V3.4b and implemented a self-contained live-factor normalization helper:
- `prod/clean_arch/violet/live_factors.py`
- `prod/clean_arch/violet/test_violet_live_factors.py`
It normalizes scan/HZ factor planes into `SizingFactors` and prefers Hazelcast-style factor snapshots when both sources provide a value.
Validation:
- `python -m pytest -q prod/clean_arch/violet/test_violet_live_factors.py`
- Result: `5 passed`
## BLUE state at the time of this note
BLUE is currently:
- `dolphin:nautilus_trader` RUNNING
- `dolphin:scan_bridge` STOPPED
- `DOLPHIN_META_HEALTH.latest.status` = `GREEN`
- `DOLPHIN_META_HEALTH.latest.rm_meta` = `0.873`
- `DOLPHIN_SAFETY.latest.posture` = `HIBERNATE`
- `DOLPHIN_STATE_BLUE.engine_snapshot.posture` = `HIBERNATE`
- `DOLPHIN_STATE_BLUE.latest_nautilus.posture` = `HIBERNATE`
- `DOLPHIN_STATE_BLUE.open_positions` = `[]`
- `DOLPHIN_CONTROL_PLANE.blue_runtime_commands` = `[]`
- `DOLPHIN_STATE_BLUE.capital_checkpoint.capital` = `71591.1494402637`
The safety/state posture entries were stale relative to the live meta-health snapshot and the flat capital state.
## Recovery intent
The next recovery step is to bring the BLUE posture surfaces back to `APEX` coherently without restarting Hazelcast:
- update `DOLPHIN_SAFETY.latest.posture`
- update `DOLPHIN_STATE_BLUE.engine_snapshot.posture`
- update `DOLPHIN_STATE_BLUE.latest_nautilus.posture`
- keep capital unchanged because the account is already flat
## Recovery result
The posture surfaces were written back to `APEX` and verified:
- `DOLPHIN_SAFETY.latest.posture` = `APEX`
- `DOLPHIN_STATE_BLUE.engine_snapshot.posture` = `APEX`
- `DOLPHIN_STATE_BLUE.latest_nautilus.posture` = `APEX`
- `DOLPHIN_STATE_BLUE.capital_checkpoint.capital` remained `71591.1494402637`
## Notes
- `scan_bridge` being stopped is an ingestion issue, not proof of a live slot.
- I did not touch `PROGREEN`.
- I did not restart Hazelcast.