Files
sanguo_vnpy_v2/sanguo_factor/decay.py
T

99 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# sanguo_factor/decay.py
"""衰减判定引擎——流水线 spec §4.2 决议 E/F/C.
三条件(起步默认+首年校准,数值措辞按 spec):
① 相对降幅: 滚动 12M IC t 值 < 晋级时 t 值 × RELATIVE_DROP
(各因子晋级基准不同,一刀切绝对值不公平);
② 绝对地板: 同时 t < ABSOLUTE_FLOOR(防「从很强掉到还算强」也叫衰减);
③ 连续性: 连续 CONSECUTIVE_MONTHS 个月满足①②才开告警(state=alert);
第 1~2 个月标黄预警(state=yellow)——月度 IC 一年 12 个点,单月噪声大.
成熟度门(决议 E): promotion_t 为 None(未晋级)→不判定(state=not_promoted).
集体水位(决议 C): 全体已晋级因子当月 t 中位数 < COLLECTIVE_MEDIAN_FLOOR
→ is_collective_decay=True——消费方(月度批评)据此出研判卡数据,
不开降权告警,因子权重冻结不动.阈值只负责叫人,处置永远人选.
"""
from __future__ import annotations
from dataclasses import dataclass
from statistics import median
RELATIVE_DROP = 0.5 # 起步默认,首年校准(决议 F)
ABSOLUTE_FLOOR = 2.0
CONSECUTIVE_MONTHS = 3 # 第 3 个连续月开告警
COLLECTIVE_MEDIAN_FLOOR = 1.0 # 集体水位地板,起步默认首年校准(决议 C)
@dataclass(frozen=True)
class DecayVerdict:
factor: str
state: str # ok|yellow|alert|not_promoted|no_data
months_consecutive: int # 尾部连续满足①②的月数(含当月)
current_t: float | None
promotion_t: float | None
ratio: float | None # current_t/promotion_t
def _next_month(month: str) -> str:
"""'2026-07' → '2026-08'(连续性判定用)."""
y, m = int(month[:4]), int(month[5:7])
y, m = (y + 1, 1) if m == 12 else (y, m + 1)
return f"{y:04d}-{m:02d}"
def _prev_month(month: str) -> str:
"""'2026-08' → '2026-07'."""
y, m = int(month[:4]), int(month[5:7])
y, m = (y - 1, 12) if m == 1 else (y, m - 1)
return f"{y:04d}-{m:02d}"
def _last_t(points: list[dict]) -> float | None:
for p in reversed(points):
if p.get("t") is not None:
return float(p["t"])
return None
def judge_factor(monthly_points: list[dict], promotion_t: float | None,
factor: str = "") -> DecayVerdict:
"""monthly_points: [{"month": "2026-05", "t": 3.1}, ...] 按月升序,t 可为 None(缺).
从序列尾部往回数「日历月连续且满足①②」的月数;断月/不满足即停.
"""
if promotion_t is None:
return DecayVerdict(factor, "not_promoted", 0,
_last_t(monthly_points), None, None)
points = [p for p in monthly_points if p.get("t") is not None]
if not points:
return DecayVerdict(factor, "no_data", 0, None, promotion_t, None)
current_t = float(points[-1]["t"])
ratio = current_t / promotion_t if promotion_t else None
streak = 0
for i in range(len(points) - 1, -1, -1):
p = points[i]
weak = p["t"] < promotion_t * RELATIVE_DROP and p["t"] < ABSOLUTE_FLOOR
if not weak:
break
if streak > 0:
later = points[i + 1] # 已计入 streak 的上一个(更晚)点
if later["month"] != _next_month(p["month"]):
break # 断月:序列不日历连续
streak += 1
state = "ok"
if streak >= CONSECUTIVE_MONTHS:
state = "alert"
elif streak > 0:
state = "yellow"
return DecayVerdict(factor, state, streak, current_t, promotion_t, ratio)
def collective_level(current_ts: list[float]) -> dict:
"""集体水位:当月全体已晋级因子 t 的中位数(决议 C).空表不报警."""
if not current_ts:
return {"median": None, "floor": COLLECTIVE_MEDIAN_FLOOR,
"is_collective_decay": False, "n": 0}
med = median(current_ts)
return {"median": med, "floor": COLLECTIVE_MEDIAN_FLOOR,
"is_collective_decay": med < COLLECTIVE_MEDIAN_FLOOR,
"n": len(current_ts)}