refactor: MVC 구조 분리 + 미사용 파일 archive 정리
- tick_trader.py를 Controller로 축소, 로직을 3개 모듈로 분리: - core/signal.py: 시그널 감지, 지표 계산 (calc_vr, calc_atr, detect_signal) - core/order.py: Upbit 주문 실행 (매수/매도/취소/조회) - core/position_manager.py: 포지션 관리, DB sync, 복구, 청산 조건 - type hints, Google docstring, 구체적 예외 타입 적용 - 50줄 초과 함수 분리 (process_signal, restore_positions) - 미사용 파일 58개 archive/ 폴더로 이동 - README.md 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
71
archive/core/fng.py
Normal file
71
archive/core/fng.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""공포탐욕지수(F&G) 조회 모듈.
|
||||
|
||||
alternative.me API로 일일 F&G 값을 가져와 메모리에 캐시한다.
|
||||
캐시 TTL은 24시간 (F&G는 하루 1회 KST 09:00 업데이트).
|
||||
|
||||
환경변수:
|
||||
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 = 86400 # 24시간 (API는 하루 1회 KST 09:00 업데이트)
|
||||
|
||||
_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.debug(f"[F&G] 진입 차단 — F&G={fv} ({label}) < {FNG_MIN_ENTRY}")
|
||||
return allowed
|
||||
Reference in New Issue
Block a user