feat: complete heaven readings and screener publication
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
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
|
||||
Reference in New Issue
Block a user