Files
upbit-trader/daemon/runner.py
joungmin 27189b1ad9 feat: add Fear & Greed filter to entry logic
- core/fng.py: F&G API wrapper with 1h cache (alternative.me)
  - FNG_MIN_ENTRY=41 (env-configurable), blocks entry below threshold
- core/strategy.py: call is_entry_allowed() before volume/regime checks
- daemon/runner.py: log F&G status on every scan cycle
- core/notify.py: include F&G value in buy/signal/status notifications
- core/trader.py: pass current F&G value to notify_buy

Backtest evidence (1y / 18 tickers / 1h candles):
  - No filter:   820 trades, 32.7% WR, avg +0.012%, KRW +95k
  - F&G >= 41:   372 trades, 39.5% WR, avg +0.462%, KRW +1.72M
  - Blocked 452 trades (avg -0.372%, saved ~1.68M KRW loss)

Also add:
- backtest_db.py: Oracle DB storage for backtest runs/results/trades
- fng_1y_backtest.py, fng_adaptive_backtest.py, fng_sim_comparison.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 15:56:17 +09:00

108 lines
4.2 KiB
Python

"""매수 기회 스캔 루프 - 60초마다 전체 시장 스캔 + 신호 종목 빠른 폴링."""
import logging
import os
import threading
import time
from core import trader
from core.fng import FNG_MIN_ENTRY, get_fng
from core.market import get_top_tickers
from core.market_regime import get_regime
from core.strategy import get_active_signals, should_buy
logger = logging.getLogger(__name__)
SCAN_INTERVAL = 60 # 전체 시장 스캔 주기 (초)
SIGNAL_POLL_INTERVAL = int(os.getenv("SIGNAL_POLL_INTERVAL", "15")) # 신호 종목 빠른 감시 주기 (초)
def _fast_poll_loop() -> None:
"""활성 신호 종목을 SIGNAL_POLL_INTERVAL 초마다 빠르게 체크.
신호 감지 후 전체 스캔 60초를 기다리지 않고, 신호 종목만 빠르게 감시하여
목표 임계값 도달 시 즉시 매수한다.
"""
logger.info(f"신호 감시 시작 (주기={SIGNAL_POLL_INTERVAL}초)")
while True:
try:
signals = get_active_signals()
if signals:
regime = get_regime()
if regime["name"] != "bear":
positions = trader.get_positions()
for ticker in list(signals):
if ticker in positions:
continue
if len(positions) >= trader.MAX_POSITIONS:
break
try:
if should_buy(ticker):
logger.info(f"[빠른감시] 매수 신호: {ticker}")
trader.buy(ticker)
time.sleep(0.1)
except Exception as e:
logger.error(f"[빠른감시] 오류 {ticker}: {e}")
except Exception as e:
logger.error(f"신호 감시 루프 오류: {e}")
time.sleep(SIGNAL_POLL_INTERVAL)
def run_scanner() -> None:
"""메인 스캔 루프."""
# 신호 종목 빠른 감시 스레드 시작
t = threading.Thread(target=_fast_poll_loop, daemon=True, name="signal-fast-poll")
t.start()
logger.info(f"스캐너 시작 (스캔={SCAN_INTERVAL}초 | 신호감시={SIGNAL_POLL_INTERVAL}초)")
while True:
try:
# 포지션 꽉 찼으면 스캔 스킵
if len(trader.get_positions()) >= trader.MAX_POSITIONS:
logger.info("포지션 최대치 도달, 스캔 스킵")
time.sleep(SCAN_INTERVAL)
continue
# Bear 레짐 시 신규 매수 완전 차단
regime = get_regime()
if regime["name"] == "bear":
logger.info(
f"[Bear차단] 레짐={regime['emoji']} BEAR "
f"(score={regime['score']:+.2f}%) — 신규 매수 스킵"
)
time.sleep(SCAN_INTERVAL)
continue
# F&G 진입 필터 로그 (should_buy 내부에서 차단하지만 스캔 전 상태 기록)
fv = get_fng()
fng_label = (
"극탐욕" if fv >= 76 else "탐욕" if fv >= 56 else
"중립" if fv >= 46 else "약공포" if fv >= 41 else
"공포" if fv >= 26 else "극공포"
)
if fv < FNG_MIN_ENTRY:
logger.info(f"[F&G차단] F&G={fv} ({fng_label}) < {FNG_MIN_ENTRY} — 이번 스캔 진입 없음")
tickers = get_top_tickers()
logger.info(f"스캔 시작: {len(tickers)}개 종목 | F&G={fv}({fng_label})")
for ticker in tickers:
# 이미 보유 중인 종목 제외
if ticker in trader.get_positions():
continue
try:
if should_buy(ticker):
logger.info(f"매수 신호: {ticker}")
trader.buy(ticker)
time.sleep(0.15) # API rate limit 방지
except Exception as e:
logger.error(f"스캔 오류 {ticker}: {e}")
time.sleep(0.3)
except Exception as e:
logger.error(f"스캐너 루프 오류: {e}")
time.sleep(SCAN_INTERVAL)