"""Explicit factor roles for multi-strategy attribution. Factors are evidence. They only become strategy behavior after a strategy contract assigns them a role in a complete trading playbook. """ from __future__ import annotations from collections.abc import Iterable PREREQUISITE = "prerequisite" TRIGGER = "trigger" CONFIRMATION = "confirmation" ENTRY = "entry" RISK = "risk" ATTRIBUTION = "attribution" UNKNOWN = "unknown" VALID_FACTOR_ROLES = { PREREQUISITE, TRIGGER, CONFIRMATION, ENTRY, RISK, ATTRIBUTION, UNKNOWN, } DEFAULT_FACTOR_ROLES: dict[str, str] = { "box_breakout_pullback_4h": TRIGGER, "box_breakout_pullback_1h": TRIGGER, "breakdown_retest_1h_short": TRIGGER, "retest_reject_15m_short": ENTRY, "market_risk_off_short": CONFIRMATION, "vp_fly_1h_current": TRIGGER, "volume_consecutive_1h": CONFIRMATION, "volume_divergence_1h": RISK, "short_tf_15m_ignition": TRIGGER, "short_tf_5m_ignition": PREREQUISITE, "short_tf_resonance": CONFIRMATION, "static_accum_4h": CONFIRMATION, "higher_lows_4h": CONFIRMATION, "compression_surge_4h": CONFIRMATION, "ignition_1h_current": TRIGGER, "ignition_4h_current": CONFIRMATION, "ignition_d1_current": CONFIRMATION, "ignition_stale": ATTRIBUTION, "dynamic_k_1h_bull": CONFIRMATION, "dynamic_k_d1_bull": CONFIRMATION, "breakout_pullback_d1": CONFIRMATION, "breakout_pullback_w1": CONFIRMATION, "breakout_15m_current": ENTRY, "pullback_15m_confirm": ENTRY, "strong_resonance_bypass": CONFIRMATION, "entry_quality_gate": RISK, "top_trader_long": CONFIRMATION, "sector_rotation": CONFIRMATION, "sentiment_resonance": CONFIRMATION, "dex_volume_spike": CONFIRMATION, "liquidity_add": CONFIRMATION, "liquidity_remove_risk": RISK, "exchange_outflow": CONFIRMATION, "exchange_inflow_risk": RISK, "whale_accumulation": CONFIRMATION, "holder_concentration_risk": RISK, "smart_money_buying": CONFIRMATION, "funding_extreme": RISK, "trend_exhaustion": RISK, "false_breakout": RISK, "high_position_reject": RISK, "risk_reward_bad": RISK, "cex_top_gainer_24h": PREREQUISITE, "unknown": UNKNOWN, } def factor_role(factor_code: str | None, overrides: dict[str, str] | None = None) -> str: code = str(factor_code or "").strip() or "unknown" mapping = {**DEFAULT_FACTOR_ROLES, **(overrides or {})} role = str(mapping.get(code) or UNKNOWN).strip() return role if role in VALID_FACTOR_ROLES else UNKNOWN def factor_roles_for_codes(codes: Iterable[str] | None, overrides: dict[str, str] | None = None) -> dict[str, str]: roles = {} for code in codes or []: text = str(code or "").strip() if text and text not in roles: roles[text] = factor_role(text, overrides=overrides) return roles def validate_factor_roles(roles: dict[str, str] | None) -> dict[str, str]: """Normalize role payloads without promoting unknown factors to triggers.""" normalized = {} for code, role in (roles or {}).items(): key = str(code or "").strip() value = str(role or "").strip() if not key: continue normalized[key] = value if value in VALID_FACTOR_ROLES else UNKNOWN return normalized