[2026-05-29] 재진입 쿨다운 기준 수정
This commit is contained in:
+34
@@ -461,6 +461,7 @@ class StockBot:
|
|||||||
self._restore_positions_from_db()
|
self._restore_positions_from_db()
|
||||||
# 당일 SL 종목 복원 (재시작 후에도 재진입 차단 유지)
|
# 당일 SL 종목 복원 (재시작 후에도 재진입 차단 유지)
|
||||||
self._restore_sl_tickers_from_db()
|
self._restore_sl_tickers_from_db()
|
||||||
|
self._restore_reentry_controls_from_db()
|
||||||
await send(f"[시작] 단타봇 가동 | 예수금: {cash:,}원 | "
|
await send(f"[시작] 단타봇 가동 | 예수금: {cash:,}원 | "
|
||||||
f"{'모의투자' if self.kis.is_mock else '실거래'}")
|
f"{'모의투자' if self.kis.is_mock else '실거래'}")
|
||||||
|
|
||||||
@@ -498,6 +499,37 @@ class StockBot:
|
|||||||
if self.sl_tickers:
|
if self.sl_tickers:
|
||||||
logger.info(f"당일 SL 종목 복원(재진입 차단): {self.sl_tickers}")
|
logger.info(f"당일 SL 종목 복원(재진입 차단): {self.sl_tickers}")
|
||||||
|
|
||||||
|
def _restore_reentry_controls_from_db(self):
|
||||||
|
"""재시작 시 오늘 청산 이력 기반 재진입 제한 상태를 복원한다."""
|
||||||
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
with get_conn() as conn:
|
||||||
|
rows = conn.execute("""
|
||||||
|
SELECT ticker, exit_time, exit_reason
|
||||||
|
FROM trades
|
||||||
|
WHERE date=?
|
||||||
|
AND exit_time IS NOT NULL
|
||||||
|
AND exit_reason IN ('TIME', 'FORCE', 'TP1', 'TP2')
|
||||||
|
ORDER BY exit_time
|
||||||
|
""", (today,)).fetchall()
|
||||||
|
|
||||||
|
restored = []
|
||||||
|
for ticker, exit_time, reason in rows:
|
||||||
|
if ticker in self.positions:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
exit_dt = datetime.strptime(exit_time, "%H:%M:%S").replace(
|
||||||
|
year=datetime.now().year,
|
||||||
|
month=datetime.now().month,
|
||||||
|
day=datetime.now().day,
|
||||||
|
)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
self.strategy.mark_final_exit(ticker, reason, exit_dt)
|
||||||
|
restored.append(f"{ticker}:{reason}")
|
||||||
|
|
||||||
|
if restored:
|
||||||
|
logger.info("재진입 제한 상태 복원: %s", ", ".join(restored))
|
||||||
|
|
||||||
def _db_save_position(self, ticker: str, pos: dict, target_price: float):
|
def _db_save_position(self, ticker: str, pos: dict, target_price: float):
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
conn.execute("""
|
conn.execute("""
|
||||||
@@ -963,6 +995,7 @@ class StockBot:
|
|||||||
if pos["qty"] <= 0:
|
if pos["qty"] <= 0:
|
||||||
del self.positions[ticker]
|
del self.positions[ticker]
|
||||||
self._db_delete_position(ticker)
|
self._db_delete_position(ticker)
|
||||||
|
self.strategy.mark_final_exit(ticker, reason)
|
||||||
else:
|
else:
|
||||||
self._db_save_position(ticker, pos, self.strategy.get_target(ticker))
|
self._db_save_position(ticker, pos, self.strategy.get_target(ticker))
|
||||||
await notify_tp1(ticker, name, pnl_pct)
|
await notify_tp1(ticker, name, pnl_pct)
|
||||||
@@ -970,6 +1003,7 @@ class StockBot:
|
|||||||
elif reason in ("TP2", "SL", "TIME", "FORCE"):
|
elif reason in ("TP2", "SL", "TIME", "FORCE"):
|
||||||
del self.positions[ticker]
|
del self.positions[ticker]
|
||||||
self._db_delete_position(ticker)
|
self._db_delete_position(ticker)
|
||||||
|
self.strategy.mark_final_exit(ticker, reason)
|
||||||
if reason == "TP2":
|
if reason == "TP2":
|
||||||
await notify_tp2(ticker, name, pnl_pct)
|
await notify_tp2(ticker, name, pnl_pct)
|
||||||
elif reason == "SL":
|
elif reason == "SL":
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ class VolatilityBreakout:
|
|||||||
self.today_open = {} # ticker → 당일 시가
|
self.today_open = {} # ticker → 당일 시가
|
||||||
self.targets = {} # ticker → 목표가
|
self.targets = {} # ticker → 목표가
|
||||||
self._entry_times: dict = {} # ticker → 마지막 진입 datetime (쿨다운 추적)
|
self._entry_times: dict = {} # ticker → 마지막 진입 datetime (쿨다운 추적)
|
||||||
|
self._exit_times: dict = {} # ticker -> 마지막 최종 청산 datetime (쿨다운 추적)
|
||||||
|
self._tp_closed_tickers: set[str] = set() # TP로 전량 청산된 당일 재진입 차단
|
||||||
|
|
||||||
# ── AI 컨텍스트 로드 ──
|
# ── AI 컨텍스트 로드 ──
|
||||||
|
|
||||||
@@ -119,6 +121,14 @@ class VolatilityBreakout:
|
|||||||
def get_target(self, ticker: str) -> float:
|
def get_target(self, ticker: str) -> float:
|
||||||
return self.targets.get(ticker, 0.0)
|
return self.targets.get(ticker, 0.0)
|
||||||
|
|
||||||
|
def mark_final_exit(self, ticker: str, reason: str, exit_time: datetime | None = None):
|
||||||
|
"""최종 청산 중 당일 재진입 제한에 필요한 상태를 기록한다."""
|
||||||
|
exit_time = exit_time or datetime.now()
|
||||||
|
if reason in ("TIME", "FORCE"):
|
||||||
|
self._exit_times[ticker] = exit_time
|
||||||
|
elif reason in ("TP1", "TP2"):
|
||||||
|
self._tp_closed_tickers.add(ticker)
|
||||||
|
|
||||||
# ── 진입 신호 판단 ──
|
# ── 진입 신호 판단 ──
|
||||||
|
|
||||||
def check_entry(self, ticker: str, name: str,
|
def check_entry(self, ticker: str, name: str,
|
||||||
@@ -136,10 +146,15 @@ class VolatilityBreakout:
|
|||||||
result["reason"] = f"진입 시간 외 ({now})"
|
result["reason"] = f"진입 시간 외 ({now})"
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# 동일 종목 재진입 쿨다운 체크
|
# TP로 전량 청산된 종목은 당일 재진입하지 않는다.
|
||||||
last_entry = self._entry_times.get(ticker)
|
if ticker in self._tp_closed_tickers:
|
||||||
if last_entry is not None:
|
result["reason"] = "TP 당일 재진입 차단"
|
||||||
elapsed = (now_dt - last_entry).total_seconds() / 60
|
return result
|
||||||
|
|
||||||
|
# TIME/FORCE 청산 후 쿨다운은 진입 시각이 아니라 청산 시각 기준이다.
|
||||||
|
last_exit = self._exit_times.get(ticker)
|
||||||
|
if last_exit is not None:
|
||||||
|
elapsed = (now_dt - last_exit).total_seconds() / 60
|
||||||
if elapsed < TICKER_REENTRY_COOLDOWN_MIN:
|
if elapsed < TICKER_REENTRY_COOLDOWN_MIN:
|
||||||
result["reason"] = f"재진입 쿨다운 ({elapsed:.0f}분 / {TICKER_REENTRY_COOLDOWN_MIN}분)"
|
result["reason"] = f"재진입 쿨다운 ({elapsed:.0f}분 / {TICKER_REENTRY_COOLDOWN_MIN}분)"
|
||||||
return result
|
return result
|
||||||
|
|||||||
Reference in New Issue
Block a user