feat(stock): /api/stock/trade-alerts 응답에 조건별 근거(reason) 문자열 추가
FE가 detail(객체)을 그대로 렌더해 React #31 크래시 → 안전한 텍스트 필드 제공. condition_reason(조건→한줄근거), agent-office telegram_trade._COND_REASON과 동일 유지.
This commit is contained in:
@@ -775,8 +775,11 @@ def delete_watchlist_item(ticker: str):
|
||||
|
||||
@app.get("/api/stock/trade-alerts")
|
||||
def list_trade_alerts(days: int = 7):
|
||||
"""매매 알람 이력 조회 (최근 N일)"""
|
||||
return {"alerts": get_alert_history(days)}
|
||||
"""매매 알람 이력 조회 (최근 N일). 조건별 근거(reason) 문자열 포함 — FE가 detail 객체 대신 렌더."""
|
||||
alerts = get_alert_history(days)
|
||||
for a in alerts:
|
||||
a["reason"] = trade_alerts.condition_reason(a.get("condition", ""))
|
||||
return {"alerts": alerts}
|
||||
|
||||
|
||||
# --- Holdings Intelligence API ---
|
||||
|
||||
@@ -36,6 +36,24 @@ DEFAULT_EXIT_PARAMS = {"stop_pct": 0.08, "take_pct": 0.25, "trailing_pct": 0.10,
|
||||
"climax_vol_x": 3.0, "climax_close_pct": 0.97}
|
||||
DEFAULT_BUY_PARAMS = {"rsi_oversold": 30, "breakout_vol_mult": 1.5, "pullback_pct": 0.02}
|
||||
|
||||
# 조건별 "왜 매수/매도인가" 한 줄 근거 — agent-office `telegram_trade._COND_REASON`과 동일 유지(sync).
|
||||
# API(/api/stock/trade-alerts)가 이 문자열을 반환해 FE가 detail(객체) 대신 안전하게 렌더한다.
|
||||
_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 condition_reason(condition: str) -> str:
|
||||
"""조건 키 → 사람이 읽는 근거 한 줄. 미지의 조건이면 조건 키 그대로."""
|
||||
return _COND_REASON.get(condition, condition)
|
||||
|
||||
|
||||
def latest_screener_candidates(conn) -> list:
|
||||
"""최신 성공(status='success') screener run의 후보 {ticker,name} 목록."""
|
||||
|
||||
@@ -70,6 +70,17 @@ def test_report_refire_after_cooldown_alerts(client, monkeypatch):
|
||||
assert r.json()["new_alerts"] == 1
|
||||
|
||||
|
||||
def test_trade_alerts_history_includes_reason_string(client):
|
||||
"""이력 API가 조건별 사람이 읽는 근거(reason) 문자열을 함께 반환한다(FE가 detail 객체 대신 렌더)."""
|
||||
firing = [{"ticker": "005930", "name": "삼성", "kind": "sell",
|
||||
"condition": "sell_ma_break", "price": 60000,
|
||||
"detail": {"ma50": 1, "ma200": 2, "severity": "high"}}]
|
||||
with patch("app.trade_alerts.notify_agent_office", return_value=True):
|
||||
_report(client, firing)
|
||||
alerts = client.get("/api/stock/trade-alerts?days=1").json()["alerts"]
|
||||
assert isinstance(alerts[0]["reason"], str) and len(alerts[0]["reason"]) > 3
|
||||
|
||||
|
||||
def test_report_resolves_stock_name_from_watchlist(client):
|
||||
"""워커 firing에 name이 없어도 NAS가 종목명을 해석해 알림에 포함한다."""
|
||||
from app import db
|
||||
|
||||
Reference in New Issue
Block a user