diff --git a/core/trader.py b/core/trader.py index a662ecf..f62eda3 100644 --- a/core/trader.py +++ b/core/trader.py @@ -60,9 +60,10 @@ def _recalc_compound_budget() -> None: logger.warning(f"[복리] 예산 재계산 실패 (이전 값 유지): {e}") # Walk-forward 필터 설정 -WF_WINDOW = int(float(os.getenv("WF_WINDOW", "5"))) # 이력 윈도우 크기 -WF_MIN_WIN_RATE = float(os.getenv("WF_MIN_WIN_RATE", "0.40")) # 최소 승률 임계값 -WF_SHADOW_WINS = int(os.getenv("WF_SHADOW_WINS", "2")) # shadow N연승 → WF 해제 +WF_WINDOW = int(float(os.getenv("WF_WINDOW", "5"))) # 이력 윈도우 크기 +WF_MIN_WIN_RATE = float(os.getenv("WF_MIN_WIN_RATE", "0.40")) # 최소 승률 임계값 +WF_SHADOW_WINS = int(os.getenv("WF_SHADOW_WINS", "2")) # shadow N연승 → WF 해제 +WF_VOL_BYPASS_THRESH = float(os.getenv("WF_VOL_BYPASS_THRESH", "10.0")) # 이 이상 vol이면 WF 무시 _lock = threading.Lock() _positions: dict = {} @@ -388,17 +389,28 @@ def restore_positions() -> None: logger.error(f"DB 포지션 삭제 실패 {ticker}: {e}") -def buy(ticker: str) -> bool: - """시장가 매수. 예산·포지션 수 확인 후 진입.""" +def buy(ticker: str, vol_ratio: float = 0.0) -> bool: + """시장가 매수. 예산·포지션 수 확인 후 진입. + + Args: + vol_ratio: 진입 시점의 거래량 배율. WF_VOL_BYPASS_THRESH 이상이면 WF 필터 무시. + """ with _lock: if ticker in _positions: logger.debug(f"{ticker} 이미 보유 중") return False + # WF 이력 항상 DB에서 직접 로드 (재시작과 무관하게 최신 이력 반영) + try: + hist = load_recent_wins(ticker, WF_WINDOW) + _trade_history[ticker] = hist # 메모리 캐시 동기화 + except Exception as e: + logger.warning(f"WF 이력 DB 로드 실패 {ticker}: {e}") + hist = _trade_history.get(ticker, []) + # 직전 매도가 +1% 이상일 때만 재진입 (손절 직후 역방향 재매수 방지) # 단, 직전 거래가 수익(승)이었으면 이 필터 스킵 — 다시 상승 시 재진입 허용 if ticker in _last_sell_prices: - hist = _get_history(ticker) last_was_win = bool(hist[-1]) if hist else False if not last_was_win: current_check = pyupbit.get_current_price(ticker) @@ -412,9 +424,14 @@ def buy(ticker: str) -> bool: return False # Walk-forward 필터: 직전 WF_WINDOW건 승률이 낮으면 진입 차단 + shadow 진입 + # vol이 WF_VOL_BYPASS_THRESH 이상이면 WF 무시 (강한 신호 우선) if WF_MIN_WIN_RATE > 0: - hist = _get_history(ticker) - if len(hist) >= WF_WINDOW: + if WF_VOL_BYPASS_THRESH > 0 and vol_ratio >= WF_VOL_BYPASS_THRESH: + logger.info( + f"[WF바이패스] {ticker} vol={vol_ratio:.1f}x ≥ {WF_VOL_BYPASS_THRESH}x" + f" → WF 필터 무시 (강한 vol 신호)" + ) + elif len(hist) >= WF_WINDOW: recent_wr = sum(hist[-WF_WINDOW:]) / WF_WINDOW if recent_wr < WF_MIN_WIN_RATE: logger.info( diff --git a/daemon/runner.py b/daemon/runner.py index 5efa585..31cfab2 100644 --- a/daemon/runner.py +++ b/daemon/runner.py @@ -35,8 +35,9 @@ def _fast_poll_loop() -> None: break try: if should_buy(ticker): - logger.info(f"[빠른감시] 매수 신호: {ticker}") - trader.buy(ticker) + vol_r = signals.get(ticker, {}).get("vol_ratio", 0.0) + logger.info(f"[빠른감시] 매수 신호: {ticker} (vol={vol_r:.1f}x)") + trader.buy(ticker, vol_ratio=vol_r) time.sleep(0.1) except Exception as e: logger.error(f"[빠른감시] 오류 {ticker}: {e}") @@ -85,8 +86,9 @@ def run_scanner() -> None: try: if should_buy(ticker): - logger.info(f"매수 신호: {ticker}") - trader.buy(ticker) + vol_r = get_active_signals().get(ticker, {}).get("vol_ratio", 0.0) + logger.info(f"매수 신호: {ticker} (vol={vol_r:.1f}x)") + trader.buy(ticker, vol_ratio=vol_r) time.sleep(0.15) # API rate limit 방지 except Exception as e: logger.error(f"스캔 오류 {ticker}: {e}")