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:
@@ -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(
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user