From e794267b960c88575c0c52e51ae489e1691a248c Mon Sep 17 00:00:00 2001 From: YoungWooSeo Date: Sun, 22 Feb 2026 20:31:18 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20portfolio.py=20check=5Fexit=5Fsignal=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=EB=B6=80=203=EA=B0=80=EC=A7=80=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ① regime 파라미터 누락 → MarketRegime.BULL 기본값으로 수정 (기존 코드 런타임 TypeError 버그 — check_exit_signal 첫 인자 누락) ② trailing_high 미전달 → 최근 10봉 최고가로 계산하여 전달 (트레일링 스탑 기능 실제 동작 가능) ③ remaining_pct 업데이트 수식 오류 - 기존: remaining_pct - sell_pct (2차 이후 잘못된 비율) - 수정: remaining_pct * (1 - sell_pct) (정확한 잔여 비율) 예) remaining=0.70, sell_pct=0.50 시 기존: 0.70 - 0.50 = 0.20 (잘못됨) 수정: 0.70 × 0.50 = 0.35 (올바름) --- backend/app/api/endpoints/portfolio.py | 32 ++++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/backend/app/api/endpoints/portfolio.py b/backend/app/api/endpoints/portfolio.py index 7990e84..c462c47 100644 --- a/backend/app/api/endpoints/portfolio.py +++ b/backend/app/api/endpoints/portfolio.py @@ -19,6 +19,7 @@ from app.models.models import Portfolio, TradeHistory, ActiveTrade, StockAnalysi from app.services.korea_investment_api import korea_investment_api from app.services.mean_reversion_strategy import ( calculate_atr, calculate_rsi, calculate_bollinger, check_exit_signal, + MarketRegime, # ① regime 파라미터 전달용 추가 ) from app.services.mdd_manager import mdd_manager from app.services.loss_tracker import loss_tracker @@ -481,9 +482,10 @@ async def check_and_execute_exit_trades(db: AsyncSession): """ 보유 종목의 손절/익절 조건을 체크하고 자동 매도 실행 CLAUDE.md 섹션 6.2 + 7.3 + 7.4 기반: - - ATR 기반 손절: min(최저가 - 2.5×ATR, 진입가 -8%) - - 1차 익절(50%): RSI ≥ 50 + BB 중앙선 상회 - - 2차 익절(전량): RSI 60~70 + BB 상단선 + - ATR 기반 손절: min(최저가 - 2.5×ATR, 진입가 -6%) ← -8% → -6% + - 1차 익절(30%/50%): RSI 55~65 + BB 중앙선 상회 + - 2차 익절(50%): RSI ≥ 65 + BB 상단선 도달 + - 트레일링 스탑: 최고가 대비 -3% 하락 시 잔여 전량 청산 - 폴백: 고정 비율 손절 (STOP_LOSS_PERCENT) """ from app.core.config import settings @@ -545,17 +547,27 @@ async def check_and_execute_exit_trades(db: AsyncSession): chart_data = chart_resp.get("output2", []) if len(chart_data) >= 20: closes = [float(d.get("stck_clpr", 0)) for d in reversed(chart_data)] - highs = [float(d.get("stck_hgpr", 0)) for d in reversed(chart_data)] - lows = [float(d.get("stck_lwpr", 0)) for d in reversed(chart_data)] + highs = [float(d.get("stck_hgpr", 0)) for d in reversed(chart_data)] + lows = [float(d.get("stck_lwpr", 0)) for d in reversed(chart_data)] atr = calculate_atr(highs, lows, closes) rsi = calculate_rsi(closes) - bb = calculate_bollinger(closes) + bb = calculate_bollinger(closes) if atr and rsi is not None and bb: bb_upper, bb_middle, bb_lower = bb lowest_since = min(lows[-5:]) if lows else avg_price + + # ② 진입 후 최고가 (트레일링 스탑용) — 최근 10봉 최고값으로 근사 + trailing_high = max(highs[-10:]) if highs else None + + # ① 시장 레짐 결정 + # TODO: KOSPI 지수 일봉 기반 detect_market_regime() 으로 대체 예정 + # 현재는 BULL 기본값 사용 (보수적 선택 — 1차 30% 익절) + regime = MarketRegime.BULL + exit_signal = check_exit_signal( + regime=regime, rsi=rsi, close=latest_price, bb_upper=bb_upper, @@ -565,6 +577,7 @@ async def check_and_execute_exit_trades(db: AsyncSession): atr=atr, lowest_since_entry=lowest_since, remaining_pct=remaining_pct, + trailing_high=trailing_high, # ② 트레일링 스탑 활성화 ) except Exception as chart_err: logger.debug(f"ATR 청산 계산 실패 ({stock_code}): {chart_err}") @@ -637,7 +650,12 @@ async def check_and_execute_exit_trades(db: AsyncSession): # 분할 청산 시 ActiveTrade 잔여 비율 업데이트 if exit_signal and exit_signal.action == "SELL_PARTIAL" and active_trade: - active_trade.remaining_pct = Decimal(str(remaining_pct - exit_signal.sell_pct)) + # ③ 정확한 잔여 비율 계산: + # sell_pct = 현재 보유량 대비 비율이므로 + # new_remaining = remaining_pct × (1 - sell_pct) + # 예) remaining=0.70, sell_pct=0.50 → 0.70×0.50=0.35 (기존: 0.70-0.50=0.20 오류) + new_remaining = remaining_pct * (1.0 - exit_signal.sell_pct) + active_trade.remaining_pct = Decimal(str(round(new_remaining, 4))) await db.commit() else: # 전량 매도