41 lines
1.4 KiB
Python
41 lines
1.4 KiB
Python
"""VCP-lite — 단기/장기 일중 변동성 비율 기반 수축률."""
|
|
|
|
import pandas as pd
|
|
|
|
from .base import ScoreNode, percentile_rank
|
|
|
|
|
|
class VcpLite(ScoreNode):
|
|
name = "vcp_lite"
|
|
label = "VCP-lite (변동성 수축)"
|
|
default_params = {"short_window": 40, "long_window": 252}
|
|
param_schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"short_window": {"type": "integer", "minimum": 10, "maximum": 120, "default": 40},
|
|
"long_window": {"type": "integer", "minimum": 60, "maximum": 504, "default": 252},
|
|
},
|
|
}
|
|
|
|
def compute(self, ctx, params: dict) -> pd.Series:
|
|
short_w = int(params.get("short_window", 40))
|
|
long_w = int(params.get("long_window", 252))
|
|
prices = ctx.prices
|
|
if prices.empty:
|
|
return pd.Series(dtype=float)
|
|
|
|
ordered = prices.sort_values("date").copy()
|
|
ordered["range_pct"] = (ordered["high"] - ordered["low"]) / ordered["close"]
|
|
|
|
def _ratio(s: pd.Series) -> float:
|
|
if len(s) < long_w:
|
|
return float("nan")
|
|
short_vol = s.tail(short_w).mean()
|
|
long_vol = s.tail(long_w).mean()
|
|
if long_vol == 0 or pd.isna(long_vol):
|
|
return float("nan")
|
|
return 1 - (short_vol / long_vol)
|
|
|
|
raw = ordered.groupby("ticker", group_keys=False)["range_pct"].apply(_ratio)
|
|
return percentile_rank(raw).fillna(50.0)
|