Merge branch 'dev' into main — 시간손절 조기발동 수정

This commit is contained in:
2026-09-23 12:54:24 +09:00
2 changed files with 77 additions and 21 deletions

View File

@@ -498,6 +498,33 @@ def check_rsi_bb_signal(
# ── 청산 신호 ─────────────────────────────────────────────────────────────────
def _elapsed_minutes(entry_time) -> Optional[float]:
"""진입 후 경과 시간(분). 값이 신뢰할 수 없으면 None.
[2026-09-23] DB에서 복원한 포지션의 entry_time 이 UTC(예전 기본값)면 KST now 와 비교 시
경과가 +540분으로 부풀어, 재배포 직후 '시간 손절'이 즉시 발동해 포지션이 몇 분 만에
청산됐다(산일전기 565분·미스토홀딩스 568분 = 실제 25·28분 + 9시간).
음수이거나 정규장 길이(09:00~15:30 = 390분)를 넘는 값은 시각 기준 불일치로 보고 무시한다.
"""
if entry_time is None:
return None
try:
from app.core.timeutil import now_kst as _now_kst
now_kst = _now_kst()
elapsed = (now_kst - entry_time).total_seconds() / 60
except Exception as e:
logger.debug(f"[DayTradeExit] 경과 시간 계산 실패: {e}")
return None
if elapsed < 0 or elapsed > 390:
logger.warning(
f"[DayTradeExit] entry_time 기준 불일치 추정 — 경과 {elapsed:.0f}분 무시 "
f"(entry_time={entry_time})"
)
return None
return elapsed
def check_day_trade_exit(
entry_price: float,
current_price: float,
@@ -536,19 +563,14 @@ def check_day_trade_exit(
)
# 3. [P2] 시간 손절 — 진입 후 설정 시간 경과 시 목표 방향 진행 미미하면 청산
if entry_time is not None:
try:
now_kst = _dt.now(pytz.timezone("Asia/Seoul")).replace(tzinfo=None)
elapsed_min = (now_kst - entry_time).total_seconds() / 60
if elapsed_min >= settings.DAY_TRADE_TIME_STOP_MINUTES:
gain_pct = (current_price - entry_price) / entry_price
if gain_pct < settings.DAY_TRADE_TIME_STOP_MIN_GAIN: # 최소 수익 미달 (수수료 커버 못함)
return DayTradeExit(
action="SELL_ALL",
reason=f"시간 손절: {elapsed_min:.0f}분 경과, 수익={gain_pct*100:.1f}% < {settings.DAY_TRADE_TIME_STOP_MIN_GAIN*100:.1f}%",
)
except Exception:
pass
elapsed_min = _elapsed_minutes(entry_time)
if elapsed_min is not None and elapsed_min >= settings.DAY_TRADE_TIME_STOP_MINUTES:
gain_pct = (current_price - entry_price) / entry_price
if gain_pct < settings.DAY_TRADE_TIME_STOP_MIN_GAIN: # 최소 수익 미달 (수수료 커버 못함)
return DayTradeExit(
action="SELL_ALL",
reason=f"시간 손절: {elapsed_min:.0f}분 경과, 수익={gain_pct*100:.1f}% < {settings.DAY_TRADE_TIME_STOP_MIN_GAIN*100:.1f}%",
)
# 4. 목표가 달성 → 50% 분할 매도 (CLAUDE.md 5.2: +2R 시 50% 분할 매도)
# [FIX] target_price<=0 방어 — 0이면 즉시 조건 충족되어 오매도되는 것을 방지
@@ -563,14 +585,8 @@ def check_day_trade_exit(
# 5. 트레일링 스탑 (최소 보유 시간 경과 + 최소 수익률 이상 시 발동)
# [whipsaw 방지] 진입 직후 순간 상승→되돌림에 트레일링이 걸리는 것을 방지
_trailing_allowed = True
if entry_time is not None:
try:
now_kst = _dt.now(pytz.timezone("Asia/Seoul")).replace(tzinfo=None)
elapsed_sec = (now_kst - entry_time).total_seconds()
if elapsed_sec < settings.DAY_TRADE_TRAILING_MIN_HOLD_SEC:
_trailing_allowed = False # 최소 보유 시간(5분) 미경과 → 하드스탑만 적용
except Exception:
pass
if elapsed_min is not None and elapsed_min * 60 < settings.DAY_TRADE_TRAILING_MIN_HOLD_SEC:
_trailing_allowed = False # 최소 보유 시간(5분) 미경과 → 하드스탑만 적용
if _trailing_allowed:
profit_pct = (highest_since_entry - entry_price) / entry_price

View File

@@ -0,0 +1,40 @@
"""시간 손절이 시각 기준 불일치로 조기 발동하지 않는지 검증.
2026-09-23: 재배포로 DB 복원된 포지션의 entry_time 이 UTC 라 KST now 와의 차이가 +540분이 되어
진입 25분 만에 '시간 손절: 565분 경과'로 청산됐다(익절까지 가지 못함).
"""
from datetime import datetime, timedelta
import pytz
from app.services.day_trading_strategy import _elapsed_minutes
KST = pytz.timezone("Asia/Seoul")
def _kst_now():
return datetime.now(KST).replace(tzinfo=None)
def test_normal_elapsed_is_returned():
v = _elapsed_minutes(_kst_now() - timedelta(minutes=40))
assert v is not None and 39 < v < 41
def test_utc_entry_time_is_rejected():
"""UTC 로 저장된 진입시각(=KST-9h) → 경과 +540분 → 무시해야 한다."""
utc_entry = _kst_now() - timedelta(hours=9) + timedelta(minutes=25) # 실제 25분 전
assert _elapsed_minutes(utc_entry) is None
def test_future_entry_time_is_rejected():
assert _elapsed_minutes(_kst_now() + timedelta(minutes=5)) is None
def test_none_entry_time():
assert _elapsed_minutes(None) is None
def test_boundary_is_market_day_length():
assert _elapsed_minutes(_kst_now() - timedelta(minutes=389)) is not None
assert _elapsed_minutes(_kst_now() - timedelta(minutes=391)) is None