전략 취약점 15건 수정 (코드 리뷰 기반)
## CRITICAL/HIGH (S1-S8) - S1: Aggressive 모드 %B 스케일링 캡 1.3 (BB 중앙선 위 매수 방지) - S2: ML scaler train-only fit (데이터 누수 제거) - S3: SIDEWAYS = ADX<20 AND slope≈0 (트렌드 초기 차단 해소) - S4: EOD 청산 실패 시 재시도 + CRITICAL 로그 - S5: 일일 손실 한도에 미실현 P&L 포함 - S6: trailing stop 고점 매 사이클 갱신 - S7: 섹터 필터 미매핑 종목 "기타" 분류 (max 3 적용) - S8: 스캐너 RSI 기준 25→46, BB 점수 -3%~+1.5% 구간 ## MEDIUM (M1-M7) - M1: 스캐너 최소 거래량 100만→10만주 - M2: ADX 히스테리시스 18/22 (국면 플리핑 방지) - M3: 분할매도 최소 수익 1.0%→0.5% - M4: ORB 갭 필터 4% + 레인지 폭 0.3% 최소 - M5: 쿨다운 스트릭 부분매도 시 미변경 - M6: MDD 강제매도 loss_tracker 기록 제거 (허위 ban 방지) - M7: RSI Wilder 스무딩으로 통일 ## 인프라/보안 - 인증 누락 엔드포인트 수정 (analysis, journals) - nginx 보안 헤더 location 블록 누락 수정 - Alembic 마이그레이션 IF NOT EXISTS 가드 추가 - SellRequest/UsOrderRequest 입력 검증 강화 - 차트 네이버 폴백 추가 (DB 데이터 없을 때) - 서버 이전 시 자동 DB 마이그레이션 + KRX 초기 수집 - CLAUDE.md 섹션 5.1 백테스트 값 동기화 (4%→6%, -6%→-7%) - MAX_SWING_POSITIONS 25→20 (CLAUDE.md 준수) - mdd_manager failed_count에서 skip 종목 제외 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,8 +73,20 @@ async def check_and_exit_positions() -> list[dict]:
|
||||
return exits
|
||||
|
||||
# 일일 손실 한도 초과 시 전체 강제 청산
|
||||
if is_daily_loss_exceeded(day_trading_state.last_total_assets, day_trading_state.daily_pnl):
|
||||
logger.warning("[DayExit] 일일 손실 한도 초과 → 전체 강제 청산")
|
||||
# [S5] 미실현 손익도 포함하여 일일 손실 한도 판단
|
||||
unrealized_pnl = 0.0
|
||||
for _code, _pos in day_trading_state.positions.items():
|
||||
_entry = _pos.get("entry_price", 0)
|
||||
_qty = _pos.get("quantity", 0)
|
||||
_cur = _pos.get("current_price", _entry)
|
||||
if _entry > 0 and _qty > 0:
|
||||
unrealized_pnl += (_cur - _entry) * _qty
|
||||
total_daily_pnl = day_trading_state.daily_pnl + unrealized_pnl
|
||||
if is_daily_loss_exceeded(day_trading_state.last_total_assets, total_daily_pnl):
|
||||
logger.warning(
|
||||
f"[DayExit] 일일 손실 한도 초과 → 전체 강제 청산 "
|
||||
f"(실현={day_trading_state.daily_pnl:.0f}, 미실현={unrealized_pnl:.0f})"
|
||||
)
|
||||
return await force_close_all(reason="일일 손실 한도 초과")
|
||||
|
||||
# EOD 강제 청산 (15:20)
|
||||
@@ -131,10 +143,13 @@ async def _check_single_position(stock_code: str) -> dict | None:
|
||||
# highest_price 갱신 — 5분봉 high 값도 반영 (30초 주기 보완)
|
||||
candle_high = max(c.high for c in candles_5m[-3:]) if len(candles_5m) >= 3 else current_price
|
||||
new_highest = max(current_price, candle_high, highest_price)
|
||||
if new_highest > highest_price:
|
||||
async with day_trading_state.trade_lock:
|
||||
if stock_code in day_trading_state.positions:
|
||||
async with day_trading_state.trade_lock:
|
||||
if stock_code in day_trading_state.positions:
|
||||
if new_highest > highest_price:
|
||||
day_trading_state.positions[stock_code]["highest_price"] = new_highest
|
||||
# [S5] 미실현 손익 계산용 현재가 갱신
|
||||
day_trading_state.positions[stock_code]["current_price"] = current_price
|
||||
if new_highest > highest_price:
|
||||
highest_price = new_highest
|
||||
|
||||
# 청산 신호 체크
|
||||
@@ -231,7 +246,7 @@ async def _execute_exit(
|
||||
day_trading_state.positions[stock_code]["quantity"] = remaining
|
||||
day_trading_state.positions[stock_code]["target_price"] = float("inf")
|
||||
day_trading_state.positions[stock_code]["partial_sold"] = True
|
||||
day_trading_state.update_pnl(profit_loss)
|
||||
day_trading_state.update_pnl(profit_loss, is_full_close=is_full_exit)
|
||||
|
||||
# [P0] loss_tracker 연동 — 전량 매도 시 연속 손실 추적
|
||||
if is_full_exit:
|
||||
@@ -295,6 +310,7 @@ async def force_close_all(reason: str = "강제 청산") -> list[dict]:
|
||||
list[dict]: 청산 실행 결과 목록
|
||||
"""
|
||||
results = []
|
||||
failed_codes: list[str] = []
|
||||
codes = list(day_trading_state.positions.keys())
|
||||
|
||||
for stock_code in codes:
|
||||
@@ -319,6 +335,33 @@ async def force_close_all(reason: str = "강제 청산") -> list[dict]:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[DayExit] 강제 청산 오류 ({stock_code}): {e}", exc_info=True)
|
||||
failed_codes.append(stock_code)
|
||||
|
||||
# S4 fix: 실패 종목 1회 재시도
|
||||
if failed_codes:
|
||||
logger.warning(f"[DayExit] 강제 청산 실패 {len(failed_codes)}건 — 2초 후 재시도: {failed_codes}")
|
||||
await asyncio.sleep(2.0)
|
||||
still_failed: list[str] = []
|
||||
for stock_code in failed_codes:
|
||||
pos = day_trading_state.get_position(stock_code)
|
||||
if not pos:
|
||||
continue
|
||||
try:
|
||||
price_resp = await korea_investment_api.get_current_price(stock_code)
|
||||
price = float(
|
||||
price_resp.get("output", {}).get("stck_prpr", 0) or 0
|
||||
) if price_resp else 0.0
|
||||
if price <= 0:
|
||||
price = float(pos.get("entry_price", 0))
|
||||
result = await _execute_exit(stock_code, pos, price, reason)
|
||||
if result:
|
||||
results.append(result)
|
||||
await asyncio.sleep(0.3)
|
||||
except Exception as e:
|
||||
logger.critical(f"[DayExit] 강제 청산 재시도 실패 ({stock_code}): {e} — 포지션 미청산 위험!", exc_info=True)
|
||||
still_failed.append(stock_code)
|
||||
if still_failed:
|
||||
logger.critical(f"[DayExit] 최종 미청산 종목: {still_failed} — 수동 청산 필요!")
|
||||
|
||||
logger.info(f"[DayExit] 강제 청산 완료: {len(results)}건 ({reason})")
|
||||
return results
|
||||
|
||||
Reference in New Issue
Block a user