From 5c4803b339bfca2e0ac3fb9a89433a3fd9574f4d Mon Sep 17 00:00:00 2001 From: shuim Date: Wed, 7 Oct 2026 11:06:08 +0900 Subject: [PATCH] =?UTF-8?q?fix(journal):=20=EC=8B=9C=EA=B0=84=EC=86=90?= =?UTF-8?q?=EC=A0=88=EC=9D=84=20=EC=86=90=EC=A0=88=EA=B3=BC=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC(time=5Fstop)=20+=20=EC=B2=AD=EC=82=B0=20=EC=84=A4?= =?UTF-8?q?=EB=AA=85=20=EC=9E=84=EA=B3=84=EA=B0=92=EC=9D=84=20=EC=8B=A4?= =?UTF-8?q?=ED=9A=A8=20=EC=84=A4=EC=A0=95=EA=B0=92=EC=9C=BC=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _parse_exit_type: '시간 손절'/'시간손절'/time_stop → time_stop (이전엔 '손절' 글자로 stop_loss 로 분류돼 -0.1% 시간손절이 '-6% 손절 도달'로 표시되고 손절 비율 인사이트가 부풀었다) - 일지 청산 설명의 하드코딩(-6%, RSI 65+, 1.5×ATR)을 settings 실효값으로(§4) - 분석 화면 청산 유형 한글 표시명 Co-Authored-By: Claude Opus 5.5 --- backend/app/services/trade_analyzer.py | 23 +++++++++++++--- .../tests/test_journal_time_stop_20261007.py | 27 +++++++++++++++++++ frontend/src/pages/Analysis.tsx | 15 ++++++++++- 3 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 backend/tests/test_journal_time_stop_20261007.py diff --git a/backend/app/services/trade_analyzer.py b/backend/app/services/trade_analyzer.py index da5aba1..68bc3c4 100644 --- a/backend/app/services/trade_analyzer.py +++ b/backend/app/services/trade_analyzer.py @@ -71,6 +71,10 @@ def _parse_exit_type(exit_reason: Optional[str]) -> str: if not exit_reason: return "unknown" reason = exit_reason.lower() + # 시간손절(DT "시간 손절: N분 경과…", 스윙 "시간손절 …")은 손실 한도 도달이 아니라 + # 보유 시간 초과 후 조기 정리다 — "손절" 글자만 보고 stop_loss 로 묶지 않는다(2026-10-07). + if "time_stop" in reason or "시간 손절" in reason or "시간손절" in reason: + return "time_stop" if "stop_loss" in reason or "손절" in reason: return "stop_loss" if "trailing" in reason or "트레일링" in reason: @@ -777,10 +781,23 @@ def generate_journal_for_trade(trade: TradeHistory, perf_context: Optional[Dict] result_comment = "손절 처리되었습니다. 진입 조건 및 시장 국면을 재검토하세요." # 청산 유형별 코멘트 + # 임계값은 하드코딩하지 않고 실효 설정값을 쓴다(CLAUDE.md §4). + from app.core.config import settings as _s exit_comments = { - "stop_loss": "→ 손절 기준(-6% 또는 ATR 기반)에 도달하여 강제 청산.", - "trailing_stop": "→ 트레일링 스탑(고점-1.5×ATR)에 도달. 고점 대비 일정 비율 하락.", - "take_profit": "→ 익절 기준(+2R 또는 RSI 65+)에 도달하여 수익 확정.", + "stop_loss": ( + f"→ 손절 기준(스윙 -{abs(float(_s.STOP_LOSS_PERCENT)):g}% / 고변동 -{abs(float(_s.STOP_LOSS_HIGH_VOL)):g}%, " + f"ATR×{float(_s.ATR_STOP_MULTIPLIER):g}, 데이트레이딩은 진입 시 손절가)에 도달하여 청산." + ), + "time_stop": ( + f"→ 시간손절: 데이트레이딩은 진입 {int(_s.DAY_TRADE_TIME_STOP_MINUTES)}분 경과 후 수익률 " + f"{float(_s.DAY_TRADE_TIME_STOP_MIN_GAIN) * 100:+.1f}% 미만, 스윙은 장기 보유 수익 미달로 조기 정리. " + "손실 한도 도달이 아니다." + ), + "trailing_stop": f"→ 트레일링 스탑(고점-{float(_s.ATR_TRAILING_MULTIPLIER):g}×ATR)에 도달. 고점 대비 일정 비율 하락.", + "take_profit": ( + f"→ 익절 기준(RSI {float(_s.MEAN_REVERSION_RSI_EXIT_THRESHOLD):g}+ 1차 / " + f"{float(_s.MEAN_REVERSION_RSI_FULL_EXIT):g}+ 전량, 데이트레이딩 목표가)에 도달하여 수익 확정." + ), "eod_forced": "→ 장 마감 강제 청산(15:20 EOD). 포지션 오버나이트 방지.", "rsi_exit": "→ RSI 과매수 구간 진입으로 부분/전량 청산.", "manual": "→ 수동 청산 처리.", diff --git a/backend/tests/test_journal_time_stop_20261007.py b/backend/tests/test_journal_time_stop_20261007.py new file mode 100644 index 0000000..0e96436 --- /dev/null +++ b/backend/tests/test_journal_time_stop_20261007.py @@ -0,0 +1,27 @@ +"""시간손절이 '손절(stop_loss)'로 분류·서술되던 문제 (2026-10-07).""" +from app.core.config import settings +from app.services.day_trading_strategy import TIME_STOP_REASON_PREFIX +from app.services.trade_analyzer import _parse_exit_type + + +def test_dt_time_stop_reason_is_time_stop(): + reason = f"{TIME_STOP_REASON_PREFIX}: 32분 경과, 수익=-0.4% < {settings.DAY_TRADE_TIME_STOP_MIN_GAIN * 100:.1f}%" + assert _parse_exit_type(reason) == "time_stop" + + +def test_swing_time_stop_reason_is_time_stop(): + assert _parse_exit_type("시간손절: 20거래일 보유, 수익 1.2% < 3%") == "time_stop" + + +def test_real_stop_loss_still_stop_loss(): + assert _parse_exit_type("손절: -7.2% (기준 -7%)") == "stop_loss" + assert _parse_exit_type("stop_loss hit") == "stop_loss" + + +def test_journal_comment_uses_settings_not_hardcoded(): + import inspect + from app.services import trade_analyzer + + src = inspect.getsource(trade_analyzer) + assert "손절 기준(-6%" not in src # 옛 하드코딩 서술 제거 + assert "RSI 65+" not in src diff --git a/frontend/src/pages/Analysis.tsx b/frontend/src/pages/Analysis.tsx index 69ddf23..e6e4ebb 100644 --- a/frontend/src/pages/Analysis.tsx +++ b/frontend/src/pages/Analysis.tsx @@ -6,6 +6,19 @@ import { } from 'lucide-react' import api from '../lib/api' +/** 청산 유형 표시명 (백엔드 trade_analyzer._parse_exit_type 키) */ +const EXIT_TYPE_LABELS: Record = { + stop_loss: '손절', + time_stop: '시간손절 (조기 정리)', + trailing_stop: '트레일링 스탑', + take_profit: '익절', + eod_forced: '장 마감 청산', + rsi_exit: 'RSI 청산', + manual: '수동 청산', + unknown: '미상', +} + + // ── 타입 정의 ───────────────────────────────────────────────────────────────── interface PerformanceSummary { @@ -287,7 +300,7 @@ export default function Analysis() { {Object.entries(perf.by_exit_type).map(([etype, stats]) => (
- {etype} + {EXIT_TYPE_LABELS[etype] ?? etype} {stats.count}건
= 0 ? 'text-green-400' : 'text-red-400'}`}>