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:
203
prod/clean_arch/violet/live_factors.py
Normal file
203
prod/clean_arch/violet/live_factors.py
Normal 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()
|
||||||
101
prod/clean_arch/violet/test_violet_live_factors.py
Normal file
101
prod/clean_arch/violet/test_violet_live_factors.py
Normal 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})
|
||||||
68
prod/docs/VIOLET_OA_DEV_STATUS.md
Normal file
68
prod/docs/VIOLET_OA_DEV_STATUS.md
Normal 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.
|
||||||
Reference in New Issue
Block a user