feat: math core v3 engine upgrade

This commit is contained in:
Celes Renata
2026-06-27 12:21:41 +00:00
parent 365bc5d4b7
commit b4bf0f2361
34 changed files with 11693 additions and 3 deletions
+291
View File
@@ -10,6 +10,10 @@ All decisions are rule-based with no model involvement. The LLM is only
used downstream for optional thesis wording (a separate task).
Requirements: 7.1, 7.2, 7.3, 7.4, 14.1, 14.2, 14.3, 14.4, 14.5, 14.6
v3 additions:
- ReturnDistribution and EV gate (Requirement 12)
- Regime-aware eligibility and mode escalation (Requirements 13.113.5)
"""
from __future__ import annotations
@@ -464,3 +468,290 @@ def evaluate_eligibility(
p_bull=p_bull if probabilistic else None,
pipeline_mode="probabilistic" if probabilistic else "heuristic",
)
# ---------------------------------------------------------------------------
# V3 Return Distribution and EV Gate (Requirements 12.112.7)
# ---------------------------------------------------------------------------
_V3_MIN_EDGE: dict[str, float] = {
"panic": 0.0100,
"trend_following": 0.0035,
"mean_reversion": 0.0050,
"uncertainty": 0.0075,
}
_V3_ELIGIBILITY_THRESHOLDS: dict[str, tuple[float, float, float]] = {
# (confidence_min, contradiction_max, strength_min)
"panic": (0.70, 0.25, 0.36),
"trend_following": (0.55, 0.40, 0.20),
"mean_reversion": (0.60, 0.35, 0.26),
"uncertainty": (0.65, 0.30, 0.30),
}
@dataclass(frozen=True)
class ReturnDistribution:
"""V3 return distribution model output.
Encapsulates the horizon-scaled volatility, expected return, risk-adjusted
expected value, regime minimum edge, and final eligibility decision.
Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7
"""
sigma_h: float # realized_vol_20d * sqrt(horizon_days / 252)
mu_h: float # tanh(A_projected / 3.0) * confidence * sigma_h
ev_long: float # mu_h - costs - 0.10 * CVaR_5
min_edge: float # regime-specific minimum edge
eligible: bool
def compute_return_distribution(
a_projected: float,
confidence: float,
realized_vol_20d: float,
horizon_days: int,
costs: float,
regime: str,
*,
confidence_actual: float = 1.0,
contradiction: float = 0.0,
n_eff_total: float = 0.0,
data_quality: float = 1.0,
) -> ReturnDistribution:
"""Compute the v3 return distribution and EV gate eligibility.
Implements the return distribution model from the v3 math spec:
- sigma_h: horizon-scaled volatility
- mu_h: expected return using tanh-compressed projected alpha
- CVaR_5: Gaussian approximation of 5th percentile tail loss
- EV_long: risk-adjusted expected value after costs and tail risk
Eligibility requires:
- EV_long > regime min_edge
- EV_long > max(0.0025, 0.25 * costs)
- confidence_actual >= regime confidence_min
- contradiction <= regime contradiction_max
- n_eff_total >= 2.0
- data_quality >= 0.50
Args:
a_projected: Projected evidence state A_projected_h from projection.
confidence: Multiplicative confidence from the v3 pipeline.
realized_vol_20d: 20-day realized annualized volatility.
If <= 0 or unavailable, defaults to 0.25.
horizon_days: Trading days for the horizon (1, 7, 30, or 90).
costs: Total costs (spread + slippage + commission).
regime: Market regime string (panic, trend_following, mean_reversion, uncertainty).
confidence_actual: The raw confidence value for threshold checks (defaults to
same as confidence if not separately provided).
contradiction: Contradiction score in [0, 1].
n_eff_total: Total effective evidence count across clusters.
data_quality: Data quality score in [0, 1].
Returns:
ReturnDistribution with computed fields and eligibility decision.
Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7
"""
# Default volatility when unavailable (Req 12.7)
if realized_vol_20d is None or realized_vol_20d <= 0: # type: ignore[redundant-expr]
realized_vol_20d = 0.25
# Req 12.1: sigma_h = realized_vol_20d * sqrt(horizon_days / 252)
sigma_h = realized_vol_20d * math.sqrt(horizon_days / 252.0)
# Req 12.2: mu_h = tanh(A_projected / 3.0) * confidence * sigma_h
mu_h = math.tanh(a_projected / 3.0) * confidence * sigma_h
# Req 12.3: CVaR_5 = sigma_h * 1.645 * 1.4
cvar_5 = sigma_h * 1.645 * 1.4
# Req 12.3: EV_long = mu_h - costs - 0.10 * CVaR_5
ev_long = mu_h - costs - 0.10 * cvar_5
# Req 12.4: regime-specific min_edge
min_edge = _V3_MIN_EDGE.get(regime, _V3_MIN_EDGE["uncertainty"])
# Req 12.5: EV_long > min_edge AND EV_long > max(0.0025, 0.25 * costs)
ev_gate_passed = ev_long > min_edge and ev_long > max(0.0025, 0.25 * costs)
# Req 12.6: Additional eligibility checks
thresholds = _V3_ELIGIBILITY_THRESHOLDS.get(
regime, _V3_ELIGIBILITY_THRESHOLDS["uncertainty"]
)
confidence_min, contradiction_max, _strength_min = thresholds
quality_gate_passed = (
confidence_actual >= confidence_min
and contradiction <= contradiction_max
and n_eff_total >= 2.0
and data_quality >= 0.50
)
eligible = ev_gate_passed and quality_gate_passed
return ReturnDistribution(
sigma_h=sigma_h,
mu_h=mu_h,
ev_long=ev_long,
min_edge=min_edge,
eligible=eligible,
)
# ===========================================================================
# v3 Regime-Aware Eligibility and Mode Escalation (Requirements 13.113.5)
# ===========================================================================
# Direction thresholds for action mapping (from bayesian.py)
_V3_DIRECTION_THRESHOLDS: dict[str, tuple[float, float]] = {
"panic": (0.68, 0.32),
"trend_following": (0.60, 0.40),
"mean_reversion": (0.63, 0.37),
"uncertainty": (0.65, 0.35),
}
@dataclass(frozen=True)
class V3Eligibility:
"""Result of v3 regime-aware eligibility evaluation.
Requirements: 13.113.5
"""
action: str # "BUY", "SELL", "HOLD", "WATCH"
mode: str # "live", "paper", "informational"
eligible: bool # meets regime thresholds
reasons: list[str] # reasons for non-eligibility or downgrade
def compute_v3_eligibility(
p_up: float,
ev_long: float,
min_edge: float,
confidence: float,
contradiction: float,
strength: float,
n_eff_total: float,
data_quality: float,
regime: str,
has_existing_position: bool = False,
risk_engine_passed: bool = True,
) -> V3Eligibility:
"""Compute regime-aware eligibility and mode escalation.
Requirements: 13.113.5
Steps:
1. Check regime-specific eligibility gates (confidence, contradiction, strength)
2. Determine action (BUY/SELL/HOLD/WATCH) based on P_up and EV
3. Escalate mode (live/paper/informational) based on signal quality
Args:
p_up: Posterior probability of upward move from Bayesian posterior.
ev_long: Expected value from return distribution.
min_edge: Regime-specific minimum edge from EV gate.
confidence: Multiplicative confidence score in [0, 1].
contradiction: LLR entropy contradiction in [0, 1].
strength: Signal strength = abs(2 * P_up - 1).
n_eff_total: Effective evidence count across all clusters.
data_quality: Data quality score in [0, 1].
regime: Current market regime string.
has_existing_position: Whether the entity already has an open position.
risk_engine_passed: Whether the risk engine approved the trade.
Returns:
V3Eligibility with action, mode, eligible flag, and reasons.
"""
reasons: list[str] = []
# --- 1. Regime-specific eligibility gates (Requirement 13.1) ---
conf_min, contra_max, str_min = _V3_ELIGIBILITY_THRESHOLDS.get(
regime, (0.65, 0.30, 0.30) # default to uncertainty thresholds
)
eligible = True
if confidence < conf_min:
eligible = False
reasons.append(f"confidence {confidence:.3f} < regime min {conf_min:.2f}")
if contradiction > contra_max:
eligible = False
reasons.append(
f"contradiction {contradiction:.3f} > regime max {contra_max:.2f}"
)
if strength < str_min:
eligible = False
reasons.append(f"strength {strength:.3f} < regime min {str_min:.2f}")
if data_quality < 0.50:
eligible = False
reasons.append(f"data_quality {data_quality:.3f} < 0.50")
if n_eff_total < 2.0:
eligible = False
reasons.append(f"n_eff_total {n_eff_total:.2f} < 2.0")
# --- 2. Action mapping (Requirement 13.2) ---
bull_thresh, _bear_thresh = _V3_DIRECTION_THRESHOLDS.get(
regime, (0.65, 0.35)
)
if not eligible:
action = "WATCH"
elif p_up >= bull_thresh and ev_long > min_edge:
action = "BUY"
elif has_existing_position and ev_long <= 0:
# SELL when existing position and exit EV > hold EV
# Simplified: EV_exit > EV_hold approximated as ev_long <= 0
# (holding has negative expected value → better to exit)
action = "SELL"
elif has_existing_position:
action = "HOLD"
else:
action = "WATCH"
# --- 3. Mode escalation (Requirements 13.3, 13.4, 13.5) ---
if action in ("BUY", "SELL"):
# Check live eligibility (Requirement 13.3)
if (
confidence >= 0.75
and contradiction <= 0.20
and n_eff_total >= 5
and ev_long > 2 * min_edge
and risk_engine_passed
):
mode = "live"
# Check paper eligibility (Requirement 13.4)
elif (
confidence >= 0.60
and ev_long > min_edge
and risk_engine_passed
):
mode = "paper"
else:
mode = "informational"
if confidence < 0.60:
reasons.append(
f"paper requires confidence >= 0.60, got {confidence:.3f}"
)
if ev_long <= min_edge:
reasons.append(
f"paper requires EV > min_edge ({min_edge:.4f}), got {ev_long:.4f}"
)
if not risk_engine_passed:
reasons.append("risk engine did not pass")
else:
# HOLD and WATCH are always informational (Requirement 13.5)
mode = "informational"
return V3Eligibility(
action=action,
mode=mode,
eligible=eligible,
reasons=reasons,
)