feat: WF always reads from DB + vol bypass threshold

- buy() now always loads WF history from trade_results DB directly
  (not lazy-cached in memory) → restart-safe, no stale-history issue
- Added WF_VOL_BYPASS_THRESH (default 10.0x): if vol_ratio at entry
  exceeds this threshold, WF filter is skipped regardless of win rate
- buy() now accepts vol_ratio param; runner.py passes it from
  get_active_signals() for both fast-poll and main scan loops

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-03-04 10:54:52 +09:00
parent 2499ea08ef
commit ab5c963803
2 changed files with 31 additions and 12 deletions

View File

@@ -63,6 +63,7 @@ def _recalc_compound_budget() -> None:
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(

View File

@@ -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}")