"""
策略智能体 — 多策略模式下的精细化绩效追踪、自适应学习与康复机制。

核心理念：策略连亏不应只被惩罚，而应被诊断、适配、康复。
每个策略实例维护自己的"体检报告"，系统据此动态分配风险预算、
调整投票权重、并自动发现策略在不同市况下的专长区域。

Author: OpenClaw Agent
"""

from __future__ import annotations

import json
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple


# ═══════════════════════════════════════════════════════════════════════════════
# Data Classes
# ═══════════════════════════════════════════════════════════════════════════════

@dataclass
class RegimeFitness:
    """单一市况维度下的策略适配度追踪。"""
    total: int = 0
    wins: int = 0
    total_pnl: float = 0.0
    win_rate: float = 0.0
    avg_pnl: float = 0.0
    profit_factor: float = 0.0
    gross_profit: float = 0.0
    gross_loss: float = 0.0
    last_updated: float = 0.0

    def record(self, pnl: float) -> None:
        self.total += 1
        self.total_pnl += pnl
        if pnl > 0:
            self.wins += 1
            self.gross_profit += pnl
        else:
            self.gross_loss += abs(pnl)
        self.win_rate = self.wins / max(1, self.total)
        self.avg_pnl = self.total_pnl / max(1, self.total)
        self.profit_factor = self.gross_profit / max(0.01, self.gross_loss)
        self.last_updated = time.time()


@dataclass
class LossDiagnosis:
    """单笔亏损的诊断分类，用于模式识别和改进建议。"""
    pnl: float
    regime: str
    session: str
    entry_confidence: float
    counter_trend_risk: str
    execution_advice: str
    failure_mode: str  # from pattern_memory
    diagnosis: str  # classified reason
    ts: int

    def to_dict(self) -> dict:
        return {
            "pnl": self.pnl,
            "regime": self.regime,
            "session": self.session,
            "entry_confidence": self.entry_confidence,
            "counter_trend_risk": self.counter_trend_risk,
            "execution_advice": self.execution_advice,
            "failure_mode": self.failure_mode,
            "diagnosis": self.diagnosis,
            "ts": self.ts,
        }


@dataclass
class InstanceHealth:
    """策略实例的综合健康评估。"""
    instance_id: str
    strategy_id: str
    score: float  # 0-100
    grade: str  # "A"(≥80), "B"(≥60), "C"(≥40), "D"(≥20), "F"(<20)

    # Factor scores (0-100 each)
    win_rate_score: float
    profit_factor_score: float
    consistency_score: float    # low loss streak → high score
    regime_fit_score: float     # current regime match quality
    recency_score: float        # recent performance weighted more

    # Raw metrics
    total_trades: int
    win_rate: float
    profit_factor: float
    loss_streak: int
    sharpe_20: float
    total_pnl: float

    # Adaptive suggestions
    suggested_action: str  # "normal", "reduce_size", "shadow_only", "rehabilitate"
    suggested_scale: float  # 0.0 - 1.0 multiplier for position size
    rehabilitation_progress: float  # 0.0 - 1.0, only meaningful when shadow_only

    # Regime specialization
    best_regime: str
    best_regime_win_rate: float
    current_regime_fitness: float  # 0.0-1.0, how well this strategy fits current market

    # Loss diagnosis
    recent_loss_diagnoses: List[dict] = field(default_factory=list)
    primary_loss_pattern: str = ""  # dominant failure mode

    def to_dict(self) -> dict:
        return {
            "instance_id": self.instance_id,
            "strategy_id": self.strategy_id,
            "score": round(self.score, 1),
            "grade": self.grade,
            "win_rate_score": round(self.win_rate_score, 1),
            "profit_factor_score": round(self.profit_factor_score, 1),
            "consistency_score": round(self.consistency_score, 1),
            "regime_fit_score": round(self.regime_fit_score, 1),
            "recency_score": round(self.recency_score, 1),
            "total_trades": self.total_trades,
            "win_rate": round(self.win_rate, 4),
            "profit_factor": round(self.profit_factor, 2),
            "loss_streak": self.loss_streak,
            "sharpe_20": round(self.sharpe_20, 4),
            "total_pnl": round(self.total_pnl, 2),
            "suggested_action": self.suggested_action,
            "suggested_scale": round(self.suggested_scale, 4),
            "rehabilitation_progress": round(self.rehabilitation_progress, 4),
            "best_regime": self.best_regime,
            "best_regime_win_rate": round(self.best_regime_win_rate, 4),
            "current_regime_fitness": round(self.current_regime_fitness, 4),
            "primary_loss_pattern": self.primary_loss_pattern,
        }


# ═══════════════════════════════════════════════════════════════════════════════
# Loss Diagnosis Engine
# ═══════════════════════════════════════════════════════════════════════════════

LOSS_PATTERNS = {
    "REGIME_MISMATCH": {
        "label": "市况错配",
        "description": "策略在不擅长的市况下交易",
        "indicators": ["regime not in best_regime", "current_regime_fitness < 0.3"],
        "action": "收紧市况过滤，仅在优势市况交易",
    },
    "OVERTRADING_HIGH_RISK": {
        "label": "高风险过度交易",
        "description": "在 HIGH 风险环境下开仓",
        "indicators": ["counter_trend_risk == HIGH", "loss in HIGH risk"],
        "action": "HIGH 风险时晋升为 shadow_only",
    },
    "LOW_CONFIDENCE_ENTRY": {
        "label": "低置信度入场",
        "description": "入场置信度不足导致亏损",
        "indicators": ["entry_confidence < 0.5", "loss on low confidence"],
        "action": "提高 min_confidence 阈值",
    },
    "IGNORED_EXECUTION_ADVICE": {
        "label": "忽视执行建议",
        "description": "Interpreter 建议 LIMIT_ONLY 或 WAIT，但仍开市价单",
        "indicators": ["execution_advice in LIMIT_ONLY/WAIT_CONFIRMATION", "loss"],
        "action": "严格遵循执行建议",
    },
    "SESSION_WEAKNESS": {
        "label": "时段弱势",
        "description": "在特定交易时段表现持续不佳",
        "indicators": ["same session losses > 60%", "session win_rate < 0.35"],
        "action": "在该时段降低仓位或暂停",
    },
    "ADVERSE_VOLATILITY": {
        "label": "不利波动",
        "description": "高波动环境下止损被扫",
        "indicators": ["loss with volatility_state == HIGH", "failure_mode contains STOP_HUNT"],
        "action": "高波动时加宽止损或暂停",
    },
    "PATTERN_FAILURE": {
        "label": "形态失败",
        "description": "技术形态信号失败",
        "indicators": ["failure_mode not NONE", "failure_mode not UNKNOWN"],
        "action": "检查形态识别参数，收紧确认条件",
    },
}


def _classify_loss(row: dict) -> str:
    """根据平仓记录诊断亏损原因。"""
    diagnoses = []

    regime = str(row.get("selection_regime", row.get("market_structure", "")) or "").upper()
    risk = str(row.get("counter_trend_risk", "") or "").upper()
    confidence = float(row.get("entry_confidence", 0.5) or 0.5)
    advice = str(row.get("execution_advice", "") or "").upper()
    failure = str(row.get("failure_mode", "NONE") or "NONE").upper()

    if confidence < 0.50:
        diagnoses.append("LOW_CONFIDENCE_ENTRY")
    if risk == "HIGH":
        diagnoses.append("OVERTRADING_HIGH_RISK")
    if advice in {"LIMIT_ONLY", "WAIT_CONFIRMATION", "BLOCK_COUNTER_TREND"}:
        diagnoses.append("IGNORED_EXECUTION_ADVICE")
    if failure and failure not in {"NONE", "UNKNOWN"}:
        diagnoses.append(f"PATTERN_FAILURE:{failure}")
    if regime in {"RANGE", "RANGING", "UNCERTAIN"}:
        diagnoses.append("REGIME_MISMATCH")

    if not diagnoses:
        diagnoses.append("NORMAL_LOSS")

    return "|".join(diagnoses)


# ═══════════════════════════════════════════════════════════════════════════════
# InstancePerformanceTracker
# ═══════════════════════════════════════════════════════════════════════════════

class InstancePerformanceTracker:
    """每个策略实例的独立绩效追踪与智能分析。"""

    def __init__(self, instance_id: str, strategy_id: str):
        self.instance_id = instance_id
        self.strategy_id = strategy_id

        # Full trade history from recent_closures
        self._all_trades: List[dict] = []

        # Regime fitness: keyed by regime label
        self.regime_fitness: Dict[str, RegimeFitness] = {}

        # Session fitness: keyed by session name (ASIA, LONDON, NY, etc.)
        self.session_fitness: Dict[str, RegimeFitness] = {}

        # Recent loss diagnoses (last 10 losses)
        self._loss_diagnoses: List[LossDiagnosis] = []

        # Rolling metrics
        self._win_rate_20: float = 0.0
        self._win_rate_all: float = 0.0
        self._profit_factor: float = 0.0
        self._loss_streak: int = 0
        self._sharpe_20: float = 0.0
        self._total_pnl: float = 0.0
        self._total_trades: int = 0

        # Rehabilitation state
        self._penalty_level: str = "none"  # "none", "soft", "hard"
        self._shadow_trades_since_penalty: int = 0
        self._shadow_wins_since_penalty: int = 0
        self._penalty_applied_at: float = 0.0
        self._min_shadow_trades_to_rehab: int = 5  # need 5 good shadow trades to rehab

        # Last computed health
        self._cached_health: Optional[InstanceHealth] = None
        self._health_cache_ts: float = 0.0
        self._health_cache_ttl: float = 30.0  # 30 second cache

    # ── Data Ingestion ────────────────────────────────────────────────────

    def ingest_recent_closures(self, all_closures: List[dict]) -> None:
        """从 agent_state.json 的 recent_closures 提取本实例的交易记录。"""
        self._all_trades = [
            row for row in all_closures
            if (
                str(row.get("instance_id", "") or "").strip() == self.instance_id
                or str(row.get("strategy_id", "") or "").strip() == self.strategy_id
            )
        ]
        self._rebuild_metrics()

    def _rebuild_metrics(self) -> None:
        """从 _all_trades 重建所有滚动指标。"""
        trades = self._all_trades
        self._total_trades = len(trades)

        if not trades:
            self._win_rate_20 = 0.0
            self._win_rate_all = 0.0
            self._profit_factor = 0.0
            self._loss_streak = 0
            self._sharpe_20 = 0.0
            self._total_pnl = 0.0
            return

        # PnL stats
        pnls = [float(t.get("pnl", 0.0) or 0.0) for t in trades]
        self._total_pnl = sum(pnls)

        # Win rate (all)
        wins_all = sum(1 for p in pnls if p > 0)
        self._win_rate_all = wins_all / max(1, self._total_trades)

        # Win rate (last 20)
        recent_20 = pnls[-20:]
        wins_20 = sum(1 for p in recent_20 if p > 0)
        self._win_rate_20 = wins_20 / max(1, len(recent_20))

        # Profit factor
        gross_profit = sum(p for p in pnls if p > 0)
        gross_loss = abs(sum(p for p in pnls if p < 0))
        self._profit_factor = gross_profit / max(0.01, gross_loss)

        # Loss streak (scan from end)
        streak = 0
        for p in reversed(pnls):
            if p < 0:
                streak += 1
            else:
                break
        self._loss_streak = streak

        # Sharpe (last 20, simplified — assumes risk-free = 0)
        if len(recent_20) >= 3:
            mean_pnl = sum(recent_20) / len(recent_20)
            variance = sum((p - mean_pnl) ** 2 for p in recent_20) / max(1, len(recent_20) - 1)
            std_pnl = variance ** 0.5
            self._sharpe_20 = mean_pnl / max(0.0001, std_pnl) * (len(recent_20) ** 0.5)
        else:
            self._sharpe_20 = 0.0

        # Build regime fitness
        self._build_regime_fitness()
        self._build_session_fitness()
        self._build_loss_diagnoses()

    def _build_regime_fitness(self) -> None:
        self.regime_fitness = {}
        for row in self._all_trades:
            regime = str(row.get("selection_regime", row.get("market_structure", "UNKNOWN")) or "UNKNOWN").upper()
            if regime not in self.regime_fitness:
                self.regime_fitness[regime] = RegimeFitness()
            self.regime_fitness[regime].record(float(row.get("pnl", 0.0) or 0.0))

    def _build_session_fitness(self) -> None:
        self.session_fitness = {}
        for row in self._all_trades:
            session = str(row.get("session_mode", "UNKNOWN") or "UNKNOWN").upper()
            if session not in self.session_fitness:
                self.session_fitness[session] = RegimeFitness()
            self.session_fitness[session].record(float(row.get("pnl", 0.0) or 0.0))

    def _build_loss_diagnoses(self) -> None:
        self._loss_diagnoses = []
        for row in self._all_trades:
            pnl = float(row.get("pnl", 0.0) or 0.0)
            if pnl < 0:
                self._loss_diagnoses.append(LossDiagnosis(
                    pnl=pnl,
                    regime=str(row.get("selection_regime", row.get("market_structure", "")) or ""),
                    session=str(row.get("session_mode", "") or ""),
                    entry_confidence=float(row.get("entry_confidence", 0.5) or 0.5),
                    counter_trend_risk=str(row.get("counter_trend_risk", "") or ""),
                    execution_advice=str(row.get("execution_advice", "") or ""),
                    failure_mode=str(row.get("failure_mode", "NONE") or "NONE"),
                    diagnosis=_classify_loss(row),
                    ts=int(row.get("ts", 0) or 0),
                ))
        # Keep last 10
        self._loss_diagnoses = self._loss_diagnoses[-10:]

    # ── Health Scoring ────────────────────────────────────────────────────

    def compute_health(
        self,
        current_regime: str = "",
        current_session: str = "",
        decision_policy: Optional[dict] = None,
    ) -> InstanceHealth:
        """计算策略实例的综合健康评分 (0-100)。

        五因子加权：
          - win_rate_score:     30%  近期胜率
          - profit_factor_score: 25%  盈亏比
          - consistency_score:   20%  连亏惩罚
          - regime_fit_score:    15%  当前市况适配度
          - recency_score:       10%  最近表现权重
        """
        now = time.time()
        if self._cached_health and (now - self._health_cache_ts) < self._health_cache_ttl:
            return self._cached_health

        # ── Win rate score (30%) ──
        wr = self._win_rate_20 if self._total_trades >= 5 else (self._win_rate_all if self._total_trades > 0 else 0.5)
        if self._total_trades < 3:
            win_rate_score = 50.0  # neutral for new strategies
        else:
            win_rate_score = min(100.0, max(0.0, wr * 100.0 + 20.0 * min(1.0, self._total_trades / 20.0)))

        # ── Profit factor score (25%) ──
        if self._total_trades < 3:
            profit_factor_score = 50.0
        else:
            pf = self._profit_factor
            if pf >= 2.0:
                profit_factor_score = 100.0
            elif pf >= 1.5:
                profit_factor_score = 85.0
            elif pf >= 1.2:
                profit_factor_score = 70.0
            elif pf >= 1.0:
                profit_factor_score = 55.0
            elif pf >= 0.8:
                profit_factor_score = 40.0
            elif pf >= 0.5:
                profit_factor_score = 25.0
            else:
                profit_factor_score = 10.0

        # ── Consistency score (20%) ──
        streak = self._loss_streak
        if streak == 0:
            consistency_score = 100.0
        elif streak == 1:
            consistency_score = 75.0
        elif streak == 2:
            consistency_score = 50.0
        elif streak == 3:
            consistency_score = 30.0
        elif streak == 4:
            consistency_score = 15.0
        else:
            consistency_score = 5.0

        # ── Regime fit score (15%) ──
        regime_fit_score = 50.0
        best_regime = "UNKNOWN"
        best_regime_wr = 0.0
        current_fitness = 0.5
        if self.regime_fitness:
            scored_regimes = [
                (r, f.win_rate * min(1.0, f.total / 5.0), f.total)
                for r, f in self.regime_fitness.items()
            ]
            scored_regimes.sort(key=lambda x: x[1], reverse=True)
            best_regime = scored_regimes[0][0]
            best_regime_wr = scored_regimes[0][1]

            if current_regime:
                current_fit = self.regime_fitness.get(current_regime.upper())
                if current_fit and current_fit.total >= 2:
                    current_fitness = min(1.0, current_fit.win_rate * min(1.0, current_fit.total / 10.0) + 0.3)
                    if current_fit.win_rate >= 0.6:
                        regime_fit_score = 90.0
                    elif current_fit.win_rate >= 0.5:
                        regime_fit_score = 70.0
                    elif current_fit.win_rate >= 0.4:
                        regime_fit_score = 50.0
                    elif current_fit.win_rate >= 0.3:
                        regime_fit_score = 30.0
                    else:
                        regime_fit_score = 15.0
                else:
                    regime_fit_score = 40.0
                    current_fitness = 0.4
        else:
            best_regime = "UNKNOWN"
            best_regime_wr = 0.0
            current_fitness = 0.5

        # ── Recency score (10%) ──
        if len(self._all_trades) >= 3:
            last_3 = [float(t.get("pnl", 0.0) or 0.0) for t in self._all_trades[-3:]]
            last_3_wins = sum(1 for p in last_3 if p > 0)
            last_3_pnl = sum(last_3)
            if last_3_wins >= 2 and last_3_pnl > 0:
                recency_score = 90.0
            elif last_3_wins == 2 and last_3_pnl <= 0:
                recency_score = 65.0
            elif last_3_wins == 1 and last_3_pnl > 0:
                recency_score = 60.0
            elif last_3_wins == 1:
                recency_score = 40.0
            else:
                recency_score = 15.0
        else:
            recency_score = 50.0

        # ── Composite score ──
        score = (
            win_rate_score * 0.30
            + profit_factor_score * 0.25
            + consistency_score * 0.20
            + regime_fit_score * 0.15
            + recency_score * 0.10
        )

        # ── Grade ──
        if score >= 80:
            grade = "A"
        elif score >= 60:
            grade = "B"
        elif score >= 40:
            grade = "C"
        elif score >= 20:
            grade = "D"
        else:
            grade = "F"

        # ── Suggested action & scale ──
        suggested_action, suggested_scale = self._determine_action(score, streak)

        # ── Primary loss pattern ──
        primary_loss_pattern = ""
        if self._loss_diagnoses:
            pattern_counts: Dict[str, int] = {}
            for d in self._loss_diagnoses:
                for part in d.diagnosis.split("|"):
                    base = part.split(":")[0]
                    pattern_counts[base] = pattern_counts.get(base, 0) + 1
            if pattern_counts:
                primary_loss_pattern = max(pattern_counts, key=pattern_counts.get)

        # ── Rehabilitation progress ──
        rehab_progress = 0.0
        if self._penalty_level != "none" and self._min_shadow_trades_to_rehab > 0:
            rehab_progress = min(1.0, self._shadow_trades_since_penalty / self._min_shadow_trades_to_rehab)
            if self._shadow_wins_since_penalty <= 0 and self._shadow_trades_since_penalty >= 3:
                rehab_progress *= 0.5  # no wins → half progress

        self._cached_health = InstanceHealth(
            instance_id=self.instance_id,
            strategy_id=self.strategy_id,
            score=score,
            grade=grade,
            win_rate_score=win_rate_score,
            profit_factor_score=profit_factor_score,
            consistency_score=consistency_score,
            regime_fit_score=regime_fit_score,
            recency_score=recency_score,
            total_trades=self._total_trades,
            win_rate=self._win_rate_20 if self._total_trades >= 5 else self._win_rate_all,
            profit_factor=self._profit_factor,
            loss_streak=self._loss_streak,
            sharpe_20=self._sharpe_20,
            total_pnl=self._total_pnl,
            suggested_action=suggested_action,
            suggested_scale=suggested_scale,
            rehabilitation_progress=rehab_progress,
            best_regime=best_regime,
            best_regime_win_rate=best_regime_wr,
            current_regime_fitness=current_fitness,
            recent_loss_diagnoses=[d.to_dict() for d in self._loss_diagnoses[-5:]],
            primary_loss_pattern=primary_loss_pattern,
        )
        self._health_cache_ts = now
        return self._cached_health

    def _determine_action(self, score: float, loss_streak: int) -> Tuple[str, float]:
        """根据健康评分和连亏数决定策略处置。

        新策略（<3笔交易）不给处罚，以鼓励探索。
        有经验的策略根据评分和连亏数逐级收紧。
        """
        is_new = self._total_trades < 3
        if is_new:
            if loss_streak >= 3:
                return "reduce_size", 0.55
            return "normal", 1.0

        if loss_streak >= 5:
            return "shadow_only", 0.0
        if loss_streak >= 4:
            return "rehabilitate", 0.15
        if score < 20:
            return "shadow_only", 0.0
        if score < 35:
            return "rehabilitate", 0.25
        if score < 50:
            return "reduce_size", 0.55
        if score < 65:
            return "reduce_size", 0.75
        return "normal", 1.0

    # ── Rehabilitation ────────────────────────────────────────────────────

    def apply_penalty(self, level: str) -> None:
        """施加惩罚并将策略送入康复模式。"""
        self._penalty_level = level
        self._penalty_applied_at = time.time()
        self._shadow_trades_since_penalty = 0
        self._shadow_wins_since_penalty = 0

    def record_shadow_trade(self, pnl: float) -> None:
        """记录影子交易结果，用于康复进度追踪。"""
        self._shadow_trades_since_penalty += 1
        if pnl > 0:
            self._shadow_wins_since_penalty += 1

    def check_rehabilitation(self) -> bool:
        """检查是否满足康复条件。"""
        if self._penalty_level == "none":
            return True
        if self._shadow_trades_since_penalty < self._min_shadow_trades_to_rehab:
            return False
        shadow_wr = self._shadow_wins_since_penalty / max(1, self._shadow_trades_since_penalty)
        if shadow_wr >= 0.5:
            self._penalty_level = "none"
            return True
        return False

    @property
    def is_penalized(self) -> bool:
        return self._penalty_level != "none"


# ═══════════════════════════════════════════════════════════════════════════════
# StrategyIntelligenceHub — 多策略协调中心
# ═══════════════════════════════════════════════════════════════════════════════

class StrategyIntelligenceHub:
    """管理所有策略实例的智能体，负责跨策略协调。

    关键职责：
    1. 维护每个实例的独立绩效追踪
    2. 计算风险预算分配
    3. 评估跨策略相关性
    4. 提供健康加权的投票权重
    5. 管理康复流程
    """

    def __init__(self):
        self._trackers: Dict[str, InstancePerformanceTracker] = {}

    def get_or_create_tracker(self, instance_id: str, strategy_id: str) -> InstancePerformanceTracker:
        key = instance_id
        if key not in self._trackers:
            self._trackers[key] = InstancePerformanceTracker(instance_id, strategy_id)
        return self._trackers[key]

    def refresh_all(
        self,
        project_root: Path,
        active_instances: List[dict],
    ) -> Dict[str, InstanceHealth]:
        """从 agent_state.json 刷新所有活跃实例的绩效数据。

        Returns:
            {instance_id: InstanceHealth} 字典
        """
        all_closures = _load_recent_closures(project_root)
        results: Dict[str, InstanceHealth] = {}

        for inst in active_instances:
            instance_id = str(inst.get("instance_id", "") or "").strip()
            strategy_id = str(inst.get("strategy_id", "") or "").strip()
            if not instance_id:
                continue

            tracker = self.get_or_create_tracker(instance_id, strategy_id)
            tracker.ingest_recent_closures(all_closures)
            results[instance_id] = tracker.compute_health()

        return results

    def allocate_risk_budget(
        self,
        health_scores: Dict[str, InstanceHealth],
        total_budget_lots: float,
        min_lot: float = 0.01,
        max_lot_per_instance: float = 0.20,
    ) -> Dict[str, float]:
        """按健康评分分配风险预算。

        budget_i = (health_i^2 / Σ health^2) × total_budget

        使用平方加权让健康差距更大的策略获得更多预算，坏的策略获得更少。
        结果钳制在 [min_lot, max_lot_per_instance] 区间。
        """
        if not health_scores:
            return {}

        # Square weighting: health_i^2 / sum(health^2)
        squared = {iid: max(1.0, h.score) ** 2 for iid, h in health_scores.items()}
        total_sq = sum(squared.values())
        if total_sq <= 0:
            return {iid: min_lot for iid in health_scores}

        budget = {}
        for iid, sq in squared.items():
            share = sq / total_sq
            budget[iid] = round(max(min_lot, min(max_lot_per_instance, share * total_budget_lots)), 2)

        return budget

    def compute_voting_weights(
        self,
        health_scores: Dict[str, InstanceHealth],
        current_regime: str = "",
    ) -> Dict[str, float]:
        """计算健康加权投票权重，取代 binary entry_selection。

        vote_power_i = health_score_i × regime_fit_i

        归一化到总和为 1.0。
        health < 20 或 suggested_action == "shadow_only" 的实例权重为 0。
        """
        if not health_scores:
            return {}

        raw_weights: Dict[str, float] = {}
        for iid, h in health_scores.items():
            if h.suggested_action == "shadow_only":
                raw_weights[iid] = 0.0
            else:
                regime_mult = max(0.2, h.current_regime_fitness)
                raw_weights[iid] = max(0.0, h.score * regime_mult / 100.0)

        total = sum(raw_weights.values())
        if total <= 0:
            # All shadow_only — assign equal minimum
            n = max(1, len(raw_weights))
            return {iid: 1.0 / n for iid in raw_weights}

        return {iid: round(w / total, 4) for iid, w in raw_weights.items()}

    def evaluate_cross_strategy_correlation(
        self,
        strategy_signals: List[dict],
        health_scores: Dict[str, InstanceHealth],
    ) -> Dict[str, Any]:
        """评估跨策略信号相关性，检测分歧和共识。

        Returns:
            {
                "consensus": "ALIGNED" | "DIVERGENT" | "MIXED",
                "aligned_instances": [...],
                "divergent_instances": [...],
                "net_bias": "BUY" | "SELL" | "NONE",
                "confidence_adjustment": float,  # multiplier on min_confidence
                "volume_adjustment": float,      # multiplier on volume
                "cooldown_adjustment_sec": int,  # added cooldown seconds
            }
        """
        actionable = [
            s for s in strategy_signals
            if str(s.get("action", "HOLD") or "HOLD").upper() != "HOLD"
            and str(s.get("side", "NONE") or "NONE").upper() in {"BUY", "SELL"}
        ]

        if len(actionable) < 2:
            return {
                "consensus": "SINGLE_SIGNAL",
                "aligned_instances": [s.get("_instance_id", "") for s in actionable],
                "divergent_instances": [],
                "net_bias": str(actionable[0].get("side", "NONE")) if actionable else "NONE",
                "confidence_adjustment": 1.0,
                "volume_adjustment": 1.0,
                "cooldown_adjustment_sec": 0,
            }

        buy_signals = [s for s in actionable if str(s.get("side", "")).upper() == "BUY"]
        sell_signals = [s for s in actionable if str(s.get("side", "")).upper() == "SELL"]

        buy_health = sum(
            health_scores.get(s.get("_instance_id", ""), InstanceHealth("", "", 50, "C", 50, 50, 50, 50, 50, 0, 0, 0, 0, 0, "normal", 1.0, 0, "", 0, 0, 0.0)).score
            for s in buy_signals
        )
        sell_health = sum(
            health_scores.get(s.get("_instance_id", ""), InstanceHealth("", "", 50, "C", 50, 50, 50, 50, 50, 0, 0, 0, 0, 0, "normal", 1.0, 0, "", 0, 0, 0.0)).score
            for s in sell_signals
        )

        if buy_signals and not sell_signals:
            return {
                "consensus": "ALIGNED_BUY",
                "aligned_instances": [s.get("_instance_id", "") for s in buy_signals],
                "divergent_instances": [],
                "net_bias": "BUY",
                "confidence_adjustment": 1.0,
                "volume_adjustment": min(1.15, 1.0 + 0.05 * len(buy_signals)),
                "cooldown_adjustment_sec": -30,
            }
        if sell_signals and not buy_signals:
            return {
                "consensus": "ALIGNED_SELL",
                "aligned_instances": [s.get("_instance_id", "") for s in sell_signals],
                "divergent_instances": [],
                "net_bias": "SELL",
                "confidence_adjustment": 1.0,
                "volume_adjustment": min(1.15, 1.0 + 0.05 * len(sell_signals)),
                "cooldown_adjustment_sec": -30,
            }

        # Divergence: both BUY and SELL signals
        divergent_instances = [
            s.get("_instance_id", "") for s in actionable
        ]
        # Higher health side wins
        if buy_health > sell_health:
            net_bias = "BUY"
            bias_ratio = sell_health / max(1.0, buy_health)
        else:
            net_bias = "SELL"
            bias_ratio = buy_health / max(1.0, sell_health)

        return {
            "consensus": "DIVERGENT",
            "aligned_instances": [s.get("_instance_id", "") for s in (buy_signals if net_bias == "BUY" else sell_signals)],
            "divergent_instances": divergent_instances,
            "net_bias": net_bias,
            "confidence_adjustment": max(0.6, 1.0 - bias_ratio * 0.4),  # divergence tanks confidence
            "volume_adjustment": max(0.4, 1.0 - bias_ratio * 0.6),      # and reduces volume
            "cooldown_adjustment_sec": int(bias_ratio * 120),             # and adds cooldown
        }


# ═══════════════════════════════════════════════════════════════════════════════
# Helpers
# ═══════════════════════════════════════════════════════════════════════════════

def _load_recent_closures(project_root: Path) -> List[dict]:
    """从 trading_ledger.ndjson（唯一事实源）读取最近交易记录。

    回退：如果 ledger 不存在，尝试 agent_state.json 兼容旧数据。
    """
    # Primary: trading_ledger.ndjson (single source of truth)
    ledger_paths = [
        project_root / "logs" / "trading_ledger.ndjson",
        project_root.parent / "logs" / "trading_ledger.ndjson" if project_root.name != "logs" else None,
    ]
    for lp in ledger_paths:
        if lp and lp.exists():
            try:
                lines = lp.read_text(encoding="utf-8").strip().split("\n")
                rows = []
                for line in lines[-200:]:  # 最近200条
                    if not line.strip():
                        continue
                    try:
                        entry = json.loads(line)
                        if entry.get("action") == "CLOSE" and isinstance(entry, dict):
                            rows.append(entry)
                    except json.JSONDecodeError:
                        continue
                if rows:
                    return rows
            except Exception:
                pass

    # Fallback: agent_state.json (legacy)
    state_path = project_root / "data" / "bridge" / "state" / "agent_state.json"
    try:
        payload = json.loads(state_path.read_text(encoding="utf-8"))
    except Exception:
        return []
    rows = payload.get("recent_closures", []) if isinstance(payload, dict) else []
    return [row for row in rows if isinstance(row, dict)]


# ═══════════════════════════════════════════════════════════════════════════════
# Module-level hub singleton
# ═══════════════════════════════════════════════════════════════════════════════

_hub: Optional[StrategyIntelligenceHub] = None


def get_intelligence_hub() -> StrategyIntelligenceHub:
    global _hub
    if _hub is None:
        _hub = StrategyIntelligenceHub()
    return _hub
