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:
@@ -60,9 +60,10 @@ def _recalc_compound_budget() -> None:
|
|||||||
logger.warning(f"[복리] 예산 재계산 실패 (이전 값 유지): {e}")
|
logger.warning(f"[복리] 예산 재계산 실패 (이전 값 유지): {e}")
|
||||||
|
|
||||||
# Walk-forward 필터 설정
|
# Walk-forward 필터 설정
|
||||||
WF_WINDOW = int(float(os.getenv("WF_WINDOW", "5"))) # 이력 윈도우 크기
|
WF_WINDOW = int(float(os.getenv("WF_WINDOW", "5"))) # 이력 윈도우 크기
|
||||||
WF_MIN_WIN_RATE = float(os.getenv("WF_MIN_WIN_RATE", "0.40")) # 최소 승률 임계값
|
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_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()
|
_lock = threading.Lock()
|
||||||
_positions: dict = {}
|
_positions: dict = {}
|
||||||
@@ -388,17 +389,28 @@ def restore_positions() -> None:
|
|||||||
logger.error(f"DB 포지션 삭제 실패 {ticker}: {e}")
|
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:
|
with _lock:
|
||||||
if ticker in _positions:
|
if ticker in _positions:
|
||||||
logger.debug(f"{ticker} 이미 보유 중")
|
logger.debug(f"{ticker} 이미 보유 중")
|
||||||
return False
|
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% 이상일 때만 재진입 (손절 직후 역방향 재매수 방지)
|
# 직전 매도가 +1% 이상일 때만 재진입 (손절 직후 역방향 재매수 방지)
|
||||||
# 단, 직전 거래가 수익(승)이었으면 이 필터 스킵 — 다시 상승 시 재진입 허용
|
# 단, 직전 거래가 수익(승)이었으면 이 필터 스킵 — 다시 상승 시 재진입 허용
|
||||||
if ticker in _last_sell_prices:
|
if ticker in _last_sell_prices:
|
||||||
hist = _get_history(ticker)
|
|
||||||
last_was_win = bool(hist[-1]) if hist else False
|
last_was_win = bool(hist[-1]) if hist else False
|
||||||
if not last_was_win:
|
if not last_was_win:
|
||||||
current_check = pyupbit.get_current_price(ticker)
|
current_check = pyupbit.get_current_price(ticker)
|
||||||
@@ -412,9 +424,14 @@ def buy(ticker: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Walk-forward 필터: 직전 WF_WINDOW건 승률이 낮으면 진입 차단 + shadow 진입
|
# Walk-forward 필터: 직전 WF_WINDOW건 승률이 낮으면 진입 차단 + shadow 진입
|
||||||
|
# vol이 WF_VOL_BYPASS_THRESH 이상이면 WF 무시 (강한 신호 우선)
|
||||||
if WF_MIN_WIN_RATE > 0:
|
if WF_MIN_WIN_RATE > 0:
|
||||||
hist = _get_history(ticker)
|
if WF_VOL_BYPASS_THRESH > 0 and vol_ratio >= WF_VOL_BYPASS_THRESH:
|
||||||
if len(hist) >= WF_WINDOW:
|
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
|
recent_wr = sum(hist[-WF_WINDOW:]) / WF_WINDOW
|
||||||
if recent_wr < WF_MIN_WIN_RATE:
|
if recent_wr < WF_MIN_WIN_RATE:
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -35,8 +35,9 @@ def _fast_poll_loop() -> None:
|
|||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
if should_buy(ticker):
|
if should_buy(ticker):
|
||||||
logger.info(f"[빠른감시] 매수 신호: {ticker}")
|
vol_r = signals.get(ticker, {}).get("vol_ratio", 0.0)
|
||||||
trader.buy(ticker)
|
logger.info(f"[빠른감시] 매수 신호: {ticker} (vol={vol_r:.1f}x)")
|
||||||
|
trader.buy(ticker, vol_ratio=vol_r)
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[빠른감시] 오류 {ticker}: {e}")
|
logger.error(f"[빠른감시] 오류 {ticker}: {e}")
|
||||||
@@ -85,8 +86,9 @@ def run_scanner() -> None:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if should_buy(ticker):
|
if should_buy(ticker):
|
||||||
logger.info(f"매수 신호: {ticker}")
|
vol_r = get_active_signals().get(ticker, {}).get("vol_ratio", 0.0)
|
||||||
trader.buy(ticker)
|
logger.info(f"매수 신호: {ticker} (vol={vol_r:.1f}x)")
|
||||||
|
trader.buy(ticker, vol_ratio=vol_r)
|
||||||
time.sleep(0.15) # API rate limit 방지
|
time.sleep(0.15) # API rate limit 방지
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"스캔 오류 {ticker}: {e}")
|
logger.error(f"스캔 오류 {ticker}: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user