199 lines
6.7 KiB
Python
199 lines
6.7 KiB
Python
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
|