From a05e6ba8ca136c11677cb47c3252d717eba30b9c Mon Sep 17 00:00:00 2001 From: gahusb Date: Wed, 13 May 2026 07:52:17 +0900 Subject: [PATCH] =?UTF-8?q?feat(stock-lab):=20=ED=85=94=EB=A0=88=EA=B7=B8?= =?UTF-8?q?=EB=9E=A8=20=EB=85=B8=EB=93=9C=20=ED=92=80=20=EB=9D=BC=EB=B2=A8?= =?UTF-8?q?=20+=20=EC=9B=90=20=EB=8B=A8=EC=9C=84=20=ED=91=9C=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์•„์ด์ฝ˜(๐Ÿ‘ค์™ธ/๐Ÿ†™๊ณ /...) ์ œ๊ฑฐํ•˜๊ณ  ํ’€ ํ•œ๊ธ€ ๋ผ๋ฒจ๋กœ ๋ณ€๊ฒฝ (์™ธ๊ตญ์ธ/๊ฑฐ๋ž˜๋Ÿ‰๊ธ‰์ฆ/20์ผ๋ชจ๋ฉ˜ํ…€/52์ฃผ์‹ ๊ณ ๊ฐ€/RS๋ ˆ์ดํŒ…/์ดํ‰์„ ์ •๋ฐฐ์—ด/VCP์ˆ˜์ถ•) - ๊ฐ€๊ฒฉ์€ "103,917์›" ํ˜•ํƒœ๋กœ ์› ๋‹จ์œ„ ๋ช…์‹œ - ํ™œ์„ฑ ๋…ธ๋“œ ์—†์„ ๋•Œ fallback ๋ฌธ๊ตฌ - ํ…Œ์ŠคํŠธ๋„ ์ƒˆ ํฌ๋งท์œผ๋กœ ๊ฐฑ์‹  + ์› ๋‹จ์œ„ ๊ฒ€์ฆ ์‹ ๊ทœ ์ผ€์ด์Šค --- stock-lab/app/screener/telegram.py | 40 +++++++++++++++---------- stock-lab/app/test_screener_telegram.py | 35 ++++++++++++++++------ 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/stock-lab/app/screener/telegram.py b/stock-lab/app/screener/telegram.py index 842bb69..522a35f 100644 --- a/stock-lab/app/screener/telegram.py +++ b/stock-lab/app/screener/telegram.py @@ -4,14 +4,15 @@ from __future__ import annotations import datetime as dt -NODE_ICONS = { - "foreign_buy": "๐Ÿ‘ค์™ธ", - "volume_surge": "โšก๊ฑฐ", - "momentum": "๐Ÿš€๋ชจ", - "high52w": "๐Ÿ†™๊ณ ", - "rs_rating": "๐Ÿ’ชRS", - "ma_alignment": "๐Ÿ“ˆMA", - "vcp_lite": "๐ŸŒ€VCP", +# ๋…ธ๋“œ๋ณ„ ํ’€ ๋ผ๋ฒจ (์•„์ด์ฝ˜ ๋Œ€์‹  ์‚ฌ์šฉ โ€” ์‚ฌ์šฉ์ž๊ฐ€ ๋ช…ํ™•ํ•œ ์ด๋ฆ„ ์„ ํ˜ธ) +NODE_LABELS = { + "foreign_buy": "์™ธ๊ตญ์ธ", + "volume_surge": "๊ฑฐ๋ž˜๋Ÿ‰๊ธ‰์ฆ", + "momentum": "20์ผ๋ชจ๋ฉ˜ํ…€", + "high52w": "52์ฃผ์‹ ๊ณ ๊ฐ€", + "rs_rating": "RS๋ ˆ์ดํŒ…", + "ma_alignment": "์ดํ‰์„ ์ •๋ฐฐ์—ด", + "vcp_lite": "VCP์ˆ˜์ถ•", } PAGE_BASE = "https://gahusb.synology.me/stock/screener" @@ -25,9 +26,21 @@ def _escape_md(s: str) -> str: def _format_won(n) -> str: + """1,234,567์› ํ˜•ํƒœ (None ์‹œ '-').""" if n is None: - return "-" - return f"{int(n):,}" + return "\\-" + return f"{int(n):,}์›" + + +def _format_active_nodes(scores: dict, threshold: int = 70) -> str: + """70์  ์ด์ƒ ๋…ธ๋“œ๋ฅผ '๋ผ๋ฒจ ์ ์ˆ˜' ํ˜•ํƒœ๋กœ ๋‚˜์—ด, ์ฝค๋งˆ ๊ตฌ๋ถ„.""" + active = [] + for name, sc in scores.items(): + label = NODE_LABELS.get(name) + if label is None or sc < threshold: + continue + active.append(f"{_escape_md(label)} {int(sc)}") + return " ยท ".join(active) if active else "\\(70์  ์ด์ƒ ๋…ธ๋“œ ์—†์Œ\\)" def build_telegram_payload(asof: dt.date, mode: str, survivors_count: int, @@ -40,17 +53,14 @@ def build_telegram_payload(asof: dt.date, mode: str, survivors_count: int, lines = [] for r in rows[:10]: - icons = " ".join( - NODE_ICONS[name] for name, sc in r["scores"].items() - if sc >= 70 and name in NODE_ICONS - ) + nodes_str = _format_active_nodes(r.get("scores", {})) score_str = f"{r['total_score']:.1f}" r_pct = r.get("r_pct") r_pct_str = f"{r_pct:.1f}" if r_pct is not None else "-" lines.append( f"{r['rank']}\\. *{_escape_md(r['name'])}* `{r['ticker']}` " f"โญ {_escape_md(score_str)}\n" - f" {icons}\n" + f" {nodes_str}\n" f" ์ง„์ž… {_format_won(r.get('entry_price'))} " f"์†์ ˆ {_format_won(r.get('stop_price'))} " f"์ต์ ˆ {_format_won(r.get('target_price'))} " diff --git a/stock-lab/app/test_screener_telegram.py b/stock-lab/app/test_screener_telegram.py index 606b459..1d5761d 100644 --- a/stock-lab/app/test_screener_telegram.py +++ b/stock-lab/app/test_screener_telegram.py @@ -31,7 +31,7 @@ def test_build_payload_includes_top10_and_link(): assert "42" in text # run_id ๋งํฌ -def test_score_threshold_filters_icons(): +def test_score_threshold_filters_node_labels(): rows = [{ "rank": 1, "ticker": "A", "name": "A์ฃผ", "total_score": 80, @@ -41,11 +41,28 @@ def test_score_threshold_filters_icons(): "target_price": 53750, "r_pct": 3.5, }] p = build_telegram_payload(dt.date(2026, 5, 12), "auto", 100, 1, rows, run_id=1) - # foreign_buy(90), momentum(70), rs_rating(80), ma_alignment(80) ๋งŒ ํ‘œ์‹œ (โ‰ฅ70) - assert "๐Ÿ‘ค์™ธ" in p["text"] - assert "๐Ÿš€๋ชจ" in p["text"] - assert "๐Ÿ’ชRS" in p["text"] - assert "๐Ÿ“ˆMA" in p["text"] - assert "โšก๊ฑฐ" not in p["text"] - assert "๐Ÿ†™๊ณ " not in p["text"] - assert "๐ŸŒ€VCP" not in p["text"] + text = p["text"] + # โ‰ฅ70 ๋…ธ๋“œ๋งŒ ํ’€ ๋ผ๋ฒจ๋กœ ํ‘œ์‹œ (foreign_buy=90, momentum=70, rs_rating=80, ma_alignment=80) + assert "์™ธ๊ตญ์ธ 90" in text + assert "20์ผ๋ชจ๋ฉ˜ํ…€ 70" in text + assert "RS๋ ˆ์ดํŒ… 80" in text + assert "์ดํ‰์„ ์ •๋ฐฐ์—ด 80" in text + # <70 ๋…ธ๋“œ๋Š” ์ˆจ๊น€ (volume_surge=50, high52w=30, vcp_lite=60) + assert "๊ฑฐ๋ž˜๋Ÿ‰๊ธ‰์ฆ" not in text + assert "52์ฃผ์‹ ๊ณ ๊ฐ€" not in text + assert "VCP์ˆ˜์ถ•" not in text + + +def test_prices_have_won_suffix(): + rows = [{ + "rank": 1, "ticker": "A", "name": "A์ฃผ", + "total_score": 80, + "scores": {"foreign_buy": 80}, + "close": 50000, "entry_price": 50250, "stop_price": 48500, + "target_price": 53750, "r_pct": 3.5, + }] + p = build_telegram_payload(dt.date(2026, 5, 12), "auto", 100, 1, rows, run_id=1) + text = p["text"] + assert "50,250์›" in text + assert "48,500์›" in text + assert "53,750์›" in text