"""공포탐욕지수(F&G) 조회 모듈. alternative.me API로 일일 F&G 값을 가져와 메모리에 캐시한다. 캐시 TTL은 1시간 (F&G는 하루 1회 업데이트). 환경변수: FNG_MIN_ENTRY (기본값 41): 이 값 미만이면 진입 차단 """ from __future__ import annotations import json import logging import os import time import urllib.request from datetime import datetime logger = logging.getLogger(__name__) FNG_MIN_ENTRY = int(os.getenv("FNG_MIN_ENTRY", "41")) # 진입 허용 최소 F&G 값 _FNG_API_URL = "https://api.alternative.me/fng/?limit=1&format=json" _CACHE_TTL = 3600 # 1시간 _fng_value: int | None = None _fng_cached_at: float = 0.0 _fng_date_str: str = "" def get_fng() -> int: """오늘의 F&G 지수 반환 (0~100). API 실패 시 50(중립) 반환.""" global _fng_value, _fng_cached_at, _fng_date_str now = time.time() if _fng_value is not None and (now - _fng_cached_at) < _CACHE_TTL: return _fng_value try: with urllib.request.urlopen(_FNG_API_URL, timeout=5) as r: data = json.loads(r.read()) entry = data["data"][0] _fng_value = int(entry["value"]) _fng_cached_at = now _fng_date_str = entry.get("timestamp", "") logger.info( f"[F&G] 지수={_fng_value} ({entry.get('value_classification','')}) " f"날짜={datetime.fromtimestamp(int(_fng_date_str)).strftime('%Y-%m-%d') if _fng_date_str else '?'}" ) except Exception as e: logger.warning(f"[F&G] API 조회 실패: {e} → 캐시/중립값 사용") if _fng_value is None: _fng_value = 50 # 폴백: 중립 return _fng_value # type: ignore[return-value] def is_entry_allowed() -> bool: """현재 F&G 기준으로 진입 허용 여부 반환. F&G ≥ FNG_MIN_ENTRY(41) 이면 True. 극공포/공포 구간(< 41)이면 False → 진입 차단. """ fv = get_fng() allowed = fv >= FNG_MIN_ENTRY if not allowed: label = ( "극공포" if fv <= 25 else "공포" if fv <= 40 else "약공포" ) logger.info(f"[F&G] 진입 차단 — F&G={fv} ({label}) < {FNG_MIN_ENTRY}") return allowed