from __future__ import annotations from typing import Any FREQUENCY_VALIDITY_DAYS = { "每日": 1, "每日9:25": 1, "每周": 5, "双周": 10, "月度": 20, "事件驱动": 5, } def signal_validity(mode: str, formula: dict[str, Any] | None) -> dict[str, Any]: if mode == "smart": return { "type": "until_regime_change", "label": "当前阶段不变时有效", } meta = (formula or {}).get("meta") or {} frequency = str(meta.get("frequency") or "每日") days = FREQUENCY_VALIDITY_DAYS.get(frequency, 1) return { "type": "trading_days", "days": days, "label": f"{days}个交易日", } def attach_strategy_validity(strategy: dict[str, Any]) -> None: formula = strategy.get("formula") or {} meta = formula.setdefault("meta", {}) mode = "curated" if meta.get("library") == "curated" else "smart" meta["signal_validity"] = signal_validity(mode, formula) def build_candidate_archive( runs: list[dict[str, Any]], strategies: list[dict[str, Any]], trading_dates: list[str], as_of_date: str, as_of_regime: str, history_limit: int = 1200, ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: strategy_formulas = { str(item.get("name") or ""): item.get("formula") or {} for item in strategies } date_positions = {trade_date: index for index, trade_date in enumerate(trading_dates)} as_of_position = date_positions.get(as_of_date, len(trading_dates) - 1) history: dict[tuple[str, str, str], dict[str, Any]] = {} active: dict[tuple[str, str], dict[str, Any]] = {} ordered_runs = sorted( runs, key=lambda item: ( str((item.get("meta") or {}).get("trade_date") or ""), int((item.get("meta") or {}).get("run_id") or 0), ), reverse=True, ) for result in ordered_runs: meta = result.get("meta") or {} mode = str(meta.get("mode") or "smart") if mode not in {"smart", "curated", "quant"}: continue selection_date = str(meta.get("trade_date") or "").replace("-", "") strategy_name = str(meta.get("strategy_name") or "未命名策略") regime = str(meta.get("regime") or "") formula = result.get("formula") or strategy_formulas.get(strategy_name) or {} validity = signal_validity(mode, formula) valid, valid_until, remaining = _signal_state( validity, selection_date, regime, trading_dates, date_positions, as_of_position, as_of_regime, ) hit = { "selection_date": selection_date, "strategy_name": strategy_name, "regime": regime, "run_id": int(meta.get("run_id") or 0), "validity": validity, "valid_until": valid_until, "remaining_trading_days": remaining, "active": valid, } for candidate in result.get("candidates") or []: code = str(candidate.get("code") or "") if not code: continue history_key = (mode, selection_date, code) history_row = history.setdefault( history_key, _archive_row(candidate, mode, selection_date), ) candidate_hit = {**hit, "score_display": candidate.get("score_display")} _append_hit(history_row, candidate_hit) if valid: active_key = (mode, code) active_row = active.get(active_key) if active_row is None: active_row = _archive_row(candidate, mode, selection_date) active[active_key] = active_row _append_hit(active_row, candidate_hit) history_rows = sorted( history.values(), key=lambda item: (item["selection_date"], _numeric_score(item["score_display"])), reverse=True, )[: max(1, int(history_limit))] active_rows = sorted( active.values(), key=lambda item: (item["selection_date"], _numeric_score(item["score_display"])), reverse=True, ) for row in [*history_rows, *active_rows]: _finalize_archive_row(row) return active_rows, history_rows def _signal_state( validity: dict[str, Any], selection_date: str, regime: str, trading_dates: list[str], date_positions: dict[str, int], as_of_position: int, as_of_regime: str, ) -> tuple[bool, str, int | None]: if validity.get("type") == "until_regime_change": return regime == as_of_regime, "", None days = max(1, int(validity.get("days") or 1)) selected_position = date_positions.get(selection_date) if selected_position is None or as_of_position < selected_position: return False, "", 0 elapsed = as_of_position - selected_position valid = elapsed < days valid_position = selected_position + days - 1 valid_until = ( trading_dates[valid_position] if 0 <= valid_position < len(trading_dates) else "" ) return valid, valid_until, max(0, days - elapsed) if valid else 0 def _archive_row( candidate: dict[str, Any], mode: str, selection_date: str ) -> dict[str, Any]: return { "mode": mode, "selection_date": selection_date, "code": str(candidate.get("code") or ""), "name": str(candidate.get("name") or ""), "sector": str(candidate.get("sector") or ""), "score_display": candidate.get("score_display"), "pct_chg": candidate.get("pct_chg"), "return_5d": candidate.get("return_5d"), "hits": [], } def _append_hit(row: dict[str, Any], hit: dict[str, Any]) -> None: identity = (hit["strategy_name"], hit["regime"], hit["run_id"]) existing = { (item["strategy_name"], item["regime"], item["run_id"]) for item in row["hits"] } if identity not in existing: row["hits"].append(dict(hit)) def _finalize_archive_row(row: dict[str, Any]) -> None: hits = row.get("hits") or [] active_hits = [item for item in hits if item.get("active")] row["matched_strategies"] = list( dict.fromkeys(item["strategy_name"] for item in hits) ) row["regimes"] = list(dict.fromkeys(item["regime"] for item in hits if item["regime"])) row["active"] = bool(active_hits) row["status"] = "持续有效" if active_hits else "已到期" labels = list( dict.fromkeys(item["validity"]["label"] for item in (active_hits or hits)) ) row["validity_label"] = " / ".join(labels) def _numeric_score(value: Any) -> float: try: return float(value) except (TypeError, ValueError): return -1.0