Files
auto-stock/backend/app/core/config.py
shuim 408bcce62e fix: 자동매매 신뢰성·소액계좌 안전배치 5종
감사에서 드러난 신뢰성·조용한실패 버그 수정. 전략값(§5)·시그니처(§4) 불변.

① KIS API 신뢰성(korea_investment_api, config): 스로틀에 _throttle_lock 추가로
   thundering herd(8코루틴 동시발사) 제거, API_TIMEOUT 10→20초, circuit_breaker
   임계 15→7, 재시도 백오프 지터. → 시간당 121건 실패의 근원 완화.
② portfolio summary 보존(portfolio): 잔고 API 실패 시 summary가 영영 None으로
   남아 현금 0→매수 스킵되던 것을, 최초 실패 시 holdings 기반 부분 summary로 채움.
③ output2 빈리스트 가드: KIS가 output2=[] 반환 시 IndexError로 MDD 업데이트가
   조용히 스킵(하방보호 비활성)되던 것 방어. swing/scheduler·adaptive_strategy
   (실버그) + daytrading 2곳(정규화). trade_executor:555는 load-bearing 가드라
   의도적 미변경.
④ MIN_BUY_AMOUNT config화+소액완화(trade_executor, config): 하드코딩 50만을
   MIN_BUY_AMOUNT_KRW로. 자산 833만↓ 계좌가 전종목 차단되던 것을 자산 대비로
   완화(큰 계좌는 기존 50만 하한 불변).
⑤ adaptive 서브버그(adaptive_strategy): fallback 시 _last_update_ts 갱신(refresh
   남발 방지), force_mode_override 전체필드(crisis의 max_daily_trades=0 실제 적용),
   get_event_loop→get_running_loop.

검증: code-verifier 통과(CONFIRMED 버그/회귀 없음, 4개 위험지점 안전), pytest 442 통과.
(day_trading.py·entry_service.py는 소량 인접 선재편집 포함 — 검증 완료.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBmzf1HDnvFHLraJ8eaaKJ
2026-09-16 16:59:59 +09:00

444 lines
31 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
애플리케이션 설정 모듈
- pydantic-settings 기반 .env 파일 로드
- DB, Redis, KIS API, AI 프로바이더 등 전역 설정 관리
"""
import os
from pathlib import Path
from pydantic_settings import BaseSettings
from functools import lru_cache
# 프로젝트 루트 디렉토리 (backend의 상위 디렉토리)
BASE_DIR = Path(__file__).resolve().parent.parent.parent
BACKEND_ENV_FILE = BASE_DIR / ".env"
class Settings(BaseSettings):
# ═════════════════════════════════════════════════════════════════════════════
# 서버 & 환경 설정
# ═════════════════════════════════════════════════════════════════════════════
APP_NAME: str = "Auto Stock Trading System"
ENVIRONMENT: str = "development"
DEBUG: bool = False # 운영 기본값 False (.env에서 DEBUG=true로 개발 모드 활성화)
SECRET_KEY: str = "change-this-secret-key" # JWT/WS 서명용
API_KEY: str = "" # API 인증용 (비어있으면 SECRET_KEY 폴백)
BACKEND_HOST: str = "0.0.0.0"
BACKEND_PORT: int = 8000
# ═════════════════════════════════════════════════════════════════════════════
# 데이터베이스 & 메시지 큐
# ═════════════════════════════════════════════════════════════════════════════
DATABASE_URL: str = "postgresql://autostock:autostock_password@localhost:5432/autostock_db"
REDIS_URL: str = "redis://localhost:6379/0"
RABBITMQ_URL: str = "amqp://autostock:autostock_password@localhost:5672/"
# ═════════════════════════════════════════════════════════════════════════════
# KIS API 설정 (Korea Investment Securities)
# ═════════════════════════════════════════════════════════════════════════════
# 모드 선택: "virtual" (모의투자) 또는 "real" (실전투자)
KIS_MODE: str = "virtual"
# 모의투자 설정
KIS_VIRTUAL_APP_KEY: str = ""
KIS_VIRTUAL_APP_SECRET: str = ""
KIS_VIRTUAL_ACCOUNT_NO: str = ""
KIS_VIRTUAL_URL: str = "https://openapivts.koreainvestment.com:29443"
# 실전투자 설정
KIS_REAL_APP_KEY: str = ""
KIS_REAL_APP_SECRET: str = ""
KIS_REAL_ACCOUNT_NO: str = ""
KIS_REAL_URL: str = "https://openapi.koreainvestment.com:9443"
# 실제 사용되는 KIS API 설정 (KIS_MODE에 따라 자동 결정)
KOREA_INVESTMENT_APP_KEY: str = ""
KOREA_INVESTMENT_APP_SECRET: str = ""
KOREA_INVESTMENT_ACCOUNT_NUMBER: str = ""
KOREA_INVESTMENT_API_BASE_URL: str = "https://openapi.koreainvestment.com:9443"
# 레거시 호환 (직접 지정 시 우선)
KIS_APP_KEY: str = ""
KIS_APP_SECRET: str = ""
KIS_ACCOUNT_NO: str = ""
KIS_BASE_URL: str = ""
# WebSocket 관련
KIS_WS_APPROVAL_KEY: str = "" # 수동 지정 시 우선, 비어 있으면 자동 발급
KIS_WS_REAL_URL: str = "wss://openapi.koreainvestment.com:21000/websocket"
KIS_WS_VIRTUAL_URL: str = "wss://openapivts.koreainvestment.com:29443/websocket"
KIS_WS_ENABLED: bool = True # WebSocket 실시간 스트림 활성화
KIS_SYNC_INTERVAL: int = 10 # 구독 목록 동기화 주기 (초)
KIS_RECONNECT_WAIT: int = 5 # 재연결 대기 시간 (초)
KIS_APPROVAL_KEY_TTL: int = 82800 # approval_key 캐시 유효시간 (초, 23시간)
# 기타 외부 API
DATAGOKR_API_KEY: str = "" # 공공데이터포털 금융위원회 주식시세 API
# ═════════════════════════════════════════════════════════════════════════════
# AI & MCP 설정
# ═════════════════════════════════════════════════════════════════════════════
AI_PROVIDER: str = "gemini" # "gemini" 또는 "claude" (CLAUDE.md 1.2)
ANTHROPIC_API_KEY: str = ""
GEMINI_API_KEY: str = ""
GEMINI_MODEL: str = "gemini-2.0-flash"
MCP_SERVER_URL: str = "http://localhost:8001"
MCP_API_KEY: str = "" # MCP 서버 인증 키 (backend -> MCP 통신 시 X-API-Key 헤더)
MCP_VALIDATION_ENABLED: bool = True # 기본 ON — AI 검증 없이 자동매매 차단
MCP_CONFIDENCE_THRESHOLD: float = 0.5 # AI 신뢰도 최소값 (0.0~1.0)
MCP_ANALYSIS_MIN_SCORE: int = 70 # 기본 70점 — .env에서 MCP_ANALYSIS_MIN_SCORE=N 으로 조정
NEWS_API_KEY: str = ""
NEWS_ENABLED: bool = True # 뉴스 수집/감성 분석 활성화
NEWS_COLLECT_INTERVAL: int = 1800 # 뉴스 수집 주기 (초, 기본 30분)
NEWS_CACHE_TTL: int = 1800 # Redis 캐시 TTL (초, 30분)
NEWS_BLOCK_SCORE: float = -0.5 # 이 이하 → 진입 차단
NEWS_REDUCE_SCORE: float = -0.3 # 이 이하 → 포지션 50% 축소
NEWS_MAX_ARTICLES: int = 20 # 종목당 수집할 최대 뉴스 개수
# ═════════════════════════════════════════════════════════════════════════════
# 토큰 & JSON 최적화
# ═════════════════════════════════════════════════════════════════════════════
TOKEN_SAVE_MODE: bool = False # True: JSON 압축 + 차트 30일 제한 (CLAUDE.md 1.2)
# ═════════════════════════════════════════════════════════════════════════════
# 기술적 지표 기본값
# ═════════════════════════════════════════════════════════════════════════════
RSI_PERIOD: int = 14 # RSI 계산 기간
RSI_OVERSOLD_LEVEL: float = 30.0 # RSI 과매도 수준
RSI_OVERBOUGHT_LEVEL: float = 70.0 # RSI 과매수 수준
RSI_RECOVERY_RANGE_MIN: float = 30.0 # RSI 회복 범위 최소값
RSI_RECOVERY_RANGE_MAX: float = 45.0 # RSI 회복 범위 최대값
RSI_STABLE_RANGE_MIN: float = 45.0 # RSI 안정 범위 최소값
RSI_STABLE_RANGE_MAX: float = 60.0 # RSI 안정 범위 최대값
ATR_PERIOD: int = 14 # ATR 계산 기간
BOLLINGER_PERIOD: int = 20 # 볼린저 밴드 기간 (20일)
BOLLINGER_STDDEV: float = 2.0 # 볼린저 밴드 표준편차 배수
MA_SHORT_PERIOD: int = 5 # 단기 이동평균 기간 (5일)
MA_MEDIUM_PERIOD: int = 20 # 중기 이동평균 기간 (20일)
MA_LONG_PERIOD: int = 60 # 장기 이동평균 기간 (60일)
# ═════════════════════════════════════════════════════════════════════════════
# 공통 리스크 관리 (섹션 5)
# ═════════════════════════════════════════════════════════════════════════════
MAX_POSITION_SIZE: float = 0.06 # 백테스트 최적: 종목당 6% (기존 4%, 적응형 모드에서 오버라이드)
MAX_HOLDINGS_COUNT: int = 20 # 동시 보유 최대 20종목 (섹션 5.1)
LEVERAGE_ETF_MAX_RATIO: float = 0.20 # 레버리지/인버스 ETF 최대 20% (섹션 5.1)
# 자본 배분 글로벌 가드 — 전 전략 합산 현금 사용 상한
MAX_TOTAL_CAPITAL_USAGE: float = 0.95 # 전체 자본의 95%까지만 투자 (5% 현금 보유)
# 수수료/세금 (net PnL 계산용)
KR_COMMISSION_RATE: float = 0.00015 # 매수+매도 각 0.015%
KR_SELL_TAX_RATE_KOSPI: float = 0.0018 # 코스피 증권거래세 0.18%
KR_SELL_TAX_RATE_KOSDAQ: float = 0.0023 # 코스닥 증권거래세 0.23%
US_COMMISSION_RATE: float = 0.0 # 미국 주식 수수료 (증권사별 상이)
# 손절(SL) 및 익절(TP) 기본값
STOP_LOSS_PERCENT: float = 7.0 # 백테스트 최적: 저변동 종목 손절 7% (기존 6%, 적응형 모드에서 오버라이드)
STOP_LOSS_HIGH_VOL: float = 10.0 # 고변동 종목 손절 10% (양수로 저장, 사용 시 음수 변환)
TAKE_PROFIT_PERCENT: float = 5.0 # 익절 기본값
ATR_STOP_MULTIPLIER: float = 2.5 # 2.5 × ATR 손절 (섹션 5.2)
ATR_TRAILING_MULTIPLIER: float = 1.5 # 1.5 × ATR 트레일링
# MDD(최대 낙폭) 관리 (섹션 5.3)
MDD_PAUSE_THRESHOLD: float = -10.0 # -10% MDD: 신규진입 중단 + 30% 축소
MDD_FORCE_THRESHOLD: float = -20.0 # -20% MDD: 50% 강제 축소
# 가격 필터 (섹션 5.4)
MIN_PRICE_KRW: int = 5000 # 국내 5,000원 이하 매수 금지
MIN_PRICE_USD: float = 4.0 # 미국 $4 이하 매수 금지
# 최소 매수 금액 가드 (수수료 대비 비효율 방지, trade_executor._execute_buy)
# 큰 계좌는 이 값이 그대로 하한으로 사용되고, 소액 계좌는 자산 대비로 완화된다
# (trade_executor._MIN_BUY_AMOUNT 산출 로직 참고).
MIN_BUY_AMOUNT_KRW: float = 500_000 # 기존 하드코딩 _MIN_BUY_AMOUNT와 동일한 기본값
# 연속 손실 관리 (섹션 5.5)
CONSECUTIVE_LOSS_BAN_COUNT: int = 3 # 3회 연속 손실
CONSECUTIVE_LOSS_BAN_DAYS: int = 30 # 30거래일 진입 금지
# ═════════════════════════════════════════════════════════════════════════════
# 평균회귀(스윙) 전략 (섹션 6)
# ═════════════════════════════════════════════════════════════════════════════
MEAN_REVERSION_RSI_THRESHOLD: float = 46.0 # 백테스트 최적: BULL 진입 RSI < 46 (기존 40)
MEAN_REVERSION_MULTIPLIER_STRONG: float = 1.50 # 강한 신호 배수 (+50%)
MEAN_REVERSION_MULTIPLIER_NORMAL: float = 1.25 # 중간 신호 배수 (+25%)
MEAN_REVERSION_MULTIPLIER_WEAK: float = 1.00 # 약한 신호 배수 (기본값)
MEAN_REVERSION_MAX_CAPITAL: float = 0.30 # 전체 자본의 30% 이내
MEAN_REVERSION_MA200_PERIOD: int = 200 # MA200 기간
MEAN_REVERSION_MA200_LOOKBACK: int = 205 # MA200 계산용 데이터 요구량
MEAN_REVERSION_SLOPE_THRESHOLD: float = 0.001 # MA200 기울기 횡보 판별 (0.1%)
MEAN_REVERSION_ADX_SIDEWAYS: float = 20.0 # ADX < 20 → SIDEWAYS
MEAN_REVERSION_BEAR_RSI_THRESHOLD: float = 35.0 # 백테스트 최적: BEAR 진입 RSI < 35 (기존 30)
MEAN_REVERSION_BEAR_VOLUME_MULT: float = 1.5 # BEAR 거래량 > 1.5 × 평균
MEAN_REVERSION_BB_UPPER_THRESHOLD: float = 0.99 # BB 상단 도달 임계값 (99%)
MEAN_REVERSION_RSI_EXIT_THRESHOLD: float = 55.0 # 백테스트 최적: 1차 익절 RSI >= 55 (기존 50)
MEAN_REVERSION_RSI_FULL_EXIT: float = 70.0 # 백테스트 최적: 전량 청산 RSI >= 70 (기존 65)
MEAN_REVERSION_RSI_STRONG_THRESHOLD: float = 20.0 # RSI <= 20 → 강한 신호
MEAN_REVERSION_RSI_NORMAL_THRESHOLD: float = 25.0 # RSI <= 25 → 중간 신호
MEAN_REVERSION_ADX_STRONG_THRESHOLD: float = 15.0 # ADX < 15 → 추세 약함
MEAN_REVERSION_ADX_STRONG_REDUCE: float = 30.0 # ADX > 30 → 추세 강함
# ═════════════════════════════════════════════════════════════════════════════
# 단기(15분봉) 전략 (섹션 7.2)
# ═════════════════════════════════════════════════════════════════════════════
SHORT_TERM_MIN_ATR_RATIO: float = 0.008 # ATR/close ≥ 0.8% : 거래비용 커버
SHORT_TERM_RSI_LOOKBACK: int = 3 # RSI 반등 확인용 과거 봉 수
SHORT_TERM_VOLUME_MULTIPLIER: float = 1.3 # 거래량 > 1.3배 증가
SHORT_TERM_SKIP_OPEN_HOUR: int = 9 # 장 초반 제외 시간
SHORT_TERM_SKIP_OPEN_MIN: int = 30 # 장 초반 제외 분 (09:30 이전 제외)
SHORT_TERM_SKIP_CLOSE_HOUR: int = 15 # 마감 제외 시간
SHORT_TERM_SKIP_CLOSE_MIN: int = 15 # 마감 제외 분 (15:15 이후 제외)
SHORT_TERM_BB_LOWER_BUFFER: float = 1.01 # BB 하단 근처 버퍼 (+1%)
SHORT_TERM_HARD_STOP_PCT: float = 0.02 # 하드 손절 -2%
SHORT_TERM_ATR_PROFIT_MULTIPLIER: float = 1.5 # 익절 목표 = entry + 1.5×ATR
SHORT_TERM_COMMISSION_RATE: float = 0.003 # 수수료율
SHORT_TERM_SLIPPAGE_RATE: float = 0.002 # 슬리피지율
# ═════════════════════════════════════════════════════════════════════════════
# 데이트레이딩(5분봉) 전략 (섹션 7.3)
# ═════════════════════════════════════════════════════════════════════════════
DAY_TRADE_ENABLED: bool = True # 데이트레이딩 활성화
DAY_TRADE_MAX_POSITIONS: int = 3 # 동시 최대 종목 수
DAY_TRADE_CAPITAL_RATIO: float = 0.25 # 전체 자본 대비 최대 25%
DAY_TRADE_PER_STOCK_RATIO: float = 0.04 # 종목당 최대 4%
DAY_TRADE_MAX_CAPITAL_RATIO: float = 0.04 # 종목당 최대 4% 자본
DAY_TRADE_MAX_RATIO_DT: float = 0.25 # 전체 데이트레이딩 최대 25%
DAY_TRADE_STOP_LOSS_PCT: float = 0.02 # 종목 손절 -2%
DAY_TRADE_DAILY_LOSS_LIMIT: float = -0.03 # 일일 손실 한도 -3%
DAY_TRADE_EOD_FLAT_HOUR: int = 15 # EOD 강제 청산 시간 (15시)
DAY_TRADE_EOD_FLAT_MIN: int = 20 # EOD 강제 청산 분 (20분)
DAY_TRADE_ORB_VOL_MULT: float = 1.5 # ORB 돌파 거래량 배수
DAY_TRADE_TRAILING_TRIGGER: float = 0.01 # 트레일링 발동 최소 수익률 (1%)
DAY_TRADE_RSI_BREAKEVEN_THRESHOLD: float = 40.0 # RSI_BB 진입 기준 (RSI ≤ 40)
DAY_TRADE_ORB_STOP_LOSS_PCT: float = 0.03 # ORB 손절 -3% (기존 2% → 노이즈 손절 방지)
DAY_TRADE_REVERSION_STOP_LOSS_PCT: float = 0.035 # VWAP/RSI_BB 손절 -3.5% (기존 2.5% → 반등 여유 확보)
DAY_TRADE_TRAILING_ATR_MULT: float = 1.5 # 트레일링 ATR 배수 (1.0→1.5 whipsaw 방지)
DAY_TRADE_ORB_BREAKOUT_MARGIN: float = 0.002 # ORB 돌파 최소 여유 +0.2%
DAY_TRADE_COST_PCT: float = 0.005 # 수수료+슬리피지 합산 (매수+매도)
DAY_TRADE_ENTRY_CUTOFF_HOUR: int = 14 # 신규 진입 마감 시간 (CLAUDE.md 7.3: 14:30)
DAY_TRADE_ENTRY_CUTOFF_MIN: int = 30 # 신규 진입 마감 분
DAY_TRADE_EOD_HOUR: int = 15 # EOD 강제 청산 시작 시간
DAY_TRADE_EOD_MIN: int = 20 # EOD 강제 청산 시작 분
DAY_TRADE_ORB_BREAKOUT_VOL_MULT: float = 1.5 # ORB 거래량 배수 (vs 분당평균)
DAY_TRADE_VWAP_PRICE_LOWER: float = 0.99 # VWAP -1% 하단 경계
DAY_TRADE_VWAP_PRICE_UPPER: float = 1.005 # VWAP +0.5% 상단 경계
DAY_TRADE_VWAP_RSI_THRESHOLD: float = 45.0 # VWAP RSI <= 45 (기존 40 → 45: 5분봉에서 40 이하는 드뭄)
DAY_TRADE_RSI_BB_RSI_THRESHOLD: float = 35.0 # RSI_BB RSI <= 35 (기존 30 → 35: 5분봉에서 30 이하는 극히 드뭄)
DAY_TRADE_RSI_BB_BB_LOWER_BUFFER: float = 1.01 # RSI_BB BB 하단 +1% 버퍼
DAY_TRADE_VOLUME_MULTIPLIER: float = 1.3 # 거래량 배수 (1.3배 이상)
DAY_TRADE_ORB_ATR_FLOOR_MULT: float = 1.0 # ORB ATR 손절 기본 배수
DAY_TRADE_VWAP_ATR_MULT: float = 1.5 # VWAP ATR 손절 배수
DAY_TRADE_RSI_BB_ATR_MULT: float = 1.5 # RSI_BB ATR 손절 배수
DAY_TRADE_TARGET_MULTIPLIER: float = 1.5 # 목표가 = entry + risk × 1.5R (기존 2.0 → 현실적 목표)
DAY_TRADE_TIME_STOP_MINUTES: int = 30 # 시간 손절 (30분)
DAY_TRADE_TIME_STOP_MIN_GAIN: float = -0.003 # 시간 손절: -0.3% 미만 시 청산 (기존 +0.5% → 수익 중이면 보유 유지)
DAY_TRADE_ORB_MARGIN_CHECK: float = 0.03 # ORB 목표가 최소 마진 (3%)
DAY_TRADE_TRAILING_DEFAULT: float = 0.02 # 트레일링 기본값 -2% (ATR 없을 때)
DAY_TRADE_TRAILING_MIN_HOLD_SEC: int = 300 # 트레일링 활성화 최소 보유 시간 (초, 5분)
DAY_TRADE_MIN_STOP_LOSS_PCT: float = 0.02 # 최소 손절폭 -2.0% (기존 1.0% → 노이즈 손절 방지)
# ═════════════════════════════════════════════════════════════════════════════
# 코스닥 스캐너 (섹션 7.1)
# ═════════════════════════════════════════════════════════════════════════════
# (코스닥 스캐너 파라미터 추가 필요 시 여기에 추가)
# ═════════════════════════════════════════════════════════════════════════════
# 미국 주식 매매 (섹션 7.4)
# ═════════════════════════════════════════════════════════════════════════════
US_TRADING_ENABLED: bool = False # 미국 주식 자동매매 활성화 (기본 OFF)
US_MAX_CAPITAL_RATIO: float = 0.30 # 전체 자본 대비 미국 주식 최대 비중 (30%)
US_SCAN_INTERVAL: int = 300 # 미국장 스캔 간격 (초, 기본 5분)
# ═════════════════════════════════════════════════════════════════════════════
# 필터 & 시장 국면 (섹션 5.4)
# ═════════════════════════════════════════════════════════════════════════════
TREND_FILTER_ENABLED: bool = False
TREND_MA_SHORT: int = 20
TREND_MA_LONG: int = 60
TREND_TOLERANCE_PCT: float = 0.005
# 실적 필터 (Phase 2) — 기본 OFF. point-in-time 실적 데이터로 백테스트 검증 전엔
# 켜지 말 것(CLAUDE.md §8.1-4: 미검증 전략 실전 투입 금지). 현재 데이터원=네이버.
FUNDAMENTAL_FILTER_ENABLED: bool = False
FUNDAMENTAL_BLOCK_NEGATIVE_EPS: bool = True # 적자(EPS<0) 종목 진입 차단
FUNDAMENTAL_MIN_UPSIDE_PCT: float = 0.0 # 컨센서스 목표가 상승여력 최소치(%). 이하면 차단
OVERHEAT_FILTER_ENABLED: bool = True # 급등 과열 필터
OVERHEAT_3DAY_MAX: float = 15.0
OVERHEAT_5DAY_MAX: float = 20.0
OVERHEAT_10DAY_MAX: float = 30.0
OVERHEAT_DISPARITY_MAX: float = 108.0
BEAR_MARKET_FILTER_ENABLED: bool = True # 하락장 방어 필터
BEAR_MARKET_MIN_SCORE: int = 5
BEAR_MARKET_MAX_TRADES: int = 3
BEAR_MARKET_POSITION_RATIO: float = 0.5
# ═════════════════════════════════════════════════════════════════════════════
# 시장 상황 & 비상 정지
# ═════════════════════════════════════════════════════════════════════════════
TRADING_HALTED: bool = False # .env에서 TRADING_HALTED=true 로 즉시 전체 매수 중단
MARKET_CRASH_HALT_PCT: float = -3.0 # 지수 당일 -3% 이상 급락 시 자동 매수 차단
# ═════════════════════════════════════════════════════════════════════════════
# 일반 트레이딩 설정
# ═════════════════════════════════════════════════════════════════════════════
AUTO_TRADE_ENABLED: bool = True
MAX_DAILY_TRADES: int = 5
MAX_DAILY_BUYS_PER_STOCK: int = 2 # 종목당 일일 최대 매수 횟수
ANALYSIS_STOCK_COUNT: int = 300 # 배치 분석 종목 수
AUTO_TRADE_TOP_N_PER_RULE: int = 2 # 규칙당 자동매매 실행 최대 종목 수
# ═════════════════════════════════════════════════════════════════════════════
# 감시 목록 (watchlist.py)
# ═════════════════════════════════════════════════════════════════════════════
WATCHLIST_INIT_SIZE: int = 150 # 아침 배치 초기화 종목 수
WATCHLIST_MAX_SIZE: int = 150 # 최대 관리 종목 수
WATCHLIST_ENTRY_SCORE: int = 60 # 장중 신규 진입 기준 점수
WATCHLIST_EXIT_SCORE: int = 20 # 장중 제거 기준 점수
# ═════════════════════════════════════════════════════════════════════════════
# ML / 머신러닝
# ═════════════════════════════════════════════════════════════════════════════
ML_TF_ENABLED: bool = True # ML 모델 기반 보조지표 사용
ML_ENSEMBLE_ENABLED: bool = True # ML Ensemble (LightGBM + LSTM + RF)
ML_ENSEMBLE_MODEL_DIR: str = "models/ensemble"
ML_ENSEMBLE_RULE_WEIGHT: float = 0.3 # 룰베이스 스코어 비중
ML_ENSEMBLE_ML_WEIGHT: float = 0.7 # ML 스코어 비중
ML_ENSEMBLE_BUY_THRESHOLD: int = 60 # 매수 최소 앙상블 스코어 (0~100)
ML_ENSEMBLE_CONFIDENCE_MIN: float = 0.5 # 최소 신뢰도
# ═════════════════════════════════════════════════════════════════════════════
# API & 네트워크
# ═════════════════════════════════════════════════════════════════════════════
API_MAX_RETRIES: int = 3 # 최대 재시도 횟수
API_RETRY_DELAY: float = 1.5 # 초기 재시도 대기 시간 (초)
API_RETRY_BACKOFF: float = 2.0 # 지수 백오프 배율
API_MIN_REQUEST_INTERVAL: float = 0.15 # 최소 요청 간격 (초) — 기존 0.5→0.15 (캐시+커넥션풀로 부하 감소)
API_MAX_CONCURRENT_REQUESTS: int = 8 # 동시 최대 요청 수 (기존 4→8, 커넥션풀로 안정적)
API_TIMEOUT: float = 20.0 # API 타임아웃 (초) — 기존 30→10→20 (KIS 느린 조회가 10초 넘어 ReadTimeout 폭증 완화, connect는 10초 유지)
API_ERROR_THRESHOLD: float = 0.3 # 오류율 임계값 (30%)
API_MONITORING_WINDOW: int = 100 # 모니터링 윈도우 (최근 N개 요청)
# ═════════════════════════════════════════════════════════════════════════════
# Telegram 알림
# ═════════════════════════════════════════════════════════════════════════════
TELEGRAM_BOT_TOKEN: str = "" # 텔레그램 봇 토큰 (빈 문자열 = 비활성화)
TELEGRAM_CHAT_ID: str = "" # 알림 수신 채팅 ID (빈 문자열 = 비활성화)
# ═════════════════════════════════════════════════════════════════════════════
# 로깅 & 모니터링
# ═════════════════════════════════════════════════════════════════════════════
LOG_LEVEL: str = "INFO"
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._apply_kis_mode()
self._apply_docker_db_override()
def _apply_docker_db_override(self):
"""If running inside Docker, convert localhost in DATABASE_URL to the
postgres service hostname.
Users often copy `.env.example` which defaults to
`postgresql://...@localhost:5432/...`. That works on the host machine,
but inside the backend container `localhost` refers to the container
itself, not the Postgres server. When `/.dockerenv` exists we assume the
code is running in Docker and automatically patch the URL.
"""
if os.path.exists("/.dockerenv"):
# simple string replacement; pydantic does not throw if format
# changes after initialization
if "localhost" in self.DATABASE_URL:
self.DATABASE_URL = self.DATABASE_URL.replace("localhost", "postgres")
def _apply_kis_mode(self):
"""KIS_MODE에 따라 모의/실전 API 설정을 자동 선택"""
is_real = self.KIS_MODE.lower() == "real"
# 1순위: KIS_MODE에 따른 VIRTUAL/REAL 변수
if is_real:
mode_key = self.KIS_REAL_APP_KEY
mode_secret = self.KIS_REAL_APP_SECRET
mode_account = self.KIS_REAL_ACCOUNT_NO
mode_url = self.KIS_REAL_URL
else:
mode_key = self.KIS_VIRTUAL_APP_KEY
mode_secret = self.KIS_VIRTUAL_APP_SECRET
mode_account = self.KIS_VIRTUAL_ACCOUNT_NO
mode_url = self.KIS_VIRTUAL_URL
# 2순위: 레거시 KIS_APP_KEY 등 (직접 지정된 경우)
fallback_key = self.KIS_APP_KEY
fallback_secret = self.KIS_APP_SECRET
fallback_account = self.KIS_ACCOUNT_NO
fallback_url = self.KIS_BASE_URL
# 최종 적용 (mode > legacy > KOREA_INVESTMENT_ 기존값)
self.KOREA_INVESTMENT_APP_KEY = mode_key or fallback_key or self.KOREA_INVESTMENT_APP_KEY
self.KOREA_INVESTMENT_APP_SECRET = mode_secret or fallback_secret or self.KOREA_INVESTMENT_APP_SECRET
self.KOREA_INVESTMENT_ACCOUNT_NUMBER = mode_account or fallback_account or self.KOREA_INVESTMENT_ACCOUNT_NUMBER
self.KOREA_INVESTMENT_API_BASE_URL = mode_url or fallback_url or self.KOREA_INVESTMENT_API_BASE_URL
@property
def is_virtual_trading(self) -> bool:
"""모의투자 모드 여부"""
return self.KIS_MODE.lower() != "real"
class Config:
env_file = str(BACKEND_ENV_FILE) if BACKEND_ENV_FILE.exists() else ".env"
case_sensitive = True
extra = "ignore" # .env 파일의 추가 필드 무시
env_file_encoding = "utf-8"
def validate_required(self) -> list[str]:
"""필수 환경변수 누락 여부를 검사하고 경고 목록을 반환한다.
시작 시 lifespan에서 호출하여 운영자에게 조기 경보를 제공한다.
누락 시 예외를 발생시키지 않으므로 서비스 기동은 계속된다.
"""
warnings: list[str] = []
# KIS API 키: 현재 모드에 맞는 키 필수
if not self.KOREA_INVESTMENT_APP_KEY:
warnings.append(
f"KIS_{self.KIS_MODE.upper()}_APP_KEY 미설정 — "
f"{'KIS_VIRTUAL_APP_KEY' if self.is_virtual_trading else 'KIS_REAL_APP_KEY'} 확인 필요"
)
if not self.KOREA_INVESTMENT_APP_SECRET:
warnings.append(
f"KIS_{self.KIS_MODE.upper()}_APP_SECRET 미설정"
)
if not self.KOREA_INVESTMENT_ACCOUNT_NUMBER:
warnings.append(
f"KIS_{self.KIS_MODE.upper()}_ACCOUNT_NO 미설정"
)
# 약한 시크릿 키 감지
weak_secrets = {"change-this-secret-key", "auto_stock_secret_key_change_in_production_2024", "secret"}
if self.SECRET_KEY.lower() in weak_secrets or len(self.SECRET_KEY) < 32:
warnings.append(
"SECRET_KEY가 약합니다! 운영 환경에서는 반드시 강력한 키를 사용하세요: "
"python -c \"import secrets; print(secrets.token_urlsafe(64))\""
)
# 운영 환경 DEBUG 모드 경고
if self.DEBUG and not self.is_virtual_trading:
warnings.append("⚠️ 실전투자 모드(KIS_MODE=real)에서 DEBUG=true — 보안 위험!")
# DB / Redis
if "localhost" in self.DATABASE_URL and os.path.exists("/.dockerenv"):
warnings.append("DATABASE_URL에 localhost 포함 — Docker 환경에서 postgres 호스트명 확인 필요")
# MCP 검증 활성화 시 AI 키 필요
if self.MCP_VALIDATION_ENABLED and not self.ANTHROPIC_API_KEY and not self.GEMINI_API_KEY:
warnings.append("MCP_VALIDATION_ENABLED=True 이지만 ANTHROPIC_API_KEY / GEMINI_API_KEY 미설정")
return warnings
@lru_cache()
def get_settings() -> Settings:
return Settings()
settings = get_settings()