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
+105
View File
@@ -11,6 +11,7 @@ from __future__ import annotations
import json
import logging
import math
from dataclasses import dataclass, field
from datetime import datetime, timezone
@@ -955,3 +956,107 @@ def apply_accelerated_decay(
return accelerated
return standard_decay
# ---------------------------------------------------------------------------
# V3 Macro Layer — Noisy-OR Exposure & LLR Emission (Requirements: 9.19.5)
# ---------------------------------------------------------------------------
# Noisy-OR weights per dimension
_V3_MACRO_WEIGHTS: dict[str, float] = {
"geo": 0.35,
"supply": 0.25,
"commodity": 0.25,
"sector": 0.15,
}
# Resilience dampener per tier
_V3_RESILIENCE_DAMPENER: dict[str, float] = {
"global_leader": 0.70,
"multinational": 0.85,
"regional": 1.00,
"domestic": 1.20,
}
def compute_normalized_macro_exposure(
overlaps: dict[str, float],
tier: str = "regional",
) -> float:
"""Compute normalized macro exposure via noisy-OR.
E_raw = 1 - product(1 - w_k × O_k)
E_max = 1 - product(1 - w_k)
E_macro = E_raw / E_max × resilience_dampener
Args:
overlaps: Dimension overlap values keyed by 'geo', 'supply',
'commodity', 'sector'. Missing keys treated as 0.0.
tier: Market position tier for resilience dampening.
Returns:
Normalized macro exposure in [0, ∞) (can exceed 1.0 for domestic
tier due to 1.20 dampener, but typically in [0, ~1.2]).
Requirements: 9.1, 9.2, 9.3
"""
# E_raw = 1 - product(1 - w_k × O_k)
product_raw = 1.0
for dim, weight in _V3_MACRO_WEIGHTS.items():
o_k = max(0.0, min(1.0, overlaps.get(dim, 0.0)))
product_raw *= (1.0 - weight * o_k)
e_raw = 1.0 - product_raw
# E_max = 1 - product(1 - w_k) — theoretical max when all overlaps = 1.0
product_max = 1.0
for weight in _V3_MACRO_WEIGHTS.values():
product_max *= (1.0 - weight)
e_max = 1.0 - product_max
# Guard against zero (should never happen with default weights)
if e_max <= 0.0:
return 0.0
# Normalize to [0, 1]
e_macro = e_raw / e_max
# Apply resilience dampener per tier
dampener = _V3_RESILIENCE_DAMPENER.get(tier, 1.0)
return e_macro * dampener
def compute_macro_llr(
macro_impact: float,
event_confidence: float,
q_recency: float,
macro_direction: int,
) -> float:
"""Compute macro LLR for shared posterior.
p_macro = clamp(0.50 + 0.30 × macro_impact × event_confidence × q_recency, 0.501, 0.80)
LLR_macro = macro_direction × ln(p_macro / (1 - p_macro))
When macro_direction == 0, returns 0.0 (neutral — no directional signal).
Args:
macro_impact: Normalized macro impact score (typically [0, 1]).
event_confidence: Event classification confidence [0, 1].
q_recency: Recency quality factor [0, 1].
macro_direction: +1 for positive, -1 for negative, 0 for neutral.
Returns:
Log-likelihood ratio for the macro signal. Feeds directly into the
shared posterior without separate post-hoc modifier.
Requirements: 9.4, 9.5
"""
if macro_direction == 0:
return 0.0
# p_macro = clamp(0.50 + 0.30 × macro_impact × event_confidence × q_recency, 0.501, 0.80)
p_macro = 0.50 + 0.30 * macro_impact * event_confidence * q_recency
p_macro = max(0.501, min(0.80, p_macro))
# LLR_macro = macro_direction × ln(p_macro / (1 - p_macro))
llr = macro_direction * math.log(p_macro / (1.0 - p_macro))
return llr