M1 │ 스캐너 최소 거래량 100만→10만주 │ kosdaq_scanner.py │
├─────┼─────────────────────────────────────────────────────┼────────────────────────────────┤ │ M2 │ ADX 히스테리시스 18/22 (플리핑 방지) │ mean_reversion_strategy.py │ ├─────┼─────────────────────────────────────────────────────┼────────────────────────────────┤ │ M3 │ 분할매도 최소 수익 1.0%→0.5% │ mean_reversion_strategy.py │ ├─────┼─────────────────────────────────────────────────────┼────────────────────────────────┤ │ M4 │ ORB 갭 필터 4% + 레인지 폭 0.3% 최소 │ day_trading_strategy.py │ ├─────┼─────────────────────────────────────────────────────┼────────────────────────────────┤ │ M5 │ 쿨다운 스트릭 부분매도 시 미변경 │ daytrading/position_manager.py │ ├─────┼─────────────────────────────────────────────────────┼────────────────────────────────┤ │ M6 │ MDD 강제매도 loss_tracker 기록 제거 (허위 ban 방지) │ mdd_manager.py │ ├─────┼─────────────────────────────────────────────────────┼────────────────────────────────┤ │ M7 │ RSI Wilder 스무딩으로 통일 │ mean_reversion_strategy.py
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
|
||||
|
||||
@@ -153,20 +153,26 @@ class DayTradingState:
|
||||
self.portfolio_cache_dirty = True
|
||||
return pos
|
||||
|
||||
def update_pnl(self, realized: float) -> None:
|
||||
def update_pnl(self, realized: float, is_full_close: bool = True) -> None:
|
||||
"""실현 손익 누적 및 연속 손실 카운트 업데이트.
|
||||
|
||||
손실 시 loss_streak_today 증가, 이익 시 리셋.
|
||||
3연패 이상이면 is_cooldown_active()가 True 반환.
|
||||
|
||||
연속 손실 카운트는 전량 매도(is_full_close=True)일 때만 업데이트.
|
||||
분할 매도(is_full_close=False)나 손익분기(realized==0)는 카운트에 영향 없음.
|
||||
|
||||
Args:
|
||||
realized: 실현 손익 (원). 양수=이익, 음수=손실
|
||||
is_full_close: 전량 매도 여부. False이면 streak 카운트 미변경
|
||||
"""
|
||||
self.daily_pnl += realized
|
||||
if not is_full_close:
|
||||
return
|
||||
if realized < 0:
|
||||
self.loss_streak_today += 1
|
||||
else:
|
||||
# 이익 발생 시 연속 손실 카운트 리셋
|
||||
elif realized > 0:
|
||||
# 이익 발생 시 연속 손실 카운트 리셋 (손익분기 realized==0은 중립 — 리셋하지 않음)
|
||||
self.loss_streak_today = 0
|
||||
|
||||
def get_position(self, stock_code: str) -> dict | None:
|
||||
|
||||
Reference in New Issue
Block a user