import argparse
from src.flock_compat import flock, LOCK_EX, LOCK_SH, LOCK_UN, LOCK_NB
from src.symbol_resolver import classify_symbol, normalize_symbol, pick_best_instrument
from src.symbol_calibrator import calibrate_symbol_params, recalibrate_from_bars
import hashlib
import json
import os
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from contextlib import contextmanager
from copy import deepcopy
from dataclasses import dataclass, field
from decimal import Decimal, ROUND_HALF_UP
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Optional

import logging

logger = logging.getLogger(__name__)

# Support both `python src/pipeline.py` and `from src.pipeline import ...`
# when caller cwd is `bridge/` and top-level bare imports are used below.
_SRC_DIR = Path(__file__).resolve().parent
if str(_SRC_DIR) not in sys.path:
    sys.path.append(str(_SRC_DIR))

import config_manager
from executor import run_engine
from execution_state import (
    apply_execution_report,
    create_or_get_signal_state,
    get_signal_state,
    is_final_state,
    reconcile_open_signals,
    transition_state,
    transition_states,
)
from incident_engine import evaluate_incidents, resolve_auto_actions
from live_volume import resolve_live_open_guard, resolve_live_volume_plan
try:
    from src.ledger import append_ledger_event
except ModuleNotFoundError:
    # Support direct script execution: `python3 src/pipeline.py`
    from ledger import append_ledger_event
from mt5_adapter import load_mt5_snapshot, to_engine_request
from mt5_execution import dispatch_execution
from ea_bridge_live import EABridgeLiveSource
from mt5_live import MT5LiveSource
from agent_decision_contract import load_agent_decision_contract
from llm_verifier import verify_and_calibrate
from market_context import (
    build_regime_audit_payload,
    is_defensive_regime,
    is_range_regime,
    is_trend_regime,
    normalize_regime_label,
    normalize_session_bucket,
    resolve_unified_market_context,
)
from canonical_market import build_canonical_market_snapshot
from runtime_state.decision_snapshot import (
    build_macro_direction_snapshot,
    resolve_allowed_side,
    resolve_macro_authority,
)
from position_tier import (
    PositionTierContext,
    apply_position_tier_state,
    evaluate_position_tier,
)
from src.strategy_registry import (
    LLM_TO_REGIME,
    SESSION_NOISE_BLOCK,
    canonical_strategy_id,
    get_strategy_runtime_contract,
    is_strategy_regime_compatible,
    is_strategy_route_family_compatible,
    strategy_route_family_alignment,
    strategy_type_priority_for_route,
    strategy_style,
)
from strategy_fit import evaluate_strategy_market_fit
from src.tactical_scheduler import extract_market_fingerprint, score_all_strategies, STRATEGY_PROFILES
from src.strategy_feedback import build_correction_coefficients, apply_feedback_correction
from src.strategic_analyzer import classify_trend_stage, score_range_quality, garch_risk_multiplier
from strategy_intelligence import get_intelligence_hub
from tenant_manager import load_runtime_state, tenant_paths
from metrics_agg import aggregate_exec_stats
try:
    from src.position_lifecycle import (
        normalize_position_attribution,
        sync_close_fill,
        sync_close_request,
        sync_open_fill,
        sync_open_request,
    )
except ModuleNotFoundError:
    from position_lifecycle import (
        normalize_position_attribution,
        sync_close_fill,
        sync_close_request,
        sync_open_fill,
        sync_open_request,
    )

_cooldown = config_manager.get_execution_cooldown_params()
ACK_RETRY_COOLDOWN_SECONDS = _cooldown["ack_retry_cooldown_sec"]
TIMEOUT_RETRY_COOLDOWN_SECONDS = _cooldown["timeout_retry_cooldown_sec"]
PENDING_ENTRY_TTL_SECONDS = {
    "OPEN_LIMIT": _cooldown["pending_entry_open_limit_sec"],
    "ARM_WAIT_CONFIRMATION": _cooldown["pending_entry_arm_wait_sec"],
}
MAX_TIMEOUT_RETRIES = _cooldown["max_timeout_retries"]
LIVE_SOURCE_RETRY_ERRORS = {
    "LIVE_SOURCE_SNAPSHOT_UNAVAILABLE",
    "LIVE_SOURCE_NOT_CONNECTED",
}
_LICENSE_CHECK_INTERVAL = 3600.0
_LAST_LICENSE_CHECK = 0.0
_RUNTIME_BLACKBOARD = None
_MAILBOX_LAST_EMIT: Dict[str, float] = {}
_MAILBOX_LAST_EMIT_TTL_SECONDS = 6.0 * 3600.0
_MAILBOX_LAST_EMIT_MAX_KEYS = 2048
_PIPELINE_LOCK_HANDLE = None
APP_DIR_ENV = "LOBSTERQUANT_APP_DIR"
# Signal dedup cache (process memory, not disk-dependent)
_RECENT_DISPATCHED_SIGNALS: Dict[str, float] = {}
_SIGNAL_DEDUP_WINDOW_SEC = 60.0
# Multi-strategy open throttle: minimum seconds between consecutive position opens.
# Prevents rapid-fire order placement when all instances produce viable signals
# on every bar. Default 120s = one open per 2 minutes maximum.
_LAST_OPEN_TIME: float = 0.0
_MIN_OPEN_INTERVAL_SEC = 120.0


def _get_min_open_interval_sec() -> float:
    """Return the effective min-open-interval, allowing meta_decision drift."""
    try:
        from src.learning.meta_decision import get_threshold as _mt
        return float(
            config_manager.get_effective_param(
                "min_open_interval_sec",
                _mt("min_open_interval_sec"),
            )
            or _MIN_OPEN_INTERVAL_SEC
        )
    except Exception:
        return _MIN_OPEN_INTERVAL_SEC


DEFAULT_EXECUTION_MIN_CONFIDENCE = config_manager.get_effective_min_confidence_threshold()
MACRO_FRESH_TTL_SECONDS = 20.0 * 60.0
MACRO_EXPIRE_TTL_SECONDS = 45.0 * 60.0
TRENDING_REGIMES = {"WEAK_TREND", "STEALTH_TREND", "STRONG_TREND"}
TRENDING_SUBTYPES = {
    "FLAG_COMPRESSION",
    "REPRICING_IMPULSE",
    "CONFIRM_BREAK",
    "TREND_FIRST_PULLBACK",
    "TREND_CHANNEL",
}
RANGING_REGIMES = {"RANGING"}
RANGING_SUBTYPES = {
    "WIDE_BOX",
    "LIQUIDITY_SWEEP",
    "SPRING",
    "UPTHRUST",
    "REVERSAL_SETUP",
    "NARROW_BOX",
    "DRIFTING_CHANNEL",
}
STRUCTURAL_HARD_BLOCK_ADVICES = {"LIMIT_ONLY", "WAIT_CONFIRMATION", "BLOCK_COUNTER_TREND", "AVOID"}


@dataclass
class PipelineRunContext:
    project_root: Path
    code_root: Optional[Path]
    tenant_id: str
    source: str
    runtime_intent: str
    output_path: Path
    execution_mode: str
    symbol: str
    execution_report_path: Optional[Path]
    enable_incident_autofix: bool
    runtime_state: Dict[str, Any] = field(default_factory=dict)
    promotion_info: Dict[str, Any] = field(default_factory=dict)
    mt5_snapshot: Dict[str, Any] = field(default_factory=dict)
    engine_request: Dict[str, Any] = field(default_factory=dict)
    scheduler_snapshot: Dict[str, Any] = field(default_factory=dict)
    agent_context: Dict[str, Any] = field(default_factory=dict)
    strategy_signals: list[Dict[str, Any]] = field(default_factory=list)
    aggregated_intent: Dict[str, Any] = field(default_factory=dict)
    signal: Dict[str, Any] = field(default_factory=dict)
    signal_id: str = ""
    signal_attribution: Dict[str, Any] = field(default_factory=dict)
    state_path: Optional[Path] = None
    pending_registry_update: Optional[dict] = None


@dataclass
class PipelineOutcome:
    execution_report: Dict[str, Any] = field(default_factory=dict)
    incidents: list[Dict[str, Any]] = field(default_factory=list)
    auto_actions: list[Dict[str, Any]] = field(default_factory=list)
    ledger_events: list[str] = field(default_factory=list)
    extra_fields: Dict[str, Any] = field(default_factory=dict)
    include_runtime: bool = True
    append_runtime_log: bool = False
    sync_mailbox: bool = False


@dataclass
class StrategyOpportunity:
    strategy_id: str = ""
    instance_id: str = ""
    directional_view: str = "NONE"
    setup_quality: str = "INVALID"
    entry_style_preference: str = "MARKET"
    long_opportunity_score: float = 0.0
    short_opportunity_score: float = 0.0
    urgency: str = "LOW"
    confidence: float = 0.0
    actionable: bool = False
    source_action: str = "HOLD"
    source_side: str = "NONE"
    notes: list[str] = field(default_factory=list)


@dataclass
class ExecutionConstraint:
    advice: str = "ALLOW_FOLLOW"
    allowed_side: str = "BOTH"
    allowed_entry_styles: list[str] = field(default_factory=lambda: ["MARKET", "LIMIT"])
    confirmation_required: bool = False
    probe_size_scale: float = 0.5
    structural_side: str = "NONE"
    macro_direction: str = "NEUTRAL"
    block_new_open: bool = False
    reason: str = ""


@dataclass
class MaterializedOpportunity:
    materialized_action: str = "NO_SETUP"
    final_action: str = "HOLD"
    final_side: str = "NONE"
    entry_style: str = "MARKET"
    size_scale: float = 1.0
    eligible_for_aggregation: bool = False
    reason: str = ""


@dataclass
class ExecutionDecision:
    decision: str = "HOLD"
    action: str = "HOLD"
    side: str = "NONE"
    selected_strategy_id: str = ""
    selected_instance_id: str = ""
    entry_owner_strategy_id: str = ""
    entry_owner_instance_id: str = ""
    executed_strategy_id: str = ""
    executed_instance_id: str = ""
    reason: str = ""
    confidence: float = 0.0
    route_family: str = ""
    market_family: str = ""
    entry_family: str = ""
    fit_reason: str = ""


def _to_bool(value: str) -> bool:
    return str(value).lower() in {"1", "true", "yes", "on"}


def _build_execution_decision(
    *,
    action: str,
    side: str,
    selected_strategy_id: str,
    selected_instance_id: str,
    entry_owner_strategy_id: str,
    entry_owner_instance_id: str,
    reason: str,
    confidence: float,
    route: Optional[Dict[str, Any]] = None,
    canonical_market_snapshot: Optional[Dict[str, Any]] = None,
    selection: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    route_payload = route if isinstance(route, dict) else {}
    market_snapshot = (
        canonical_market_snapshot
        if isinstance(canonical_market_snapshot, dict)
        else route_payload.get("canonical_market_snapshot", {})
        if isinstance(route_payload.get("canonical_market_snapshot", {}), dict)
        else {}
    )
    normalized_market = (
        market_snapshot.get("normalized", {})
        if isinstance(market_snapshot.get("normalized", {}), dict)
        else {}
    )
    selection_payload = selection if isinstance(selection, dict) else {}
    normalized_action = str(action or "HOLD").upper()
    normalized_side = str(side or "NONE").upper()
    decision = "EXECUTE" if normalized_action == "OPEN" and normalized_side in {"BUY", "SELL"} else "HOLD"
    payload = ExecutionDecision(
        decision=decision,
        action=normalized_action,
        side=normalized_side,
        selected_strategy_id=str(selected_strategy_id or ""),
        selected_instance_id=str(selected_instance_id or ""),
        entry_owner_strategy_id=str(entry_owner_strategy_id or ""),
        entry_owner_instance_id=str(entry_owner_instance_id or ""),
        executed_strategy_id=str(entry_owner_strategy_id if decision == "EXECUTE" else ""),
        executed_instance_id=str(entry_owner_instance_id if decision == "EXECUTE" else ""),
        reason=str(reason or ""),
        confidence=round(max(0.0, float(confidence or 0.0)), 4),
        route_family=str(route_payload.get("route_family", "") or ""),
        market_family=str(normalized_market.get("market_family", "") or ""),
        entry_family=str(normalized_market.get("entry_family", "") or ""),
        fit_reason=str(selection_payload.get("reason", "") or ""),
    )
    return {
        "decision": payload.decision,
        "action": payload.action,
        "side": payload.side,
        "selected_strategy_id": payload.selected_strategy_id,
        "selected_instance_id": payload.selected_instance_id,
        "entry_owner_strategy_id": payload.entry_owner_strategy_id,
        "entry_owner_instance_id": payload.entry_owner_instance_id,
        "executed_strategy_id": payload.executed_strategy_id,
        "executed_instance_id": payload.executed_instance_id,
        "reason": payload.reason,
        "confidence": payload.confidence,
        "route_family": payload.route_family,
        "market_family": payload.market_family,
        "entry_family": payload.entry_family,
        "fit_reason": payload.fit_reason,
    }


def _resolve_route_selected_identity(
    route_payload: Optional[Dict[str, Any]],
    *,
    fallback_strategy_id: str = "",
    fallback_instance_id: str = "",
) -> tuple[str, str]:
    route = route_payload if isinstance(route_payload, dict) else {}
    selection = route.get("selection", {}) if isinstance(route.get("selection", {}), dict) else {}
    selection_mode = str(selection.get("selection_mode", "") or "").strip()
    winner_strategy_id = str(selection.get("winner_strategy_id", "") or "").strip()
    winner_instance_id = str(selection.get("winner_instance_id", "") or "").strip()
    if selection_mode == "route_order" and not winner_strategy_id and not winner_instance_id:
        return "", ""
    selected_strategy_id = str(
        winner_strategy_id
        or route.get("selected_strategy_id")
        or fallback_strategy_id
        or ""
    ).strip()
    selected_instance_id = str(
        winner_instance_id
        or route.get("selected_instance_id")
        or fallback_instance_id
        or ""
    ).strip()
    return selected_instance_id, selected_strategy_id


def _resolve_runtime_selected_identity(
    ctx: PipelineRunContext,
    *,
    route_snapshot: Optional[Dict[str, Any]] = None,
) -> tuple[str, str]:
    fallback_instance_id = str(
        ctx.signal.get("_selected_instance_id")
        or ctx.signal.get("selected_instance_id")
        or ctx.aggregated_intent.get("selected_instance_id")
        or ctx.scheduler_snapshot.get("selected_instance_id", "")
        or ""
    ).strip()
    fallback_strategy_id = str(
        ctx.signal.get("_selected_strategy")
        or ctx.signal.get("selected_strategy_id")
        or ctx.aggregated_intent.get("selected_strategy_id")
        or ctx.scheduler_snapshot.get("selected_strategy_id", "")
        or ""
    ).strip()
    route_payload = route_snapshot
    if not isinstance(route_payload, dict):
        route_payload = (
            ctx.aggregated_intent.get("route", {})
            if isinstance(ctx.aggregated_intent.get("route", {}), dict)
            else ctx.scheduler_snapshot.get("route", {})
            if isinstance(ctx.scheduler_snapshot.get("route", {}), dict)
            else {}
        )
    return _resolve_route_selected_identity(
        route_payload,
        fallback_strategy_id=fallback_strategy_id,
        fallback_instance_id=fallback_instance_id,
    )


def _build_runtime_policy_sources() -> Dict[str, Any]:
    tracked_params = {
        "max_positions_per_strategy": 1,
        "max_same_side_per_strategy": 1,
        "min_open_interval_sec": _MIN_OPEN_INTERVAL_SEC,
        "max_open_positions": config_manager.get_effective_param("max_positions", 4),
    }
    param_sources: Dict[str, Any] = {}
    for param_name, default_value in tracked_params.items():
        try:
            value, source = config_manager.get_effective_param_with_source(param_name, default_value)
        except Exception:
            value, source = default_value, "lookup_error"
        param_sources[param_name] = {
            "value": value,
            "source": str(source or ""),
        }
    return {
        "storage_root": str(config_manager.get_app_dir()),
        "config_dir": str(config_manager.get_config_dir()),
        "override_config_path": str(config_manager.override_config_path()),
        "effective_config_path": str(config_manager.effective_config_path()),
        "params": param_sources,
    }


def _synthesize_runtime_execution_decision(
    ctx: "PipelineRunContext",
    *,
    source: str,
    fallback_reason: str = "",
    route_snapshot_override: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    existing_decision = (
        ctx.aggregated_intent.get("execution_decision", {})
        if isinstance(ctx.aggregated_intent.get("execution_decision", {}), dict)
        else {}
    )
    route_snapshot = (
        deepcopy(route_snapshot_override)
        if isinstance(route_snapshot_override, dict)
        else deepcopy(ctx.aggregated_intent.get("route", {}))
        if isinstance(ctx.aggregated_intent.get("route", {}), dict)
        else deepcopy(ctx.scheduler_snapshot.get("route", {}))
        if isinstance(ctx.scheduler_snapshot.get("route", {}), dict)
        else {}
    )
    selection_payload = (
        ctx.aggregated_intent.get("selection", {})
        if isinstance(ctx.aggregated_intent.get("selection", {}), dict)
        else route_snapshot.get("selection", {})
        if isinstance(route_snapshot.get("selection", {}), dict)
        else {}
    )
    trace_payload = ctx.signal.get("trace", {}) if isinstance(ctx.signal.get("trace", {}), dict) else {}
    selected_instance_id, selected_strategy_id = _resolve_runtime_selected_identity(
        ctx,
        route_snapshot=route_snapshot,
    )
    entry_owner_instance_id, entry_owner_strategy_id = _resolve_runtime_execution_owner(ctx)
    reason = ""
    for candidate in (
        existing_decision.get("reason"),
        ctx.signal.get("reject_reason"),
        fallback_reason,
        ctx.aggregated_intent.get("selected_reason"),
        ctx.scheduler_snapshot.get("reason"),
    ):
        text = str(candidate or "").strip()
        if text:
            reason = text
            break
    confidence_raw = (
        existing_decision.get("confidence")
        if existing_decision.get("confidence") not in (None, "")
        else ctx.aggregated_intent.get("confidence")
        if ctx.aggregated_intent.get("confidence") not in (None, "")
        else trace_payload.get("confidence", 0.0)
    )
    try:
        confidence = float(confidence_raw or 0.0)
    except (TypeError, ValueError):
        confidence = 0.0
    canonical_market_snapshot = (
        deepcopy(route_snapshot.get("canonical_market_snapshot", {}))
        if isinstance(route_snapshot.get("canonical_market_snapshot", {}), dict)
        else {}
    )
    if not canonical_market_snapshot:
        canonical_market_snapshot = _resolve_runtime_canonical_market_snapshot(ctx)
    synthesized = _build_execution_decision(
        action=str(
            ctx.signal.get("action", ctx.aggregated_intent.get("action", "HOLD"))
            or "HOLD"
        ),
        side=str(
            ctx.signal.get("side", ctx.aggregated_intent.get("side", "NONE"))
            or "NONE"
        ),
        selected_strategy_id=selected_strategy_id,
        selected_instance_id=selected_instance_id,
        entry_owner_strategy_id=entry_owner_strategy_id,
        entry_owner_instance_id=entry_owner_instance_id,
        reason=reason,
        confidence=confidence,
        route=route_snapshot,
        canonical_market_snapshot=canonical_market_snapshot,
        selection=selection_payload,
    )
    merged = dict(existing_decision)
    merged.update(synthesized)
    merged["decision_source"] = str(existing_decision.get("decision_source", "") or source).strip() or source
    return merged


def _make_hold_signal(
    strategy_id: str,
    instance_id: str,
    reason: str,
    evidence: Optional[list[str]] = None,
) -> Dict[str, Any]:
    hold_evidence = list(evidence or [])
    if reason and reason not in hold_evidence:
        hold_evidence.append(reason)
    return {
        "action": "HOLD",
        "side": "NONE",
        "confidence": 0.0,
        "reject_reason": reason,
        "strategy_id": strategy_id,
        "_sig_strategy_id": strategy_id,
        "instance_id": instance_id,
        "_instance_id": instance_id,
        "instance_weight": 0.0,
        "entry_selected": False,
        "trace": {"confidence": 0.0, "evidence": hold_evidence},
        "indicators": {},
    }


def _enforce_system_market_truth(
    signal: Dict[str, Any],
    *,
    strategy_id: str,
    system_regime: str,
    system_route_family: str,
) -> Dict[str, Any]:
    normalized_regime = normalize_regime_label(system_regime or "UNKNOWN")
    normalized_route_family = str(system_route_family or "").strip().lower()
    indicators = signal.get("indicators", {}) if isinstance(signal.get("indicators", {}), dict) else {}
    trace = signal.get("trace", {}) if isinstance(signal.get("trace", {}), dict) else {}
    evidence = trace.get("evidence", []) if isinstance(trace.get("evidence", []), list) else []
    raw_indicator_regime = normalize_regime_label(indicators.get("regime", ""))
    raw_market_structure = str(indicators.get("market_structure", "") or "").strip().upper()
    if raw_indicator_regime and raw_indicator_regime != normalized_regime:
        indicators.setdefault("generator_regime", raw_indicator_regime)
    if raw_market_structure and raw_market_structure != normalized_regime:
        indicators.setdefault("generator_market_structure", raw_market_structure)
    indicators["regime"] = normalized_regime
    indicators["system_regime"] = normalized_regime
    if normalized_route_family:
        indicators["system_route_family"] = normalized_route_family
    signal["system_regime"] = normalized_regime
    signal["selection_regime"] = normalized_regime
    if normalized_route_family:
        signal["system_route_family"] = normalized_route_family
    marker = f"system_market_truth:{normalized_regime}:{normalized_route_family or 'unknown'}"
    if marker not in evidence:
        evidence.append(marker)
    suppress_reason = ""
    action_upper = str(signal.get("action", "HOLD") or "HOLD").upper()
    if action_upper == "OPEN":
        if normalized_route_family == "blocked":
            suppress_reason = f"system_route_blocked:{normalized_regime}"
        elif (
            normalized_route_family in {"trend", "range"}
            and strategy_id
            and not is_strategy_route_family_compatible(strategy_id, normalized_route_family)[0]
        ):
            suppress_reason = f"system_regime_override:{normalized_regime}"
    if suppress_reason:
        signal.setdefault("generator_reason", str(signal.get("reason", signal.get("reject_reason", "")) or ""))
        signal["action"] = "HOLD"
        signal["side"] = "NONE"
        signal["reason"] = suppress_reason
        signal["reject_reason"] = suppress_reason
        indicators["suppressed"] = True
        indicators["suppression_reason"] = suppress_reason
        if suppress_reason not in evidence:
            evidence.append(suppress_reason)
    trace["evidence"] = evidence
    signal["trace"] = trace
    signal["indicators"] = indicators
    return signal


def _read_json_dict(path: Path) -> Dict[str, Any]:
    if not path.exists():
        return {}
    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
        return payload if isinstance(payload, dict) else {}
    except Exception:
        return {}


def _runtime_state_path(project_root: Path, tenant_id: str) -> Path:
    return tenant_paths(project_root, tenant_id).get("runtime", project_root / "ops/runtime_state.json")


def _save_runtime_state(project_root: Path, tenant_id: str, state: Dict[str, Any]) -> None:
    _write_json(_runtime_state_path(project_root, tenant_id), state)


def _build_runtime_market_state_snapshot(
    route_snapshot: Dict[str, Any],
    canonical_market_snapshot: Dict[str, Any],
    execution_decision: Dict[str, Any],
    *,
    selected_strategy_id: str,
    selected_instance_id: str,
) -> Dict[str, Any]:
    market_context = (
        route_snapshot.get("market_context", {})
        if isinstance(route_snapshot.get("market_context", {}), dict)
        else {}
    )
    normalized_market = (
        canonical_market_snapshot.get("normalized", {})
        if isinstance(canonical_market_snapshot.get("normalized", {}), dict)
        else {}
    )
    return {
        "updated_at": datetime.now(timezone.utc).isoformat(),
        "regime": str(market_context.get("regime", "") or ""),
        "selection_regime": str(market_context.get("selection_regime", market_context.get("regime", "")) or ""),
        "regime_source": str(market_context.get("regime_source", "") or ""),
        "regime_confidence": round(float(market_context.get("regime_confidence", 0.0) or 0.0), 4),
        "effective_regime_confidence": round(float(market_context.get("effective_regime_confidence", 0.0) or 0.0), 4),
        "regime_4d": str(market_context.get("regime_4d", "") or ""),
        "interpreter_regime": str(market_context.get("interpreter_regime", "") or ""),
        "regime_subtype": str(market_context.get("regime_subtype", "") or ""),
        "route_family": str(route_snapshot.get("route_family", market_context.get("route_family", "")) or ""),
        "session_mode": str(market_context.get("session_mode", "") or ""),
        "session_bucket": str(market_context.get("session_bucket", "") or ""),
        "market_family": str(normalized_market.get("market_family", "") or ""),
        "entry_family": str(normalized_market.get("entry_family", "") or ""),
        "direction_bias": str(normalized_market.get("direction_bias", "") or ""),
        "canonical_snapshot_id": str(canonical_market_snapshot.get("snapshot_id", "") or ""),
        "selected_strategy_id": str(selected_strategy_id or ""),
        "selected_instance_id": str(selected_instance_id or ""),
        "execution_reason": str(execution_decision.get("reason", "") or ""),
    }


def _dedupe_string_list(values: Any) -> list[str]:
    if not isinstance(values, list):
        return []
    items: list[str] = []
    for value in values:
        text = str(value or "").strip()
        if text and text not in items:
            items.append(text)
    return items


def _incident_controls(runtime_state: Dict[str, Any]) -> Dict[str, Any]:
    controls = runtime_state.get("incident_controls", {}) if isinstance(runtime_state.get("incident_controls", {}), dict) else {}
    return dict(controls)


def _parse_auto_action_step(step: str) -> tuple[str, str]:
    raw = str(step or "").strip()
    if not raw:
        return "", ""
    action, sep, value = raw.partition(":")
    if not sep:
        return action.strip().upper(), ""
    return action.strip().upper(), value.strip()


def _apply_auto_action_step(controls: Dict[str, Any], step: str, *, executed_at: str) -> tuple[bool, Optional[str]]:
    action, raw_value = _parse_auto_action_step(step)
    if not action:
        return False, None
    if action == "DISABLE_NEW_OPEN":
        controls["disable_new_open"] = _to_bool(raw_value or "true")
        controls["disable_new_open_reason"] = f"auto_action:{step}"
        controls["disable_new_open_updated_at"] = executed_at
        return True, None
    if action == "SET_RUNTIME_MODE" and raw_value:
        controls["runtime_mode"] = raw_value
        controls["runtime_mode_updated_at"] = executed_at
        return True, None
    if action == "PAGE_ONCALL":
        return True, "PAGE_ONCALL requested by incident auto-action" if _to_bool(raw_value or "true") else None
    if action == "EXECUTOR_RECONCILE":
        controls["executor_reconcile_requested"] = _to_bool(raw_value or "true")
        controls["executor_reconcile_updated_at"] = executed_at
        return True, None
    if action == "RETRY_WITH_BACKOFF":
        controls["retry_with_backoff_requested"] = _to_bool(raw_value or "true")
        controls["retry_with_backoff_updated_at"] = executed_at
        return True, None
    if action == "RAISE_SPREAD_FILTER":
        try:
            controls["spread_filter_multiplier"] = float(raw_value)
            controls["spread_filter_updated_at"] = executed_at
            return True, None
        except Exception:
            return False, None
    if action == "REDUCE_MAX_RISK_PER_TRADE":
        try:
            controls["max_risk_per_trade_scale"] = float(raw_value)
            controls["max_risk_per_trade_updated_at"] = executed_at
            return True, None
        except Exception:
            return False, None
    return False, None


def _consume_pending_auto_actions(project_root: Path, tenant_id: str) -> tuple[Dict[str, Any], list[str], list[str], list[str]]:
    runtime_state = load_runtime_state(project_root, tenant_id)
    pending_auto_actions = _dedupe_string_list(runtime_state.get("pending_auto_actions", []))
    if not pending_auto_actions:
        return runtime_state, [], [], []
    executed_at = datetime.now(timezone.utc).isoformat()
    controls = _incident_controls(runtime_state)
    applied_steps: list[str] = []
    alert_messages: list[str] = []
    unsupported_steps: list[str] = []
    for step in pending_auto_actions:
        applied, alert_text = _apply_auto_action_step(controls, step, executed_at=executed_at)
        if not applied:
            unsupported_steps.append(step)
            continue
        applied_steps.append(step)
        if alert_text:
            alert_messages.append(alert_text)
    runtime_state["incident_controls"] = controls
    runtime_state["pending_auto_actions"] = []
    runtime_state["last_auto_action_execution"] = {
        "executed_at": executed_at,
        "applied_steps": applied_steps,
        "alerts": alert_messages,
        "unsupported_steps": unsupported_steps,
    }
    _save_runtime_state(project_root, tenant_id, runtime_state)
    return runtime_state, applied_steps, alert_messages, unsupported_steps


def _store_pending_auto_actions(project_root: Path, tenant_id: str, auto_actions: Any) -> None:
    runtime_state = load_runtime_state(project_root, tenant_id)
    runtime_state["pending_auto_actions"] = _dedupe_string_list(auto_actions)
    runtime_state["last_auto_actions_emitted_at"] = datetime.now(timezone.utc).isoformat()
    _save_runtime_state(project_root, tenant_id, runtime_state)


def _pid_exists(pid: int) -> bool:
    if int(pid or 0) <= 0:
        return False
    try:
        import psutil

        return bool(psutil.pid_exists(int(pid)))
    except Exception:
        try:
            os.kill(int(pid), 0)
            return True
        except PermissionError:
            return True
        except (OSError, SystemError, ValueError):
            return False


def _load_data_quality_rules(project_root: Path) -> Dict[str, Any]:
    return _read_json_dict(project_root / "config" / "data_quality_rules.json")


def _resolve_market_data_delay_gate_ms(project_root: Path, runtime_state: Dict[str, Any]) -> float:
    controls = _incident_controls(runtime_state)
    for candidate in (
        controls.get("market_data_delay_gate_ms"),
        runtime_state.get("market_data_delay_gate_ms"),
    ):
        try:
            value = float(candidate)
            if value > 0:
                return value
        except Exception:
            continue
    rules = _load_data_quality_rules(project_root)
    for check in rules.get("pre_trade_checks", []):
        if not isinstance(check, dict):
            continue
        if str(check.get("id", "") or "").strip() == "freshness_check" or str(check.get("metric", "") or "").strip() == "market_data_delay_ms":
            try:
                threshold = float(check.get("threshold", 0) or 0)
                if threshold > 0:
                    return threshold
            except Exception:
                continue
    return 2000.0


def _is_live_source_retryable_error(exc: Exception) -> bool:
    text = str(exc or "").strip()
    if not text:
        return False
    if text in LIVE_SOURCE_RETRY_ERRORS:
        return True
    return any(marker in text for marker in LIVE_SOURCE_RETRY_ERRORS)


def _acquire_pipeline_singleton_lock(project_root: Path, runtime_intent: str, source: str, execution_mode: str):
    global _PIPELINE_LOCK_HANDLE
    if runtime_intent != "live_trade":
        return None
    lock_dir = project_root / "logs"
    lock_dir.mkdir(parents=True, exist_ok=True)
    lock_path = lock_dir / f"pipeline.{runtime_intent}.{source}.{execution_mode}.lock"
    owner_path = lock_dir / f"pipeline.{runtime_intent}.{source}.{execution_mode}.lock.owner"
    handle = lock_path.open("a+", encoding="utf-8")
    try:
        handle.seek(0)
        flock(handle.fileno(), LOCK_EX | LOCK_NB)
    except BlockingIOError:
        handle.close()
        # 鈹€鈹€ Stale lock detection: if holder PID is missing or dead, break the lock 鈹€鈹€
        try:
            owner = owner_path.read_text(encoding="utf-8").strip()
        except Exception:
            owner = ""
        if owner:
            import re
            m = re.search(r'pid=(\d+)', owner)
            if m:
                old_pid = int(m.group(1))
                if not _pid_exists(old_pid):
                    owner_path.unlink(missing_ok=True)
                    lock_path.unlink(missing_ok=True)
                    return _acquire_pipeline_singleton_lock(
                        project_root, runtime_intent, source, execution_mode
                    )
        # No valid owner info 鈥?stale lock, take over
        owner_path.unlink(missing_ok=True)
        lock_path.unlink(missing_ok=True)
        return _acquire_pipeline_singleton_lock(
            project_root, runtime_intent, source, execution_mode
        )
    try:
        owner_path.write_text(
            f"pid={os.getpid()} runtime_intent={runtime_intent} source={source} execution_mode={execution_mode}\n",
            encoding="utf-8",
        )
    except Exception:
        pass
    _PIPELINE_LOCK_HANDLE = handle
    return handle


def _release_pipeline_singleton_lock() -> None:
    global _PIPELINE_LOCK_HANDLE
    handle = _PIPELINE_LOCK_HANDLE
    if handle is None:
        return
    try:
        handle.seek(0)
        flock(handle.fileno(), LOCK_UN)
    except Exception:
        pass
    try:
        handle.close()
    except Exception:
        pass


@contextmanager
def _scheduler_snapshot_lock(project_root: Path):
    lock_path = project_root / "config" / "strategy_market_state.lock"
    lock_path.parent.mkdir(parents=True, exist_ok=True)
    handle = lock_path.open("a+", encoding="utf-8")
    try:
        handle.seek(0)
        flock(handle.fileno(), LOCK_SH)
        yield
    finally:
        try:
            handle.seek(0)
            flock(handle.fileno(), LOCK_UN)
        except Exception:
            pass
        handle.close()


def _check_license_periodic() -> None:
    try:
        import time as t
        from src.license.guard import enforce_license_periodic
        if t.time() - _LAST_LICENSE_CHECK > _LICENSE_CHECK_INTERVAL:
            enforce_license_periodic()
            _LAST_LICENSE_CHECK = t.time()
    except Exception:
        pass  # License check failures raise inside enforce_license_periodic


def _validate_runtime_intent(source: str, execution_mode: str, runtime_intent: str) -> None:
    src = source.lower()
    exe = execution_mode.lower()
    intent = runtime_intent.lower()
    if intent == "offline_train":
        if src != "file" or exe not in {"file", "mock"}:
            raise RuntimeError("offline_train requires source=file and execution-mode in [file,mock]")
        return
    if intent == "paper_trade":
        if src not in {"mt5", "ea_feed"} or exe not in {"mock"}:
            raise RuntimeError("paper_trade requires source in [mt5,ea_feed] and execution-mode=mock")
        return
    if intent == "live_trade":
        if src not in {"mt5", "ea_feed"} or exe not in {"direct", "ea_bridge"}:
            raise RuntimeError("live_trade requires source in [mt5,ea_feed] and execution-mode in [direct,ea_bridge]")
        return
    raise RuntimeError("runtime-intent must be one of [offline_train,paper_trade,live_trade]")


def _build_runtime_metrics(engine_request: Dict[str, Any], signal: Dict[str, Any]) -> Dict[str, Any]:
    market = engine_request.get("market_metrics", {})
    return {
        "market_data_delay_ms": float(market.get("market_data_delay_ms", 0)),
        "ack_timeout_count_5m": float(engine_request.get("runtime_feedback", {}).get("ack_timeout_count_5m", 0)),
        "slippage_points_p95_15m": float(engine_request.get("runtime_feedback", {}).get("slippage_points_p95_15m", 0)),
        "daily_token_usage_pct": float(engine_request.get("runtime_feedback", {}).get("daily_token_usage_pct", 0)),
        "governor_status": signal.get("governor_status", "APPROVED"),
    }


def _ts_to_epoch(ts: str) -> float:
    if not ts:
        return 0.0
    try:
        return datetime.fromisoformat(ts.replace("Z", "+00:00")).timestamp()
    except Exception:
        return 0.0


def _coerce_timestamp_like(value: Any) -> float:
    if value in (None, ""):
        return 0.0
    if isinstance(value, (int, float)):
        return float(value)
    text = str(value or "").strip()
    if not text:
        return 0.0
    try:
        return float(text)
    except (TypeError, ValueError):
        return _ts_to_epoch(text)


def _macro_freshness_state(macro_context: Dict[str, Any]) -> Dict[str, Any]:
    updated_ts = max(
        _safe_float(macro_context.get("updated_ts", macro_context.get("ts", 0)) or 0.0, 0.0),
        _coerce_timestamp_like(macro_context.get("updated_at", macro_context.get("generated_at", ""))),
    )
    valid_until_ts = _coerce_timestamp_like(macro_context.get("valid_until", ""))
    age_sec = time.time() - updated_ts if updated_ts > 0 else 99999.0
    invalid_contract_window = valid_until_ts > 0 and updated_ts > 0 and valid_until_ts <= updated_ts
    expired_flag = bool(macro_context.get("expired", False))
    stale_flag = bool(macro_context.get("stale", False))
    degraded_flag = bool(macro_context.get("degraded", False))
    phase = str(macro_context.get("phase", "") or "").upper()
    ttl_stale = age_sec > MACRO_FRESH_TTL_SECONDS
    ttl_expired = age_sec > MACRO_EXPIRE_TTL_SECONDS
    hard_bind_unsafe = (
        stale_flag
        or degraded_flag
        or expired_flag
        or invalid_contract_window
        or ttl_stale
        or ttl_expired
        or phase in {"STALE", "EXPIRED"}
    )
    return {
        "updated_ts": updated_ts,
        "valid_until_ts": valid_until_ts,
        "age_sec": age_sec,
        "invalid_contract_window": invalid_contract_window,
        "ttl_stale": ttl_stale,
        "ttl_expired": ttl_expired,
        "hard_bind_unsafe": hard_bind_unsafe,
    }


def _state_retry_meta(entry: Dict[str, Any]) -> Dict[str, Any]:
    history = entry.get("history", [])
    timeout_count = 0
    last_state = str(entry.get("state", "")).upper()
    last_ts = ""
    if history:
        last = history[-1]
        last_state = str(last.get("state", last_state)).upper()
        last_ts = str(last.get("ts", ""))
        for ev in history:
            if str(ev.get("state", "")).upper() == "TIMEOUT":
                timeout_count += 1
    return {"state": last_state, "last_ts": last_ts, "timeout_count": timeout_count}


def _write_json(path: Path, payload: Dict[str, Any]) -> None:
    from src.atomic_write import atomic_write_json
    try:
        atomic_write_json(path, payload)
    except (ValueError, TypeError) as e:
        # Circular reference guard 鈥?JSON can't serialize circular refs.
        # Fall back to a sanitized copy with `_original` references stripped.
        import logging
        logging.getLogger("pipeline").warning(
            "[_write_json] direct serialization failed (%s), falling back to sanitized copy",
            type(e).__name__,
        )
        _sanitized = _deep_sanitize_for_json(payload)
        atomic_write_json(path, _sanitized)


        _session_id = "runtime-snapshot-stall"
        _env_candidates: list[Path] = []
        _exe_path = Path(sys.executable).resolve()
        _code_path = Path(__file__).resolve()
        for _base in [Path.cwd(), _exe_path.parent, *_exe_path.parents, _code_path.parent, *_code_path.parents]:
            _candidate = _base / ".dbg" / "runtime-snapshot-stall.env"
            if _candidate not in _env_candidates:
                _env_candidates.append(_candidate)
        for _env_path in _env_candidates:
            if not _env_path.exists():
                continue
            _content = _env_path.read_text(encoding="utf-8")
            for _line in _content.splitlines():
                if _line.startswith("DEBUG_SERVER_URL="):
                    _url = _line.split("=", 1)[1].strip() or _url
                elif _line.startswith("DEBUG_SESSION_ID="):
                    _session_id = _line.split("=", 1)[1].strip() or _session_id
            break
        urllib.request.urlopen(
            urllib.request.Request(
                _url,
                data=json.dumps(
                    {
                        "sessionId": _session_id,
                        "runId": "pre-fix",
                        "hypothesisId": str(hypothesis_id or "").strip() or "A",
                        "location": str(location or "pipeline.py"),
                        "msg": f"[DEBUG] {message}",
                        "data": data or {},
                        "ts": int(time.time() * 1000),
                    },
                    ensure_ascii=False,
                ).encode("utf-8"),
                headers={"Content-Type": "application/json"},
            ),
            timeout=0.8,
        ).read()
    except Exception:
        pass


def _deep_sanitize_for_json(obj: Any, _seen: Optional[set] = None) -> Any:
    """Recursively strip _original and other problematic keys for JSON serialization."""
    if _seen is None:
        _seen = set()
    obj_id = id(obj)
    if obj_id in _seen:
        return "<circular>"
    if isinstance(obj, dict):
        _seen.add(obj_id)
        result = {}
        for k, v in obj.items():
            if k in ("_original",):
                continue
            result[k] = _deep_sanitize_for_json(v, _seen)
        _seen.discard(obj_id)
        return result
    if isinstance(obj, list):
        _seen.add(obj_id)
        result = [
            _deep_sanitize_for_json(item, _seen) if isinstance(item, (dict, list)) else item
            for item in obj
        ]
        _seen.discard(obj_id)
        return result
    return obj


def _resolve_runtime_root(code_root: Path) -> Path:
    if getattr(sys, 'frozen', False):
        from config_manager import get_app_dir as _get_app_dir
        return _get_app_dir()
    app_dir = os.environ.get(APP_DIR_ENV, "").strip()
    if app_dir:
        try:
            root = Path(app_dir).expanduser()
            root.mkdir(parents=True, exist_ok=True)
            return root
        except Exception:
            pass
    return code_root


def _resolve_runtime_path(runtime_root: Path, raw_path: str) -> Path:
    path = Path(raw_path)
    if path.is_absolute():
        return path
    return runtime_root / path


def _append_log(path: Path, payload: Dict[str, Any]) -> None:
    from src.data_layer import append_ndjson
    append_ndjson(path, payload)


def _runtime_payload(runtime_intent: str, source: str, execution_mode: str) -> Dict[str, Any]:
    return {
        "intent": runtime_intent,
        "source": source,
        "execution_mode": execution_mode,
    }


def _build_idle_signal(
    *,
    signal_id: str,
    symbol: str,
    reject_reason: str,
    evidence: list[str],
    risk_flags: list[str],
) -> Dict[str, Any]:
    now_iso = datetime.now(timezone.utc).isoformat()
    return {
        "signal_id": signal_id,
        "timestamp": now_iso,
        "symbol": symbol,
        "action": "HOLD",
        "side": "NONE",
        "mode": "UNCERTAIN",
        "governor_status": "REJECTED",
        "reject_reason": reject_reason,
        "trace": {
            "confidence": 0,
            "evidence": evidence,
            "risk_flags": risk_flags,
        },
    }


def _build_execution_report(
    signal_id: str,
    status: str,
    reason: str,
    **extra: Any,
) -> Dict[str, Any]:
    report: Dict[str, Any] = {
        "signal_id": signal_id,
        "status": status,
        "reason": reason,
    }
    for key, value in extra.items():
        if value in (None, "", [], {}):
            continue
        report[key] = value
    return report


def _build_pipeline_envelope(ctx: PipelineRunContext, outcome: PipelineOutcome) -> Dict[str, Any]:
    route_snapshot = (
        ctx.scheduler_snapshot.get("route", {})
        if isinstance(ctx.scheduler_snapshot.get("route", {}), dict)
        else {}
    )
    route_market_context = (
        route_snapshot.get("market_context", {})
        if isinstance(route_snapshot.get("market_context", {}), dict)
        else {}
    )
    regime_audit = build_regime_audit_payload(route_market_context)
    # Sanitize signal dict 鈥?remove _original references that create circular
    # references when serialized to JSON (signal 鈫?contributors 鈫?_original 鈫?signal)
    signal_sanitized: Dict[str, Any] = {}
    if isinstance(ctx.signal, dict):
        for k, v in ctx.signal.items():
            if k == "_original":
                continue
            if isinstance(v, dict):
                signal_sanitized[k] = {sk: sv for sk, sv in v.items() if sk != "_original"}
            elif isinstance(v, list):
                signal_sanitized[k] = [
                    {ik: iv for ik, iv in (item.items() if isinstance(item, dict) else []) if ik != "_original"}
                    if isinstance(item, dict) else item
                    for item in v
                ]
            else:
                signal_sanitized[k] = v
    else:
        signal_sanitized = ctx.signal if isinstance(ctx.signal, dict) else {}

    effective_route_family = str(
        route_snapshot.get("route_family", "") or regime_audit.get("route_family", "")
    ).strip().lower()
    selected_instance_id, selected_strategy_id = _resolve_runtime_selected_identity(
        ctx,
        route_snapshot=route_snapshot,
    )
    envelope: Dict[str, Any] = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "tenant_id": ctx.tenant_id or "default",
        "signal": signal_sanitized,
        "incidents": outcome.incidents,
        "auto_actions": outcome.auto_actions,
        "execution_report": outcome.execution_report or {},
        "regime_audit": regime_audit,
        "route_audit": {
            "route_family": effective_route_family,
            "selected_strategy_id": str(selected_strategy_id or ""),
            "selected_instance_id": str(selected_instance_id or ""),
            "allowed_side": str(route_snapshot.get("allowed_side", "") or "").upper(),
        },
    }
    if outcome.include_runtime:
        envelope["_runtime"] = _runtime_payload(ctx.runtime_intent, ctx.source, ctx.execution_mode)
    # Debug fields omitted from ledger to keep entries lean.
    # Raw snapshots live in state files (agent_state.json / execution_state.json).
    if ctx.promotion_info:
        envelope["_rollout"] = ctx.promotion_info
    if outcome.extra_fields:
        envelope.update(outcome.extra_fields)
    return envelope


def _compact_runtime_snapshot(payload: Any) -> Dict[str, Any]:
    if not isinstance(payload, dict):
        return {}
    compact: Dict[str, Any] = {}
    for key, value in payload.items():
        if key == "state" or value in (None, "", [], {}):
            continue
        compact[key] = deepcopy(value)
    return compact


def _resolve_runtime_execution_owner(ctx: PipelineRunContext) -> tuple[str, str]:
    route_snapshot = (
        ctx.scheduler_snapshot.get("route", {})
        if isinstance(ctx.scheduler_snapshot.get("route", {}), dict)
        else {}
    )
    execution_decision = (
        ctx.aggregated_intent.get("execution_decision", {})
        if isinstance(ctx.aggregated_intent.get("execution_decision", {}), dict)
        else {}
    )
    selected_instance_id = str(
        ctx.signal.get("_selected_instance_id")
        or ctx.signal.get("selected_instance_id")
        or ctx.aggregated_intent.get("selected_instance_id")
        or ctx.scheduler_snapshot.get("selected_instance_id", "")
        or ""
    ).strip()
    selected_strategy_id = str(
        ctx.signal.get("_selected_strategy")
        or ctx.signal.get("selected_strategy_id")
        or ctx.aggregated_intent.get("selected_strategy_id")
        or ctx.scheduler_snapshot.get("selected_strategy_id", "")
        or ""
    ).strip()
    entry_owner_instance_id = str(
        execution_decision.get("entry_owner_instance_id")
        or ctx.aggregated_intent.get("entry_owner_instance_id")
        or route_snapshot.get("entry_owner_instance_id")
        or selected_instance_id
        or ""
    ).strip()
    entry_owner_strategy_id = str(
        execution_decision.get("entry_owner_strategy_id")
        or ctx.aggregated_intent.get("entry_owner_strategy_id")
        or route_snapshot.get("entry_owner_strategy_id")
        or selected_strategy_id
        or ""
    ).strip()
    action_upper = str(ctx.signal.get("action", "HOLD") or "HOLD").upper()
    if selected_instance_id and (
        not entry_owner_instance_id
        or entry_owner_instance_id.startswith("inst_")
        or (action_upper == "OPEN" and entry_owner_instance_id != selected_instance_id)
    ):
        entry_owner_instance_id = selected_instance_id
    if selected_strategy_id and (
        not entry_owner_strategy_id
        or (action_upper == "OPEN" and entry_owner_strategy_id != selected_strategy_id)
    ):
        entry_owner_strategy_id = selected_strategy_id
    if (
        action_upper == "OPEN"
        or selected_instance_id != entry_owner_instance_id
        or selected_strategy_id != entry_owner_strategy_id
    ):
        pass
    return entry_owner_instance_id, entry_owner_strategy_id


def _resolve_runtime_canonical_market_snapshot(ctx: PipelineRunContext) -> Dict[str, Any]:
    def _snapshot_is_stale(snapshot: Dict[str, Any]) -> bool:
        snapshot_generated_at = str(snapshot.get("generated_at", "") or "").strip()
        if snapshot_generated_at:
            try:
                snapshot_dt = datetime.fromisoformat(snapshot_generated_at.replace("Z", "+00:00"))
                return (datetime.now(timezone.utc) - snapshot_dt).total_seconds() > 30 * 60
            except Exception:
                return True
        return bool(snapshot)

    route_snapshot = (
        ctx.scheduler_snapshot.get("route", {})
        if isinstance(ctx.scheduler_snapshot.get("route", {}), dict)
        else {}
    )
    snapshot = (
        deepcopy(route_snapshot.get("canonical_market_snapshot", {}))
        if isinstance(route_snapshot.get("canonical_market_snapshot", {}), dict)
        else {}
    )
    if snapshot and not _snapshot_is_stale(snapshot):
        return snapshot
    aggregated_snapshot = (
        deepcopy(ctx.aggregated_intent.get("canonical_market_snapshot", {}))
        if isinstance(ctx.aggregated_intent.get("canonical_market_snapshot", {}), dict)
        else {}
    )
    if aggregated_snapshot and not _snapshot_is_stale(aggregated_snapshot):
        return aggregated_snapshot
    snapshot = snapshot or aggregated_snapshot
    snapshot_is_stale = _snapshot_is_stale(snapshot)
    if snapshot_is_stale or not snapshot:
        route_market_context = (
            route_snapshot.get("market_context", {})
            if isinstance(route_snapshot.get("market_context", {}), dict)
            else {}
        )
        macro_context = (
            ctx.agent_context.get("macro", {})
            if isinstance(ctx.agent_context.get("macro", {}), dict)
            else {}
        )
        interpreter_context = (
            ctx.agent_context.get("interpreter", {})
            if isinstance(ctx.agent_context.get("interpreter", {}), dict)
            else {}
        )
        snapshot = build_canonical_market_snapshot(
            route_market_context,
            macro_context=macro_context,
            interpreter_context=interpreter_context,
            symbol=normalize_symbol(
                ctx.engine_request.get("symbol", route_market_context.get("symbol", "XAUUSD"))
            ),
            timeframe=str(
                route_market_context.get("timeframe")
                or ctx.engine_request.get("timeframe")
                or ctx.engine_request.get("interval")
                or "M5"
            ).upper(),
        )
    return snapshot if isinstance(snapshot, dict) else {}


def _build_runtime_route_snapshot(ctx: PipelineRunContext) -> Dict[str, Any]:
    aggregated_route = (
        deepcopy(ctx.aggregated_intent.get("route", {}))
        if isinstance(ctx.aggregated_intent.get("route", {}), dict)
        else {}
    )
    route_snapshot = aggregated_route or (
        deepcopy(ctx.scheduler_snapshot.get("route", {}))
        if isinstance(ctx.scheduler_snapshot.get("route", {}), dict)
        else {}
    )
    entry_owner_instance_id, entry_owner_strategy_id = _resolve_runtime_execution_owner(ctx)
    canonical_market_snapshot = _resolve_runtime_canonical_market_snapshot(ctx)
    if canonical_market_snapshot:
        route_snapshot["canonical_market_snapshot"] = deepcopy(canonical_market_snapshot)
    selected_instance_id, selected_strategy_id = _resolve_runtime_selected_identity(
        ctx,
        route_snapshot=route_snapshot,
    )
    route_snapshot["selected_instance_id"] = str(selected_instance_id or "")
    route_snapshot["selected_strategy_id"] = str(selected_strategy_id or "")
    if entry_owner_instance_id:
        route_snapshot["entry_owner_instance_id"] = entry_owner_instance_id
    if entry_owner_strategy_id:
        route_snapshot["entry_owner_strategy_id"] = entry_owner_strategy_id
    aggregated_selection = (
        deepcopy(ctx.aggregated_intent.get("selection", {}))
        if isinstance(ctx.aggregated_intent.get("selection", {}), dict)
        else {}
    )
    selection = aggregated_selection or (
        deepcopy(route_snapshot.get("selection", {}))
        if isinstance(route_snapshot.get("selection", {}), dict)
        else {}
    )
    if selection:
        selection_mode = str(selection.get("selection_mode", "") or "").strip()
        if selection_mode == "candidate_pool":
            selection.setdefault(
                "candidate_pool_winner_instance_id",
                str(selection.get("winner_instance_id", "") or ""),
            )
            selection.setdefault(
                "candidate_pool_winner_strategy_id",
                str(selection.get("winner_strategy_id", "") or ""),
            )
        else:
            selection.pop("candidate_pool_winner_instance_id", None)
            selection.pop("candidate_pool_winner_strategy_id", None)
    if selection:
        route_snapshot["selection"] = selection
    return route_snapshot


def _derive_runtime_risk_decision(ctx: PipelineRunContext, outcome: PipelineOutcome) -> Dict[str, Any]:
    route_snapshot = (
        ctx.scheduler_snapshot.get("route", {})
        if isinstance(ctx.scheduler_snapshot.get("route", {}), dict)
        else {}
    )
    entry_owner_instance_id, entry_owner_strategy_id = _resolve_runtime_execution_owner(ctx)
    existing = (
        ctx.aggregated_intent.get("risk_decision", {})
        if isinstance(ctx.aggregated_intent.get("risk_decision", {}), dict)
        else {}
    )
    execution_report = outcome.execution_report if isinstance(outcome.execution_report, dict) else {}
    reject_reason = str(ctx.signal.get("reject_reason", "") or "")
    report_reason = str(execution_report.get("reason", "") or "")
    reason = reject_reason or report_reason
    decision = str(existing.get("decision", "") or "").lower()
    if not decision:
        decision = "allow"
        if str(execution_report.get("status", "") or "").upper() in {"REJECTED", "TIMEOUT"}:
            decision = "block"
        elif str(ctx.signal.get("action", "HOLD") or "HOLD").upper() == "HOLD" and reject_reason:
            decision = "block"
    risk_code = ""
    if decision == "block" and reason:
        risk_code = reason
    guard_snapshot = _compact_runtime_snapshot(existing.get("guard_snapshot", {}))
    if not guard_snapshot:
        guard_snapshot = _compact_runtime_snapshot(ctx.signal.get("open_guard", {}))
    if not guard_snapshot:
        guard_snapshot = _compact_runtime_snapshot(ctx.signal.get("structural_conflict_guard", {}))
    if not guard_snapshot:
        binding_direction = str(ctx.signal.get("binding_direction", "") or "")
        if binding_direction:
            guard_snapshot = {"binding_direction": binding_direction}
    risk_decision = {
        **existing,
        "decision": decision,
        "execution_owner": {
            "owner_mode": "pipeline_single_track",
            "entry_owner_instance_id": entry_owner_instance_id,
            "entry_owner_strategy_id": entry_owner_strategy_id,
        },
    }
    risk_decision.pop("risk_code", None)
    if risk_code:
        risk_decision["risk_code"] = risk_code
    if guard_snapshot:
        risk_decision["guard_snapshot"] = guard_snapshot
    return risk_decision


def _sync_runtime_scheduler_snapshot(ctx: PipelineRunContext, outcome: PipelineOutcome) -> None:
    if ctx.state_path is None:
        return
    route_snapshot = _build_runtime_route_snapshot(ctx)
    entry_owner_instance_id, entry_owner_strategy_id = _resolve_runtime_execution_owner(ctx)
    canonical_market_snapshot = (
        deepcopy(route_snapshot.get("canonical_market_snapshot", {}))
        if isinstance(route_snapshot.get("canonical_market_snapshot", {}), dict)
        else {}
    )
    if not canonical_market_snapshot:
        canonical_market_snapshot = _resolve_runtime_canonical_market_snapshot(ctx)
    risk_decision = _derive_runtime_risk_decision(ctx, outcome)
    ctx.aggregated_intent["risk_decision"] = risk_decision
    execution_decision = _synthesize_runtime_execution_decision(
        ctx,
        source="runtime_snapshot",
        fallback_reason=str(
            (outcome.execution_report or {}).get("reason", "")
            if isinstance(outcome.execution_report, dict)
            else ""
        ),
        route_snapshot_override=route_snapshot,
    )
    ctx.aggregated_intent["execution_decision"] = deepcopy(execution_decision)
    policy_sources = _build_runtime_policy_sources()
    route_selected_instance_id, route_selected_strategy_id = _resolve_runtime_selected_identity(
        ctx,
        route_snapshot=route_snapshot,
    )
    route_snapshot["selected_instance_id"] = str(route_selected_instance_id or "")
    route_snapshot["selected_strategy_id"] = str(route_selected_strategy_id or "")
    route_order_missing_winner = (
        str(
            route_snapshot.get("selection", {}).get("selection_mode", "")
            if isinstance(route_snapshot.get("selection", {}), dict)
            else ""
            or ""
        ).strip()
        == "route_order"
        and not str(
            route_snapshot.get("selection", {}).get("winner_instance_id", "")
            if isinstance(route_snapshot.get("selection", {}), dict)
            else ""
            or ""
        ).strip()
        and not str(
            route_snapshot.get("selection", {}).get("winner_strategy_id", "")
            if isinstance(route_snapshot.get("selection", {}), dict)
            else ""
            or ""
        ).strip()
    )
    aggregated_selected_instance_id = (
        ""
        if route_order_missing_winner
        else str(ctx.aggregated_intent.get("selected_instance_id", "") or "").strip()
    )
    aggregated_selected_strategy_id = (
        ""
        if route_order_missing_winner
        else str(ctx.aggregated_intent.get("selected_strategy_id", "") or "").strip()
    )
    executed_open = (
        str(execution_decision.get("decision", "") or "").upper() == "EXECUTE"
        and str(execution_decision.get("action", "") or "").upper() == "OPEN"
    )
    selected_instance_id = str(
        (
            execution_decision.get("executed_instance_id")
            or aggregated_selected_instance_id
            or execution_decision.get("selected_instance_id")
            or route_selected_instance_id
        )
        if executed_open
        else (
            execution_decision.get("selected_instance_id")
            or route_selected_instance_id
            or aggregated_selected_instance_id
        )
        or ""
    ).strip()
    selected_strategy_id = str(
        (
            execution_decision.get("executed_strategy_id")
            or aggregated_selected_strategy_id
            or execution_decision.get("selected_strategy_id")
            or route_selected_strategy_id
        )
        if executed_open
        else (
            execution_decision.get("selected_strategy_id")
            or route_selected_strategy_id
            or aggregated_selected_strategy_id
        )
        or ""
    ).strip()
    aggregated_reason = str(
        ctx.aggregated_intent.get("materialized_reason", "")
        if isinstance(ctx.aggregated_intent, dict)
        else ""
        or ""
    )
    if not aggregated_reason:
        aggregated_reason = str(ctx.signal.get("materialized_reason", "") or "")
    if not aggregated_reason:
        aggregated_reason = str(execution_decision.get("fit_reason", "") or "")
    if not aggregated_reason:
        aggregated_reason = str(
            route_snapshot.get("selection", {}).get("reason", "")
            if isinstance(route_snapshot.get("selection", {}), dict)
            else ""
            or ""
        )
    aggregated_materialized_action = str(
        ctx.aggregated_intent.get("materialized_action", "")
        if isinstance(ctx.aggregated_intent, dict)
        else ""
        or ""
    )
    if not aggregated_materialized_action:
        aggregated_materialized_action = str(ctx.signal.get("materialized_action", "") or "")
    if not aggregated_materialized_action:
        aggregated_materialized_action = str(execution_decision.get("action", "") or "")
    payload = {
        "snapshot_id": str(ctx.signal.get("signal_id", ctx.signal.get("trace_id", "")) or ""),
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "trace_id": str(ctx.signal.get("trace_id", ctx.signal.get("signal_id", "")) or ""),
        "signal_id": str(ctx.signal.get("signal_id", "") or ""),
        "mode": str(ctx.scheduler_snapshot.get("mode", "single_active") or "single_active"),
        "arbiter": str(ctx.scheduler_snapshot.get("arbiter", "allocation-weighted") or "allocation-weighted"),
        "reason": str(ctx.scheduler_snapshot.get("reason", "runtime_scheduler") or "runtime_scheduler"),
        "selected_instance_id": str(selected_instance_id or ""),
        "selected_strategy_id": str(selected_strategy_id or ""),
        "route_selected_instance_id": str(route_selected_instance_id or ""),
        "route_selected_strategy_id": str(route_selected_strategy_id or ""),
        "entry_owner_instance_id": entry_owner_instance_id,
        "entry_owner_strategy_id": entry_owner_strategy_id,
        "route": {
            **deepcopy(route_snapshot),
            "selected_instance_id": str(route_selected_instance_id or ""),
            "selected_strategy_id": str(route_selected_strategy_id or ""),
        },
        "selection": deepcopy(route_snapshot.get("selection", {})) if isinstance(route_snapshot.get("selection", {}), dict) else {},
        "canonical_market_snapshot": deepcopy(canonical_market_snapshot),
        "regime_audit": build_regime_audit_payload(
            route_snapshot.get("market_context", {}) if isinstance(route_snapshot.get("market_context", {}), dict) else {}
        ),
        "session_scheduler": {
            "enabled": bool(route_snapshot.get("session_scheduler_enabled", False)),
            "mode": str(route_snapshot.get("session_gate_mode", "soft_weight") or "soft_weight"),
            "current_session": str(route_snapshot.get("session_bucket", "") or ""),
            "current_session_profile": str(route_snapshot.get("session_mode", "") or ""),
            "current_policy": deepcopy(route_snapshot.get("session_current_policy", {})),
            "qualified_strategy_ids": list(route_snapshot.get("session_qualified_strategy_ids", [])),
            "blocked_strategy_ids": list(route_snapshot.get("session_blocked_strategy_ids", [])),
            "candidate_strategy_ids": list(route_snapshot.get("session_candidate_strategy_ids", [])),
            "decision": deepcopy(route_snapshot.get("session_decision", {})),
        },
        "execution_owner": deepcopy(risk_decision.get("execution_owner", {})),
        "execution_decision": deepcopy(execution_decision),
        "signal_aggregation": {
            "action": str(
                ctx.aggregated_intent.get("action", ctx.signal.get("action", "HOLD"))
                if isinstance(ctx.aggregated_intent, dict)
                else ctx.signal.get("action", "HOLD")
                or "HOLD"
            ),
            "side": str(
                ctx.aggregated_intent.get("side", ctx.signal.get("side", "NONE"))
                if isinstance(ctx.aggregated_intent, dict)
                else ctx.signal.get("side", "NONE")
                or "NONE"
            ),
            "materialized_action": aggregated_materialized_action,
            "materialized_reason": aggregated_reason,
            "selected_instance_id": str(
                aggregated_selected_instance_id or selected_instance_id
                or selected_instance_id
            ),
            "selected_strategy_id": str(
                aggregated_selected_strategy_id or selected_strategy_id
                or selected_strategy_id
            ),
            "blocked_reasons": list(
                ctx.aggregated_intent.get("blocked_reasons", [])
                if isinstance(ctx.aggregated_intent, dict) and isinstance(ctx.aggregated_intent.get("blocked_reasons", []), list)
                else []
            ),
        },
        "policy_sources": policy_sources,
        "risk_decision": risk_decision,
        "source_versions": {
            "effective_params": str(ctx.scheduler_snapshot.get("config_revision", "manual") or "manual"),
        },
    }
    _trace_selection_state(
        ctx.project_root,
        "runtime_snapshot_emit",
        route_snapshot,
        selection=route_snapshot.get("selection", {}) if isinstance(route_snapshot.get("selection", {}), dict) else {},
        extra={
            "decision_source": str(execution_decision.get("decision_source", "") or ""),
            "fit_reason": str(execution_decision.get("fit_reason", "") or ""),
        },
    )
    snapshot_path = ctx.project_root / "logs/runtime_scheduler_snapshot.json"
    _write_json(snapshot_path, payload)
    runtime_state = load_runtime_state(ctx.project_root, ctx.tenant_id)
    runtime_state["market_state"] = _build_runtime_market_state_snapshot(
        route_snapshot,
        canonical_market_snapshot,
        execution_decision,
        selected_strategy_id=str(selected_strategy_id or ""),
        selected_instance_id=str(selected_instance_id or ""),
    )
    runtime_state["selection_diagnostics"] = {
        "selected_strategy_id": str(selected_strategy_id or ""),
        "selected_instance_id": str(selected_instance_id or ""),
        "entry_owner_strategy_id": entry_owner_strategy_id,
        "entry_owner_instance_id": entry_owner_instance_id,
        "route_family": str(route_snapshot.get("route_family", "") or ""),
        "route_reason": str(
            route_snapshot.get("selection", {}).get("reason", "")
            if isinstance(route_snapshot.get("selection", {}), dict)
            else ""
            or ""
        ),
        "execution_reason": str(execution_decision.get("reason", "") or ""),
    }
    runtime_state["last_runtime_scheduler_snapshot_path"] = str(ctx.project_root / "logs/runtime_scheduler_snapshot.json")
    _save_runtime_state(ctx.project_root, ctx.tenant_id, runtime_state)


def _emit_pipeline_outcome(ctx: PipelineRunContext, outcome: PipelineOutcome) -> Dict[str, Any]:
    _sync_runtime_scheduler_snapshot(ctx, outcome)
    envelope = _build_pipeline_envelope(ctx, outcome)
    # Sanitize circular references before any JSON serialization
    # (signal dict may contain _original references from signal_arbitrator)
    envelope = _deep_sanitize_for_json(envelope)
    _store_pending_auto_actions(ctx.project_root, ctx.tenant_id, outcome.auto_actions)
    _write_json(ctx.output_path, envelope)
    if outcome.sync_mailbox:
        _sync_swarm_mailbox(
            ctx.project_root,
            ctx.mt5_snapshot,
            ctx.signal,
            outcome.execution_report or None,
        )
    if outcome.append_runtime_log:
        _append_log(ctx.project_root / "logs/runtime.log", envelope)
    decision_ledger_path = ctx.project_root / "logs/decision_ledger.ndjson"
    for event_name in outcome.ledger_events:
        append_ledger_event(decision_ledger_path, event_name, envelope)
    return envelope


def _get_runtime_blackboard(project_root: Path):
    global _RUNTIME_BLACKBOARD
    target_path = str(project_root / "blackboard_state.json")
    current_path = getattr(_RUNTIME_BLACKBOARD, "file_path", None) if _RUNTIME_BLACKBOARD is not None else None
    if _RUNTIME_BLACKBOARD is None or str(current_path or "") != target_path:
        try:
            from src.swarm.blackboard import Blackboard
        except ModuleNotFoundError:
            from swarm.blackboard import Blackboard
        _RUNTIME_BLACKBOARD = Blackboard(target_path)
    return _RUNTIME_BLACKBOARD


def _runtime_blackboard_log(project_root: Path, role: str, msg_type: str, text: str) -> None:
    _get_runtime_blackboard(project_root).add_log(role, msg_type, text)


def _runtime_blackboard_update(project_root: Path, namespace: str, key: str, value: Any) -> None:
    try:
        _get_runtime_blackboard(project_root).update(namespace, key, value)
    except Exception:
        pass


# 鈹€鈹€ 绛栫暐璺敱鏃ュ織 鈹€鈹€
_ROUTING_LOG_PATH: Optional[Path] = None
_ROUTING_LAST_LINE: str = ""
_SELECTION_TRACE_PATH: Optional[Path] = None


def _log_strategy_routing(
    project_root: Path,
    agent_context: Dict[str, Any],
    scheduler_snapshot: Dict[str, Any],
) -> None:
    """Write strategy routing decisions to a persistent log for audit.

    Logs regime classification, candidate strategies, selected strategy, and
    the reason for any routing changes. Emits at most once per distinct routing
    state to avoid log spam.
    """
    global _ROUTING_LOG_PATH, _ROUTING_LAST_LINE
    try:
        route = scheduler_snapshot.get("route", {}) if isinstance(scheduler_snapshot.get("route", {}), dict) else {}
        regime_audit = build_regime_audit_payload(
            route.get("market_context", {}) if isinstance(route.get("market_context", {}), dict) else {}
        )
        regime_4d = normalize_regime_label(agent_context.get("regime", "UNKNOWN"))
        route_family = route.get("route_family", "?")
        candidates = route.get("candidate_strategy_ids", [])
        selected = scheduler_snapshot.get("selected_strategy_id", "?")
        fallback = route.get("fallback", "")
        garbage = route.get("garbage_reason", "")
        allowed_side = route.get("allowed_side", "BOTH")

        line = (
            f"regime={regime_audit.get('regime', regime_4d)}|source={regime_audit.get('regime_source', 'UNKNOWN')}|"
            f"conf={regime_audit.get('regime_confidence', 0.0):.2f}|family={route_family}|"
            f"candidates={','.join(candidates[:5])}|"
            f"selected={selected}|side={allowed_side}"
            + (f"|fallback={fallback}" if fallback else "")
            + (f"|garbage={garbage[:60]}" if garbage else "")
        )
        if line == _ROUTING_LAST_LINE:
            return  # 鍘婚噸锛氬悓涓€鐘舵€佷笉閲嶅鍐?
        _ROUTING_LAST_LINE = line

        if _ROUTING_LOG_PATH is None:
            _ROUTING_LOG_PATH = project_root / "data" / "strategy_routing.log"
        _ROUTING_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
        with open(_ROUTING_LOG_PATH, "a", encoding="utf-8") as f:
            ts = time.strftime("%Y-%m-%dT%H:%M:%S")
            f.write(f"[{ts}] {line}\n")
        # 婊氬姩锛氫繚鐣欐渶杩?2000 琛?
        try:
            text = _ROUTING_LOG_PATH.read_text(encoding="utf-8")
            lines = text.strip().split("\n")
            if len(lines) > 2000:
                _ROUTING_LOG_PATH.write_text("\n".join(lines[-2000:]) + "\n", encoding="utf-8")
        except Exception:
            pass
    except Exception:
        pass  # 鏃ュ織澶辫触涓嶅奖鍝嶄氦鏄?


def _trace_selection_state(
    project_root: Optional[Path],
    stage: str,
    route: Optional[Dict[str, Any]],
    *,
    selection: Optional[Dict[str, Any]] = None,
    extra: Optional[Dict[str, Any]] = None,
) -> None:
    global _SELECTION_TRACE_PATH
    if project_root is None:
        return
    try:
        route_payload = route if isinstance(route, dict) else {}
        selection_payload = (
            selection
            if isinstance(selection, dict)
            else route_payload.get("selection", {})
            if isinstance(route_payload.get("selection", {}), dict)
            else {}
        )
        if _SELECTION_TRACE_PATH is None:
            _SELECTION_TRACE_PATH = project_root / "logs" / "selection_trace.ndjson"
        _SELECTION_TRACE_PATH.parent.mkdir(parents=True, exist_ok=True)
        payload = {
            "ts": datetime.now(timezone.utc).isoformat(),
            "stage": str(stage or ""),
            "module_file": str(__file__),
            "route_family": str(route_payload.get("route_family", "") or ""),
            "entry_policy": str(route_payload.get("entry_policy", "") or ""),
            "fallback": str(route_payload.get("fallback", "") or ""),
            "selection": deepcopy(selection_payload) if isinstance(selection_payload, dict) else {},
            "extra": deepcopy(extra) if isinstance(extra, dict) else {},
        }
        with open(_SELECTION_TRACE_PATH, "a", encoding="utf-8") as f:
            f.write(json.dumps(payload, ensure_ascii=False) + "\n")
    except Exception:
        pass


def _sync_multi_strategy_blackboard(
    project_root: Path,
    scheduler_snapshot: Dict[str, Any],
    strategy_signals: list[Dict[str, Any]],
    aggregated_intent: Dict[str, Any],
    report: Optional[Dict[str, Any]] = None,
) -> None:
    contributor_rows = (
        aggregated_intent.get("contributors", [])
        if isinstance(aggregated_intent.get("contributors", []), list)
        else []
    )
    selected_instance_id, selected_strategy_id = _resolve_route_selected_identity(
        scheduler_snapshot.get("route", {}) if isinstance(scheduler_snapshot.get("route", {}), dict) else {},
        fallback_strategy_id=str(aggregated_intent.get("selected_strategy_id", "") or ""),
        fallback_instance_id=str(aggregated_intent.get("selected_instance_id", "") or ""),
    )
    instance_rows: Dict[str, Dict[str, Any]] = {}
    aggregated_action = str(aggregated_intent.get("action", "HOLD") or "HOLD").upper()
    aggregated_side = str(aggregated_intent.get("side", "NONE") or "NONE").upper()
    blocked_reasons = [
        str(item) for item in aggregated_intent.get("blocked_reasons", [])
        if str(item).strip()
    ] if isinstance(aggregated_intent.get("blocked_reasons", []), list) else []
    selected_reason = str(aggregated_intent.get("selected_reason", "") or "")
    non_risk_hold_reasons = {
        "AGGREGATED_HOLD_LOW_NET_SCORE",
        "AGGREGATED_HOLD_LOW_ACTIONABLE_WEIGHT",
        "AGGREGATED_HOLD_NO_ACTIONABLE_SIGNAL",
    }
    governor_blockers = [
        reason
        for reason in blocked_reasons
        if not any(marker in reason for marker in non_risk_hold_reasons)
    ]
    for signal in strategy_signals:
        if not isinstance(signal, dict):
            continue
        instance_id = str(signal.get("_instance_id", "") or "").strip()
        if not instance_id:
            continue
        side = str(signal.get("side", "NONE") or "NONE").upper()
        action = str(signal.get("action", "HOLD") or "HOLD").upper()
        signed_score = _signal_signed_score(signal, float(signal.get("instance_weight", 0.0) or 0.0))
        suppression_reason = ""
        if aggregated_action == "HOLD" and action != "HOLD":
            suppression_reason = "conflict_or_low_net_score"
        elif action != "HOLD" and side != aggregated_side:
            suppression_reason = "opposite_side_offset"
        instance_rows[instance_id] = {
            "strategy_id": str(signal.get("_sig_strategy_id", signal.get("strategy_id", "")) or ""),
            "action": action,
            "side": side,
            "confidence": round(float((signal.get("trace", {}) or {}).get("confidence", 0.0) or 0.0), 4),
            "signed_score": round(float(signed_score), 6),
            "weight": round(float(signal.get("instance_weight", 0.0) or 0.0), 4),
            "selected": instance_id == selected_instance_id,
            "suppressed": bool(suppression_reason),
            "suppression_reason": suppression_reason,
            "raw_reason": _signal_reason_text(signal),
            "reject_reason": str(signal.get("reject_reason", "") or ""),
        }
    # 鈹€鈹€ Per-instance health scoring (computed once, used for both risk_state and health_score) 鈹€鈹€
    hub = get_intelligence_hub()
    instances_for_health = scheduler_snapshot.get("instances", []) if isinstance(scheduler_snapshot.get("instances", []), list) else []
    health_results = hub.refresh_all(project_root, instances_for_health)
    instance_health_score = {
        iid: h.score
        for iid, h in health_results.items()
    }
    for iid, h in health_results.items():
        if iid not in instance_health_score:
            instance_health_score[iid] = 50.0
    for instance in instances_for_health:
        iid = str(instance.get("instance_id", "") or "").strip()
        if iid and iid not in instance_health_score:
            instance_health_score[iid] = 50.0

    instance_risk_state: Dict[str, str] = {}
    for row in contributor_rows:
        if not isinstance(row, dict):
            continue
        iid = str(row.get("instance_id", "") or "").strip()
        if not iid:
            continue
        base_state = "blocked" if str(row.get("reason", "") or "").strip() else "normal"
        health = health_results.get(iid)
        if health:
            if health.suggested_action == "shadow_only":
                base_state = "blocked"
            elif health.suggested_action == "rehabilitate":
                base_state = "rehab"
            elif health.suggested_action == "reduce_size" and base_state == "normal":
                base_state = "reduced"
        instance_risk_state[iid] = base_state
    for iid, health in health_results.items():
        if iid not in instance_risk_state:
            if health.suggested_action == "shadow_only":
                instance_risk_state[iid] = "blocked"
            elif health.suggested_action == "rehabilitate":
                instance_risk_state[iid] = "rehab"
            elif health.suggested_action == "reduce_size":
                instance_risk_state[iid] = "reduced"
            else:
                instance_risk_state[iid] = "normal"
    active_strategy_ids = (
        list(scheduler_snapshot.get("active_strategy_ids", []))
        if isinstance(scheduler_snapshot.get("active_strategy_ids", []), list)
        else []
    )
    strategy_loaded_ids = list(dict.fromkeys(str(item).strip() for item in active_strategy_ids if str(item).strip()))
    strategy_health_map: Dict[str, Dict[str, Any]] = {}
    for instance in instances_for_health:
        if not isinstance(instance, dict):
            continue
        iid = str(instance.get("instance_id", "") or "").strip()
        sid = str(instance.get("strategy_id", "") or "").strip()
        if not sid:
            continue
        if sid not in strategy_loaded_ids:
            strategy_loaded_ids.append(sid)
        health = health_results.get(iid)
        score = float(getattr(health, "score", 50.0) or 50.0) if health is not None else 50.0
        previous = strategy_health_map.get(sid)
        if previous is None or score >= float(previous.get("_score", 0.0) or 0.0):
            strategy_health_map[sid] = {
                "strategy_id": sid,
                "loaded": True,
                "healthy": bool(health is not None and getattr(health, "suggested_action", "normal") != "shadow_only"),
                "instance_id": iid,
                "score": round(score, 2),
                "risk_state": instance_risk_state.get(iid, "normal"),
                "_score": score,
            }
    for sid in strategy_loaded_ids:
        if sid not in strategy_health_map:
            strategy_health_map[sid] = {
                "strategy_id": sid,
                "loaded": True,
                "healthy": True,
                "instance_id": "",
                "score": 50.0,
                "risk_state": "normal",
                "_score": 50.0,
            }
    for value in strategy_health_map.values():
        value.pop("_score", None)
    global_state = "blocked" if governor_blockers else "normal"
    if isinstance(report, dict) and str(report.get("status", "") or "").upper() in {"REJECTED", "TIMEOUT", "RETRY_EXHAUSTED", "INIT_FAILED"}:
        global_state = "blocked"

    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "mode", str(scheduler_snapshot.get("mode", "single_active") or "single_active"))
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "arbiter", str(scheduler_snapshot.get("arbiter", "allocation-weighted") or "allocation-weighted"))
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "reason", str(scheduler_snapshot.get("reason", "runtime_scheduler") or "runtime_scheduler"))
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "active_strategy_ids", list(scheduler_snapshot.get("active_strategy_ids", [])) if isinstance(scheduler_snapshot.get("active_strategy_ids", []), list) else [])
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "active_instance_ids", list(scheduler_snapshot.get("active_instance_ids", [])) if isinstance(scheduler_snapshot.get("active_instance_ids", []), list) else [])
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "strategy_weights", dict(scheduler_snapshot.get("strategy_weights", {})) if isinstance(scheduler_snapshot.get("strategy_weights", {}), dict) else {})
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "instance_weights", dict(scheduler_snapshot.get("instance_weights", {})) if isinstance(scheduler_snapshot.get("instance_weights", {}), dict) else {})
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "selected_strategy_id", selected_strategy_id)
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "selected_instance_id", selected_instance_id)
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "loaded_strategy_ids", strategy_loaded_ids)
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "strategy_health", strategy_health_map)
    route_snapshot = (
        scheduler_snapshot.get("route", {})
        if isinstance(scheduler_snapshot.get("route", {}), dict)
        else {}
    )
    selected_signal = strategy_signals[0] if strategy_signals and isinstance(strategy_signals[0], dict) else {}
    runtime_entry_owner_instance_id = str(
        aggregated_intent.get("entry_owner_instance_id")
        or selected_signal.get("_selected_instance_id")
        or selected_signal.get("selected_instance_id")
        or route_snapshot.get("entry_owner_instance_id")
        or ""
    )
    runtime_entry_owner_strategy_id = str(
        aggregated_intent.get("entry_owner_strategy_id")
        or selected_signal.get("_selected_strategy")
        or selected_signal.get("selected_strategy_id")
        or route_snapshot.get("entry_owner_strategy_id")
        or ""
    )
    runtime_canonical_market_snapshot = (
        aggregated_intent.get("canonical_market_snapshot", {})
        if isinstance(aggregated_intent.get("canonical_market_snapshot", {}), dict)
        else route_snapshot.get("canonical_market_snapshot", {})
        if isinstance(route_snapshot.get("canonical_market_snapshot", {}), dict)
        else {}
    )
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "route", route_snapshot)
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "route_family", str(route_snapshot.get("route_family", "") or ""))
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "allowed_side", str(route_snapshot.get("allowed_side", "BOTH") or "BOTH"))
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "candidate_strategy_ids", list(route_snapshot.get("candidate_strategy_ids", [])) if isinstance(route_snapshot.get("candidate_strategy_ids", []), list) else [])
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "router_weights", dict(route_snapshot.get("weights", {})) if isinstance(route_snapshot.get("weights", {}), dict) else {})
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "direction_guard", dict(route_snapshot.get("direction_guard", {})) if isinstance(route_snapshot.get("direction_guard", {}), dict) else {})
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "selection", dict(route_snapshot.get("selection", {})) if isinstance(route_snapshot.get("selection", {}), dict) else {})
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "canonical_market_snapshot", dict(runtime_canonical_market_snapshot) if isinstance(runtime_canonical_market_snapshot, dict) else {})
    _runtime_blackboard_update(project_root, "Scheduler_Runtime", "execution_decision", dict(aggregated_intent.get("execution_decision", {})) if isinstance(aggregated_intent.get("execution_decision", {}), dict) else {})

    _runtime_blackboard_update(project_root, "Strategy_Instances", "instances", instance_rows)
    _runtime_blackboard_update(project_root, "Strategy_Instances", "selected_strategy_id", selected_strategy_id)
    _runtime_blackboard_update(project_root, "Strategy_Instances", "selected_instance_id", selected_instance_id)
    _runtime_blackboard_update(project_root, "Strategy_Instances", "contributors_count", len(contributor_rows))

    _runtime_blackboard_update(project_root, "Execution_Arbitration", "final_action", aggregated_action)
    _runtime_blackboard_update(project_root, "Execution_Arbitration", "final_side", aggregated_side)
    _runtime_blackboard_update(project_root, "Execution_Arbitration", "net_score", round(float(aggregated_intent.get("net_score", 0.0) or 0.0), 6))
    _runtime_blackboard_update(project_root, "Execution_Arbitration", "confidence", round(float(aggregated_intent.get("confidence", 0.0) or 0.0), 4))
    _runtime_blackboard_update(
        project_root,
        "Execution_Arbitration",
        "hold_mode",
        "blocked_hold" if aggregated_action == "HOLD" and governor_blockers else "signal_hold" if aggregated_action == "HOLD" and blocked_reasons else "selected_action",
    )
    _runtime_blackboard_update(project_root, "Execution_Arbitration", "blocked_reasons", blocked_reasons)
    _runtime_blackboard_update(project_root, "Execution_Arbitration", "selected_reason", selected_reason)
    _runtime_blackboard_update(project_root, "Execution_Arbitration", "hold_diagnostics", _hold_diagnostics_summary(aggregated_intent) if aggregated_action == "HOLD" else {})
    _runtime_blackboard_update(project_root, "Execution_Arbitration", "selected_strategy_id", selected_strategy_id)
    _runtime_blackboard_update(project_root, "Execution_Arbitration", "selected_instance_id", selected_instance_id)
    _runtime_blackboard_update(project_root, "Execution_Arbitration", "entry_owner_strategy_id", runtime_entry_owner_strategy_id)
    _runtime_blackboard_update(project_root, "Execution_Arbitration", "entry_owner_instance_id", runtime_entry_owner_instance_id)
    _runtime_blackboard_update(project_root, "Execution_Arbitration", "selection", dict(route_snapshot.get("selection", {})) if isinstance(route_snapshot.get("selection", {}), dict) else {})
    _runtime_blackboard_update(project_root, "Execution_Arbitration", "canonical_market_snapshot", dict(runtime_canonical_market_snapshot) if isinstance(runtime_canonical_market_snapshot, dict) else {})
    _runtime_blackboard_update(project_root, "Execution_Arbitration", "execution_decision", dict(aggregated_intent.get("execution_decision", {})) if isinstance(aggregated_intent.get("execution_decision", {}), dict) else {})
    _runtime_blackboard_update(project_root, "Execution_Arbitration", "last_execution_status", str(report.get("status", "") or "") if isinstance(report, dict) else "")

    _runtime_blackboard_update(project_root, "Risk_Protocol", "governor_blockers", governor_blockers)
    _runtime_blackboard_update(project_root, "Risk_Protocol", "active_strategy_ids", list(scheduler_snapshot.get("active_strategy_ids", [])) if isinstance(scheduler_snapshot.get("active_strategy_ids", []), list) else [])
    _runtime_blackboard_update(project_root, "Risk_Protocol", "active_instance_ids", list(scheduler_snapshot.get("active_instance_ids", [])) if isinstance(scheduler_snapshot.get("active_instance_ids", []), list) else [])
    _runtime_blackboard_update(project_root, "Risk_Protocol", "instance_risk_state", instance_risk_state)
    _runtime_blackboard_update(project_root, "Risk_Protocol", "instance_health_score", instance_health_score)
    _runtime_blackboard_update(project_root, "Risk_Protocol", "per_instance_health_details", {
        iid: h.to_dict()
        for iid, h in health_results.items()
    })
    _runtime_blackboard_update(project_root, "Risk_Protocol", "health_alerts", list(scheduler_snapshot.get("health_alerts", [])) if isinstance(scheduler_snapshot.get("health_alerts", []), list) else [])
    _runtime_blackboard_update(project_root, "Risk_Protocol", "cross_strategy_correlation", aggregated_intent.get("correlation", {}))
    _runtime_blackboard_update(project_root, "Risk_Protocol", "voting_weights", aggregated_intent.get("voting_weights", {}))
    _runtime_blackboard_update(project_root, "Risk_Protocol", "global_state", global_state)


def _mailbox_emit(project_root: Path, role: str, msg_type: str, text: str, dedupe_key: str, min_interval_sec: float) -> None:
    now = time.time()
    stale_before = now - _MAILBOX_LAST_EMIT_TTL_SECONDS
    stale_keys = [key for key, ts in _MAILBOX_LAST_EMIT.items() if float(ts or 0.0) < stale_before]
    for key in stale_keys:
        _MAILBOX_LAST_EMIT.pop(key, None)
    if len(_MAILBOX_LAST_EMIT) > _MAILBOX_LAST_EMIT_MAX_KEYS:
        newest_items = sorted(
            _MAILBOX_LAST_EMIT.items(),
            key=lambda item: float(item[1] or 0.0),
            reverse=True,
        )[:_MAILBOX_LAST_EMIT_MAX_KEYS]
        _MAILBOX_LAST_EMIT.clear()
        _MAILBOX_LAST_EMIT.update(newest_items)
    last_ts = _MAILBOX_LAST_EMIT.get(dedupe_key, 0.0)
    if now - last_ts < min_interval_sec:
        return
    _MAILBOX_LAST_EMIT[dedupe_key] = now
    _runtime_blackboard_log(project_root, role, msg_type, text)


def _emit_scheduler_health_alerts(project_root: Path, scheduler_snapshot: Dict[str, Any]) -> None:
    for alert in _dedupe_string_list(scheduler_snapshot.get("health_alerts", [])):
        _mailbox_emit(
            project_root,
            "Risk_Commissar",
            "WARN",
            f"Runtime scheduler alert: {alert}",
            f"scheduler_alert:{alert}",
            60.0,
        )


def _signal_reason_text(signal: Dict[str, Any]) -> str:
    trace = signal.get("trace", {}) if isinstance(signal.get("trace", {}), dict) else {}
    evidence = trace.get("evidence", [])
    if isinstance(evidence, list) and evidence:
        return " | ".join(str(item) for item in evidence[:4])
    reject_reason = str(signal.get("reject_reason", "") or "").strip()
    if reject_reason:
        return reject_reason
    return str(signal.get("mode", "UNCERTAIN") or "UNCERTAIN")


def _hold_diagnostics_summary(aggregated_intent: Dict[str, Any]) -> dict[str, Any]:
    contributors = (
        aggregated_intent.get("contributors", [])
        if isinstance(aggregated_intent.get("contributors", []), list)
        else []
    )
    blocked_reasons = [
        str(item) for item in aggregated_intent.get("blocked_reasons", [])
        if str(item).strip()
    ] if isinstance(aggregated_intent.get("blocked_reasons", []), list) else []
    suppressed: list[dict[str, Any]] = []
    for row in contributors:
        if not isinstance(row, dict):
            continue
        action = str(row.get("action", "HOLD") or "HOLD").upper()
        reason = str(row.get("reason", "") or "").strip()
        if action == "HOLD" or reason:
            suppressed.append(
                {
                    "instance_id": str(row.get("instance_id", "") or ""),
                    "strategy_id": str(row.get("strategy_id", "") or ""),
                    "action": action,
                    "side": str(row.get("side", "NONE") or "NONE").upper(),
                    "reason": reason or "INSTANCE_SIGNAL_HOLD",
                    "confidence": float(row.get("confidence", 0.0) or 0.0),
                    "weight": float(row.get("weight", 0.0) or 0.0),
                    "signed_score": float(row.get("signed_score", 0.0) or 0.0),
                }
            )
    return {
        "blocked_reasons": blocked_reasons,
        "suppressed_instances": suppressed,
        "actionable_count": int(aggregated_intent.get("actionable_count", 0) or 0),
        "actionable_weight_total": float(aggregated_intent.get("actionable_weight_total", 0.0) or 0.0),
        "actionable_weight_floor": float(aggregated_intent.get("actionable_weight_floor", 0.0) or 0.0),
        "net_score": float(aggregated_intent.get("net_score", 0.0) or 0.0),
        "raw_net_score": float(aggregated_intent.get("raw_net_score", 0.0) or 0.0),
        "selected_instance_id": str(aggregated_intent.get("selected_instance_id", "") or ""),
        "selected_strategy_id": str(aggregated_intent.get("selected_strategy_id", "") or ""),
    }


def _sync_swarm_mailbox(
    project_root: Path,
    mt5_snapshot: Dict[str, Any],
    signal: Dict[str, Any],
    report: Optional[Dict[str, Any]],
) -> None:
    market = mt5_snapshot.get("market", {}) if isinstance(mt5_snapshot.get("market", {}), dict) else {}
    bid = float(
        mt5_snapshot.get("bid", mt5_snapshot.get("Bid", market.get("bid", market.get("Bid", 0.0)))) or 0.0
    )
    ask = float(
        mt5_snapshot.get("ask", mt5_snapshot.get("Ask", market.get("ask", market.get("Ask", 0.0)))) or 0.0
    )
    mode = str(signal.get("mode", "UNCERTAIN") or "UNCERTAIN")
    if bid > 0 and ask > 0:
        quote_text = f"Quote: {bid:.2f}/{ask:.2f} [{mode}]"
        quote_key = f"quote:{mode}:{round(bid, 2)}:{round(ask, 2)}"
        _mailbox_emit(project_root, "Micro_Sniper", "REPORT", quote_text, quote_key, 30.0)

    action = str(signal.get("action", "HOLD") or "HOLD").upper()
    side = str(signal.get("side", "NONE") or "NONE").upper()
    confidence = float((signal.get("trace", {}) or {}).get("confidence", 0.0) or 0.0) * 100.0
    reason_text = _signal_reason_text(signal)
    inferred_side = "SELL" if "lt" in reason_text.lower() or "bear" in reason_text.lower() else "BUY"

    if action != "HOLD" and side in {"BUY", "SELL"}:
        side_zh = "LONG(BUY)" if side == "BUY" else "SHORT(SELL)"
        alert_text = f"Signal: {side_zh} (conf: {confidence:.1f}%), reason: {reason_text}"
        _mailbox_emit(project_root, "Micro_Sniper", "alert", alert_text, f"signal:{signal.get('signal_id')}", 1.0)
    elif confidence >= 60.0:
        side_zh = "LONG(BUY)" if side == "BUY" else "SHORT(SELL)"
        alert_text = f"Signal alert: {side_zh} (conf: {confidence:.1f}%), reason: {reason_text}"
        _mailbox_emit(project_root, "Micro_Sniper", "alert", alert_text, f"opportunity:{signal.get('signal_id')}", 2.0)
        side_zh = "SHORT(SELL)" if inferred_side == "SELL" else "LONG(BUY)"
        alert_text = f"Potential {side_zh} opportunity (conf: {confidence:.1f}%), result HOLD: {reason_text}"
        status = str(report.get("status", "") or "").upper()
        reason = str(report.get("reason", "") or "").strip()
        if status == "REJECTED":
            risk_text = f'Execution rejected: {reason or "UNKNOWN_REJECT_REASON"}'
            _mailbox_emit(project_root, "Risk_Commissar", "WARN", risk_text, f"risk:reject:{signal.get('signal_id')}:{reason}", 20.0)
        elif status in {"ACKED", "FILLED"} and side in {"BUY", "SELL"}:
            side_zh = "LONG(BUY)" if side == "BUY" else "SHORT(SELL)"
            risk_text = f"Risk: {side_zh} execution status={status}"
            _mailbox_emit(project_root, "Risk_Commissar", "success", risk_text, f"risk:exec:{signal.get('signal_id')}:{status}", 20.0)
    elif action == "HOLD" and confidence >= 60.0:
        side_zh = "SHORT(SELL)" if inferred_side == "SELL" else "LONG(BUY)"
        risk_text = (
            f"Risk report: {side_zh} execution status update"
            f"Detail: {reason_text}"
        )
        _mailbox_emit(project_root, "Risk_Commissar", "INFO", risk_text, f"risk:hold:{signal.get('signal_id')}", 20.0)


def _load_snapshot(source: str, input_path: Path, live_client: Optional[Any]) -> Dict[str, Any]:
    if source in {"mt5", "ea_feed"}:
        if live_client is None:
            raise RuntimeError("LIVE_SOURCE_NOT_CONNECTED")
        snap = live_client.snapshot()
        if snap is None:
            raise RuntimeError("LIVE_SOURCE_SNAPSHOT_UNAVAILABLE")
        return snap
    return load_mt5_snapshot(input_path)


def _load_effective_payload(project_root: Path) -> Dict[str, Any]:
    path = project_root / "logs" / "effective_params.json"
    if not path.exists():
        return {}
    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
        return payload if isinstance(payload, dict) else {}
    except Exception:
        return {}


def _get_running_strategy_ids() -> list[str]:
    """Return strategy_ids of all user-deployed running instances.

    Single source: unified_strategy_state.json (via unified_strategy_state module).
    """
    try:
        from src.unified_strategy_state import get_running_strategy_ids as _get_ids
        return _get_ids()
    except ImportError:
        # Unified state module not available 鈥?last-resort fallback
        try:
            from bridge.gold_trader import _daemon_log
            _daemon_log("[LEGACY_FALLBACK] _get_running_strategy_ids: unified module not importable 鈥?returning empty strategy set", "WARN")
        except Exception:
            pass
        return []


def _get_active_ea_symbol(default: str = "XAUUSD") -> str:
    """Read the active symbol from terminal_status.json (EA chart symbol)."""
    try:
        from config_manager import get_mt5_dir
        import pathlib as _pl
        _ts = _pl.Path(get_mt5_dir()) / "gold_autotrade" / "bridge" / "terminal_status.json"
        if _ts.exists():
            _snap = json.loads(_ts.read_text(encoding="utf-8"))
            _sym = str(_snap.get("symbol", "") or "").strip().upper()
            if _sym:
                return _sym
    except Exception:
        pass
    return default


def _load_runtime_instances(project_root: Path) -> list[dict[str, Any]]:
    """Return running instances as a list (pipeline-compatible format).

    Single source: unified_strategy_state.json.
    """
    try:
        from src.unified_strategy_state import get_running_instances
        return get_running_instances()
    except ImportError:
        # Legacy fallback for boot-time before migration 鈥?unified module not available
        try:
            from bridge.gold_trader import _daemon_log
            _daemon_log("[LEGACY_FALLBACK] _load_runtime_instances: unified module not importable", "WARN")
        except Exception:
            pass
        return _load_runtime_instances_legacy(project_root)


def _load_runtime_instances_legacy(project_root: Path) -> list[dict[str, Any]]:
    """[DEPRECATED] Legacy fallback reader 鈥?only used when unified module is unavailable.

    This function reads strategy_market_state.json directly. It will be removed
    once all deployments have migrated to unified_strategy_state.json.
    """
    path = project_root / "config" / "strategy_market_state.json"
    if not path.exists():
        return []
    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return []
    runtime_instances = payload.get("runtime_instances", []) if isinstance(payload.get("runtime_instances", []), list) else []
    return [item for item in runtime_instances if isinstance(item, dict)]


def _load_scheduler_snapshot(project_root: Path, engine_request: Dict[str, Any]) -> Dict[str, Any]:
    """Load scheduler snapshot from unified strategy state.

    Single source: unified_strategy_state.json 鈫?scheduler block.
    Falls back to legacy merge only if unified module is unavailable.
    """
    try:
        from src.unified_strategy_state import load_unified_state, get_scheduler, get_instances
        state = load_unified_state(project_root, use_cache=False)
        scheduler = get_scheduler(state)
        instances_dict = get_instances(state)
        running_instances = [
            dict(inst)
            for inst in instances_dict.values()
            if isinstance(inst, dict) and str(inst.get("status", "")).lower() == "running"
        ]
        running_instance_ids = [
            str(inst.get("instance_id", "")).strip()
            for inst in running_instances
            if str(inst.get("instance_id", "")).strip()
        ]
        running_strategy_ids: list[str] = []
        running_strategy_instance_map: Dict[str, list[str]] = {}
        instance_weights_from_running: Dict[str, float] = {}
        total_allocation = 0.0
        for inst in running_instances:
            instance_id = str(inst.get("instance_id", "")).strip()
            strategy_id = canonical_strategy_id(inst.get("strategy_id", ""))
            if not instance_id or not strategy_id:
                continue
            if strategy_id not in running_strategy_ids:
                running_strategy_ids.append(strategy_id)
            running_strategy_instance_map.setdefault(strategy_id, []).append(instance_id)
            alloc = max(0.0, float(inst.get("allocation", inst.get("weight", 0.0)) or 0.0))
            instance_weights_from_running[instance_id] = alloc
            total_allocation += alloc

        # Honour pause/resume: filter active_instance_ids to only running instances
        active_iids = [
            iid for iid in scheduler.get("active_instance_ids", [])
            if str(instances_dict.get(iid, {}).get("status", "")).lower() == "running"
        ]
        scheduler_strategy_ids = [
            canonical_strategy_id(item)
            for item in scheduler.get("active_strategy_ids", [])
            if str(item).strip()
        ] if isinstance(scheduler.get("active_strategy_ids", []), list) else []
        running_set = set(running_instance_ids)
        active_set = set(active_iids)
        scheduler_is_stale = bool(running_set) and (
            not active_iids
            or active_set != running_set
            or set(scheduler_strategy_ids) != set(running_strategy_ids)
        )
        if scheduler_is_stale:
            if total_allocation > 0:
                normalized_instance_weights = {
                    instance_id: round(weight / total_allocation, 4)
                    for instance_id, weight in instance_weights_from_running.items()
                }
            else:
                equal_weight = round(1.0 / max(1, len(running_instance_ids)), 4) if running_instance_ids else 0.0
                normalized_instance_weights = {
                    instance_id: equal_weight for instance_id in running_instance_ids
                }
            strategy_weights = {
                strategy_id: round(
                    sum(normalized_instance_weights.get(instance_id, 0.0) for instance_id in instance_ids),
                    4,
                )
                for strategy_id, instance_ids in running_strategy_instance_map.items()
            }
            selected_instance_id = ""
            selected_strategy_id = ""
            if normalized_instance_weights:
                selected_instance_id = max(
                    normalized_instance_weights.items(),
                    key=lambda item: (float(item[1]), item[0]),
                )[0]
                for strategy_id, instance_ids in running_strategy_instance_map.items():
                    if selected_instance_id in instance_ids:
                        selected_strategy_id = strategy_id
                        break
            scheduler = {
                **dict(scheduler),
                "mode": "single_active" if len(running_instance_ids) == 1 else "multi_active" if running_instance_ids else "idle",
                "arbiter": str(scheduler.get("arbiter", "allocation-weighted") or "allocation-weighted"),
                "reason": "reconciled_from_running_instances",
                "active_instances": list(running_instance_ids),
                "active_instance_ids": list(running_instance_ids),
                "active_strategy_ids": list(running_strategy_ids),
                "instance_weights": normalized_instance_weights,
                "strategy_weights": strategy_weights,
                "strategy_instance_map": running_strategy_instance_map,
                "instances": running_instances,
                "selected_instance_id": selected_instance_id,
                "selected_strategy_id": selected_strategy_id,
            }
            return scheduler
        if active_iids:
            scheduler["active_instance_ids"] = active_iids
            scheduler["active_instances"] = active_iids
        scheduler_instance_weights = (
            scheduler.get("instance_weights", {})
            if isinstance(scheduler.get("instance_weights", {}), dict)
            else {}
        )
        if active_iids and (
            not scheduler_instance_weights
            or any(str(iid) not in scheduler_instance_weights for iid in active_iids)
            or all(float(scheduler_instance_weights.get(iid, 0.0) or 0.0) <= 0.0 for iid in active_iids)
        ):
            rebuilt_instance_weights: Dict[str, float] = {}
            rebuilt_strategy_weights: Dict[str, float] = {}
            rebuilt_strategy_instance_map: Dict[str, list[str]] = {}
            total_alloc = 0.0
            for instance_id in active_iids:
                row = instances_dict.get(instance_id, {})
                strategy_id = canonical_strategy_id(row.get("strategy_id", ""))
                alloc = max(0.0, float(row.get("allocation", row.get("weight", 0.0)) or 0.0))
                rebuilt_instance_weights[instance_id] = alloc
                total_alloc += alloc
                if strategy_id:
                    rebuilt_strategy_weights[strategy_id] = rebuilt_strategy_weights.get(strategy_id, 0.0) + alloc
                    rebuilt_strategy_instance_map.setdefault(strategy_id, []).append(instance_id)
            if total_alloc > 0:
                rebuilt_instance_weights = {
                    instance_id: round(weight / total_alloc, 4)
                    for instance_id, weight in rebuilt_instance_weights.items()
                }
                rebuilt_strategy_weights = {
                    strategy_id: round(weight / total_alloc, 4)
                    for strategy_id, weight in rebuilt_strategy_weights.items()
                }
            elif active_iids:
                equal_weight = round(1.0 / len(active_iids), 4)
                rebuilt_instance_weights = {instance_id: equal_weight for instance_id in active_iids}
                rebuilt_strategy_weights = {
                    strategy_id: round(
                        sum(rebuilt_instance_weights.get(instance_id, 0.0) for instance_id in instance_ids),
                        4,
                    )
                    for strategy_id, instance_ids in rebuilt_strategy_instance_map.items()
                }
            scheduler = {
                **dict(scheduler),
                "reason": "recovered_missing_instance_weights",
                "instance_weights": rebuilt_instance_weights,
                "strategy_weights": rebuilt_strategy_weights,
                "strategy_instance_map": rebuilt_strategy_instance_map,
            }

        if scheduler.get("active_instance_ids") or scheduler.get("active_strategy_ids"):
            # Build instance_map for downstream consumers
            if "instances" not in scheduler or not scheduler["instances"]:
                scheduler["instances"] = [
                    inst for inst in instances_dict.values()
                    if isinstance(inst, dict)
                    and str(inst.get("instance_id", "")).strip() in active_iids
                ] if active_iids else list(instances_dict.values())
            # Ensure selected_strategy_id/selected_instance_id never empty
            # when active instances exist (regardless of stale check).
            if not scheduler.get("selected_strategy_id") and scheduler.get("instance_weights"):
                _weights = scheduler["instance_weights"]
                _strategy_weights = scheduler.get("strategy_weights", {})
                _top_sid = max(_strategy_weights.items(), key=lambda kv: (float(kv[1]), kv[0]))[0] if _strategy_weights else ""
                _sid_map = scheduler.get("strategy_instance_map", {})
                _top_iid = (_sid_map.get(_top_sid) or [None])[0] if _top_sid else ""
                if _top_iid and _top_sid:
                    scheduler["selected_instance_id"] = _top_iid
                    scheduler["selected_strategy_id"] = _top_sid
            return scheduler
        local_config = _read_json_dict(project_root / "config" / "active_instances.json")
        raw_instances = local_config.get("instances", []) if isinstance(local_config.get("instances", []), list) else []
        explicit_instances = [item for item in raw_instances if isinstance(item, dict) and item.get("enabled", True)]
        if explicit_instances:
            fallback_instance_ids: list[str] = []
            fallback_strategy_ids: list[str] = []
            fallback_instance_weights: Dict[str, float] = {}
            fallback_strategy_weights: Dict[str, float] = {}
            fallback_strategy_instance_map: Dict[str, list[str]] = {}
            fallback_instances: list[Dict[str, Any]] = []
            total_alloc = 0.0
            for inst in explicit_instances:
                instance_id = str(inst.get("id", "")).strip()
                strategy_id = canonical_strategy_id(inst.get("strategy_id", ""))
                if not instance_id or not strategy_id:
                    continue
                alloc = max(0.0, float(inst.get("allocation", 0.0) or 0.0))
                total_alloc += alloc
                fallback_instance_ids.append(instance_id)
                if strategy_id not in fallback_strategy_ids:
                    fallback_strategy_ids.append(strategy_id)
                fallback_instance_weights[instance_id] = alloc
                fallback_strategy_weights[strategy_id] = fallback_strategy_weights.get(strategy_id, 0.0) + alloc
                fallback_strategy_instance_map.setdefault(strategy_id, []).append(instance_id)
                fallback_instances.append(
                    dict(
                        instances_dict.get(instance_id, {}),
                        instance_id=instance_id,
                        strategy_id=strategy_id,
                        status="running",
                        allocation=alloc,
                    )
                )
            if fallback_instance_ids:
                if total_alloc > 0:
                    fallback_instance_weights = {
                        instance_id: round(weight / total_alloc, 4)
                        for instance_id, weight in fallback_instance_weights.items()
                    }
                    fallback_strategy_weights = {
                        strategy_id: round(
                            sum(
                                fallback_instance_weights.get(instance_id, 0.0)
                                for instance_id in instance_ids
                            ),
                            4,
                        )
                        for strategy_id, instance_ids in fallback_strategy_instance_map.items()
                    }
                else:
                    equal_weight = round(1.0 / len(fallback_instance_ids), 4)
                    fallback_instance_weights = {
                        instance_id: equal_weight for instance_id in fallback_instance_ids
                    }
                    fallback_strategy_weights = {
                        strategy_id: round(
                            sum(
                                fallback_instance_weights.get(instance_id, 0.0)
                                for instance_id in instance_ids
                            ),
                            4,
                        )
                        for strategy_id, instance_ids in fallback_strategy_instance_map.items()
                    }
                selected_instance_id = max(
                    fallback_instance_weights.items(),
                    key=lambda item: (float(item[1]), item[0]),
                )[0]
                selected_strategy_id = ""
                for strategy_id, instance_ids in fallback_strategy_instance_map.items():
                    if selected_instance_id in instance_ids:
                        selected_strategy_id = strategy_id
                        break
                scheduler = {
                    **dict(scheduler),
                    "mode": "single_active" if len(fallback_instance_ids) == 1 else "multi_active",
                    "arbiter": str(local_config.get("arbiter", scheduler.get("arbiter", "allocation-weighted")) or "allocation-weighted"),
                    "reason": "recovered_from_active_instances",
                    "active_instances": fallback_instance_ids,
                    "active_instance_ids": fallback_instance_ids,
                    "active_strategy_ids": fallback_strategy_ids,
                    "instance_weights": fallback_instance_weights,
                    "strategy_weights": fallback_strategy_weights,
                    "strategy_instance_map": fallback_strategy_instance_map,
                    "instances": fallback_instances,
                    "selected_instance_id": selected_instance_id,
                    "selected_strategy_id": selected_strategy_id,
                }
                return scheduler
        scheduler = {
            **dict(scheduler),
            "mode": "idle",
            "arbiter": str(scheduler.get("arbiter", "allocation-weighted") or "allocation-weighted"),
            "reason": "no_running_instances",
            "active_instances": [],
            "active_instance_ids": [],
            "active_strategy_ids": [],
            "instance_weights": {},
            "strategy_weights": {},
            "strategy_instance_map": {},
            "instances": [],
            "selected_instance_id": "",
            "selected_strategy_id": "",
            "health_alerts": ["NO_RUNNING_INSTANCES"],
        }
        return scheduler
    except ImportError:
        pass  # unified module not available 鈥?fall through to legacy

    # [DEPRECATED] Legacy fallback 鈥?fires only when unified_strategy_state is unavailable.
    try:
        from bridge.gold_trader import _daemon_log
        _daemon_log("[LEGACY_FALLBACK] _load_scheduler_snapshot: unified state unavailable, using legacy path", "WARN")
    except Exception:
        pass
    return _load_scheduler_snapshot_legacy(project_root, engine_request)


def _load_scheduler_snapshot_legacy(project_root: Path, engine_request: Dict[str, Any]) -> Dict[str, Any]:
    """Legacy scheduler fallback 鈥?used only when unified module is unavailable."""
    with _scheduler_snapshot_lock(project_root):
        effective_payload = _load_effective_payload(project_root)
        scheduler = (
            effective_payload.get("runtime_scheduler", {})
            if isinstance(effective_payload.get("runtime_scheduler", {}), dict)
            else {}
        )
        runtime_instances = _load_runtime_instances(project_root)
    instance_map = {
        str(item.get("instance_id", "")).strip(): item
        for item in runtime_instances
        if str(item.get("instance_id", "")).strip()
    }
    # Honour pause/resume: exclude paused instances from active_instance_ids
    active_running_ids = [
        iid for iid, item in instance_map.items()
        if str(item.get("status", "running")).lower() == "running"
    ]
    # Update scheduler active_instance_ids from the strategy_market_state
    # so the pipeline respects pause/resume without restart.
    if active_running_ids and isinstance(scheduler.get("active_instance_ids"), list):
        scheduler["active_instance_ids"] = active_running_ids
        scheduler["active_instances"] = active_running_ids
    # When server-provided runtime_scheduler is empty, read local active_instances.json
    scheduler_has_active = bool(
        scheduler.get("active_instance_ids")
        or scheduler.get("active_instances")
        or scheduler.get("active_strategy_ids")
    )
    if not scheduler_has_active:
        local_config = _read_json_dict(project_root / "config" / "active_instances.json")
        mode = "single_active"
        arbiter = "allocation-weighted"
        explicit_instances: list[dict[str, Any]] = []
        if local_config:
            mode = str(local_config.get("mode", "single_active") or "single_active")
            arbiter = str(local_config.get("arbiter", "allocation-weighted") or "allocation-weighted")
            raw_instances = local_config.get("instances", [])
            if isinstance(raw_instances, list):
                explicit_instances = [item for item in raw_instances if isinstance(item, dict)]
        if explicit_instances:
            scheduler = {
                "mode": mode,
                "arbiter": arbiter,
                "reason": "local_active_instances",
                "active_instance_ids": [],
                "active_strategy_ids": [],
                "instance_weights": {},
                "strategy_weights": {},
                "strategy_instance_map": {},
                "instances": [],  # populated below from instance_map
            }
            for inst in explicit_instances:
                if not inst.get("enabled", True):
                    continue
                iid = str(inst.get("id", "")).strip()
                sid = str(inst.get("strategy_id", "")).strip()
                if not iid or not sid:
                    continue
                scheduler["active_instance_ids"].append(iid)
                if sid not in scheduler["active_strategy_ids"]:
                    scheduler["active_strategy_ids"].append(sid)
                scheduler["instance_weights"][iid] = 1.0
                scheduler["strategy_weights"][sid] = scheduler["strategy_weights"].get(sid, 0.0) + 1.0
                scheduler["strategy_instance_map"].setdefault(sid, []).append(iid)
                if iid not in instance_map:
                    instance_map[iid] = {
                        "instance_id": iid,
                        "strategy_id": sid,
                        "strategy_name": sid,
                        "role": "live_primary",
                        "allocation": 1.0,
                        "symbol": str(_get_active_ea_symbol("XAUUSD")),
                        "param_overrides": inst.get("params", {}),
                    }
            scheduler["instances"] = list(instance_map.values())
        elif instance_map:
            scheduler = {
                "mode": mode,
                "arbiter": arbiter,
                "reason": "auto_discovered_runtime_instances",
                "active_instance_ids": [],
                "active_strategy_ids": [],
                "instance_weights": {},
                "strategy_weights": {},
                "strategy_instance_map": {},
            }
            for iid, item in instance_map.items():
                sid = str(item.get("strategy_id", "")).strip()
                if not sid:
                    continue
                scheduler["active_instance_ids"].append(iid)
                if sid not in scheduler["active_strategy_ids"]:
                    scheduler["active_strategy_ids"].append(sid)
                scheduler["instance_weights"][iid] = float(item.get("allocation", 1.0) or 1.0)
                scheduler["strategy_weights"][sid] = scheduler["strategy_weights"].get(sid, 0.0) + 1.0
                scheduler["strategy_instance_map"].setdefault(sid, []).append(iid)
    raw_active_instance_values = scheduler.get("active_instance_ids", scheduler.get("active_instances", []))
    active_instance_source_present = "active_instance_ids" in scheduler or "active_instances" in scheduler
    active_instance_ids = [
        str(item)
        for item in raw_active_instance_values
        if str(item).strip()
    ] if isinstance(raw_active_instance_values, list) else []
    active_strategy_ids = [
        str(item)
        for item in scheduler.get("active_strategy_ids", [])
        if str(item).strip()
    ] if isinstance(scheduler.get("active_strategy_ids", []), list) else []
    instance_weights = (
        scheduler.get("instance_weights", {})
        if isinstance(scheduler.get("instance_weights", {}), dict)
        else {}
    )
    if active_instance_ids and (
        not instance_weights
        or any(instance_id not in instance_weights for instance_id in active_instance_ids)
        or all(float(instance_weights.get(instance_id, 0.0) or 0.0) <= 0.0 for instance_id in active_instance_ids)
    ):
        total_alloc = 0.0
        rebuilt_strategy_weights: Dict[str, float] = {}
        rebuilt_strategy_instance_map: Dict[str, list[str]] = {}
        for instance_id in active_instance_ids:
            row = instance_map.get(instance_id, {})
            strategy_id = canonical_strategy_id(row.get("strategy_id", ""))
            alloc = max(0.0, float(row.get("allocation", row.get("weight", 0.0)) or 0.0))
            instance_weights[instance_id] = alloc
            total_alloc += alloc
            if strategy_id:
                rebuilt_strategy_weights[strategy_id] = rebuilt_strategy_weights.get(strategy_id, 0.0) + alloc
                rebuilt_strategy_instance_map.setdefault(strategy_id, []).append(instance_id)
        if total_alloc > 0:
            instance_weights = {
                instance_id: round(weight / total_alloc, 4)
                for instance_id, weight in instance_weights.items()
            }
            strategy_weights = {
                strategy_id: round(weight / total_alloc, 4)
                for strategy_id, weight in rebuilt_strategy_weights.items()
            }
        elif active_instance_ids:
            equal_weight = round(1.0 / len(active_instance_ids), 4)
            instance_weights = {instance_id: equal_weight for instance_id in active_instance_ids}
            strategy_weights = {
                strategy_id: round(
                    sum(instance_weights.get(instance_id, 0.0) for instance_id in instance_ids),
                    4,
                )
                for strategy_id, instance_ids in rebuilt_strategy_instance_map.items()
            }
        strategy_instance_map = rebuilt_strategy_instance_map
        scheduler["reason"] = "legacy_recovered_missing_instance_weights"
        scheduler["instance_weights"] = instance_weights
        scheduler["strategy_weights"] = strategy_weights
        scheduler["strategy_instance_map"] = strategy_instance_map
    strategy_weights = (
        scheduler.get("strategy_weights", {})
        if isinstance(scheduler.get("strategy_weights", {}), dict)
        else {}
    )
    strategy_instance_map = (
        scheduler.get("strategy_instance_map", {})
        if isinstance(scheduler.get("strategy_instance_map", {}), dict)
        else {}
    )
    config_revision = str(
        scheduler.get("config_revision")
        or effective_payload.get("config_revision")
        or effective_payload.get("revision")
        or "manual"
    )
    health_alerts: list[str] = []
    if active_instance_source_present and not active_instance_ids:
        health_alerts.append("ACTIVE_INSTANCE_IDS_EMPTY")
    instances: list[dict[str, Any]] = []
    for instance_id in active_instance_ids:
        row = instance_map.get(instance_id, {})
        if not row:
            continue
        instances.append(
            {
                "instance_id": instance_id,
                "strategy_id": str(row.get("strategy_id", "")).strip(),
                "strategy_name": str(row.get("strategy_name", row.get("strategy_id", "")) or ""),
                "role": str(row.get("role", "live_support") or "live_support"),
                "allocation": float(row.get("allocation", 0.0) or 0.0),
                "weight": float(instance_weights.get(instance_id, 0.0) or 0.0),
                "config_revision": str(row.get("config_revision", config_revision) or config_revision),
                "param_overrides": row.get("param_overrides", {}) if isinstance(row.get("param_overrides", {}), dict) else {},
            }
        )
    if active_instance_ids and not instances:
        health_alerts.append("ACTIVE_INSTANCE_IDS_UNRESOLVED")
    if not instances:
        try:
            from bridge.gold_trader import _daemon_log
            _daemon_log(
                "[LEGACY_CRITICAL] _load_scheduler_snapshot_legacy: no instances found 鈥?returning idle scheduler snapshot. "
                "Multi-strategy routing remains disabled until unified_strategy_state.json is restored.",
                "CRITICAL",
            )
        except Exception:
            pass
        scheduler = {
            "mode": "idle",
            "arbiter": "legacy-empty-scheduler",
            "reason": "legacy_empty_scheduler",
        }
        if active_instance_source_present:
            health_alerts.append("ACTIVE_INSTANCE_IDS_FALLBACK_TO_LEGACY")
        health_alerts.append("LEGACY_EMPTY_SCHEDULER")
    return {
        "mode": str(scheduler.get("mode", "single_active") or "single_active"),
        "arbiter": str(scheduler.get("arbiter", "allocation-weighted") or "allocation-weighted"),
        "reason": str(scheduler.get("reason", "runtime_scheduler") or "runtime_scheduler"),
        "config_revision": config_revision,
        "active_instance_ids": active_instance_ids,
        "active_strategy_ids": active_strategy_ids,
        "instance_weights": {str(key): float(value or 0.0) for key, value in instance_weights.items()},
        "strategy_weights": {str(key): float(value or 0.0) for key, value in strategy_weights.items()},
        "strategy_instance_map": {
            str(key): [str(item) for item in value if str(item).strip()]
            for key, value in strategy_instance_map.items()
            if isinstance(value, list)
        },
        "instances": instances,
        "health_alerts": health_alerts,
        "decision_policy": (
            scheduler.get("decision_policy", {})
            if isinstance(scheduler.get("decision_policy", {}), dict)
            else {}
        ),
    }


def _safe_float(value: Any, default: float = 0.0) -> float:
    try:
        return float(value)
    except Exception:
        return default


def _normalize_regime_label(value: Any) -> str:
    return normalize_regime_label(value)


def _coerce_bool(value: Any, default: bool = False) -> bool:
    if isinstance(value, bool):
        return value
    if isinstance(value, str):
        lowered = value.strip().lower()
        if lowered in {"1", "true", "yes", "on"}:
            return True
        if lowered in {"0", "false", "no", "off"}:
            return False
    return default


def _bounded_float(value: Any, default: float, *, low: float = 0.0, high: float = 2.0) -> float:
    try:
        return round(max(low, min(high, float(value))), 4)
    except Exception:
        return default


def _normalize_string_list(values: Any) -> list[str]:
    if not isinstance(values, list):
        return []
    normalized: list[str] = []
    for item in values:
        text = str(item or "").strip()
        if text and text not in normalized:
            normalized.append(text)
    return normalized


def _normalize_entry_style(value: Any, default: str = "MARKET") -> str:
    text = str(value or default).strip().upper().replace("-", "_").replace(" ", "_")
    style_aliases = {
        "MARKET": "MARKET",
        "LIMIT": "LIMIT",
        "PULLBACK_LIMIT": "PULLBACK_LIMIT",
        "CONFIRMATION_ONLY": "CONFIRMATION_ONLY",
    }
    return style_aliases.get(text, default)


def _normalize_entry_style_list(values: Any, fallback: list[str]) -> list[str]:
    items = values if isinstance(values, list) else fallback
    normalized: list[str] = []
    for item in items:
        style = _normalize_entry_style(item)
        if style not in normalized:
            normalized.append(style)
    return normalized or list(fallback)


def _default_execution_advice_matrix() -> dict[str, dict[str, Any]]:
    return {
        "ALLOW_FOLLOW": {
            "allowed_entry_styles": ["MARKET", "LIMIT"],
            "confirmation_required": False,
            "probe_size_scale": 0.6,
        },
        "LIMIT_ONLY": {
            "allowed_entry_styles": ["LIMIT", "PULLBACK_LIMIT"],
            "confirmation_required": False,
            "probe_size_scale": 0.45,
        },
        "WAIT_CONFIRMATION": {
            "allowed_entry_styles": ["LIMIT", "PULLBACK_LIMIT", "CONFIRMATION_ONLY"],
            "confirmation_required": True,
            "probe_size_scale": 0.35,
        },
        "BLOCK_COUNTER_TREND": {
            "allowed_entry_styles": ["MARKET", "LIMIT"],
            "confirmation_required": False,
            "probe_size_scale": 0.25,
        },
    }


def _normalize_execution_advice_matrix(raw: Any) -> dict[str, dict[str, Any]]:
    base = _default_execution_advice_matrix()
    payload = raw if isinstance(raw, dict) else {}
    normalized: dict[str, dict[str, Any]] = {}
    for advice, defaults in base.items():
        row = payload.get(advice, {}) if isinstance(payload.get(advice, {}), dict) else {}
        normalized[advice] = {
            "allowed_entry_styles": _normalize_entry_style_list(
                row.get("allowed_entry_styles", defaults["allowed_entry_styles"]),
                defaults["allowed_entry_styles"],
            ),
            "confirmation_required": _coerce_bool(
                row.get("confirmation_required", defaults["confirmation_required"]),
                defaults["confirmation_required"],
            ),
            "probe_size_scale": _bounded_float(
                row.get("probe_size_scale", defaults["probe_size_scale"]),
                defaults["probe_size_scale"],
                low=0.1,
                high=1.0,
            ),
        }
    return normalized


def _normalize_router_family(
    raw: Any,
    *,
    primary_strategy_id: str,
    support_strategy_ids: list[str],
    rank_weights: list[float],
) -> dict[str, Any]:
    payload = raw if isinstance(raw, dict) else {}
    primary = str(payload.get("primary_strategy_id", primary_strategy_id) or primary_strategy_id).strip()
    supports = _normalize_string_list(payload.get("support_strategy_ids", support_strategy_ids))
    candidates = _normalize_string_list([primary, *supports])
    if not candidates:
        candidates = _normalize_string_list([primary_strategy_id, *support_strategy_ids])
    if not candidates:
        return {
            "primary_strategy_id": "",
            "support_strategy_ids": [],
            "rank_weights": [],
        }
    raw_weights = payload.get("rank_weights", rank_weights)
    rank_values = raw_weights if isinstance(raw_weights, list) else rank_weights
    normalized_weights: list[float] = []
    for idx in range(len(candidates)):
        default_weight = max(0.0, float(rank_weights[idx] if idx < len(rank_weights) else 0.0))
        candidate_weight = rank_values[idx] if idx < len(rank_values) else default_weight
        normalized_weights.append(
            _bounded_float(candidate_weight, default_weight, low=0.0, high=1.0)
        )
    total = sum(normalized_weights)
    if total <= 0:
        normalized_weights = [max(0.0, float(item or 0.0)) for item in rank_weights[: len(candidates)]]
        total = sum(normalized_weights)
    if total <= 0:
        normalized_weights = [round(1.0 / max(1, len(candidates)), 4)] * len(candidates)
    else:
        normalized_weights = [round(weight / total, 4) for weight in normalized_weights]
    return {
        "primary_strategy_id": candidates[0],
        "support_strategy_ids": candidates[1:],
        "rank_weights": normalized_weights,
    }


def _resolve_rank_weights(rank_weights: list[float], count: int) -> list[float]:
    if count <= 0:
        return []
    weights = [max(0.0, float(item or 0.0)) for item in rank_weights[:count]]
    if len(weights) < count:
        weights.extend([0.0] * (count - len(weights)))
    total = sum(weights)
    if total <= 0:
        return [round(1.0 / max(1, count), 4)] * count
    return [round(weight / total, 6) for weight in weights]


def _merge_route_candidate_ids(configured_candidate_ids: list[str], running_ids: list[str]) -> list[str]:
    merged: list[str] = []
    for strategy_id in [*configured_candidate_ids, *running_ids]:
        canonical_id = canonical_strategy_id(strategy_id)
        if canonical_id and canonical_id not in merged:
            merged.append(canonical_id)
    return merged


def _rank_route_candidate_ids(
    candidate_ids: list[str],
    route_family: str,
    candidate_fit_map: dict[str, dict[str, Any]] | None = None,
    configured_candidates: list[str] | None = None,
) -> list[str]:
    fit_map = candidate_fit_map if isinstance(candidate_fit_map, dict) else {}
    configured_priority = {
        canonical_strategy_id(strategy_id): idx
        for idx, strategy_id in enumerate(configured_candidates or [])
        if canonical_strategy_id(strategy_id)
    }
    deduped_ids: list[str] = []
    for strategy_id in candidate_ids:
        canonical_id = canonical_strategy_id(strategy_id)
        if canonical_id and canonical_id not in deduped_ids:
            deduped_ids.append(canonical_id)
    return sorted(
        deduped_ids,
        key=lambda strategy_id: (
            -float((fit_map.get(strategy_id, {}) or {}).get("fit_score", 0.0) or 0.0),
            -float(strategy_route_family_alignment(strategy_id, route_family) or 0.0),
            (0 if str(route_family or "").strip().lower() == "uncertain" else -int(strategy_type_priority_for_route(strategy_id, route_family) or 0)),
            configured_priority.get(strategy_id, 10_000),
            strategy_id,
        ),
    )


def _route_prefers_candidate_pool(route_family: str) -> bool:
    return str(route_family or "").strip().lower() in {"range", "range_loosening", "uncertain"}


def _select_route_order_strategy(
    ordered_strategies: list[str],
    selection_rows: list[Dict[str, Any]],
) -> Dict[str, Any]:
    strategy_priority = {
        canonical_strategy_id(strategy_id): idx
        for idx, strategy_id in enumerate(ordered_strategies)
        if canonical_strategy_id(strategy_id)
    }
    eligible_rows = [
        row for row in selection_rows
        if isinstance(row, dict) and bool(row.get("eligible", False))
    ]
    ranked_rows = sorted(
        eligible_rows,
        key=lambda row: (
            strategy_priority.get(canonical_strategy_id(row.get("strategy_id", "")), 10_000),
            -float(row.get("final_score", 0.0) or 0.0),
            -float(row.get("health_score", 0.0) or 0.0),
            -float(row.get("signal_score", 0.0) or 0.0),
            str(row.get("instance_id", "") or ""),
        ),
    )
    winner = ranked_rows[0] if ranked_rows else {}
    runner_up = ranked_rows[1] if len(ranked_rows) > 1 else {}
    if not winner:
        return {
            "passed": False,
            "reason": "NO_ROUTE_ORDER_CANDIDATE",
            "winner_strategy_id": "",
            "winner_instance_id": "",
            "winner_score": 0.0,
            "runner_up_score": 0.0,
            "selection_margin": 0.0,
            "ranked_candidates": [],
            "selection_mode": "route_order",
        }
    winner_score = round(float(winner.get("final_score", 0.0) or 0.0), 4)
    runner_score = round(float(runner_up.get("final_score", 0.0) or 0.0), 4)
    return {
        "passed": True,
        "reason": "ROUTE_ORDER_SELECTED",
        "winner_strategy_id": str(winner.get("strategy_id", "") or ""),
        "winner_instance_id": str(winner.get("instance_id", "") or ""),
        "winner_score": winner_score,
        "runner_up_score": runner_score,
        "selection_margin": round(max(0.0, winner_score - runner_score), 4),
        "ranked_candidates": ranked_rows,
        "selection_mode": "route_order",
    }


def _call_signal_arbitrator(
    arbitration_payload: list[Dict[str, Any]],
    *,
    regime: str,
    route_family: str,
    garbage_allowed_types: list[str] | None = None,
    garbage_size_multiplier: float = 1.0,
) -> dict[str, Any]:
    from src.signal_arbitrator import arbitrate

    try:
        return arbitrate(
            arbitration_payload,
            regime=regime,
            route_family=route_family,
            garbage_allowed_types=garbage_allowed_types,
            garbage_size_multiplier=garbage_size_multiplier,
        )
    except TypeError:
        return arbitrate(arbitration_payload, regime=regime)


def _instance_route_meta(instance: Dict[str, Any]) -> Dict[str, Any]:
    template = (
        instance.get("template_snapshot", {})
        if isinstance(instance.get("template_snapshot", {}), dict)
        else {}
    )
    runtime_context = (
        template.get("runtime_context", {})
        if isinstance(template.get("runtime_context", {}), dict)
        else {}
    )
    coordination_contract = (
        template.get("coordination_contract", {})
        if isinstance(template.get("coordination_contract", {}), dict)
        else {}
    )

    def _normalized_list(value: Any) -> list[str]:
        if not isinstance(value, list):
            return []
        return [normalize_regime_label(item) for item in value if str(item).strip()]

    applicable_markets = _normalized_list(
        template.get("applicable_markets", runtime_context.get("applicable_markets", []))
    )
    conflict_groups = [
        str(item).strip().lower()
        for item in (
            template.get("conflict_groups", coordination_contract.get("conflict_groups", []))
            if isinstance(template.get("conflict_groups", coordination_contract.get("conflict_groups", [])), list)
            else []
        )
        if str(item).strip()
    ]
    return {
        "applicable_markets": applicable_markets,
        "conflict_groups": conflict_groups,
        "allow_same_group_stack": bool(
            template.get(
                "allow_same_group_stack",
                coordination_contract.get("allow_same_group_stack", False),
            )
        ),
    }


def _route_family_market_tags(route_family: str) -> set[str]:
    family = str(route_family or "").strip().lower()
    if family == "trend":
        return {"TREND", "TRENDING"}
    if family == "range":
        return {"RANGE", "RANGING", "LOW_VOLATILITY"}
    if family == "uncertain":
        return {"TRANSITION", "RANGE", "RANGING", "LOW_VOLATILITY", "TREND", "TRENDING"}
    return set()


def _strategy_id_matches_route_family(strategy_id: str, route_family: str) -> bool:
    return is_strategy_route_family_compatible(strategy_id, route_family)[0]


def _expand_uncertain_route_candidates(
    router: Dict[str, Any],
    base_candidates: list[str],
    *,
    running_ids: list[str] | None = None,
) -> list[str]:
    merged: list[str] = []
    running_set = {
        canonical_strategy_id(strategy_id)
        for strategy_id in (running_ids or [])
        if canonical_strategy_id(strategy_id)
    }
    for strategy_id in base_candidates:
        canonical_id = canonical_strategy_id(strategy_id)
        if running_set and canonical_id not in running_set:
            continue
        if canonical_id and canonical_id not in merged:
            merged.append(canonical_id)

    for family, limit in (("trend", 2), ("range", 2)):
        family_cfg = router.get(family, {}) if isinstance(router.get(family, {}), dict) else {}
        family_primary = canonical_strategy_id(family_cfg.get("primary_strategy_id", ""))
        family_supports = [
            canonical_strategy_id(item)
            for item in (
                family_cfg.get("support_strategy_ids", [])
                if isinstance(family_cfg.get("support_strategy_ids", []), list)
                else []
            )[:limit]
        ]
        for strategy_id in [family_primary, *family_supports]:
            if running_set and strategy_id not in running_set:
                continue
            if strategy_id and strategy_id not in merged:
                merged.append(strategy_id)
    return merged


def _resolve_running_strategy_fallback(
    running_ids: list[str],
    route_family: str,
    regime: str,
    regime_confidence: float,
    session_mode: str,
) -> tuple[list[str], list[str], str]:
    normalized_route_family = str(route_family or "").strip().lower()
    available_ids = [
        canonical_strategy_id(item)
        for item in running_ids
        if str(item or "").strip()
    ]
    if not available_ids:
        return [], [], ""

    compatible_running = [
        strategy_id
        for strategy_id in available_ids
        if is_strategy_regime_compatible(
            strategy_id,
            regime,
            regime_confidence=regime_confidence,
            session_mode=session_mode,
            route_family=normalized_route_family,
        )[0]
    ]
    family_order: list[str]
    if str(regime or "").upper() == "TRANSITION" or normalized_route_family == "uncertain":
        family_order = ["uncertain", "range", "trend"]
    elif normalized_route_family == "trend":
        family_order = ["trend"]
    elif normalized_route_family in {"range", "range_loosening"}:
        family_order = ["range"]
    else:
        family_order = [normalized_route_family]

    for family in family_order:
        family_compatible = [
            strategy_id for strategy_id in compatible_running
            if _strategy_id_matches_route_family(strategy_id, family)
        ]
        if family_compatible:
            return family_compatible, family_compatible, family_compatible[0]

    for family in family_order:
        family_available = [
            strategy_id for strategy_id in available_ids
            if _strategy_id_matches_route_family(strategy_id, family)
        ]
        if family_available:
            compatible_family_available = [
                strategy_id for strategy_id in compatible_running if strategy_id in family_available
            ]
            return family_available, compatible_family_available, family_available[0]

    if normalized_route_family in {"range", "range_loosening", "trend", "uncertain"} or str(regime or "").upper() == "TRANSITION":
        return [], [], ""
    primary = compatible_running[0] if compatible_running else available_ids[0]
    return list(available_ids), compatible_running, primary


def _strategy_matches_route_family(rows: list[Dict[str, Any]], route_family: str) -> bool:
    expected_tags = _route_family_market_tags(route_family)
    if not expected_tags:
        return True
    for row in rows:
        if not isinstance(row, dict):
            continue
        meta = _instance_route_meta(row)
        markets = set(meta.get("applicable_markets", []))
        if not markets or markets & expected_tags:
            return True
    return False


def _dedupe_route_candidates_by_conflict_signature(
    strategy_ids: list[str],
    available_by_strategy: Dict[str, list[Dict[str, Any]]],
) -> tuple[list[str], dict[str, str]]:
    filtered: list[str] = []
    suppressed: dict[str, str] = {}
    seen_signatures: dict[tuple[str, ...], str] = {}
    for strategy_id in strategy_ids:
        rows = available_by_strategy.get(strategy_id, [])
        row = rows[0] if rows else {}
        meta = _instance_route_meta(row if isinstance(row, dict) else {})
        signature = tuple(sorted(meta.get("conflict_groups", [])))
        if signature and not bool(meta.get("allow_same_group_stack", False)):
            owner = seen_signatures.get(signature)
            if owner:
                suppressed[strategy_id] = owner
                continue
            seen_signatures[signature] = strategy_id
        filtered.append(strategy_id)
    return filtered, suppressed


def _load_recent_closures(project_root: Path) -> list[Dict[str, Any]]:
    # runtime_open_guard.json 鏄?pipeline 鑷繁缁存姢鐨勬潈濞佸钩浠撹褰曟簮銆?
    # gold_trader.py 閫€褰瑰悗 agent_state.json 鍙兘杩囨湡锛屼紭鍏堣 runtime銆?
    primary = project_root / "data" / "runtime_open_guard.json"
    fallback = project_root / "data" / "bridge" / "state" / "agent_state.json"
    rows = []
    for source in (primary, fallback):
        try:
            payload = json.loads(source.read_text(encoding="utf-8"))
        except Exception:
            continue
        candidates = payload.get("recent_closures", []) if isinstance(payload, dict) else []
        if isinstance(candidates, list) and candidates:
            rows = candidates
            break
    return _sanitize_recent_closures(rows)


def _is_realized_runtime_closure(row: Dict[str, Any]) -> bool:
    if not isinstance(row, dict):
        return False
    close_action = str(row.get("close_action", "") or "").upper()
    source = str(row.get("source", "") or "").lower()
    if source == "pipeline_close_detection":
        return False
    if close_action.startswith("PIPELINE_MT5_"):
        return False
    try:
        float(row.get("pnl", 0.0) or 0.0)
    except Exception:
        return False
    side = str(row.get("side", "") or "").upper()
    if side not in {"BUY", "SELL"}:
        return False
    strategy_id = str(row.get("strategy_id", "") or "").strip()
    instance_id = str(row.get("instance_id", "") or "").strip()
    if not strategy_id and not instance_id:
        return False
    symbol = str(row.get("symbol", "") or "").strip().upper()
    if symbol and symbol != "XAUUSD":
        return False
    return True


def _sanitize_recent_closures(rows: Any) -> list[Dict[str, Any]]:
    if not isinstance(rows, list):
        return []
    return [row for row in rows if _is_realized_runtime_closure(row)]


def _runtime_open_guard_path(project_root: Path) -> Path:
    return project_root / "data" / "runtime_open_guard.json"


def _load_runtime_open_guard_state(project_root: Path) -> Dict[str, Any]:
    path = _runtime_open_guard_path(project_root)
    if not path.exists():
        return {}
    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}
    if not isinstance(payload, dict):
        return {}
    if "recent_closures" in payload:
        payload["recent_closures"] = _sanitize_recent_closures(payload.get("recent_closures"))
    for legacy_key in (
        "_shrink_applied",
        "_shrink_cap",
        "_shrink_reasons",
        "_shrink_telemetry",
        "_shrink_params_adapted",
    ):
        payload.pop(legacy_key, None)
    return payload


def _runtime_dispatch_state(runtime_guard: Dict[str, Any]) -> Dict[str, Any]:
    state = runtime_guard.get("dispatch_state", {}) if isinstance(runtime_guard.get("dispatch_state", {}), dict) else {}
    dedup = state.get("dedup", {}) if isinstance(state.get("dedup", {}), dict) else {}
    pending_sides = state.get("pending_sides", {}) if isinstance(state.get("pending_sides", {}), dict) else {}
    return {
        "last_open_time": float(state.get("last_open_time", 0.0) or 0.0),
        "last_open_time_by_strategy": state.get("last_open_time_by_strategy", {}) if isinstance(state.get("last_open_time_by_strategy", {}), dict) else {},
        "dedup": {str(k): float(v or 0.0) for k, v in dedup.items()},
        "pending_sides": {str(k): float(v or 0.0) for k, v in pending_sides.items()},
    }


def _save_runtime_open_guard_state(project_root: Path, runtime_state: Dict[str, Any]) -> Dict[str, Any]:
    for legacy_key in (
        "_shrink_applied",
        "_shrink_cap",
        "_shrink_reasons",
        "_shrink_telemetry",
        "_shrink_params_adapted",
    ):
        runtime_state.pop(legacy_key, None)
    runtime_state["updated_at"] = datetime.now(timezone.utc).isoformat()
    _write_json(_runtime_open_guard_path(project_root), runtime_state)
    return runtime_state


def _load_agent_state_snapshot(project_root: Path) -> Dict[str, Any]:
    path = project_root / "data" / "bridge" / "state" / "agent_state.json"
    if not path.exists():
        return {}
    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}
    return payload if isinstance(payload, dict) else {}


def _extract_snapshot_position(mt5_snapshot: Dict[str, Any], engine_request: Dict[str, Any]) -> Dict[str, Any]:
    account = engine_request.get("account_metrics", {}) if isinstance(engine_request.get("account_metrics", {}), dict) else {}
    positions_all = mt5_snapshot.get("positions_all", []) if isinstance(mt5_snapshot.get("positions_all", []), list) else []
    positions_count = int(mt5_snapshot.get("positions_count", 0) or 0)
    if positions_all:
        first = positions_all[0]
        raw_side = first.get("side", first.get("type", ""))
        position_side = str(raw_side or "").upper()
        if position_side not in {"BUY", "SELL"}:
            raw_side_text = str(raw_side or "").strip()
            if raw_side_text == "0":
                position_side = "BUY"
            elif raw_side_text == "1":
                position_side = "SELL"
        return {
            "position_open": True,
            "position_ticket": int(first.get("ticket", 0) or 0),
            "position_side": position_side,
        }
    return {
        "position_open": False,
        "position_ticket": 0,
        "position_side": "",
    }


def _refresh_runtime_open_guard_state(
    project_root: Path,
    *,
    mt5_snapshot: Dict[str, Any],
    engine_request: Dict[str, Any],
) -> Dict[str, Any]:
    runtime_state = _load_runtime_open_guard_state(project_root)
    agent_state = _load_agent_state_snapshot(project_root)
    if not runtime_state:
        runtime_state = {}

    # 浠?agent_state 鍚堝苟鏁版嵁锛堜粎琛ュ厖锛屼笉瑕嗙洊宸叉湁杩愯鏃舵暟鎹級
    # gold_trader.py 閫€褰瑰悗 agent_state 鍙兘杩囨湡锛宺untime_open_guard 鎵嶆槸鏉冨▉銆?
    for key in ("incident_controls", "consecutive_losses"):
        value = agent_state.get(key)
        if value not in (None, "", [], {}):
            existing = runtime_state.get(key)
            if existing in (None, "", [], {}):
                runtime_state[key] = value

    # recent_closures 鐢?pipeline 鑷繁缁存姢锛屼笉浠?agent_state 澶嶅埗銆?
    # 鍙湪 runtime 瀹屽叏娌℃湁 recent_closures 鏃舵墠浠?agent_state 鍋氫竴娆℃€у垵濮嬪寲銆?
    for key in ("recent_closures",):
        value = agent_state.get(key)
        sanitized = _sanitize_recent_closures(value)
        if sanitized:
            existing = runtime_state.get(key)
            if not isinstance(existing, list) or not existing:
                runtime_state[key] = list(sanitized)

    # last_close_time: 鍙湪 runtime 涓?0 涓?agent 鏈夋洿鏂伴矞鐨勫€兼椂鎵嶅悓姝?
    for key in ("last_close_time", "last_closed_ts"):
        value = agent_state.get(key)
        rt_val = float(runtime_state.get(key, 0.0) or 0.0)
        if float(value or 0.0) > rt_val:
            runtime_state[key] = float(value or 0.0)

    # last_closed_side / last_close_action: 鍙湪 runtime 鏃犳暟鎹椂浠?agent 鍒濆鍖?
    for key in ("last_closed_side", "last_close_action"):
        value = agent_state.get(key)
        if value not in (None, "",):
            existing = runtime_state.get(key)
            if existing in (None, "",):
                runtime_state[key] = value

    # _position_strategy_map: 鍙湪 runtime 鏃犳瀛楁鏃朵粠 agent 鍋氫竴娆℃€у垵濮嬪寲
    for key in ("_position_strategy_map",):
        value = agent_state.get(key)
        if isinstance(value, dict) and value:
            existing = runtime_state.get(key)
            if not isinstance(existing, dict) or not existing:
                runtime_state[key] = dict(value)

    previous_open = bool(runtime_state.get("position_open", False))
    previous_ticket = int(runtime_state.get("position_ticket", 0) or 0)
    previous_side = str(runtime_state.get("position_side", "") or "").upper()
    current_position = _extract_snapshot_position(mt5_snapshot, engine_request)
    current_open = bool(current_position.get("position_open", False))

    # 鈹€鈹€ Safety net: if no positions are open, clear stale position maps 鈹€鈹€
    # BUG FIX: 鍙竻闄?_position_strategy_map锛坱icket鈫抯trategy 鏄犲皠锛宲os 鍏充簡灏辫繃鏈燂級銆?
    # _strategy_close_times 鍖呭惈骞充粨鍐峰嵈鏃堕棿锛屽繀椤诲湪鎵€鏈変粨浣嶅钩鎺夊悗缁х画淇濈暀锛?
    # 鍚﹀垯鍚岀瓥鐣?cooldown 琚竻闆讹紝瀵艰嚧绔嬪埢閲嶆柊寮€浠撱€?
    if not current_open and not previous_open:
        pos_all = runtime_state.get("positions_all", [])
        if not pos_all or len(pos_all) == 0:
            stale_map = runtime_state.get("_position_strategy_map", {})
            if isinstance(stale_map, dict) and stale_map:
                runtime_state["_position_strategy_map"] = {}

    # 鈹€鈹€ Rebuild _position_strategy_map from positions_all 鈹€鈹€
    # FILLED handler may miss entries (e.g., EA bridge report lacks ticket field).
    # This ensures the map always reflects current positions with known strategy_id.
    merged_positions = runtime_state.get("positions_all", [])
    if isinstance(merged_positions, list) and merged_positions:
        pos_map = runtime_state.get("_position_strategy_map", {})
        if not isinstance(pos_map, dict):
            pos_map = {}
        for mp in merged_positions:
            if not isinstance(mp, dict):
                continue
            ticket = int(mp.get("ticket", 0) or 0)
            sid = str(mp.get("strategy_id", "") or "").strip()
            if ticket > 0 and sid:
                pos_map[str(ticket)] = sid
        runtime_state["_position_strategy_map"] = pos_map
    current_ticket = int(current_position.get("position_ticket", 0) or 0)
    current_side = str(current_position.get("position_side", "") or "").upper()
    now_ts = time.time()

    if previous_open and previous_ticket > 0:
        close_detected = (not current_open) or (current_open and current_ticket > 0 and current_ticket != previous_ticket)
        if close_detected:
            runtime_state["last_close_time"] = now_ts
            runtime_state["last_closed_ts"] = now_ts
            runtime_state["last_closed_side"] = previous_side
            runtime_state["last_closed_ticket"] = previous_ticket
            # 鈹€鈹€ 娓呯悊鍚岀瓥鐣ユ寔浠撴槧灏? 宸插钩浠?ticket 浠?_position_strategy_map 涓Щ闄?鈹€鈹€
            closed_strategy_id = ""
            if previous_ticket > 0:
                pos_map = runtime_state.get("_position_strategy_map", {})
                if isinstance(pos_map, dict) and str(previous_ticket) in pos_map:
                    closed_strategy_id = str(pos_map.pop(str(previous_ticket), "") or "")
                    runtime_state["_position_strategy_map"] = pos_map
                # 鍏滃簳锛氫粠 state 鐨勬爣閲忓瓧娈佃鍙栵紙鍗曠瓥鐣ュ吋瀹癸級
                if not closed_strategy_id:
                    closed_strategy_id = str(
                        runtime_state.get("position_strategy_id", "")
                        or runtime_state.get("last_closed_strategy", "")
                        or ""
                    ).strip()
            close_action = (
                "PIPELINE_MT5_TICKET_ROLLOVER" if current_open and current_ticket != previous_ticket
                else "PIPELINE_MT5_FLAT_DETECTED"
            )
            runtime_state["last_close_action"] = close_action
            # 涓嶅啀鎶?MT5 骞充粨妫€娴嬩簨浠跺啓鍏?recent_closures銆?
            # 杩欎簺璁板綍娌℃湁鐪熷疄鎴愪氦鐩堜簭锛屼細鎶婂姩鎬佺缉浠撻暱鏈熷帇鍋忓埌鏋佸皬鎵嬫暟銆?

    runtime_state["position_open"] = current_open
    runtime_state["position_ticket"] = current_ticket
    runtime_state["position_side"] = current_side
    # Merge MT5 positions with existing pipeline-added fields (strategy_id, etc.)
    # instead of blindly overwriting. MT5 provides authoritative ticket/side/volume/pnl,
    # pipeline provides strategy attribution.
    mt5_positions = mt5_snapshot.get("positions_all", []) if isinstance(mt5_snapshot.get("positions_all", []), list) else []
    existing_positions = runtime_state.get("positions_all", [])
    if not isinstance(existing_positions, list):
        existing_positions = []
    # Build index of existing positions by ticket to preserve pipeline-added fields
    existing_by_ticket: Dict[int, Dict[str, Any]] = {}
    for ep in existing_positions:
        if isinstance(ep, dict) and ep.get("ticket"):
            existing_by_ticket[int(ep["ticket"])] = ep
    merged_positions: list[Dict[str, Any]] = []
    for mp in mt5_positions:
        if not isinstance(mp, dict):
            continue
        ticket = int(mp.get("ticket", 0) or 0)
        if ticket > 0 and ticket in existing_by_ticket:
            # Merge: MT5 data is authoritative for market fields; pipeline fields survive
            merged = dict(mp)
            ep = existing_by_ticket[ticket]
            for key in ("strategy_id", "instance_id", "opened_at"):
                incoming_value = merged.get(key)
                if key in ep and (
                    key not in merged
                    or incoming_value is None
                    or (isinstance(incoming_value, str) and not incoming_value.strip())
                ):
                    merged[key] = ep[key]
            merged_positions.append(merged)
        else:
            entry = dict(mp)
            ticket = int(entry.get("ticket", 0) or 0)
            if ticket > 0 and "strategy_id" not in entry:
                pos_map = runtime_state.get("_position_strategy_map", {})
                if isinstance(pos_map, dict) and str(ticket) in pos_map:
                    entry["strategy_id"] = pos_map[str(ticket)]
            merged_positions.append(entry)
    runtime_state["positions_all"] = merged_positions
    runtime_state["positions_count"] = int(mt5_snapshot.get("positions_count", len(merged_positions)) or 0)
    runtime_state["regime"] = str(engine_request.get("ai_trade_plan", {}).get("regime", runtime_state.get("regime", "")) or runtime_state.get("regime", ""))
    return _save_runtime_open_guard_state(project_root, runtime_state)


def _loss_streak(rows: list[Dict[str, Any]]) -> int:
    streak = 0
    for row in reversed(rows):
        try:
            pnl_value = float(row.get("pnl", 0.0) or 0.0)
        except Exception:
            continue
        if pnl_value < 0:
            streak += 1
        else:
            break
    return streak


def _hit_rate(rows: list[Dict[str, Any]]) -> float:
    if not rows:
        return 0.0
    hits = 0
    for row in rows:
        try:
            if float(row.get("pnl", 0.0) or 0.0) > 0:
                hits += 1
        except Exception:
            continue
    return hits / max(1, len(rows))


def _inject_account_risk_metrics(engine_request: Dict[str, Any], project_root: Path) -> None:
    """鐢?agent_state.json 閲岀殑鐪熷疄鏁版嵁瑕嗙洊 account_metrics 涓殑纭紪鐮?0銆?

    ea_bridge_live / mt5_live 鐨?snapshot 灏?consecutive_losses 鍜?
    daily_realized_loss_pct 纭紪鐮佷负 0锛屽鑷?loss_streak_cooldown 鍜?
    daily_loss_circuit_breaker 鐔旀柇瑙勫垯姘歌繙涓嶈Е鍙戙€傛鍑芥暟浠?state 鏂囦欢
    璇诲彇瀹為檯骞充粨璁板綍锛岃绠楃湡瀹炲€煎苟娉ㄥ叆 engine_request銆?
    """
    rows = _load_recent_closures(project_root)
    account = engine_request.get("account_metrics", {})
    if not isinstance(account, dict):
        account = {}
        engine_request["account_metrics"] = account

    # 鈹€鈹€ consecutive_losses锛氫粠鏈€杩戝钩浠撳€掑簭鎵弿杩炰簭 鈹€鈹€
    streak = _loss_streak(rows)
    account["consecutive_losses"] = int(streak)

    # 鈹€鈹€ daily_realized_loss_pct锛氫粖鏃ョ疮璁″凡瀹炵幇浜忔崯鍗犳潈鐩婄櫨鍒嗘瘮 鈹€鈹€
    if rows:
        try:
            today_start = int(datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0).timestamp())
            today_pnl = sum(
                float(row.get("pnl", 0.0) or 0.0)
                for row in rows
                if isinstance(row, dict) and int(row.get("ts", 0) or 0) >= today_start
            )
            equity = float(account.get("equity", 0) or account.get("balance", 0) or 0.0)
            if today_pnl < 0 and equity > 1.0:
                account["daily_realized_loss_pct"] = round(abs(today_pnl) / equity * 100.0, 4)
            else:
                account["daily_realized_loss_pct"] = 0.0
        except Exception:
            account.setdefault("daily_realized_loss_pct", 0.0)
    else:
        account.setdefault("daily_realized_loss_pct", 0.0)


def _sync_agent_state_from_mt5(ctx: PipelineRunContext) -> None:
    """姣忚疆灏?MT5 鐪熷疄鎸佷粨鍚屾鍒?agent_state.json锛屾秷闄ゅ菇鐏靛崟銆?

    gold_trader.py 宸蹭笉鍦ㄤ富寰幆涓繍琛岋紙launcher 寮哄埗鍒囧埌 pipeline 鍗曞叆鍙ｏ級锛?
    浣?GUI 浠嶄粠 agent_state.json 璇诲彇鎸佷粨灞曠ず銆俻ipeline 姣忚疆 call 姝ゅ嚱鏁帮紝
    纭繚 agent_state.json 鐨?position_open / positions_all 涓?MT5 淇濇寔鍚屾銆?
    """
    if ctx.source not in {"mt5", "ea_feed"}:
        return
    snap = getattr(ctx, "mt5_snapshot", None)
    if not isinstance(snap, dict):
        return
    state_path = ctx.project_root / "data" / "bridge" / "state" / "agent_state.json"

    positions_all = snap.get("positions_all", [])
    position_open = bool(positions_all)

    account = snap.get("account", {}) if isinstance(snap.get("account", {}), dict) else {}
    equity = float(account.get("equity", 0.0) or 0.0)
    balance = float(account.get("balance", 0.0) or 0.0)

    # BUG FIX: 鍐欏叆鍓嶉噸鏂板姞杞芥枃浠讹紝鍙洿鏂?position 閿紝淇濈暀 close-tracking / cooldown 鏁版嵁銆?
    # 鍘熸湁閫昏緫鍦?loaded base 涓婄洿鎺ヤ慨鏀瑰悗鍐欏洖锛屼絾 loaded base 鍙兘缂哄皯鍏朵粬杩涚▼鍐欑殑鏂?key銆?
    try:
        state_path.parent.mkdir(parents=True, exist_ok=True)
        # Re-read to minimize RMW race window
        try:
            fresh = json.loads(state_path.read_text(encoding="utf-8")) if state_path.exists() else {}
        except Exception:
            fresh = {}
        if not isinstance(fresh, dict):
            fresh = {}
        # Only update position-related keys in fresh; never touch close-tracking keys
        fresh["position_open"] = position_open
        fresh["positions_all"] = positions_all
        if balance > 0 and equity > 0:
            fresh["balance"] = balance
            fresh["equity"] = equity
        elif equity > 0:
            fresh["balance"] = equity
            fresh["equity"] = equity
        if position_open and positions_all:
            p = positions_all[0] if isinstance(positions_all[0], dict) else {}
            fresh["position_ticket"] = int(p.get("ticket", 0) or 0)
            fresh["position_side"] = str(p.get("side", "BUY") or "BUY").upper()
            fresh["position_entry"] = float(p.get("entry", 0.0) or 0.0)
            fresh["position_price"] = float(p.get("entry", 0.0) or 0.0)
            fresh["position_sl"] = float(p.get("sl", 0.0) or 0.0)
            fresh["position_tp"] = float(p.get("tp", 0.0) or 0.0)
            fresh["position_vol"] = float(p.get("volume", 0.0) or 0.0)
        else:
            fresh["position_ticket"] = 0
            fresh["position_side"] = None
            fresh["position_entry"] = 0.0
            fresh["position_price"] = 0.0
            fresh["position_sl"] = 0.0
            fresh["position_tp"] = 0.0
            fresh["position_vol"] = 0.0
        # Drop corrupted agent_direction (null values)
        ad = fresh.get("agent_direction")
        if isinstance(ad, dict) and ad.get("direction") is None:
            fresh.pop("agent_direction", None)
        tmp = state_path.with_suffix(state_path.suffix + ".tmp")
        tmp.write_text(json.dumps(fresh, ensure_ascii=False, default=str), encoding="utf-8")
        os.replace(tmp, state_path)
    except Exception:
        pass


def _check_floating_loss_guard(project_root: Path, macro_direction: str) -> str:
    """Floating P&L leading indicator 鈥?detect distress BEFORE position closes.

    Returns reason string if soft_penalty should trigger, empty string otherwise.
    This prevents waiting for 3 closed losses when the current position is already
    deep underwater (>1.5 ATR).
    """
    try:
        from bridge.gold_trader import load_state
        state = load_state()
    except Exception:
        return ""
    if not state.get("position_open"):
        return ""
    pos_side = str(state.get("position_side", "") or "").upper()
    pos_entry = float(state.get("position_entry", state.get("position_price", 0)) or 0)
    if pos_side not in {"BUY", "SELL"} or pos_entry <= 0:
        return ""
    # Check if current position aligns with the macro direction being guarded
    if macro_direction == "LONG" and pos_side != "BUY":
        return ""
    if macro_direction == "SHORT" and pos_side != "SELL":
        return ""
    # Get current price from snapshot
    try:
        import json
        from pathlib import Path

        # * 璺ㄥ钩鍙板揩鐓ц矾寰勮В鏋?鈥?浼樺厛 config_manager锛宔nv 娆′箣锛岄」鐩洰褰曞厹搴?
        snap = {}
        _candidates = [
            project_root / "data" / "bridge" / "market_snapshot.json",
            project_root / "bridge" / "market_snapshot.json",
        ]
        # 浠?config_manager 鎴?env 鑾峰彇 MT5 鍩虹鐩綍锛堣法骞冲彴锛?
        _env_mt5 = os.environ.get("EA_BRIDGE_FILES_DIR", "").strip()
        if _env_mt5:
            _mt5_base = Path(_env_mt5)
        else:
            try:
                from config_manager import get_mt5_dir as _get_mt5_dir
                _mt5_base = Path(_get_mt5_dir())
            except Exception:
                _mt5_base = None
        if _mt5_base and _mt5_base.exists():
            _candidates.append(_mt5_base / "gold_autotrade" / "bridge" / "market_snapshot.json")
            _candidates.append(_mt5_base / "market_snapshot.json")
        # 浠呭湪 macOS 涓婅拷鍔?Wine 鍏滃簳璺緞
        if sys.platform == "darwin":
            _wine_snap = (
                Path.home() / "Library/Application Support/net.metaquotes.wine.metatrader5"
                / "drive_c/users/user/AppData/Roaming/MetaQuotes/Terminal/Common/Files/market_snapshot.json"
            )
            _candidates.append(_wine_snap)
        for _sp in _candidates:
            if _sp.exists():
                try:
                    snap = json.loads(_sp.read_text(encoding="utf-8"))
                    break
                except Exception:
                    continue
        if not snap:
            return ""
        instruments = snap.get("instruments", {})
        preferred_symbol = normalize_symbol(
            snap.get("symbol")
            or (snap.get("tick", {}) if isinstance(snap.get("tick", {}), dict) else {}).get("symbol")
            or "XAUUSD"
        )
        gold = {}
        if isinstance(instruments, dict):
            _, gold_payload = pick_best_instrument(instruments, preferred_symbol)
            gold = gold_payload if isinstance(gold_payload, dict) else {}
        bid = float(gold.get("bid", 0)) if isinstance(gold, dict) else 0
        ask = float(gold.get("ask", 0)) if isinstance(gold, dict) else 0
        if bid <= 0 or ask <= 0:
            bid = float(snap.get("tick", {}).get("bid", 0))
            ask = float(snap.get("tick", {}).get("ask", 0))
        current_price = (bid + ask) / 2.0
    except Exception:
        return ""
    if current_price <= 0:
        return ""
    # Compute floating P&L in points
    if pos_side == "BUY":
        pnl_pts = current_price - pos_entry
    else:
        pnl_pts = pos_entry - current_price
    # Estimate ATR from bars
    try:
        bars = []
        if isinstance(gold, dict):
            bars = gold.get("bars", [])
        if not bars and isinstance(snap, dict):
            bars = snap.get("bars", [])
        if not bars and isinstance(snap, dict):
            instr = snap.get("instruments", {})
            if isinstance(instr, dict):
                _, matched = pick_best_instrument(instr, preferred_symbol)
                if isinstance(matched, dict):
                    bars = matched.get("bars", [])
        atr = _estimate_atr(bars) if bars else 0.0
        if atr <= 0:
            atr = current_price * 0.001
    except Exception:
        atr = current_price * 0.001
    if atr <= 0:
        return ""
    floating_loss_atr = abs(pnl_pts) / atr if pnl_pts < 0 else 0.0
    if floating_loss_atr >= 1.5:
        return f"floating_loss:{round(floating_loss_atr,1)}xATR_macro_{macro_direction}"
    return ""


def _estimate_atr(bars: list) -> float:
    """Quick ATR estimate from OHLC bars."""
    if len(bars) < 3:
        return 0.0
    trs = []
    for i in range(1, min(len(bars), 20)):
        b, prev = bars[i], bars[i - 1]
        hl = abs(b["high"] - b["low"])
        hc = abs(b["high"] - prev["close"])
        lc = abs(b["low"] - prev["close"])
        trs.append(max(hl, hc, lc))
    return sum(trs) / len(trs) if trs else 0.0


def _compute_direction_loss_guard(
    project_root: Path,
    macro_state: Dict[str, Any],
    policy: Dict[str, Any],
) -> Dict[str, Any]:
    macro_policy = policy.get("macro", {}) if isinstance(policy.get("macro", {}), dict) else {}
    guard_policy = (
        macro_policy.get("direction_loss_guard", {})
        if isinstance(macro_policy.get("direction_loss_guard", {}), dict)
        else {}
    )
    result = {
        "enabled": bool(guard_policy.get("enabled", True)),
        "state": "inactive",
        "reason": "",
        "scale": 1.0,
        "sample_count": 0,
        "aligned_count": 0,
        "counter_count": 0,
        "aligned_hit_rate": 0.0,
        "counter_hit_rate": 0.0,
        "aligned_loss_streak": 0,
        "effective_direction": str(macro_state.get("effective_direction", "NEUTRAL") or "NEUTRAL"),
        "disempowered": False,
    }
    macro_direction = str(macro_state.get("direction", "NEUTRAL") or "NEUTRAL").upper()
    if not result["enabled"] or macro_direction not in {"LONG", "SHORT"}:
        return result

    rows = _load_recent_closures(project_root)
    lookback = int(float(guard_policy.get("lookback", 8) or 8))
    min_samples = int(float(guard_policy.get("min_samples", 3) or 3))
    relevant_rows = [
        row
        for row in rows
        if str(row.get("agent_direction", "") or "").upper() == macro_direction
    ][-max(1, lookback) :]
    if not relevant_rows:
        result["state"] = "no_samples"
        return result

    aligned_rows = [row for row in relevant_rows if bool(row.get("agent_aligned", False))]
    counter_rows = [row for row in relevant_rows if not bool(row.get("agent_aligned", False))]
    aligned_hit_rate = _hit_rate(aligned_rows)
    counter_hit_rate = _hit_rate(counter_rows)
    aligned_loss_streak = _loss_streak(aligned_rows)
    counter_delta = float(guard_policy.get("counter_win_bias_delta", 0.15) or 0.15)
    counter_edge = (
        len(counter_rows) >= min_samples
        and counter_hit_rate >= min(1.0, aligned_hit_rate + counter_delta)
    )
    soft_threshold = int(float(guard_policy.get("aligned_loss_streak_soft", 2) or 2))
    hard_threshold = int(float(guard_policy.get("aligned_loss_streak_hard", 3) or 3))
    soft_scale = _bounded_float(guard_policy.get("soft_scale", 0.55), 0.55, low=0.0, high=1.0)
    hard_scale = _bounded_float(guard_policy.get("hard_scale", 0.2), 0.2, low=0.0, high=1.0)
    neutralize_on_hard = _coerce_bool(guard_policy.get("neutralize_on_hard", False), False)
    # 鈹€鈹€ Floating P&L leading indicator: detect open-position distress 鈹€鈹€
    floating_penalty = _check_floating_loss_guard(project_root, macro_direction)
    scale = 1.0
    state = "normal"
    reason = ""
    # Check hard conditions first (closed trades > floating loss)
    if aligned_loss_streak >= hard_threshold:
        scale = hard_scale
        state = "hard_penalty"
        reason = f"aligned_loss_streak:{aligned_loss_streak}"
    elif counter_edge and len(aligned_rows) >= min_samples and aligned_hit_rate <= 0.5:
        scale = hard_scale
        state = "hard_penalty"
        reason = f"counter_edge:{round(counter_hit_rate, 3)}>{round(aligned_hit_rate, 3)}"
    elif aligned_loss_streak >= soft_threshold:
        scale = soft_scale
        state = "soft_penalty"
        reason = f"aligned_loss_streak:{aligned_loss_streak}"
    elif counter_edge:
        scale = soft_scale
        state = "soft_penalty"
        reason = f"counter_edge:{round(counter_hit_rate, 3)}>{round(aligned_hit_rate, 3)}"
    # Fallback: floating loss penalty (soft, only if no stronger condition triggered)
    if state == "normal" and floating_penalty:
        scale = soft_scale
        state = "soft_penalty"
        reason = f"floating_loss:{floating_penalty}"
    effective_direction = str(macro_state.get("effective_direction", macro_direction) or macro_direction).upper()
    disempowered = False
    if state == "hard_penalty" and neutralize_on_hard:
        effective_direction = "NEUTRAL"
        disempowered = True
    result.update(
        {
            "state": state,
            "reason": reason,
            "scale": round(scale, 4),
            "sample_count": len(relevant_rows),
            "aligned_count": len(aligned_rows),
            "counter_count": len(counter_rows),
            "aligned_hit_rate": round(aligned_hit_rate, 4),
            "counter_hit_rate": round(counter_hit_rate, 4),
            "aligned_loss_streak": aligned_loss_streak,
            "effective_direction": effective_direction,
            "disempowered": disempowered,
        }
    )
    return result


def _compute_macro_bias(macro_payload: Dict[str, Any], policy: Dict[str, Any]) -> Dict[str, Any]:
    return build_macro_direction_snapshot(
        macro_payload,
        (policy.get("macro", {}) if isinstance(policy.get("macro", {}), dict) else {}),
    )


def _strategy_matches_session_policy(
    strategy_id: str,
    session_bucket: str,
    session_policy: Dict[str, Any],
) -> bool:
    contract = get_strategy_runtime_contract(strategy_id)
    style = str(contract.get("strategy_style", strategy_style(strategy_id)) or "hybrid").strip().lower()
    allowed_ids = {
        canonical_strategy_id(item)
        for item in session_policy.get("allowed_strategy_ids", [])
        if str(item).strip()
    } if isinstance(session_policy.get("allowed_strategy_ids", []), list) else set()
    allowed_groups = {
        str(item).strip().lower()
        for item in session_policy.get("allowed_strategy_groups", [])
        if str(item).strip()
    } if isinstance(session_policy.get("allowed_strategy_groups", []), list) else set()

    # Exact ids win. Group matching is a secondary fallback because several
    # legacy built-ins still expose only broad styles such as trend/reversion.
    if allowed_ids and canonical_strategy_id(strategy_id) in allowed_ids:
        return True
    if allowed_groups:
        if style in allowed_groups:
            return True
        if "breakout" in allowed_groups and style == "trend":
            return True
        if "momentum" in allowed_groups and style == "trend":
            return True
        if "composite" in allowed_groups and canonical_strategy_id(strategy_id) == "composite":
            return True
    allowed_sessions = {
        normalize_session_bucket(item)
        for item in contract.get("applicable_sessions", [])
        if str(item).strip()
    }
    return not allowed_sessions or "ALL" in allowed_sessions or session_bucket in allowed_sessions


def _build_session_scheduler_route(
    policy: Dict[str, Any],
    market_context: Dict[str, Any],
    candidate_ids: list[str],
    compatible_candidates: list[str],
) -> Dict[str, Any]:
    scheduler_cfg = (
        policy.get("session_scheduler", {})
        if isinstance(policy.get("session_scheduler", {}), dict)
        else _default_session_scheduler_policy()
    )
    session_bucket = str(market_context.get("session_bucket", "NORMAL") or "NORMAL").upper()
    session_mode = str(market_context.get("session_mode", "NORMAL") or "NORMAL").upper()
    policies = (
        scheduler_cfg.get("policies", {})
        if isinstance(scheduler_cfg.get("policies", {}), dict)
        else {}
    )
    base_policy = _default_session_scheduler_policy()["policies"].get(session_bucket, _default_session_scheduler_policy()["policies"]["LONDON"])
    current_policy = (
        policies.get(session_bucket, base_policy)
        if isinstance(policies.get(session_bucket, base_policy), dict)
        else base_policy
    )
    allowed_sessions = {
        str(item).strip().upper()
        for item in scheduler_cfg.get("allowed_sessions", [])
        if str(item).strip()
    } if isinstance(scheduler_cfg.get("allowed_sessions", []), list) else set()
    blocked_sessions = {
        str(item).strip().upper()
        for item in scheduler_cfg.get("blocked_sessions", [])
        if str(item).strip()
    } if isinstance(scheduler_cfg.get("blocked_sessions", []), list) else set()
    route_family = str(market_context.get("route_family", "") or "").strip().lower()
    if route_family == "blocked":
        return {
            "enabled": True,
            "mode": "hard_block",
            "current_session": session_bucket,
            "current_session_profile": session_mode,
            "user_allowed_sessions": sorted(allowed_sessions),
            "user_blocked_sessions": sorted(blocked_sessions),
            "current_policy": current_policy,
            "qualified_strategy_ids": [],
            "blocked_strategy_ids": list(candidate_ids),
            "candidate_strategy_ids": [],
            "decision": {
                "allow_entry": False,
                "allow_exit": True,
                "allow_add_position": False,
                "reduce_only": True,
                "reason_code": "ROUTE_BLOCKED",
                "reason_text": "褰撳墠璺敱涓?blocked锛岀姝㈡柊寮€浠擄紝浠呭厑璁稿噺浠?鍑哄満",
            },
        }
    qualified_ids = [
        strategy_id for strategy_id in candidate_ids
        if _strategy_matches_session_policy(strategy_id, session_bucket, current_policy)
    ]
    blocked_ids = [strategy_id for strategy_id in candidate_ids if strategy_id not in qualified_ids]
    session_candidates = [strategy_id for strategy_id in compatible_candidates if strategy_id in qualified_ids]
    enabled = bool(scheduler_cfg.get("enabled", False))
    if not enabled:
        return {
            "enabled": False,
            "mode": "soft_weight",
            "current_session": session_bucket,
            "current_session_profile": session_mode,
            "user_allowed_sessions": sorted(allowed_sessions),
            "user_blocked_sessions": sorted(blocked_sessions),
            "current_policy": current_policy,
            "qualified_strategy_ids": qualified_ids,
            "blocked_strategy_ids": blocked_ids,
            "candidate_strategy_ids": session_candidates or list(compatible_candidates),
            "decision": {
                "allow_entry": bool(
                    scheduler_cfg.get("default_when_disabled", {}).get("allow_new_entry", True)
                    if isinstance(scheduler_cfg.get("default_when_disabled", {}), dict)
                    else True
                ),
                "allow_exit": True,
                "allow_add_position": True,
                "reduce_only": False,
                "reason_code": "SESSION_SOFT_WEIGHT",
                "reason_text": f"褰撳墠鏈惎鐢ㄦ椂娈靛己绾︽潫锛屼氦鏄撴椂娈典粎浣滀负璇勫垎鍙傝€?({session_bucket})",
            },
        }

    blocked_by_user = session_bucket in blocked_sessions
    missing_from_allowlist = bool(allowed_sessions) and session_bucket not in allowed_sessions
    policy_denied = (
        not bool(current_policy.get("enabled", True))
        or not bool(current_policy.get("allow_new_entry", True))
    )
    allow_entry = not (blocked_by_user or missing_from_allowlist or policy_denied)
    reason_code = "ENTRY_ALLOWED"
    reason_text = f"Session {session_bucket}: no entry allowed"
    if blocked_by_user:
        reason_code = "SESSION_BLOCKED"
        reason_text = f"Session {session_bucket}: observe only, entry blocked"
    elif missing_from_allowlist:
        reason_code = "SESSION_NOT_ALLOWED"
        reason_text = f"Session {session_bucket}: trend blocked, observe only"
    elif policy_denied:
        reason_code = "SESSION_POLICY_DENIED"
        reason_text = f"Session {session_bucket}: full block on new entries"
    return {
        "enabled": True,
        "mode": "hard_gate",
        "current_session": session_bucket,
        "current_session_profile": session_mode,
        "user_allowed_sessions": sorted(allowed_sessions),
        "user_blocked_sessions": sorted(blocked_sessions),
        "current_policy": current_policy,
        "qualified_strategy_ids": qualified_ids,
        "blocked_strategy_ids": blocked_ids,
        "candidate_strategy_ids": session_candidates,
        "decision": {
            "allow_entry": allow_entry,
            "allow_exit": True,
            "allow_add_position": bool(current_policy.get("allow_add_position", True)),
            "reduce_only": not allow_entry and bool(current_policy.get("allow_reduce_only", True)),
            "reason_code": reason_code,
            "reason_text": reason_text,
        },
    }


_MARKET_CONTEXT_CACHE_SEC = 5.0
_last_market_context_cache: Dict[str, Any] = {
    "fingerprint": "",
    "ts": 0.0,
    "value": {},
}


def _build_market_context_cache_fingerprint(
    agent_context: Dict[str, Any],
    engine_request: Dict[str, Any],
    mt5_snapshot: Optional[Dict[str, Any]] = None,
) -> str:
    agent = agent_context if isinstance(agent_context, dict) else {}
    request = engine_request if isinstance(engine_request, dict) else {}
    snapshot = mt5_snapshot if isinstance(mt5_snapshot, dict) else {}
    regime_4d = (
        agent.get("regime_classification", {})
        if isinstance(agent.get("regime_classification", {}), dict)
        else {}
    )
    interpreter = (
        agent.get("interpreter", {})
        if isinstance(agent.get("interpreter", {}), dict)
        else {}
    )
    macro = agent.get("macro", {}) if isinstance(agent.get("macro", {}), dict) else {}
    session_profile = (
        agent.get("session_profile", {})
        if isinstance(agent.get("session_profile", {}), dict)
        else {}
    )
    m1_loosening = (
        agent.get("m1_loosening", {})
        if isinstance(agent.get("m1_loosening", {}), dict)
        else {}
    )
    suppressed_intents = (
        agent.get("suppressed_strategy_intents", [])
        if isinstance(agent.get("suppressed_strategy_intents", []), list)
        else []
    )
    bars = snapshot.get("bars", []) if isinstance(snapshot.get("bars", []), list) else []
    last_bar = bars[-1] if bars and isinstance(bars[-1], dict) else {}
    plan = (
        request.get("ai_trade_plan", {})
        if isinstance(request.get("ai_trade_plan", {}), dict)
        else {}
    )
    cache_payload = {
        "resolver_id": id(resolve_unified_market_context),
        "symbol": normalize_symbol(request.get("symbol", snapshot.get("symbol", "XAUUSD"))),
        "timeframe": str(
            request.get("timeframe")
            or request.get("interval")
            or plan.get("timeframe")
            or last_bar.get("timeframe")
            or "M5"
        ).upper(),
        "regime": str(agent.get("regime", "") or ""),
        "regime_confidence": _safe_float(agent.get("regime_confidence", 0.0), 0.0),
        "regime_classification": {
            "regime": str(regime_4d.get("regime", "") or ""),
            "confidence": _safe_float(regime_4d.get("confidence", 0.0), 0.0),
            "atr_ratio": _safe_float(regime_4d.get("atr_ratio", 0.0), 0.0),
            "transition_count": int(regime_4d.get("transition_count", 0) or 0),
            "change_count": int(regime_4d.get("change_count", 0) or 0),
            "flags": list(regime_4d.get("flags", []))
            if isinstance(regime_4d.get("flags", []), list)
            else [],
        },
        "interpreter": {
            "market_structure": str(interpreter.get("market_structure", "") or ""),
            "market_regime": str(interpreter.get("market_regime", "") or ""),
            "regime_subtype": str(interpreter.get("regime_subtype", "") or ""),
            "execution_advice": str(interpreter.get("execution_advice", "") or ""),
            "counter_trend_risk": str(interpreter.get("counter_trend_risk", "") or ""),
            "structural_side": str(interpreter.get("structural_side", "") or ""),
        },
        "macro": {
            "direction": str(macro.get("direction", "") or ""),
            "effective_direction": str(macro.get("effective_direction", "") or ""),
            "binding_direction": str(macro.get("binding_direction", "") or ""),
            "macro_authority": str(macro.get("macro_authority", "") or ""),
            "state": str(macro.get("state", "") or ""),
        },
        "session_mode": str(session_profile.get("mode", agent.get("_session_info", {}).get("session_mode", "NORMAL")) or "NORMAL"),
        "m1_loosening": {
            "loosening": bool(m1_loosening.get("loosening", False)),
            "direction": str(m1_loosening.get("direction", "NEUTRAL") or "NEUTRAL"),
            "confidence": _safe_float(m1_loosening.get("confidence", 0.0), 0.0),
        },
        "suppressed_strategy_intents": [
            {
                "strategy_id": canonical_strategy_id(item.get("strategy_id", "")),
                "strategy_family": str(item.get("strategy_family", "") or ""),
                "intended_side": str(item.get("intended_side", "") or ""),
            }
            for item in suppressed_intents
            if isinstance(item, dict)
        ],
        "plan": {
            "regime": str(plan.get("regime", "") or ""),
            "mode": str(plan.get("mode", "") or ""),
            "regime_subtype": str(plan.get("regime_subtype", "") or ""),
            "timeframe": str(plan.get("timeframe", "") or ""),
        },
        "snapshot": {
            "spread": _safe_float(snapshot.get("spread", 0.0), 0.0),
            "bar_count": len(bars),
            "last_bar_time": str(
                last_bar.get("time")
                or last_bar.get("timestamp")
                or last_bar.get("datetime")
                or ""
            ),
            "last_bar_close": _safe_float(last_bar.get("close", 0.0), 0.0),
        },
    }
    payload = json.dumps(cache_payload, sort_keys=True, ensure_ascii=True, default=str)
    return hashlib.sha1(payload.encode("utf-8")).hexdigest()


def _resolve_market_context_cached(
    agent_context: Dict[str, Any],
    engine_request: Dict[str, Any],
    mt5_snapshot: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    global _last_market_context_cache

    fingerprint = _build_market_context_cache_fingerprint(agent_context, engine_request, mt5_snapshot)
    now_ts = time.time()
    cached_fingerprint = str(_last_market_context_cache.get("fingerprint", "") or "")
    cached_ts = _safe_float(_last_market_context_cache.get("ts", 0.0), 0.0)
    cached_value = (
        _last_market_context_cache.get("value", {})
        if isinstance(_last_market_context_cache.get("value", {}), dict)
        else {}
    )
    if (
        cached_value
        and cached_fingerprint == fingerprint
        and now_ts - cached_ts <= _MARKET_CONTEXT_CACHE_SEC
    ):
        return deepcopy(cached_value)

    market_context = resolve_unified_market_context(
        agent_context,
        engine_request,
        mt5_snapshot=mt5_snapshot,
    )
    _last_market_context_cache = {
        "fingerprint": fingerprint,
        "ts": now_ts,
        "value": deepcopy(market_context),
    }
    return deepcopy(market_context)


def _build_strategy_route(agent_context: Dict[str, Any], engine_request: Dict[str, Any]) -> Dict[str, Any]:
    macro = agent_context.get("macro", {}) if isinstance(agent_context.get("macro", {}), dict) else {}
    interpreter = agent_context.get("interpreter", {}) if isinstance(agent_context.get("interpreter", {}), dict) else {}
    policy = (
        agent_context.get("decision_policy", {})
        if isinstance(agent_context.get("decision_policy", {}), dict)
        else {}
    )
    router = policy.get("router", {}) if isinstance(policy.get("router", {}), dict) else {}
    plan = engine_request.get("ai_trade_plan", {}) if isinstance(engine_request.get("ai_trade_plan", {}), dict) else {}

    # 鈹€鈹€ Inject M1 loosening data + suppressed strategy intents 鈹€鈹€
    global _M1_LOOSENING_TICK, _prev_suppressed_strategy_intents
    _M1_LOOSENING_TICK += 1
    if _M1_LOOSENING_TICK >= _M1_LOOSENING_INTERVAL:
        _M1_LOOSENING_TICK = 0
        try:
            from bridge.strategy import _read_bars
            m1_bars = _read_bars(30, timeframe="M1")
            if m1_bars and len(m1_bars) >= 15:
                from src.regime_router import detect_range_loosening
                agent_context["m1_loosening"] = detect_range_loosening(m1_bars)
        except Exception:
            agent_context.pop("m1_loosening", None)
    if _prev_suppressed_strategy_intents:
        agent_context["suppressed_strategy_intents"] = list(_prev_suppressed_strategy_intents)

    market_context = _resolve_market_context_cached(
        agent_context,
        engine_request,
        mt5_snapshot=engine_request,
    )
    regime = str(market_context.get("regime", "TRANSITION") or "TRANSITION").upper()
    subtype = str(market_context.get("regime_subtype", "") or "").upper()
    _regime_4d_conf = float(
        market_context.get("effective_regime_confidence")
        or market_context.get("regime_confidence", 0.0)
        or 0.0
    )
    canonical_symbol = normalize_symbol(engine_request.get("symbol", market_context.get("symbol", "XAUUSD")))
    canonical_timeframe = str(
        market_context.get("timeframe")
        or engine_request.get("timeframe")
        or engine_request.get("interval")
        or "M5"
    )
    canonical_market_snapshot = build_canonical_market_snapshot(
        market_context,
        macro_context=macro,
        interpreter_context=interpreter,
        symbol=canonical_symbol,
        timeframe=canonical_timeframe,
    )
    snapshot_generated_at = str(canonical_market_snapshot.get("generated_at", "") or "").strip()
    canonical_snapshot_is_stale = False
    if snapshot_generated_at:
        try:
            snapshot_dt = datetime.fromisoformat(snapshot_generated_at.replace("Z", "+00:00"))
            canonical_snapshot_is_stale = (datetime.now(timezone.utc) - snapshot_dt).total_seconds() > 30 * 60
        except Exception:
            canonical_snapshot_is_stale = True
    elif canonical_market_snapshot:
        canonical_snapshot_is_stale = True
    if canonical_snapshot_is_stale or not canonical_market_snapshot:
        canonical_market_snapshot = build_canonical_market_snapshot(
            market_context,
            macro_context=macro,
            interpreter_context=interpreter,
            symbol=canonical_symbol,
            timeframe=canonical_timeframe,
        )
    normalized_market = (
        canonical_market_snapshot.get("normalized", {})
        if isinstance(canonical_market_snapshot.get("normalized", {}), dict)
        else {}
    )

    # 鈹€鈹€ Dynamic strategy selection: start from deployed running instances, but
    # keep regime routing as the primary gate. Do not silently widen to all
    # strategies unless there is no compatible candidate left.
    running_ids = _get_running_strategy_ids()

    # 鈹€鈹€ Centralized garbage detection 鈹€鈹€
    garbage_reason = ""
    garbage_allowed_types: list[str] = []
    garbage_size_multiplier = 1.0
    try:
        from src.learning.garbage_detector import classify_market
        posture = classify_market(interpreter, engine_request)
        if posture["block_all"]:
            route_family = "blocked"
            garbage_reason = posture["reason"]
        else:
            garbage_allowed_types = list(posture.get("allowed_types", []) or [])
            garbage_size_multiplier = float(posture.get("size_multiplier", 1.0) or 1.0)
    except Exception:
        pass

    route_family = str(market_context.get("route_family", "") or "").strip().lower()
    if route_family not in {"trend", "range", "range_loosening", "blocked", "uncertain"}:
        route_family = ""
    normalized_entry_family = str(normalized_market.get("entry_family", "") or "").strip().lower()
    normalized_family = str(normalized_market.get("market_family", "") or "").strip().upper()
    normalized_confidence = _safe_float(normalized_market.get("confidence", 0.0), 0.0)
    if garbage_reason:
        route_family = "blocked"
    if not route_family:
        route_family = "uncertain"

    effective_direction = str(macro.get("effective_direction", "NEUTRAL") or "NEUTRAL").upper()
    macro_authority = _resolve_macro_authority_context(macro, regime=regime)
    binding_direction = str(
        macro.get("binding_direction", macro.get("direction", effective_direction))
        or "NEUTRAL"
    ).upper()
    # 鈹€鈹€ Agent direction bias: agent_dir is a weighted preference, NOT a hard lock 鈹€鈹€
    # Macro direction biases trade weighting; it does NOT block counter-direction signals.
    # HARD_BIND is reserved for fresh, high-confidence agent reads that pass all guards.
    # Counter-direction signals always pass through with _counter_direction_penalty marker.
    try:
        from bridge.gold_trader import get_agent_direction
        agent_dir_override, _agent_meta = get_agent_direction()
        agent_dir_override = str(agent_dir_override or "NEUTRAL").upper()
        agent_age_sec = _safe_float(
            _agent_meta.get("macro_age_sec", _agent_meta.get("age_seconds", 0)) or 0, 0
        ) if isinstance(_agent_meta, dict) else 0.0
        agent_fresh = agent_age_sec < MACRO_FRESH_TTL_SECONDS if agent_age_sec > 0 else True
        degraded = bool(_agent_meta.get("degraded", False)) if isinstance(_agent_meta, dict) else False
        if agent_dir_override in {"LONG", "SHORT"} and agent_fresh and not degraded:
            binding_direction = agent_dir_override
            # Keep existing macro_authority from _resolve_macro_authority_context()
            # 鈥?do NOT force HARD_BIND. The authority tier is determined by
            # bias_weight, staleness, direction_guard, and loss_guard already.
    except Exception:
        pass
    allowed_side = resolve_allowed_side(binding_direction, macro_authority)

    # 鈹€鈹€ Safety net: if allowed_side is single-direction but macro_authority is SOFT_BIAS,
    #     force BOTH. Direction preference is expressed via weights, not side-blocking. 鈹€鈹€
    macro_confidence = float(macro.get("confidence", 0.0) or 0.0)
    macro_state = str(macro.get("state", "UNKNOWN") or "UNKNOWN").upper()
    macro_degraded = bool(macro.get("degraded", False))
    if macro_authority == "SOFT_BIAS" and allowed_side in {"BUY", "SELL"}:
        if macro_state == "FRESH" and macro_confidence >= 0.55 and not macro_degraded:
            macro_authority = "HARD_BIND"
        else:
            allowed_side = "BOTH"

    original_route_family = route_family
    route_adjustment = ""
    canonical_realign = False
    if not garbage_reason and normalized_entry_family in {"trend", "range"}:
        if (
            route_family == "uncertain"
            and normalized_confidence >= max(0.65, float(_regime_4d_conf or 0.0) + 0.1)
        ):
            canonical_realign = True
        elif (
            route_family in {"trend", "range"}
            and route_family != normalized_entry_family
            and normalized_confidence >= 0.75
            and normalized_confidence + 0.1 >= float(_regime_4d_conf or 0.0)
        ):
            canonical_realign = True
    if canonical_realign:
        route_family = normalized_entry_family
        route_adjustment = (
            f"canonical_override:{normalized_entry_family}:"
            f"{normalized_confidence:.2f}>{float(_regime_4d_conf or 0.0):.2f}"
        )

    route_config = router.get(route_family, {}) if isinstance(router.get(route_family, {}), dict) else {}

    blocked_candidate_ids = [canonical_strategy_id(item) for item in running_ids if str(item or "").strip()]
    if route_family == "blocked":
        normalized_family = {"primary_strategy_id": "", "support_strategy_ids": [],
                             "rank_weights": [], "candidate_strategy_ids": []}
    else:
        # Prefer configured route candidates. Running instances are only used as a
        # secondary fallback when the router configuration is missing.
        default_primary = running_ids[0] if running_ids else ""
        default_supports = running_ids[1:] if len(running_ids) > 1 else []
        rank_weights = [round(1.0 / len(running_ids), 4)] * len(running_ids) if running_ids else []
        normalized_family = _normalize_router_family(
            route_config,
            primary_strategy_id=default_primary,
            support_strategy_ids=default_supports,
            rank_weights=rank_weights,
        )
    primary = normalized_family["primary_strategy_id"]
    supports = normalized_family["support_strategy_ids"]
    configured_candidates = [canonical_strategy_id(item) for item in [primary, *supports] if str(item or "").strip()]
    candidate_ids = _merge_route_candidate_ids(configured_candidates, running_ids)
    configured_candidate_ids = list(candidate_ids)
    if route_family == "uncertain":
        candidate_ids = _expand_uncertain_route_candidates(router, candidate_ids, running_ids=running_ids)
    compatible_candidates: list[str] = []
    regime_compatible_candidates: list[str] = []
    regime_confidence = float(_regime_4d_conf or 0.0)
    if (
        regime_confidence <= 0.01
        and route_family in {"trend", "range", "range_loosening"}
        and regime not in {"", "UNKNOWN", "TRANSITION"}
    ):
        regime_confidence = 0.30
    elif (
        regime_confidence <= 0.01
        and route_family == "uncertain"
        and regime in {"TRANSITION", "RANGING"}
    ):
        regime_confidence = 0.35
    session_mode = str(market_context.get("session_mode", "") or "").upper()
    session_bucket = str(market_context.get("session_bucket", session_mode) or session_mode).upper()
    canonical_candidates: list[str] = []
    final_candidates: list[str] = []
    candidate_fit_map: Dict[str, Dict[str, Any]] = {}
    if route_family == "blocked":
        session_scheduler_route = _build_session_scheduler_route(
            policy,
            market_context,
            blocked_candidate_ids,
            [],
        )
        candidate_ids = []
        compatible_candidates = []
        primary = ""
    else:
        for strategy_id in candidate_ids:
            compatible, _reason = is_strategy_regime_compatible(
                strategy_id,
                regime,
                regime_confidence=regime_confidence,
                session_mode=session_bucket,
                route_family=route_family,
            )
            if compatible:
                compatible_candidates.append(strategy_id)
        regime_compatible_candidates = list(compatible_candidates)
        available_candidate_ids = [strategy_id for strategy_id in candidate_ids if strategy_id in running_ids]
        available_compatible_candidates = [
            strategy_id for strategy_id in compatible_candidates if strategy_id in running_ids
        ]
        if not available_candidate_ids or not available_compatible_candidates:
            fallback_candidate_ids, fallback_compatible_candidates, fallback_primary = _resolve_running_strategy_fallback(
                running_ids,
                route_family,
                regime,
                regime_confidence,
                session_bucket,
            )
            if fallback_candidate_ids:
                candidate_ids = fallback_candidate_ids
                compatible_candidates = (
                    fallback_compatible_candidates
                    if fallback_compatible_candidates
                    else [strategy_id for strategy_id in compatible_candidates if strategy_id in candidate_ids]
                )
                primary = fallback_primary or candidate_ids[0]
                supports = [strategy_id for strategy_id in candidate_ids if strategy_id != primary]
                normalized_family["candidate_strategy_ids"] = list(candidate_ids)
                normalized_family["primary_strategy_id"] = primary
                normalized_family["support_strategy_ids"] = list(supports)
                normalized_family["rank_weights"] = _resolve_rank_weights(
                    normalized_family.get("rank_weights", []),
                    len(candidate_ids),
                )
                regime_compatible_candidates = list(compatible_candidates)
        for strategy_id in compatible_candidates:
            contract = get_strategy_runtime_contract(strategy_id)
            fit_result = evaluate_strategy_market_fit(contract, canonical_market_snapshot)
            candidate_fit_map[strategy_id] = fit_result
            if bool(fit_result.get("eligible", False)):
                canonical_candidates.append(strategy_id)
        if canonical_candidates:
            compatible_candidates = canonical_candidates
        compatible_candidates = _rank_route_candidate_ids(
            compatible_candidates,
            route_family,
            candidate_fit_map,
            configured_candidates,
        )
        session_scheduler_route = _build_session_scheduler_route(
            policy,
            market_context,
            candidate_ids,
            compatible_candidates,
        )
        preferred_candidates = session_scheduler_route.get("candidate_strategy_ids", [])
        if isinstance(preferred_candidates, list) and preferred_candidates:
            preferred_set = {canonical_strategy_id(item) for item in preferred_candidates}
            final_candidates = [
                strategy_id for strategy_id in compatible_candidates
                if strategy_id in preferred_set
            ] + [
                strategy_id for strategy_id in compatible_candidates
                if strategy_id not in preferred_set
            ]
        else:
            final_candidates = list(compatible_candidates)
        final_candidates = _rank_route_candidate_ids(
            final_candidates,
            route_family,
            candidate_fit_map,
            configured_candidates,
        )
        if final_candidates:
            primary = final_candidates[0]
            supports = [strategy_id for strategy_id in final_candidates if strategy_id != primary]
        else:
            primary = ""
            supports = []
    if route_family == "blocked":
        regime_compatible_candidates = []
        final_candidates = []

    _route_payload = {
        "route_family": route_family,
        "original_route_family": original_route_family,
        "route_adjustment": route_adjustment,
        "regime": regime,
        "regime_confidence": regime_confidence,
        "regime_subtype": subtype,
        "garbage_reason": garbage_reason,
        "garbage_allowed_types": garbage_allowed_types,
        "garbage_size_multiplier": garbage_size_multiplier,
        "primary_strategy_id": canonical_strategy_id(primary),
        "configured_candidate_strategy_ids": list(configured_candidate_ids),
        "regime_compatible_candidate_strategy_ids": list(regime_compatible_candidates),
        "fit_eligible_candidate_strategy_ids": list(canonical_candidates),
        "candidate_strategy_ids": list(final_candidates),
        "compatible_candidate_strategy_ids": list(final_candidates),
        "selection_candidate_strategy_ids": list(final_candidates),
        "rank_weights": list(normalized_family["rank_weights"]),
        "allowed_side": allowed_side,
        "session_mode": session_mode,
        "macro_authority": macro_authority,
        "macro_state": str(macro.get("state", "UNKNOWN") or "UNKNOWN"),
        "macro_direction": str(macro.get("direction", "NEUTRAL") or "NEUTRAL").upper(),
        "effective_macro_direction": effective_direction,
        "binding_macro_direction": binding_direction,
        "direction_guard": (
            macro.get("direction_guard", {})
            if isinstance(macro.get("direction_guard", {}), dict)
            else {}
        ),
        "session_bucket": str(market_context.get("session_bucket", "") or ""),
        "session_scheduler_enabled": bool(session_scheduler_route.get("enabled", False)),
        "session_gate_mode": str(session_scheduler_route.get("mode", "soft_weight") or "soft_weight"),
        "session_current_policy": deepcopy(session_scheduler_route.get("current_policy", {})),
        "session_qualified_strategy_ids": list(session_scheduler_route.get("qualified_strategy_ids", [])),
        "session_blocked_strategy_ids": list(session_scheduler_route.get("blocked_strategy_ids", [])),
        "session_candidate_strategy_ids": list(session_scheduler_route.get("candidate_strategy_ids", [])),
        "session_decision": deepcopy(session_scheduler_route.get("decision", {})),
        "session_user_allowed_sessions": list(session_scheduler_route.get("user_allowed_sessions", [])),
        "session_user_blocked_sessions": list(session_scheduler_route.get("user_blocked_sessions", [])),
        "reason": (
            f"{route_family}:{regime}:{subtype or 'NO_SUBTYPE'}:{allowed_side}:{route_adjustment}"
            if route_adjustment
            else f"{route_family}:{regime}:{subtype or 'NO_SUBTYPE'}:{allowed_side}"
        ),
        "market_context": market_context,
        "canonical_market_snapshot": canonical_market_snapshot,
        "candidate_fit_map": candidate_fit_map,
    }
    return _route_payload


def _route_scheduler_snapshot(
    scheduler_snapshot: Dict[str, Any],
    agent_context: Dict[str, Any],
    engine_request: Dict[str, Any],
) -> Dict[str, Any]:
    route = _build_strategy_route(agent_context, engine_request)
    route_market_context = (
        route.get("market_context", {})
        if isinstance(route.get("market_context", {}), dict)
        else {}
    )
    route_macro_context = (
        agent_context.get("macro", {})
        if isinstance(agent_context.get("macro", {}), dict)
        else {}
    )
    route_interpreter_context = (
        agent_context.get("interpreter", {})
        if isinstance(agent_context.get("interpreter", {}), dict)
        else {}
    )
    route_snapshot = (
        route.get("canonical_market_snapshot", {})
        if isinstance(route.get("canonical_market_snapshot", {}), dict)
        else {}
    )
    snapshot_generated_at = str(route_snapshot.get("generated_at", "") or "").strip()
    snapshot_is_stale = False
    if snapshot_generated_at:
        try:
            snapshot_dt = datetime.fromisoformat(snapshot_generated_at.replace("Z", "+00:00"))
            snapshot_is_stale = (datetime.now(timezone.utc) - snapshot_dt).total_seconds() > 30 * 60
        except Exception:
            snapshot_is_stale = True
    elif route_snapshot:
        snapshot_is_stale = True
    if snapshot_is_stale or not route_snapshot:
        route["canonical_market_snapshot"] = build_canonical_market_snapshot(
            route_market_context,
            macro_context=route_macro_context,
            interpreter_context=route_interpreter_context,
            symbol=normalize_symbol(
                engine_request.get("symbol", route_market_context.get("symbol", "XAUUSD"))
            ),
            timeframe=str(
                route_market_context.get("timeframe")
                or engine_request.get("timeframe")
                or engine_request.get("interval")
                or "M5"
            ).upper(),
        )
    if str(route.get("route_family", "") or "").strip().lower() == "blocked":
        routed = dict(scheduler_snapshot)
        routed["route"] = {
            **route,
            "candidate_strategy_ids": [],
            "compatible_strategy_ids": [],
            "compatible_candidate_strategy_ids": [],
        }
        routed["instances"] = []
        routed["selected_strategy_id"] = ""
        routed["selected_instance_id"] = ""
        routed["strategy_weights"] = {}
        routed["instance_weights"] = {}
        routed["selection"] = {
            "passed": False,
            "reason": "ROUTE_BLOCKED",
            "winner_strategy_id": "",
            "winner_instance_id": "",
            "winner_score": 0.0,
            "runner_up_score": 0.0,
            "selection_margin": 0.0,
            "ranked_candidates": [],
        }
        return routed
    instances = scheduler_snapshot.get("instances", []) if isinstance(scheduler_snapshot.get("instances", []), list) else []
    # 鈹€鈹€ Safety net: if snapshot has no instances (stale after config migration),
    #     reload from unified state on-the-fly instead of returning empty. 鈹€鈹€
    if not instances:
        try:
            from src.unified_strategy_state import load_unified_state, get_instances
            unified = load_unified_state(use_cache=False)
            instances_dict = get_instances(unified)
            active_iids = scheduler_snapshot.get("active_instance_ids", [])
            if not active_iids:
                active_iids = [
                    iid for iid, inst in instances_dict.items()
                    if isinstance(inst, dict) and str(inst.get("status", "")).lower() == "running"
                ]
            instances = [
                inst for inst in instances_dict.values()
                if isinstance(inst, dict) and str(inst.get("instance_id", "")).strip() in active_iids
            ] if active_iids else list(instances_dict.values())
        except Exception:
            pass
    legacy_selected_instance = str(scheduler_snapshot.get("selected_instance_id", "") or "").strip().startswith("inst_")
    legacy_route_owner = str(route.get("entry_owner_instance_id", "") or "").strip().startswith("inst_")
    if legacy_selected_instance or legacy_route_owner:
        try:
            from src.unified_strategy_state import load_unified_state, get_running_instances

            running_instances = get_running_instances(load_unified_state(use_cache=False))
            if running_instances:
                instances = running_instances
                strategy_instance_map = {
                    canonical_strategy_id(item.get("strategy_id", "")): str(item.get("instance_id", "") or "").strip()
                    for item in running_instances
                    if isinstance(item, dict) and str(item.get("instance_id", "") or "").strip()
                }
                owner_strategy_id = canonical_strategy_id(
                    route.get("entry_owner_strategy_id", scheduler_snapshot.get("selected_strategy_id", ""))
                )
                selected_strategy_id = canonical_strategy_id(
                    scheduler_snapshot.get("selected_strategy_id", route.get("selected_strategy_id", ""))
                )
                if owner_strategy_id and strategy_instance_map.get(owner_strategy_id):
                    route["entry_owner_instance_id"] = strategy_instance_map[owner_strategy_id]
                if selected_strategy_id and strategy_instance_map.get(selected_strategy_id):
                    scheduler_snapshot = dict(scheduler_snapshot)
                    scheduler_snapshot["selected_instance_id"] = strategy_instance_map[selected_strategy_id]
        except Exception:
            pass
    if len(instances) <= 1:
        routed = dict(scheduler_snapshot)
        routed["route"] = route
        routed["instances"] = instances
        return routed

    performance_factors = _load_pretrade_performance_factors(agent_context)
    strategy_perf_bucket = (
        performance_factors.get("strategy", {})
        if isinstance(performance_factors.get("strategy", {}), dict)
        else {}
    )
    available_by_strategy: Dict[str, list[Dict[str, Any]]] = {}
    for instance in instances:
        if not isinstance(instance, dict):
            continue
        strategy_id = canonical_strategy_id(instance.get("strategy_id", ""))
        if not strategy_id:
            continue
        normalized_instance = dict(instance)
        normalized_instance["strategy_id"] = strategy_id
        available_by_strategy.setdefault(strategy_id, []).append(normalized_instance)

    route["entry_policy"] = "regime_directed"
    available_ids = [canonical_strategy_id(item) for item in available_by_strategy.keys()]
    route_family_val = str(route.get("route_family", "") or "")
    route_regime = str(route.get("regime", "") or "")
    route_session_bucket = str(route.get("session_bucket", route.get("session_mode", "")) or "")
    route_regime_confidence = float(route.get("regime_confidence", 0.0) or 0.0)
    route_fit_map = route.get("candidate_fit_map", {}) if isinstance(route.get("candidate_fit_map", {}), dict) else {}
    configured_candidates = [
        canonical_strategy_id(item)
        for item in (
            route.get(
                "selection_candidate_strategy_ids",
                route.get("compatible_candidate_strategy_ids", route.get("candidate_strategy_ids", [])),
            )
            if isinstance(
                route.get(
                    "selection_candidate_strategy_ids",
                    route.get("compatible_candidate_strategy_ids", route.get("candidate_strategy_ids", [])),
                ),
                list,
            )
            else []
        )
    ]
    compatible_available_ids = [strategy_id for strategy_id in configured_candidates if strategy_id in available_ids]
    ordered_strategies = list(compatible_available_ids)
    if not ordered_strategies:
        compatible_available_ids = [
            strategy_id
            for strategy_id in available_ids
            if is_strategy_regime_compatible(
                strategy_id,
                route_regime,
                regime_confidence=route_regime_confidence,
                session_mode=route_session_bucket,
                route_family=route_family_val,
            )[0]
        ]
        fallback_family = route_family_val
        if fallback_family == "trend" and route_regime in {"TRANSITION", "RANGING"}:
            fallback_family = "range"
        fallback_families: list[str] = [fallback_family]
        if fallback_family in ("uncertain", "") or route_regime == "TRANSITION":
            fallback_families = ["uncertain", "range", "trend"]
        for family in fallback_families:
            family_candidates = [
                strategy_id
                for strategy_id in compatible_available_ids
                if _strategy_id_matches_route_family(strategy_id, family)
            ]
            ordered_strategies = _rank_route_candidate_ids(
                family_candidates,
                family,
                route_fit_map,
                configured_candidates,
            )
            if ordered_strategies:
                break
        if not ordered_strategies and compatible_available_ids:
            rescue_family = fallback_families[0] if fallback_families else route_family_val
            ordered_strategies = _rank_route_candidate_ids(
                compatible_available_ids,
                rescue_family,
                route_fit_map,
                configured_candidates,
            )
            if ordered_strategies:
                route["fallback"] = "dynamic_candidate_rescue"
    survival_controls: Dict[str, Dict[str, Any]] = {}
    filtered_ordered_strategies: list[str] = []
    governance_excluded: Dict[str, str] = {}
    downweighted_strategy_ids: list[str] = []
    for strategy_id in ordered_strategies:
        perf_row = (
            strategy_perf_bucket.get(str(strategy_id).strip().lower(), {})
            if isinstance(strategy_perf_bucket, dict)
            else {}
        )
        decision = str(perf_row.get("decision", "neutral") or "neutral").strip().lower()
        live_multiplier = max(0.0, float(perf_row.get("live_multiplier", 1.0) or 1.0))
        control = {
            "decision": decision,
            "live_multiplier": round(live_multiplier, 4),
            "reason": str(perf_row.get("reason", "") or ""),
        }
        if decision == "block" or live_multiplier <= 0.05:
            governance_excluded[strategy_id] = control["reason"] or "performance_block"
            survival_controls[strategy_id] = {
                **control,
                "candidate_action": "pause",
            }
            continue
        if decision == "downweight" or live_multiplier < 1.0:
            downweighted_strategy_ids.append(strategy_id)
            survival_controls[strategy_id] = {
                **control,
                "candidate_action": "downweight",
            }
        elif decision == "favor":
            survival_controls[strategy_id] = {
                **control,
                "candidate_action": "favor",
            }
        filtered_ordered_strategies.append(strategy_id)
    ordered_strategies = filtered_ordered_strategies
    route["candidate_strategy_ids"] = list(ordered_strategies)
    route["compatible_candidate_strategy_ids"] = list(ordered_strategies)
    route["selection_candidate_strategy_ids"] = list(ordered_strategies)
    if governance_excluded:
        route["governance_excluded_strategy_ids"] = dict(governance_excluded)
    if downweighted_strategy_ids:
        route["downweighted_strategy_ids"] = list(downweighted_strategy_ids)
    if survival_controls:
        route["survival_controls"] = survival_controls
    # Fallback for edge cases (ordered_strategies already set above)
    if not ordered_strategies:
        routed = dict(scheduler_snapshot)
        routed["route"] = {
            **route,
            "candidate_strategy_ids": [],
            "compatible_candidate_strategy_ids": [],
            "selection_candidate_strategy_ids": [],
            "compatible_strategy_ids": [],
            "fallback": "no_candidate_available",
        }
        routed["selected_strategy_id"] = ""
        routed["selected_instance_id"] = ""
        routed["strategy_weights"] = {}
        routed["instance_weights"] = {}
        routed["selection"] = {
            "passed": False,
            "reason": "NO_CANDIDATE_AVAILABLE",
            "winner_strategy_id": "",
            "winner_instance_id": "",
            "winner_score": 0.0,
            "runner_up_score": 0.0,
            "selection_margin": 0.0,
            "ranked_candidates": [],
        }
        return routed

    configured_total = len(
        [
            strategy_id
            for strategy_id in configured_candidates
            if strategy_id in available_by_strategy
        ]
    )
    if configured_total > len(ordered_strategies):
            route.setdefault("fallback", "partial_candidate_coverage")

    deduped_strategies, suppressed_conflicts = _dedupe_route_candidates_by_conflict_signature(
        ordered_strategies,
        available_by_strategy,
    )
    if deduped_strategies:
        ordered_strategies = deduped_strategies
    if suppressed_conflicts:
        route["suppressed_conflicts"] = suppressed_conflicts
    route["compatible_strategy_ids"] = compatible_available_ids

    configured_candidates = route.get("candidate_strategy_ids", []) if isinstance(route.get("candidate_strategy_ids", []), list) else []
    configured_rank_weights = (
        route.get("rank_weights", [])
        if isinstance(route.get("rank_weights", []), list)
        else []
    )
    strategy_route_weights: Dict[str, float] = {}
    for strategy_id in ordered_strategies:
        try:
            configured_index = configured_candidates.index(strategy_id)
        except ValueError:
            configured_index = -1
        configured_weight = (
            configured_rank_weights[configured_index]
            if 0 <= configured_index < len(configured_rank_weights)
            else 0.0
        )
        strategy_route_weights[strategy_id] = round(max(0.0, _safe_float(configured_weight, 0.0)), 6)
    resolved_rank_weights = _resolve_rank_weights(configured_rank_weights, len(ordered_strategies))
    execution_strategy_weights = {
        strategy_id: resolved_rank_weights[idx]
        for idx, strategy_id in enumerate(ordered_strategies)
    }
    if not any(weight > 0.0 for weight in strategy_route_weights.values()):
        strategy_route_weights = dict(execution_strategy_weights)
    if survival_controls:
        for strategy_id in list(execution_strategy_weights.keys()):
            control = survival_controls.get(strategy_id, {})
            multiplier = max(0.0, float(control.get("live_multiplier", 1.0) or 1.0))
            if multiplier != 1.0:
                execution_strategy_weights[strategy_id] = round(
                    float(execution_strategy_weights.get(strategy_id, 0.0) or 0.0) * multiplier,
                    6,
                )
                strategy_route_weights[strategy_id] = round(
                    float(strategy_route_weights.get(strategy_id, 0.0) or 0.0) * multiplier,
                    6,
                )
        positive_total = sum(weight for weight in execution_strategy_weights.values() if weight > 0.0)
        if positive_total > 0:
            execution_strategy_weights = {
                strategy_id: round(weight / positive_total, 6)
                for strategy_id, weight in execution_strategy_weights.items()
            }

    routed_instances: list[Dict[str, Any]] = []
    instance_weights: Dict[str, float] = {}
    strategy_weights: Dict[str, float] = {}
    strategy_instance_map: Dict[str, list[str]] = {}
    selection_payload: Dict[str, Any] = {
        "passed": False,
        "reason": "NO_SELECTION",
        "winner_strategy_id": "",
        "winner_instance_id": "",
        "winner_score": 0.0,
        "runner_up_score": 0.0,
        "selection_margin": 0.0,
        "ranked_candidates": [],
    }
    canonical_snapshot = (
        route.get("canonical_market_snapshot", {})
        if isinstance(route.get("canonical_market_snapshot", {}), dict)
        else {}
    )

    for strategy_id in ordered_strategies:
        rows = available_by_strategy.get(strategy_id, [])
        if not rows:
            continue
        base_weight = float(execution_strategy_weights.get(strategy_id, 0.0) or 0.0)
        row_total = sum(max(0.0, float(row.get("weight", row.get("allocation", 0.0)) or 0.0)) for row in rows)
        if row_total <= 0:
            row_total = float(len(rows))
        for row in rows:
            routed_row = dict(row)
            instance_id = str(routed_row.get("instance_id", "") or "")
            row_weight = max(0.0, float(routed_row.get("weight", routed_row.get("allocation", 0.0)) or 0.0))
            if row_weight <= 0:
                row_weight = 1.0
            normalized_weight = base_weight * (row_weight / row_total)
            routed_row["weight"] = round(normalized_weight, 6)
            routed_instances.append(routed_row)
            instance_weights[instance_id] = round(normalized_weight, 6)
            strategy_instance_map.setdefault(strategy_id, []).append(instance_id)
        strategy_weights[strategy_id] = round(base_weight, 6)
    try:
        _cached_health = get_intelligence_hub().refresh_all(Path.cwd(), routed_instances)
    except Exception:
        _cached_health = {}
    selection_rows: list[Dict[str, Any]] = []
    for routed_row in routed_instances:
        instance_id = str(routed_row.get("instance_id", "") or "")
        strategy_id = canonical_strategy_id(routed_row.get("strategy_id", ""))
        contract = get_strategy_runtime_contract(strategy_id)
        fit_result = evaluate_strategy_market_fit(contract, canonical_snapshot)
        runtime_compatible, runtime_reason = is_strategy_regime_compatible(
            strategy_id,
            route_regime,
            regime_confidence=route_regime_confidence,
            session_mode=route_session_bucket,
            route_family=route_family_val,
        )
        health = _cached_health.get(instance_id)
        health_score = float(getattr(health, "score", 50.0) or 50.0) / 100.0
        regime_fit = float(getattr(health, "current_regime_fitness", fit_result.get("fit_score", 0.0)) or fit_result.get("fit_score", 0.0))
        suggested_action = str(getattr(health, "suggested_action", "normal") or "normal")
        signal_score = round((float(fit_result.get("fit_score", 0.0) or 0.0) + regime_fit) / 2.0, 4)
        final_score = round(
            (float(fit_result.get("fit_score", 0.0) or 0.0) * 0.45)
            + (health_score * 0.30)
            + (regime_fit * 0.25),
            4,
        )
        selection_rows.append(
            {
                "instance_id": instance_id,
                "strategy_id": strategy_id,
                "eligible": bool(fit_result.get("eligible", False)) and runtime_compatible and suggested_action != "shadow_only",
                "route_family": str(route.get("route_family", "") or ""),
                "route_fit_score": round(
                    strategy_route_family_alignment(
                        strategy_id,
                        str(route.get("route_family", "") or ""),
                    ),
                    4,
                ),
                "route_priority": strategy_type_priority_for_route(
                    strategy_id,
                    str(route.get("route_family", "") or ""),
                ),
                "fit_score": round(float(fit_result.get("fit_score", 0.0) or 0.0), 4),
                "health_score": round(health_score, 4),
                "signal_score": signal_score,
                "final_score": final_score,
                "reasons": list(fit_result.get("reasons", [])),
                "runtime_compatible": bool(runtime_compatible),
                "runtime_reason": str(runtime_reason or ""),
                "suggested_action": suggested_action,
            }
        )
    route_family_value = str(route.get("route_family", "") or "")
    selection_payload = _select_route_order_strategy(
        ordered_strategies,
        selection_rows,
    )
    _trace_selection_state(
        Path.cwd(),
        "route_scheduler_snapshot",
        route,
        selection=selection_payload,
        extra={
            "ordered_strategies": list(ordered_strategies),
            "selection_rows_count": len(selection_rows),
        },
    )

    if not routed_instances:
        routed = dict(scheduler_snapshot)
        routed["route"] = {**route, "fallback": "empty_routed_instances"}
        return routed

    selection_mode = str(selection_payload.get("selection_mode", "single_best") or "single_best")
    selection_passed = bool(selection_payload.get("passed", False))
    selected_instance_id = str(selection_payload.get("winner_instance_id", "") or "")
    selected_strategy_id = str(selection_payload.get("winner_strategy_id", "") or "")
    if not selection_passed:
        selected_instance_id = ""
        selected_strategy_id = ""
    elif selection_mode == "route_order" and not (selected_instance_id and selected_strategy_id):
        ranked_candidates = (
            selection_payload.get("ranked_candidates", [])
            if isinstance(selection_payload.get("ranked_candidates", []), list)
            else []
        )
        advisory_top = ranked_candidates[0] if ranked_candidates else {}
        selected_instance_id = str(
            selected_instance_id
            or advisory_top.get("instance_id", "")
            or selection_payload.get("winner_instance_id", "")
            or ""
        )
        selected_strategy_id = str(
            selected_strategy_id
            or advisory_top.get("strategy_id", "")
            or selection_payload.get("winner_strategy_id", "")
            or ""
        )

    routed = dict(scheduler_snapshot)
    route["weights"] = strategy_route_weights
    route["routed_strategy_ids"] = list(ordered_strategies)
    route["selection"] = selection_payload
    route["entry_owner_instance_id"] = selected_instance_id
    route["entry_owner_strategy_id"] = selected_strategy_id
    routed.update(
        {
            "mode": "single_active" if len(routed_instances) == 1 else "multi_active",
            "arbiter": "route-order-selector",
            "reason": f"strategy_router:{str(route.get('reason', '') or 'ROUTE_APPLIED')}",
            "active_instance_ids": [str(item.get("instance_id", "") or "") for item in routed_instances],
            "active_strategy_ids": ordered_strategies,
            "instance_weights": instance_weights,
            "strategy_weights": strategy_weights,
            "strategy_instance_map": strategy_instance_map,
            "instances": routed_instances,
            "selected_instance_id": selected_instance_id,
            "selected_strategy_id": selected_strategy_id,
            "route": route,
        }
    )
    return routed


def _resolve_binding_direction(macro_context: Dict[str, Any]) -> str:
    binding_direction = str(
        macro_context.get(
            "binding_direction",
            macro_context.get("direction", macro_context.get("effective_direction", "NEUTRAL")),
        )
        or "NEUTRAL"
    ).upper()
    if binding_direction not in {"LONG", "SHORT", "NEUTRAL"}:
        return "NEUTRAL"
    return binding_direction


def _resolve_macro_authority_context(macro_context: Dict[str, Any], regime: str = "") -> str:
    regime_u = str(regime or macro_context.get("regime", "") or "").upper()
    freshness = _macro_freshness_state(macro_context)
    authority = str(macro_context.get("macro_authority", "") or "").upper()
    direction_guard = (
        macro_context.get("direction_guard", {})
        if isinstance(macro_context.get("direction_guard", {}), dict)
        else {}
    )
    guard_state = str(direction_guard.get("state", "inactive") or "inactive").lower()
    disempowered = bool(macro_context.get("direction_disempowered", False))
    # Only respect direction_guard.disempowered flag (set by neutralize_on_hard=True).
    # hard_penalty WITHOUT neutralize_on_hard should NOT disempower direction.
    if guard_state == "hard_penalty" and direction_guard.get("disempowered", False):
        disempowered = True
    if authority == "DISABLED" or disempowered:
        return "DISABLED"
    if authority == "SOFT_BIAS" or guard_state == "soft_penalty":
        return "SOFT_BIAS"
    if authority == "HARD_BIND" and guard_state not in {"soft_penalty", "hard_penalty"}:
        authority = "HARD_BIND"
    else:
        authority = resolve_macro_authority(
            str(
                macro_context.get(
                    "binding_direction",
                    macro_context.get("direction", macro_context.get("effective_direction", "NEUTRAL")),
                )
                or "NEUTRAL"
            ),
            state=str(macro_context.get("state", "UNKNOWN") or "UNKNOWN"),
            degraded=bool(macro_context.get("degraded", False)),
            bias_weight=_safe_float(macro_context.get("bias_weight", 0.0), 0.0),
            disempowered=disempowered,
        )
    if authority == "HARD_BIND" and (
        guard_state == "soft_penalty" or freshness.get("hard_bind_unsafe", False)
    ):
        authority = "SOFT_BIAS"
    if regime_u in ("RANGING", "TRANSITION"):
        agent_dir = _resolve_binding_direction(macro_context)
        if authority == "DISABLED":
            return "DISABLED"
        if agent_dir in {"LONG", "SHORT"} and not freshness.get("hard_bind_unsafe", False):
            return "HARD_BIND"
        return "SOFT_BIAS"
    return authority


def _resolve_execution_binding_direction(macro_context: Dict[str, Any]) -> str:
    if _resolve_macro_authority_context(macro_context) != "HARD_BIND":
        return "NEUTRAL"
    return _resolve_binding_direction(macro_context)


def _binding_side_conflicts(binding_direction: str, signal_side: str) -> bool:
    binding_upper = str(binding_direction or "NEUTRAL").upper()
    side_upper = str(signal_side or "NONE").upper()
    return (
        (binding_upper == "LONG" and side_upper == "SELL")
        or (binding_upper == "SHORT" and side_upper == "BUY")
    )


def _structural_side_conflicts(structural_side: str, signal_side: str) -> bool:
    structural_upper = str(structural_side or "NONE").upper()
    side_upper = str(signal_side or "NONE").upper()
    return structural_upper in {"BUY", "SELL"} and side_upper in {"BUY", "SELL"} and structural_upper != side_upper


def _resolve_structural_side(
    *,
    structural_side: str,
    dominant_direction: str,
    continuation_bias: str,
) -> str:
    side_upper = str(structural_side or "").upper()
    if side_upper in {"BUY", "SELL"}:
        return side_upper
    dominant_upper = str(dominant_direction or "").upper()
    continuation_upper = str(continuation_bias or "").upper()
    if dominant_upper == "BULLISH" and continuation_upper != "BEARISH":
        return "BUY"
    if dominant_upper == "BEARISH" and continuation_upper != "BULLISH":
        return "SELL"
    if continuation_upper == "BULLISH" and dominant_upper != "BEARISH":
        return "BUY"
    if continuation_upper == "BEARISH" and dominant_upper != "BULLISH":
        return "SELL"
    return "NONE"


def _build_structural_hard_conflict_guard(
    macro_context: Dict[str, Any],
    interpreter_context: Dict[str, Any],
) -> Dict[str, Any]:
    binding_direction = _resolve_binding_direction(macro_context)
    macro_authority = _resolve_macro_authority_context(macro_context)
    dominant_direction = str(interpreter_context.get("dominant_direction", "") or "").upper()
    continuation_bias = str(interpreter_context.get("continuation_bias", "") or "").upper()
    structural_side = _resolve_structural_side(
        structural_side=str(interpreter_context.get("structural_side", "") or ""),
        dominant_direction=dominant_direction,
        continuation_bias=continuation_bias,
    )
    execution_advice = str(interpreter_context.get("execution_advice", "ALLOW_FOLLOW") or "ALLOW_FOLLOW").upper()
    counter_trend_risk = str(interpreter_context.get("counter_trend_risk", "LOW") or "LOW").upper()
    same_side_votes = 0
    opposite_side_votes = 0
    if structural_side == "BUY":
        same_side_votes += int(dominant_direction == "BULLISH")
        same_side_votes += int(continuation_bias == "BULLISH")
        opposite_side_votes += int(dominant_direction == "BEARISH")
        opposite_side_votes += int(continuation_bias == "BEARISH")
    elif structural_side == "SELL":
        same_side_votes += int(dominant_direction == "BEARISH")
        same_side_votes += int(continuation_bias == "BEARISH")
        opposite_side_votes += int(dominant_direction == "BULLISH")
        opposite_side_votes += int(continuation_bias == "BULLISH")
    conflict_with_binding = _binding_side_conflicts(binding_direction, structural_side)
    macro_hard_bind_active = macro_authority == "HARD_BIND" and binding_direction in {"LONG", "SHORT"}
    ready = (
        structural_side in {"BUY", "SELL"}
        and execution_advice in STRUCTURAL_HARD_BLOCK_ADVICES
        and not macro_hard_bind_active
    )
    return {
        "enabled": ready,
        "macro_authority": macro_authority,
        "binding_direction": binding_direction,
        "structural_side": structural_side,
        "dominant_direction": dominant_direction,
        "continuation_bias": continuation_bias,
        "execution_advice": execution_advice,
        "counter_trend_risk": counter_trend_risk,
        "same_side_votes": same_side_votes,
        "opposite_side_votes": opposite_side_votes,
        "reason": (
            f"structural_side={structural_side or 'NONE'} binding={binding_direction} "
            f"authority={macro_authority} advice={execution_advice} risk={counter_trend_risk}"
        ),
    }


def _build_binding_rejection_outcome(
    ctx: PipelineRunContext,
    *,
    binding_direction: str,
    block_reason: str,
) -> PipelineOutcome:
    side = str(ctx.signal.get("side", "NONE") or "NONE").upper()
    ctx.signal["reject_reason"] = block_reason
    ctx.signal["binding_direction"] = binding_direction
    ctx.aggregated_intent["execution_decision"] = _synthesize_runtime_execution_decision(
        ctx,
        source="binding_rejection",
        fallback_reason=block_reason,
    )
    return PipelineOutcome(
        execution_report=_build_execution_report(
            ctx.signal_id,
            "REJECTED",
            block_reason,
            action=str(ctx.signal.get("action", "HOLD") or "HOLD").upper(),
            side=side,
            binding_direction=binding_direction,
        ),
        ledger_events=["macro_binding_blocked"],
        append_runtime_log=True,
    )


def _build_open_guard_rejection_outcome(
    ctx: PipelineRunContext,
    *,
    block_reason: str,
    guard_snapshot: Dict[str, Any],
) -> PipelineOutcome:
    side = str(ctx.signal.get("side", "NONE") or "NONE").upper()
    ctx.signal["reject_reason"] = block_reason
    ctx.signal["open_guard"] = dict(guard_snapshot)
    ctx.aggregated_intent["execution_decision"] = _synthesize_runtime_execution_decision(
        ctx,
        source="open_guard_rejection",
        fallback_reason=block_reason,
    )
    return PipelineOutcome(
        execution_report=_build_execution_report(
            ctx.signal_id,
            "REJECTED",
            block_reason,
            action=str(ctx.signal.get("action", "HOLD") or "HOLD").upper(),
            side=side,
            open_guard=dict(guard_snapshot),
        ),
        ledger_events=["runtime_open_guard_blocked"],
        append_runtime_log=True,
    )


def _default_session_scheduler_policy() -> Dict[str, Any]:
    return {
        "enabled": False,
        "mode": "soft_weight",
        "allowed_sessions": ["LONDON", "NEW_YORK", "OVERLAP"],
        "blocked_sessions": [],
        "default_when_disabled": {
            "session_as_soft_factor": True,
            "allow_new_entry": True,
        },
        "policies": {
            "ASIA": {
                "enabled": True,
                "allow_new_entry": False,
                "allow_add_position": False,
                "allow_reduce_only": True,
                "risk_scale": 0.3,
                "entry_mode": "observe_only",
                "allowed_strategy_groups": ["mean_reversion"],
                "allowed_strategy_ids": ["rsi_extreme", "boll_band", "key_level"],
                "soft_score_bias": -0.2,
                "min_confidence_override": 0.78,
            },
            "LONDON": {
                "enabled": True,
                "allow_new_entry": True,
                "allow_add_position": True,
                "allow_reduce_only": True,
                "risk_scale": 1.0,
                "entry_mode": "normal",
                "allowed_strategy_groups": ["trend", "breakout"],
                "allowed_strategy_ids": ["ema_macd", "pullback_fade", "adx_breakout", "wyckoff"],
                "soft_score_bias": 0.12,
                "min_confidence_override": 0.62,
            },
            "NEW_YORK": {
                "enabled": True,
                "allow_new_entry": True,
                "allow_add_position": True,
                "allow_reduce_only": True,
                "risk_scale": 0.9,
                "entry_mode": "normal",
                "allowed_strategy_groups": ["trend", "momentum"],
                "allowed_strategy_ids": ["ema_macd", "wyckoff", "composite"],
                "soft_score_bias": 0.08,
                "min_confidence_override": 0.64,
            },
            "OVERLAP": {
                "enabled": True,
                "allow_new_entry": True,
                "allow_add_position": True,
                "allow_reduce_only": True,
                "risk_scale": 1.0,
                "entry_mode": "aggressive",
                "allowed_strategy_groups": ["trend", "breakout", "momentum"],
                "allowed_strategy_ids": ["ema_macd", "pullback_fade", "wyckoff", "composite"],
                "soft_score_bias": 0.15,
                "min_confidence_override": 0.60,
            },
        },
    }


def _default_decision_policy() -> Dict[str, Any]:
    return {
        "policy_version": "1",
        "profile": "agent_guided_multi_strategy",
        "macro": {
            "role": "directional_gate",
            "degraded_policy": "soften_to_neutral",
            "aligned_scale": 1.1,
            "countertrend_scale": 0.35,
            "neutral_scale": 1.0,
            "direction_loss_guard": {
                "enabled": True,
                "lookback": 8,
                "min_samples": 3,
                "aligned_loss_streak_soft": 5,
                "aligned_loss_streak_hard": 8,
                "counter_win_bias_delta": 0.15,
                "soft_scale": 0.55,
                "hard_scale": 0.2,
                "neutralize_on_hard": False,
            },
        },
        "interpreter": {
            "role": "execution_bias_only",
            "buy_scale": 1.0,
            "sell_scale": 1.0,
            "limit_countertrend_scale": 0.5,
            "block_countertrend_scale": 0.0,
        },
        "risk": {
            "role": "active_governor",
            "per_instance": {
                "health_scoring": {
                    "enabled": True,
                    "min_trades_for_scoring": 3,
                    "score_ttl_sec": 30,
                },
                "loss_guard": {
                    "enabled": True,
                    "soft_loss_streak": 2,
                    "hard_loss_streak": 4,
                    "shadow_loss_streak": 5,
                    "soft_scale": 0.55,
                    "hard_scale": 0.25,
                    "rehab_min_trades": 5,
                    "rehab_min_win_rate": 0.50,
                },
                "regime_fitness": {
                    "enabled": True,
                    "min_samples_per_regime": 2,
                    "unfamiliar_regime_scale": 0.6,
                },
            },
            "cross_strategy": {
                "correlation_tracking": {
                    "enabled": True,
                    "divergence_confidence_penalty": 0.4,
                    "divergence_volume_penalty": 0.6,
                    "divergence_cooldown_sec": 120,
                    "alignment_volume_bonus": 0.05,
                },
            },
            "risk_budget": {
                "allocation_method": "health_squared",
                "min_lot_per_instance": 0.01,
                "max_lot_per_instance": 0.20,
            },
        },
        "execution": {
            "aggregation": "weighted_net_confidence",
            "min_confidence": DEFAULT_EXECUTION_MIN_CONFIDENCE,
            "entry_policy": "regime_directed",
            "advice_matrix": _default_execution_advice_matrix(),
        },
        "session_scheduler": _default_session_scheduler_policy(),
        "router": {
            "trend": {
                "primary_strategy_id": "ema_macd",
                "support_strategy_ids": ["pullback_fade", "adx_breakout", "wyckoff"],
                "rank_weights": [0.4, 0.25, 0.2, 0.15],
            },
            "range": {
                "primary_strategy_id": "rsi_extreme",
                "support_strategy_ids": ["boll_band", "key_level", "grid_osc"],
                "rank_weights": [0.35, 0.3, 0.2, 0.15],
            },
            "uncertain": {
                "primary_strategy_id": "composite",
                "support_strategy_ids": ["wyckoff", "key_level", "ema_macd"],
                "rank_weights": [0.4, 0.25, 0.2, 0.15],
            },
        },
    }


def _normalize_decision_policy(raw: Dict[str, Any]) -> Dict[str, Any]:
    base = _default_decision_policy()
    payload = raw if isinstance(raw, dict) else {}
    macro = payload.get("macro", {}) if isinstance(payload.get("macro", {}), dict) else {}
    interpreter = payload.get("interpreter", {}) if isinstance(payload.get("interpreter", {}), dict) else {}
    risk = payload.get("risk", {}) if isinstance(payload.get("risk", {}), dict) else {}
    execution = payload.get("execution", {}) if isinstance(payload.get("execution", {}), dict) else {}
    session_scheduler = (
        payload.get("session_scheduler", {})
        if isinstance(payload.get("session_scheduler", {}), dict)
        else {}
    )
    router = payload.get("router", {}) if isinstance(payload.get("router", {}), dict) else {}
    macro_guard = (
        macro.get("direction_loss_guard", {})
        if isinstance(macro.get("direction_loss_guard", {}), dict)
        else {}
    )

    def _normalize_string_list(value: Any, fallback: list[str]) -> list[str]:
        if not isinstance(value, list):
            return list(fallback)
        items = [str(item).strip().upper() for item in value if str(item).strip()]
        return list(dict.fromkeys(items)) or list(fallback)

    def _normalize_session_policy(raw_policy: Any, base_policy: Dict[str, Any]) -> Dict[str, Any]:
        policy = raw_policy if isinstance(raw_policy, dict) else {}
        return {
            "enabled": _coerce_bool(policy.get("enabled", base_policy["enabled"]), base_policy["enabled"]),
            "allow_new_entry": _coerce_bool(
                policy.get("allow_new_entry", base_policy["allow_new_entry"]),
                base_policy["allow_new_entry"],
            ),
            "allow_add_position": _coerce_bool(
                policy.get("allow_add_position", base_policy["allow_add_position"]),
                base_policy["allow_add_position"],
            ),
            "allow_reduce_only": _coerce_bool(
                policy.get("allow_reduce_only", base_policy["allow_reduce_only"]),
                base_policy["allow_reduce_only"],
            ),
            "risk_scale": _bounded_float(policy.get("risk_scale", base_policy["risk_scale"]), base_policy["risk_scale"]),
            "entry_mode": str(policy.get("entry_mode", base_policy["entry_mode"]) or base_policy["entry_mode"]),
            "allowed_strategy_groups": _normalize_string_list(
                policy.get("allowed_strategy_groups", base_policy["allowed_strategy_groups"]),
                list(base_policy["allowed_strategy_groups"]),
            ),
            "allowed_strategy_ids": _normalize_string_list(
                policy.get("allowed_strategy_ids", base_policy["allowed_strategy_ids"]),
                list(base_policy["allowed_strategy_ids"]),
            ),
            "soft_score_bias": _bounded_float(
                policy.get("soft_score_bias", base_policy["soft_score_bias"]),
                base_policy["soft_score_bias"],
                low=-1.0,
                high=1.0,
            ),
            "min_confidence_override": _bounded_float(
                policy.get("min_confidence_override", base_policy["min_confidence_override"]),
                base_policy["min_confidence_override"],
                low=0.0,
                high=1.0,
            ),
        }

    return {
        "policy_version": str(payload.get("policy_version", base["policy_version"]) or base["policy_version"]),
        "profile": str(payload.get("profile", base["profile"]) or base["profile"]),
        "macro": {
            "role": str(macro.get("role", base["macro"]["role"]) or base["macro"]["role"]),
            "degraded_policy": str(
                macro.get("degraded_policy", base["macro"]["degraded_policy"])
                or base["macro"]["degraded_policy"]
            ),
            "aligned_scale": _bounded_float(
                macro.get("aligned_scale", base["macro"]["aligned_scale"]),
                base["macro"]["aligned_scale"],
            ),
            "countertrend_scale": _bounded_float(
                macro.get("countertrend_scale", base["macro"]["countertrend_scale"]),
                base["macro"]["countertrend_scale"],
            ),
            "neutral_scale": _bounded_float(
                macro.get("neutral_scale", base["macro"]["neutral_scale"]),
                base["macro"]["neutral_scale"],
            ),
            "direction_loss_guard": {
                "enabled": _coerce_bool(
                    macro_guard.get("enabled", base["macro"]["direction_loss_guard"]["enabled"]),
                    base["macro"]["direction_loss_guard"]["enabled"],
                ),
                "lookback": int(
                    max(
                        1,
                        min(
                            40,
                            int(float(macro_guard.get("lookback", base["macro"]["direction_loss_guard"]["lookback"]) or base["macro"]["direction_loss_guard"]["lookback"])),
                        ),
                    )
                ),
                "min_samples": int(
                    max(
                        1,
                        min(
                            20,
                            int(float(macro_guard.get("min_samples", base["macro"]["direction_loss_guard"]["min_samples"]) or base["macro"]["direction_loss_guard"]["min_samples"])),
                        ),
                    )
                ),
                "aligned_loss_streak_soft": int(
                    max(
                        1,
                        min(
                            10,
                            int(float(macro_guard.get("aligned_loss_streak_soft", base["macro"]["direction_loss_guard"]["aligned_loss_streak_soft"]) or base["macro"]["direction_loss_guard"]["aligned_loss_streak_soft"])),
                        ),
                    )
                ),
                "aligned_loss_streak_hard": int(
                    max(
                        1,
                        min(
                            10,
                            int(float(macro_guard.get("aligned_loss_streak_hard", base["macro"]["direction_loss_guard"]["aligned_loss_streak_hard"]) or base["macro"]["direction_loss_guard"]["aligned_loss_streak_hard"])),
                        ),
                    )
                ),
                "counter_win_bias_delta": _bounded_float(
                    macro_guard.get("counter_win_bias_delta", base["macro"]["direction_loss_guard"]["counter_win_bias_delta"]),
                    base["macro"]["direction_loss_guard"]["counter_win_bias_delta"],
                    low=0.0,
                    high=1.0,
                ),
                "soft_scale": _bounded_float(
                    macro_guard.get("soft_scale", base["macro"]["direction_loss_guard"]["soft_scale"]),
                    base["macro"]["direction_loss_guard"]["soft_scale"],
                    low=0.0,
                    high=1.0,
                ),
                "hard_scale": _bounded_float(
                    macro_guard.get("hard_scale", base["macro"]["direction_loss_guard"]["hard_scale"]),
                    base["macro"]["direction_loss_guard"]["hard_scale"],
                    low=0.0,
                    high=1.0,
                ),
                "neutralize_on_hard": _coerce_bool(
                    macro_guard.get("neutralize_on_hard", base["macro"]["direction_loss_guard"]["neutralize_on_hard"]),
                    base["macro"]["direction_loss_guard"]["neutralize_on_hard"],
                ),
            },
        },
        "interpreter": {
            "role": str(interpreter.get("role", base["interpreter"]["role"]) or base["interpreter"]["role"]),
            "buy_scale": _bounded_float(
                interpreter.get("buy_scale", base["interpreter"]["buy_scale"]),
                base["interpreter"]["buy_scale"],
            ),
            "sell_scale": _bounded_float(
                interpreter.get("sell_scale", base["interpreter"]["sell_scale"]),
                base["interpreter"]["sell_scale"],
            ),
            "limit_countertrend_scale": _bounded_float(
                interpreter.get(
                    "limit_countertrend_scale",
                    base["interpreter"]["limit_countertrend_scale"],
                ),
                base["interpreter"]["limit_countertrend_scale"],
            ),
            "block_countertrend_scale": _bounded_float(
                interpreter.get(
                    "block_countertrend_scale",
                    base["interpreter"]["block_countertrend_scale"],
                ),
                base["interpreter"]["block_countertrend_scale"],
            ),
        },
        "risk": {
            "role": str(risk.get("role", base["risk"]["role"]) or base["risk"]["role"]),
            "per_instance": {
                "health_scoring": {
                    "enabled": _coerce_bool(
                        risk.get("per_instance", {}).get("health_scoring", {}).get("enabled", True),
                        True,
                    ),
                    "min_trades_for_scoring": int(float(
                        risk.get("per_instance", {}).get("health_scoring", {}).get("min_trades_for_scoring", 3) or 3
                    )),
                    "score_ttl_sec": int(float(
                        risk.get("per_instance", {}).get("health_scoring", {}).get("score_ttl_sec", 30) or 30
                    )),
                },
                "loss_guard": {
                    "enabled": _coerce_bool(
                        risk.get("per_instance", {}).get("loss_guard", {}).get("enabled", True),
                        True,
                    ),
                    "soft_loss_streak": int(float(
                        risk.get("per_instance", {}).get("loss_guard", {}).get("soft_loss_streak", 2) or 2
                    )),
                    "hard_loss_streak": int(float(
                        risk.get("per_instance", {}).get("loss_guard", {}).get("hard_loss_streak", 4) or 4
                    )),
                    "shadow_loss_streak": int(float(
                        risk.get("per_instance", {}).get("loss_guard", {}).get("shadow_loss_streak", 5) or 5
                    )),
                    "soft_scale": _bounded_float(
                        risk.get("per_instance", {}).get("loss_guard", {}).get("soft_scale", 0.55),
                        0.55, low=0.0, high=1.0,
                    ),
                    "hard_scale": _bounded_float(
                        risk.get("per_instance", {}).get("loss_guard", {}).get("hard_scale", 0.25),
                        0.25, low=0.0, high=1.0,
                    ),
                    "rehab_min_trades": int(float(
                        risk.get("per_instance", {}).get("loss_guard", {}).get("rehab_min_trades", 5) or 5
                    )),
                    "rehab_min_win_rate": _bounded_float(
                        risk.get("per_instance", {}).get("loss_guard", {}).get("rehab_min_win_rate", 0.50),
                        0.50, low=0.0, high=1.0,
                    ),
                },
                "regime_fitness": {
                    "enabled": _coerce_bool(
                        risk.get("per_instance", {}).get("regime_fitness", {}).get("enabled", True),
                        True,
                    ),
                    "min_samples_per_regime": int(float(
                        risk.get("per_instance", {}).get("regime_fitness", {}).get("min_samples_per_regime", 2) or 2
                    )),
                    "unfamiliar_regime_scale": _bounded_float(
                        risk.get("per_instance", {}).get("regime_fitness", {}).get("unfamiliar_regime_scale", 0.6),
                        0.6, low=0.0, high=1.0,
                    ),
                },
            },
            "cross_strategy": {
                "correlation_tracking": {
                    "enabled": _coerce_bool(
                        risk.get("cross_strategy", {}).get("correlation_tracking", {}).get("enabled", True),
                        True,
                    ),
                    "divergence_confidence_penalty": _bounded_float(
                        risk.get("cross_strategy", {}).get("correlation_tracking", {}).get("divergence_confidence_penalty", 0.4),
                        0.4, low=0.0, high=1.0,
                    ),
                    "divergence_volume_penalty": _bounded_float(
                        risk.get("cross_strategy", {}).get("correlation_tracking", {}).get("divergence_volume_penalty", 0.6),
                        0.6, low=0.0, high=1.0,
                    ),
                    "divergence_cooldown_sec": int(float(
                        risk.get("cross_strategy", {}).get("correlation_tracking", {}).get("divergence_cooldown_sec", 120) or 120
                    )),
                    "alignment_volume_bonus": _bounded_float(
                        risk.get("cross_strategy", {}).get("correlation_tracking", {}).get("alignment_volume_bonus", 0.05),
                        0.05, low=0.0, high=0.5,
                    ),
                },
            },
            "risk_budget": {
                "allocation_method": str(
                    risk.get("risk_budget", {}).get("allocation_method", "health_squared") or "health_squared"
                ),
                "min_lot_per_instance": _bounded_float(
                    risk.get("risk_budget", {}).get("min_lot_per_instance", 0.01),
                    0.01, low=0.0, high=0.10,
                ),
                "max_lot_per_instance": _bounded_float(
                    risk.get("risk_budget", {}).get("max_lot_per_instance", 0.20),
                    0.20, low=0.0, high=10.0,
                ),
            },
        },
        "execution": {
            "aggregation": str(
                execution.get("aggregation", base["execution"]["aggregation"])
                or base["execution"]["aggregation"]
            ),
            "min_confidence": _bounded_float(
                execution.get("min_confidence", base["execution"]["min_confidence"]),
                base["execution"]["min_confidence"],
                low=0.0,
                high=1.0,
            ),
            "entry_policy": str(
                execution.get("entry_policy", base["execution"]["entry_policy"])
                or base["execution"]["entry_policy"]
            ),
            "advice_matrix": _normalize_execution_advice_matrix(
                execution.get("advice_matrix", base["execution"]["advice_matrix"])
            ),
        },
        "session_scheduler": {
            "enabled": _coerce_bool(
                session_scheduler.get("enabled", base["session_scheduler"]["enabled"]),
                base["session_scheduler"]["enabled"],
            ),
            "mode": str(
                session_scheduler.get("mode", base["session_scheduler"]["mode"])
                or base["session_scheduler"]["mode"]
            ),
            "allowed_sessions": _normalize_string_list(
                session_scheduler.get("allowed_sessions", base["session_scheduler"]["allowed_sessions"]),
                list(base["session_scheduler"]["allowed_sessions"]),
            ),
            "blocked_sessions": _normalize_string_list(
                session_scheduler.get("blocked_sessions", base["session_scheduler"]["blocked_sessions"]),
                list(base["session_scheduler"]["blocked_sessions"]),
            ),
            "default_when_disabled": {
                "session_as_soft_factor": _coerce_bool(
                    session_scheduler.get("default_when_disabled", {}).get(
                        "session_as_soft_factor",
                        base["session_scheduler"]["default_when_disabled"]["session_as_soft_factor"],
                    ),
                    base["session_scheduler"]["default_when_disabled"]["session_as_soft_factor"],
                ),
                "allow_new_entry": _coerce_bool(
                    session_scheduler.get("default_when_disabled", {}).get(
                        "allow_new_entry",
                        base["session_scheduler"]["default_when_disabled"]["allow_new_entry"],
                    ),
                    base["session_scheduler"]["default_when_disabled"]["allow_new_entry"],
                ),
            },
            "policies": {
                session_name: _normalize_session_policy(
                    session_scheduler.get("policies", {}).get(session_name, {}),
                    base["session_scheduler"]["policies"][session_name],
                )
                for session_name in ("ASIA", "LONDON", "NEW_YORK", "OVERLAP")
            },
        },
        "router": {
            "trend": _normalize_router_family(
                router.get("trend", {}),
                primary_strategy_id=base["router"]["trend"]["primary_strategy_id"],
                support_strategy_ids=base["router"]["trend"]["support_strategy_ids"],
                rank_weights=base["router"]["trend"]["rank_weights"],
            ),
            "range": _normalize_router_family(
                router.get("range", {}),
                primary_strategy_id=base["router"]["range"]["primary_strategy_id"],
                support_strategy_ids=base["router"]["range"]["support_strategy_ids"],
                rank_weights=base["router"]["range"]["rank_weights"],
            ),
            "uncertain": _normalize_router_family(
                router.get("uncertain", {}),
                primary_strategy_id=base["router"]["uncertain"]["primary_strategy_id"],
                support_strategy_ids=base["router"]["uncertain"]["support_strategy_ids"],
                rank_weights=base["router"]["uncertain"]["rank_weights"],
            ),
        },
    }


def _apply_panel_risk_floor(policy: Dict[str, Any]) -> Dict[str, Any]:
    """Sync execution min_confidence to the effective panel risk setting."""
    panel_min_conf = config_manager.get_effective_min_confidence_threshold(
        DEFAULT_EXECUTION_MIN_CONFIDENCE
    )
    exec_block = (
        policy.get("execution", {})
        if isinstance(policy.get("execution", {}), dict)
        else {}
    )
    if isinstance(policy.get("execution"), dict):
        policy["execution"]["min_confidence"] = panel_min_conf
    else:
        policy["execution"] = {
            "min_confidence": panel_min_conf,
            "aggregation": "weighted_net_confidence",
            "entry_policy": "regime_directed",
        }

    # 鈹€鈹€ max_consecutive_losses (hard_loss_streak) 鈹€鈹€
    panel_max_consec = config_manager.get_effective_param("max_consecutive_losses", None)
    if panel_max_consec is not None:
        panel_max_consec = int(panel_max_consec)
        loss_guard = {}
        if (isinstance(policy.get("risk"), dict) and
            isinstance(policy["risk"].get("per_instance"), dict) and
            isinstance(policy["risk"]["per_instance"].get("loss_guard"), dict)):
            loss_guard = policy["risk"]["per_instance"]["loss_guard"]
        contract_hard = int(loss_guard.get("hard_loss_streak", 4) or 4)
        effective_hard = min(panel_max_consec, contract_hard)
        effective_soft = max(1, effective_hard - 1)
        if (isinstance(policy.get("risk"), dict) and
            isinstance(policy["risk"].get("per_instance"), dict) and
            isinstance(policy["risk"]["per_instance"].get("loss_guard"), dict)):
            policy["risk"]["per_instance"]["loss_guard"]["hard_loss_streak"] = effective_hard
            policy["risk"]["per_instance"]["loss_guard"]["soft_loss_streak"] = effective_soft
            if "shadow_loss_streak" in policy["risk"]["per_instance"]["loss_guard"]:
                policy["risk"]["per_instance"]["loss_guard"]["shadow_loss_streak"] = effective_hard + 1

    # 鈹€鈹€ spread_kill 鈹€鈹€
    panel_spread_kill = config_manager.get_effective_param("spread_kill", None)
    if panel_spread_kill is not None:
        policy.setdefault("_panel", {})
        policy["_panel"]["spread_kill"] = float(panel_spread_kill)

    return policy


def _load_agent_decision_context(project_root: Path, scheduler_snapshot: Dict[str, Any]) -> Dict[str, Any]:
    contract_payload = load_agent_decision_contract()
    policy_source = (
        scheduler_snapshot.get("decision_policy", {})
        if isinstance(scheduler_snapshot.get("decision_policy", {}), dict)
        and scheduler_snapshot.get("decision_policy", {})
        else contract_payload.get("decision_policy", {})
    )
    policy = _normalize_decision_policy(policy_source)
    policy = _apply_panel_risk_floor(policy)
    macro_payload = (
        contract_payload.get("macro", {})
        if isinstance(contract_payload.get("macro", {}), dict)
        else {}
    )
    interpreter_payload = (
        contract_payload.get("interpreter", {})
        if isinstance(contract_payload.get("interpreter", {}), dict)
        else {}
    )
    dominant_direction = str(interpreter_payload.get("dominant_direction", "") or "").upper()
    continuation_bias = str(interpreter_payload.get("continuation_bias", "") or "").upper()
    structural_side = ""
    if dominant_direction == "BULLISH" or continuation_bias == "BULLISH":
        structural_side = "BUY"
    elif dominant_direction == "BEARISH" or continuation_bias == "BEARISH":
        structural_side = "SELL"
    macro_state = _compute_macro_bias(macro_payload, policy)
    direction_guard = _compute_direction_loss_guard(project_root, macro_state, policy)
    bias_before_guard = _safe_float(macro_state.get("bias_weight", 1.0), 1.0)
    macro_state["bias_weight_before_guard"] = round(bias_before_guard, 4)
    direction_guard_scale = _safe_float(direction_guard.get("scale", 1.0), 1.0)
    macro_state["bias_weight"] = round(
        max(0.0, min(1.0, bias_before_guard * direction_guard_scale)),
        4,
    )
    macro_state["effective_direction"] = str(
        direction_guard.get(
            "effective_direction",
            macro_state.get("effective_direction", "NEUTRAL"),
        )
        or "NEUTRAL"
    ).upper()
    macro_state["direction_guard"] = direction_guard
    macro_state["direction_disempowered"] = bool(direction_guard.get("disempowered", False))
    macro_state["macro_authority"] = _resolve_macro_authority_context(
        {
            **macro_state,
            "direction_guard": direction_guard,
        }
    )
    # 鈹€鈹€ 瀹忚鏂瑰悜杩囨湡闄嶇骇锛堟渶缁堜紭鍏堬級锛欳ommander 瓒呮椂鏈埛鏂?鈫?鍓ュず鏂瑰悜绾︽潫鍔?鈹€鈹€
    macro_updated_ts = _safe_float(
        macro_payload.get("updated_ts", macro_payload.get("ts", 0)) or macro_payload.get("ts", 0),
        0,
    )
    macro_age_sec = time.time() - macro_updated_ts if macro_updated_ts > 0 else 99999
    if macro_age_sec > MACRO_FRESH_TTL_SECONDS and macro_state.get("macro_authority") == "HARD_BIND":
        macro_state["stale"] = True
        macro_state["stale_age_min"] = round(macro_age_sec / 60, 0)
        macro_state["degraded"] = True
        macro_state["macro_authority"] = "SOFT_BIAS"
        macro_state["effective_direction"] = "NEUTRAL"
        macro_state["binding_direction"] = "NEUTRAL"
    macro_state["allowed_side"] = resolve_allowed_side(
        str(macro_state.get("binding_direction", macro_state.get("direction", "NEUTRAL")) or "NEUTRAL"),
        str(macro_state.get("macro_authority", "DISABLED") or "DISABLED"),
    )
    # 鈹€鈹€ 鍐欏叆 direction_quality 鍒?contract锛岃 Commander 鎰熺煡琚灦绌虹姸鎬?鈹€鈹€
    if macro_state["direction_disempowered"]:
        try:
            from agent_decision_contract import update_macro_direction_quality
            update_macro_direction_quality({
                "disempowered": True,
                "disempower_reason": str(direction_guard.get("reason", "") or ""),
                "aligned_loss_streak": int(direction_guard.get("aligned_loss_streak", 0) or 0),
                "aligned_hit_rate": float(direction_guard.get("aligned_hit_rate", 0.0) or 0.0),
                "counter_hit_rate": float(direction_guard.get("counter_hit_rate", 0.0) or 0.0),
                "disempowered_direction": str(macro_state.get("direction", "NEUTRAL") or "NEUTRAL"),
            })
        except Exception:
            pass
    else:
        try:
            from agent_decision_contract import update_macro_direction_quality
            update_macro_direction_quality({
                "disempowered": False,
                "aligned_loss_streak": int(direction_guard.get("aligned_loss_streak", 0) or 0),
                "aligned_hit_rate": float(direction_guard.get("aligned_hit_rate", 0.0) or 0.0),
                "counter_hit_rate": float(direction_guard.get("counter_hit_rate", 0.0) or 0.0),
            })
        except Exception:
            pass
    structural_guard = _build_structural_hard_conflict_guard(
        macro_state,
        {
            "structural_side": structural_side,
            "dominant_direction": dominant_direction,
            "continuation_bias": continuation_bias,
            "execution_advice": str(interpreter_payload.get("execution_advice", "ALLOW_FOLLOW") or "ALLOW_FOLLOW").upper(),
            "counter_trend_risk": str(interpreter_payload.get("counter_trend_risk", "LOW") or "LOW").upper(),
        },
    )
    return {
        "decision_policy": policy,
        "contract": {
            "updated_at": str(contract_payload.get("updated_at", "") or ""),
            "lineage": (
                contract_payload.get("lineage", {})
                if isinstance(contract_payload.get("lineage", {}), dict)
                else {}
            ),
        },
        "macro": macro_state,
        "interpreter": {
            "execution_advice": str(interpreter_payload.get("execution_advice", "ALLOW_FOLLOW") or "ALLOW_FOLLOW").upper(),
            "buy_multiplier": float(interpreter_payload.get("buy_multiplier", 1.0) or 1.0),
            "sell_multiplier": float(interpreter_payload.get("sell_multiplier", 1.0) or 1.0),
            "structural_side": structural_side,
            "dominant_direction": dominant_direction,
            "continuation_bias": continuation_bias,
            "market_structure": _normalize_regime_label(interpreter_payload.get("market_regime", interpreter_payload.get("market_structure", ""))),
            "regime_subtype": _normalize_regime_label(interpreter_payload.get("regime_subtype", "")),
            "counter_trend_risk": str(interpreter_payload.get("counter_trend_risk", "LOW") or "LOW").upper(),
            "entry_timing_hint": str(interpreter_payload.get("entry_timing_hint", "") or "").upper(),
            "llm_confidence": float(interpreter_payload.get("llm_confidence", config_manager.get_interpreter_fallback_confidence()) or config_manager.get_interpreter_fallback_confidence()),
            "hard_conflict_guard": structural_guard,
        },
    }


def _apply_agent_decision_policy(
    strategy_signals: list[Dict[str, Any]],
    agent_context: Dict[str, Any],
) -> list[Dict[str, Any]]:
    policy = _normalize_decision_policy(agent_context.get("decision_policy", {}))
    macro_context = agent_context.get("macro", {}) if isinstance(agent_context.get("macro", {}), dict) else {}
    interpreter_context = (
        agent_context.get("interpreter", {})
        if isinstance(agent_context.get("interpreter", {}), dict)
        else {}
    )
    effective_macro_direction = str(macro_context.get("effective_direction", "NEUTRAL") or "NEUTRAL").upper()
    binding_macro_direction = _resolve_binding_direction(macro_context)
    macro_authority = _resolve_macro_authority_context(macro_context)
    macro_bias_weight = _safe_float(macro_context.get("bias_weight", 1.0), 1.0)
    macro_role = str(policy.get("macro", {}).get("role", "directional_gate") or "directional_gate")
    interpreter_role = str(policy.get("interpreter", {}).get("role", "execution_bias_only") or "execution_bias_only")
    execution_advice = str(interpreter_context.get("execution_advice", "ALLOW_FOLLOW") or "ALLOW_FOLLOW").upper()
    structural_side = str(interpreter_context.get("structural_side", "") or "").upper()
    min_confidence = float(
        policy.get("execution", {}).get(
            "min_confidence",
            config_manager.get_effective_min_confidence_threshold(
                DEFAULT_EXECUTION_MIN_CONFIDENCE
            ),
        )
        or config_manager.get_effective_min_confidence_threshold(
            DEFAULT_EXECUTION_MIN_CONFIDENCE
        )
    )

    for signal in strategy_signals:
        action = str(signal.get("action", "HOLD") or "HOLD").upper()
        side = str(signal.get("side", "NONE") or "NONE").upper()
        trace = signal.get("trace", {}) if isinstance(signal.get("trace", {}), dict) else {}
        evidence = trace.get("evidence", []) if isinstance(trace.get("evidence", []), list) else []
        raw_weight = float(signal.get("instance_weight", 0.0) or 0.0)
        adjusted_weight = raw_weight
        raw_confidence = float(trace.get("confidence", 0.0) or 0.0)
        confidence = raw_confidence
        effects: list[str] = []

        if action == "OPEN" and side in {"BUY", "SELL"}:
            # Skip macro directional scaling entirely when authority is DISABLED
            macro_authority_check = str(macro_context.get("macro_authority", "") or "").upper()
            if macro_authority_check == "DISABLED":
                pass  # no macro influence 鈥?don't adjust weights
            elif macro_role in {"directional_gate", "directional_hard_block"}:
                aligned = (
                    (effective_macro_direction == "LONG" and side == "BUY")
                    or (effective_macro_direction == "SHORT" and side == "SELL")
                )
                countertrend = (
                    (effective_macro_direction == "LONG" and side == "SELL")
                    or (effective_macro_direction == "SHORT" and side == "BUY")
                )
                macro_policy = policy.get("macro", {})
                aligned_scale = 1.0  # default: no change
                if isinstance(macro_policy, dict):
                    if aligned:
                        aligned_scale = 1.0 + ((float(macro_policy.get("aligned_scale", 1.2)) - 1.0) * macro_bias_weight)
                        adjusted_weight *= aligned_scale
                        effects.append("macro_aligned_boost")
                    elif countertrend:
                        # 鈹€鈹€ Counter-direction: penalize weight, never hard-block 鈹€鈹€
                        # Macro direction is a BIAS, not a LOCK. Counter-direction
                        # signals get scaled down but are NOT zeroed out.
                        if macro_role == "directional_hard_block":
                            # Heavy penalty for counter-direction under strong macro
                            ct_scale = 0.25  # was hard block 鈫?now 0.25x weight floor
                        else:
                            ct_scale = float(macro_policy.get("countertrend_scale", 0.5))
                        countertrend_scale = 1.0 - ((1.0 - ct_scale) * macro_bias_weight)
                        adjusted_weight *= countertrend_scale
                        # Mark signal with penalty for downstream sizing
                        signal["_counter_direction_penalty"] = True
                        effects.append("macro_countertrend_scaled")
                    else:
                        neutral_scale = float(macro_policy.get("neutral_scale", 0.8))
                        adjusted_weight *= neutral_scale
                interp_policy = policy.get("interpreter", {})
                if isinstance(interp_policy, dict):
                    if interpreter_role == "execution_bias_only":
                        base_scale = float(interp_policy.get("buy_scale", 1.0) if side == "BUY" else interp_policy.get("sell_scale", 1.0))
                        runtime_scale = float(
                            interpreter_context.get("buy_multiplier", 1.0)
                            if side == "BUY"
                            else interpreter_context.get("sell_multiplier", 1.0)
                        )
                        signal["interpreter_position_multiplier"] = round(max(0.05, base_scale * runtime_scale), 4)
                        if structural_side and side != structural_side:
                            if execution_advice == "BLOCK_COUNTER_TREND":
                                adjusted_weight *= float(interp_policy.get("block_countertrend_scale", 0.3))
                                effects.append("interpreter_block_countertrend")
                            elif execution_advice in {"LIMIT_ONLY", "WAIT_CONFIRMATION"}:
                                adjusted_weight *= float(interp_policy.get("limit_countertrend_scale", 0.5))
                                effects.append("interpreter_limit_countertrend")

        adjusted_weight = round(max(0.0, adjusted_weight), 6)
        trace["confidence"] = round(max(0.0, min(0.99, confidence)), 4)
        trace["policy"] = {
            "profile": str(policy.get("profile", "") or ""),
            "macro_direction": str(macro_context.get("direction", "NEUTRAL") or "NEUTRAL"),
            "effective_macro_direction": effective_macro_direction,
            "binding_macro_direction": binding_macro_direction,
            "macro_authority": macro_authority,
            "macro_bias_weight": round(macro_bias_weight, 4),
            "macro_state": str(macro_context.get("state", "UNKNOWN") or "UNKNOWN"),
            "execution_advice": execution_advice,
            "structural_side": structural_side,
            "raw_instance_weight": round(raw_weight, 6),
            "adjusted_instance_weight": adjusted_weight,
            "effects": effects,
        }
        for item in effects:
            if item not in evidence:
                evidence.append(item)
        if action == "OPEN" and adjusted_weight <= 0.0 and not str(signal.get("reject_reason", "") or "").strip():
            signal["action"] = "HOLD"
            signal["side"] = "NONE"
            signal["reject_reason"] = "AGENT_POLICY_BLOCKED"
        trace["evidence"] = evidence
        signal["trace"] = trace
        signal["raw_instance_weight"] = round(raw_weight, 6)
        signal["instance_weight"] = adjusted_weight
        signal["_policy_effects"] = effects
        signal["_effective_min_confidence"] = min_confidence
    return strategy_signals


def _signal_signed_score(signal: Dict[str, Any], instance_weight: float) -> float:
    action = str(signal.get("action", "HOLD") or "HOLD").upper()
    side = str(signal.get("side", "NONE") or "NONE").upper()
    confidence = float((signal.get("trace", {}) or {}).get("confidence", 0.0) or 0.0)
    if action == "HOLD" or side not in {"BUY", "SELL"}:
        return 0.0
    return round(confidence * (1.0 if side == "BUY" else -1.0) * max(0.0, instance_weight), 6)


def _is_actionable_signal(signal: Dict[str, Any]) -> bool:
    action = str(signal.get("action", "HOLD") or "HOLD").upper()
    side = str(signal.get("side", "NONE") or "NONE").upper()
    return action != "HOLD" and side in {"BUY", "SELL"}


def _strategy_opportunity_to_dict(opportunity: StrategyOpportunity) -> Dict[str, Any]:
    return {
        "strategy_id": opportunity.strategy_id,
        "instance_id": opportunity.instance_id,
        "directional_view": opportunity.directional_view,
        "setup_quality": opportunity.setup_quality,
        "entry_style_preference": opportunity.entry_style_preference,
        "long_opportunity_score": round(opportunity.long_opportunity_score, 4),
        "short_opportunity_score": round(opportunity.short_opportunity_score, 4),
        "urgency": opportunity.urgency,
        "confidence": round(opportunity.confidence, 4),
        "actionable": opportunity.actionable,
        "source_action": opportunity.source_action,
        "source_side": opportunity.source_side,
        "notes": list(opportunity.notes),
    }


def _execution_constraint_to_dict(constraint: ExecutionConstraint) -> Dict[str, Any]:
    return {
        "advice": constraint.advice,
        "allowed_side": constraint.allowed_side,
        "allowed_entry_styles": list(constraint.allowed_entry_styles),
        "confirmation_required": constraint.confirmation_required,
        "probe_size_scale": round(constraint.probe_size_scale, 4),
        "structural_side": constraint.structural_side,
        "macro_direction": constraint.macro_direction,
        "block_new_open": constraint.block_new_open,
        "reason": constraint.reason,
    }


def _materialized_opportunity_to_dict(materialized: MaterializedOpportunity) -> Dict[str, Any]:
    return {
        "materialized_action": materialized.materialized_action,
        "final_action": materialized.final_action,
        "final_side": materialized.final_side,
        "entry_style": materialized.entry_style,
        "size_scale": round(materialized.size_scale, 4),
        "eligible_for_aggregation": materialized.eligible_for_aggregation,
        "reason": materialized.reason,
    }


def _opportunity_primary_score(opportunity: StrategyOpportunity) -> float:
    if opportunity.directional_view == "BUY":
        return max(0.0, float(opportunity.long_opportunity_score))
    if opportunity.directional_view == "SELL":
        return max(0.0, float(opportunity.short_opportunity_score))
    return 0.0


def _opportunity_signed_score(opportunity: StrategyOpportunity, instance_weight: float) -> float:
    score = _opportunity_primary_score(opportunity)
    if opportunity.directional_view == "BUY":
        return round(score * max(0.0, instance_weight), 6)
    if opportunity.directional_view == "SELL":
        return round(-score * max(0.0, instance_weight), 6)
    return 0.0


def _resolve_entry_style_preference(
    signal: Dict[str, Any],
    interpreter_context: Dict[str, Any],
) -> str:
    explicit_style = _normalize_entry_style(signal.get("entry_style", ""))
    if explicit_style != "MARKET" or str(signal.get("entry_style", "")).strip():
        return explicit_style
    execution_advice = str(interpreter_context.get("execution_advice", "ALLOW_FOLLOW") or "ALLOW_FOLLOW").upper()
    entry_timing_hint = str(interpreter_context.get("entry_timing_hint", "") or "").upper()
    if execution_advice == "WAIT_CONFIRMATION":
        return "CONFIRMATION_ONLY"
    if execution_advice == "LIMIT_ONLY" or entry_timing_hint == "WAIT_PULLBACK":
        return "PULLBACK_LIMIT"
    return "MARKET"


def _opportunity_setup_quality(action: str, side: str, confidence: float) -> str:
    if action != "OPEN" or side not in {"BUY", "SELL"}:
        return "INVALID"
    if confidence >= 0.8:
        return "STRONG"
    if confidence >= 0.5:
        return "VALID"
    if confidence >= 0.25:
        return "WEAK"
    return "INVALID"


def _opportunity_urgency(setup_quality: str, entry_style_preference: str, confidence: float) -> str:
    if entry_style_preference == "MARKET" and confidence >= 0.75:
        return "HIGH"
    if setup_quality in {"STRONG", "VALID"} and entry_style_preference in {"MARKET", "LIMIT"}:
        return "MEDIUM"
    return "LOW"


def _build_strategy_opportunity(
    signal: Dict[str, Any],
    interpreter_context: Dict[str, Any],
) -> StrategyOpportunity:
    action = str(signal.get("action", "HOLD") or "HOLD").upper()
    side = str(signal.get("side", "NONE") or "NONE").upper()
    confidence = float((signal.get("trace", {}) or {}).get("confidence", signal.get("confidence", 0.0)) or 0.0)
    setup_quality = _opportunity_setup_quality(action, side, confidence)
    entry_style_preference = _resolve_entry_style_preference(signal, interpreter_context)
    long_score = confidence if action == "OPEN" and side == "BUY" else 0.0
    short_score = confidence if action == "OPEN" and side == "SELL" else 0.0
    notes = []
    reject_reason = str(signal.get("reject_reason", "") or "")
    if reject_reason:
        notes.append(reject_reason)
    return StrategyOpportunity(
        strategy_id=str(signal.get("_sig_strategy_id", signal.get("strategy_id", "")) or ""),
        instance_id=str(signal.get("_instance_id", signal.get("instance_id", "")) or ""),
        directional_view=side if side in {"BUY", "SELL"} else "NONE",
        setup_quality=setup_quality,
        entry_style_preference=entry_style_preference,
        long_opportunity_score=round(long_score, 4),
        short_opportunity_score=round(short_score, 4),
        urgency=_opportunity_urgency(setup_quality, entry_style_preference, confidence),
        confidence=round(confidence, 4),
        actionable=action == "OPEN" and side in {"BUY", "SELL"},
        source_action=action,
        source_side=side,
        notes=notes,
    )


def _resolve_execution_constraint(
    agent_context: Dict[str, Any],
    route: Dict[str, Any],
    decision_policy: Dict[str, Any],
) -> ExecutionConstraint:
    macro_context = agent_context.get("macro", {}) if isinstance(agent_context.get("macro", {}), dict) else {}
    interpreter_context = (
        agent_context.get("interpreter", {})
        if isinstance(agent_context.get("interpreter", {}), dict)
        else {}
    )
    advice = str(interpreter_context.get("execution_advice", "ALLOW_FOLLOW") or "ALLOW_FOLLOW").upper()
    advice_matrix = (
        decision_policy.get("execution", {}).get("advice_matrix", {})
        if isinstance(decision_policy.get("execution", {}), dict)
        else {}
    )
    if not isinstance(advice_matrix, dict):
        advice_matrix = _default_execution_advice_matrix()
    advice_row = advice_matrix.get(advice, advice_matrix.get("ALLOW_FOLLOW", _default_execution_advice_matrix()["ALLOW_FOLLOW"]))
    allowed_side = str(
        route.get("allowed_side", macro_context.get("allowed_side", "BOTH"))
        or macro_context.get("allowed_side", "BOTH")
        or "BOTH"
    ).upper()
    structural_side = str(interpreter_context.get("structural_side", "NONE") or "NONE").upper()
    macro_direction = str(macro_context.get("effective_direction", macro_context.get("direction", "NEUTRAL")) or "NEUTRAL").upper()
    reason = f"advice={advice} allowed_side={allowed_side} structural_side={structural_side} macro={macro_direction}"
    session_current_policy = route.get("session_current_policy", {}) if isinstance(route.get("session_current_policy", {}), dict) else {}
    entry_mode = str(session_current_policy.get("entry_mode", "normal") or "normal").lower()
    allowed_entry_styles = _normalize_entry_style_list(
        advice_row.get("allowed_entry_styles", ["MARKET", "LIMIT"]),
        ["MARKET", "LIMIT"],
    )
    if entry_mode == "aggressive" and "PULLBACK_LIMIT" not in allowed_entry_styles:
        allowed_entry_styles.append("PULLBACK_LIMIT")
    block_new_open = entry_mode == "observe_only"
    return ExecutionConstraint(
        advice=advice,
        allowed_side=allowed_side,
        allowed_entry_styles=allowed_entry_styles,
        confirmation_required=_coerce_bool(advice_row.get("confirmation_required", False), False),
        probe_size_scale=_bounded_float(advice_row.get("probe_size_scale", 0.5), 0.5, low=0.1, high=1.0),
        structural_side=structural_side,
        macro_direction=macro_direction,
        block_new_open=block_new_open,
        reason=f"{reason} entry_mode={entry_mode}",
    )


def _materialize_opportunity(
    opportunity: StrategyOpportunity,
    constraint: ExecutionConstraint,
    *,
    min_confidence: float,
) -> MaterializedOpportunity:
    side = opportunity.directional_view
    score = _opportunity_primary_score(opportunity)
    allowed_styles = set(constraint.allowed_entry_styles)
    preferred_style = opportunity.entry_style_preference
    entry_style = "MARKET" if "MARKET" in allowed_styles else "LIMIT"
    if preferred_style == "PULLBACK_LIMIT" and "PULLBACK_LIMIT" in allowed_styles:
        entry_style = "PULLBACK_LIMIT"
    elif preferred_style in {"LIMIT", "PULLBACK_LIMIT"} and allowed_styles.intersection({"LIMIT", "PULLBACK_LIMIT"}):
        entry_style = "LIMIT"
    elif preferred_style == "CONFIRMATION_ONLY":
        entry_style = "LIMIT" if "LIMIT" in allowed_styles or "PULLBACK_LIMIT" in allowed_styles else "MARKET"
    if not opportunity.actionable or side not in {"BUY", "SELL"} or opportunity.setup_quality == "INVALID":
        return MaterializedOpportunity(
            materialized_action="NO_SETUP",
            reason="NO_ACTIONABLE_SETUP",
        )
    if constraint.allowed_side in {"BUY", "SELL"} and side != constraint.allowed_side:
        return MaterializedOpportunity(
            materialized_action="BLOCKED",
            reason=f"SIDE_NOT_ALLOWED:{constraint.allowed_side}",
        )
    if constraint.block_new_open:
        return MaterializedOpportunity(
            materialized_action="BLOCKED",
            reason="NEW_OPEN_BLOCKED",
        )
    if constraint.confirmation_required:
        return MaterializedOpportunity(
            materialized_action="ARM_WAIT_CONFIRMATION",
            entry_style=entry_style,
            eligible_for_aggregation=True,
            reason="WAIT_CONFIRMATION_REQUIRED",
        )
    if opportunity.setup_quality == "WEAK" and score >= max(0.0, min_confidence * 0.75):
        return MaterializedOpportunity(
            materialized_action="OPEN_PROBE",
            final_action="OPEN",
            final_side=side,
            entry_style=entry_style,
            size_scale=constraint.probe_size_scale,
            eligible_for_aggregation=True,
            reason="WEAK_SETUP_PROBE",
        )
    near_threshold_probe_floor = max(0.0, min_confidence - 0.05)
    if opportunity.setup_quality == "VALID" and score >= near_threshold_probe_floor and score < min_confidence:
        return MaterializedOpportunity(
            materialized_action="OPEN_PROBE",
            final_action="OPEN",
            final_side=side,
            entry_style=entry_style,
            size_scale=constraint.probe_size_scale,
            eligible_for_aggregation=True,
            reason="NEAR_THRESHOLD_PROBE",
        )
    if score < min_confidence:
        return MaterializedOpportunity(
            materialized_action="NO_SETUP",
            reason="OPPORTUNITY_SCORE_BELOW_THRESHOLD",
        )
    return MaterializedOpportunity(
        materialized_action="OPEN_LIMIT" if entry_style in ("LIMIT", "PULLBACK_LIMIT") else "OPEN_MARKET",
        final_action="OPEN",
        final_side=side,
        entry_style=entry_style,
        size_scale=1.0,
        eligible_for_aggregation=True,
        reason="READY_TO_EXECUTE",
    )


def _apply_price_target_overrides(
    plan: Dict[str, Any],
    param_overrides: Dict[str, Any],
) -> list[str]:
    applied: list[str] = []
    if not isinstance(param_overrides, dict) or not param_overrides:
        return applied
    if "atr_sl_mult" not in param_overrides and "tp_rr" not in param_overrides:
        return applied
    side = str(plan.get("side", "NONE") or "NONE").upper()
    if side not in {"BUY", "SELL"}:
        return applied
    try:
        entry_price = float(plan.get("entry_price", 0.0) or 0.0)
        atr_v = float(((plan.get("indicators", {}) or {}).get("atr14", 0.0) or 0.0))
    except Exception:
        return applied
    if entry_price <= 0 or atr_v <= 0:
        return applied
    try:
        sl_mult = float(param_overrides.get("atr_sl_mult", 0.0) or 0.0)
    except Exception:
        sl_mult = 0.0
    if sl_mult > 0:
        sl_distance = max(atr_v * sl_mult, 0.01)
        if side == "BUY":
            plan["sl_price"] = round(entry_price - sl_distance, 6)
        else:
            plan["sl_price"] = round(entry_price + sl_distance, 6)
        applied.append("atr_sl_mult")
    try:
        tp_rr = float(param_overrides.get("tp_rr", 0.0) or 0.0)
    except Exception:
        tp_rr = 0.0
    try:
        sl_price = float(plan.get("sl_price", 0.0) or 0.0)
    except Exception:
        sl_price = 0.0
    risk_distance = abs(entry_price - sl_price)
    if tp_rr > 0 and risk_distance > 0:
        tp_distance = max(risk_distance * tp_rr, 0.01)
        if side == "BUY":
            plan["tp_price"] = round(entry_price + tp_distance, 6)
        else:
            plan["tp_price"] = round(entry_price - tp_distance, 6)
        applied.append("tp_rr")
    return applied


def _apply_panel_sltp_to_ai_plan(
    mt5_snapshot: Dict[str, Any],
    engine_request: Dict[str, Any],
) -> None:
    plan = engine_request.get("ai_trade_plan", {}) if isinstance(engine_request.get("ai_trade_plan", {}), dict) else {}
    if not plan:
        return
    action = str(plan.get("action", "HOLD") or "HOLD").upper()
    side = str(plan.get("side", "NONE") or "NONE").upper()
    if action != "OPEN" or side not in {"BUY", "SELL"}:
        return
    try:
        entry_price = float(plan.get("entry_price", 0.0) or 0.0)
    except Exception:
        return
    if entry_price <= 0:
        market = mt5_snapshot.get("market", {}) if isinstance(mt5_snapshot.get("market", {}), dict) else {}
        try:
            entry_price = float(
                market.get("ask" if side == "BUY" else "bid", 0.0)
                or 0.0
            )
        except Exception:
            entry_price = 0.0
    if entry_price <= 0:
        return
    sl_pips = float(config_manager.get_effective_param("sl_pips", 0) or 0)
    tp_pips = float(config_manager.get_effective_param("tp_pips", 0) or 0)
    if sl_pips <= 0 or tp_pips <= 0:
        return
    # 鈹€鈹€ 鍏ㄥ搧绉嶈嚜閫傚簲 pip 璁＄畻锛堜娇鐢?MT5 鐪熷疄 digits/point锛夆攢鈹€
    # 瑙勫垯: digits>=5 鈫?1pip=10*point (鐜颁唬澶栨眹) | digits==4 鈫?1pip=1*point (鑰佸紡澶栨眹)
    #        digits<=3 鈫?1pip=10*point (閲戝睘/鍟嗗搧/鎸囨暟/鍔犲瘑)
    try:
        from src.price_digits import get_symbol_digits, get_point
        _digits = get_symbol_digits(preferred_symbol="XAUUSD")
        _point = get_point(preferred_symbol="XAUUSD")
        if _digits >= 5:
            price_step = _point * 10    # EURUSD 5-digit: 0.00001*10=0.0001
        elif _digits == 4:
            price_step = _point         # EURUSD 4-digit(鏃?: 1pip=1point
        else:
            price_step = _point * 10    # 閲戝睘/鍟嗗搧/鎸囨暟/鍔犲瘑: 閫氱敤10*point
    except Exception:
        price_step = 0.10  # fallback: 2-digit gold
    sl_distance = round(sl_pips * price_step, 6)
    tp_distance = round(tp_pips * price_step, 6)
    if side == "BUY":
        plan["sl_price"] = round(entry_price - sl_distance, 6)
        plan["tp_price"] = round(entry_price + tp_distance, 6)
    else:
        plan["sl_price"] = round(entry_price + sl_distance, 6)
        plan["tp_price"] = round(entry_price - tp_distance, 6)
    risk_flags = plan.get("risk_flags", []) if isinstance(plan.get("risk_flags", []), list) else []
    if "PANEL_SLTP_ENFORCED" not in risk_flags:
        risk_flags.append("PANEL_SLTP_ENFORCED")
    plan["risk_flags"] = risk_flags
    engine_request["ai_trade_plan"] = plan


def _normalize_pipeline_bars(raw_bars: Any) -> list[dict[str, Any]]:
    if not isinstance(raw_bars, list):
        return []
    normalized: list[dict[str, Any]] = []
    synthetic_time = 0
    for row in raw_bars:
        if not isinstance(row, dict):
            continue
        try:
            time_value = int(float(row.get("time", 0) or 0))
            open_value = float(row.get("open", 0.0) or 0.0)
            high_value = float(row.get("high", 0.0) or 0.0)
            low_value = float(row.get("low", 0.0) or 0.0)
            close_value = float(row.get("close", 0.0) or 0.0)
        except Exception:
            continue
        if high_value <= 0 or low_value <= 0 or close_value <= 0:
            continue
        if time_value <= 0:
            synthetic_time += 60
            time_value = synthetic_time
        else:
            synthetic_time = max(synthetic_time + 60, time_value)
        normalized.append(
            {
                "time": time_value,
                "open": open_value,
                "high": high_value,
                "low": low_value,
                "close": close_value,
                "tick_volume": float(
                    row.get("tick_volume", row.get("volume", row.get("real_volume", 0.0))) or 0.0
                ),
            }
        )
    normalized.sort(key=lambda item: int(item.get("time", 0) or 0))
    return normalized


def _ensure_snapshot_bars(
    mt5_snapshot: Dict[str, Any],
    *,
    symbol: str = "XAUUSD",
    min_count: int = 50,
    preferred_count: int = 160,
) -> list[dict[str, Any]]:
    if not isinstance(mt5_snapshot, dict):
        return []
    existing_bars = _normalize_pipeline_bars(mt5_snapshot.get("bars", []))
    if len(existing_bars) >= min_count:
        mt5_snapshot["bars"] = existing_bars
        return existing_bars[-preferred_count:] if len(existing_bars) >= preferred_count else existing_bars
    nested_market = mt5_snapshot.get("market", {}) if isinstance(mt5_snapshot.get("market", {}), dict) else {}
    nested_bars = _normalize_pipeline_bars(nested_market.get("bars", []))
    if len(nested_bars) >= min_count:
        mt5_snapshot["bars"] = nested_bars
        mt5_snapshot["_bars_source"] = "snapshot_market"
        return nested_bars[-preferred_count:] if len(nested_bars) >= preferred_count else nested_bars
    resolved_symbol = str(mt5_snapshot.get("symbol", symbol) or symbol or "XAUUSD").strip().upper() or "XAUUSD"
    plan = mt5_snapshot.get("ai_trade_plan", {}) if isinstance(mt5_snapshot.get("ai_trade_plan", {}), dict) else {}
    fallback_timeframe = str(
        mt5_snapshot.get("timeframe")
        or nested_market.get("timeframe")
        or plan.get("timeframe")
        or "M5"
    ).strip().upper() or "M5"
    fallback_bars: list[dict[str, Any]] = []
    try:
        from bridge.strategy import _read_bars

        fallback_bars = _normalize_pipeline_bars(
            _read_bars(max(preferred_count, min_count), timeframe=fallback_timeframe, symbol=resolved_symbol) or []
        )
    except Exception:
        fallback_bars = []
    if len(fallback_bars) >= min_count:
        mt5_snapshot["bars"] = fallback_bars
        mt5_snapshot["_bars_source"] = "bridge_strategy"
        return fallback_bars[-preferred_count:] if len(fallback_bars) >= preferred_count else fallback_bars
    if existing_bars:
        last_bar_time = int(existing_bars[-1].get("time", 0) or 0) if existing_bars else 0
        if last_bar_time > 0 and (time.time() - last_bar_time) < 1800:
            mt5_snapshot["bars"] = existing_bars
            return existing_bars[-preferred_count:] if len(existing_bars) >= preferred_count else existing_bars
    mt5_snapshot["bars"] = []
    return []


def _hydrate_llm_verification_indicators(
    ctx: PipelineRunContext,
    features: Dict[str, Any],
) -> Dict[str, Any]:
    """Backfill verification indicators from live bars when trace indicators are absent."""
    if all(features.get(key) is not None for key in ("rsi14", "adx14", "ema9", "ema21")):
        return features
    snap = getattr(ctx, "mt5_snapshot", None)
    if not isinstance(snap, dict):
        return features
    bars = _ensure_snapshot_bars(snap, symbol=ctx.symbol, min_count=30, preferred_count=80)
    if len(bars) < 26:
        return features
    try:
        from signal_generators.ema_macd_signal import _adx, _ema, _rsi

        closes = [float(bar.get("close", 0.0) or 0.0) for bar in bars[-60:]]
        if not closes:
            return features
        if features.get("ema9") is None:
            ema9_series = _ema(closes, 9)
            if ema9_series:
                features["ema9"] = float(ema9_series[-1] or 0.0)
        if features.get("ema21") is None:
            ema21_series = _ema(closes, 21)
            if ema21_series:
                features["ema21"] = float(ema21_series[-1] or 0.0)
        if features.get("rsi14") is None:
            rsi_series = _rsi(closes, 14)
            if rsi_series:
                features["rsi14"] = float(rsi_series[-1] or 0.0)
        if features.get("adx14") is None:
            adx_val, _adx_label, _adx_series = _adx(bars, 14)
            if adx_val is not None:
                features["adx14"] = float(adx_val or 0.0)
    except Exception:
        return features
    return features


def _build_strategy_specific_ai_plan(
    ctx: PipelineRunContext,
    instance: Dict[str, Any],
    scheduler_snapshot: Dict[str, Any],
    base_plan: Dict[str, Any],
) -> tuple[Optional[Dict[str, Any]], Dict[str, Any]]:
    strategy_id = str(instance.get("strategy_id", "") or "").strip()
    if not strategy_id:
        return None, {}
    bars = _ensure_snapshot_bars(
        ctx.mt5_snapshot,
        symbol=str(ctx.mt5_snapshot.get("symbol", ctx.symbol) or ctx.symbol or "XAUUSD"),
        min_count=50,
        preferred_count=200,
    )
    if not bars:
        return None, {"error": "bars_unavailable"}
    try:
        from signal_generators import create_generator
    except Exception as exc:
        return None, {"error": f"generator_import_failed:{exc}"}

    generator_params = (
        instance.get("param_overrides", {})
        if isinstance(instance.get("param_overrides", {}), dict)
        else {}
    )
    route_family = str(scheduler_snapshot.get("route", {}).get("route_family", "") or "").strip().lower()
    if route_family:
        generator_params["_system_route_family"] = route_family
        generator_params["_system_regime"] = str(
            scheduler_snapshot.get("route", {}).get("regime", "")
            or scheduler_snapshot.get("route", {}).get("selection_regime", "")
            or ""
        )
    llm_direction = str(base_plan.get("direction", base_plan.get("side", "")) or "").upper()
    llm_confidence = float(base_plan.get("confidence", 0.0) or 0.0)
    if llm_direction in {"BUY", "SELL", "LONG", "SHORT"} and llm_confidence > 0.3:
        generator_params["_llm_direction"] = "BUY" if llm_direction in {"BUY", "LONG"} else "SELL"
        generator_params["_llm_confidence"] = llm_confidence
    try:
        generator = create_generator(strategy_id, generator_params or None)
        raw_signal = generator.analyze(bars, generator_params or None)
    except Exception as exc:
        return None, {"error": f"generator_analyze_failed:{strategy_id}:{exc}"}
    if not isinstance(raw_signal, dict):
        return None, {"error": "generator_signal_invalid"}

    payload = raw_signal.get("payload", {}) if isinstance(raw_signal.get("payload", {}), dict) else {}
    plan = deepcopy(base_plan)
    plan["strategy_id"] = strategy_id
    raw_action = str(payload.get("action", "HOLD") or "HOLD").upper()
    derived_side = str(payload.get("side", "NONE") or "NONE").upper()
    if raw_action in {"BUY", "SELL"}:
        plan["action"] = "OPEN"
        plan["side"] = raw_action
    elif raw_action == "OPEN" and derived_side in {"BUY", "SELL"}:
        plan["action"] = "OPEN"
        plan["side"] = derived_side
    else:
        plan["action"] = "HOLD"
        plan["side"] = "NONE"
    plan["confidence"] = float(payload.get("confidence", 0.0) or 0.0)
    plan["signal_id"] = str(
        raw_signal.get("trace_id")
        or base_plan.get("signal_id")
        or f"strategy_{strategy_id}_{int(time.time())}"
    )
    plan["regime"] = str(
        base_plan.get("regime")
        or (
            scheduler_snapshot.get("route", {})
            if isinstance(scheduler_snapshot.get("route", {}), dict)
            else {}
        ).get("regime")
        or "TRANSITION"
    )
    plan["selection_regime"] = str(
        base_plan.get("selection_regime")
        or (
            scheduler_snapshot.get("route", {})
            if isinstance(scheduler_snapshot.get("route", {}), dict)
            else {}
        ).get("regime")
        or plan["regime"]
    )
    evidence = list(base_plan.get("evidence", [])) if isinstance(base_plan.get("evidence", []), list) else []
    marker = f"strategy_generator:{strategy_id}"
    if marker not in evidence:
        evidence.append(marker)
    reason_text = str(payload.get("reason", "") or "").strip()
    if reason_text:
        evidence.append(reason_text)
    plan["evidence"] = evidence
    indicators = payload.get("indicators", {})
    if isinstance(indicators, dict) and indicators:
        plan["indicators"] = indicators
    if plan["action"] == "OPEN":
        if payload.get("entry") is not None:
            plan["entry_price"] = float(payload.get("entry", 0.0) or 0.0)
        if payload.get("sl") is not None:
            plan["sl_price"] = float(payload.get("sl", 0.0) or 0.0)
        if payload.get("tp") is not None:
            plan["tp_price"] = float(payload.get("tp", 0.0) or 0.0)
    else:
        for key in ("entry_price", "sl_price", "tp_price"):
            plan.pop(key, None)
    return plan, {
        "trace_id": str(raw_signal.get("trace_id", "") or ""),
        "reason": reason_text,
        "counter_direction_allowed": bool(raw_signal.get("_counter_direction_allowed", False)),
    }


def _pending_entry_registry_path(project_root: Path) -> Path:
    return project_root / "data" / "pending_entry_intents.json"


def _load_pending_entry_registry(project_root: Path) -> Dict[str, Any]:
    path = _pending_entry_registry_path(project_root)
    if not path.exists():
        return {"entries": []}
    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {"entries": []}
    if not isinstance(payload, dict):
        return {"entries": []}
    entries = payload.get("entries", [])
    payload["entries"] = entries if isinstance(entries, list) else []
    return payload


def _save_pending_entry_registry(project_root: Path, payload: Dict[str, Any]) -> None:
    path = _pending_entry_registry_path(project_root)
    path.parent.mkdir(parents=True, exist_ok=True)
    serialized = payload if isinstance(payload, dict) else {"entries": []}
    serialized["entries"] = serialized.get("entries", []) if isinstance(serialized.get("entries", []), list) else []
    path.write_text(json.dumps(serialized, ensure_ascii=False, indent=2), encoding="utf-8")


def _is_executable_materialized_action(materialized_action: str) -> bool:
    return str(materialized_action or "").upper() in {"OPEN_MARKET", "OPEN_LIMIT", "OPEN_PROBE"}


def _market_bid_ask(mt5_snapshot: Dict[str, Any]) -> tuple[float, float, bool]:
    """Returns (bid, ask, prices_valid). prices_valid=False means fallback zeros from stale data."""
    market = mt5_snapshot.get("market", {}) if isinstance(mt5_snapshot.get("market", {}), dict) else {}
    try:
        bid_raw = market.get("bid")
        bid = float(bid_raw) if bid_raw is not None else 0.0
    except Exception:
        bid = 0.0
    try:
        ask_raw = market.get("ask")
        ask = float(ask_raw) if ask_raw is not None else 0.0
    except Exception:
        ask = 0.0
    prices_valid = bid > 0 and ask > 0
    return bid, ask, prices_valid


def _resolve_pullback_entry_price(signal: Dict[str, Any], mt5_snapshot: Dict[str, Any], market_context: Dict[str, Any]) -> float:
    side = str(signal.get("side", "NONE") or "NONE").upper()
    if side not in {"BUY", "SELL"}:
        return float(signal.get("entry_price", signal.get("price", 0.0)) or 0.0)
    bid, ask, prices_valid = _market_bid_ask(mt5_snapshot)
    current_price = ask if side == "BUY" else bid
    try:
        requested_price = float(signal.get("entry_price", signal.get("price", 0.0)) or 0.0)
    except Exception:
        requested_price = 0.0
    momentum = market_context.get("momentum", {}) if isinstance(market_context.get("momentum", {}), dict) else {}
    atr = float(momentum.get("atr", 0.0) or 0.0)
    regime = str(market_context.get("regime", "UNKNOWN") or "UNKNOWN").upper()
    risk_flags = signal.get("risk_flags", []) if isinstance(signal.get("risk_flags", []), list) else []
    if not prices_valid or current_price <= 0:
        return requested_price
    if atr <= 0:
        return requested_price if requested_price > 0 else current_price
    pullback_factor = 0.60
    if regime in {"RANGING", "TRANSITION"}:
        pullback_factor = 0.45
    if "FALLING_KNIFE_HARD" in risk_flags:
        pullback_factor += 0.30
    elif "FALLING_KNIFE_SOFT" in risk_flags:
        pullback_factor += 0.15
    if side == "BUY":
        fallback_price = current_price - (atr * pullback_factor)
        if requested_price > 0:
            return round(min(requested_price, fallback_price), 3)
        return round(fallback_price, 3)
    fallback_price = current_price + (atr * pullback_factor)
    if requested_price > 0:
        return round(max(requested_price, fallback_price), 3)
    return round(fallback_price, 3)


def _build_pending_entry_record(ctx: PipelineRunContext) -> Dict[str, Any]:
    signal = deepcopy(ctx.signal)
    materialized_action = str(signal.get("materialized_action", "") or "").upper()
    created_ts = time.time()
    ttl = int(PENDING_ENTRY_TTL_SECONDS.get(materialized_action, 15 * 60))
    market_context = _resolve_market_context_cached(
        ctx.agent_context,
        ctx.engine_request,
        mt5_snapshot=ctx.mt5_snapshot,
    )
    entry_price = float(signal.get("entry_price", signal.get("price", 0.0)) or 0.0)
    if materialized_action == "OPEN_LIMIT":
        entry_price = _resolve_pullback_entry_price(signal, ctx.mt5_snapshot, market_context)
    return {
        "pending_entry_id": str(signal.get("signal_id", "") or f"pending_{int(created_ts)}"),
        "materialized_action": materialized_action,
        "entry_style": str(signal.get("entry_style", "MARKET") or "MARKET").upper(),
        "side": str(signal.get("side", "NONE") or "NONE").upper(),
        "entry_price": entry_price,
        "confidence": float((signal.get("trace", {}) or {}).get("confidence", signal.get("confidence", 0.0)) or 0.0),
        "strategy_id": str(signal.get("selected_strategy_id", signal.get("strategy_id", "")) or ""),
        "instance_id": str(signal.get("selected_instance_id", signal.get("instance_id", "")) or ""),
        "created_ts": created_ts,
        "expire_ts": created_ts + ttl,
        "market_context": market_context,
        "signal": signal,
    }


def _register_pending_entry(ctx: PipelineRunContext) -> Dict[str, Any]:
    record = _build_pending_entry_record(ctx)
    entry_price = float(record.get("entry_price", 0.0) or 0.0)
    if entry_price <= 0:
        return _build_execution_report(
            ctx.signal_id,
            "REJECTED",
            "PENDING_ENTRY_NO_PRICE",
            materialized_action=record.get("materialized_action", ""),
            entry_style=record.get("entry_style", ""),
        )
    registry = _load_pending_entry_registry(ctx.project_root)
    entries = registry.get("entries", []) if isinstance(registry.get("entries", []), list) else []
    filtered: list[Dict[str, Any]] = []
    for item in entries:
        if not isinstance(item, dict):
            continue
        same_materialized = str(item.get("materialized_action", "") or "").upper() == record["materialized_action"]
        same_strategy = str(item.get("strategy_id", "") or "") == record["strategy_id"]
        same_side = str(item.get("side", "") or "").upper() == record["side"]
        same_instance = str(item.get("instance_id", "") or "") == record.get("instance_id", "")
        if same_materialized and same_strategy and same_side and same_instance:
            continue
        filtered.append(item)
    filtered.append(record)
    registry["entries"] = filtered
    registry["updated_at"] = datetime.now(timezone.utc).isoformat()
    _save_pending_entry_registry(ctx.project_root, registry)
    reason = "SOFT_LIMIT_REGISTERED" if record["materialized_action"] == "OPEN_LIMIT" else "WAIT_CONFIRMATION_ARMED"
    return _build_execution_report(
        ctx.signal_id,
        "ACKED",
        reason,
        pending_entry_id=record["pending_entry_id"],
        pending_entry_kind=record["materialized_action"],
        entry_style=record["entry_style"],
        materialized_action=record["materialized_action"],
    )


def _pending_entry_triggered(entry: Dict[str, Any], mt5_snapshot: Dict[str, Any]) -> bool:
    side = str(entry.get("side", "NONE") or "NONE").upper()
    entry_price = float(entry.get("entry_price", 0.0) or 0.0)
    if side not in {"BUY", "SELL"} or entry_price <= 0:
        return False
    bid, ask, _prices_ok = _market_bid_ask(mt5_snapshot)
    if side == "BUY":
        return _prices_ok and ask <= entry_price
    return _prices_ok and bid >= entry_price


def _pending_confirmation_resolved(entry: Dict[str, Any], current_signal: Dict[str, Any]) -> bool:
    current_action = str(current_signal.get("action", "HOLD") or "HOLD").upper()
    current_side = str(current_signal.get("side", "NONE") or "NONE").upper()
    current_materialized = str(current_signal.get("materialized_action", "") or "").upper()
    pending_side = str(entry.get("side", "NONE") or "NONE").upper()
    if current_action != "OPEN" or current_side != pending_side:
        return False
    return _is_executable_materialized_action(current_materialized)


def _apply_pending_entry_promotions(ctx: PipelineRunContext) -> None:
    registry = _load_pending_entry_registry(ctx.project_root)
    entries = registry.get("entries", []) if isinstance(registry.get("entries", []), list) else []
    if not entries:
        return
    now_ts = time.time()
    kept_entries: list[Dict[str, Any]] = []
    promoted_signal: Optional[Dict[str, Any]] = None
    promoted_reason = ""
    market_context = _resolve_market_context_cached(
        ctx.agent_context,
        ctx.engine_request,
        mt5_snapshot=ctx.mt5_snapshot,
    )

    def _has_fk_hard_flag(entry: dict) -> bool:
        """妫€鏌?pending entry 鐨勫師濮?signal 鏄惁鎼哄甫 FALLING_KNIFE_HARD 椋庨櫓鏍囧織"""
        sig = entry.get("signal", {})
        if not isinstance(sig, dict):
            return False
        flags = sig.get("risk_flags", [])
        return isinstance(flags, list) and "FALLING_KNIFE_HARD" in flags

    def _has_fk_soft_flag(entry: dict) -> bool:
        sig = entry.get("signal", {})
        if not isinstance(sig, dict):
            return False
        flags = sig.get("risk_flags", [])
        return isinstance(flags, list) and "FALLING_KNIFE_SOFT" in flags

    for entry in entries:
        if not isinstance(entry, dict):
            continue
        if float(entry.get("expire_ts", 0.0) or 0.0) <= now_ts:
            continue
        materialized_action = str(entry.get("materialized_action", "") or "").upper()
        if materialized_action == "ARM_WAIT_CONFIRMATION":
            # Path 1: Engine signal confirms (original promotion path)
            if _pending_confirmation_resolved(entry, ctx.signal):
                if promoted_signal is None:
                    base_signal = deepcopy(entry.get("signal", {})) if isinstance(entry.get("signal", {}), dict) else {}
                    origin_signal_id = str(base_signal.get("signal_id", entry.get("pending_entry_id", "pending")) or "pending")
                    base_signal["signal_id"] = f"{origin_signal_id}__confirmation_resolved"
                    base_signal["origin_signal_id"] = origin_signal_id
                    base_signal["materialized_action"] = "OPEN_MARKET"
                    base_signal["materialized_reason"] = "CONFIRMATION_RESOLVED"
                    base_signal["pending_entry_id"] = str(entry.get("pending_entry_id", "") or "")
                    bid, ask, _ = _market_bid_ask(ctx.mt5_snapshot)
                    entry_side = str(entry.get("side", "") or "").upper()
                    if entry_side == "BUY" and ask > 0:
                        base_signal["entry_price"] = ask
                    elif entry_side == "SELL" and bid > 0:
                        base_signal["entry_price"] = bid
                    promoted_signal = base_signal
                    promoted_reason = "CONFIRMATION_RESOLVED"
                continue
            # Path 2: Price-triggered fallback (after minimum hold time)
            if promoted_signal is None:
                entry_price = float(entry.get("entry_price", 0.0) or 0.0)
                created_ts = float(entry.get("created_ts", now_ts) or now_ts)
                hold_seconds = now_ts - created_ts if created_ts > 0 else 0
                if entry_price > 0 and hold_seconds >= 120 and _pending_entry_triggered(entry, ctx.mt5_snapshot):
                    live_fk = _detect_falling_knife_risk(ctx, str(entry.get("side", "") or "").upper())
                    if _has_fk_hard_flag(entry) or bool(live_fk.get("should_reject_open", False)):
                        kept_entries.append(entry)
                        continue
                    if _has_fk_soft_flag(entry) and str(market_context.get("route_family", "") or "") in {"range", "range_loosening", "uncertain"}:
                        kept_entries.append(entry)
                        continue
                    base_signal = deepcopy(entry.get("signal", {})) if isinstance(entry.get("signal", {}), dict) else {}
                    origin_signal_id = str(base_signal.get("signal_id", entry.get("pending_entry_id", "pending")) or "pending")
                    base_signal["signal_id"] = f"{origin_signal_id}__price_triggered"
                    base_signal["origin_signal_id"] = origin_signal_id
                    base_signal["materialized_action"] = "OPEN_MARKET"
                    base_signal["materialized_reason"] = "CONFIRMATION_PRICE_TRIGGERED"
                    base_signal["pending_entry_id"] = str(entry.get("pending_entry_id", "") or "")
                    bid, ask, _ = _market_bid_ask(ctx.mt5_snapshot)
                    entry_side = str(entry.get("side", "") or "").upper()
                    if entry_side == "BUY" and ask > 0:
                        base_signal["entry_price"] = ask
                    elif entry_side == "SELL" and bid > 0:
                        base_signal["entry_price"] = bid
                    promoted_signal = base_signal
                    promoted_reason = "CONFIRMATION_PRICE_TRIGGERED"
                    continue
            kept_entries.append(entry)
            continue
        if materialized_action == "OPEN_LIMIT":
            if promoted_signal is None and _pending_entry_triggered(entry, ctx.mt5_snapshot):
                live_fk = _detect_falling_knife_risk(ctx, str(entry.get("side", "") or "").upper())
                if _has_fk_hard_flag(entry) or bool(live_fk.get("should_reject_open", False)):
                    kept_entries.append(entry)
                    continue
                if _has_fk_soft_flag(entry) and str(market_context.get("route_family", "") or "") in {"range", "uncertain"}:
                    kept_entries.append(entry)
                    continue
                base_signal = deepcopy(entry.get("signal", {})) if isinstance(entry.get("signal", {}), dict) else {}
                origin_signal_id = str(base_signal.get("signal_id", entry.get("pending_entry_id", "pending")) or "pending")
                base_signal["signal_id"] = f"{origin_signal_id}__soft_limit_trigger"
                base_signal["origin_signal_id"] = origin_signal_id
                base_signal["materialized_action"] = "OPEN_MARKET"
                base_signal["materialized_reason"] = "SOFT_LIMIT_TRIGGERED"
                base_signal["pending_entry_id"] = str(entry.get("pending_entry_id", "") or "")
                # 鏇存柊鍏ュ満浠锋牸涓哄綋鍓嶅競浠凤紝鑰岄潪鎸傚崟浠?
                bid, ask, _ = _market_bid_ask(ctx.mt5_snapshot)
                entry_side = str(entry.get("side", "") or "").upper()
                if entry_side == "BUY" and ask > 0:
                    base_signal["entry_price"] = ask
                elif entry_side == "SELL" and bid > 0:
                    base_signal["entry_price"] = bid
                promoted_signal = base_signal
                promoted_reason = "SOFT_LIMIT_TRIGGERED"
                continue
            kept_entries.append(entry)
            continue
        kept_entries.append(entry)
    registry["entries"] = kept_entries
    registry["updated_at"] = datetime.now(timezone.utc).isoformat()
    _save_pending_entry_registry(ctx.project_root, registry)
    ctx.pending_registry_update = registry
    if promoted_signal is not None:
        live_fk = _detect_falling_knife_risk(ctx, str(promoted_signal.get("side", "NONE") or "NONE").upper())
        if bool(live_fk.get("should_reject_open", False)):
            ctx.signal["action"] = "HOLD"
            ctx.signal["side"] = "NONE"
            ctx.signal["reject_reason"] = f"PENDING_PROMOTION_FALLING_KNIFE:{live_fk.get('reason', '')}"
            return
        promoted_strategy_id = canonical_strategy_id(
            promoted_signal.get("strategy_id", promoted_signal.get("_selected_strategy", ""))
        )
        regime_label = str(market_context.get("regime", ctx.agent_context.get("regime", ctx.aggregated_intent.get("regime", "UNKNOWN"))) or "UNKNOWN")
        regime_confidence = float(
            market_context.get("effective_regime_confidence")
            or market_context.get("regime_confidence", 0.0)
            or 0.0
        )
        session_mode = str(market_context.get("session_mode", "") or "").upper()
        session_bucket = str(market_context.get("session_bucket", session_mode) or session_mode).upper()
        compatible, reject_reason = is_strategy_regime_compatible(
            promoted_strategy_id,
            regime_label,
            regime_confidence=regime_confidence,
            session_mode=session_bucket,
        )
        if not compatible:
            ctx.signal["action"] = "HOLD"
            ctx.signal["side"] = "NONE"
            ctx.signal["reject_reason"] = f"PENDING_PROMOTION_RECHECK:{reject_reason}"
            return
        ctx.signal = promoted_signal
        ctx.signal_id = str(promoted_signal.get("signal_id", ctx.signal_id) or ctx.signal_id)
        pending_resolution = {
            "reason": promoted_reason,
            "pending_entry_id": str(promoted_signal.get("pending_entry_id", "") or ""),
        }
        ctx.aggregated_intent["pending_entry_resolution"] = pending_resolution
        ctx.signal_attribution = _attach_signal_attribution(ctx.signal, ctx.aggregated_intent)


def _build_instance_signals(
    ctx: PipelineRunContext,
    engine_request: Dict[str, Any],
    project_root: Path,
    scheduler_snapshot: Dict[str, Any],
) -> list[Dict[str, Any]]:
    instances = scheduler_snapshot.get("instances", []) if isinstance(scheduler_snapshot.get("instances", []), list) else []
    # Filter out paused instances 鈥?engine must respect the pause flag, not just the UI
    instances = [
        inst for inst in instances
        if str(inst.get("status", "running") or "running").lower() != "paused"
    ]
    multiple_instances = len(instances) > 1
    current_open_positions = int(float(engine_request.get("account_metrics", {}).get("current_open_positions", 0) or 0))
    selected_entry_instance_id = str(scheduler_snapshot.get("selected_instance_id", "") or "").strip()
    route = scheduler_snapshot.get("route", {}) if isinstance(scheduler_snapshot.get("route", {}), dict) else {}
    allowed_side = str(route.get("allowed_side", "BOTH") or "BOTH").upper()
    session_decision = (
        route.get("session_decision", {})
        if isinstance(route.get("session_decision", {}), dict)
        else {}
    )
    # regime_directed: ALL regime-compatible instances run every bar.
    # Each instance independently generates its signal; the regime gate
    # (signal_arbitrator REGIME_ALLOWLIST) filters non-compatible signals.
    # This enables true multi-strategy parallel execution.
    entry_selection_active = False  # all instances run
    target_instances = instances
    signals: list[Dict[str, Any]] = []
    base_plan = engine_request.get("ai_trade_plan", {}) if isinstance(engine_request.get("ai_trade_plan", {}), dict) else {}
    try:
        _policy_params = _build_runtime_policy_sources().get("params", {})
        max_positions_per_strategy = int(float(_policy_params.get("max_positions_per_strategy", {}).get("value", 1) or 1))
    except Exception:
        max_positions_per_strategy = 1

    def _render_instance_signal(instance: Dict[str, Any]) -> Dict[str, Any]:
        # Shallow copy + targeted deep copy of only the mutable ai_trade_plan
        # avoids expensive full deepcopy of market_snapshot/bars on every instance
        instance_request = dict(engine_request)
        raw_plan = instance_request.get("ai_trade_plan", {})
        plan = deepcopy(raw_plan) if isinstance(raw_plan, dict) else {}
        strategy_id = str(instance.get("strategy_id", "")).strip()
        instance_id = str(instance.get("instance_id", "")).strip()
        strategy_plan_meta: Dict[str, Any] = {}
        param_overrides = (
            instance.get("param_overrides", {})
            if isinstance(instance.get("param_overrides", {}), dict)
            else {}
        )
        route_market_context = (
            route.get("market_context", {})
            if isinstance(route.get("market_context", {}), dict)
            else {}
        )
        system_regime = normalize_regime_label(
            route.get("regime")
            or route.get("selection_regime")
            or route_market_context.get("selection_regime")
            or route_market_context.get("regime")
            or (ctx.agent_context or {}).get("regime", "UNKNOWN")
            or "UNKNOWN"
        )
        system_route_family = str(
            route.get("route_family", route_market_context.get("route_family", ""))
            or ""
        ).strip().lower()
        if strategy_id:
            plan["strategy_id"] = strategy_id
        plan["_instance_id"] = instance_id
        if multiple_instances:
            derived_plan, strategy_plan_meta = _build_strategy_specific_ai_plan(
                ctx,
                instance,
                scheduler_snapshot,
                plan,
            )
            if isinstance(derived_plan, dict):
                plan = derived_plan
            else:
                error_reason = str(strategy_plan_meta.get("error", "plan_unavailable") or "plan_unavailable")
                hold_signal = _make_hold_signal(
                    strategy_id,
                    instance_id,
                    f"INSTANCE_PLAN_FAILED:{error_reason}",
                    [f"instance_plan_failed:{error_reason}"],
                )
                hold_signal["entry_selected"] = not entry_selection_active or instance_id == selected_entry_instance_id
                return hold_signal
        plan["_system_regime"] = system_regime
        plan["_system_route_family"] = system_route_family
        applied_override_keys: list[str] = []
        if "risk_per_trade" in param_overrides:
            try:
                # Contract stores risk as decimal (0.01 = 1%), while executor expects pct.
                plan["risk_per_trade_pct"] = round(float(param_overrides.get("risk_per_trade", 0.0) or 0.0) * 100.0, 4)
                applied_override_keys.append("risk_per_trade")
            except Exception:
                pass
        for override_key in ("volume_lots", "entry_price", "sl_price", "tp_price"):
            if override_key not in param_overrides:
                continue
            try:
                plan[override_key] = float(param_overrides.get(override_key, 0.0) or 0.0)
                applied_override_keys.append(override_key)
            except Exception:
                continue
        if "max_positions" in param_overrides:
            try:
                plan["max_positions"] = int(float(param_overrides.get("max_positions", 0.0) or 0.0))
                applied_override_keys.append("max_positions")
            except Exception:
                pass
        applied_override_keys.extend(_apply_price_target_overrides(plan, param_overrides))
        instance_request["ai_trade_plan"] = plan
        instance_request["runtime_scheduler"] = scheduler_snapshot
        # Inject regime-aware params so signal generators use adaptive SL/TP
        _rp = (ctx.agent_context or {}).get("regime_params", {}) if hasattr(ctx, "agent_context") else {}
        if isinstance(_rp, dict) and _rp:
            for _rk in ("sl_atr", "tp_atr", "min_confidence"):
                if _rk in _rp and _rk not in param_overrides:
                    plan[_rk] = _rp[_rk]
        instance_request["strategy_context"] = {
            "instance_id": instance_id,
            "strategy_id": strategy_id,
            "role": str(instance.get("role", "live_support") or "live_support"),
            "param_overrides": param_overrides,
            "config_revision": str(instance.get("config_revision", scheduler_snapshot.get("config_revision", "manual"))),
        }
        signal = run_engine(instance_request, project_root)
        base_signal_id = str(signal.get("signal_id", f"sig_{strategy_id or 'strategy'}_{int(time.time())}") or f"sig_{strategy_id or 'strategy'}_{int(time.time())}")
        if multiple_instances:
            signal["signal_id"] = f"{base_signal_id}__{instance_id}"
        signal["_base_signal_id"] = base_signal_id
        signal["_instance_id"] = instance_id
        signal["_sig_strategy_id"] = strategy_id
        signal["strategy_id"] = strategy_id
        signal["config_revision"] = str(instance.get("config_revision", scheduler_snapshot.get("config_revision", "manual")))
        signal["instance_weight"] = float(scheduler_snapshot.get("instance_weights", {}).get(instance_id, instance.get("weight", 0.0)) or 0.0)
        signal["instance_role"] = str(instance.get("role", "live_support") or "live_support")
        signal["param_overrides"] = param_overrides
        signal["entry_selected"] = not entry_selection_active or instance_id == selected_entry_instance_id
        if strategy_plan_meta.get("counter_direction_allowed"):
            signal["_counter_direction_allowed"] = True
        if str(strategy_plan_meta.get("trace_id", "") or "").strip():
            signal["_strategy_trace_id"] = str(strategy_plan_meta.get("trace_id", "") or "")
        signal = _enforce_system_market_truth(
            signal,
            strategy_id=strategy_id,
            system_regime=system_regime,
            system_route_family=system_route_family,
        )
        if applied_override_keys:
            trace = signal.get("trace", {}) if isinstance(signal.get("trace", {}), dict) else {}
            evidence = trace.get("evidence", []) if isinstance(trace.get("evidence", []), list) else []
            for override_key in applied_override_keys:
                marker = f"instance_override:{override_key}"
                if marker not in evidence:
                    evidence.append(marker)
            trace["evidence"] = evidence
            signal["trace"] = trace
            signal["applied_execution_overrides"] = applied_override_keys
        if bool(signal["param_overrides"].get("reduce_only", False)) and str(signal.get("action", "HOLD")).upper() == "OPEN":
            trace = signal.get("trace", {}) if isinstance(signal.get("trace", {}), dict) else {}
            evidence = trace.get("evidence", []) if isinstance(trace.get("evidence", []), list) else []
            evidence.append("instance_reduce_only")
            trace["evidence"] = evidence
            signal["trace"] = trace
            signal["action"] = "HOLD"
            signal["side"] = "NONE"
            signal["reject_reason"] = "INSTANCE_REDUCE_ONLY"
        if (
            not bool(session_decision.get("allow_entry", True))
            and str(signal.get("action", "HOLD")).upper() == "OPEN"
        ):
            trace = signal.get("trace", {}) if isinstance(signal.get("trace", {}), dict) else {}
            evidence = trace.get("evidence", []) if isinstance(trace.get("evidence", []), list) else []
            evidence.append("session_scheduler_block")
            trace["evidence"] = evidence
            signal["trace"] = trace
            signal["action"] = "HOLD"
            signal["side"] = "NONE"
            signal["reject_reason"] = str(
                session_decision.get("reason_code", "SESSION_POLICY_DENIED") or "SESSION_POLICY_DENIED"
            )
        elif (
            not bool(session_decision.get("allow_add_position", True))
            and current_open_positions > 0
            and str(signal.get("action", "HOLD")).upper() == "OPEN"
        ):
            trace = signal.get("trace", {}) if isinstance(signal.get("trace", {}), dict) else {}
            evidence = trace.get("evidence", []) if isinstance(trace.get("evidence", []), list) else []
            evidence.append("session_scheduler_no_add_position")
            trace["evidence"] = evidence
            signal["trace"] = trace
            signal["action"] = "HOLD"
            signal["side"] = "NONE"
            signal["reject_reason"] = "SESSION_ADD_POSITION_DISABLED"
        if max_positions_per_strategy > 0:
            _sid_check = str(signal.get("strategy_id", "") or "").strip()
            if _sid_check:
                _strategy_count = 0
                _pos_map = ctx.runtime_state.get("_position_strategy_map", {})
                if isinstance(_pos_map, dict):
                    _strategy_count += sum(1 for _sid in _pos_map.values() if str(_sid or "").strip() == _sid_check)
                _strategy_count += sum(
                    1 for _s in signals
                    if isinstance(_s, dict)
                    and str(_s.get("strategy_id", "") or "").strip() == _sid_check
                    and str(_s.get("action", "?")).upper() != "HOLD"
                )
                if _strategy_count >= max_positions_per_strategy:
                    _trace = signal.get("trace", {}) if isinstance(signal.get("trace", {}), dict) else {}
                    _evidence = _trace.get("evidence", []) if isinstance(_trace.get("evidence", []), list) else []
                    _evidence.append("max_positions_per_strategy_reached")
                    _trace["evidence"] = _evidence
                    signal["trace"] = _trace
                    signal["action"] = "HOLD"
                    signal["side"] = "NONE"
                    signal["reject_reason"] = f"MAX_POSITIONS_PER_STRATEGY_{_sid_check}_{_strategy_count}>={max_positions_per_strategy}"
        max_positions_limit = int(float(signal["param_overrides"].get("max_positions", 0.0) or 0.0)) if "max_positions" in signal["param_overrides"] else 0
        if (
            max_positions_limit > 0
            and current_open_positions >= max_positions_limit
            and str(signal.get("action", "HOLD")).upper() == "OPEN"
        ):
            trace = signal.get("trace", {}) if isinstance(signal.get("trace", {}), dict) else {}
            evidence = trace.get("evidence", []) if isinstance(trace.get("evidence", []), list) else []
            evidence.append("instance_max_positions_reached")
            trace["evidence"] = evidence
            signal["trace"] = trace
            signal["action"] = "HOLD"
            signal["side"] = "NONE"
            signal["reject_reason"] = f"INSTANCE_MAX_POSITIONS_{current_open_positions}>={max_positions_limit}"
        if (
            allowed_side in {"BUY", "SELL"}
            and str(signal.get("action", "HOLD")).upper() == "OPEN"
            and str(signal.get("side", "NONE") or "NONE").upper() in {"BUY", "SELL"}
            and str(signal.get("side", "NONE") or "NONE").upper() != allowed_side
        ):
            # 鈹€鈹€ Counter-direction: penalize, don't block 鈹€鈹€
            # Macro direction is a BIAS, not a LOCK. Counter-direction signals
            # pass through with _counter_direction_penalty marker for downstream
            # weight/size reduction, but are NOT hard-blocked.
            # Exception: _counter_direction_allowed strategies bypass even the penalty.
            if signal.get("_counter_direction_allowed"):
                signal["_direction_override"] = True
                try:
                    from bridge.gold_trader import _daemon_log
                    _daemon_log(f"[DIRECTION_EXEMPT] {signal.get('strategy_id','')} "
                                f"side={signal.get('side','')} exempt from allowed_side={allowed_side}")
                except Exception:
                    pass
            else:
                trace = signal.get("trace", {}) if isinstance(signal.get("trace", {}), dict) else {}
                evidence = trace.get("evidence", []) if isinstance(trace.get("evidence", []), list) else []
                evidence.append("counter_direction_penalty")
                trace["evidence"] = evidence
                signal["trace"] = trace
                signal["_counter_direction_penalty"] = True
                signal["_counter_direction"] = str(signal.get("side", ""))  # record which side was penalized
        return signal

    if entry_selection_active and len(target_instances) == 1:
        # 鈹€鈹€ Render primary with crash protection 鈹€鈹€
        try:
            primary_signal = _render_instance_signal(target_instances[0])
        except Exception as exc:
            crash_id = str(target_instances[0].get("instance_id", "?") or "?")
            primary_signal = {
                "action": "HOLD", "side": "NONE",
                "reject_reason": f"INSTANCE_CRASH:{crash_id}:{exc}",
                "_instance_id": crash_id,
                "_sig_strategy_id": str(target_instances[0].get("strategy_id", "") or ""),
                "instance_weight": 0.0, "entry_selected": False,
                "trace": {"confidence": 0.0, "evidence": [f"entry_owner_crash:{crash_id}"]},
            }
        signals.append(primary_signal)
        primary_reason = str(primary_signal.get("reject_reason", "") or "")
        primary_crashed = primary_reason.startswith("INSTANCE_CRASH:")
        side_blocked = primary_reason.startswith("ROUTE_ALLOWED_SIDE_BLOCK:") and allowed_side in {"BUY", "SELL"}
        if primary_crashed or side_blocked:
            fallback_candidates = [
                inst
                for inst in instances
                if str(inst.get("instance_id", "") or "").strip() != selected_entry_instance_id
            ]
            if fallback_candidates:
                fallback_signals: list[Dict[str, Any]] = []
                for fb_inst in fallback_candidates:
                    try:
                        fallback_signals.append(_render_instance_signal(fb_inst))
                    except Exception:
                        continue
                actionable_fallbacks = [s for s in fallback_signals if _is_actionable_signal(s)]
                if actionable_fallbacks:
                    selected_fallback = max(
                        actionable_fallbacks,
                        key=lambda s: (
                            abs(_signal_signed_score(s, float(s.get("instance_weight", 0.0) or 0.0))),
                            float((s.get("trace", {}) or {}).get("confidence", 0.0) or 0.0),
                            str(s.get("_instance_id", "") or ""),
                        ),
                    )
                    fallback_instance_id = str(selected_fallback.get("_instance_id", "") or "").strip()
                    if fallback_instance_id:
                        evidence_tag = "entry_owner_crash_failover" if primary_crashed else "entry_owner_route_side_failover"
                        failover_reason = "ENTRY_OWNER_CRASH_FAILOVER" if primary_crashed else "ENTRY_OWNER_ROUTE_ALLOWED_SIDE_BLOCK"
                        for fb_signal in fallback_signals:
                            if str(fb_signal.get("_instance_id", "") or "").strip() != fallback_instance_id:
                                continue
                            fb_signal["entry_selected"] = True
                            fb_signal["_entry_owner_override"] = fallback_instance_id
                            fb_signal["_entry_failover_reason"] = failover_reason
                            fb_trace = (
                                fb_signal.get("trace", {})
                                if isinstance(fb_signal.get("trace", {}), dict)
                                else {}
                            )
                            fb_evidence = (
                                fb_trace.get("evidence", [])
                                if isinstance(fb_trace.get("evidence", []), list)
                                else []
                            )
                            fb_evidence.append(evidence_tag)
                            fb_trace["evidence"] = fb_evidence
                            fb_signal["trace"] = fb_trace
                            signals.append(fb_signal)
                            break
        return signals

    # Parallel execution when multiple instances 鈥?each run_engine() is
    # an independent CPU-bound rule evaluation with no shared mutable state.
    if len(target_instances) <= 1:
        for instance in target_instances:
            try:
                signals.append(_render_instance_signal(instance))
            except Exception as exc:
                instance_id = str(instance.get("instance_id", "?") or "?")
                signals.append({
                    "action": "HOLD", "side": "NONE",
                    "reject_reason": f"INSTANCE_CRASH:{instance_id}:{exc}",
                    "_instance_id": instance_id,
                    "_sig_strategy_id": str(instance.get("strategy_id", "") or ""),
                    "instance_weight": 0.0,
                    "entry_selected": False,
                    "trace": {"confidence": 0.0, "evidence": [f"instance_crash:{instance_id}"]},
                })
    else:
        with ThreadPoolExecutor(max_workers=min(len(target_instances), 4)) as pool:
            future_map = {pool.submit(_render_instance_signal, inst): inst for inst in target_instances}
            for future in as_completed(future_map):
                try:
                    signals.append(future.result())
                except Exception as exc:
                    instance = future_map[future]
                    instance_id = str(instance.get("instance_id", "?") or "?")
                    signals.append({
                        "action": "HOLD", "side": "NONE",
                        "reject_reason": f"INSTANCE_CRASH:{instance_id}:{exc}",
                        "_instance_id": instance_id,
                        "_sig_strategy_id": str(instance.get("strategy_id", "") or ""),
                        "instance_weight": 0.0,
                        "entry_selected": False,
                        "trace": {"confidence": 0.0, "evidence": [f"instance_crash:{instance_id}"]},
                    })
    # Preserve original insertion order for deterministic aggregation
    instance_order = {str(inst.get("instance_id", "") or ""): idx for idx, inst in enumerate(target_instances)}
    signals.sort(key=lambda s: instance_order.get(str(s.get("_instance_id", "") or ""), 9999))
    return signals


def _is_plugin_signals_enabled(project_root: Path) -> bool:
    """Check if the plugin-based signal path is enabled via feature flag."""
    rules_path = project_root / "config" / "strategy_rules_v3.json"
    if not rules_path.exists():
        return False
    try:
        rules = json.loads(rules_path.read_text(encoding="utf-8"))
        return bool(
            rules.get("feature_flags", {}).get("plugin_signals", False)
        )
    except Exception:
        return False


def _build_plugin_signals(
    mt5_snapshot: Dict[str, Any],
    project_root: Path,
    scheduler_snapshot: Dict[str, Any],
) -> list[Dict[str, Any]]:
    """Build signals from dynamic strategy plugins (StrategyPlugin interface).

    Feature-flagged behind ``feature_flags.plugin_signals`` in
    strategy_rules_v3.json.  Discovers plugins via DynamicStrategyRegistry,
    calls on_market_snapshot() on each, and returns pipeline-compatible
    signal dicts.
    """
    try:
        from strategy_sdk.dynamic_registry import get_dynamic_strategy_registry
        from strategy_sdk.types import MarketSnapshot
    except ImportError:
        return []

    registry = get_dynamic_strategy_registry()
    ids = registry.list_ids()
    if not ids:
        return []

    bars = mt5_snapshot.get("bars", [])
    if isinstance(bars, list) and len(bars) > 0 and isinstance(bars[0], dict):
        pass
    else:
        return []

    raw_symbol = str(mt5_snapshot.get("symbol", "XAUUSD"))
    raw_timeframe = str(mt5_snapshot.get("timeframe", "M5"))
    now_ms = int(time.time() * 1000)
    snapshot = MarketSnapshot(
        symbol=raw_symbol,
        timeframe=raw_timeframe,
        timestamp_ms=now_ms,
        bars=bars,
    )

    signals: list[Dict[str, Any]] = []
    for sid in ids:
        try:
            spec = registry.get_spec(sid)
            # 鈹€鈹€ Use strategy_lifecycle for trust_level gating 鈹€鈹€
            try:
                from src.strategy_lifecycle import EXECUTABLE_STAGES
                _exec_stages = EXECUTABLE_STAGES
            except ImportError:
                _exec_stages = {"paper", "live_reduced", "live_full", "proven"}
            if spec.trust_level not in _exec_stages:
                continue
            plugin = registry.instantiate(sid)
        except Exception:
            continue

        try:
            intent = plugin.on_market_snapshot(snapshot)
        except Exception:
            continue

        if intent.action == "HOLD":
            continue

        instance_weight = float(
            scheduler_snapshot.get("strategy_weights", {}).get(sid, 0.0) or 0.0
        )
        signal_id = f"plugin_{sid}_{int(time.time())}"

        signals.append(
            {
                "signal_id": signal_id,
                "_base_signal_id": signal_id,
                "_instance_id": f"plugin:{sid}",
                "_sig_strategy_id": sid,
                "strategy_id": canonical_strategy_id(sid),
                "action": "OPEN" if str(intent.action or "").upper() != "HOLD" else "HOLD",
                "side": intent.side,
                "trace": {
                    "confidence": intent.confidence,
                    "evidence": ["plugin_signal", f"plugin:{sid}"],
                    "reason": intent.reason,
                },
                "applied_execution_overrides": [],
                "instance_weight": instance_weight,
                "instance_role": "plugin_support",
                "param_overrides": {},
                "entry_selected": False,
                "config_revision": spec.version,
                "reject_reason": None,
            }
        )

    return signals


def _load_pretrade_performance_factors(agent_context: Optional[Dict[str, Any]]) -> Dict[str, Any]:
    context = agent_context if isinstance(agent_context, dict) else {}
    artifact = context.get("performance_factors")
    if isinstance(artifact, dict) and any(
        isinstance(artifact.get(key), dict) for key in ("strategy", "regime", "session", "entry")
    ):
        return artifact
    try:
        from src.live_performance_factors import load_live_performance_factors

        artifact = load_live_performance_factors(force_refresh=False)
    except Exception:
        artifact = {}
    if isinstance(context, dict):
        context["performance_factors"] = artifact if isinstance(artifact, dict) else {}
    return artifact if isinstance(artifact, dict) else {}


def _build_signal_performance_profile(
    signal: Dict[str, Any],
    agent_context: Optional[Dict[str, Any]],
    route: Dict[str, Any],
    performance_factors: Dict[str, Any],
) -> Dict[str, Any]:
    if str(signal.get("action", "HOLD") or "HOLD").upper() != "OPEN":
        return {}
    try:
        from src.live_performance_factors import build_pretrade_performance_profile

        market_context = _resolve_market_context_cached(agent_context or {}, {}, mt5_snapshot={})
        session_info = (
            (agent_context or {}).get("_session_info", {})
            if isinstance((agent_context or {}).get("_session_info", {}), dict)
            else {}
        )
        performance_ctx = {
            "market_context": market_context,
            "selection_regime": str(route.get("regime", market_context.get("regime", "UNKNOWN")) or "UNKNOWN"),
            "session_info": session_info,
            "interpreter": (
                (agent_context or {}).get("interpreter", {})
                if isinstance((agent_context or {}).get("interpreter", {}), dict)
                else {}
            ),
            "selected_strategy": str(
                signal.get("_sig_strategy_id", signal.get("strategy_id", "")) or ""
            ),
        }
        return build_pretrade_performance_profile(signal, performance_ctx, performance_factors)
    except Exception:
        return {}


def _apply_signal_performance_profile(
    signal: Dict[str, Any],
    agent_context: Optional[Dict[str, Any]],
    route: Dict[str, Any],
    performance_factors: Dict[str, Any],
) -> None:
    if str(signal.get("action", "HOLD") or "HOLD").upper() != "OPEN":
        return
    profile = _build_signal_performance_profile(signal, agent_context, route, performance_factors)
    if not isinstance(profile, dict) or not profile:
        return
    signal["performance_profile"] = profile
    trace = signal.get("trace", {}) if isinstance(signal.get("trace", {}), dict) else {}
    evidence = trace.get("evidence", []) if isinstance(trace.get("evidence", []), list) else []
    matched = profile.get("matches", {}) if isinstance(profile.get("matches", {}), dict) else {}
    matched_categories = sorted(str(key) for key in matched.keys())
    if matched_categories:
        evidence.append(f"performance_profile:{'|'.join(matched_categories)}")
    if bool(profile.get("block", False)):
        reason = str(profile.get("block_reason", "") or "PERFORMANCE_UNDERPERFORMING")
        signal["action"] = "HOLD"
        signal["side"] = "NONE"
        signal["reject_reason"] = reason
        evidence.append(f"performance_gate:block:{reason}")
        trace["evidence"] = evidence
        trace["performance"] = {
            "block": True,
            "live_multiplier": float(profile.get("live_multiplier", 1.0) or 1.0),
            "confidence_bias": float(profile.get("confidence_bias", 0.0) or 0.0),
            "matched_categories": matched_categories,
            "reason": reason,
        }
        signal["trace"] = trace
        return
    live_multiplier = float(profile.get("live_multiplier", 1.0) or 1.0)
    confidence_bias = float(profile.get("confidence_bias", 0.0) or 0.0)
    if live_multiplier != 1.0:
        current_scale = float(signal.get("entry_size_scale", 1.0) or 1.0)
        signal["entry_size_scale"] = round(max(0.1, min(1.15, current_scale * live_multiplier)), 4)
        evidence.append(f"performance_gate:size={signal['entry_size_scale']:.4f}")
    if confidence_bias:
        current_conf = float(trace.get("confidence", 0.0) or 0.0)
        trace["confidence"] = round(max(0.0, min(0.99, current_conf + confidence_bias)), 4)
        evidence.append(f"performance_gate:bias={confidence_bias:+.3f}")
    trace["evidence"] = evidence
    trace["performance"] = {
        "block": False,
        "live_multiplier": round(live_multiplier, 4),
        "confidence_bias": round(confidence_bias, 4),
        "matched_categories": matched_categories,
        "reason": ";".join(
            str(item) for item in profile.get("advisories", []) if str(item).strip()
        ),
    }
    signal["trace"] = trace


def _aggregate_instance_signals(
    strategy_signals: list[Dict[str, Any]],
    scheduler_snapshot: Dict[str, Any],
    agent_context: Optional[Dict[str, Any]] = None,
    *,
    project_root: Optional[Path] = None,
) -> tuple[list[Dict[str, Any]], Dict[str, Any]]:
    if not strategy_signals:
        now_iso = datetime.now(timezone.utc).isoformat()
        return (
            [{
                "signal_id": f"empty_scheduler_{int(time.time())}",
                "timestamp": now_iso,
                "symbol": "XAUUSD",
                "action": "HOLD",
                "side": "NONE",
                "mode": "UNCERTAIN",
                "governor_status": "REJECTED",
                "reject_reason": "EMPTY_SCHEDULER_SNAPSHOT",
                "trace": {"confidence": 0.0, "evidence": ["empty_scheduler_snapshot"], "risk_flags": ["SCHEDULER_EMPTY"]},
            }],
            {
                "action": "HOLD",
                "side": "NONE",
                "net_score": 0.0,
                "confidence": 0.0,
                "contributors": [],
                "blocked_reasons": ["EMPTY_SCHEDULER_SNAPSHOT"],
                "selected_instance_id": "",
                "selected_strategy_id": "",
            },
        )

    contributors: list[Dict[str, Any]] = []
    blocked_reasons: list[str] = []
    selected_signal: Optional[Dict[str, Any]] = None
    selected_executable_signal: Optional[Dict[str, Any]] = None
    arbitration_meta: Dict[str, Any] = {}
    scheduler_selected_instance_id = str(scheduler_snapshot.get("selected_instance_id", "") or "").strip()
    route = scheduler_snapshot.get("route", {}) if isinstance(scheduler_snapshot.get("route", {}), dict) else {}
    session_current_policy = route.get("session_current_policy", {}) if isinstance(route.get("session_current_policy", {}), dict) else {}
    session_risk_scale = float(session_current_policy.get("risk_scale", 1.0) or 1.0)
    compatible_candidate_strategy_ids = {
        canonical_strategy_id(item)
        for item in (
            route.get("compatible_candidate_strategy_ids", [])
            if isinstance(route.get("compatible_candidate_strategy_ids", []), list)
            else []
        )
        if canonical_strategy_id(item)
    }
    strategy_by_instance_id = {
        str(signal.get("_instance_id", signal.get("instance_id", "")) or "").strip(): canonical_strategy_id(
            signal.get("_sig_strategy_id", signal.get("strategy_id", ""))
        )
        for signal in strategy_signals
        if str(signal.get("_instance_id", signal.get("instance_id", "")) or "").strip()
    }
    raw_decision_policy = (
        (agent_context or {}).get("decision_policy", {})
        if isinstance((agent_context or {}).get("decision_policy", {}), dict)
        else {}
    )
    decision_policy = _normalize_decision_policy(raw_decision_policy or scheduler_snapshot.get("decision_policy", {}))
    interpreter_context = (
        (agent_context or {}).get("interpreter", {})
        if isinstance((agent_context or {}).get("interpreter", {}), dict)
        else {}
    )
    for signal in strategy_signals:
        if not isinstance(signal, dict):
            continue
        signal.setdefault("_raw_action", str(signal.get("action", "HOLD") or "HOLD").upper())
        signal.setdefault("_raw_side", str(signal.get("side", "NONE") or "NONE").upper())
        if isinstance(signal.get("materialized_opportunity", {}), dict):
            signal.setdefault("_raw_materialized_opportunity", deepcopy(signal.get("materialized_opportunity", {})))
        else:
            signal.setdefault("_raw_materialized_opportunity", {})
    execution_constraint = _resolve_execution_constraint(agent_context or {}, route, decision_policy)
    execution_constraint_payload = _execution_constraint_to_dict(execution_constraint)
    route_selection = route.get("selection", {}) if isinstance(route.get("selection", {}), dict) else {}
    canonical_market_snapshot = (
        route.get("canonical_market_snapshot", {})
        if isinstance(route.get("canonical_market_snapshot", {}), dict)
        else {}
    )
    route_family_value = str(route.get("route_family", "") or "").strip().lower()
    candidate_pool_mode = _route_prefers_candidate_pool(route_family_value)

# ── Tactical Scheduler: market-fingerprint-driven dynamic weights ──
    tactical_weights: Dict[str, float] = {}
    try:
        bars_for_fingerprint = (canonical_market_snapshot.get("bars") or
                                (agent_context or {}).get("bars") or [])
        if isinstance(bars_for_fingerprint, list) and len(bars_for_fingerprint) >= 20:
            fp = extract_market_fingerprint(
                bars_for_fingerprint,
                session_bucket=str(route.get("session_bucket", "NORMAL") or "NORMAL"),
                regime_label=str(route.get("regime", "UNKNOWN") or "UNKNOWN"),
                indicators=canonical_market_snapshot.get("indicators"),
            )
            base_weights = {
                sid: float(scheduler_snapshot.get("strategy_weights", {}).get(sid, 0.10) or 0.10)
                for sid in scheduler_snapshot.get("active_strategy_ids", [])
                if isinstance(sid, str) and sid.strip()
            }
            if not base_weights:
                base_weights = {"ema_macd": 0.18, "composite": 0.12, "wyckoff": 0.11,
                                "rsi_extreme": 0.12, "boll_band": 0.10, "key_level": 0.08,
                                "grid_osc": 0.08, "pullback_fade": 0.10, "adx_breakout": 0.11}
            scored = score_all_strategies(
                fp,
                route_weights=base_weights,
                session_bucket=str(route.get("session_bucket", "NORMAL") or "NORMAL"),
            )
            # Phase 3: apply dimension coefficients
            try:
                _dc_path = project_root / "data" / "learning" / "dimension_coefficients.json"
                if _dc_path.exists():
                    _dc = json.loads(_dc_path.read_text(encoding="utf-8"))
                    _coeffs = _dc.get("coefficients", {})
                    if _coeffs:
                        def _clamp_exp(v):
                            return max(0.5, min(1.5, float(v)))
                        _e_dir = _clamp_exp(_coeffs.get("direction", {}).get("exp", 1.0))
                        _e_tim = _clamp_exp(_coeffs.get("timing", {}).get("exp", 1.0))
                        _e_reg = _clamp_exp(_coeffs.get("regime_fit", {}).get("exp", 1.0))
                        _e_anti = _clamp_exp(_coeffs.get("anti_fade", {}).get("exp", 1.0))
                        _e_ses = _clamp_exp(_coeffs.get("session", {}).get("exp", 1.0))
                        for _sid, _s in scored.items():
                            _raw = _s["direction_score"] * _s["timing_score"] * _s["regime_fit"] * _s["anti_fade"] * _s["session_mult"]
                            _adj = (_s["direction_score"] ** _e_dir) * (_s["timing_score"] ** _e_tim) * (_s["regime_fit"] ** _e_reg) * (_s["anti_fade"] ** _e_anti) * (_s["session_mult"] ** _e_ses)
                            if _raw > 0 and _adj > 0:
                                _ratio = _adj / _raw
                                _s["final_weight"] = max(0.001, min(0.50, _s["final_weight"] * _ratio))
                                _s["dimension_coefficient_ratio"] = round(_ratio, 4)
            except Exception:
                pass
            tactical_weights = {sid: s["final_weight"] for sid, s in scored.items()}
            # Phase 1: log shadow decisions
            try:
                _sd_sid = int(time.time() * 1000000) % 10**12
                _sd_path = project_root / "logs" / "shadow_decisions.ndjson"
                _sd_path.parent.mkdir(parents=True, exist_ok=True)
                _sd_entry = json.dumps({
                    "event": "shadow_open", "shadow_id": _sd_sid,
                    "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
                    "fingerprint": {
                        "regime": fp.get("_regime", "UNKNOWN"),
                        "session": route.get("session_bucket", "NORMAL"),
                        "adx": fp.get("adx", 0), "efficiency": fp.get("efficiency", 0),
                        "vol_ratio": fp.get("bar_range_ratio", 0), "atr_expansion": fp.get("atr_expansion", 1.0),
                    },
                    "candidates": [
                        {"strategy_id": sid, "score": s["final_weight"],
                         "direction_score": s["direction_score"], "timing_score": s["timing_score"],
                         "regime_fit": s["regime_fit"], "anti_fade": s["anti_fade"],
                         "session_mult": s["session_mult"]}
                        for sid, s in scored.items()
                    ],
                }, ensure_ascii=False)
                with open(_sd_path, "a", encoding="utf-8") as _sd_f:
                    _sd_f.write(_sd_entry + "\n")
            except Exception:
                pass
            # GARCH risk: vol regime and strategy family preference
            try:
                from bridge.garch_volatility import get_vol_multiplier
                garch_raw = get_vol_multiplier(refresh=False)
                if isinstance(garch_raw, dict) and garch_raw.get("status") in ("ok", "stale"):
                    garch_risk = garch_risk_multiplier(
                        cond_vol=garch_raw.get("current_vol", 0),
                        long_run_vol=garch_raw.get("long_run_vol", 0),
                        alpha=garch_raw.get("alpha", 0),
                        beta=garch_raw.get("beta", 0),
                        annual_vol=garch_raw.get("annualized_vol_pct", 0),
                    )
                    vol_prefer = garch_risk.get("prefer_strategy_family")
                    vol_mult = garch_risk.get("multiplier", 1.0)
                    for sid in list(tactical_weights.keys()):
                        profile = STRATEGY_PROFILES.get(sid, {})
                        fam = profile.get("family", "")
                        if vol_prefer == "trend" and fam == "range":
                            tactical_weights[sid] *= 0.60
                        elif vol_prefer == "range" and fam == "trend":
                            tactical_weights[sid] *= 0.60
                    if vol_mult != 1.0:
                        for sid in list(tactical_weights.keys()):
                            tactical_weights[sid] *= vol_mult
                    tactical_weights["_garch_risk"] = garch_risk
            except Exception:
                pass
            # Pattern boost: amplify tactical_weights for winning combinations
            try:
                _wp_path = project_root / "data" / "learning" / "win_patterns_cache.json"
                if _wp_path.exists():
                    _wp = json.loads(_wp_path.read_text(encoding="utf-8"))
                    _wp_patterns = _wp.get("patterns", [])
                    _wp_patterns = [p for p in _wp_patterns if p.get("active", True)]
                    if _wp_patterns:
                        _cur_regime = str(route.get("regime", "UNKNOWN")).upper()
                        _cur_session = str(route.get("session_bucket", "NORMAL")).upper()
                        for _wp_pat in _wp_patterns:
                            _conds = _wp_pat.get("conditions", [])
                            _wr = _wp_pat.get("win_rate", 0.5)
                            _n = _wp_pat.get("sample_size", 0)
                            if _n < 10 or _wr < 0.60:
                                continue
                            _match = True; _boost_sid = None
                            for _c in _conds:
                                _f = _c.get("feature", ""); _v = str(_c.get("value", "")).upper()
                                if _f == "regime" and _cur_regime != _v: _match = False; break
                                if _f == "session" and _cur_session != _v: _match = False; break
                                if _f == "strategy_id": _boost_sid = _v
                            if _match and _boost_sid and _boost_sid in tactical_weights:
                                _boost = min(1.20, 1.0 + (_wr - 0.50) * 0.4)
                                tactical_weights[_boost_sid] *= _boost
                                tactical_weights[f"_pattern_boost_{_boost_sid}"] = round(_boost, 3)
            except Exception:
                pass
            # Feedback correction: apply learned bandit coefficients
            try:
                _fb_coeffs = build_correction_coefficients(project_root)
                if _fb_coeffs:
                    apply_feedback_correction(tactical_weights, _fb_coeffs)
            except Exception:
                pass
    except Exception:
        tactical_weights = {}

    policy_entry_policy = str(
        decision_policy.get("execution", {}).get("entry_policy", "")
        if isinstance(decision_policy.get("execution", {}), dict)
        else ""
    ).strip()
    entry_policy = policy_entry_policy or str(route.get("entry_policy", "") or "").strip()
    if entry_policy == "regime_directed" and not candidate_pool_mode:
        candidate_pool_mode = False
        route_selection["selection_mode"] = "route_order"
    elif entry_policy == "regime_directed" and candidate_pool_mode:
        route_selection["selection_mode"] = "candidate_pool"
    elif candidate_pool_mode and not str(route_selection.get("selection_mode", "") or "").strip():
        route_selection["selection_mode"] = "candidate_pool"
    route_selection_mode = str(route_selection.get("selection_mode", "") or "").strip()
    route_order_missing_winner = (
        route_selection_mode == "route_order"
        and not str(route_selection.get("winner_instance_id", "") or "").strip()
        and not str(route_selection.get("winner_strategy_id", "") or "").strip()
    )
    _trace_selection_state(
        project_root,
        "aggregate_entry_policy_applied",
        route,
        selection=route_selection,
        extra={
            "policy_entry_policy": policy_entry_policy,
            "effective_entry_policy": entry_policy,
            "candidate_pool_mode": candidate_pool_mode,
        },
    )
    if route_order_missing_winner:
        route["selected_instance_id"] = ""
        route["selected_strategy_id"] = ""
        route["entry_owner_instance_id"] = ""
        route["entry_owner_strategy_id"] = ""
    elif compatible_candidate_strategy_ids:
        scheduler_selected_strategy_id = strategy_by_instance_id.get(scheduler_selected_instance_id, "")
        if scheduler_selected_strategy_id and scheduler_selected_strategy_id not in compatible_candidate_strategy_ids:
            scheduler_selected_instance_id = ""
        route_selected_instance_id = str(route.get("entry_owner_instance_id", "") or "").strip()
        route_selected_strategy_id = canonical_strategy_id(
            route.get("entry_owner_strategy_id", route.get("selected_strategy_id", ""))
        )
        if route_selected_strategy_id and route_selected_strategy_id not in compatible_candidate_strategy_ids:
            route_selected_instance_id = ""
            route_selected_strategy_id = ""
        if not route_selected_instance_id:
            for signal in strategy_signals:
                candidate_instance_id = str(signal.get("_instance_id", signal.get("instance_id", "")) or "").strip()
                candidate_strategy_id = canonical_strategy_id(
                    signal.get("_sig_strategy_id", signal.get("strategy_id", ""))
                )
                if candidate_strategy_id in compatible_candidate_strategy_ids:
                    route_selected_instance_id = candidate_instance_id
                    route_selected_strategy_id = candidate_strategy_id
                    break
        if route_selected_instance_id:
            route["entry_owner_instance_id"] = route_selected_instance_id
        if route_selected_strategy_id:
            route["entry_owner_strategy_id"] = route_selected_strategy_id
    entry_owner_instance_id = (
        ""
        if route_order_missing_winner
        else str(route.get("entry_owner_instance_id", scheduler_selected_instance_id) or scheduler_selected_instance_id).strip()
    )
    entry_owner_override = next(
        (
            str(signal.get("_entry_owner_override", "") or "").strip()
            for signal in strategy_signals
            if str(signal.get("_entry_owner_override", "") or "").strip()
        ),
        "",
    )
    entry_failover_reason = next(
        (
            str(signal.get("_entry_failover_reason", "") or "").strip()
            for signal in strategy_signals
            if str(signal.get("_entry_failover_reason", "") or "").strip()
        ),
        "",
    )
    strict_selected_entry = entry_policy == "selected_strategy_only"
    health_weighted_vote = entry_policy == "health_weighted_vote"
    any_actionable = entry_policy == "any_actionable"
    regime_directed = entry_policy == "regime_directed"
    single_entry_enforced = strict_selected_entry or regime_directed
    occupied_strategy_ids: set[str] = set()
    if single_entry_enforced and project_root is not None:
        runtime_guard = _load_runtime_open_guard_state(project_root)
        runtime_positions = (
            runtime_guard.get("positions_all", [])
            if isinstance(runtime_guard.get("positions_all", []), list)
            else []
        )
        occupied_strategy_ids = {
            canonical_strategy_id(position.get("strategy_id", ""))
            for position in runtime_positions
            if isinstance(position, dict) and canonical_strategy_id(position.get("strategy_id", ""))
        }
        if occupied_strategy_ids:
            for signal in strategy_signals:
                if not isinstance(signal, dict):
                    continue
                signal_strategy_id = canonical_strategy_id(
                    signal.get("_sig_strategy_id", signal.get("strategy_id", ""))
                )
                raw_action = str(signal.get("_raw_action", signal.get("action", "HOLD")) or "HOLD").upper()
                if signal_strategy_id not in occupied_strategy_ids or raw_action != "OPEN":
                    continue
                reject_reason = f"SINGLE_ENTRY_DUPLICATE_STRATEGY_BLOCK:{signal_strategy_id}"
                signal["action"] = "HOLD"
                signal["side"] = "NONE"
                signal["reject_reason"] = reject_reason
                trace = signal.get("trace", {}) if isinstance(signal.get("trace", {}), dict) else {}
                evidence = trace.get("evidence", []) if isinstance(trace.get("evidence", []), list) else []
                evidence.append(f"single_entry_existing_position:{signal_strategy_id}")
                trace["evidence"] = evidence
                signal["trace"] = trace
    performance_factors = _load_pretrade_performance_factors(agent_context)

    # 鈹€鈹€ Regime-directed: LLM 瀹氭柟鍚?鈫?Regime 閫夌瓥鐣?鈫?绛栫暐鎵ц 鈹€鈹€
    if regime_directed and strategy_signals:
        try:
            raw_regime = str(route.get("regime", "UNKNOWN") or "UNKNOWN")
            raw_subtype = str(route.get("regime_subtype", "") or "").upper()
            arbitration_session_bucket = str(
                route.get(
                    "session_bucket",
                    route.get(
                        "session_mode",
                        (
                            canonical_market_snapshot.get("normalized", {})
                            if isinstance(canonical_market_snapshot.get("normalized", {}), dict)
                            else {}
                        ).get("session_bucket", ""),
                    ),
                )
                or ""
            ).upper()
            regime_label = LLM_TO_REGIME.get(raw_regime, raw_regime)
            arbitration_payload: list[Dict[str, Any]] = []
            for signal in strategy_signals:
                signal_trace = signal.get("trace", {}) if isinstance(signal.get("trace", {}), dict) else {}
                arbitration_payload.append(
                    {
                        **signal,
                        "_original": signal,
                        "strategy_id": canonical_strategy_id(
                            signal.get("strategy_id", signal.get("_sig_strategy_id", ""))
                        ),
                        "confidence": float(signal_trace.get("confidence", 0.0) or 0.0),
                        "timeframe": str(signal.get("timeframe", "M5") or "M5").upper(),
                        "session_mode": arbitration_session_bucket,
                        "regime_confidence": float(route.get("regime_confidence", 1.0) or 1.0),
                    }
                )
            arbitration = _call_signal_arbitrator(
                arbitration_payload,
                regime=regime_label,
                route_family=route_family_value,
                garbage_allowed_types=route.get("garbage_allowed_types"),
                garbage_size_multiplier=float(route.get("garbage_size_multiplier", 1.0) or 1.0),
            )
            arbitration_meta = arbitration.get("meta", {}) if isinstance(arbitration.get("meta", {}), dict) else {}
            if raw_subtype in SESSION_NOISE_BLOCK:
                session_rejected = []
                for accepted in arbitration.get("accepted", []):
                    original = accepted.get("_original")
                    sid = str(accepted.get("strategy_id", "") or "")
                    stype = strategy_style(sid)
                    if stype not in {"mean_reversion", "hybrid"}:
                        session_rejected.append(
                            {
                                **accepted,
                                "_block_reason": f"SESSION_NOISE_GATE:{raw_subtype}:{stype}_not_allowed",
                                "_original": original,
                            }
                        )
                if session_rejected:
                    arbitration["rejected"] = list(arbitration.get("rejected", [])) + session_rejected
                    rejected_instance_ids = {str(item.get("strategy_instance_id") or "") for item in session_rejected if str(item.get("strategy_instance_id") or "")}
                    arbitration["accepted"] = [
                        item for item in arbitration.get("accepted", [])
                        if str(item.get("strategy_instance_id") or "") not in rejected_instance_ids
                    ]
                    arbitration_meta["session_noise_gate"] = {
                        "subtype": raw_subtype,
                        "blocked": len(session_rejected),
                    }
            rejected_instance_ids = set()
            for rejected in arbitration.get("rejected", []):
                original = rejected.get("_original")
                if not isinstance(original, dict):
                    continue
                rejected_instance_ids.add(str(rejected.get("strategy_instance_id") or ""))
                original["action"] = "HOLD"
                original["side"] = "NONE"
                original["reject_reason"] = str(
                    rejected.get("_block_reason") or "SIGNAL_ARBITRATOR_BLOCK"
                )
                trace = original.get("trace", {}) if isinstance(original.get("trace", {}), dict) else {}
                evidence = trace.get("evidence", []) if isinstance(trace.get("evidence", []), list) else []
                evidence.append(f"signal_arbitrator:{original['reject_reason']}")
                trace["evidence"] = evidence
                original["trace"] = trace
            accepted_instance_ids = {
                str(item.get("strategy_instance_id") or "")
                for item in arbitration.get("accepted", [])
                if str(item.get("strategy_instance_id") or "")
            }
            for accepted in arbitration.get("accepted", []):
                original = accepted.get("_original")
                if not isinstance(original, dict):
                    continue
                if "volume_plan" in accepted:
                    volume_plan = float(accepted.get("volume_plan", 0.0) or 0.0)
                    if volume_plan > 0.0:
                        original["volume_plan"] = volume_plan
                        original["volume_lots"] = volume_plan
                if accepted.get("_volume_scaled"):
                    original["_volume_scaled"] = True
                    original["_volume_original"] = float(accepted.get("_volume_original", 0.0) or 0.0)
            for signal in strategy_signals:
                signal_instance_id = str(signal.get("_instance_id") or signal.get("instance_id") or canonical_strategy_id(signal.get("strategy_id", signal.get("_sig_strategy_id", ""))) or "")
                if signal_instance_id in rejected_instance_ids:
                    continue
                if accepted_instance_ids and signal_instance_id not in accepted_instance_ids:
                    signal["action"] = "HOLD"
                    signal["side"] = "NONE"
                    existing_reason = str(
                        signal.get("reject_reason")
                        or signal.get("_block_reason")
                        or signal.get("hold_reason")
                        or ""
                    ).strip()
                    signal["reject_reason"] = (
                        existing_reason
                        or str(arbitration_meta.get("reason", "") or "SIGNAL_ARBITRATOR_NOT_ACCEPTED")
                    )
                    trace = signal.get("trace", {}) if isinstance(signal.get("trace", {}), dict) else {}
                    evidence = trace.get("evidence", []) if isinstance(trace.get("evidence", []), list) else []
                    evidence.append(f"signal_arbitrator:{signal['reject_reason']}")
                    trace["evidence"] = evidence
                    signal["trace"] = trace
            _run_exit_monitoring_cycle._regime_gate_fail_count = 0
        except Exception as e:
            try:
                from bridge.gold_trader import _daemon_log
                _daemon_log(
                    f"[REGIME_GATE] signal_arbitrator failed: {e} 鈥?"
                    f"forcing HOLD on {len(strategy_signals)} signals",
                    "ERROR",
                )
            except Exception:
                pass
            # Count consecutive failures and escalate if persistent
            _regime_gate_fail_count = getattr(
                _run_exit_monitoring_cycle, '_regime_gate_fail_count', 0
            ) + 1
            _run_exit_monitoring_cycle._regime_gate_fail_count = _regime_gate_fail_count
            if _regime_gate_fail_count >= 3:
                try:
                    from bridge.gold_trader import _daemon_log
                    _daemon_log(
                        f"[REGIME_GATE] FAILED {_regime_gate_fail_count} consecutive times 鈥?"
                        f"system health degraded, check signal_arbitrator module",
                        "CRITICAL",
                    )
                except Exception:
                    pass
            for signal in strategy_signals:
                signal["action"] = "HOLD"
                signal["side"] = "NONE"
                signal["reject_reason"] = "SIGNAL_ARBITRATOR_FAILURE_CLOSED"
    # 鈹€鈹€ End regime-directed 鈹€鈹€
    if single_entry_enforced and entry_owner_override:
        entry_owner_instance_id = entry_owner_override

    # 鈹€鈹€ Entry owner crash / stale failover: 濡傛灉 owner 瀹炰緥娌℃湁 OPEN 淇″彿锛堝穿婧?琚Щ闄わ級锛岃嚜鍔ㄥ垏鎹㈠埌鏈€浣冲€欓€?鈹€鈹€
    if single_entry_enforced and entry_owner_instance_id:
        owner_alive = any(
            str(s.get("_instance_id", "") or "").strip() == entry_owner_instance_id
            and str(s.get("action", "HOLD") or "HOLD").upper() == "OPEN"
            for s in strategy_signals
        )
        if not owner_alive:
            best_fallback = max(
                (
                    s for s in strategy_signals
                    if str(s.get("_instance_id", "") or "").strip() != entry_owner_instance_id
                    and str(s.get("action", "HOLD") or "HOLD").upper() == "OPEN"
                ),
                key=lambda s: (
                    abs(_signal_signed_score(s, float(s.get("instance_weight", 0.0) or 0.0))),
                    float((s.get("trace", {}) or {}).get("confidence", 0.0) or 0.0),
                ),
                default=None,
            )
            if best_fallback is not None:
                fallback_id = str(best_fallback.get("_instance_id", "") or "").strip()
                if fallback_id:
                    entry_owner_instance_id = fallback_id
                    entry_failover_reason = "ENTRY_OWNER_CRASH_FAILOVER"

    # 鈹€鈹€ Health-weighted voting weights 鈹€鈹€
    voting_weights: Dict[str, float] = {}
    _cached_health: Dict[str, Any] = {}
    if project_root is not None:
        _instances = scheduler_snapshot.get("instances", []) if isinstance(scheduler_snapshot.get("instances", []), list) else []
        _cached_health = get_intelligence_hub().refresh_all(project_root, _instances)
        if health_weighted_vote:
            regime_for_vote = str(route.get("regime", "") or "")
            voting_weights = get_intelligence_hub().compute_voting_weights(_cached_health, current_regime=regime_for_vote)

    selected_score = 0.0
    selected_executable_score = 0.0
    raw_net_score = 0.0
    eligible_weight_total = 0.0
    eligible_count = 0
    executable_weight_total = 0.0
    executable_count = 0
    min_confidence = float(
        decision_policy.get("execution", {}).get(
            "min_confidence",
            config_manager.get_effective_min_confidence_threshold(
                DEFAULT_EXECUTION_MIN_CONFIDENCE
            ),
        )
        or config_manager.get_effective_min_confidence_threshold(
            DEFAULT_EXECUTION_MIN_CONFIDENCE
        )
    )
    effective_min_conf = min_confidence  # base; overridden per-signal with regime/adaptive adjustments
    for signal in strategy_signals:
        instance_id = str(signal.get("_instance_id", "")).strip()
        strategy_id = str(signal.get("_sig_strategy_id", signal.get("strategy_id", "")) or "").strip()
        action = str(signal.get("action", "HOLD") or "HOLD").upper()
        if action == "OPEN":
            _apply_signal_performance_profile(signal, agent_context, route, performance_factors)
            action = str(signal.get("action", "HOLD") or "HOLD").upper()
        if single_entry_enforced and action == "OPEN":
            if not entry_owner_instance_id:
                signal["action"] = "HOLD"
                signal["side"] = "NONE"
                signal["reject_reason"] = "ENTRY_SELECTION_MISSING"
            elif instance_id != entry_owner_instance_id:
                signal["action"] = "HOLD"
                signal["side"] = "NONE"
                signal["reject_reason"] = "ENTRY_SELECTION_MISMATCH"
        weight = float(signal.get("instance_weight", 0.0) or 0.0)
        # Health-weighted vote: adjust instance weight by health-derived voting power
        if health_weighted_vote and voting_weights:
            vote_w = voting_weights.get(instance_id, weight)
            if vote_w <= 0.0 and action == "OPEN":
                signal["action"] = "HOLD"
                signal["side"] = "NONE"
                signal["reject_reason"] = "HEALTH_VOTE_ZERO_WEIGHT"
            else:
                weight = vote_w
                signal["instance_weight"] = round(vote_w, 4)
        if action == "OPEN" and signal.get("_counter_direction_penalty"):
            macro_policy = decision_policy.get("macro", {}) if isinstance(decision_policy.get("macro", {}), dict) else {}
            countertrend_scale = float(macro_policy.get("countertrend_scale", 0.35) or 0.35)
            entry_size_scale = float(signal.get("entry_size_scale", 1.0) or 1.0)
            signal["entry_size_scale"] = round(min(entry_size_scale, countertrend_scale), 4)
        opportunity = _build_strategy_opportunity(signal, interpreter_context)
        # Apply regime-aware and market-adaptive confidence adjustments
        regime_params = agent_context.get("regime_params", {})
        regime_min_conf = float(regime_params.get("min_confidence", 0.0) or 0.0)
        adaptive_profile = agent_context.get("adaptive_profile", {})
        confidence_delta = float(adaptive_profile.get("confidence_delta", 0.0) or 0.0)
        # delta negative 鈫?looser threshold (boost), positive 鈫?stricter (reduce)
        effective_min_conf = max(min_confidence - confidence_delta, 0.05)
        if regime_min_conf > 0:
            effective_min_conf = max(effective_min_conf, regime_min_conf)
        # Apply session-level confidence override if configured
        session_current_policy = route.get("session_current_policy", {}) if isinstance(route.get("session_current_policy", {}), dict) else {}
        session_min_conf_override = float(session_current_policy.get("min_confidence_override", 0.0) or 0.0)
        if session_min_conf_override > 0.0:
            effective_min_conf = max(0.05, min(0.99, session_min_conf_override))
        materialized = _materialize_opportunity(opportunity, execution_constraint, min_confidence=effective_min_conf)
        opportunity_payload = _strategy_opportunity_to_dict(opportunity)
        materialized_payload = _materialized_opportunity_to_dict(materialized)
        signal["opportunity"] = opportunity_payload
        signal["execution_constraint"] = execution_constraint_payload
        signal["materialized_opportunity"] = materialized_payload
        signed_score = (
            _opportunity_signed_score(opportunity, weight)
            if materialized.eligible_for_aggregation
            else 0.0
        )
        # Apply session-level score bias for soft-weight mode
        soft_score_bias = float(session_current_policy.get("soft_score_bias", 0.0) or 0.0)
        if soft_score_bias != 0.0:
            signed_score = max(-1.0, min(1.0, signed_score + soft_score_bias))
        confidence = float((signal.get("trace", {}) or {}).get("confidence", 0.0) or 0.0)
        reason = str(signal.get("reject_reason", "") or "")
        contributor = {
            "instance_id": instance_id,
            "strategy_id": strategy_id,
            "action": str(signal.get("action", "HOLD") or "HOLD"),
            "side": str(signal.get("side", "NONE") or "NONE"),
            "weight": round(weight, 4),
            "confidence": round(confidence, 4),
            "signed_score": round(signed_score, 6),
            "reason": reason,
            "directional_view": opportunity.directional_view,
            "setup_quality": opportunity.setup_quality,
            "entry_style_preference": opportunity.entry_style_preference,
            "urgency": opportunity.urgency,
            "materialized_action": materialized.materialized_action,
            "materialized_reason": materialized.reason,
            "eligible_for_aggregation": materialized.eligible_for_aggregation,
            "performance_live_multiplier": round(
                float(
                    (
                        signal.get("performance_profile", {})
                        if isinstance(signal.get("performance_profile", {}), dict)
                        else {}
                    ).get("live_multiplier", 1.0)
                    or 1.0
                ),
                4,
            ),
            "performance_confidence_bias": round(
                float(
                    (
                        signal.get("performance_profile", {})
                        if isinstance(signal.get("performance_profile", {}), dict)
                        else {}
                    ).get("confidence_bias", 0.0)
                    or 0.0
                ),
                4,
            ),
            "performance_block": bool(
                (
                    signal.get("performance_profile", {})
                    if isinstance(signal.get("performance_profile", {}), dict)
                    else {}
                ).get("block", False)
            ),
        }
        contributors.append(contributor)
        raw_net_score += signed_score
        if reason:
            blocked_reasons.append(f"{instance_id}:{reason}" if instance_id else reason)
        elif materialized.materialized_action == "BLOCKED":
            blocked_reasons.append(f"{instance_id}:{materialized.reason}" if instance_id else materialized.reason)
        if abs(signed_score) >= abs(selected_score):
            selected_signal = signal
            selected_score = signed_score
        if materialized.eligible_for_aggregation:
            eligible_count += 1
            eligible_weight_total += max(0.0, weight)
        if materialized.final_action == "OPEN" and materialized.final_side in {"BUY", "SELL"}:
            executable_count += 1
            executable_weight_total += max(0.0, weight)
            if abs(signed_score) >= abs(selected_executable_score):
                selected_executable_signal = signal
                selected_executable_score = signed_score

    # 鈹€鈹€ Cross-strategy correlation evaluation 鈹€鈹€
    correlation: Dict[str, Any] = {"consensus": "SINGLE_SIGNAL", "confidence_adjustment": 1.0, "volume_adjustment": 1.0, "cooldown_adjustment_sec": 0, "net_bias": "NONE"}
    risk_config = decision_policy.get("risk", {}) if isinstance(decision_policy, dict) else {}
    corr_config = risk_config.get("cross_strategy", {}).get("correlation_tracking", {}) if isinstance(risk_config.get("cross_strategy", {}), dict) else {}
    if isinstance(corr_config, dict) and corr_config.get("enabled", True) and project_root is not None and _cached_health:
        correlation = get_intelligence_hub().evaluate_cross_strategy_correlation(strategy_signals, _cached_health)
        # Apply correlation adjustments
        if correlation["consensus"] == "DIVERGENT":
            divergence_penalty = float(corr_config.get("divergence_confidence_penalty", 0.4) or 0.4)
            divergence_vol_penalty = float(corr_config.get("divergence_volume_penalty", 0.6) or 0.6)
            correlation["confidence_adjustment"] = max(0.5, 1.0 - divergence_penalty)
            correlation["volume_adjustment"] = max(0.4, 1.0 - divergence_vol_penalty)
            correlation["cooldown_adjustment_sec"] = int(corr_config.get("divergence_cooldown_sec", 120) or 120)
        elif correlation["consensus"] in {"ALIGNED_BUY", "ALIGNED_SELL"}:
            alignment_bonus = float(corr_config.get("alignment_volume_bonus", 0.05) or 0.05)
            correlation["confidence_adjustment"] = 1.0
            correlation["volume_adjustment"] = min(1.15, 1.0 + alignment_bonus * len(correlation.get("aligned_instances") or []))
            correlation["cooldown_adjustment_sec"] = -30

    effective_net_score = raw_net_score
    if eligible_weight_total > 0.0:
        effective_net_score = raw_net_score / eligible_weight_total
    # Apply correlation confidence adjustment
    effective_net_score *= float(correlation.get("confidence_adjustment", 1.0) or 1.0)
    effective_net_score = max(-0.99, min(0.99, effective_net_score))
    aggregated_confidence = round(abs(effective_net_score), 4)
    net_side = "BUY" if effective_net_score > 0 else "SELL" if effective_net_score < 0 else "NONE"
    hold_reason = ""
    eligible_weight_floor = min(0.18, max(0.05, config_manager.get_effective_min_confidence_threshold(DEFAULT_EXECUTION_MIN_CONFIDENCE) * 0.8))

    # 鈹€鈹€ Aggregation payload (metadata shared across all dispatched signals) 鈹€鈹€
    aggregation_payload: Dict[str, Any] = {}
    dispatched_signals: list[Dict[str, Any]] = []

    # 鈹€鈹€ Multi-dispatch branch: regime_directed 鈫?each strategy independently executes 鈹€鈹€
    if regime_directed and not single_entry_enforced:
        # Collect all independently actionable signals (each strategy decides for itself)
        for signal in strategy_signals:
            instance_id = str(signal.get("_instance_id", "")).strip()
            strategy_id = str(signal.get("_sig_strategy_id", signal.get("strategy_id", "")) or "").strip()
            action = str(signal.get("action", "HOLD") or "HOLD").upper()
            side = str(signal.get("side", "NONE") or "NONE").upper()
            reason = str(signal.get("reject_reason", "") or "")
            opportunity_payload = signal.get("opportunity", {}) if isinstance(signal.get("opportunity", {}), dict) else {}
            materialized_payload = signal.get("materialized_opportunity", {}) if isinstance(signal.get("materialized_opportunity", {}), dict) else {}
            materialized_action = str(materialized_payload.get("materialized_action", signal.get("materialized_action", "")) or "")
            confidence = float((signal.get("trace", {}) or {}).get("confidence", 0.0) or 0.0)
            # A signal is actionable if: OPEN action, BUY/SELL side, no reject_reason, confidence >= min_confidence
            if action != "OPEN" or side not in {"BUY", "SELL"} or reason:
                continue
            mat_final_action = str(materialized_payload.get("final_action", materialized_action) or "").upper()
            mat_final_side = str(materialized_payload.get("final_side", side) or "").upper()
            if mat_final_action != "OPEN" or mat_final_side not in {"BUY", "SELL"}:
                continue
            if confidence < effective_min_conf:
                continue
            if candidate_pool_mode:
                ranked_candidates = (
                    route_selection.get("ranked_candidates", [])
                    if isinstance(route_selection.get("ranked_candidates", []), list)
                    else []
                )
                ranked_instance_ids = {
                    str(item.get("instance_id", "") or "").strip()
                    for item in ranked_candidates
                    if isinstance(item, dict) and str(item.get("instance_id", "") or "").strip()
                }
                if ranked_instance_ids and instance_id not in ranked_instance_ids:
                    continue
            # Build per-strategy dispatch-ready signal (deepcopy to isolate mutation in _dispatch_signal)
            dispatch_sig = deepcopy(signal)
            if len(strategy_signals) > 1:
                sig_fingerprint = hashlib.sha1(
                    f"{instance_id}:{strategy_id}:{int(time.time())}".encode("utf-8")
                ).hexdigest()[:8]
                dispatch_sig["signal_id"] = f"{str(signal.get('_base_signal_id', signal.get('signal_id', f'sig_{int(time.time())}')))}__multi_{sig_fingerprint}"
            dispatch_sig["action"] = mat_final_action
            dispatch_sig["side"] = mat_final_side
            dispatch_sig["entry_style"] = str(materialized_payload.get("entry_style", signal.get("entry_style", "MARKET")) or "MARKET")
            dispatch_sig["entry_size_scale"] = round(float(materialized_payload.get("size_scale", signal.get("entry_size_scale", 1.0)) or 1.0), 4)
            dispatch_sig["materialized_action"] = mat_final_action
            dispatch_sig["materialized_reason"] = str(materialized_payload.get("reason", "") or "")
            dispatch_sig["execution_constraint"] = execution_constraint_payload
            dispatch_sig["opportunity"] = opportunity_payload
            dispatch_sig["materialized_opportunity"] = materialized_payload
            dispatch_sig["_selected_instance_id"] = instance_id
            dispatch_sig["_selected_strategy"] = strategy_id
            dispatch_sig["_selection_regime"] = str(signal.get("selection_regime", signal.get("regime", "UNKNOWN")) or "UNKNOWN")
            dispatch_sig["_multi_dispatch"] = True
            dispatch_sig["_correlation_adjustment"] = {
                "consensus": str(correlation.get("consensus", "SINGLE_SIGNAL")),
                "volume_adjustment": round(float(correlation.get("volume_adjustment", 1.0) or 1.0), 4),
                "cooldown_adjustment_sec": int(correlation.get("cooldown_adjustment_sec", 0) or 0),
            }
            dispatch_sig["_session_risk_scale"] = session_risk_scale
            # Per-signal trace
            sig_trace = dispatch_sig.get("trace", {}) if isinstance(dispatch_sig.get("trace", {}), dict) else {}
            sig_trace["aggregation"] = {
                "eligible_count": eligible_count,
                "eligible_weight_total": round(eligible_weight_total, 6),
                "materialized_action": mat_final_action,
                "multi_dispatch": True,
                "correlation_consensus": str(correlation.get("consensus", "SINGLE_SIGNAL")),
            }
            sig_trace["confidence"] = confidence
            dispatch_sig["trace"] = sig_trace
            dispatched_signals.append(dispatch_sig)

        if not dispatched_signals:
            # No actionable signals 鈫?return single HOLD signal
            if strategy_signals:
                hold_sig = deepcopy(strategy_signals[0])
            else:
                hold_sig = {}
            hold_sig["action"] = "HOLD"; hold_sig["side"] = "NONE"
            hold_sig["reject_reason"] = hold_sig.get("reject_reason") or "MULTI_DISPATCH_NO_ACTIONABLE"
            hold_sig["materialized_action"] = "BLOCKED"
            hold_sig["materialized_reason"] = "MULTI_DISPATCH_NO_ACTIONABLE"
            hold_sig["_multi_dispatch"] = True
            dispatched_signals.append(hold_sig)
            hold_reason = "MULTI_DISPATCH_NO_ACTIONABLE"

        dispatch_owner_instance_id = str(
            route.get("entry_owner_instance_id", entry_owner_instance_id) or entry_owner_instance_id or ""
        ).strip()
        dispatch_owner_strategy_id = str(
            route.get("entry_owner_strategy_id")
            or route_selection.get("winner_strategy_id")
            or ""
        )
        if (
            compatible_candidate_strategy_ids
            and dispatch_owner_strategy_id
            and canonical_strategy_id(dispatch_owner_strategy_id) not in compatible_candidate_strategy_ids
        ):
            dispatch_owner_instance_id = ""
            dispatch_owner_strategy_id = ""
        if compatible_candidate_strategy_ids and not dispatch_owner_instance_id:
            for signal in strategy_signals:
                candidate_strategy_id = canonical_strategy_id(
                    signal.get("_sig_strategy_id", signal.get("strategy_id", ""))
                )
                candidate_instance_id = str(
                    signal.get("_instance_id", signal.get("instance_id", "")) or ""
                ).strip()
                if candidate_strategy_id in compatible_candidate_strategy_ids and candidate_instance_id:
                    dispatch_owner_instance_id = candidate_instance_id
                    if not dispatch_owner_strategy_id:
                        dispatch_owner_strategy_id = candidate_strategy_id
                    break
        if len(dispatched_signals) == 1:
            single_dispatch = dispatched_signals[0]
            single_dispatch_instance_id = str(
                single_dispatch.get("_selected_instance_id", "")
                or single_dispatch.get("_instance_id", "")
                or single_dispatch.get("instance_id", "")
                or ""
            ).strip()
            single_dispatch_strategy_id = str(
                single_dispatch.get("_selected_strategy", "")
                or single_dispatch.get("_sig_strategy_id", "")
                or single_dispatch.get("strategy_id", "")
                or ""
            ).strip()
            single_dispatch_action = str(single_dispatch.get("action", "HOLD") or "HOLD").upper()
            if (
                single_dispatch_instance_id
                and (
                    not dispatch_owner_instance_id
                    or dispatch_owner_instance_id.startswith("inst_")
                    or (
                        single_dispatch_action == "OPEN"
                        and dispatch_owner_instance_id != single_dispatch_instance_id
                    )
                )
            ):
                dispatch_owner_instance_id = single_dispatch_instance_id
            if (
                single_dispatch_strategy_id
                and (
                    not dispatch_owner_strategy_id
                    or (
                        single_dispatch_action == "OPEN"
                        and dispatch_owner_strategy_id != single_dispatch_strategy_id
                    )
                )
            ):
                dispatch_owner_strategy_id = single_dispatch_strategy_id
            if dispatch_owner_instance_id:
                route["entry_owner_instance_id"] = dispatch_owner_instance_id
                if route_selection_mode != "route_order":
                    route_selection["winner_instance_id"] = dispatch_owner_instance_id
            if dispatch_owner_strategy_id:
                route["entry_owner_strategy_id"] = dispatch_owner_strategy_id
                if route_selection_mode != "route_order":
                    route_selection["winner_strategy_id"] = dispatch_owner_strategy_id

        multi_action = "MULTI" if len(dispatched_signals) > 1 else str(dispatched_signals[0].get("action", "HOLD"))
        multi_side = "MULTI" if len({str(s.get("side","")).upper() for s in dispatched_signals}) > 1 else str(dispatched_signals[0].get("side", "NONE"))
        execution_decision = _build_execution_decision(
            action=multi_action,
            side=multi_side,
            selected_strategy_id=str(
                route_selection.get("winner_strategy_id")
                or dispatch_owner_strategy_id
                or ""
            ),
            selected_instance_id=str(
                route_selection.get("winner_instance_id")
                or dispatch_owner_instance_id
                or ""
            ),
            entry_owner_strategy_id=dispatch_owner_strategy_id,
            entry_owner_instance_id=dispatch_owner_instance_id,
            reason=str(hold_reason or entry_failover_reason or route_selection.get("reason", "") or ""),
            confidence=aggregated_confidence,
            route=route,
            canonical_market_snapshot=canonical_market_snapshot,
            selection=route_selection,
        )
        execution_decision["decision_source"] = "multi_dispatch_aggregation"
        aggregation_payload = {
            "action": multi_action,
            "side": multi_side,
            "multi_dispatch": True,
            "dispatch_count": len(dispatched_signals),
            "net_score": round(effective_net_score, 6),
            "raw_net_score": round(raw_net_score, 6),
            "confidence": aggregated_confidence,
            "contributors": contributors,
            "blocked_reasons": blocked_reasons,
            "entry_policy": entry_policy,
            "selection_mode": str(route_selection.get("selection_mode", "single_best") or "single_best"),
            "entry_owner_instance_id": dispatch_owner_instance_id,
            "entry_owner_strategy_id": dispatch_owner_strategy_id,
            "entry_failover_reason": entry_failover_reason,
            "eligible_count": eligible_count,
            "eligible_weight_total": round(eligible_weight_total, 6),
            "eligible_weight_floor": round(eligible_weight_floor, 6),
            "executable_count": executable_count,
            "executable_weight_total": round(executable_weight_total, 6),
            "voting_weights": {iid: round(w, 4) for iid, w in voting_weights.items()} if voting_weights else {},
            "execution_constraint": execution_constraint_payload,
            "correlation": {
                "consensus": str(correlation.get("consensus", "UNKNOWN") or "UNKNOWN"),
                "net_bias": str(correlation.get("net_bias", "NONE") or "NONE"),
                "confidence_adjustment": round(float(correlation.get("confidence_adjustment", 1.0) or 1.0), 4),
                "volume_adjustment": round(float(correlation.get("volume_adjustment", 1.0) or 1.0), 4),
                "cooldown_adjustment_sec": int(correlation.get("cooldown_adjustment_sec", 0) or 0),
                "aligned_instances": correlation.get("aligned_instances", []),
                "divergent_instances": correlation.get("divergent_instances", []),
            },
            "signal_arbitrator": arbitration_meta,
            "strategy_quality": {
                str(
                    signal.get("_sig_strategy_id", signal.get("strategy_id", "")) or ""
                ): (
                    signal.get("performance_profile", {})
                    if isinstance(signal.get("performance_profile", {}), dict)
                    else {}
                )
                for signal in strategy_signals
                if str(signal.get("_sig_strategy_id", signal.get("strategy_id", "")) or "").strip()
            },
            "per_instance_health": {
                iid: {
                    "score": round(h.score, 1),
                    "grade": h.grade,
                    "suggested_action": h.suggested_action,
                    "suggested_scale": round(h.suggested_scale, 4),
                    "loss_streak": h.loss_streak,
                    "best_regime": h.best_regime,
                    "current_regime_fitness": round(h.current_regime_fitness, 4),
                    "primary_loss_pattern": h.primary_loss_pattern,
                }
                for iid, h in _cached_health.items()
            } if project_root is not None else {},
            "scheduler_mode": str(scheduler_snapshot.get("mode", "single_active") or "single_active"),
            "scheduler_reason": str(scheduler_snapshot.get("reason", "runtime_scheduler") or "runtime_scheduler"),
            "decision_policy": decision_policy,
            "route": deepcopy(route),
            "canonical_market_snapshot": deepcopy(canonical_market_snapshot),
            "execution_decision": execution_decision,
            "agent_context": {
                "macro_direction": str(
                    ((agent_context or {}).get("macro", {}) if isinstance((agent_context or {}).get("macro", {}), dict) else {}).get("direction", "NEUTRAL")
                    or "NEUTRAL"
                ),
                "effective_macro_direction": str(
                    ((agent_context or {}).get("macro", {}) if isinstance((agent_context or {}).get("macro", {}), dict) else {}).get("effective_direction", "NEUTRAL")
                    or "NEUTRAL"
                ),
                "binding_macro_direction": _resolve_binding_direction(
                    ((agent_context or {}).get("macro", {}) if isinstance((agent_context or {}).get("macro", {}), dict) else {})
                ),
                "execution_advice": str(
                    ((agent_context or {}).get("interpreter", {}) if isinstance((agent_context or {}).get("interpreter", {}), dict) else {}).get("execution_advice", "ALLOW_FOLLOW")
                    or "ALLOW_FOLLOW"
                ),
                "structural_side": str(
                    ((agent_context or {}).get("interpreter", {}) if isinstance((agent_context or {}).get("interpreter", {}), dict) else {}).get("structural_side", "")
                    or ""
                ),
                "macro_state": str(
                    ((agent_context or {}).get("macro", {}) if isinstance((agent_context or {}).get("macro", {}), dict) else {}).get("state", "UNKNOWN")
                    or "UNKNOWN"
                ),
                "macro_bias_weight": float(
                    ((agent_context or {}).get("macro", {}) if isinstance((agent_context or {}).get("macro", {}), dict) else {}).get("bias_weight", 1.0)
                    or 1.0
                ),
                "route": (
                    (agent_context or {}).get("route", {})
                    if isinstance((agent_context or {}).get("route", {}), dict)
                    else {}
                ),
            },
        }
        if hold_reason:
            aggregation_payload["hold_reason"] = hold_reason
        _trace_selection_state(
            project_root,
            "aggregate_multi_dispatch_return",
            route,
            selection=route_selection,
            extra={
                "dispatch_count": len(dispatched_signals),
                "decision_source": str(execution_decision.get("decision_source", "") or ""),
            },
        )
        return dispatched_signals, aggregation_payload

    # 鈹€鈹€ Single-winner branch: selected_strategy_only / health_weighted_vote / any_actionable 鈹€鈹€
    if single_entry_enforced and entry_owner_instance_id:
        selected_signal = next(
            (
                signal
                for signal in strategy_signals
                if str(signal.get("_instance_id", "") or "").strip() == entry_owner_instance_id
            ),
            selected_signal,
        )
        selected_executable_signal = next(
            (
                signal
                for signal in strategy_signals
                if str(signal.get("_instance_id", "") or "").strip() == entry_owner_instance_id
                and str(signal.get("action", "HOLD") or "HOLD").upper() == "OPEN"
            ),
            selected_executable_signal,
        )
    if selected_executable_signal is not None:
        selected_signal = selected_executable_signal
    elif scheduler_selected_instance_id:
        selected_signal = next(
            (
                signal
                for signal in strategy_signals
                if str(signal.get("_instance_id", "") or "").strip() == scheduler_selected_instance_id
            ),
            selected_signal,
        )
    elif selected_signal is None:
        selected_signal = deepcopy(strategy_signals[0])
    aggregated_signal = deepcopy(selected_signal)
    selected_opportunity_payload = (
        aggregated_signal.get("opportunity", {})
        if isinstance(aggregated_signal.get("opportunity", {}), dict)
        else {}
    )
    composite_fallback_signal: Dict[str, Any] | None = None
    composite_fallback_materialized: Dict[str, Any] = {}
    composite_fallback_confidence = 0.0
    if route_family_value in {"range", "uncertain"}:
        for signal in strategy_signals:
            if canonical_strategy_id(signal.get("_sig_strategy_id", signal.get("strategy_id", ""))) != "composite":
                continue
            if bool(signal.get("suppressed", False)):
                continue
            materialized_payload = (
                signal.get("_raw_materialized_opportunity", {})
                if isinstance(signal.get("_raw_materialized_opportunity", {}), dict)
                else (
                    signal.get("materialized_opportunity", {})
                    if isinstance(signal.get("materialized_opportunity", {}), dict)
                    else {}
                )
            )
            final_action = str(
                materialized_payload.get(
                    "final_action",
                    materialized_payload.get("materialized_action", signal.get("action", "HOLD")),
                )
                or signal.get("action", "HOLD")
            ).upper()
            final_side = str(
                materialized_payload.get("final_side", signal.get("side", "NONE")) or signal.get("side", "NONE")
            ).upper()
            raw_action = str(signal.get("_raw_action", signal.get("action", "HOLD")) or signal.get("action", "HOLD")).upper()
            raw_side = str(signal.get("_raw_side", signal.get("side", "NONE")) or signal.get("side", "NONE")).upper()
            confidence = float((signal.get("trace", {}) or {}).get("confidence", signal.get("confidence", 0.0)) or 0.0)
            if (
                final_action != "OPEN"
                or final_side not in {"BUY", "SELL"}
            ) and (
                raw_action != "OPEN"
                or raw_side not in {"BUY", "SELL"}
            ):
                continue
            if confidence < effective_min_conf:
                continue
            if confidence >= composite_fallback_confidence:
                composite_fallback_signal = signal
                composite_fallback_materialized = materialized_payload
                composite_fallback_confidence = confidence

    if single_entry_enforced and not entry_owner_instance_id:
        aggregated_signal["action"] = "HOLD"
        aggregated_signal["side"] = "NONE"
        hold_reason = "AGGREGATED_HOLD_NO_SELECTED_ENTRY"
        aggregated_signal["reject_reason"] = hold_reason
    elif eligible_count == 0:
        aggregated_signal["action"] = "HOLD"
        aggregated_signal["side"] = "NONE"
        hold_reason = "AGGREGATED_HOLD_NO_ELIGIBLE_OPPORTUNITY"
        aggregated_signal["reject_reason"] = hold_reason
    elif eligible_weight_total < eligible_weight_floor:
        aggregated_signal["action"] = "HOLD"
        aggregated_signal["side"] = "NONE"
        hold_reason = "AGGREGATED_HOLD_LOW_ELIGIBLE_WEIGHT"
        aggregated_signal["reject_reason"] = hold_reason
    elif aggregated_confidence < min_confidence or net_side == "NONE":
        aggregated_signal["action"] = "HOLD"
        aggregated_signal["side"] = "NONE"
        hold_reason = "AGGREGATED_HOLD_LOW_NET_SCORE"
        aggregated_signal["reject_reason"] = hold_reason
    aggregated_opportunity = StrategyOpportunity(
        strategy_id=str(aggregated_signal.get("_sig_strategy_id", aggregated_signal.get("strategy_id", "")) or ""),
        instance_id=str(aggregated_signal.get("_instance_id", aggregated_signal.get("instance_id", "")) or ""),
        directional_view=net_side if net_side in {"BUY", "SELL"} else "NONE",
        setup_quality=_opportunity_setup_quality("OPEN", net_side, aggregated_confidence) if net_side in {"BUY", "SELL"} else "INVALID",
        entry_style_preference=str(selected_opportunity_payload.get("entry_style_preference", "MARKET") or "MARKET"),
        long_opportunity_score=aggregated_confidence if net_side == "BUY" else 0.0,
        short_opportunity_score=aggregated_confidence if net_side == "SELL" else 0.0,
        urgency=str(selected_opportunity_payload.get("urgency", "LOW") or "LOW"),
        confidence=aggregated_confidence,
        actionable=eligible_count > 0 and net_side in {"BUY", "SELL"},
        source_action="OPEN" if eligible_count > 0 else "HOLD",
        source_side=net_side,
        notes=[f"eligible_count={eligible_count}", f"executable_count={executable_count}"],
    )
    aggregated_materialized = _materialize_opportunity(
        aggregated_opportunity,
        execution_constraint,
        min_confidence=effective_min_conf,
    )
    if (
        hold_reason == "AGGREGATED_HOLD_LOW_NET_SCORE"
        and aggregated_materialized.final_action == "OPEN"
        and aggregated_materialized.final_side in {"BUY", "SELL"}
    ):
        hold_reason = ""
        aggregated_signal.pop("reject_reason", None)
    if not hold_reason:
        aggregated_signal["action"] = aggregated_materialized.final_action
        aggregated_signal["side"] = aggregated_materialized.final_side
        aggregated_signal["entry_style"] = aggregated_materialized.entry_style
        aggregated_signal["entry_size_scale"] = round(aggregated_materialized.size_scale, 4)
        aggregated_signal["materialized_action"] = aggregated_materialized.materialized_action
        aggregated_signal["materialized_reason"] = aggregated_materialized.reason
        if aggregated_materialized.final_action == "HOLD" and aggregated_materialized.reason:
            aggregated_signal["reject_reason"] = aggregated_materialized.reason
    else:
        aggregated_signal["materialized_action"] = "BLOCKED"
        aggregated_signal["materialized_reason"] = hold_reason
    aggregated_signal["_session_risk_scale"] = session_risk_scale
    if (
        hold_reason in {"AGGREGATED_HOLD_NO_SELECTED_ENTRY", "AGGREGATED_HOLD_NO_ELIGIBLE_OPPORTUNITY"}
    ):
        if composite_fallback_signal is None and route_family_value in {"range", "uncertain"}:
            best_fallback_confidence = 0.0
            for signal in strategy_signals:
                sid = canonical_strategy_id(signal.get("_sig_strategy_id", signal.get("strategy_id", "")))
                fallback_payload = (
                    signal.get("_raw_materialized_opportunity", {})
                    if isinstance(signal.get("_raw_materialized_opportunity", {}), dict)
                    else (
                        signal.get("materialized_opportunity", {})
                        if isinstance(signal.get("materialized_opportunity", {}), dict)
                        else {}
                    )
                )
                fallback_action = str(
                    fallback_payload.get("final_action", signal.get("action", "HOLD")) or signal.get("action", "HOLD")
                ).upper()
                fallback_side = str(
                    fallback_payload.get("final_side", signal.get("side", "NONE")) or signal.get("side", "NONE")
                ).upper()
                if fallback_action == "OPEN" and fallback_side in {"BUY", "SELL"}:
                    sig_conf = float((signal.get("trace", {}) or {}).get("confidence", signal.get("confidence", 0.0)) or 0.0)
                    if sig_conf > best_fallback_confidence:
                        best_fallback_confidence = sig_conf
                        composite_fallback_signal = signal
                        composite_fallback_materialized = fallback_payload
                        composite_fallback_confidence = sig_conf
        if composite_fallback_signal is None:
            pass
        else:
            aggregated_signal = deepcopy(composite_fallback_signal)
            fallback_action = str(
                composite_fallback_materialized.get("final_action", aggregated_signal.get("action", "OPEN"))
                or aggregated_signal.get("action", "OPEN")
            ).upper()
            fallback_side = str(
                composite_fallback_materialized.get("final_side", aggregated_signal.get("side", "NONE"))
                or aggregated_signal.get("side", "NONE")
            ).upper()
            if fallback_action != "OPEN" or fallback_side not in {"BUY", "SELL"}:
                fallback_action = str(
                    aggregated_signal.get("_raw_action", aggregated_signal.get("action", "OPEN"))
                    or aggregated_signal.get("action", "OPEN")
                ).upper()
                fallback_side = str(
                    aggregated_signal.get("_raw_side", aggregated_signal.get("side", "NONE"))
                    or aggregated_signal.get("side", "NONE")
                ).upper()
            aggregated_signal.pop("reject_reason", None)
            aggregated_signal["action"] = fallback_action
            aggregated_signal["side"] = fallback_side
            aggregated_signal["entry_style"] = str(
                composite_fallback_materialized.get("entry_style", aggregated_signal.get("entry_style", "MARKET"))
                or "MARKET"
            )
            aggregated_signal["entry_size_scale"] = round(
                float(composite_fallback_materialized.get("size_scale", aggregated_signal.get("entry_size_scale", 1.0)) or 1.0),
                4,
            )
            aggregated_signal["materialized_action"] = str(
                composite_fallback_materialized.get("materialized_action", fallback_action) or fallback_action
            )
            if aggregated_signal["materialized_action"] == "NO_SETUP" and fallback_action == "OPEN":
                aggregated_signal["materialized_action"] = "OPEN"
            aggregated_signal["materialized_reason"] = str(
                composite_fallback_materialized.get("reason", "COMPOSITE_FALLBACK_OPEN") or "COMPOSITE_FALLBACK_OPEN"
            )
            if aggregated_signal["materialized_reason"] in {"", "NO_ACTIONABLE_SETUP"}:
                aggregated_signal["materialized_reason"] = str(
                    composite_fallback_materialized.get("reason", "COMPOSITE_FALLBACK_OPEN") or "COMPOSITE_FALLBACK_OPEN"
                )
            aggregated_signal["fallback_reason"] = "COMPOSITE_FALLBACK_OPEN"
            route_selection["reason"] = "COMPOSITE_FALLBACK_OPEN"
            hold_reason = ""
            aggregated_confidence = max(aggregated_confidence, composite_fallback_confidence)
            if fallback_side in {"BUY", "SELL"}:
                net_side = fallback_side
            fallback_instance_id = str(
                aggregated_signal.get("_instance_id", aggregated_signal.get("instance_id", "")) or ""
            ).strip()
            fallback_strategy_id = str(
                aggregated_signal.get("_sig_strategy_id", aggregated_signal.get("strategy_id", "")) or ""
            ).strip()
            if fallback_instance_id:
                entry_owner_instance_id = fallback_instance_id
                route["entry_owner_instance_id"] = fallback_instance_id
                route_selection["winner_instance_id"] = fallback_instance_id
            if fallback_strategy_id:
                route["entry_owner_strategy_id"] = fallback_strategy_id
                route_selection["winner_strategy_id"] = fallback_strategy_id
    if hold_reason and not str(route_selection.get("reason", "") or "").strip():
        route_selection["reason"] = hold_reason
    elif (
        not str(route_selection.get("reason", "") or "").strip()
        and str(aggregated_signal.get("action", "HOLD") or "HOLD").upper() == "HOLD"
    ):
        fallback_route_reason = str(
            aggregated_signal.get("materialized_reason", "")
            or aggregated_signal.get("reject_reason", "")
            or ""
        ).strip()
        if fallback_route_reason:
            route_selection["reason"] = fallback_route_reason
    base_signal_id = str(aggregated_signal.get("_base_signal_id", aggregated_signal.get("signal_id", f"sig_{int(time.time())}")) or f"sig_{int(time.time())}")
    if len(strategy_signals) > 1:
        fingerprint = hashlib.sha1("|".join(sorted(str(item.get("_instance_id", "")) for item in strategy_signals)).encode("utf-8")).hexdigest()[:8]
        aggregated_signal["signal_id"] = f"{base_signal_id}__agg_{fingerprint}"
    selected_instance_id = str(aggregated_signal.get("_instance_id", "") or "")
    selected_strategy_id = str(aggregated_signal.get("_sig_strategy_id", aggregated_signal.get("strategy_id", "")) or "")
    contributor_strategy_by_instance = {
        str(item.get("instance_id", "") or "").strip(): canonical_strategy_id(item.get("strategy_id", ""))
        for item in contributors
        if isinstance(item, dict) and str(item.get("instance_id", "") or "").strip()
    }
    if single_entry_enforced:
        selected_instance_id = "" if route_order_missing_winner else str(entry_owner_instance_id or selected_instance_id or "")
        if selected_instance_id:
            aggregated_signal["_selected_instance_id"] = selected_instance_id
        selected_strategy_id = (
            ""
            if route_order_missing_winner
            else str(
                route.get("entry_owner_strategy_id")
                or route_selection.get("winner_strategy_id")
                or selected_strategy_id
                or ""
            )
        )
        if selected_strategy_id:
            aggregated_signal["_selected_strategy"] = selected_strategy_id
    selected_instance_strategy_id = contributor_strategy_by_instance.get(selected_instance_id, "")
    if (
        selected_instance_strategy_id
        and canonical_strategy_id(selected_strategy_id) != selected_instance_strategy_id
    ):
        selected_strategy_id = selected_instance_strategy_id
        aggregated_signal["_selected_strategy"] = selected_strategy_id
        trace = aggregated_signal.get("trace", {}) if isinstance(aggregated_signal.get("trace", {}), dict) else {}
        evidence = trace.get("evidence", []) if isinstance(trace.get("evidence", []), list) else []
        if "selected_strategy_instance_realigned" not in evidence:
            evidence.append("selected_strategy_instance_realigned")
        trace["evidence"] = evidence
        aggregated_signal["trace"] = trace
    entry_owner_strategy_id = str(
        route.get("entry_owner_strategy_id")
        or route_selection.get("winner_strategy_id")
        or selected_strategy_id
        or ""
    )
    entry_owner_instance_id = str(entry_owner_instance_id or selected_instance_id or "")
    if selected_instance_id:
        entry_owner_instance_id = selected_instance_id
        route["entry_owner_instance_id"] = selected_instance_id
    if selected_strategy_id:
        entry_owner_strategy_id = selected_strategy_id
        route["entry_owner_strategy_id"] = selected_strategy_id
    if str(route_selection.get("selection_mode", "") or "").strip() == "candidate_pool":
        route_selection.setdefault(
            "candidate_pool_winner_instance_id",
            str(route_selection.get("winner_instance_id", "") or ""),
        )
        route_selection.setdefault(
            "candidate_pool_winner_strategy_id",
            str(route_selection.get("winner_strategy_id", "") or ""),
        )
    else:
        route_selection.pop("candidate_pool_winner_instance_id", None)
        route_selection.pop("candidate_pool_winner_strategy_id", None)
    if selected_instance_id:
        if route_selection_mode != "route_order":
            route_selection["winner_instance_id"] = selected_instance_id
    if selected_strategy_id:
        if route_selection_mode != "route_order":
            route_selection["winner_strategy_id"] = selected_strategy_id
    entry_owner_instance_strategy = contributor_strategy_by_instance.get(entry_owner_instance_id, "")
    if (
        entry_owner_instance_strategy
        and canonical_strategy_id(entry_owner_strategy_id) != entry_owner_instance_strategy
    ):
        entry_owner_strategy_id = entry_owner_instance_strategy
        route["entry_owner_strategy_id"] = entry_owner_strategy_id
        route["entry_owner_instance_id"] = entry_owner_instance_id
        trace = aggregated_signal.get("trace", {}) if isinstance(aggregated_signal.get("trace", {}), dict) else {}
        evidence = trace.get("evidence", []) if isinstance(trace.get("evidence", []), list) else []
        if "entry_owner_strategy_instance_realigned" not in evidence:
            evidence.append("entry_owner_strategy_instance_realigned")
        trace["evidence"] = evidence
        aggregated_signal["trace"] = trace
    selected_reason = str(aggregated_signal.get("reject_reason", "") or hold_reason or "")
    execution_decision = _build_execution_decision(
        action=str(aggregated_signal.get("action", "HOLD") or "HOLD"),
        side=str(aggregated_signal.get("side", "NONE") or "NONE"),
        selected_strategy_id=selected_strategy_id,
        selected_instance_id=selected_instance_id,
        entry_owner_strategy_id=entry_owner_strategy_id,
        entry_owner_instance_id=entry_owner_instance_id,
        reason=selected_reason,
        confidence=aggregated_confidence,
        route=route,
        canonical_market_snapshot=canonical_market_snapshot,
        selection=route_selection,
    )
    if str(route_selection.get("selection_mode", "") or "").strip() == "route_order":
        execution_decision["decision_source"] = "route_order_selection"
    aggregation_payload = {
        "action": str(aggregated_signal.get("action", "HOLD") or "HOLD"),
        "side": str(aggregated_signal.get("side", "NONE") or "NONE"),
        "materialized_action": str(aggregated_signal.get("materialized_action", "") or ""),
        "materialized_reason": str(aggregated_signal.get("materialized_reason", "") or ""),
        "multi_dispatch": False,
        "dispatch_count": 1,
        "net_score": round(effective_net_score, 6),
        "raw_net_score": round(raw_net_score, 6),
        "confidence": aggregated_confidence,
        "contributors": contributors,
        "blocked_reasons": blocked_reasons,
        "selection_mode": str(route_selection.get("selection_mode", "single_best") or "single_best"),
        "selected_instance_id": selected_instance_id,
        "selected_strategy_id": selected_strategy_id,
        "selected_reason": selected_reason,
        "entry_policy": entry_policy,
        "entry_owner_instance_id": entry_owner_instance_id,
        "entry_owner_strategy_id": str(execution_decision.get("entry_owner_strategy_id", entry_owner_strategy_id) or entry_owner_strategy_id or ""),
        "entry_failover_reason": entry_failover_reason,
        "eligible_count": eligible_count,
        "eligible_weight_total": round(eligible_weight_total, 6),
        "eligible_weight_floor": round(eligible_weight_floor, 6),
        "executable_count": executable_count,
        "executable_weight_total": round(executable_weight_total, 6),
        "voting_weights": {iid: round(w, 4) for iid, w in voting_weights.items()} if voting_weights else {},
        "execution_constraint": execution_constraint_payload,
        "selected_opportunity": selected_opportunity_payload,
        "aggregated_opportunity": _strategy_opportunity_to_dict(aggregated_opportunity),
        "aggregated_materialized_opportunity": _materialized_opportunity_to_dict(aggregated_materialized),
        "correlation": {
            "consensus": str(correlation.get("consensus", "UNKNOWN") or "UNKNOWN"),
            "net_bias": str(correlation.get("net_bias", "NONE") or "NONE"),
            "confidence_adjustment": round(float(correlation.get("confidence_adjustment", 1.0) or 1.0), 4),
            "volume_adjustment": round(float(correlation.get("volume_adjustment", 1.0) or 1.0), 4),
            "cooldown_adjustment_sec": int(correlation.get("cooldown_adjustment_sec", 0) or 0),
            "aligned_instances": correlation.get("aligned_instances", []),
            "divergent_instances": correlation.get("divergent_instances", []),
        },
        "signal_arbitrator": arbitration_meta,
        "strategy_quality": {
            str(
                signal.get("_sig_strategy_id", signal.get("strategy_id", "")) or ""
            ): (
                signal.get("performance_profile", {})
                if isinstance(signal.get("performance_profile", {}), dict)
                else {}
            )
            for signal in strategy_signals
            if str(signal.get("_sig_strategy_id", signal.get("strategy_id", "")) or "").strip()
        },
        "per_instance_health": {
            iid: {
                "score": round(h.score, 1),
                "grade": h.grade,
                "suggested_action": h.suggested_action,
                "suggested_scale": round(h.suggested_scale, 4),
                "loss_streak": h.loss_streak,
                "best_regime": h.best_regime,
                "current_regime_fitness": round(h.current_regime_fitness, 4),
                "primary_loss_pattern": h.primary_loss_pattern,
            }
            for iid, h in _cached_health.items()
        } if project_root is not None else {},
        "scheduler_mode": str(scheduler_snapshot.get("mode", "single_active") or "single_active"),
        "scheduler_reason": str(scheduler_snapshot.get("reason", "runtime_scheduler") or "runtime_scheduler"),
        "decision_policy": decision_policy,
        "route": deepcopy(route),
        "selection": deepcopy(route_selection),
        "canonical_market_snapshot": deepcopy(canonical_market_snapshot),
        "execution_decision": execution_decision,
        "agent_context": {
            "macro_direction": str(
                ((agent_context or {}).get("macro", {}) if isinstance((agent_context or {}).get("macro", {}), dict) else {}).get("direction", "NEUTRAL")
                or "NEUTRAL"
            ),
            "effective_macro_direction": str(
                ((agent_context or {}).get("macro", {}) if isinstance((agent_context or {}).get("macro", {}), dict) else {}).get("effective_direction", "NEUTRAL")
                or "NEUTRAL"
            ),
            "binding_macro_direction": _resolve_binding_direction(
                ((agent_context or {}).get("macro", {}) if isinstance((agent_context or {}).get("macro", {}), dict) else {})
            ),
            "execution_advice": str(
                ((agent_context or {}).get("interpreter", {}) if isinstance((agent_context or {}).get("interpreter", {}), dict) else {}).get("execution_advice", "ALLOW_FOLLOW")
                or "ALLOW_FOLLOW"
            ),
            "structural_side": str(
                ((agent_context or {}).get("interpreter", {}) if isinstance((agent_context or {}).get("interpreter", {}), dict) else {}).get("structural_side", "")
                or ""
            ),
            "macro_state": str(
                ((agent_context or {}).get("macro", {}) if isinstance((agent_context or {}).get("macro", {}), dict) else {}).get("state", "UNKNOWN")
                or "UNKNOWN"
            ),
            "macro_bias_weight": float(
                ((agent_context or {}).get("macro", {}) if isinstance((agent_context or {}).get("macro", {}), dict) else {}).get("bias_weight", 1.0)
                or 1.0
            ),
            "route": (
                (agent_context or {}).get("route", {})
                if isinstance((agent_context or {}).get("route", {}), dict)
                else {}
            ),
        },
    }
    if hold_reason and hold_reason not in aggregation_payload["blocked_reasons"]:
        aggregation_payload["blocked_reasons"].append(hold_reason)
    trace = aggregated_signal.get("trace", {}) if isinstance(aggregated_signal.get("trace", {}), dict) else {}
    trace["aggregation"] = {
        "net_score": aggregation_payload["net_score"],
        "raw_net_score": aggregation_payload["raw_net_score"],
        "eligible_count": aggregation_payload["eligible_count"],
        "eligible_weight_total": aggregation_payload["eligible_weight_total"],
        "eligible_weight_floor": aggregation_payload["eligible_weight_floor"],
        "materialized_action": aggregation_payload["materialized_action"],
        "contributors": contributors,
        "multi_dispatch": False,
    }
    trace["confidence"] = aggregated_confidence
    aggregated_signal["trace"] = trace
    aggregated_signal["execution_constraint"] = execution_constraint_payload
    aggregated_signal["opportunity"] = aggregation_payload["aggregated_opportunity"]
    aggregated_signal["materialized_opportunity"] = aggregation_payload["aggregated_materialized_opportunity"]
    aggregated_signal["_selected_instance_id"] = aggregation_payload["selected_instance_id"]
    aggregated_signal["_selected_strategy"] = aggregation_payload["selected_strategy_id"]
    _trace_selection_state(
        project_root,
        "aggregate_single_dispatch_return",
        route,
        selection=route_selection,
        extra={
            "decision_source": str(execution_decision.get("decision_source", "") or ""),
            "selected_strategy_id": str(aggregation_payload.get("selected_strategy_id", "") or ""),
            "selected_instance_id": str(aggregation_payload.get("selected_instance_id", "") or ""),
        },
    )
    return [aggregated_signal], aggregation_payload


def _signal_attribution_metadata(
    signal: Dict[str, Any],
    aggregated_intent: Dict[str, Any],
) -> Dict[str, Any]:
    macro_context = (
        aggregated_intent.get("agent_context", {})
        if isinstance(aggregated_intent.get("agent_context", {}), dict)
        else {}
    ).get("macro", {})
    if not isinstance(macro_context, dict):
        macro_context = {}
    selected_instance_id = str(
        signal.get("_selected_instance_id")
        or aggregated_intent.get("selected_instance_id")
        or signal.get("_instance_id")
        or signal.get("instance_id")
        or ""
    ).strip()
    selected_strategy_id = str(
        signal.get("_selected_strategy")
        or aggregated_intent.get("selected_strategy_id")
        or signal.get("_sig_strategy_id")
        or signal.get("strategy_id")
        or ""
    ).strip()
    contributors = (
        aggregated_intent.get("contributors", [])
        if isinstance(aggregated_intent.get("contributors", []), list)
        else []
    )
    route = (
        aggregated_intent.get("route", {})
        if isinstance(aggregated_intent.get("route", {}), dict)
        else {}
    )
    route_market_context = (
        route.get("market_context", {})
        if isinstance(route.get("market_context", {}), dict)
        else {}
    )
    regime_audit = build_regime_audit_payload(
        {
            **route_market_context,
            "selection_regime": signal.get("selection_regime", route_market_context.get("selection_regime", signal.get("regime", ""))),
            "regime": signal.get("regime", route_market_context.get("regime", "")),
            "regime_source": signal.get("regime_source", route_market_context.get("regime_source", "")),
            "regime_confidence": signal.get("regime_confidence", route_market_context.get("regime_confidence", 0.0)),
            "route_family": signal.get("route_family", route.get("route_family", route_market_context.get("route_family", ""))),
        }
    )
    entry_owner_instance_id = str(
        route.get("entry_owner_instance_id")
        or aggregated_intent.get("entry_owner_instance_id")
        or selected_instance_id
        or signal.get("_instance_id")
        or signal.get("instance_id")
        or ""
    ).strip()
    entry_owner_strategy_id = str(
        route.get("entry_owner_strategy_id")
        or aggregated_intent.get("entry_owner_strategy_id")
        or selected_strategy_id
        or signal.get("_sig_strategy_id")
        or signal.get("strategy_id")
        or ""
    ).strip()
    contributor_strategy_by_instance = {
        str(row.get("instance_id", "") or "").strip(): canonical_strategy_id(row.get("strategy_id", ""))
        for row in contributors
        if isinstance(row, dict) and str(row.get("instance_id", "") or "").strip()
    }
    selected_instance_strategy = contributor_strategy_by_instance.get(selected_instance_id, "")
    if selected_instance_strategy and canonical_strategy_id(selected_strategy_id) != selected_instance_strategy:
        selected_strategy_id = selected_instance_strategy
    entry_owner_instance_strategy = contributor_strategy_by_instance.get(entry_owner_instance_id, "")
    if entry_owner_instance_strategy and canonical_strategy_id(entry_owner_strategy_id) != entry_owner_instance_strategy:
        entry_owner_strategy_id = entry_owner_instance_strategy
    action_upper = str(signal.get("action", "HOLD") or "HOLD").upper()
    side_upper = str(signal.get("side", "NONE") or "NONE").upper()
    if action_upper == "OPEN" and side_upper in {"BUY", "SELL"} and contributors:
        same_side_contributors = [
            row
            for row in contributors
            if isinstance(row, dict)
            and str(row.get("action", "HOLD") or "HOLD").upper() == "OPEN"
            and str(row.get("side", "NONE") or "NONE").upper() == side_upper
            and bool(row.get("eligible_for_aggregation", True))
        ]
        if same_side_contributors:
            best_owner = max(
                same_side_contributors,
                key=lambda row: (
                    abs(float(row.get("signed_score", 0.0) or 0.0)),
                    float(row.get("confidence", 0.0) or 0.0),
                    float(row.get("weight", 0.0) or 0.0),
                ),
            )
            entry_owner_instance_id = str(
                best_owner.get("instance_id")
                or best_owner.get("selected_instance_id")
                or entry_owner_instance_id
                or ""
            ).strip()
            entry_owner_strategy_id = str(
                best_owner.get("strategy_id")
                or best_owner.get("selected_strategy_id")
                or entry_owner_strategy_id
                or ""
            ).strip()
    metadata = {
        "action": str(signal.get("action", "HOLD") or "HOLD").upper(),
        "side": str(signal.get("side", "NONE") or "NONE").upper(),
        "symbol": str(signal.get("symbol", "XAUUSD") or "XAUUSD"),
        "entry_style": str(signal.get("entry_style", "MARKET") or "MARKET").upper(),
        "materialized_action": str(signal.get("materialized_action", "") or ""),
        "volume_lots": float(signal.get("volume_lots", signal.get("volume", signal.get("lots", 0.0))) or 0.0),
        "requested_volume_lots": float(signal.get("requested_volume_lots", signal.get("volume_lots", signal.get("volume", signal.get("lots", 0.0)))) or 0.0),
        "configured_base_lots": float(signal.get("configured_base_lots", 0.0) or 0.0),
        "rule_cap_lots": float(signal.get("rule_cap_lots", 0.0) or 0.0),
        "rule_cap_reasons": signal.get("rule_cap_reasons", []) if isinstance(signal.get("rule_cap_reasons", []), list) else [],
        "position_tier": str(signal.get("position_tier", "NORMAL") or "NORMAL"),
        "tier_target_lots": float(signal.get("tier_target_lots", 0.0) or 0.0),
        "tier_reason_code": str(signal.get("tier_reason_code", "") or ""),
        "tier_reason_text": str(signal.get("tier_reason_text", "") or ""),
        "tier_recovery_needed": signal.get("tier_recovery_needed", []) if isinstance(signal.get("tier_recovery_needed", []), list) else [],
        "decision_id": str(signal.get("decision_id", signal.get("signal_id", "")) or ""),
        "trace_id": str(signal.get("trace_id", signal.get("signal_id", "")) or ""),
        "strategy_id": str(signal.get("strategy_id", "") or ""),
        "instance_id": str(signal.get("instance_id", selected_instance_id) or ""),
        "attributed_strategy_id": entry_owner_strategy_id,
        "attributed_instance_id": entry_owner_instance_id,
        "selected_strategy_id": selected_strategy_id,
        "selected_instance_id": selected_instance_id,
        "entry_owner_strategy_id": entry_owner_strategy_id,
        "entry_owner_instance_id": entry_owner_instance_id,
        "regime": regime_audit.get("regime", "UNKNOWN"),
        "selection_regime": regime_audit.get("selection_regime", "UNKNOWN"),
        "regime_source": regime_audit.get("regime_source", "UNKNOWN"),
        "regime_confidence": float(regime_audit.get("regime_confidence", 0.0) or 0.0),
        "route_family": regime_audit.get("route_family", ""),
        "agent_direction": str(
            signal.get("binding_direction")
            or signal.get("agent_direction")
            or macro_context.get("binding_direction")
            or macro_context.get("direction")
            or ""
        ).upper(),
        "binding_direction": str(
            signal.get("binding_direction")
            or macro_context.get("binding_direction")
            or ""
        ).upper(),
        "macro_authority": str(
            signal.get("macro_authority")
            or macro_context.get("macro_authority")
            or ""
        ).upper(),
        "allowed_side": str(
            signal.get("allowed_side")
            or (
                signal.get("execution_constraint", {})
                if isinstance(signal.get("execution_constraint", {}), dict)
                else {}
            ).get("allowed_side")
            or macro_context.get("allowed_side")
            or ""
        ).upper(),
        "contributors": contributors,
    }
    if metadata["action"] == "HOLD":
        metadata["hold_diagnostics"] = _hold_diagnostics_summary(aggregated_intent)
    metadata.update(normalize_position_attribution(metadata, action=metadata["action"]))
    return metadata


def _attach_signal_attribution(
    signal: Dict[str, Any],
    aggregated_intent: Dict[str, Any],
) -> Dict[str, Any]:
    metadata = _signal_attribution_metadata(signal, aggregated_intent)
    signal.update(
        {
            "instance_id": metadata["instance_id"],
            "attributed_instance_id": metadata["attributed_instance_id"],
            "attributed_strategy_id": metadata["attributed_strategy_id"],
            "selected_instance_id": metadata["selected_instance_id"],
            "selected_strategy_id": metadata["selected_strategy_id"],
            "decision_id": metadata["decision_id"],
            "trace_id": metadata["trace_id"],
            "contributors": metadata["contributors"],
            "selection_regime": metadata.get("selection_regime", signal.get("selection_regime", signal.get("regime", "UNKNOWN"))),
            "regime_source": metadata.get("regime_source", signal.get("regime_source", "UNKNOWN")),
            "regime_confidence": metadata.get("regime_confidence", signal.get("regime_confidence", 0.0)),
            "route_family": metadata.get("route_family", signal.get("route_family", "")),
            "session": signal.get("session", {}),
            "interpreter": signal.get("interpreter", {}),
            "adx_value": float(signal.get("adx_value", signal.get("adx", 0.0)) or 0.0),
            "atr_value": float(signal.get("atr_value", signal.get("atr", 0.0)) or 0.0),
            "vol_ratio": float(signal.get("vol_ratio", signal.get("volume_ratio", 1.0)) or 1.0),
            "spread": float(signal.get("spread", signal.get("spread_points", 0.0)) or 0.0),
        }
    )
    return metadata


def _append_instance_execution_event(
    project_root: Path,
    signal: Dict[str, Any],
    report: Optional[Dict[str, Any]],
) -> None:
    action = str(signal.get("action", "HOLD") or "HOLD").upper()
    if action not in {"OPEN", "CLOSE"}:
        return
    payload = report if isinstance(report, dict) else {}
    status = str(payload.get("status", "UNKNOWN") or "UNKNOWN").upper()
    event = {
        "ts": str(payload.get("execution_ts") or payload.get("ts") or datetime.now(timezone.utc).isoformat()),
        "signal_id": str(signal.get("signal_id", "") or ""),
        "decision_id": str(signal.get("decision_id", signal.get("signal_id", "")) or ""),
        "intent_id": str(payload.get("intent_id", signal.get("intent_id", "")) or ""),
        "action": action,
        "side": str(signal.get("side", "NONE") or "NONE").upper(),
        "status": status,
        "reason": str(payload.get("reason", "") or ""),
        "symbol": str(signal.get("symbol", "XAUUSD") or "XAUUSD"),
        "strategy_id": str(signal.get("attributed_strategy_id", signal.get("strategy_id", "")) or ""),
        "instance_id": str(signal.get("attributed_instance_id", signal.get("instance_id", "")) or ""),
        "selected_strategy_id": str(signal.get("selected_strategy_id", signal.get("strategy_id", "")) or ""),
        "selected_instance_id": str(signal.get("selected_instance_id", signal.get("instance_id", "")) or ""),
        "contributors": signal.get("contributors", []) if isinstance(signal.get("contributors", []), list) else [],
        "entry_style": str(signal.get("entry_style", "MARKET") or "MARKET").upper(),
        "materialized_action": str(signal.get("materialized_action", "") or ""),
        "order_id": payload.get("order_id"),
        "deal_id": payload.get("deal_id"),
        "retcode": payload.get("retcode"),
        "reconciliation_ok": payload.get("reconciliation_ok"),
        "volume": float(signal.get("volume_lots", signal.get("volume", signal.get("lots", 0.0))) or 0.0),
        "volume_lots": float(signal.get("volume_lots", signal.get("volume", signal.get("lots", 0.0))) or 0.0),
        "requested_volume_lots": float(signal.get("requested_volume_lots", signal.get("volume_lots", signal.get("volume", signal.get("lots", 0.0)))) or 0.0),
        "configured_base_lots": float(signal.get("configured_base_lots", 0.0) or 0.0),
        "rule_cap_lots": float(signal.get("rule_cap_lots", 0.0) or 0.0),
        "rule_cap_reasons": signal.get("rule_cap_reasons", []) if isinstance(signal.get("rule_cap_reasons", []), list) else [],
        "position_tier": str(signal.get("position_tier", "NORMAL") or "NORMAL"),
        "tier_target_lots": float(signal.get("tier_target_lots", 0.0) or 0.0),
        "tier_reason_code": str(signal.get("tier_reason_code", "") or ""),
        "tier_reason_text": str(signal.get("tier_reason_text", "") or ""),
        "tier_recovery_needed": signal.get("tier_recovery_needed", []) if isinstance(signal.get("tier_recovery_needed", []), list) else [],
        "pnl": _safe_float(payload.get("pnl")),
    }
    if action == "CLOSE":
        event["close_pnl"] = _safe_float(payload.get("close_pnl")) or _safe_float(payload.get("pnl"))
    ledger_path = project_root / "logs" / "instance_ledger.ndjson"
    ledger_path.parent.mkdir(parents=True, exist_ok=True)
    with ledger_path.open("a", encoding="utf-8") as f:
        f.write(json.dumps(event, ensure_ascii=False) + "\n")


def _quantize_open_lots(value: Any, *, enforce_min: bool = False) -> float:
    try:
        amount = float(value or 0.0)
    except (TypeError, ValueError):
        amount = 0.0
    if amount <= 0:
        return 0.0
    lot_step = float(config_manager.get_effective_param("lot_step", 0.01) or 0.01)
    min_lot = float(config_manager.get_effective_param("min_lot", lot_step) or lot_step)
    if lot_step <= 0:
        quantized = amount
    else:
        step_dec = Decimal(str(lot_step))
        quantized_dec = (Decimal(str(amount)) / step_dec).quantize(Decimal("1"), rounding=ROUND_HALF_UP) * step_dec
        quantized = float(quantized_dec.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
    if enforce_min:
        quantized = max(min_lot, quantized)
    return round(quantized, 2)


def _apply_live_volume_plan(
    signal: Dict[str, Any],
    *,
    project_root: Path,
    aggregated_intent: Optional[Dict[str, Any]] = None,
) -> None:
    action = str(signal.get("action", "HOLD") or "HOLD").upper()
    if action != "OPEN":
        return
    requested_volume = signal.get(
        "requested_volume_lots",
        signal.get("volume_plan", signal.get("volume_lots", signal.get("volume", signal.get("lots")))),
    )
    volume_plan = resolve_live_volume_plan(
        requested_volume, project_root=project_root,
        regime=str(signal.get("regime", signal.get("selection_regime", "")) or ""))
    signal["requested_volume_lots"] = volume_plan["requested_volume_lots"]
    signal["configured_base_lots"] = volume_plan["configured_base_lots"]
    signal["rule_cap_lots"] = volume_plan["rule_cap_lots"]
    signal["rule_cap_reasons"] = list(volume_plan["rule_cap_reasons"])
    signal["final_volume_lots"] = volume_plan["final_volume_lots"]
    signal["volume_lots"] = _quantize_open_lots(volume_plan["final_volume_lots"], enforce_min=True)
    signal["volume_plan"] = signal["volume_lots"]
    # Apply session-level risk scaling (e.g. lower size during quiet sessions)
    session_risk_scale = float(signal.get("_session_risk_scale", 1.0) or 1.0)
    if session_risk_scale != 1.0 and float(signal.get("volume_lots", 0) or 0) > 0:
        signal["volume_lots"] = _quantize_open_lots(float(signal["volume_lots"]) * session_risk_scale, enforce_min=True)
        signal["session_risk_scale_applied"] = round(session_risk_scale, 4)
    entry_size_scale = float(signal.get("entry_size_scale", 1.0) or 1.0)
    if entry_size_scale != 1.0 and float(signal.get("volume_lots", 0) or 0) > 0:
        signal["volume_lots"] = _quantize_open_lots(float(signal["volume_lots"]) * entry_size_scale)
        signal["entry_size_scale_applied"] = round(entry_size_scale, 4)

    # 鈹€鈹€ Per-instance health-based scaling 鈹€鈹€
    agg = aggregated_intent if isinstance(aggregated_intent, dict) else {}
    per_instance_health = agg.get("per_instance_health", {}) if isinstance(agg.get("per_instance_health", {}), dict) else {}
    instance_id = str(signal.get("_instance_id", signal.get("instance_id", "")) or "").strip()
    health_info = per_instance_health.get(instance_id, {}) if isinstance(per_instance_health.get(instance_id, {}), dict) else {}
    health_scale = float(health_info.get("suggested_scale", 1.0) or 1.0)
    if health_scale < 1.0 and float(signal.get("volume_lots", 0) or 0) > 0:
        signal["volume_lots"] = _quantize_open_lots(float(signal["volume_lots"]) * health_scale, enforce_min=True)
        signal["health_scale_applied"] = round(health_scale, 4)
        if health_scale <= 0.0:
            signal["action"] = "HOLD"
            signal["side"] = "NONE"
            signal["reject_reason"] = "HEALTH_SHADOW_ONLY"
            return

    # 鈹€鈹€ Cross-strategy correlation volume adjustment 鈹€鈹€
    correlation = agg.get("correlation", {}) if isinstance(agg.get("correlation", {}), dict) else {}
    corr_vol_adj = float(correlation.get("volume_adjustment", 1.0) or 1.0)
    if corr_vol_adj != 1.0 and float(signal.get("volume_lots", 0) or 0) > 0:
        signal["volume_lots"] = _quantize_open_lots(float(signal["volume_lots"]) * corr_vol_adj, enforce_min=True)
        signal["correlation_volume_applied"] = round(corr_vol_adj, 4)

    # 鈹€鈹€ Interpreter directional position multiplier (before budget cap so cap is the final clamp) 鈹€鈹€
    bias_mult = float(signal.get("interpreter_position_multiplier", 1.0) or 1.0)
    if bias_mult != 1.0 and float(signal.get("volume_lots", 0) or 0) > 0:
        signal["volume_lots"] = _quantize_open_lots(float(signal["volume_lots"]) * bias_mult, enforce_min=True)
        signal["interpreter_bias_applied"] = round(bias_mult, 4)

    # 鈹€鈹€ Risk budget: cap per-instance volume (final clamp, no multiplier after it) 鈹€鈹€
    risk_config = agg.get("decision_policy", {}).get("risk", {}) if isinstance(agg.get("decision_policy", {}), dict) else {}
    budget_config = risk_config.get("risk_budget", {}) if isinstance(risk_config, dict) else {}
    max_lot_per = float(budget_config.get("max_lot_per_instance", 0.20) or 0.20)
    if float(signal.get("volume_lots", 0) or 0) > max_lot_per:
        signal["volume_lots"] = _quantize_open_lots(max_lot_per, enforce_min=True)
        signal["risk_budget_cap_applied"] = round(max_lot_per, 4)
    if float(signal.get("volume_lots", 0) or 0) > 0:
        signal["volume_lots"] = _quantize_open_lots(signal["volume_lots"], enforce_min=True)


def _evaluate_runtime_prechecks(
    ctx: PipelineRunContext,
    *,
    runtime_state: Dict[str, Any],
) -> Optional[PipelineOutcome]:
    # kill_switch is passed downstream as circuit_open feature for LLM verifier
    # to apply a confidence penalty 鈥?not a hard block here.

    # 鈹€鈹€ 娴姩浜忔崯鐔旀柇锛氭诞浜忚秴杩囦綑棰?5% 鏃堕樆姝㈠紑鏂颁粨 鈹€鈹€
    floating_check = _check_floating_loss_limits(ctx)
    if floating_check is not None:
        return floating_check

    # Rollout promotion gates removed from live trading path per user directive.
    # Non-live strategies (shadow/paper/canary) are offline-training only.
    ctx.promotion_info = {"mode": "full"}
    return None


def _check_floating_loss_limits(ctx: PipelineRunContext) -> Optional[PipelineOutcome]:
    """娴姩浜忔崯鐔旀柇锛氬凡鎸佷粨鏃舵诞浜忚秴杩囦綑棰濋槇鍊煎垯闃绘鏂板紑浠撱€?

    浠呬綔鐢ㄤ簬 OPEN 鎰忓浘 鈥?鍏佽骞充粨鍜?HOLD 涓嶅彈闄愬埗銆?
    鍚屾椂璁＄畻 floating_pnl_pct 渚涗笅娓搁鎺ф秷璐广€?
    """
    account = ctx.engine_request.get("account_metrics", {}) if isinstance(ctx.engine_request.get("account_metrics", {}), dict) else {}
    floating_pnl = float(account.get("floating_pnl", 0.0) or 0.0)
    balance = float(account.get("balance", 0.0) or account.get("equity", 0.0) or 0.0)
    position_open = bool(account.get("position_open", False))

    if balance > 0:
        floating_pnl_pct = round(floating_pnl / balance * 100.0, 2)
        account["floating_pnl_pct"] = floating_pnl_pct
    else:
        floating_pnl_pct = 0.0
        account["floating_pnl_pct"] = 0.0

    # 鍙寮€浠撳仛闄愬埗
    if not position_open or floating_pnl >= 0:
        return None

    # 娴簭闃堝€间粠闈㈡澘璇诲彇锛堝厹搴?5%锛?
    try:
        float_loss_threshold = float(
            config_manager.get_effective_param("max_daily_loss_pct", 5.0))
    except (TypeError, ValueError):
        float_loss_threshold = 5.0
    # 娴簭瓒呰繃闃堝€?鈫?鐔旀柇
    if floating_pnl_pct <= -float_loss_threshold:
        ctx.signal = _build_idle_signal(
            signal_id=f"floating_loss_circuit_{int(time.time())}",
            symbol=ctx.symbol,
            reject_reason=f"FLOATING_LOSS_CIRCUIT_BREAKER:{floating_pnl_pct:.2f}%",
            evidence=[f"floating_pnl_pct:{floating_pnl_pct:.2f}%"],
            risk_flags=["FLOATING_LOSS_CIRCUIT"],
        )
        return PipelineOutcome(
            ledger_events=["floating_loss_blocked"],
            include_runtime=False,
        )

    return None


def _apply_llm_verification(ctx: PipelineRunContext) -> Optional[PipelineOutcome]:
    """Stage 2-4 LLM signal verification: verify 鈫?calibrate 鈫?gate.

    Called after signal aggregation to apply DeepSeek LLM secondary review
    with rule-based fallback.  May VETO the signal or adjust its confidence.
    Returns a PipelineOutcome if the signal is vetoed (caller should return it),
    or None if the signal passes verification (possibly with adjusted confidence).
    """
    action = str(ctx.signal.get("action", "HOLD") or "HOLD").upper()
    side = str(ctx.signal.get("side", "NONE") or "NONE").upper()
    if action != "OPEN" or side not in {"BUY", "SELL"}:
        return None  # HOLD signals don't need LLM verification

    trace = ctx.signal.get("trace", {}) if isinstance(ctx.signal.get("trace", {}), dict) else {}
    original_confidence = float(trace.get("confidence", 0.0) or 0.0)

    # 鈹€鈹€ Extract features from context 鈹€鈹€
    market = ctx.engine_request.get("market_metrics", {}) if isinstance(ctx.engine_request.get("market_metrics", {}), dict) else {}
    account = ctx.engine_request.get("account_metrics", {}) if isinstance(ctx.engine_request.get("account_metrics", {}), dict) else {}
    route = ctx.scheduler_snapshot.get("route", {}) if isinstance(ctx.scheduler_snapshot.get("route", {}), dict) else {}

    spread_points = float(market.get("spread_points", 0.0) or 0.0)
    spread_p90 = float(market.get("spread_p90_30m", 30.0) or 30.0)
    spread_anomaly = spread_points > max(spread_p90 * 2.5, 80.0)

    incident_ctrl = _incident_controls(ctx.runtime_state)
    circuit_open = bool(ctx.runtime_state.get("kill_switch", False))

    # Position data from account or snapshot
    position_open = bool(account.get("position_open", False))
    position_side = str(account.get("position_side", "") or "")
    if not position_open or not position_side:
        snap = getattr(ctx, "mt5_snapshot", None)
        if isinstance(snap, dict):
            positions_all = snap.get("positions_all", [])
            if positions_all:
                position_open = True
                p0 = positions_all[0] if isinstance(positions_all[0], dict) else {}
                pos_type = int(p0.get("type", 0) or 0)
                position_side = "BUY" if pos_type == 0 else "SELL"

    features: Dict[str, Any] = {
        "regime": str(route.get("regime", "UNKNOWN") or "UNKNOWN").upper(),
        "rsi14": None,
        "adx14": None,
        "ema9": None,
        "ema21": None,
        "spread": spread_points,
        "spread_anomaly": spread_anomaly,
        "circuit_open": circuit_open,
        "consecutive_losses": int(account.get("consecutive_losses", 0) or 0),
    }

    # Attempt to pull indicators from signal trace (if signal generator attached them)
    indicators = trace.get("indicators", {}) if isinstance(trace.get("indicators", {}), dict) else {}
    if indicators:
        features["rsi14"] = indicators.get("rsi14")
        features["adx14"] = indicators.get("adx") or indicators.get("adx14")
        features["ema9"] = indicators.get("ema9")
        features["ema21"] = indicators.get("ema21")
    features = _hydrate_llm_verification_indicators(ctx, features)

    context_kwargs: Dict[str, Any] = {
        "position_open": position_open,
        "position_side": position_side,
        "session_mode": str(market.get("session", account.get("session_name", "")) or ""),
        "ts": datetime.now(timezone.utc).isoformat(),
    }

    # 鈹€鈹€ Run LLM verification 鈹€鈹€
    try:
        result = verify_and_calibrate(side, original_confidence, features, context_kwargs)
    except Exception:
        # Safety: if verification FAILS, reduce confidence to force HOLD
        ctx.promotion_info["llm_verification"] = {"final_verdict": "VETO", "reason": "verification_error"}
        signal_id = str(ctx.signal.get("signal_id", f"llm_verify_err_{int(time.time())}"))
        ctx.signal = _build_idle_signal(
            signal_id=f"{signal_id}__verify_error",
            symbol=ctx.symbol,
            reject_reason="LLM_VERIFICATION_ERROR",
            evidence=["verification_call_failed"],
            risk_flags=["LLM_VERIFY_ERROR"],
        )
        return PipelineOutcome(
            execution_report=_build_execution_report(
                str(ctx.signal.get("signal_id", "")),
                "REJECTED",
                "LLM_VERIFICATION_ERROR",
            ),
            ledger_events=["llm_verification_error"],
            append_runtime_log=True,
        )

    # Store result in ctx for downstream observability
    ctx.promotion_info["llm_verification"] = {
        "original_confidence": result.original_confidence,
        "calibrated_confidence": result.calibrated_confidence,
        "final_verdict": result.final_verdict,
        "stage2_llm_verdict": result.stage2_llm_verdict,
        "stage4_gate_passed": result.stage4_gate_passed,
        "adjustment_reason": result.adjustment_reason,
        "latency_total_ms": result.latency_total_ms,
    }

    # 鈹€鈹€ Minimum confidence gate: signals below threshold cannot execute 鈹€鈹€
    MIN_SIGNAL_CONFIDENCE = DEFAULT_EXECUTION_MIN_CONFIDENCE
    if isinstance(result, dict):
        calibrated = float(result.get("calibrated_confidence", 0.0) or 0.0)
    else:
        calibrated = float(getattr(result, "calibrated_confidence", 0.0) or 0.0)
    if calibrated < MIN_SIGNAL_CONFIDENCE:
        signal_id = str(ctx.signal.get("signal_id", f"low_conf_{int(time.time())}"))
        ctx.signal = _build_idle_signal(
            signal_id=f"{signal_id}__low_confidence",
            symbol=ctx.symbol,
            reject_reason=f"CONFIDENCE_TOO_LOW:{calibrated:.2f}<{MIN_SIGNAL_CONFIDENCE}",

            evidence=[f"calibrated_confidence:{calibrated:.2f}"],
            risk_flags=["LOW_CONFIDENCE"],        )
        return None

    # 鈹€鈹€ Apply verification result 鈹€鈹€
    final_verdict = result.get("final_verdict") if isinstance(result, dict) else getattr(result, "final_verdict", "GO")
    if final_verdict == "VETO":
        signal_id = str(ctx.signal.get("signal_id", f"llm_veto_{int(time.time())}"))
        if isinstance(result, dict):
            orig_conf = result.get("calibrated_confidence", 0.0)
            stage2 = result.get("stage4_gate_passed", False)
            reason = result.get("adjustment_reason", "")
        else:
            orig_conf = result.original_confidence
            stage2 = result.stage2_llm_verdict
            reason = result.adjustment_reason
        ctx.signal = _build_idle_signal(
            signal_id=f"{signal_id}__llm_veto",
            symbol=ctx.symbol,
            reject_reason="LLM_VERIFICATION_VETO",
            evidence=[
                f"original_confidence:{orig_conf}",
                f"stage2_verdict:{stage2}",
                f"reason:{str(reason)[:120]}",
            ],
            risk_flags=["LLM_VETO", f"STAGE2:{stage2}"],
        )
        return PipelineOutcome(
            execution_report=_build_execution_report(
                str(ctx.signal.get("signal_id", "")),
                "REJECTED",
                "LLM_VERIFICATION_VETO",
            ),
            ledger_events=["llm_verification_veto"],
            append_runtime_log=True,
        )

    # Apply calibrated confidence to signal trace
    trace["confidence"] = result.calibrated_confidence
    trace.setdefault("evidence", []).append(
        f"llm_calibrated:{result.original_confidence:.3f}->{result.calibrated_confidence:.3f}"
    )
    trace.setdefault("risk_flags", []).append(f"LLM_STAGE2:{result.stage2_llm_verdict}")
    ctx.signal["trace"] = trace

    return None


def _load_run_context(
    ctx: PipelineRunContext,
    *,
    input_path: Path,
    live_client: Optional[Any],
) -> None:
    ctx.mt5_snapshot = _load_snapshot(ctx.source, input_path, live_client)
    _ensure_snapshot_bars(
        ctx.mt5_snapshot,
        symbol=str(ctx.symbol or ctx.mt5_snapshot.get("symbol", "XAUUSD") or "XAUUSD"),
        min_count=50,
        preferred_count=160,
    )
    ctx.engine_request = to_engine_request(ctx.mt5_snapshot)
    _apply_panel_sltp_to_ai_plan(ctx.mt5_snapshot, ctx.engine_request)
    _inject_account_risk_metrics(ctx.engine_request, ctx.project_root)
    _refresh_runtime_open_guard_state(
        ctx.project_root,
        mt5_snapshot=ctx.mt5_snapshot,
        engine_request=ctx.engine_request,
    )
    # Sync runtime_open_guard back to agent_state so live_volume reads consistent data
    try:
        guard_state = _load_runtime_open_guard_state(ctx.project_root)
        if guard_state:
            agent_path = ctx.project_root / "data" / "bridge" / "state" / "agent_state.json"
            if agent_path.exists():
                agent = _load_agent_state_snapshot(ctx.project_root)
                agent["position_open"] = guard_state.get("position_open", False)
                agent["position_ticket"] = guard_state.get("position_ticket", 0)
                agent["position_entry"] = guard_state.get("position_entry", 0.0)
                agent["position_side"] = guard_state.get("position_side", "")
                agent["last_close_time"] = guard_state.get("last_close_time", 0)
                agent["last_closed_ts"] = guard_state.get("last_closed_ts", 0)
                agent["recent_closures"] = guard_state.get("recent_closures", [])
                agent["consecutive_losses"] = guard_state.get("consecutive_losses", 0)
                agent["incident_controls"] = guard_state.get("incident_controls", {})
                # BUG FIX: 琛ラ綈涔嬪墠閬楁紡鐨?key锛屽惁鍒?cooldown / reentry veto 澶辨晥
                agent["_strategy_close_times"] = guard_state.get("_strategy_close_times", {})
                agent["_strategy_close_sides"] = guard_state.get("_strategy_close_sides", {})
                agent["last_closed_strategy"] = guard_state.get("last_closed_strategy", "")
                agent["last_close_action"] = guard_state.get("last_close_action", "")
                agent["trailing_telemetry"] = guard_state.get("trailing_telemetry", {})
                _write_json(agent_path, agent)
    except Exception:
        pass
    ctx.engine_request["runtime_feedback"] = ctx.mt5_snapshot.get("runtime_feedback", {})
    ctx.scheduler_snapshot = _load_scheduler_snapshot(ctx.project_root, ctx.engine_request)
    # Update interpreter contract from session_analyzer so downstream
    # consumers (verify_and_calibrate, decision_snapshot) get real market
    # structure data instead of fallback defaults.
    try:
        from bridge.session_analyzer import export_market_structure
        from agent_decision_contract import update_interpreter_contract
        interpreter_data = export_market_structure()
        if interpreter_data:
            update_interpreter_contract(interpreter_data)
    except Exception:
        pass
    ctx.agent_context = _load_agent_decision_context(ctx.project_root, ctx.scheduler_snapshot)
    # 鈹€鈹€ Regime classification: 4-dimension (ADX+EMA+ATR+Volume) 鈹€鈹€
    try:
        from bridge.session_analyzer import _adx as _calc_adx
        # Pull enough M5 bars for a stable EMA50/ATR baseline; 50 bars only
        # produces the first EMA50 point and makes regime switching laggy.
        bars = _ensure_snapshot_bars(
            ctx.mt5_snapshot,
            symbol=str(ctx.symbol or ctx.mt5_snapshot.get("symbol", "XAUUSD") or "XAUUSD"),
            min_count=50,
            preferred_count=160,
        )
        ctx.mt5_snapshot["bars"] = bars
        if bars and len(bars) >= 15:
            # 鈹€鈹€ 4D Classifier (regime_router) replaces ADX-only (regime_aware_params) 鈹€鈹€
            from src.regime_router import classify_regime as _classify_4d
            prev_regime = str(ctx.agent_context.get("regime", "UNKNOWN") or "UNKNOWN")
            _trans_count = int(ctx.agent_context.get("_transition_count", 0) or 0)
            _change_count = int(ctx.agent_context.get("_regime_change_count", 0) or 0)
            _result_4d = _classify_4d(
                bars,
                prev_regime=prev_regime,
                transition_count=_trans_count,
                change_count=_change_count,
            )
            # Store 4D result
            ctx.agent_context["regime"] = _result_4d["regime"]
            ctx.agent_context["regime_confidence"] = _result_4d["confidence"]
            try:
                from src.strategy_registry import list_compatible_strategies
                from bridge.gold_trader import get_brain

                session_mode = str(
                    (
                        ctx.agent_context.get("session_profile", {})
                        if isinstance(ctx.agent_context.get("session_profile", {}), dict)
                        else {}
                    ).get("mode", "")
                    or ""
                ).upper()
                ctx.agent_context["regime_allowed_strategies"] = list_compatible_strategies(
                    _result_4d["regime"],
                    regime_confidence=float(_result_4d.get("confidence", 0.0) or 0.0),
                    session_mode=session_mode,
                    brain=get_brain(),
                    min_win_rate=0.40,
                )
            except Exception:
                ctx.agent_context["regime_allowed_strategies"] = []
            ctx.agent_context["regime_weight_profile"] = _result_4d.get("weight_profile", {})
            ctx.agent_context["regime_flags"] = _result_4d.get("flags", [])
            ctx.agent_context["regime_classification"] = _result_4d
            ctx.agent_context["_transition_count"] = int(_result_4d.get("transition_count", 0) or 0)
            ctx.agent_context["_regime_change_count"] = int(_result_4d.get("change_count", 0) or 0)
            # 鈹€鈹€ Keep ADX-based params for downstream compatibility 鈹€鈹€
            adx_val, _ = _calc_adx(bars)
            from src.regime_aware_params import get_regime_param_bank
            bank = get_regime_param_bank()
            regime_params = bank.get_params_for_adx(adx_val)
            ctx.agent_context["regime_params"] = regime_params
            ctx.agent_context["adx_value"] = adx_val
    except Exception:
        pass  # non-blocking: regime classification is best-effort
    # 鈹€鈹€ Market adaptive profile confidence delta 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
    try:
        from src.market_adaptive_profile import build_market_adaptive_profile
        profile = build_market_adaptive_profile()
        ctx.agent_context["adaptive_profile"] = profile
    except Exception:
        pass  # non-blocking
    ctx.scheduler_snapshot = _route_scheduler_snapshot(ctx.scheduler_snapshot, ctx.agent_context, ctx.engine_request)
    ctx.agent_context["route"] = (
        ctx.scheduler_snapshot.get("route", {})
        if isinstance(ctx.scheduler_snapshot.get("route", {}), dict)
        else {}
    )
    # 鈹€鈹€ 绛栫暐鍒囨崲鏃ュ織锛氳褰曟瘡娆″競鍐靛垽瀹氫笌绛栫暐璺敱鍙樺寲 鈹€鈹€
    _log_strategy_routing(ctx.project_root, ctx.agent_context, ctx.scheduler_snapshot)
    ctx.strategy_signals = _build_instance_signals(ctx, ctx.engine_request, ctx.project_root, ctx.scheduler_snapshot)
    if _is_plugin_signals_enabled(ctx.project_root):
        plugin_signals = _build_plugin_signals(
            ctx.mt5_snapshot, ctx.project_root, ctx.scheduler_snapshot
        )
        ctx.strategy_signals.extend(plugin_signals)
    _collect_suppressed_strategy_intents(ctx.strategy_signals)
    ctx.strategy_signals = _apply_agent_decision_policy(ctx.strategy_signals, ctx.agent_context)
    ctx.strategy_signals, ctx.aggregated_intent = _aggregate_instance_signals(
        ctx.strategy_signals,
        ctx.scheduler_snapshot,
        ctx.agent_context,
        project_root=ctx.project_root,
    )
    # Multi-dispatch: ctx.strategy_signals is now a list of dispatch-ready signals.
    # For backward compatibility with downstream code that reads ctx.signal:
    ctx.signal = ctx.strategy_signals[0] if ctx.strategy_signals else {}
    ctx.signal_attribution = _attach_signal_attribution(ctx.signal, ctx.aggregated_intent)
    _sync_multi_strategy_blackboard(
        ctx.project_root,
        ctx.scheduler_snapshot,
        ctx.strategy_signals,
        ctx.aggregated_intent,
    )
    _emit_scheduler_health_alerts(ctx.project_root, ctx.scheduler_snapshot)
    _mailbox_emit(
        ctx.project_root,
        "Risk_Commissar",
        "REPORT",
        f"铚傜兢鐘舵€?| 璋冨害={ctx.scheduler_snapshot.get('mode', '?') or '?'} | "
        f"瀹炰緥瀛?{len(ctx.scheduler_snapshot.get('active_instance_ids', []))} | "
        f"绛栫暐={len(ctx.scheduler_snapshot.get('active_strategy_ids', []))} | "
        f"琛屾儏={ctx.scheduler_snapshot.get('route', {}).get('regime', '?') or '?'} | "
        f"瀹忚={ctx.scheduler_snapshot.get('route', {}).get('effective_macro_direction', '?') or '?'} | "
        f"淇″彿={ctx.aggregated_intent.get('action', '?') or '?'} "
        f"缃俊搴?{float(ctx.aggregated_intent.get('confidence', 0) or 0):.0%}",
        "scheduler_periodic_heartbeat",
        25.0,
    )
    ctx.signal_id = str(ctx.signal.get("signal_id"))
    ctx.state_path = ctx.project_root / "data/execution_state.json"


def _evaluate_market_data_delay_gate(ctx: PipelineRunContext) -> Optional[PipelineOutcome]:
    market_metrics = (
        ctx.engine_request.get("market_metrics", {})
        if isinstance(ctx.engine_request.get("market_metrics", {}), dict)
        else {}
    )
    raw_delay = market_metrics.get("market_data_delay_ms", 0.0)
    # If live source couldn't compute a trustworthy delay (None), compute it
    # from the pipeline's own bar data 鈥?making the pipeline the single source of truth.
    if raw_delay is None:
        now_ms = int(time.time() * 1000)
        # Pull bars from the full mt5_snapshot (not market_metrics which is filtered)
        market_full = ctx.mt5_snapshot.get("market", {}) if isinstance(ctx.mt5_snapshot.get("market", {}), dict) else {}
        bars = market_full.get("bars", []) if isinstance(market_full.get("bars", []), list) else []
        if bars:
            try:
                last_bar_time = int(bars[-1].get("time", 0) or 0)
                if last_bar_time > 1_000_000_000:
                    raw_delay = max(0.0, now_ms - last_bar_time * 1000.0)
            except Exception:
                pass
        if raw_delay is None:
            # If we truly cannot determine delay, skip the gate rather than
            # silently passing with a fake zero.
            return None
    delay_ms = float(raw_delay or 0.0)
    threshold_ms = _resolve_market_data_delay_gate_ms(ctx.project_root, ctx.runtime_state)
    if threshold_ms <= 0 or delay_ms <= threshold_ms:
        return None
    ctx.signal = _build_idle_signal(
        signal_id=f"market_data_delay_gate_{int(time.time())}",
        symbol=ctx.symbol,
        reject_reason="MARKET_DATA_DELAY_GATE",
        evidence=[
            f"market_data_delay_ms={delay_ms:.0f}",
            f"market_data_delay_threshold_ms={threshold_ms:.0f}",
        ],
        risk_flags=["MARKET_DATA_DELAY_GATE", "MARKET_DATA_STALE"],
    )
    return PipelineOutcome(
        execution_report=_build_execution_report(
            str(ctx.signal.get("signal_id", "")),
            "REJECTED",
            "MARKET_DATA_DELAY_GATE",
            market_data_delay_ms=round(delay_ms, 3),
            market_data_delay_threshold_ms=round(threshold_ms, 3),
        ),
        ledger_events=["market_data_delay_gate"],
        append_runtime_log=True,
    )


def _evaluate_retry_outcome(ctx: PipelineRunContext) -> Optional[PipelineOutcome]:
    if ctx.state_path is None:
        raise RuntimeError("pipeline state_path is not initialized")
    action_upper = str(ctx.signal.get("action", "HOLD") or "HOLD").upper()
    if action_upper == "HOLD":
        return None
    create_or_get_signal_state(ctx.state_path, ctx.signal_id)
    existing = get_signal_state(ctx.state_path, ctx.signal_id) or {}
    retry_meta = _state_retry_meta(existing)
    now_epoch = time.time()
    last_epoch = _ts_to_epoch(str(retry_meta.get("last_ts", "")))
    age = max(0.0, now_epoch - last_epoch) if last_epoch > 0 else 10**9
    if is_final_state(ctx.state_path, ctx.signal_id):
        return PipelineOutcome(
            execution_report=_build_execution_report(
                ctx.signal_id,
                "ACKED",
                f"DUPLICATE_SIGNAL_BLOCKED_STATE_{existing.get('state', 'UNKNOWN')}",
            ),
            ledger_events=["duplicate_signal_blocked"],
            append_runtime_log=True,
        )
    if retry_meta.get("state") == "ACKED" and age < ACK_RETRY_COOLDOWN_SECONDS:
        return PipelineOutcome(
            execution_report=_build_execution_report(ctx.signal_id, "ACKED", "ACK_RETRY_THROTTLED"),
            ledger_events=["ack_retry_throttled"],
            append_runtime_log=True,
        )
    if retry_meta.get("state") == "TIMEOUT":
        timeout_count = int(retry_meta.get("timeout_count", 0))
        if timeout_count >= MAX_TIMEOUT_RETRIES:
            transition_state(
                ctx.state_path,
                ctx.signal_id,
                "REJECTED",
                reason="RETRY_EXHAUSTED_TIMEOUT",
                metadata=ctx.signal_attribution,
            )
            return PipelineOutcome(
                execution_report=_build_execution_report(ctx.signal_id, "REJECTED", "RETRY_EXHAUSTED_TIMEOUT"),
                ledger_events=["timeout_retry_exhausted"],
                append_runtime_log=True,
            )
        if age < TIMEOUT_RETRY_COOLDOWN_SECONDS:
            return PipelineOutcome(
                execution_report=_build_execution_report(ctx.signal_id, "TIMEOUT", "TIMEOUT_RETRY_THROTTLED"),
                ledger_events=["timeout_retry_throttled"],
                append_runtime_log=True,
            )
    return None


def _detect_falling_knife_risk(ctx: PipelineRunContext, signal_side: str) -> Dict[str, Any]:
    """Detect counter-trend momentum risk using ATR-normalised price movement.

    Instead of fixed percentage thresholds, this uses:
      - ATR from bars to express momentum in volatility units
      - Market regime to adjust sensitivity (trend tolerates more pullback)
      - Volume context for confirmation
      - Bar range vs close-to-close delta for V-reversal detection

    Returns risk_level = LOW / MEDIUM / HIGH.
    """
    result = {
        "risk_level": "LOW",
        "momentum_direction": "NEUTRAL",
        "momentum_atr": 0.0,
        "momentum_strength_pct": 0.0,
        "bar_timeframe": "",
        "should_block_market": False,
        "should_force_pullback": False,
        "should_reject_open": False,
        "reason": "",
    }

    side = str(signal_side or "").upper()
    if side not in {"BUY", "SELL"}:
        return result

    try:
        market_context = _resolve_market_context_cached(
            ctx.agent_context,
            ctx.engine_request,
            mt5_snapshot=getattr(ctx, "mt5_snapshot", None),
        )
        momentum = market_context.get("momentum", {}) if isinstance(market_context.get("momentum", {}), dict) else {}
        atr = float(momentum.get("atr", 0.0) or 0.0)
        if atr <= 0:
            return result

        m3_atr = float(momentum.get("m3_atr", 0.0) or 0.0)
        weighted_atr = float(momentum.get("weighted_atr", 0.0) or 0.0)
        result["momentum_atr"] = round(abs(weighted_atr), 3)
        signal = ctx.signal if isinstance(getattr(ctx, "signal", None), dict) else {}
        result["bar_timeframe"] = str(
            momentum.get("timeframe")
            or getattr(ctx, "mt5_snapshot", {}).get("timeframe")
            or getattr(ctx, "engine_request", {}).get("timeframe")
            or signal.get("timeframe")
            or "M5"
        ).upper()
        result["momentum_direction"] = str(momentum.get("momentum_direction", "NEUTRAL") or "NEUTRAL")
        if result["momentum_direction"] not in {"BULLISH", "BEARISH"}:
            return result  # flat, no risk

        # 鈹€鈹€ 3. Contrarian check 鈹€鈹€
        is_contrarian = (
            (side == "SELL" and weighted_atr > 0) or
            (side == "BUY" and weighted_atr < 0)
        )
        if not is_contrarian:
            return result

        abs_m3 = abs(m3_atr)
        abs_weighted = abs(weighted_atr)

        regime = str(market_context.get("regime", "UNKNOWN") or "UNKNOWN").upper()
        session_mode = str(market_context.get("session_mode", "NORMAL") or "NORMAL").upper()
        counter_trend_risk = str(market_context.get("counter_trend_risk", "LOW") or "LOW").upper()
        thresholds_cfg = config_manager.get_falling_knife_thresholds()
        cfg_high_scale = max(
            0.65,
            min(
                1.6,
                (
                    float(thresholds_cfg.get("high_m3", 0.25) or 0.25) / 0.25
                    + float(thresholds_cfg.get("high_weighted", 0.35) or 0.35) / 0.35
                ) / 2.0,
            ),
        )
        cfg_medium_scale = max(
            0.65,
            min(
                1.6,
                (
                    float(thresholds_cfg.get("medium_m3", 0.12) or 0.12) / 0.12
                    + float(thresholds_cfg.get("medium_weighted", 0.18) or 0.18) / 0.18
                ) / 2.0,
            ),
        )
        if regime in {"STRONG_TREND", "WEAK_TREND", "STEALTH_TREND"}:
            high_atr, medium_atr = 2.0, 1.25
        elif regime in {"RANGING", "TRANSITION"}:
            high_atr, medium_atr = 1.05, 0.65
        elif regime in {"VOLATILITY_SPIKE", "EXTREME"}:
            high_atr, medium_atr = 2.3, 1.5
        else:
            high_atr, medium_atr = 1.45, 0.95
        high_atr *= cfg_high_scale
        medium_atr *= cfg_medium_scale

        # 鈹€鈹€ 5. Volume confirmation: if vol is contracting, the move is less trustworthy 鈹€鈹€
        snap = getattr(ctx, "mt5_snapshot", None)
        bars = snap.get("bars", []) if isinstance(snap, dict) and isinstance(snap.get("bars", []), list) else []
        vol_contracting = False
        try:
            vols = [float(b.get("tick_volume", 0) or 0) for b in bars[-10:]]
            if len(vols) >= 5:
                recent_vol = sum(vols[-3:]) / 3
                baseline_vol = sum(vols[:5]) / 5 if sum(vols[:5]) > 0 else 1
                vol_contracting = recent_vol < baseline_vol * 0.7
        except Exception:
            pass
        if vol_contracting:
            high_atr *= 0.8   # contracting vol 鈫?lower threshold, more cautious
            medium_atr *= 0.8

        # 鈹€鈹€ 6. V-reversal detection (range >> close-to-close delta) 鈹€鈹€
        if len(bars) >= 3:
            recent_3 = bars[-3:]
            try:
                _highs = [float(b.get("high", b.get("close", 0)) or 0) for b in recent_3]
                _lows = [float(b.get("low", b.get("close", 0)) or 0) for b in recent_3]
                _peak = max(_highs)
                _trough = min(_lows)
                if _peak > 0 and atr > 0:
                    range_atr = (_peak - _trough) / atr
                    # V-reversal: range > 2.5 ATR but close-to-close < 0.5 ATR
                    if range_atr > 2.5 and abs_m3 < 0.5:
                        if result["risk_level"] == "LOW":
                            result["risk_level"] = "MEDIUM"
                            result["should_block_market"] = True
                            result["reason"] = (
                                f"FALLING_KNIFE_V_RECOVERY:{side}_vs_{result['momentum_direction']}"
                                f"_range={range_atr:.1f}ATR_m3={m3_atr:.1f}ATR"
                            )
            except Exception:
                pass

        # 鈹€鈹€ 7. Classify risk 鈹€鈹€
        if abs_m3 >= high_atr or abs_weighted >= high_atr:
            result["risk_level"] = "HIGH"
            result["should_block_market"] = True
            result["should_force_pullback"] = True
            result["reason"] = (
                f"FALLING_KNIFE:{side}_vs_{result['momentum_direction']}"
                f"_m3={m3_atr:.1f}ATR_w={weighted_atr:.1f}ATR"
            )
        elif abs_m3 >= medium_atr or abs_weighted >= medium_atr:
            result["risk_level"] = "MEDIUM"
            result["should_block_market"] = True
            result["reason"] = (
                f"FALLING_KNIFE_MEDIUM:{side}_vs_{result['momentum_direction']}"
                f"_m3={m3_atr:.1f}ATR"
            )

        result["momentum_strength_pct"] = round(min(200.0, (abs_weighted / max(high_atr, 0.01)) * 100.0), 1)
        if result["risk_level"] == "HIGH" and (
            regime in {"RANGING", "TRANSITION", "VOLATILITY_SPIKE", "FAKE_TREND", "EXTREME"}
            or session_mode in {"ASIA_NOISE", "ASIA_DEFENSIVE"}
            or counter_trend_risk == "HIGH"
        ):
            result["should_reject_open"] = True
            result["reason"] = f"{result['reason']}:HARD_REJECT:{regime}:{session_mode}:{counter_trend_risk}"
        elif result["risk_level"] == "MEDIUM" and regime in {"RANGING", "TRANSITION"} and counter_trend_risk == "HIGH":
            result["should_force_pullback"] = True

        return result
    except Exception:
        return result


def _evaluate_guard_outcome(ctx: PipelineRunContext) -> Optional[PipelineOutcome]:
    if ctx.state_path is None:
        ctx.state_path = ctx.project_root / "data/execution_state.json"
    macro_context = (
        ctx.agent_context.get("macro", {}) if isinstance(ctx.agent_context.get("macro", {}), dict) else {}
    )
    macro_authority = _resolve_macro_authority_context(macro_context)
    nominal_binding_direction = _resolve_binding_direction(macro_context)
    binding_direction = _resolve_execution_binding_direction(macro_context)
    if binding_direction == "NEUTRAL":
        agg_macro = (
            ctx.aggregated_intent.get("agent_context", {})
            if isinstance(ctx.aggregated_intent.get("agent_context", {}), dict)
            else {}
        )
        fallback_binding = _resolve_execution_binding_direction(agg_macro)
        fallback_authority = _resolve_macro_authority_context(agg_macro)
        if fallback_authority == "HARD_BIND" and fallback_binding in {"LONG", "SHORT"}:
            binding_direction = fallback_binding
    action_upper = str(ctx.signal.get("action", "HOLD") or "HOLD").upper()
    signal_side_upper = str(ctx.signal.get("side", "NONE") or "NONE").upper()
    # 鈹€鈹€ Incident controls hard block (P0 fix: make incident_engine controls actually block trading) 鈹€鈹€
    if action_upper == "OPEN" and signal_side_upper in {"BUY", "SELL"}:
        incident_ctrl = _incident_controls(ctx.runtime_state)
        if bool(incident_ctrl.get("disable_new_open", False)):
            block_reason = f"INCIDENT_DISABLE_NEW_OPEN:{incident_ctrl.get('disable_new_open_reason', 'auto_action')}"
            transition_state(ctx.state_path, ctx.signal_id, "REJECTED", reason=block_reason, metadata=ctx.signal_attribution)
            return _build_open_guard_rejection_outcome(
                ctx,
                block_reason=block_reason,
                guard_snapshot={"allow_open": False, "reason": block_reason, "incident_controls": incident_ctrl},
            )
        if bool(ctx.runtime_state.get("kill_switch", False)):
            block_reason = "KILL_SWITCH_HARD_BLOCK"
            transition_state(ctx.state_path, ctx.signal_id, "REJECTED", reason=block_reason, metadata=ctx.signal_attribution)
            return _build_open_guard_rejection_outcome(
                ctx,
                block_reason=block_reason,
                guard_snapshot={"allow_open": False, "reason": block_reason, "kill_switch": True},
            )
    if (
        action_upper == "OPEN"
        and signal_side_upper in {"BUY", "SELL"}
        and binding_direction in {"LONG", "SHORT"}
        and _binding_side_conflicts(binding_direction, signal_side_upper)
    ):
        block_reason = "MACRO_DIRECTION_BINDING_BLOCK"
        transition_state(ctx.state_path, ctx.signal_id, "REJECTED", reason=block_reason, metadata=ctx.signal_attribution)
        return _build_binding_rejection_outcome(
            ctx,
            binding_direction=nominal_binding_direction,
            block_reason=block_reason,
        )

    structural_guard = (
        (
            (
                ctx.agent_context.get("interpreter", {})
                if isinstance(ctx.agent_context.get("interpreter", {}), dict)
                else {}
            ).get("hard_conflict_guard", {})
        )
        if isinstance(ctx.agent_context, dict)
        else {}
    )
    if (
        action_upper == "OPEN"
        and signal_side_upper in {"BUY", "SELL"}
        and isinstance(structural_guard, dict)
        and bool(structural_guard.get("enabled", False))
        and macro_authority != "HARD_BIND"
        and _structural_side_conflicts(
            str(structural_guard.get("structural_side", "NONE") or "NONE").upper(),
            signal_side_upper,
        )
    ):
        block_reason = "STRUCTURAL_DIRECTION_CONFLICT_BLOCK"
        ctx.signal["structural_conflict_guard"] = dict(structural_guard)
        ctx.aggregated_intent["structural_conflict_guard"] = dict(structural_guard)
        transition_state(ctx.state_path, ctx.signal_id, "REJECTED", reason=block_reason, metadata=ctx.signal_attribution)
        return _build_binding_rejection_outcome(
            ctx,
            binding_direction=nominal_binding_direction,
            block_reason=block_reason,
        )

    if action_upper == "OPEN" and signal_side_upper in {"BUY", "SELL"}:
        # 鈹€鈹€ 璇诲彇 account_metrics锛坆alance/equity 鏉ヨ嚜 mt5_adapter锛夆攢鈹€
        account = (ctx.engine_request.get("account_metrics", {})
                   if isinstance(ctx.engine_request, dict) else {})
        route_payload = (
            ctx.scheduler_snapshot.get("route", {})
            if isinstance(ctx.scheduler_snapshot.get("route", {}), dict)
            else {}
        )
        execution_decision = (
            ctx.aggregated_intent.get("execution_decision", {})
            if isinstance(ctx.aggregated_intent.get("execution_decision", {}), dict)
            else {}
        )
        entry_policy_name = str(
            route_payload.get("entry_policy")
            or ctx.aggregated_intent.get("entry_policy")
            or ""
        ).strip().lower()
        single_entry_mode = bool(
            str(execution_decision.get("decision", "") or "").upper() == "EXECUTE"
            and entry_policy_name in {"regime_directed", "selected_strategy_only"}
        )
        entry_owner_strategy_id = canonical_strategy_id(
            str(
                execution_decision.get("entry_owner_strategy_id")
                or route_payload.get("entry_owner_strategy_id")
                or ctx.signal_attribution.get("entry_owner_strategy_id", "")
                or ctx.signal_attribution.get("attributed_strategy_id", "")
                or ctx.signal.get("strategy_id", "")
                or ""
            )
        )
        existing_runtime_guard = _load_runtime_open_guard_state(ctx.project_root)
        existing_positions = (
            existing_runtime_guard.get("positions_all", [])
            if isinstance(existing_runtime_guard.get("positions_all", []), list)
            else []
        )

        # 鈹€鈹€ Guard 0: 闃查鍒€锛堥槻鎺ラ鍒€锛夆攢鈹€
        knife = _detect_falling_knife_risk(ctx, signal_side_upper)
        knife_risk = str(knife.get("risk_level", "LOW") or "LOW").upper()
        # 濮嬬粓璁板綍椋炲垁鍒ゅ畾缁撴灉锛屽嵆浣?LOW 涔熻涓嬫父鍙
        ctx.signal_attribution["falling_knife"] = {
            "risk_level": knife_risk,
            "momentum_direction": knife.get("momentum_direction"),
            "momentum_strength_pct": knife.get("momentum_strength_pct"),
            "bar_timeframe": knife.get("bar_timeframe", ""),
            "reason": knife.get("reason", ""),
        }
        if bool(knife.get("should_reject_open", False)):
            block_reason = str(knife.get("reason", "FALLING_KNIFE_HARD_REJECT") or "FALLING_KNIFE_HARD_REJECT")
            transition_state(ctx.state_path, ctx.signal_id, "REJECTED", reason=block_reason, metadata=ctx.signal_attribution)
            return _build_open_guard_rejection_outcome(
                ctx,
                block_reason=block_reason,
                guard_snapshot={
                    "allow_open": False,
                    "reason": block_reason,
                    "falling_knife": dict(ctx.signal_attribution.get("falling_knife", {})),
                },
            )
        if knife_risk in {"HIGH", "MEDIUM"}:
            ctx.signal["entry_style_forced_by"] = f"FALLING_KNIFE_{knife_risk}"
            ctx.signal.setdefault("risk_flags", [])
            if isinstance(ctx.signal.get("risk_flags"), list):
                flag = "FALLING_KNIFE_HARD" if knife_risk == "HIGH" else "FALLING_KNIFE_SOFT"
                if flag not in ctx.signal["risk_flags"]:
                    ctx.signal["risk_flags"].append(flag)
            if knife_risk == "HIGH" or bool(knife.get("should_force_pullback", False)):
                ctx.signal["entry_timing_hint"] = "WAIT_PULLBACK"
                ctx.signal["entry_style"] = "PULLBACK_LIMIT"
            else:
                # MEDIUM: force limit order, no market chasing
                ctx.signal["entry_style"] = "LIMIT"

        # 鈹€鈹€ Guard 1: 姣忔棩浜忔崯鐔旀柇 (max_daily_loss_pct) 鈹€鈹€
        try:
            panel_max_daily_loss_pct = float(
                config_manager.get_effective_param("max_daily_loss_pct", 2.0))
        except (TypeError, ValueError):
            panel_max_daily_loss_pct = 2.0
        if panel_max_daily_loss_pct > 0:
            try:
                daily_loss_pct = float(account.get("daily_realized_loss_pct", 0.0) or 0.0)
            except (TypeError, ValueError):
                daily_loss_pct = 0.0
            if daily_loss_pct >= panel_max_daily_loss_pct:
                block_reason = (f"DAILY_LOSS_CIRCUIT_BREAKER:{daily_loss_pct:.2f}%>="
                                f"{panel_max_daily_loss_pct:.1f}%")
                transition_state(ctx.state_path, ctx.signal_id, "REJECTED",
                                 reason=block_reason, metadata=ctx.signal_attribution)
                return _build_open_guard_rejection_outcome(
                    ctx, block_reason=block_reason,
                    guard_snapshot={"allow_open": False, "reason": block_reason,
                                    "daily_realized_loss_pct": daily_loss_pct,
                                    "max_daily_loss_pct": panel_max_daily_loss_pct})

        # 鈹€鈹€ Guard 2: 璐︽埛鎬讳簭鎹熺啍鏂?(account_total_loss_stop_pct) 鈹€鈹€
        try:
            total_loss_stop_pct = float(
                config_manager.get_effective_param("account_total_loss_stop_pct", 30.0))
        except (TypeError, ValueError):
            total_loss_stop_pct = 30.0
        if total_loss_stop_pct > 0:
            try:
                balance = float(account.get("balance", 0.0) or 0.0)
                equity = float(account.get("equity", 0.0) or 0.0)
            except (TypeError, ValueError):
                balance = equity = 0.0
            if balance > 0:
                drawdown_pct = round((balance - equity) / balance * 100.0, 2)
                if drawdown_pct >= total_loss_stop_pct:
                    block_reason = (f"ACCOUNT_TOTAL_LOSS_STOP:{drawdown_pct:.2f}%>="
                                    f"{total_loss_stop_pct:.1f}%")
                    transition_state(ctx.state_path, ctx.signal_id, "REJECTED",
                                     reason=block_reason, metadata=ctx.signal_attribution)
                    return _build_open_guard_rejection_outcome(
                        ctx, block_reason=block_reason,
                        guard_snapshot={"allow_open": False, "reason": block_reason,
                                        "drawdown_pct": drawdown_pct,
                                        "total_loss_stop_pct": total_loss_stop_pct})

        # 鈹€鈹€ Guard 3: 鍏ㄥ眬鏈€澶ф寔浠撴暟 (max_open_positions) 鈹€鈹€
        try:
            max_open_positions = int(float(
                config_manager.get_effective_param("max_open_positions",
                                                   config_manager.get_effective_param("max_positions", 4))))
        except (TypeError, ValueError):
            max_open_positions = 4
        if max_open_positions > 0:
            try:
                current_positions = int(float(account.get("current_open_positions", 0) or 0))
            except (TypeError, ValueError):
                current_positions = 0
            if current_positions >= max_open_positions:
                block_reason = f"MAX_OPEN_POSITIONS:{current_positions}>={max_open_positions}"
                transition_state(ctx.state_path, ctx.signal_id, "REJECTED",
                                 reason=block_reason, metadata=ctx.signal_attribution)
                return _build_open_guard_rejection_outcome(
                    ctx, block_reason=block_reason,
                    guard_snapshot={"allow_open": False, "reason": block_reason,
                                    "current_open_positions": current_positions,
                                    "max_open_positions": max_open_positions})

        # 鈹€鈹€ Guard 3.5: 鍗曞競鍐靛崟绛栫暐妯″紡涓嬶紝涓嶅厑璁稿悓绛栫暐閲嶅寮€浠?/ 鍚屽悜鍙犱粨 鈹€鈹€
        if single_entry_mode and existing_positions:
            same_strategy_positions = [
                position
                for position in existing_positions
                if isinstance(position, dict)
                and canonical_strategy_id(position.get("strategy_id", "")) == entry_owner_strategy_id
            ]
            if entry_owner_strategy_id and same_strategy_positions:
                block_reason = f"SINGLE_ENTRY_DUPLICATE_STRATEGY_BLOCK:{entry_owner_strategy_id}"
                transition_state(ctx.state_path, ctx.signal_id, "REJECTED",
                                 reason=block_reason, metadata=ctx.signal_attribution)
                return _build_open_guard_rejection_outcome(
                    ctx,
                    block_reason=block_reason,
                    guard_snapshot={
                        "allow_open": False,
                        "reason": block_reason,
                        "entry_owner_strategy_id": entry_owner_strategy_id,
                        "existing_same_strategy_positions": len(same_strategy_positions),
                    },
                )
            same_side_positions = [
                position
                for position in existing_positions
                if isinstance(position, dict)
                and str(position.get("side", "") or "").upper() == signal_side_upper
            ]
            if same_side_positions:
                block_reason = f"SINGLE_ENTRY_SAME_SIDE_BLOCK:{signal_side_upper}"
                transition_state(ctx.state_path, ctx.signal_id, "REJECTED",
                                 reason=block_reason, metadata=ctx.signal_attribution)
                return _build_open_guard_rejection_outcome(
                    ctx,
                    block_reason=block_reason,
                    guard_snapshot={
                        "allow_open": False,
                        "reason": block_reason,
                        "entry_owner_strategy_id": entry_owner_strategy_id,
                        "same_side": signal_side_upper,
                        "existing_same_side_positions": len(same_side_positions),
                    },
                )

        # 鈹€鈹€ Guard 4: 骞充粨鍐峰嵈鏈?+ 鍚岃竟閲嶅叆閿?鈹€鈹€
        cross_correlation = ctx.aggregated_intent.get("correlation", {}) if isinstance(ctx.aggregated_intent.get("correlation", {}), dict) else {}
        cooldown_adj = int(cross_correlation.get("cooldown_adjustment_sec", 0) or 0)
        open_guard = resolve_live_open_guard(
            project_root=ctx.project_root,
            side=signal_side_upper,
            cooldown_adjustment_sec=cooldown_adj,
            regime=str(ctx.agent_context.get("regime", "") or ""),
            strategy_id=str(
                ctx.signal.get("strategy_id", "") or ""
                or (ctx.signal.get("signal", {}) or {}).get("strategy_id", "")
                or (ctx.signal.get("payload", {}) or {}).get("strategy_id", "")
                or ""
            ),
        )
        if not bool(open_guard.get("allow_open", True)):
            block_reason = str(open_guard.get("reason", "") or "RUNTIME_OPEN_GUARD_BLOCK")
            transition_state(ctx.state_path, ctx.signal_id, "REJECTED", reason=block_reason, metadata=ctx.signal_attribution)
            return _build_open_guard_rejection_outcome(
                ctx,
                block_reason=block_reason,
                guard_snapshot=open_guard,
            )
    return None


def _sync_request_side_effects(ctx: PipelineRunContext) -> None:
    action_upper = str(ctx.signal.get("action", "HOLD") or "HOLD").upper()
    try:
        if action_upper == "OPEN":
            sync_open_request(ctx.signal_attribution, data_dir=ctx.project_root / "data", reason="pipeline_dispatch")
            return
        if action_upper in {"CLOSE", "REDUCE"}:
            sync_close_request(
                ctx.signal_attribution,
                data_dir=ctx.project_root / "data",
                ticket=int(ctx.signal.get("ticket", 0) or 0),
                reason="pipeline_dispatch",
            )
    except Exception:
        pass  # position_lifecycle is auxiliary; never crash the pipeline


def _execution_report_fill_price(report: Dict[str, Any], signal: Dict[str, Any]) -> float:
    for key in ("deal_price", "fill_price", "price", "price_open"):
        try:
            value = float(report.get(key, 0.0) or 0.0)
        except Exception:
            value = 0.0
        if value > 0.0:
            return value
    try:
        return float(signal.get("entry_price", signal.get("price", 0.0)) or 0.0)
    except Exception:
        return 0.0


def _apply_execution_side_effects(
    ctx: PipelineRunContext,
    report: Optional[Dict[str, Any]],
) -> Optional[Dict[str, Any]]:
    if report is None:
        return None
    if ctx.state_path is None:
        raise RuntimeError("pipeline state_path is not initialized")
    report.update(ctx.signal_attribution)
    action_upper = str(ctx.signal.get("action", "HOLD") or "HOLD").upper()
    status_upper = str(report.get("status", "") or "").upper()

    def _clear_pending_dispatch_side() -> None:
        side_upper = str(ctx.signal.get("side", "") or "").upper()
        if side_upper not in {"BUY", "SELL"}:
            return
        try:
            _rog_map = _load_runtime_open_guard_state(ctx.project_root)
            if isinstance(_rog_map, dict):
                dispatch_state = _runtime_dispatch_state(_rog_map)
                # Clear both legacy flat key and all per-strategy composite keys matching this side
                dispatch_state["pending_sides"].pop(side_upper, None)
                _strat_id = canonical_strategy_id(ctx.signal.get("strategy_id", ctx.signal.get("_selected_strategy", "")))
                if _strat_id:
                    dispatch_state["pending_sides"].pop(f"{_strat_id}:{side_upper}", None)
                _rog_map["dispatch_state"] = dispatch_state
                _save_runtime_open_guard_state(ctx.project_root, _rog_map)
        except Exception:
            pass

    if action_upper == "OPEN" and status_upper in {
        "REJECTED",
        "TIMEOUT",
        "RECONCILED",
        "RETRY_EXHAUSTED",
        "INIT_FAILED",
    }:
        _clear_pending_dispatch_side()
    if status_upper == "FILLED":
        if action_upper == "OPEN":
            filled_entry_price = _execution_report_fill_price(report, ctx.signal)
            filled_volume = float(
                report.get("deal_volume", report.get("volume", report.get("lots", ctx.signal.get("volume_lots", 0.0))))
                or 0.0
            )
            confirmed_ticket = int(
                report.get("deal_id", 0)
                or report.get("ticket", 0)
                or ctx.signal.get("ticket", 0)
                or 0
            )
            if confirmed_ticket > 0:
                try:
                    sync_open_fill(
                        ctx.signal_attribution,
                        data_dir=ctx.project_root / "data",
                        ticket=confirmed_ticket,
                        side=str(ctx.signal.get("side", "NONE") or "NONE"),
                        volume=filled_volume if filled_volume > 0.0 else float(ctx.signal.get("volume_lots", 0.0) or 0.0),
                        entry_price=filled_entry_price,
                        sl=float(ctx.signal.get("sl_price", 0.0) or 0.0),
                        tp=float(ctx.signal.get("tp_price", 0.0) or 0.0),
                        symbol=str(ctx.signal.get("symbol", ctx.symbol) or ctx.symbol),
                        source="pipeline",
                    )
                except Exception:
                    pass  # position_lifecycle auxiliary; non-blocking
                # 鈹€鈹€ 鍚岀瓥鐣ユ寔浠撹鏁版槧灏? ticket 鈫?strategy_id 鈹€鈹€
                _signal_strategy = str(
                    ctx.signal_attribution.get("attributed_strategy_id", "")
                    or ctx.signal_attribution.get("selected_strategy_id", "")
                    or ctx.signal.get("strategy_id", "")
                    or ""
                ).strip()
                if _signal_strategy and confirmed_ticket > 0:
                    try:
                        _rog_map = _load_runtime_open_guard_state(ctx.project_root)
                        if isinstance(_rog_map, dict):
                            _rog_map.setdefault("_position_strategy_map", {})[str(confirmed_ticket)] = canonical_strategy_id(_signal_strategy)
                            # 鈹€鈹€ 鍚屾鎸佷粨鐘舵€佸埌 runtime_open_guard 鈹€鈹€
                            # 淇: 涔嬪墠鍙洿鏂颁簡 _position_strategy_map锛屼絾 position_open/
                            # positions_all/position_ticket/position_side 鏈悓姝ワ紝瀵艰嚧涓嬩竴鍛ㄦ湡
                            # _refresh_runtime_open_guard_state 鐨勫畨鍏ㄧ綉璇垽涓烘棤鎸佷粨鑰屾竻绌烘槧灏勶紝
                            # 鏈€缁堢粫杩囨墍鏈変粨浣嶄笂闄愭鏌ワ紝鍚屼竴绛栫暐鍙棤闄愬紑浠撱€?
                            _side_upper = str(ctx.signal.get("side", "") or "").upper()
                            _rog_map["position_open"] = True
                            _rog_map["position_ticket"] = confirmed_ticket
                            _rog_map["position_side"] = _side_upper
                            new_position = {
                                "ticket": confirmed_ticket,
                                "side": _side_upper,
                                "volume": filled_volume if filled_volume > 0.0 else float(ctx.signal.get("volume_lots", 0.0) or 0.0),
                                "entry": filled_entry_price,
                                "sl": float(ctx.signal.get("sl_price", 0.0) or 0.0),
                                "tp": float(ctx.signal.get("tp_price", 0.0) or 0.0),
                                "symbol": str(ctx.signal.get("symbol", ctx.symbol) or ctx.symbol),
                                "strategy_id": canonical_strategy_id(_signal_strategy),
                                "opened_at": datetime.now(timezone.utc).isoformat(),
                            }
                            positions_all = _rog_map.get("positions_all", [])
                            if not isinstance(positions_all, list):
                                positions_all = []
                            existing_tickets = {p.get("ticket") for p in positions_all if isinstance(p, dict)}
                            if confirmed_ticket not in existing_tickets:
                                positions_all.append(new_position)
                            _rog_map["positions_all"] = positions_all
                            dispatch_state = _runtime_dispatch_state(_rog_map)
                            dispatch_state["last_open_time"] = time.time()
                            # Per-strategy open time tracking
                            _open_strat_id = canonical_strategy_id(str(ctx.signal_attribution.get("attributed_strategy_id", ctx.signal_attribution.get("selected_strategy_id", ""))) or "")
                            if _open_strat_id:
                                dispatch_state.setdefault("last_open_time_by_strategy", {})[_open_strat_id] = time.time()
                            dispatch_state["pending_sides"].pop(_side_upper, None)
                            # Pop per-strategy pending key too
                            if _open_strat_id:
                                dispatch_state["pending_sides"].pop(f"{_open_strat_id}:{_side_upper}", None)
                            _rog_map["dispatch_state"] = dispatch_state
                            _save_runtime_open_guard_state(ctx.project_root, _rog_map)
                    except Exception:
                        pass  # non-blocking: map sync is best-effort
        elif action_upper in {"CLOSE", "REDUCE"}:
            try:
                fill_exit_price = _execution_report_fill_price(report, ctx.signal)
                sync_close_fill(
                    ctx.signal_attribution,
                    data_dir=ctx.project_root / "data",
                    ticket=int(ctx.signal.get("ticket", report.get("deal_id", report.get("order_id", 0))) or 0),
                    exit_price=fill_exit_price,
                    pnl=float(report.get("pnl", 0.0) or 0.0),
                    side=str(ctx.signal.get("side", "NONE") or "NONE"),
                    reason=str(report.get("reason", "") or ""),
                    source="pipeline",
                )
            except Exception:
                pass  # position_lifecycle auxiliary; non-blocking
            # 鈹€鈹€ 骞充粨鍚庡悓姝ユ竻鐞?runtime_open_guard 鈹€鈹€
            try:
                _rog_map_close = _load_runtime_open_guard_state(ctx.project_root)
                if isinstance(_rog_map_close, dict):
                    _close_ticket = int(ctx.signal.get("ticket", report.get("deal_id", report.get("order_id", 0))) or 0)
                    # 浠?_position_strategy_map 绉婚櫎宸插钩浠?ticket
                    if _close_ticket > 0 and str(_close_ticket) in _rog_map_close.get("_position_strategy_map", {}):
                        _rog_map_close.setdefault("_position_strategy_map", {}).pop(str(_close_ticket), None)
                    # 浠?positions_all 绉婚櫎宸插钩浠?ticket
                    _pos_all = _rog_map_close.get("positions_all", [])
                    if isinstance(_pos_all, list) and _close_ticket > 0:
                        _rog_map_close["positions_all"] = [p for p in _pos_all if isinstance(p, dict) and p.get("ticket") != _close_ticket]
                    # 濡傛灉鎵€鏈夋寔浠撳凡骞充粨锛岄噸缃?position_open
                    _remaining = _rog_map_close.get("positions_all", [])
                    if not _remaining or len(_remaining) == 0:
                        _rog_map_close["position_open"] = False
                        _rog_map_close["position_ticket"] = 0
                        _rog_map_close["position_side"] = ""
                    _save_runtime_open_guard_state(ctx.project_root, _rog_map_close)
            except Exception:
                pass
            _clear_pending_dispatch_side()
    report["execution_ts"] = str(report.get("execution_ts") or report.get("ts") or datetime.now(timezone.utc).isoformat())
    apply_execution_report(ctx.state_path, ctx.signal_id, report)
    _append_instance_execution_event(ctx.project_root, ctx.signal, report)
    _sync_multi_strategy_blackboard(
        ctx.project_root,
        ctx.scheduler_snapshot,
        ctx.strategy_signals,
        ctx.aggregated_intent,
        report,
    )
    return report


def _dispatch_signal(ctx: PipelineRunContext) -> Optional[Dict[str, Any]]:
    if ctx.state_path is None:
        raise RuntimeError("pipeline state_path is not initialized")
    # Save pending entry registry updates after all guard checks pass
    if ctx.pending_registry_update is not None:
        _save_pending_entry_registry(ctx.project_root, ctx.pending_registry_update)
        ctx.pending_registry_update = None
    _apply_live_volume_plan(ctx.signal, project_root=ctx.project_root, aggregated_intent=ctx.aggregated_intent)
    # Learning: apply avoidance rules from lesson_learner
    try:
        from src.learning.lesson_learner import get_active_avoidance_rules
        _ar_action = str(ctx.signal.get("action", "HOLD") or "HOLD").upper()
        if _ar_action == "OPEN":
            _ar_rules = get_active_avoidance_rules()
            if _ar_rules:
                _ar_strat = canonical_strategy_id(ctx.signal.get("strategy_id", ctx.signal.get("_selected_strategy", "")))
                _ar_regime = str(ctx.agent_context.get("regime", "UNKNOWN")).upper()
                _ar_session = str(ctx.agent_context.get("session_bucket", "")).upper()
                for _ar_rule in _ar_rules:
                    try:
                        _ar_conds = json.loads(str(_ar_rule.get("conditions", "[]") or "[]"))
                    except Exception:
                        continue
                    _ar_match = True
                    for _ar_c in _ar_conds:
                        _ar_f = _ar_c.get("feature", ""); _ar_v = str(_ar_c.get("value", "")).upper()
                        if _ar_f == "regime" and _ar_regime != _ar_v: _ar_match = False; break
                        if _ar_f == "session" and _ar_session != _ar_v: _ar_match = False; break
                        if _ar_f == "strategy_id" and _ar_strat != _ar_v: _ar_match = False; break
                    if _ar_match:
                        _ar_action_type = str(_ar_rule.get("action", "") or "")
                        if _ar_action_type == "require_cooldown_or_min_size":
                            ctx.signal["action"] = "HOLD"; ctx.signal["side"] = "NONE"
                            ctx.signal["reject_reason"] = f"AVOIDANCE_RULE:{_ar_rule.get('lesson_type','UNKNOWN')}"
                            transition_state(ctx.state_path, ctx.signal_id, "REJECTED",
                                             reason=f"AVOIDANCE_RULE:{_ar_rule.get('lesson_type','UNKNOWN')}")
                            return _build_execution_report(ctx.signal_id, "REJECTED",
                                                           f"AVOIDANCE_RULE:{_ar_rule.get('lesson_type','UNKNOWN')}")
                        else:
                            ctx.signal["_avoidance_rule"] = _ar_rule.get("lesson_type", "UNKNOWN")
                            ctx.signal["instance_weight"] = float(ctx.signal.get("instance_weight", 0.10) or 0.10) * 0.50
    except Exception:
        pass

    runtime_guard = _load_runtime_open_guard_state(ctx.project_root)
    dispatch_state = _runtime_dispatch_state(runtime_guard)
    # === Runtime-state signal dedup: prevents duplicate OPEN signals ===
    _dedup_action = str(ctx.signal.get("action", "HOLD") or "HOLD").upper()
    if _dedup_action == "OPEN":
        _dedup_side = str(ctx.signal.get("side", "") or "").upper()
        _dedup_entry = round(float(ctx.signal.get("entry_price", 0) or ctx.signal.get("price", 0)), 2)
        _dedup_strategy = canonical_strategy_id(ctx.signal.get("_selected_strategy", ctx.signal.get("strategy_id", "")))
        _dedup_fp = "OPEN_{}_{}_{}".format(_dedup_side, _dedup_entry, _dedup_strategy)
        _dedup_now = time.time()
        _dedup_cache = dispatch_state["dedup"]
        _dedup_last = _dedup_cache.get(_dedup_fp, 0.0)
        if _dedup_now - _dedup_last < _SIGNAL_DEDUP_WINDOW_SEC:
            _mailbox_emit(ctx.project_root, "Signal_Dedup", "WARN",
                "Dedup block: {} ({}s ago)".format(_dedup_fp, int(_dedup_now - _dedup_last)),
                _dedup_fp, _SIGNAL_DEDUP_WINDOW_SEC,
            )
            ctx.signal["action"] = "HOLD"
            ctx.signal["side"] = "NONE"
            ctx.signal["reject_reason"] = "DEDUP_REJECTED:" + _dedup_fp
            transition_state(ctx.state_path, ctx.signal_id, "REJECTED",
                             reason="DEDUP_REJECTED:" + _dedup_fp)
            return _build_execution_report(ctx.signal_id, "REJECTED",
                                           "DEDUP_REJECTED:" + _dedup_fp)
        _dedup_cache[_dedup_fp] = _dedup_now
        dispatch_state["dedup"] = {
            key: value for key, value in _dedup_cache.items() if _dedup_now - value <= 300
        }
        runtime_guard["dispatch_state"] = dispatch_state
        _save_runtime_open_guard_state(ctx.project_root, runtime_guard)
    # === End dedup check ===

    # === Inter-cycle open throttle (per-strategy) ===
    _dedup_now = time.time()
    if _dedup_action == "OPEN":
        _sig_strategy_throttle = canonical_strategy_id(ctx.signal.get("strategy_id", ctx.signal.get("_selected_strategy", "")))
        # Per-strategy throttle: read from last_open_time_by_strategy, fall back to global last_open_time
        _per_strat_open_times = dispatch_state.get("last_open_time_by_strategy", {}) if isinstance(dispatch_state.get("last_open_time_by_strategy", {}), dict) else {}
        _last_open_time = float(_per_strat_open_times.get(_sig_strategy_throttle, 0.0) or 0.0)
        if _last_open_time <= 0:
            # Backward compat: fall back to global last_open_time
            _last_open_time = float(dispatch_state.get("last_open_time", 0.0) or 0.0)
        _elapsed_since_last_open = _dedup_now - _last_open_time
        _min_interval = _get_min_open_interval_sec()
        if _elapsed_since_last_open < _min_interval and _last_open_time > 0:
            _remaining_throttle = int(_min_interval - _elapsed_since_last_open)
            ctx.signal["action"] = "HOLD"
            ctx.signal["side"] = "NONE"
            ctx.signal["reject_reason"] = f"OPEN_THROTTLE:{_remaining_throttle}s_remaining"
            transition_state(ctx.state_path, ctx.signal_id, "REJECTED",
                             reason=f"OPEN_THROTTLE:{_remaining_throttle}s_remaining")
            return _build_execution_report(ctx.signal_id, "REJECTED",
                                           f"OPEN_THROTTLE:{_remaining_throttle}s_remaining")
    # === End throttle check ===

    # === Per-strategy position limit ===
    if _dedup_action == "OPEN":
        _sig_strategy = canonical_strategy_id(ctx.signal.get("strategy_id", ctx.signal.get("_selected_strategy", "")))
        if _sig_strategy:
            try:
                _max_positions_per_strategy = int(config_manager.get_effective_param("max_positions_per_strategy", 1) or 1)
                _pos_map = runtime_guard.get("_position_strategy_map", {}) if isinstance(runtime_guard.get("_position_strategy_map", {}), dict) else {}
                if not runtime_guard.get("positions_all") and _pos_map:
                    runtime_guard["_position_strategy_map"] = {}
                    _pos_map = {}
                    _save_runtime_open_guard_state(ctx.project_root, runtime_guard)
                _strat_open_count = sum(
                    1
                    for sid in _pos_map.values()
                    if canonical_strategy_id(sid) == _sig_strategy
                )
                if _strat_open_count >= _max_positions_per_strategy:
                    ctx.signal["action"] = "HOLD"; ctx.signal["side"] = "NONE"
                    ctx.signal["reject_reason"] = (
                        f"STRATEGY_POSITION_LIMIT:{_sig_strategy}:{_strat_open_count}>={_max_positions_per_strategy}"
                    )
                    transition_state(ctx.state_path, ctx.signal_id, "REJECTED",
                                     reason=f"STRATEGY_POSITION_LIMIT:{_sig_strategy}")
                    return _build_execution_report(ctx.signal_id, "REJECTED",
                                                   f"STRATEGY_POSITION_LIMIT:{_sig_strategy}")
            except Exception: pass

    # === Runtime-state pending dispatch guard (per-strategy) ===
    if _dedup_action == "OPEN":
        _pd_side = str(ctx.signal.get("side", "") or "").upper()
        _pd_strategy = canonical_strategy_id(ctx.signal.get("strategy_id", ctx.signal.get("_selected_strategy", "")))
        if _pd_side in ("BUY", "SELL"):
            # Per-strategy pending guard: key = "strategy_id:side"
            _pd_composite_key = f"{_pd_strategy}:{_pd_side}"
            _all_pending = dispatch_state.get("pending_sides", {}) if isinstance(dispatch_state.get("pending_sides", {}), dict) else {}
            pending_sides = {
                key: value
                for key, value in _all_pending.items()
                if time.time() - float(value or 0.0) <= 120
            }
            dispatch_state["pending_sides"] = pending_sides
            runtime_guard["dispatch_state"] = dispatch_state
            _save_runtime_open_guard_state(ctx.project_root, runtime_guard)
            # Check both per-strategy key and legacy flat key
            _is_pending = _pd_composite_key in pending_sides or _pd_side in pending_sides
            if _is_pending:
                ctx.signal["action"] = "HOLD"; ctx.signal["side"] = "NONE"
                ctx.signal["reject_reason"] = f"PENDING_DISPATCH_GUARD:{_pd_strategy}:{_pd_side}"
                transition_state(ctx.state_path, ctx.signal_id, "REJECTED", reason=f"PENDING_DISPATCH_GUARD:{_pd_strategy}:{_pd_side}")
                return _build_execution_report(ctx.signal_id, "REJECTED", f"PENDING_DISPATCH_GUARD:{_pd_strategy}:{_pd_side}")

    # === Same-direction position limit (per-strategy) ===
    if _dedup_action == "OPEN":
        _sig_side = str(ctx.signal.get("side", "") or "").upper()
        _sig_same_side_strategy = canonical_strategy_id(
            ctx.signal.get(
                "entry_owner_strategy_id",
                ctx.signal.get(
                    "attributed_strategy_id",
                    ctx.signal.get("strategy_id", ctx.signal.get("_selected_strategy", "")),
                ),
            )
        )
        if _sig_side in ("BUY", "SELL"):
            try:
                # Per-strategy same-side cap (default 1 per strategy), with global cap as backstop
                _max_same_side_per_strat = int(config_manager.get_effective_param("max_same_side_per_strategy", 1) or 1)
                _max_same_side_global = int(config_manager.get_effective_param("max_same_side_positions", 4) or 4)
                _sd_all = runtime_guard.get("positions_all", []) if isinstance(runtime_guard.get("positions_all", []), list) else []
                # Per-strategy count: same side AND same strategy_id
                _sd_same_strat = sum(1 for p in _sd_all
                    if isinstance(p, dict)
                    and str(p.get("side","") or "").upper() == _sig_side
                    and str(p.get("strategy_id","") or "").strip() == _sig_same_side_strategy)
                # Global count (backward compat): same side regardless of strategy
                _sd_same_global = sum(1 for p in _sd_all
                    if isinstance(p, dict) and str(p.get("side","") or "").upper() == _sig_side)
                if _sd_same_strat >= _max_same_side_per_strat:
                    ctx.signal["action"] = "HOLD"; ctx.signal["side"] = "NONE"
                    ctx.signal["reject_reason"] = f"SAME_SIDE_PER_STRATEGY_LIMIT:{_sig_same_side_strategy}:{_sig_side}:{_sd_same_strat}>={_max_same_side_per_strat}"
                    transition_state(ctx.state_path, ctx.signal_id, "REJECTED",
                                     reason=f"SAME_SIDE_PER_STRATEGY_LIMIT:{_sig_same_side_strategy}:{_sig_side}")
                    return _build_execution_report(ctx.signal_id, "REJECTED",
                                                   f"SAME_SIDE_PER_STRATEGY_LIMIT:{_sig_same_side_strategy}:{_sig_side}")
                if _sd_same_global >= _max_same_side_global:
                    _debug_multi_strategy_event(
                        "D",
                        "src/pipeline.py:_dispatch_signal:same_side_global",
                        "[DEBUG] dispatch rejected by global same-side limit",
                        {
                            "side": _sig_side,
                            "same_side_global_count": _sd_same_global,
                            "max_same_side_global": _max_same_side_global,
                        },
                    )
                    ctx.signal["action"] = "HOLD"; ctx.signal["side"] = "NONE"
                    ctx.signal["reject_reason"] = f"SAME_SIDE_GLOBAL_LIMIT:{_sig_side}:{_sd_same_global}>={_max_same_side_global}"
                    transition_state(ctx.state_path, ctx.signal_id, "REJECTED",
                                     reason=f"SAME_SIDE_GLOBAL_LIMIT:{_sig_side}")
                    return _build_execution_report(ctx.signal_id, "REJECTED",
                                                   f"SAME_SIDE_GLOBAL_LIMIT:{_sig_side}")
            except Exception: pass

    # === Total exposure cap (panel max_position_size) ===
    if _dedup_action == "OPEN":
        try:
            _panel_max_lot = float(config_manager.get_effective_param("max_position_size", 0.03) or 0.03)
            _current_lot = float(ctx.signal.get("volume_lots", 0) or 0)
            # Sum existing position lots
            _pos_all = runtime_guard.get("positions_all", []) if isinstance(runtime_guard.get("positions_all", []), list) else []
            _existing_total = sum(float(p.get("volume", p.get("volume_lots", 0)) or 0) for p in _pos_all if isinstance(p, dict))
            if _existing_total + _current_lot > _panel_max_lot:
                ctx.signal["action"] = "HOLD"
                ctx.signal["side"] = "NONE"
                ctx.signal["reject_reason"] = f"TOTAL_EXPOSURE_CAP:existing={_existing_total:.2f}+new={_current_lot:.2f}>{_panel_max_lot:.2f}"
                transition_state(ctx.state_path, ctx.signal_id, "REJECTED",
                                 reason=f"TOTAL_EXPOSURE_CAP:{_existing_total:.2f}+{_current_lot:.2f}>{_panel_max_lot:.2f}")
                return _build_execution_report(ctx.signal_id, "REJECTED",
                                               f"TOTAL_EXPOSURE_CAP:{_existing_total:.2f}+{_current_lot:.2f}>{_panel_max_lot:.2f}")
        except Exception:
            pass  # non-blocking
    # === End total exposure cap ===

    ctx.signal_attribution = _attach_signal_attribution(ctx.signal, ctx.aggregated_intent)
    materialized_action = str(ctx.signal.get("materialized_action", "") or "").upper()
    # If guard forced LIMIT/PULLBACK_LIMIT entry style, route through OPEN_LIMIT pending entry
    if materialized_action == "OPEN_MARKET":
        entry_style = str(ctx.signal.get("entry_style", "MARKET") or "MARKET").upper()
        if entry_style in {"LIMIT", "PULLBACK_LIMIT"}:
            ctx.signal["materialized_action"] = "OPEN_LIMIT"
            ctx.signal["materialized_reason"] = "LIMIT_FORCED_BY_GUARD"
            materialized_action = "OPEN_LIMIT"
    if materialized_action in {"OPEN_LIMIT", "ARM_WAIT_CONFIRMATION"}:
        transition_states(
            ctx.state_path,
            ctx.signal_id,
            [{"state": "VALIDATED", "metadata": ctx.signal_attribution}],
        )
        report = _register_pending_entry(ctx)
        return _apply_execution_side_effects(ctx, report)
    _sync_request_side_effects(ctx)
    if _dedup_action == "OPEN":
        _dispatch_side = str(ctx.signal.get("side", "") or "").upper()
        _dispatch_strat = canonical_strategy_id(ctx.signal.get("strategy_id", ctx.signal.get("_selected_strategy", "")))
        # Per-strategy composite key + legacy flat key for backward compat
        dispatch_state["pending_sides"][f"{_dispatch_strat}:{_dispatch_side}"] = time.time()
        dispatch_state["pending_sides"][_dispatch_side] = time.time()
        runtime_guard["dispatch_state"] = dispatch_state
        _save_runtime_open_guard_state(ctx.project_root, runtime_guard)
    from mt5_execution import generate_intent_id

    intent_id = generate_intent_id(ctx.signal)
    ctx.signal["intent_id"] = intent_id
    transition_states(
        ctx.state_path,
        ctx.signal_id,
        [{"state": "VALIDATED", "metadata": ctx.signal_attribution}],
    )
    # 鈹€鈹€ GoldRiskManager gate 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
    # Evaluate risk before dispatch 鈥?blocks circuit-breaker, daily loss, session filter, etc.
    try:
        from bridge.risk_manager import GoldRiskManager
        bid, ask, prices_ok = _market_bid_ask(ctx.mt5_snapshot)
        spread = float(ctx.mt5_snapshot.get("spread", 0) if isinstance(ctx.mt5_snapshot, dict) else 0)
        if prices_ok:
            spread = abs(ask - bid)
        risk_state = load_state() if ctx.state_path else {}
        risk_market_context = _resolve_market_context_cached(
            ctx.agent_context,
            ctx.engine_request,
            mt5_snapshot=ctx.mt5_snapshot,
        )
        risk_ctx = {
            "price_bid": bid,
            "price_ask": ask,
            "spread": spread,
            "state": risk_state,
            "params": ctx.agent_context,
            "session_info": ctx.agent_context.get("_session_info", {}),
            "interpreter": ctx.agent_context.get("interpreter", {}),
            "performance_factors": ctx.agent_context.get("performance_factors", {}),
            "market_context": risk_market_context,
            "selection_regime": risk_market_context.get("regime", "UNKNOWN"),
            "selected_strategy": ctx.signal.get("selected_strategy_id", ctx.signal.get("strategy_id", "")),
        }
        risk_result = GoldRiskManager().evaluate(ctx.signal, risk_ctx)
        risk_checks = risk_result.get("payload", {}).get("risk_checks", [])
        if any(c.get("result") == "BLOCK" for c in risk_checks):
            drop_reason = str(risk_result.get("payload", {}).get("drop_reason", "RISK_MANAGER_BLOCK") or "RISK_MANAGER_BLOCK")
            ctx.signal["action"] = "HOLD"
            ctx.signal["side"] = "NONE"
            ctx.signal["reject_reason"] = drop_reason
            transition_state(ctx.state_path, ctx.signal_id, "REJECTED", reason=drop_reason)
            return _build_execution_report(
                ctx.signal_id, "REJECTED", drop_reason,
                materialized_action=ctx.signal.get("materialized_action", ""),
            )
        # Apply risk-adjusted position sizing
        risk_payload = risk_result.get("payload", {})
        if risk_payload.get("lots", 0) > 0:
            ctx.signal["volume_lots"] = _quantize_open_lots(risk_payload["lots"], enforce_min=True)
        if risk_payload.get("sl_price"):
            ctx.signal["sl_price"] = risk_payload["sl_price"]
        if risk_payload.get("tp_price"):
            ctx.signal["tp_price"] = risk_payload["tp_price"]
    except Exception:
        pass  # non-blocking: risk manager failures don't block dispatch
    # 鈹€鈹€ End GoldRiskManager gate 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€

    # 鈹€鈹€ Position Tier State Machine 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
    try:
        runtime_guard = _load_runtime_open_guard_state(ctx.project_root)
        market = ctx.engine_request.get("market_metrics", {}) if isinstance(ctx.engine_request.get("market_metrics", {}), dict) else {}
        account = ctx.engine_request.get("account_metrics", {}) if isinstance(ctx.engine_request.get("account_metrics", {}), dict) else {}
        route = ctx.scheduler_snapshot.get("route", {}) if isinstance(ctx.scheduler_snapshot.get("route", {}), dict) else {}
        regime = str(
            route.get("regime")
            or ctx.agent_context.get("regime")
            or ctx.signal.get("selection_regime")
            or ctx.signal.get("regime")
            or runtime_guard.get("regime", "UNKNOWN")
            or "UNKNOWN"
        ).upper()
        tier_context = PositionTierContext(
            strategy_id=canonical_strategy_id(ctx.signal.get("strategy_id", ctx.signal.get("_selected_strategy", ""))),
            current_tier=runtime_guard.get("position_tier", "NORMAL"),
            regime=regime,
            runtime_state=runtime_guard,
            consecutive_losses=int(account.get("consecutive_losses", runtime_guard.get("consecutive_losses", 0)) or 0),
            daily_realized_loss_pct=float(account.get("daily_realized_loss_pct", 0.0) or 0.0),
            floating_pnl_pct=float(account.get("floating_pnl_pct", 0.0) or 0.0),
            spread_anomaly=bool(market.get("spread_anomaly", False)),
            circuit_open=bool(runtime_guard.get("kill_switch", False)),
            execution_degraded=bool(runtime_guard.get("execution_degraded", False)),
            macro_blocked=bool(runtime_guard.get("macro_blocked", False)),
        )
        tier_decision = evaluate_position_tier(tier_context)
        ctx.signal["position_tier"] = tier_decision.tier
        ctx.signal["tier_target_lots"] = round(float(tier_decision.target_lots or 0.0), 4)
        ctx.signal["tier_reason_code"] = tier_decision.reason_code
        ctx.signal["tier_reason_text"] = tier_decision.reason_text
        ctx.signal["tier_recovery_needed"] = list(tier_decision.recovery_needed)
        if tier_decision.tier == "BLOCKED":
            ctx.signal["action"] = "HOLD"
            ctx.signal["side"] = "NONE"
            ctx.signal["reject_reason"] = tier_decision.reason_code
            transition_state(ctx.state_path, ctx.signal_id, "REJECTED", reason=tier_decision.reason_code)
            runtime_guard = apply_position_tier_state(runtime_guard, tier_decision)
            _save_runtime_open_guard_state(ctx.project_root, runtime_guard)
            return _build_execution_report(
                ctx.signal_id,
                "REJECTED",
                tier_decision.reason_code,
                materialized_action=ctx.signal.get("materialized_action", ""),
            )
        current_lots = float(ctx.signal.get("volume_lots", 0.0) or 0.0)
        tier_target_lots = float(tier_decision.target_lots or 0.0)
        if current_lots > 0 and tier_target_lots > 0:
            ctx.signal["volume_lots"] = _quantize_open_lots(min(current_lots, tier_target_lots), enforce_min=True)
        runtime_guard = apply_position_tier_state(runtime_guard, tier_decision)
        _save_runtime_open_guard_state(ctx.project_root, runtime_guard)
    except Exception:
        pass
    # 鈹€鈹€ End Position Tier State Machine 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€

    # 鈹€鈹€ Extreme market circuit breaker 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
    try:
        from src.market_analyzer import detect_extreme_market
        from bridge.strategy import _read_bars_from_ea
        bars = _read_bars_from_ea(count=300)
        if bars and len(bars) >= 20:
            closes = [b.get("close", 0) for b in bars]
            cb = detect_extreme_market(bars, closes)
            if cb.get("circuit_break") and cb.get("level") == "EXTREME":
                ctx.signal["action"] = "HOLD"
                ctx.signal["side"] = "NONE"
                ctx.signal["reject_reason"] = f"CIRCUIT_BREAKER:{cb.get('level')}:{cb.get('reason','')}"
                return _build_execution_report(
                    ctx.signal_id, "REJECTED", ctx.signal["reject_reason"],
                    materialized_action=ctx.signal.get("materialized_action", ""),
                )
    except Exception:
        pass
    # 鈹€鈹€ End circuit breaker 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€

    report = dispatch_execution(
        ctx.signal,
        ctx.execution_mode,
        ctx.symbol,
        ctx.execution_report_path,
        intent_id=intent_id,
    )
    if report is not None:
        transition_states(
            ctx.state_path,
            ctx.signal_id,
            [{"state": "SENT", "metadata": ctx.signal_attribution}],
        )
    return _apply_execution_side_effects(ctx, report)


def _build_completion_outcome(
    ctx: PipelineRunContext,
    report: Optional[Dict[str, Any]],
) -> PipelineOutcome:
    static_root = ctx.code_root or ctx.project_root
    runbook_path = static_root / "runbooks/incident_playbooks.json"
    metrics = _build_runtime_metrics(ctx.engine_request, ctx.signal)
    incidents = evaluate_incidents(runbook_path, metrics)
    auto_actions = []
    if ctx.enable_incident_autofix:
        for incident in incidents:
            auto_actions.extend(resolve_auto_actions(runbook_path, incident["detector_id"]))
    ledger_events = ["signal_envelope"]
    return PipelineOutcome(
        execution_report=report or {},
        incidents=incidents,
        auto_actions=auto_actions,
        ledger_events=ledger_events,
        append_runtime_log=True,
        sync_mailbox=True,
    )


# 鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺?
# run_once 鈥?鍞竴浜ゆ槗涓诲叆鍙?(Sole Trading Entry Point)
#
# 鏆傛€佹灦鏋?(2026-05-14):
#   pipeline.py 鎷ユ湁鎵€鏈変氦鏄撹鍐虫潈锛氫俊鍙风敓鎴愪笌鑱氬悎銆佸畯瑙傛柟鍚戠粦瀹氫笌椋庢帶
#   veto銆佹墽琛岀姸鎬佹満鎺ㄨ繘銆佹槸鍚﹀厑璁稿紑/骞充粨銆佺粺涓€鏃ュ織/ledger/runtime snapshot
#   杈撳嚭銆俫old_trader.py 宸插墛鍑忎负 --monitor-only 妗ユ帴灞傘€?
#
# 鍘熷垯: 鍗曞叆鍙ｃ€佸崟鍐崇瓥鑰呫€佸崟鐘舵€佹満銆佸崟浜嬪疄婧愩€佸崟鍥炴姤褰掗泦鍣ㄣ€佸崟鍐欒€?
# 鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺?
def _apply_symbol_calibration(ctx):
    """Auto-calibrate symbol parameters: startup category defaults + live bar recalibration."""
    symbol = (ctx.symbol or "").strip().upper()
    if not symbol:
        return
    # Startup: calibrate from category defaults (once per session)
    if not getattr(_apply_symbol_calibration, "_once", False):
        try:
            calibrate_symbol_params(symbol, save=True)
            _apply_symbol_calibration._once = True
        except Exception:
            pass
    # Runtime: recalibrate from live bars if enough data
    try:
        snap = ctx.mt5_data_payload or {}
        bars = []
        for key in (symbol, "gold", "XAUUSD"):
            nested = snap.get(key, snap.get("instruments", {}).get(key, {})) if isinstance(snap, dict) else {}
            if isinstance(nested, dict):
                bars = nested.get("bars", [])
                if bars:
                    break
        if bars and len(bars) >= 120:
            recalibrate_from_bars(symbol, bars)
    except Exception:
        pass


def run_once(
    project_root: Path,
    code_root: Optional[Path],
    tenant_id: str,
    source: str,
    runtime_intent: str,
    input_path: Path,
    output_path: Path,
    execution_mode: str,
    symbol: str,
    execution_report_path: Optional[Path],
    enable_incident_autofix: bool,
    live_client: Optional[Any],
) -> Dict[str, Any]:
    _check_license_periodic()
    _validate_runtime_intent(source, execution_mode, runtime_intent)
    runtime_state, applied_auto_actions, auto_action_alerts, unsupported_auto_actions = _consume_pending_auto_actions(project_root, tenant_id)
    ctx = PipelineRunContext(
        project_root=project_root,
        code_root=code_root,
        tenant_id=tenant_id,
        source=source,
        runtime_intent=runtime_intent,
        output_path=output_path,
        execution_mode=execution_mode,
        symbol=symbol,
        execution_report_path=execution_report_path,
        enable_incident_autofix=enable_incident_autofix,
        runtime_state=runtime_state,
    )
    if applied_auto_actions:
        ctx.promotion_info["applied_auto_actions"] = applied_auto_actions
    if unsupported_auto_actions:
        ctx.promotion_info["unsupported_auto_actions"] = unsupported_auto_actions
    for index, alert_text in enumerate(auto_action_alerts):
        _mailbox_emit(
            project_root,
            "Risk_Commissar",
            "WARN",
            alert_text,
            f"auto_action_alert:{index}:{alert_text}",
            30.0,
        )
    outcome = _evaluate_runtime_prechecks(ctx, runtime_state=runtime_state)
    if outcome is not None:
        return _emit_pipeline_outcome(ctx, outcome)

    _load_run_context(ctx, input_path=input_path, live_client=live_client)
    _apply_symbol_calibration(ctx)

    outcome = _evaluate_market_data_delay_gate(ctx)
    if outcome is not None:
        return _emit_pipeline_outcome(ctx, outcome)

    _apply_pending_entry_promotions(ctx)

    outcome = _evaluate_retry_outcome(ctx)
    if outcome is not None:
        return _emit_pipeline_outcome(ctx, outcome)

    # 鈹€鈹€ Multi-dispatch: each independently actionable signal gets dispatched 鈹€鈹€
    reports: list[Dict[str, Any]] = []
    for idx, per_signal in enumerate(ctx.strategy_signals):
        if not isinstance(per_signal, dict):
            continue
        action = str(per_signal.get("action", "HOLD") or "HOLD").upper()
        if action != "OPEN":
            continue
        side = str(per_signal.get("side", "NONE") or "NONE").upper()
        if side not in {"BUY", "SELL"}:
            continue
        # Swap ctx state for this signal's dispatch
        ctx.signal = per_signal
        ctx.signal_id = str(per_signal.get("signal_id", f"sig_multi_{idx}_{int(time.time())}"))
        ctx.signal_attribution = _attach_signal_attribution(per_signal, ctx.aggregated_intent)
        llm_outcome = _apply_llm_verification(ctx)
        if llm_outcome is not None:
            if llm_outcome.execution_report:
                reports.append(llm_outcome.execution_report)
            continue
        action = str(ctx.signal.get("action", "HOLD") or "HOLD").upper()
        side = str(ctx.signal.get("side", "NONE") or "NONE").upper()
        if action != "OPEN" or side not in {"BUY", "SELL"}:
            continue
        guard_outcome = _evaluate_guard_outcome(ctx)
        if guard_outcome is not None:
            if guard_outcome.execution_report:
                reports.append(guard_outcome.execution_report)
            continue
        action = str(ctx.signal.get("action", "HOLD") or "HOLD").upper()
        side = str(ctx.signal.get("side", "NONE") or "NONE").upper()
        if action != "OPEN" or side not in {"BUY", "SELL"}:
            continue
        # Check per-strategy position guard (already enforced inside _dispatch_signal)
        report = _dispatch_signal(ctx)
        if report is not None:
            reports.append(report)
    # Default report: ensure report is always bound for _build_completion_outcome
    report: Optional[Dict[str, Any]] = {"status": "HOLD", "reason": "MULTI_DISPATCH_NO_OPEN_SIGNALS"}
    # Restore first signal as ctx.signal for downstream compatibility
    if ctx.strategy_signals and isinstance(ctx.strategy_signals[0], dict):
        ctx.signal = ctx.strategy_signals[0]
        ctx.signal_id = str(ctx.strategy_signals[0].get("signal_id", ""))
        ctx.signal_attribution = _attach_signal_attribution(ctx.strategy_signals[0], ctx.aggregated_intent)
    # gold_trader.py 宸查€€褰癸紝agent_state.json 鐢?pipeline 鑷缁存姢銆?
    _sync_agent_state_from_mt5(ctx)
    # Build combined outcome from all reports
    if reports:
        report = reports[0]  # primary report for backward compat
        report["_multi_reports"] = reports if len(reports) > 1 else None
    outcome = _build_completion_outcome(ctx, report)
    return _emit_pipeline_outcome(ctx, outcome)


def _cleanup_stale_logs(project_root: Path) -> dict[str, int]:
    """Truncate accumulating log/ledger files to prevent unbounded disk growth.

    Returns counts of truncated/pruned files for observability.
    """
    logs_dir = project_root / "logs"
    if not logs_dir.exists():
        return {}

    stats: dict[str, int] = {}
    now = time.time()
    DAY_SEC = 86400

    # 鈹€鈹€ ndjson ledgers: keep last N lines 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
    ndjson_limits: dict[str, int] = {
        "decision_ledger.ndjson": 10_000,
        "trading_ledger.ndjson": 10_000,
        "restart_fingerprint.ndjson": 2_000,
        "signal_ledger.ndjson": 5_000,
        "risk_events.ndjson": 5_000,
        "decision_signal_links.ndjson": 5_000,
        "shadow_learner_decisions.ndjson": 5_000,
        "live_learner_selections.ndjson": 5_000,
        "autonomy_loop_history.ndjson": 2_000,
    }
    for filename, max_lines in ndjson_limits.items():
        path = logs_dir / filename
        if not path.exists():
            continue
        try:
            lines = path.read_text(encoding="utf-8").splitlines()
            if len(lines) > max_lines:
                path.write_text("\n".join(lines[-max_lines:]) + "\n", encoding="utf-8")
                stats[filename] = len(lines) - max_lines
        except Exception:
            pass

    # 鈹€鈹€ runtime.log: rotate when > 8K lines, keep last 3 rotations 鈹€鈹€
    runtime_log = logs_dir / "runtime.log"
    if runtime_log.exists():
        try:
            lines = runtime_log.read_text(encoding="utf-8").splitlines()
            if len(lines) > 8000:
                ts_tag = time.strftime("%Y%m%d_%H%M%S", time.gmtime())
                archive_path = logs_dir / f"runtime.{ts_tag}.log"
                runtime_log.rename(archive_path)
                stats["runtime.log"] = len(lines)
                # prune old archives beyond 3
                runtime_archives = sorted(logs_dir.glob("runtime.*.log"), key=lambda p: p.stat().st_mtime)
                for old in runtime_archives[:-3]:
                    old.unlink(missing_ok=True)
        except Exception:
            pass

    # 鈹€鈹€ swarm/*.jsonl: keep last 5K lines each 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
    swarm_dir = logs_dir / "swarm"
    if swarm_dir.exists():
        for jsonl_file in swarm_dir.glob("*.jsonl"):
            try:
                lines = jsonl_file.read_text(encoding="utf-8").splitlines()
                if len(lines) > 5_000:
                    jsonl_file.write_text("\n".join(lines[-5_000:]) + "\n", encoding="utf-8")
                    stats[f"swarm/{jsonl_file.name}"] = len(lines) - 5_000
            except Exception:
                pass

    # 鈹€鈹€ gold_trader.*.log: remove files older than 7 days 鈹€鈹€鈹€鈹€鈹€鈹€
    for old_log in logs_dir.glob("gold_trader.*.log"):
        try:
            if now - old_log.stat().st_mtime > 7 * DAY_SEC:
                old_log.unlink(missing_ok=True)
                stats[f"pruned:{old_log.name}"] = 1
        except Exception:
            pass

    # 鈹€鈹€ decision_ledger_archive_*.ndjson: keep last 3 days 鈹€鈹€鈹€鈹€鈹€
    for old in sorted(logs_dir.glob("decision_ledger_archive_*.ndjson"),
                       key=lambda p: p.stat().st_mtime)[:-3]:
        try:
            old.unlink(missing_ok=True)
            stats[f"pruned:{old.name}"] = 1
        except Exception:
            pass

    # 鈹€鈹€ Legacy migration backups: keep max 3, remove >30 days old 鈹€鈹€
    config_dir = project_root / "config"
    backup_base = config_dir / "backup"
    if backup_base.exists():
        legacy_backups = sorted(
            [d for d in backup_base.iterdir() if d.is_dir() and d.name.startswith("legacy_migration_")],
            key=lambda p: p.stat().st_mtime,
            reverse=True,
        )
        # Keep only the 3 most recent
        for old in legacy_backups[3:]:
            try:
                import shutil as _shutil
                _shutil.rmtree(old, ignore_errors=True)
                stats[f"pruned:backup/{old.name}"] = 1
            except Exception:
                pass
        # Remove >30 days old even if within the 3-recent window
        for backup_dir in legacy_backups[:3]:
            try:
                if now - backup_dir.stat().st_mtime > 30 * DAY_SEC:
                    import shutil as _shutil
                    _shutil.rmtree(backup_dir, ignore_errors=True)
                    stats[f"pruned:old_backup/{backup_dir.name}"] = 1
            except Exception:
                pass

    return stats


def _reconcile_stale_signals(project_root: Path) -> None:
    """Reconcile stale signals (SENT/TIMEOUT 鈫?RECONCILED) at startup."""
    try:
        state_path = project_root / "data/execution_state.json"
        if state_path.exists():
            result = reconcile_open_signals(state_path)
            reconciled = result.get("reconciled_signals", [])
            if reconciled:
                import logging as _reconcile_log
                _reconcile_log.warning("reconciled %d stale signals on startup: %s", len(reconciled), reconciled[:20])
    except Exception:
        pass


_CLEANUP_TICK = 0
_CLEANUP_INTERVAL = 300  # every ~300 pipeline cycles (~10 min at 2s poll)
_MACRO_REFRESH_TICK = 0
_MACRO_REFRESH_INTERVAL = 1800  # try macro direction refresh every ~1800 cycles (~1 hour at 2s poll)
_MACRO_EVENT_SCAN_TICK = 0
_MACRO_EVENT_SCAN_INTERVAL = 30  # scan market-shift triggers every ~30 cycles (~1 min at 2s poll)
_REFLECTION_TICK = 0
_REFLECTION_INTERVAL = 900  # run reflection every ~900 cycles (~30 min at 2s poll)
_MACRO_EVENT_TRIGGER_COOLDOWN_SECONDS = 10.0 * 60.0
_MACRO_EVENT_SCORE_JUMP_DELTA = 1.5
_MACRO_EVENT_LAST_SIGNAL: Dict[str, Any] = {}
_MACRO_EVENT_LAST_TRIGGER_TS = 0.0

# 鈹€鈹€ Range loosening: suppressed strategy intent tracking 鈹€鈹€
_prev_suppressed_strategy_intents: list[dict[str, Any]] = []
_M1_LOOSENING_TICK = 0
_M1_LOOSENING_INTERVAL = 30  # M1 data refresh every ~30 cycles (~1 min at 2s poll)

# 鈹€鈹€ Exit monitoring state (single-cycle, driven by main polling loop) 鈹€鈹€
_EXIT_MONITOR_CYCLE = 0
_EXIT_MT5_ABSENT_COUNT = 0


# 鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺?
# Range loosening: suppressed strategy intent collection
# 鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺?

def _collect_suppressed_strategy_intents(signals: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Scan signals for regime-suppressed entries and extract strategy intent.

    When a signal generator was suppressed by system_regime_override or
    system_route_blocked, we record which strategy family intended which
    direction.  This feeds the range-loosening state machine.
    """
    intents: list[dict[str, Any]] = []
    for sig in (signals if isinstance(signals, list) else []):
        if not isinstance(sig, dict):
            continue
        reason = str(sig.get("reason", "") or sig.get("reject_reason", "") or "")
        indicators = sig.get("indicators", {}) if isinstance(sig.get("indicators", {}), dict) else {}
        if indicators.get("suppressed") or "system_regime_override" in reason or "system_route_blocked" in reason:
            strategy_id = str(sig.get("strategy_id", sig.get("_sig_strategy_id", "")) or "")
            # Map strategy_id to family using same classification as exit_optimizer
            sid = strategy_id.lower().strip()
            family = ""
            if sid in {"ema_macd", "elliott_wave", "wyckoff", "adx_breakout", "pullback_fade"}:
                family = "trend"
            elif sid in {"rsi_extreme", "boll_band", "rsi_divergence", "key_level"}:
                family = "mean_reversion"
            elif sid in {"grid_osc"}:
                family = "grid"
            elif sid in {"breakout_strategy"}:
                family = "breakout"
            else:
                family = sid or "unknown"
            intended_side = str(indicators.get("intended_side", "") or "").strip().upper()
            intended_conf = float(indicators.get("intended_confidence", 0.0) or 0.0)
            intents.append({
                "strategy_family": family,
                "strategy_id": strategy_id,
                "intended_side": intended_side if intended_side in ("BUY", "SELL") else "",
                "confidence": intended_conf,
            })
    global _prev_suppressed_strategy_intents
    _prev_suppressed_strategy_intents = intents
    return intents


# 鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺?
# Macro direction refresh
# 鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺?

def _try_run_reflection_cycle(project_root: Path) -> None:
    """Run continuous reflection to generate trading lessons."""
    try:
        from src.learning.lesson_reflector import run_reflection_cycle
        run_reflection_cycle()
    except Exception:
        pass


def _try_macro_direction_refresh(project_root: Path) -> None:
    """Run the persisted AI macro analysis refresh path."""
    try:
        from src.deepseek_agent import run_macro_analysis

        run_macro_analysis(force_refresh=True, trigger_reason="hourly")
    except Exception:
        pass


def _macro_event_vol_bucket(vol_ratio: float) -> str:
    from src.learning.meta_decision import get_threshold as _gd
    _vh = _gd("vol_high_cutoff")
    _ve = _gd("vol_elevated_cutoff")
    _vl = _gd("vol_low_cutoff")
    if vol_ratio > _vh:
        return "HIGH"
    if vol_ratio > _ve:
        return "ELEVATED"
    if vol_ratio < _vl:
        return "LOW"
    return "NORMAL"


def _build_macro_event_signal(symbol: str = "") -> Dict[str, Any]:
    try:
        from bridge.garch_volatility import get_vol_multiplier
        from bridge.session_analyzer import analyze_all_sessions

        session_data = analyze_all_sessions(symbol=symbol)
        if not isinstance(session_data, dict) or not session_data.get("all_sessions"):
            return {}
        vol_payload = get_vol_multiplier(refresh=False)
        vol_ratio = float(vol_payload.get("vol_ratio", 1.0) or 1.0)
        return {
            "regime": str(session_data.get("regime", "UNKNOWN") or "UNKNOWN").strip().upper(),
            "direction": str(
                session_data.get("regime_aware_direction", session_data.get("overall_direction", "NEUTRAL"))
                or "NEUTRAL"
            ).strip().upper(),
            "overall_score": round(float(session_data.get("overall_score", 0.0) or 0.0), 3),
            "vol_ratio": round(vol_ratio, 3),
            "vol_bucket": _macro_event_vol_bucket(vol_ratio),
        }
    except Exception:
        return {}


def _detect_macro_event_trigger(previous: Dict[str, Any], current: Dict[str, Any]) -> str:
    from src.learning.meta_decision import get_threshold as _gd_mt
    if not previous or not current:
        return ""
    previous_regime = str(previous.get("regime", "") or "").upper()
    current_regime = str(current.get("regime", "") or "").upper()
    if (
        previous_regime
        and current_regime
        and previous_regime != current_regime
        and current_regime != "UNKNOWN"
    ):
        return "regime_shift"
    previous_bucket = str(previous.get("vol_bucket", "") or "").upper()
    current_bucket = str(current.get("vol_bucket", "") or "").upper()
    bucket_rank = {"LOW": 0, "NORMAL": 1, "ELEVATED": 2, "HIGH": 3}
    if bucket_rank.get(current_bucket, -1) > bucket_rank.get(previous_bucket, -1) and current_bucket in {"ELEVATED", "HIGH"}:
        return "volatility_surge"
    previous_score = float(previous.get("overall_score", 0.0) or 0.0)
    current_score = float(current.get("overall_score", 0.0) or 0.0)
    if abs(current_score - previous_score) >= _gd_mt("score_jump_delta"):
        return "score_jump"
    previous_direction = str(previous.get("direction", "") or "").upper()
    current_direction = str(current.get("direction", "") or "").upper()
    if (
        previous_direction
        and current_direction
        and previous_direction != current_direction
        and current_direction in {"BULLISH", "BEARISH", "SLIGHTLY_BULLISH", "SLIGHTLY_BEARISH"}
    ):
        return "direction_flip"
    return ""


def _macro_snapshot_refresh_reason() -> str:
    try:
        from runtime_state.decision_snapshot import load_decision_snapshot

        snapshot = load_decision_snapshot()
    except Exception:
        snapshot = {}
    macro_snapshot = (
        snapshot.get("macro", {})
        if isinstance(snapshot, dict) and isinstance(snapshot.get("macro", {}), dict)
        else snapshot
        if isinstance(snapshot, dict)
        else {}
    )
    freshness = _macro_freshness_state(macro_snapshot if isinstance(macro_snapshot, dict) else {})
    if bool(freshness.get("ttl_expired", False)):
        return "macro_snapshot_expired"
    if bool(freshness.get("ttl_stale", False)):
        return "macro_snapshot_stale"
    return ""


def _maybe_trigger_macro_event_refresh(project_root: Path, *, symbol: str = "") -> None:
    from src.learning.meta_decision import get_threshold as _get_mt, record_outcome as _roc_mt
    global _MACRO_EVENT_LAST_SIGNAL, _MACRO_EVENT_LAST_TRIGGER_TS
    current_signal = _build_macro_event_signal(symbol=symbol)
    if not current_signal:
        return
    trigger_reason = _detect_macro_event_trigger(_MACRO_EVENT_LAST_SIGNAL, current_signal)
    if not trigger_reason:
        trigger_reason = _macro_snapshot_refresh_reason()
    previous_signal = dict(_MACRO_EVENT_LAST_SIGNAL) if isinstance(_MACRO_EVENT_LAST_SIGNAL, dict) else {}
    _MACRO_EVENT_LAST_SIGNAL = current_signal
    _mu_cd = _get_mt("macro_event_cooldown_sec")
    cooldown_remaining = max(0.0, _mu_cd - (time.time() - _MACRO_EVENT_LAST_TRIGGER_TS))
    if trigger_reason or cooldown_remaining > 0:
        if not trigger_reason:
            return
    now_ts = time.time()
    if now_ts - _MACRO_EVENT_LAST_TRIGGER_TS < _get_mt("macro_event_cooldown_sec"):
        return
    _MACRO_EVENT_LAST_TRIGGER_TS = now_ts
    try:
        from src.deepseek_agent import run_macro_analysis
        from src.learning.meta_decision import record_outcome as _ro_mt

        run_macro_analysis(force_refresh=True, trigger_reason=trigger_reason)
        try:
            _ro_mt("macro_event_cooldown_sec", was_applied=True, was_correct=True)
        except Exception:
            logger.warning("Failed to record macro cooldown outcome (correct)", exc_info=True)
    except Exception:
        try:
            _roc_mt("macro_event_cooldown_sec", was_applied=True, was_correct=False)
        except Exception:
            logger.warning("Failed to record macro cooldown outcome (incorrect)", exc_info=True)


# 鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺?
# Inline exit helpers 鈥?small, pure, no external deps needed
# 鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺?

def _check_sltp_conditions(bid: float, ask: float, state: dict) -> tuple:
    """Check if price hit SL/TP. Returns (triggered, reason, action)."""
    if not state.get("position_open"):
        return False, None, None
    side = state.get("position_side", "BUY")
    sl = state.get("position_sl", 0)
    tp = state.get("position_tp", 0)
    spread = round((ask - bid) * 100, 1)
    if spread > 150 or sl <= 0:
        return False, None, None
    if side == "BUY":
        if bid <= sl:
            return True, f"SL bid={bid}<={sl}", "CLOSE"
        if tp > 0 and ask >= tp:
            return True, f"TP ask={ask}>={tp}", "CLOSE"
    else:
        if ask >= sl:
            return True, f"SL ask={ask}>={sl}", "CLOSE"
        if tp > 0 and bid <= tp:
            return True, f"TP bid={bid}<={tp}", "CLOSE"
    return False, None, None


def _seal_close_guard(state: dict, *, closed_side: str = "", closed_ticket: int = 0,
                      close_action: str = "CLOSE", close_ts: float = None) -> None:
    """Mark post-close guard fields to prevent same-side re-entry."""
    if not isinstance(state, dict):
        return
    ts = float(close_ts or time.time())
    state["last_close_time"] = ts
    state["last_closed_ts"] = ts
    if closed_side:
        state["last_closed_side"] = closed_side.upper()
    if closed_ticket:
        state["last_closed_ticket"] = int(closed_ticket)
    state["last_close_action"] = str(close_action or "CLOSE").upper()
    # Multi-strategy tracking: propagate per-strategy close metadata
    # so cooldown_remaining_sec can distinguish strategies
    pos_strategy_id = str(state.get("position_strategy_id", "") or
                          state.get("position_attribution", {}).get("strategy_id", "") or "")
    if pos_strategy_id and closed_side:
        strat_times = state.setdefault("_strategy_close_times", {})
        if isinstance(strat_times, dict):
            strat_times[pos_strategy_id] = ts
        strat_sides = state.setdefault("_strategy_close_sides", {})
        if isinstance(strat_sides, dict):
            strat_sides[pos_strategy_id] = str(closed_side).upper()
        state["last_closed_strategy"] = pos_strategy_id

    # 鈹€鈹€ Sync close tracking to runtime_open_guard.json 鈹€鈹€
    # resolve_live_open_guard reads runtime_open_guard.json FIRST before falling
    # back to agent_state.json. Without this sync, cooldown checks never see close times.
    try:
        from config_manager import get_data_dir as _gd
        _rog_path = _gd() / "runtime_open_guard.json"
        if _rog_path.exists():
            _rog = json.loads(_rog_path.read_text(encoding="utf-8"))
        else:
            _rog = {}
        if isinstance(_rog, dict):
            _rog["last_close_time"] = ts
            _rog["last_closed_ts"] = ts
            if closed_side:
                _rog["last_closed_side"] = closed_side.upper()
            if pos_strategy_id:
                _rog.setdefault("_strategy_close_times", {})[pos_strategy_id] = ts
                _rog["last_closed_strategy"] = pos_strategy_id
            _rog_path.parent.mkdir(parents=True, exist_ok=True)
            _rog_path.write_text(json.dumps(_rog, ensure_ascii=False), encoding="utf-8")
    except Exception:
        pass


def _reset_position_state(state: dict) -> None:
    """Zero all position fields after close (mutates in-place)."""
    for k in ("position_open", "position_ticket", "position_price", "position_sl",
              "position_tp", "position_vol", "position_entry", "position_time"):
        state[k] = 0 if k != "position_open" else False
    state["position_open"] = False
    state["position_side"] = None
    state["position_signal_id"] = ""
    state["position_decision_id"] = ""
    state["position_regime"] = "UNKNOWN"
    state["position_attribution"] = {}
    state["l1_breakeven_done"] = False
    state["partial_done"] = False
    state["trend_run_mode"] = False
    state["trailing_telemetry"] = {}
    state["_mt5_absent_count"] = 0


# MT5 read error counter: after N consecutive failures while position is tracked
# as open, force-close to prevent permanent ghost orders when MT5 files are
# consistently unreadable (EA crash, file lock contention, etc.).
_MT5_SYNC_ERROR_MAX = 5  # ~10 seconds at 2s poll

def _sync_mt5_to_state(state: dict) -> dict:
    """Pull MT5 real position fields into state dict (mutates in-place).

    When MT5 reads fail consistently while state believes a position is open,
    force-close after _MT5_SYNC_ERROR_MAX consecutive failures rather than
    silently preserving stale position state indefinitely (ghost order).
    """
    from bridge.gold_trader import get_mt5_position_state
    mt5 = get_mt5_position_state()
    if mt5.get("error"):
        was_open = bool(state.get("position_open", False))
        if was_open:
            err_count = int(state.get("_mt5_sync_err_count", 0) or 0) + 1
            state["_mt5_sync_err_count"] = err_count
            if err_count >= _MT5_SYNC_ERROR_MAX:
                state["position_open"] = False
                state["_mt5_sync_err_count"] = 0
                state["_mt5_sync_forced_close"] = True
                state["_mt5_sync_forced_close_reason"] = str(mt5.get("error", "unknown"))
        return state
    # MT5 read succeeded 鈥?reset error counter
    state["_mt5_sync_err_count"] = 0
    state.pop("_mt5_sync_forced_close", None)
    state.pop("_mt5_sync_forced_close_reason", None)
    for k in ("balance", "equity"):
        if k in mt5 and mt5[k] is not None:
            state[k] = float(mt5[k])
    if bool(mt5.get("position_open")) and int(mt5.get("position_ticket", 0) or 0) > 0:
        for k in ("position_open", "position_ticket", "position_side",
                  "position_price", "position_sl", "position_tp",
                  "position_vol", "position_entry", "positions_all"):
            if k in mt5:
                state[k] = mt5[k]
    else:
        state["position_open"] = False
        if "positions_all" in mt5:
            state["positions_all"] = mt5["positions_all"]
    return state


def _persist_trail_telemetry(telemetry: dict) -> None:
    """Write trailing telemetry snapshot to logs dir."""
    import json as _json
    from bridge.bridge_constants import LOGS_DIR
    dest = LOGS_DIR / "trailing_telemetry_latest.json"
    tmp = dest.with_suffix(".tmp")
    try:
        tmp.write_text(_json.dumps(telemetry, ensure_ascii=False, default=str), encoding="utf-8")
        tmp.replace(dest)
    except Exception:
        pass


def _compute_close_pnl(state_before_close: dict, exit_price: float) -> float:
    """Get REAL close PnL from MT5 by position ticket lookup.
    
    Priority:
    1. MT5 deal history (real profit) - authoritative
    2. Fallback: estimate from price diff
    
    The fallback is inaccurate; run sync_mt5_history.py regularly to ensure brain DB 
    has real PnL from MT5 deal history.
    """
    side = str(state_before_close.get("position_side", "BUY") or "BUY").upper()
    entry = float(state_before_close.get("position_entry", 0.0) or 0.0)
    vol = float(state_before_close.get("position_vol", 0.01) or 0.01)
    # Try MT5 deal lookup first
    try:
        import MetaTrader5 as mt5
        if mt5.initialize():
            from datetime import datetime, timedelta
            now = datetime.now()
            deals = mt5.history_deals_get(now - timedelta(hours=48), now)
            if deals:
                ticket = int(state_before_close.get("position_ticket", 0) or 0)
                pos_id = int(state_before_close.get("position_position_id", 0) or 0)
                if pos_id > 0:
                    for d in deals:
                        if d.position_id == pos_id and d.profit != 0:
                            mt5.shutdown()
                            return round(d.profit, 2)
                elif ticket > 0:
                    for d in deals:
                        if d.ticket == ticket and d.profit != 0:
                            mt5.shutdown()
                            return round(d.profit, 2)
            mt5.shutdown()
    except Exception:
        pass
    # Fallback: rough estimate
    if vol > 0.10:
        vol = 0.01
    direction = 1.0 if side == "BUY" else -1.0 if side == "SELL" else 0.0
    pnl = (exit_price - entry) * direction * vol * 100
    return round(pnl, 2)




def _build_exit_indicators(state: dict, bid: float, ask: float, project_root: Path) -> Dict[str, Any]:
    """Build technical indicators for exit_optimizer.evaluate_exit()."""
    indicators: Dict[str, Any] = {
        "atr": 3.0,
        "adx": 25.0,
        "adx_label": "UNCERTAIN",
        "rsi": 50.0,
        "macd": 0.0,
        "macd_signal": 0.0,
        "macd_hist": 0.0,
        "macd_hist_prev": 0.0,
        "macd_norm": 0.0,
        "macd_divergence": "NONE",
        "ema9": 0.0,
        "ema21": 0.0,
        "bars": [],
        "_data_valid": False,
    }
    try:
        from bridge.strategy import _read_bars
        bars = _read_bars(120)
        if bars and len(bars) >= 26:
            from signal_generators.ema_macd_signal import _adx, _atr, _ema, _macd, _rsi, _macd_norm, _detect_macd_divergence
            closes = [float(b.get("close", 0)) for b in bars[-60:]]
            highs = [float(b.get("high", 0)) for b in bars[-60:]]
            lows = [float(b.get("low", 0)) for b in bars[-60:]]
            indicators["ema9"] = _ema(closes, 9)[-1] if closes else 0
            indicators["ema21"] = _ema(closes, 21)[-1] if closes else 0
            adx_val, adx_label, _adx_series = _adx(bars, 14)
            indicators["adx"] = adx_val if adx_val is not None else 25.0
            indicators["adx_label"] = adx_label
            indicators["rsi"] = _rsi(closes, 14)[-1] if closes else 50.0
            macd_line, signal_line, hist = _macd(closes)
            indicators["macd"] = macd_line[-1] if macd_line else 0.0
            indicators["macd_signal"] = signal_line[-1] if signal_line else 0.0
            indicators["macd_hist"] = hist[-1] if hist else 0.0
            c_atr = _atr(bars, 14)[-1] if bars else 3.0
            indicators["atr"] = c_atr
            indicators["macd_norm"] = round(_macd_norm(indicators.get("macd_hist", 0), c_atr), 5)
            indicators["macd_divergence"] = _detect_macd_divergence(closes, macd_line).get("divergence", "NONE")
            # Cross-cycle MACD hist state 鈥?only update when bars are fresh
            last_hist = _build_exit_indicators._last_macd_hist
            indicators["macd_hist_prev"] = last_hist if last_hist is not None else indicators["macd_hist"]
            _build_exit_indicators._last_macd_hist = indicators["macd_hist"]
            indicators["bars"] = bars
            indicators["_data_valid"] = True
    except Exception:
        pass
    return indicators

_build_exit_indicators._last_macd_hist = None


# 鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺?
# Exit monitoring cycle (pipeline owns this, calls gold_trader / exit_optimizer directly)
# 鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺?

def _run_exit_monitoring_cycle(
    state: Dict[str, Any],
    project_root: Path,
    args: argparse.Namespace,
    symbol: str,
    source: str,
    live_client: Any,
    poll_seconds: float,
) -> None:
    """Single-cycle exit monitoring. Pipeline main() calls this every poll_seconds
    while position_open=True.  NOT a while-loop 鈥?one iteration per call.

    Calls gold_trader directly for state I/O and EA bridge; calls exit_optimizer
    directly for strategy decisions.
    """
    global _EXIT_MONITOR_CYCLE, _EXIT_MT5_ABSENT_COUNT

    # Direct imports from the modules that own the logic 鈥?no wrappers.
    from bridge.gold_trader import (
        _finalize_position_closure,
        _apply_realized_closure_stats,
        _position_owned_by_pipeline,
        get_price,
        load_state,
        process_ea_reports,
        reconcile_positions,
        save_state,
        write_trade_request,
    )
    from bridge.exit_optimizer import evaluate_exit, execute_exit_actions

    _EXIT_MONITOR_CYCLE += 1
    # was_open must account for three cases:
    # 1. Normal: state.position_open is True (MT5 confirms position)
    # 2. Transition: _prev_was_open flag set by polling loop on open鈫抍losed transition
    # 3. Mid-count: _mt5_absent_count > 0 means we're confirming a close across cycles
    was_open = (
        bool(state.get("position_open", False))
        or bool(state.get("_prev_was_open", False))
        or (int(state.get("_mt5_absent_count", 0) or 0) > 0)
    )
    state.pop("_prev_was_open", None)
    prev_ticket = int(state.get("position_ticket", 0) or 0)
    prev_side = str(state.get("position_side", "") or "").upper()

    # 1. Process EA reports
    state_before_ea = dict(state)
    pipeline_owned = _position_owned_by_pipeline(state)
    state = process_ea_reports(state)
    if not state.get("position_open") and was_open:
        if pipeline_owned:
            # process_ea_reports skips close bookkeeping for pipeline-owned
            # positions (line 4736); pipeline must fill the gap.
            bid, ask, _, _, _ = get_price()
            exit_price = (bid + ask) / 2.0 if bid and ask else float(
                state_before_ea.get("position_entry", state_before_ea.get("position_price", 0)) or 0
            )
            pos_time = float(state_before_ea.get("position_time", time.time()))
            duration_min = int((time.time() - pos_time) / 60) if pos_time > 0 else 0
            close_pnl = _compute_close_pnl(state_before_ea, exit_price)
            _finalize_position_closure(
                state_before_ea, exit_price, close_pnl, duration_min,
                done_reason="EA_REPORT_CLOSE_DETECTED", close_ts=time.time(),
            )
        _seal_close_guard(state, closed_side=prev_side, closed_ticket=prev_ticket,
                          close_action="EA_REPORT_CLOSE_DETECTED")
        _reset_position_state(state)
        save_state(state)
        _EXIT_MONITOR_CYCLE = 0
        return

    # 2. Sync MT5 position fields
    state = _sync_mt5_to_state(state)
    if state.get("position_open"):
        _EXIT_MT5_ABSENT_COUNT = 0  # MT5 confirms position, reset blink counter
        state["_mt5_absent_count"] = 0  # reset persisted counter too

    # 2a. Forced close: _sync_mt5_to_state hit consecutive MT5 read errors while
    #     tracking an open position 鈥?MT5 files are consistently unreadable.
    #     Bypass the 3-count confirmation and close immediately to prevent
    #     permanent ghost orders when EA crashes or files are corrupted.
    if was_open and not state.get("position_open") and state.get("_mt5_sync_forced_close"):
        _EXIT_MT5_ABSENT_COUNT = 0
        state["_mt5_absent_count"] = 0
        bid, ask, _, _, _ = get_price()
        exit_price = (bid + ask) / 2.0 if bid and ask else 0.0
        state_before_close = dict(state)
        close_pnl = _compute_close_pnl(state_before_close, exit_price)
        pos_time = float(state_before_close.get("position_time", time.time()))
        duration_min = int((time.time() - pos_time) / 60) if pos_time > 0 else 0
        forced_reason = str(state.get("_mt5_sync_forced_close_reason", "unknown"))
        _finalize_position_closure(
            state_before_close, exit_price, close_pnl, duration_min,
            done_reason=f"MT5_SYNC_FORCED_CLOSE:{forced_reason}",
            close_ts=time.time(),
        )
        _seal_close_guard(state, closed_side=prev_side, closed_ticket=prev_ticket,
                          close_action="MT5_SYNC_FORCED_CLOSE")
        _reset_position_state(state)
        save_state(state)
        _EXIT_MONITOR_CYCLE = 0
        return

    # 3. Detect external close (MT5 position disappeared).
    #     Use state-persisted _mt5_absent_count so the 3-count confirmation
    #     survives across save_state/load_state cycles and polling-loop
    #     iterations.  The counter is reset to 0 whenever MT5 confirms the
    #     position is still open (step 2 above) or cleanup completes.
    if was_open and not state.get("position_open"):
        absent_count = int(state.get("_mt5_absent_count", 0) or 0) + 1
        state["_mt5_absent_count"] = absent_count
        if absent_count < 3:
            save_state(state)
            return
        state["_mt5_absent_count"] = 0
        bid, ask, _, _, _ = get_price()
        exit_price = (bid + ask) / 2.0 if bid and ask else 0.0
        state_before_close = dict(state)
        close_pnl = _compute_close_pnl(state_before_close, exit_price)
        pos_time = float(state_before_close.get("position_time", time.time()))
        duration_min = int((time.time() - pos_time) / 60) if pos_time > 0 else 0
        _finalize_position_closure(
            state_before_close, exit_price, close_pnl, duration_min,
            done_reason="EXTERNAL_CLOSE_DETECTED",
            close_ts=time.time(),
        )
        _seal_close_guard(state, closed_side=prev_side, closed_ticket=prev_ticket,
                          close_action="EXTERNAL_CLOSE_DETECTED")
        _reset_position_state(state)
        save_state(state)
        return

    if not state.get("position_open"):
        state["_mt5_absent_count"] = 0
        if was_open:
            _seal_close_guard(state, closed_side=prev_side, closed_ticket=prev_ticket,
                              close_action="MONITOR_EXIT_DETECTED")
        save_state(state)
        _EXIT_MONITOR_CYCLE = 0
        return

    # 4. Get current prices
    bid, ask, _, price_source, _ = get_price()
    if bid is None:
        return

    # 5. SL/TP hit check
    triggered, reason, action = _check_sltp_conditions(bid, ask, state)
    if triggered:
        state_before_close = dict(state)
        exit_price = (bid + ask) / 2.0 if bid and ask else 0.0
        pos_time = float(state.get("position_time", time.time()))
        duration_min = int((time.time() - pos_time) / 60) if pos_time > 0 else 0
        write_trade_request(
            action or "CLOSE", state.get("position_side", "BUY"),
            0, 0, 0, state.get("position_vol", 0.01),
            sig_id=str(state.get("position_signal_id") or state.get("position_decision_id") or ""),
            decision_id=str(state.get("position_decision_id") or ""),
            ticket=int(state.get("position_ticket", 0) or 0),
            request_origin="pipeline_sltp",
        )
        time.sleep(1.5)  # brief wait for MT5 EA to process, then re-check
        state = load_state()
        state = process_ea_reports(state)
        if not state.get("position_open"):
            if pipeline_owned:
                close_pnl = _compute_close_pnl(state_before_close, exit_price)
                _finalize_position_closure(
                    state_before_close, exit_price, close_pnl, duration_min,
                    done_reason="SLTP_CLOSE_DETECTED", close_ts=time.time(),
                )
            _seal_close_guard(state, closed_side=prev_side, closed_ticket=prev_ticket,
                              close_action="SLTP_CLOSE_DETECTED")
            _reset_position_state(state)
            save_state(state)
            _EXIT_MONITOR_CYCLE = 0
            return

    # 6. Macro escape hatch
    try:
        from bridge.macro_calendar import get_macro_event_signal
        macro_signal = get_macro_event_signal()
        if macro_signal.get("tier") == "BLOCK":
            state_before_close = dict(state)
            exit_price = (bid + ask) / 2.0 if bid and ask else 0.0
            pos_time = float(state.get("position_time", time.time()))
            duration_min = int((time.time() - pos_time) / 60) if pos_time > 0 else 0
            write_trade_request(
                "CLOSE", state.get("position_side", "BUY"),
                0, 0, 0, state.get("position_vol", 0.01),
                sig_id=str(state.get("position_signal_id") or state.get("position_decision_id") or ""),
                decision_id=str(state.get("position_decision_id") or ""),
                ticket=int(state.get("position_ticket", 0) or 0),
                request_origin="pipeline_macro_escape",
            )
            time.sleep(1.5)  # brief wait for MT5 EA to process, then re-check
            state = load_state()
            state = process_ea_reports(state)
            if not state.get("position_open"):
                if pipeline_owned:
                    close_pnl = _compute_close_pnl(state_before_close, exit_price)
                    _finalize_position_closure(
                        state_before_close, exit_price, close_pnl, duration_min,
                        done_reason="MACRO_ESCAPE_CLOSE_DETECTED", close_ts=time.time(),
                    )
                _seal_close_guard(state, closed_side=prev_side, closed_ticket=prev_ticket,
                                  close_action="MACRO_ESCAPE_CLOSE_DETECTED")
                _reset_position_state(state)
                save_state(state)
                _EXIT_MONITOR_CYCLE = 0
                return
    except Exception:
        pass

    # 7. Exit optimizer (core strategy)
    try:
        indicators = _build_exit_indicators(state, bid, ask, project_root)
        atr_v = float(indicators.get("atr", 3.0) or 3.0)
        strategy_id = state.get("position_strategy_id", "") or ""
        # 鈹€鈹€ Use CURRENT regime (from live session_analyzer), not stale position_regime 鈹€鈹€
        regime = ""
        try:
            from bridge.session_analyzer import analyze_all_sessions
            _live = analyze_all_sessions()
            if isinstance(_live, dict) and "error" not in _live:
                regime = str(_live.get("regime", "") or "")
        except Exception:
            pass
        if not regime:
            regime = ""  # Don't fall back to stale position_regime; exit optimizer will use DEFAULT path

        # 鈹€鈹€ Multi-dimensional market sensing 鈹€鈹€
        market_ctx = None
        try:
            from src.exit_market_sensor import get_exit_market_sensor
            sensor = get_exit_market_sensor()
            bars_for_sensor = indicators.get("bars", [])
            if bars_for_sensor and indicators.get("_data_valid"):
                spread_val = ask - bid if ask and bid else 0.0
                try:
                    from bridge.garch_volatility import get_vol_multiplier
                    gv = get_vol_multiplier(refresh=False)
                    vol_ratio_garch = gv.get("vol_ratio", 1.0)
                except Exception:
                    vol_ratio_garch = 1.0
                market_ctx = sensor.sense(
                    bars_for_sensor, bid, ask, spread_val,
                    regime_label=regime or "UNKNOWN",
                    vol_ratio=vol_ratio_garch,
                )
        except Exception:
            market_ctx = None  # non-blocking

        should_exit, reason, exit_detail = evaluate_exit(
            bid, ask, state, indicators, atr_v,
            strategy_id=strategy_id, regime=regime,
            market_ctx=market_ctx,
        )

        if isinstance(exit_detail, dict):
            # Persist exit-optimizer state flags across cycles
            for exit_key, state_key in (
                ("mark_l1_done", "l1_breakeven_done"),
                ("mark_l2_closed", "l2_partial_closed"),
                ("mark_l3_closed", "l3_partial_closed"),
            ):
                if exit_detail.get(exit_key):
                    state[state_key] = True
            # 绔嬪嵆鎸佷箙鍖栨爣璁帮紝闃叉鍚庣画 load_state() 瑕嗙洊
            try:
                save_state(state)
            except Exception:
                pass
            trail = exit_detail.get("trail_debug", {})
            if isinstance(trail, dict):
                state["trailing_telemetry"] = {**trail, "bid": bid, "ask": ask,
                    "updated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}
                _persist_trail_telemetry(state["trailing_telemetry"])

        if should_exit and isinstance(exit_detail, dict) and "actions" in exit_detail:
            from bridge.bridge_constants import BRIDGE_TO_EA
            state_before_close = dict(state)
            exit_price = (bid + ask) / 2.0 if bid and ask else 0.0
            pos_time = float(state.get("position_time", time.time()))
            duration_min = int((time.time() - pos_time) / 60) if pos_time > 0 else 0
            execute_exit_actions(exit_detail["actions"], state, bid, ask)
            time.sleep(1.0)  # brief wait for EA to process exit actions
            state = load_state()
            state = process_ea_reports(state)
            if not state.get("position_open"):
                if pipeline_owned:
                    close_pnl = _compute_close_pnl(state_before_close, exit_price)
                    _finalize_position_closure(
                        state_before_close, exit_price, close_pnl, duration_min,
                        done_reason="EXIT_OPTIMIZER_CLOSE_DETECTED", close_ts=time.time(),
                    )
                _seal_close_guard(state, closed_side=prev_side, closed_ticket=prev_ticket,
                                  close_action="EXIT_OPTIMIZER_CLOSE_DETECTED")
                _reset_position_state(state)
                save_state(state)
                _EXIT_MONITOR_CYCLE = 0
                return
    except Exception:
        import traceback
        try:
            from bridge.gold_trader import _daemon_log
            _daemon_log(f"Exit optimizer cycle error: {traceback.format_exc()}")
        except Exception:
            pass

    # 8. Periodic reconcile (every ~30 cycles 鈮?60s at 2s poll)
    if _EXIT_MONITOR_CYCLE % 30 == 0:
        state = reconcile_positions(state)

    # 9. Persist state
    save_state(state)


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--input-path", required=True)
    parser.add_argument("--output-path", required=True)
    parser.add_argument("--source", default="file", choices=["file", "mt5", "ea_feed"])
    parser.add_argument("--runtime-intent", default="offline_train", choices=["offline_train", "paper_trade", "live_trade"])
    parser.add_argument("--tenant-id", default="")
    parser.add_argument("--symbol", default="")
    parser.add_argument("--timeframe", default="M5")
    parser.add_argument("--execution-mode", default="file", choices=["file", "mock", "direct", "ea_bridge"])
    parser.add_argument("--execution-report-path", default="")
    parser.add_argument("--poll-seconds", type=float, default=2.0)
    parser.add_argument("--enable-incident-autofix", default="true")
    parser.add_argument("--once", action="store_true")
    args = parser.parse_args()

    # 鈹€鈹€ License gate 鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€
    try:
        from src.license.guard import enforce_license
        enforce_license()
    except Exception as e:
        print(f"License check failed: {e}", file=sys.stderr)
        return 1

    code_root = Path(__file__).resolve().parents[1]
    project_root = _resolve_runtime_root(code_root)
    input_path = _resolve_runtime_path(project_root, args.input_path)
    output_path = _resolve_runtime_path(project_root, args.output_path)
    execution_report_path = (
        _resolve_runtime_path(project_root, args.execution_report_path)
        if args.execution_report_path
        else None
    )
    enable_incident_autofix = _to_bool(args.enable_incident_autofix)

    # 鈹€鈹€ Universal symbol auto-detection: EA auto-follow first, user override second 鈹€鈹€
    if not args.symbol:
        # 1. Try terminal_status.json (EA chart symbol 鈥?primary, auto-follow)
        try:
            from config_manager import get_mt5_dir
            snap_path = Path(get_mt5_dir()) / "gold_autotrade" / "bridge" / "terminal_status.json"
            if not snap_path.exists():
                snap_path = Path(get_mt5_dir()) / "gold_autotrade" / "bridge" / "market_snapshot.json"
            if snap_path.exists():
                snap = json.loads(snap_path.read_text(encoding="utf-8"))
                sym = snap.get("symbol", "") or snap.get("marketSymbol", "") or ""
                if sym:
                    args.symbol = str(sym).strip().upper()
        except Exception:
            pass
    if not args.symbol:
        # 2. Try active_instances.json (user temporary override)
        try:
            from config_manager import get_config_dir
            inst_path = get_config_dir() / "active_instances.json"
            if inst_path.exists():
                inst = json.loads(inst_path.read_text(encoding="utf-8"))
                for _k, _v in inst.items():
                    if isinstance(_v, dict) and _v.get("symbol"):
                        args.symbol = str(_v["symbol"]).strip().upper()
                        break
        except Exception:
            pass

    live_client = None
    if args.source == "mt5":
        live_client = MT5LiveSource(args.symbol or "XAUUSD", args.timeframe)
        if not live_client.connect():
            raise SystemExit("MT5 connect failed: source=mt5 requires realtime connection")
    if args.source == "ea_feed":
        live_client = EABridgeLiveSource(args.symbol, args.timeframe, project_root=project_root)
        if not args.symbol and live_client is not None:
            # Auto-detect symbol from first EA snapshot
            for _ in range(10):  # Wait up to 10 polls
                try:
                    snap = live_client.snapshot()
                    if snap and snap.get("symbol"):
                        args.symbol = snap["symbol"]
                        live_client.symbol = snap["symbol"]
                        break
                except Exception:
                    pass
                import time as _t; _t.sleep(1)
        if not live_client.connect():
            raise SystemExit("EA feed connect failed: source=ea_feed requires bridge market_snapshot.json")

    _acquire_pipeline_singleton_lock(project_root, args.runtime_intent, args.source, args.execution_mode)

    # One-shot log cleanup at startup (defensive: stale files from before this fix).
    _cleanup_stale_logs(project_root)

    # Reconcile stale signals from previous session (e.g. SENT/TIMEOUT 鈫?RECONCILED).
    _reconcile_stale_signals(project_root)

    if args.once:
        try:
            run_once(
                project_root,
                code_root,
                args.tenant_id,
                args.source,
                args.runtime_intent,
                input_path,
                output_path,
                args.execution_mode,
                args.symbol,
                execution_report_path,
                enable_incident_autofix,
                live_client,
            )
            print(json.dumps({"ok": True, "mode": "once", "output": str(output_path)}, ensure_ascii=False))
            return 0
        finally:
            if live_client is not None:
                live_client.close()
            _release_pipeline_singleton_lock()

    last_mtime = 0.0
    poll_seconds = float(args.poll_seconds)
    global _CLEANUP_TICK, _MACRO_REFRESH_TICK, _MACRO_EVENT_SCAN_TICK, _REFLECTION_TICK
    # Track previous position state across poll iterations so we can detect
    # open鈫抍losed transitions.  load_state() syncs with MT5 and may clear
    # position_open before _run_exit_monitoring_cycle gets a chance to run
    # close bookkeeping (finalize/seal/reset).  When a transition is detected,
    # we force one last exit-monitoring cycle with _prev_was_open=True so the
    # 3-count confirmation and cleanup logic can fire.
    _prev_position_open = False
    try:
        while True:
            try:
                # 鈹€鈹€ Kill-switch: instant circuit breaker 鈹€鈹€
                ks = project_root / "data" / "kill_switch"
                if ks.exists():
                    from bridge.gold_trader import load_state as _ks_state, write_trade_request as _ks_close
                    _ks_s = _ks_state()
                    for _ks_pos in (_ks_s.get("positions_all", []) or []):
                        _ks_side = str(_ks_pos.get("side", "BUY")).upper()
                        _ks_close_side = "SELL" if _ks_side == "BUY" else "BUY"
                        try:
                            _ks_close(
                                symbol=args.symbol, action="CLOSE", side=_ks_close_side,
                                volume=float(_ks_pos.get("volume", 0.01) or 0.01),
                                entry=0.0, sl=0.0, tp=0.0,
                                decision_id=f"kill_switch_{_ks_pos.get('ticket', 0)}",
                            )
                        except Exception:
                            pass
                    ks.unlink()
                    raise SystemExit("Kill-switch activated 鈥?all positions closed, pipeline halted")

                # Phase 2: exit monitoring branch 鈥?pipeline owns exit orchestration.
                # When position is open, drive exit monitoring directly (no middleman).
                if args.source in {"mt5", "ea_feed"} and args.runtime_intent == "live_trade":
                    from bridge.gold_trader import load_state
                    state = load_state()
                    # Enter exit monitoring when:
                    #  - position is open (normal case), OR
                    #  - we're mid-count confirming an external close (_mt5_absent_count > 0)
                    if state.get("position_open") or state.get("_mt5_absent_count", 0) > 0:
                        # If position just closed (load_state cleared it but we were
                        # tracking one), communicate the transition via _prev_was_open.
                        if not state.get("position_open") and _prev_position_open:
                            state["_prev_was_open"] = True
                        _run_exit_monitoring_cycle(
                            state, project_root, args,
                            args.symbol, args.source, live_client,
                            poll_seconds,
                        )
                        _CLEANUP_TICK += 1
                        _MACRO_REFRESH_TICK += 1
                        _MACRO_EVENT_SCAN_TICK += 1
                        _REFLECTION_TICK += 1
                        if _CLEANUP_TICK >= _CLEANUP_INTERVAL:
                            _cleanup_stale_logs(project_root)
                            _CLEANUP_TICK = 0
                        if _MACRO_REFRESH_TICK >= _MACRO_REFRESH_INTERVAL:
                            _try_macro_direction_refresh(project_root)
                            _MACRO_REFRESH_TICK = 0
                        if _MACRO_EVENT_SCAN_TICK >= _MACRO_EVENT_SCAN_INTERVAL:
                            _maybe_trigger_macro_event_refresh(project_root, symbol=args.symbol)
                            _MACRO_EVENT_SCAN_TICK = 0
                        _prev_position_open = bool(state.get("position_open", False))
                        # 澶氱瓥鐣ュ苟鍙戯細涓嶅啀璺宠繃淇″彿鐢熸垚銆備粨浣嶆暟閲忎笂闄愮敱 _dispatch_signal
                        # 涓殑 Guard 3 (max_open_positions) 鍜?resolve_live_open_guard 缁熶竴鎺у埗銆?
                        # 閫€鍑虹洃瑙嗐€佸畯瑙傛柟鍚戝埛鏂般€佸畾鏃舵竻鐞嗙瓑浠诲姟宸插湪涓婇潰鎵ц瀹屾瘯銆?
                    else:
                        _prev_position_open = False

                if args.source in {"mt5", "ea_feed"}:
                    run_once(
                        project_root,
                        code_root,
                        args.tenant_id,
                        args.source,
                        args.runtime_intent,
                        input_path,
                        output_path,
                        args.execution_mode,
                        args.symbol,
                        execution_report_path,
                        enable_incident_autofix,
                        live_client,
                    )
                    _CLEANUP_TICK += 1
                    _MACRO_REFRESH_TICK += 1
                    _MACRO_EVENT_SCAN_TICK += 1
                    _REFLECTION_TICK += 1
                    if _CLEANUP_TICK >= _CLEANUP_INTERVAL:
                        _cleanup_stale_logs(project_root)
                        _CLEANUP_TICK = 0
                    if _MACRO_REFRESH_TICK >= _MACRO_REFRESH_INTERVAL:
                        _try_macro_direction_refresh(project_root)
                        _MACRO_REFRESH_TICK = 0
                    if _MACRO_EVENT_SCAN_TICK >= _MACRO_EVENT_SCAN_INTERVAL:
                        _maybe_trigger_macro_event_refresh(project_root, symbol=args.symbol)
                        _MACRO_EVENT_SCAN_TICK = 0
                elif input_path.exists():
                    current_mtime = input_path.stat().st_mtime
                    if current_mtime > last_mtime:
                        run_once(
                            project_root,
                            code_root,
                            args.tenant_id,
                            args.source,
                            args.runtime_intent,
                            input_path,
                            output_path,
                            args.execution_mode,
                            args.symbol,
                            execution_report_path,
                            enable_incident_autofix,
                            live_client,
                        )
                        last_mtime = current_mtime
            except RuntimeError as exc:
                # Live feed may be temporarily unavailable during EA/terminal warm-up.
                if args.source in {"mt5", "ea_feed"} and _is_live_source_retryable_error(exc):
                    time.sleep(poll_seconds)
                    continue
                raise
            time.sleep(poll_seconds)
    finally:
        if live_client is not None:
            live_client.close()
        _release_pipeline_singleton_lock()


if __name__ == "__main__":
    raise SystemExit(main())

