Compare commits

..

2 Commits

Author SHA1 Message Date
18babaf910 Merge branch 'dev' into main — 시간손절 분류 분리
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:06:46 +09:00
5c4803b339 fix(journal): 시간손절을 손절과 분리(time_stop) + 청산 설명 임계값을 실효 설정값으로
- _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 <noreply@anthropic.com>
2026-10-07 11:06:08 +09:00
3 changed files with 61 additions and 4 deletions

View File

@@ -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": "→ 수동 청산 처리.",

View File

@@ -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

View File

@@ -6,6 +6,19 @@ import {
} from 'lucide-react'
import api from '../lib/api'
/** 청산 유형 표시명 (백엔드 trade_analyzer._parse_exit_type 키) */
const EXIT_TYPE_LABELS: Record<string, string> = {
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]) => (
<div key={etype} className="flex items-center justify-between py-2 border-b border-gray-700/50 last:border-0">
<div>
<span className="text-sm text-gray-200 font-medium">{etype}</span>
<span className="text-sm text-gray-200 font-medium">{EXIT_TYPE_LABELS[etype] ?? etype}</span>
<span className="ml-2 text-xs text-gray-500">{stats.count}건</span>
</div>
<span className={`text-sm ${stats.avg_return >= 0 ? 'text-green-400' : 'text-red-400'}`}>