feat(stock-lab): 텔레그램 노드 풀 라벨 + 원 단위 표기

- 아이콘(👤외/🆙고/...) 제거하고 풀 한글 라벨로 변경
  (외국인/거래량급증/20일모멘텀/52주신고가/RS레이팅/이평선정배열/VCP수축)
- 가격은 "103,917원" 형태로 원 단위 명시
- 활성 노드 없을 때 fallback 문구
- 테스트도 새 포맷으로 갱신 + 원 단위 검증 신규 케이스
This commit is contained in:
2026-05-13 07:52:17 +09:00
parent 4a333434ac
commit a05e6ba8ca
2 changed files with 51 additions and 24 deletions

View File

@@ -4,14 +4,15 @@ from __future__ import annotations
import datetime as dt import datetime as dt
NODE_ICONS = { # 노드별 풀 라벨 (아이콘 대신 사용 — 사용자가 명확한 이름 선호)
"foreign_buy": "👤외", NODE_LABELS = {
"volume_surge": "⚡거", "foreign_buy": "외국인",
"momentum": "🚀모", "volume_surge": "거래량급증",
"high52w": "🆙고", "momentum": "20일모멘텀",
"rs_rating": "💪RS", "high52w": "52주신고가",
"ma_alignment": "📈MA", "rs_rating": "RS레이팅",
"vcp_lite": "🌀VCP", "ma_alignment": "이평선정배열",
"vcp_lite": "VCP수축",
} }
PAGE_BASE = "https://gahusb.synology.me/stock/screener" PAGE_BASE = "https://gahusb.synology.me/stock/screener"
@@ -25,9 +26,21 @@ def _escape_md(s: str) -> str:
def _format_won(n) -> str: def _format_won(n) -> str:
"""1,234,567원 형태 (None 시 '-')."""
if n is None: if n is None:
return "-" return "\\-"
return f"{int(n):,}" 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, 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 = [] lines = []
for r in rows[:10]: for r in rows[:10]:
icons = " ".join( nodes_str = _format_active_nodes(r.get("scores", {}))
NODE_ICONS[name] for name, sc in r["scores"].items()
if sc >= 70 and name in NODE_ICONS
)
score_str = f"{r['total_score']:.1f}" score_str = f"{r['total_score']:.1f}"
r_pct = r.get("r_pct") r_pct = r.get("r_pct")
r_pct_str = f"{r_pct:.1f}" if r_pct is not None else "-" r_pct_str = f"{r_pct:.1f}" if r_pct is not None else "-"
lines.append( lines.append(
f"{r['rank']}\\. *{_escape_md(r['name'])}* `{r['ticker']}` " f"{r['rank']}\\. *{_escape_md(r['name'])}* `{r['ticker']}` "
f"{_escape_md(score_str)}\n" 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('entry_price'))} "
f"손절 {_format_won(r.get('stop_price'))} " f"손절 {_format_won(r.get('stop_price'))} "
f"익절 {_format_won(r.get('target_price'))} " f"익절 {_format_won(r.get('target_price'))} "

View File

@@ -31,7 +31,7 @@ def test_build_payload_includes_top10_and_link():
assert "42" in text # run_id 링크 assert "42" in text # run_id 링크
def test_score_threshold_filters_icons(): def test_score_threshold_filters_node_labels():
rows = [{ rows = [{
"rank": 1, "ticker": "A", "name": "A주", "rank": 1, "ticker": "A", "name": "A주",
"total_score": 80, "total_score": 80,
@@ -41,11 +41,28 @@ def test_score_threshold_filters_icons():
"target_price": 53750, "r_pct": 3.5, "target_price": 53750, "r_pct": 3.5,
}] }]
p = build_telegram_payload(dt.date(2026, 5, 12), "auto", 100, 1, rows, run_id=1) 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) text = p["text"]
assert "👤외" in p["text"] # ≥70 노드만 풀 라벨로 표시 (foreign_buy=90, momentum=70, rs_rating=80, ma_alignment=80)
assert "🚀모" in p["text"] assert "외국인 90" in text
assert "💪RS" in p["text"] assert "20일모멘텀 70" in text
assert "📈MA" in p["text"] assert "RS레이팅 80" in text
assert "⚡거" not in p["text"] assert "이평선정배열 80" in text
assert "🆙고" not in p["text"] # <70 노드는 숨김 (volume_surge=50, high52w=30, vcp_lite=60)
assert "🌀VCP" not in p["text"] 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