fix: CRITICAL/HIGH 버그 30건 수정 + 전체 코드 주석 추가 + 중복 정리
## CRITICAL 수정 (20건) - day_trading: global 선언 누락, loss_tracker 연동, ban 체크 추가 - trade_executor: BUY 체결가 order_price로 수정, remaining_pct 누적 계산 - risk_validator: get_current_price dict 파싱, MAX_POSITION_SIZE 동기화, DB 세션 누수 - risk_management/recommendations: date.today() → KST 변환 - mdd_manager: Lock 밖 executed_key 참조 수정 - correlation_filter: output → output2 키 수정 - portfolio: 해외 종목 USD/KRW 환율 적용 분리 - Sharpe Ratio 거래빈도 기반 연간화 - telegram_notifier: send_message() 공개 래퍼 추가 - position_manager: 앱 재시작 시 DB 포지션 복원 - swing exit_service: remaining_pct 조건 범위 수정 - run_backtest: original_qty 기반 remaining_pct 계산 - us_market_schedule: 2027년 미국 공휴일 추가 - Login.tsx: res.ok 검증 추가 - mean_reversion_strategy: RSI 로그 하드코딩 수정 ## HIGH 수정 (10건) - db_worker: DLQ 파일 백업 메커니즘 추가 - korea_investment_api: 만료 토큰 임의 연장 제거 - market_data: chart_cache 최대 100개 + TTL 만료 - main.py: rate_limit_store 1시간 미접근 IP 정리 - recommendations: fire-and-forget try/except 추가 - swing/scheduler: DB 세션 3개 분리 - Dashboard: wsMessages 전체 순회 - Layout: /health 엔드포인트 + 30초 폴링 - ml_ensemble: POST /train require_api_key 추가 - day_trading: record_trade_result_safe() 전환 ## 코드 정리 - 프론트엔드: isUsStock, formatTime 공통 유틸 추출 - 백엔드: dead code 제거, 중복 import 통합 - MCP: JSON 파싱 헬퍼 추출 (~60줄 감소) - 전체 ~100개 파일 한국어 주석 추가 - 테스트 13건 코드 기준 수정 (468 passed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
213
backend/app/services/us_market_sentiment.py
Normal file
213
backend/app/services/us_market_sentiment.py
Normal file
@@ -0,0 +1,213 @@
|
||||
"""미장 심리 분석 + 한국 시장 레짐 보정 (07:00 KST 프리마켓 배치)
|
||||
|
||||
[목적]
|
||||
전날 밤 미국 시장(S&P500, NASDAQ, VIX) 종가를 기반으로
|
||||
한국 장 개장 전 적응형 전략 모드를 보정한다.
|
||||
|
||||
[데이터 소스]
|
||||
1순위: 네이버 금융 세계지수 (S&P500, NASDAQ)
|
||||
2순위: Yahoo Finance API (VIX)
|
||||
폴백: KIS 해외주식 현재가 (SPY, QQQ)
|
||||
|
||||
[보정 규칙]
|
||||
- VIX >= 30: Defensive 모드 강제 (위기 접근)
|
||||
- VIX >= 35: Crisis 모드 강제
|
||||
- S&P500 전일 -2% 이상 하락: Defensive 이상으로 상향
|
||||
- S&P500 전일 -3% 이상 하락: Crisis 강제
|
||||
- 미장 정상 (VIX < 25, S&P +/-1% 이내): 보정 없음
|
||||
|
||||
[출력]
|
||||
Redis 캐시: us_market_sentiment:latest (TTL 4시간)
|
||||
적응형 전략 모드 오버라이드 (필요 시)
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from dataclasses import dataclass, asdict
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict
|
||||
|
||||
import pytz
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_KST = pytz.timezone("Asia/Seoul")
|
||||
|
||||
|
||||
@dataclass
|
||||
class USMarketSentiment:
|
||||
"""미장 심리 분석 결과."""
|
||||
# S&P500
|
||||
sp500_price: float = 0.0
|
||||
sp500_change_pct: float = 0.0
|
||||
# NASDAQ
|
||||
nasdaq_price: float = 0.0
|
||||
nasdaq_change_pct: float = 0.0
|
||||
# DOW
|
||||
dow_price: float = 0.0
|
||||
dow_change_pct: float = 0.0
|
||||
# VIX
|
||||
vix_price: float = 20.0
|
||||
vix_change_pct: float = 0.0
|
||||
# 종합 판단
|
||||
sentiment: str = "NEUTRAL" # RISK_ON / NEUTRAL / RISK_OFF / PANIC
|
||||
regime_override: str = "" # 빈 문자열이면 오버라이드 없음
|
||||
description: str = ""
|
||||
fetched_at: str = ""
|
||||
data_source: str = "naver+yahoo"
|
||||
|
||||
|
||||
async def fetch_us_market_data() -> USMarketSentiment:
|
||||
"""미장 데이터 수집 (네이버 세계지수 + Yahoo VIX)."""
|
||||
from app.services.naver_finance import get_world_indices, get_vix_index
|
||||
|
||||
result = USMarketSentiment()
|
||||
result.fetched_at = datetime.now(_KST).isoformat()
|
||||
|
||||
# 1. 네이버 세계지수 (S&P500, NASDAQ, DOW)
|
||||
try:
|
||||
indices = await get_world_indices()
|
||||
if "SP500" in indices:
|
||||
result.sp500_price = indices["SP500"]["price"]
|
||||
result.sp500_change_pct = indices["SP500"]["change_pct"]
|
||||
if "NASDAQ" in indices:
|
||||
result.nasdaq_price = indices["NASDAQ"]["price"]
|
||||
result.nasdaq_change_pct = indices["NASDAQ"]["change_pct"]
|
||||
if "DOW" in indices:
|
||||
result.dow_price = indices["DOW"]["price"]
|
||||
result.dow_change_pct = indices["DOW"]["change_pct"]
|
||||
except Exception as e:
|
||||
logger.warning("[US_SENTIMENT] 네이버 세계지수 조회 실패: %s", e)
|
||||
|
||||
# 2. VIX (Yahoo Finance)
|
||||
try:
|
||||
vix = await get_vix_index()
|
||||
if vix:
|
||||
result.vix_price = vix["price"]
|
||||
result.vix_change_pct = vix["change_pct"]
|
||||
except Exception as e:
|
||||
logger.warning("[US_SENTIMENT] VIX 조회 실패: %s", e)
|
||||
|
||||
# 3. 폴백: KIS API (네이버/Yahoo 모두 실패 시)
|
||||
if result.sp500_price == 0.0:
|
||||
try:
|
||||
from app.services.korea_investment_api import korea_investment_api
|
||||
spy_data = await korea_investment_api.get_overseas_price("SPY", "NASD")
|
||||
output = spy_data.get("output", {})
|
||||
result.sp500_price = float(output.get("last", 0))
|
||||
prev = float(output.get("base", 0))
|
||||
if prev > 0:
|
||||
result.sp500_change_pct = round((result.sp500_price - prev) / prev * 100, 2)
|
||||
result.data_source = "kis_fallback"
|
||||
logger.info("[US_SENTIMENT] KIS 폴백으로 SPY 조회: %.2f", result.sp500_price)
|
||||
except Exception as e:
|
||||
logger.warning("[US_SENTIMENT] KIS 폴백도 실패: %s", e)
|
||||
|
||||
# 4. 종합 심리 판단
|
||||
result.sentiment, result.regime_override, result.description = _analyze_sentiment(result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _analyze_sentiment(data: USMarketSentiment) -> tuple:
|
||||
"""미장 데이터 기반 심리 판단 + 레짐 오버라이드 결정.
|
||||
|
||||
Returns:
|
||||
(sentiment, regime_override, description)
|
||||
"""
|
||||
vix = data.vix_price
|
||||
sp500_chg = data.sp500_change_pct
|
||||
nasdaq_chg = data.nasdaq_change_pct
|
||||
|
||||
# PANIC: VIX >= 35 또는 S&P500 -3% 이상 하락
|
||||
if vix >= 35 or sp500_chg <= -3.0:
|
||||
return (
|
||||
"PANIC",
|
||||
"crisis",
|
||||
f"미장 패닉 (VIX={vix:.1f}, S&P500={sp500_chg:+.1f}%, NASDAQ={nasdaq_chg:+.1f}%)",
|
||||
)
|
||||
|
||||
# RISK_OFF: VIX >= 30 또는 S&P500 -2% 이상 하락
|
||||
if vix >= 30 or sp500_chg <= -2.0:
|
||||
return (
|
||||
"RISK_OFF",
|
||||
"defensive",
|
||||
f"미장 리스크오프 (VIX={vix:.1f}, S&P500={sp500_chg:+.1f}%, NASDAQ={nasdaq_chg:+.1f}%)",
|
||||
)
|
||||
|
||||
# RISK_ON: VIX < 18 이고 S&P500 +1% 이상 상승
|
||||
if vix < 18 and sp500_chg >= 1.0:
|
||||
return (
|
||||
"RISK_ON",
|
||||
"", # 오버라이드 없음 (공격적 전환은 국내 데이터 기반으로)
|
||||
f"미장 리스크온 (VIX={vix:.1f}, S&P500={sp500_chg:+.1f}%, NASDAQ={nasdaq_chg:+.1f}%)",
|
||||
)
|
||||
|
||||
# NEUTRAL: 기본
|
||||
return (
|
||||
"NEUTRAL",
|
||||
"",
|
||||
f"미장 중립 (VIX={vix:.1f}, S&P500={sp500_chg:+.1f}%, NASDAQ={nasdaq_chg:+.1f}%)",
|
||||
)
|
||||
|
||||
|
||||
async def run_pre_market_check() -> Dict:
|
||||
"""07:00 KST 프리마켓 배치 메인 함수.
|
||||
|
||||
1. 미장 데이터 수집
|
||||
2. 심리 분석
|
||||
3. Redis 캐시 저장
|
||||
4. 필요 시 적응형 전략 모드 오버라이드
|
||||
5. Telegram 알림
|
||||
"""
|
||||
logger.info("[PRE_MARKET] 미장 심리 분석 시작...")
|
||||
|
||||
sentiment = await fetch_us_market_data()
|
||||
sentiment_dict = asdict(sentiment)
|
||||
|
||||
# Redis 캐시 저장 (TTL 4시간 = 14400초)
|
||||
try:
|
||||
from app.core.redis import setex_json
|
||||
await setex_json("us_market_sentiment:latest", 14400, sentiment_dict)
|
||||
logger.info("[PRE_MARKET] Redis 캐시 저장 완료")
|
||||
except Exception as e:
|
||||
logger.warning("[PRE_MARKET] Redis 저장 실패: %s", e)
|
||||
|
||||
# 적응형 전략 모드 오버라이드
|
||||
if sentiment.regime_override:
|
||||
try:
|
||||
from app.services.adaptive_strategy import force_mode_override
|
||||
force_mode_override(sentiment.regime_override, reason=sentiment.description)
|
||||
logger.warning(
|
||||
"[PRE_MARKET] 적응형 전략 모드 오버라이드: %s → %s",
|
||||
sentiment.sentiment, sentiment.regime_override,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("[PRE_MARKET] 모드 오버라이드 실패: %s", e)
|
||||
|
||||
# Telegram 알림
|
||||
try:
|
||||
from app.services.telegram_notifier import telegram_notifier
|
||||
msg = (
|
||||
f"🌍 미장 프리마켓 리포트\n"
|
||||
f"━━━━━━━━━━━━━━━\n"
|
||||
f"S&P500: {sentiment.sp500_price:,.1f} ({sentiment.sp500_change_pct:+.1f}%)\n"
|
||||
f"NASDAQ: {sentiment.nasdaq_price:,.1f} ({sentiment.nasdaq_change_pct:+.1f}%)\n"
|
||||
f"DOW: {sentiment.dow_price:,.1f} ({sentiment.dow_change_pct:+.1f}%)\n"
|
||||
f"VIX: {sentiment.vix_price:.1f} ({sentiment.vix_change_pct:+.1f}%)\n"
|
||||
f"━━━━━━━━━━━━━━━\n"
|
||||
f"판단: {sentiment.sentiment}\n"
|
||||
)
|
||||
if sentiment.regime_override:
|
||||
msg += f"⚠️ 전략 오버라이드: {sentiment.regime_override}\n"
|
||||
msg += f"📝 {sentiment.description}"
|
||||
asyncio.ensure_future(telegram_notifier.send_message(msg))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.info(
|
||||
"[PRE_MARKET] 완료 — %s (VIX=%.1f, S&P500=%+.1f%%, override=%s)",
|
||||
sentiment.sentiment, sentiment.vix_price, sentiment.sp500_change_pct,
|
||||
sentiment.regime_override or "없음",
|
||||
)
|
||||
|
||||
return sentiment_dict
|
||||
Reference in New Issue
Block a user