From 7867ced1b64d50f043e00692d684da789b974585 Mon Sep 17 00:00:00 2001 From: shuim Date: Sun, 22 Feb 2026 23:29:22 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=84=EB=9E=B5=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B2=84=EA=B7=B8=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 4 +++- backend/app/services/day_trading_strategy.py | 22 +++++++++++++------ .../app/services/mean_reversion_strategy.py | 13 ++++++----- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index fa4e5ab..2a21c1e 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -16,7 +16,9 @@ "Bash(docker restart:*)", "mcp__auto-stock__get_recommendations", "Bash(pip install:*)", - "Bash(python -c:*)" + "Bash(python -c:*)", + "Bash(git pull:*)", + "Bash(git stash:*)" ] }, "enableAllProjectMcpServers": true, diff --git a/backend/app/services/day_trading_strategy.py b/backend/app/services/day_trading_strategy.py index 2abe27e..1102314 100644 --- a/backend/app/services/day_trading_strategy.py +++ b/backend/app/services/day_trading_strategy.py @@ -115,13 +115,13 @@ def aggregate_to_5min(candles: List[MinuteCandle]) -> List[MinuteCandle]: if not candles: return [] - buckets: Dict[int, List[MinuteCandle]] = {} + buckets: Dict[tuple, List[MinuteCandle]] = {} for c in candles: if not c.time: continue h = int(c.time[:2]) m = int(c.time[2:4]) - key = (h * 60 + m) // 5 + key = (c.date, (h * 60 + m) // 5) # 날짜 포함 → 다른 날 캔들 혼합 방지 buckets.setdefault(key, []).append(c) result: List[MinuteCandle] = [] @@ -234,9 +234,11 @@ def check_orb_signal( if recent_vol < orb.avg_vol_per_min * 1.5: return None - # 직전 5분봉 양봉 확인 + # 직전 5분봉 양봉 확인 (5분봉 2개 이상 없으면 보수적으로 차단) candles_5m = aggregate_to_5min(candles_1min) - if len(candles_5m) >= 2 and candles_5m[-2].close < candles_5m[-2].open: + if len(candles_5m) < 2: + return None # 데이터 부족 시 진입 차단 + if candles_5m[-2].close < candles_5m[-2].open: return None # 직전 5분봉 음봉 → 스킵 stop_loss = max(orb.low, current_price * (1 - STOP_LOSS_PCT)) @@ -298,7 +300,8 @@ def check_vwap_bounce_signal( return None atr = _atr(candles_5min, 14) - stop_loss = (current_price - 1.5 * atr) if atr else current_price * (1 - STOP_LOSS_PCT) + raw_stop = (current_price - 1.5 * atr) if atr else current_price * (1 - STOP_LOSS_PCT) + stop_loss = max(raw_stop, current_price * (1 - STOP_LOSS_PCT * 2), 1.0) target = (current_price + 2.0 * atr) if atr else vwap * 1.03 return DayTradeSignal( @@ -355,8 +358,13 @@ def check_rsi_bb_signal( return None atr = _atr(candles_5min, 14) - stop_loss = (current_price - 1.5 * atr) if atr else current_price * (1 - STOP_LOSS_PCT) - target = bb_middle # BB 중앙선까지 반등 목표 + raw_stop = (current_price - 1.5 * atr) if atr else current_price * (1 - STOP_LOSS_PCT) + stop_loss = max(raw_stop, current_price * (1 - STOP_LOSS_PCT * 2), 1.0) + # 최소 1.5R 보장: bb_middle이 너무 가까우면 R:R 확보를 위해 목표 상향 + risk = current_price - stop_loss + target = max(bb_middle, current_price + risk * 1.5) + if target <= current_price: # 방어 로직 (stop_loss ≥ current_price 극단 케이스) + return None return DayTradeSignal( stock_code=stock_code, diff --git a/backend/app/services/mean_reversion_strategy.py b/backend/app/services/mean_reversion_strategy.py index 5b75c02..b86bb36 100644 --- a/backend/app/services/mean_reversion_strategy.py +++ b/backend/app/services/mean_reversion_strategy.py @@ -210,7 +210,7 @@ def check_buy_signal( if regime == MarketRegime.BULL: rsi_ok = rsi <= 30 # ← 25 → 30으로 완화: 진입 기회 ~2배 증가 vol_ok = current_volume >= avg_volume_20d * 2 # 거래량 20일평균 200% 이상 - if rsi_ok or vol_ok: + if rsi_ok or (vol_ok and rsi <= 50): # vol_ok 단독 시 RSI 50 상한 (과열 진입 차단) bb_low_minus = bb_lower * 0.97 # -3% bb_low_plus = bb_lower * 1.015 # +1.5% if bb_low_minus <= close <= bb_low_plus: @@ -299,16 +299,17 @@ def check_exit_signal( # ── 3. 2차 익절: 1차 완료 후 ───────────────────────────────────── if remaining_pct <= threshold: - # 3a. 트레일링 스탑: 취고가 대비 -3% 하락 시 잔여 전량 청산 - # 수익 구존 보장 위해 진입가 위 구간에서만 적용 + # 3a. 트레일링 스탑: 최고가 대비 -3% 하락 시 잔여 전량 청산 + # close > entry_price 조건 제거 → dead zone 차단 + # (entry 아래로 떨어져도 trailing_high가 entry보다 높으면 즉시 청산) if trailing_high is not None and trailing_high > entry_price: trail_stop = trailing_high * 0.97 - if close < trail_stop and close > entry_price: + if close < trail_stop: return ExitSignal( action="SELL_ALL", reason=( - f"트레일링 청산: close={close:.0f} < 취고가×0.97={trail_stop:.0f} " - f"(취고가={trailing_high:.0f})" + f"트레일링 청산: close={close:.0f} < 최고가×0.97={trail_stop:.0f} " + f"(최고가={trailing_high:.0f})" ), sell_pct=1.0, )