From 80daa535588bc625f9541a2b3399a5719940359c Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 3 Jul 2026 16:14:51 +0900 Subject: [PATCH] =?UTF-8?q?feat(agent-office):=20=EB=A7=A4=EB=A7=A4?= =?UTF-8?q?=EC=95=8C=EB=9E=8C=EC=97=90=20=EC=A1=B0=EA=B1=B4=EB=B3=84=20'?= =?UTF-8?q?=EC=99=9C=20=EB=A7=A4=EC=88=98/=EB=A7=A4=EB=8F=84'=20=ED=95=9C?= =?UTF-8?q?=20=EC=A4=84=20=EA=B7=BC=EA=B1=B0(=F0=9F=92=A1)=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agent-office/app/notifiers/telegram_trade.py | 18 +++++++++++++++++- agent-office/tests/test_trade_alert_notify.py | 12 ++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/agent-office/app/notifiers/telegram_trade.py b/agent-office/app/notifiers/telegram_trade.py index 898d9b9..1ed0367 100644 --- a/agent-office/app/notifiers/telegram_trade.py +++ b/agent-office/app/notifiers/telegram_trade.py @@ -13,15 +13,31 @@ _COND_LABEL = { "sell_stop_loss": "손절", "sell_ma_break": "이평 이탈", "sell_take_profit": "익절", "sell_climax": "급등 소진", "sell_trailing_stop": "트레일링 스톱", } +# 조건별 "왜 이 시점에 매수/매도인가" 한 줄 근거 +_COND_REASON = { + "buy_ma20_pullback": "상승추세 중 MA20 지지선 눌림목 반등 — 저가 진입 기회", + "buy_breakout": "전고점·저항 돌파 + 거래량 증가 — 추세 상승 진입 신호", + "buy_rsi_bounce": "RSI 과매도(30↓)에서 반등 — 단기 낙폭과대 되돌림", + "sell_stop_loss": "평단 대비 손절선 도달 — 추가 하락 리스크 차단", + "sell_ma_break": "주요 이평선(MA50/200) 이탈 — 추세 훼손, 보유 재검토", + "sell_take_profit": "목표 수익 도달 — 이익 실현 구간", + "sell_climax": "거래량 급증 + 윗꼬리(고점 대비 하락 마감) — 분산·소진 의심", + "sell_trailing_stop":"보유기간 고점 대비 하락 — 수익 반납 방어(트레일링 스톱)", +} def format_trade_alert(a: Dict[str, Any]) -> str: kind = _KIND_LABEL.get(a["kind"], a["kind"]) cond = _COND_LABEL.get(a["condition"], a["condition"]) + reason = _COND_REASON.get(a["condition"], "") name = a.get("name") or a["ticker"] price = a.get("price") price_s = f"{int(price):,}원" if price else "-" - return f"{kind} 알람\n{name} ({a['ticker']})\n조건: {cond}\n현재가: {price_s}" + lines = [f"{kind} 알람", f"{name} ({a['ticker']})", f"조건: {cond}"] + if reason: + lines.append(f"💡 {reason}") + lines.append(f"현재가: {price_s}") + return "\n".join(lines) async def send_trade_alerts(alerts: List[Dict[str, Any]]) -> dict: diff --git a/agent-office/tests/test_trade_alert_notify.py b/agent-office/tests/test_trade_alert_notify.py index cf6844d..abe4d43 100644 --- a/agent-office/tests/test_trade_alert_notify.py +++ b/agent-office/tests/test_trade_alert_notify.py @@ -53,3 +53,15 @@ async def test_format_trade_alert_has_direction(): txt = format_trade_alert({"ticker": "005930", "name": "삼성전자", "kind": "sell", "condition": "sell_stop_loss", "price": 60000, "detail": {}}) assert "매도" in txt and "삼성전자" in txt + + +def test_format_trade_alert_includes_reason_line(): + """조건별 '왜 매수/매도해야 하는지' 한 줄 이유(💡)가 메시지에 포함된다.""" + from app.notifiers.telegram_trade import format_trade_alert + for cond in ("buy_breakout", "sell_stop_loss", "sell_trailing_stop"): + txt = format_trade_alert({"ticker": "005930", "name": "삼성전자", "kind": cond.split("_")[0], + "condition": cond, "price": 60000, "detail": {}}) + assert "💡" in txt, f"{cond}: 이유 한 줄 누락" + # 이유 라인이 조건 라벨을 그대로 반복하지 않고 실제 설명을 담아야 함 + reason_line = next(l for l in txt.split("\n") if l.startswith("💡")) + assert len(reason_line) > 6