From 4eaeea98335c16baa0b2dabb626490c26c881804 Mon Sep 17 00:00:00 2001 From: gahusb Date: Tue, 12 May 2026 08:59:55 +0900 Subject: [PATCH] =?UTF-8?q?feat(stock-lab):=20High52WProximity=20=EB=85=B8?= =?UTF-8?q?=EB=93=9C=20=E2=80=94=20=EC=8B=A0=EA=B3=A0=EA=B0=80=20=EB=8C=80?= =?UTF-8?q?=EB=B9=84=20=EA=B7=BC=EC=A0=91=EB=8F=84=20=EB=A3=B0=20=EC=A0=90?= =?UTF-8?q?=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- stock-lab/app/screener/nodes/high52w.py | 30 ++++++++++++++++++ stock-lab/app/test_screener_nodes_high52w.py | 32 ++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 stock-lab/app/screener/nodes/high52w.py create mode 100644 stock-lab/app/test_screener_nodes_high52w.py diff --git a/stock-lab/app/screener/nodes/high52w.py b/stock-lab/app/screener/nodes/high52w.py new file mode 100644 index 0000000..9b6f86d --- /dev/null +++ b/stock-lab/app/screener/nodes/high52w.py @@ -0,0 +1,30 @@ +"""52주 신고가 근접도 (룰 기반: 70% 미만 0점, 100% 도달 100점, 선형).""" + +import pandas as pd + +from .base import ScoreNode + + +class High52WProximity(ScoreNode): + name = "high52w" + label = "52주 신고가 근접도" + default_params = {"window_days": 252} + param_schema = { + "type": "object", + "properties": { + "window_days": {"type": "integer", "minimum": 60, "maximum": 504, "default": 252} + }, + } + + def compute(self, ctx, params: dict) -> pd.Series: + window = int(params.get("window_days", 252)) + prices = ctx.prices + if prices.empty: + return pd.Series(dtype=float) + + ordered = prices.sort_values("date") + last = ordered.groupby("ticker").tail(window) + agg = last.groupby("ticker").agg(close=("close", "last"), high=("high", "max")) + proximity = (agg["close"] / agg["high"]).clip(upper=1.0) + score = ((proximity - 0.7) / 0.3).clip(lower=0.0, upper=1.0) * 100.0 + return score.fillna(0.0) diff --git a/stock-lab/app/test_screener_nodes_high52w.py b/stock-lab/app/test_screener_nodes_high52w.py new file mode 100644 index 0000000..70f0ff1 --- /dev/null +++ b/stock-lab/app/test_screener_nodes_high52w.py @@ -0,0 +1,32 @@ +import datetime as dt +import pandas as pd + +from app.screener.engine import ScreenContext +from app.screener.nodes.high52w import High52WProximity +from app.screener._test_fixtures import make_master, make_prices, make_flow + + +def _ctx(master, prices, flow): + return ScreenContext(master=master, prices=prices, flow=flow, + kospi=pd.Series(dtype=float, name="kospi"), + asof=dt.date(2026, 5, 12)) + + +def test_proximity_at_high_returns_100(): + asof = dt.date(2026, 5, 12) + master = make_master(["A"]) + prices = make_prices(["A"], days=260, asof=asof, trend_pct=0.05) + flow = make_flow(["A"], days=260, asof=asof) + + out = High52WProximity().compute(_ctx(master, prices, flow), {"window_days": 252}) + assert out["A"] >= 95 + + +def test_proximity_below_70pct_returns_0(): + asof = dt.date(2026, 5, 12) + master = make_master(["A"]) + prices = make_prices(["A"], days=260, asof=asof, start_close=100000, trend_pct=-0.5) + flow = make_flow(["A"], days=260, asof=asof) + + out = High52WProximity().compute(_ctx(master, prices, flow), {"window_days": 252}) + assert out["A"] == 0