353 lines
18 KiB
Python
353 lines
18 KiB
Python
"""
|
||
애플리케이션 설정 모듈
|
||
|
||
- 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):
|
||
# Application
|
||
APP_NAME: str = "Auto Stock Trading System"
|
||
ENVIRONMENT: str = "development"
|
||
DEBUG: bool = False # 운영 기본값 False (.env에서 DEBUG=true로 개발 모드 활성화)
|
||
SECRET_KEY: str = "change-this-secret-key"
|
||
|
||
# Server
|
||
BACKEND_HOST: str = "0.0.0.0"
|
||
BACKEND_PORT: int = 8000
|
||
|
||
# Database
|
||
DATABASE_URL: str = "postgresql://autostock:autostock_password@localhost:5432/autostock_db"
|
||
|
||
# Redis
|
||
REDIS_URL: str = "redis://localhost:6379/0"
|
||
|
||
# RabbitMQ
|
||
RABBITMQ_URL: str = "amqp://autostock:autostock_password@localhost:5672/"
|
||
|
||
# Korea Investment API - 모드 선택
|
||
KIS_MODE: str = "virtual" # "virtual" (모의투자) 또는 "real" (실전투자)
|
||
|
||
# 모의투자 설정
|
||
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 = ""
|
||
|
||
# 공공데이터포털 금융위원회 주식시세 API (apis.data.go.kr)
|
||
DATAGOKR_API_KEY: str = ""
|
||
|
||
# WebSocket 접속키 (수동 지정 시 우선, 비어 있으면 자동 발급)
|
||
KIS_WS_APPROVAL_KEY: str = ""
|
||
|
||
# KIS WebSocket URL (.env에서 오버라이드 가능)
|
||
# KIS 공식 실시간 시세 WebSocket 엔드포인트
|
||
# 경로: /websocket (또는 /cgi-md5 in some cases)
|
||
KIS_WS_REAL_URL: str = "wss://openapi.koreainvestment.com:21000/websocket"
|
||
KIS_WS_VIRTUAL_URL: str = "wss://openapivts.koreainvestment.com:29443/websocket"
|
||
|
||
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"
|
||
|
||
# Claude AI
|
||
ANTHROPIC_API_KEY: str = ""
|
||
|
||
# MCP Server
|
||
MCP_SERVER_URL: str = "http://localhost:8001"
|
||
# MCP AI 검증 (자동매매 전 AI 최종 판단)
|
||
MCP_VALIDATION_ENABLED: bool = True # 기본 ON — AI 검증 없이 자동매매 차단
|
||
MCP_CONFIDENCE_THRESHOLD: float = 0.5 # AI 신뢰도 최소값 (0.0~1.0)
|
||
# MCP 장중 배치 AI 분석 최소 점수 (이 점수 이상인 종목만 AI 분석 호출)
|
||
MCP_ANALYSIS_MIN_SCORE: int = 70 # 기본 70점 — .env에서 MCP_ANALYSIS_MIN_SCORE=N 으로 조정
|
||
|
||
# News API
|
||
NEWS_API_KEY: str = ""
|
||
|
||
# Logging
|
||
LOG_LEVEL: str = "INFO"
|
||
|
||
# Trading Settings (CLAUDE.md 섹션 6 기반)
|
||
MAX_POSITION_SIZE: float = 0.04 # 1종목당 최대 3-4% (섹션 6.1)
|
||
MAX_DAILY_TRADES: int = 5
|
||
STOP_LOSS_PERCENT: float = -6.0 # 저변동 종목 손절 -6% (섹션 6.2)
|
||
TAKE_PROFIT_PERCENT: float = 5.0 # 익절 기본값
|
||
AUTO_TRADE_ENABLED: bool = True
|
||
ANALYSIS_STOCK_COUNT: int = 150 # 배치 분석 종목 수 (.env로 조정 가능)
|
||
AUTO_TRADE_TOP_N_PER_RULE: int = 2 # 규칙당 자동매매 실행 최대 종목 수
|
||
|
||
# 포지션 규칙 (섹션 6.1)
|
||
MAX_HOLDINGS_COUNT: int = 20 # 동시 보유 최대 20종목
|
||
LEVERAGE_ETF_MAX_RATIO: float = 0.20 # 레버리지/인버스 ETF 20% 이내
|
||
|
||
# ATR 기반 손절/트레일링 (섹션 6.2)
|
||
STOP_LOSS_HIGH_VOL: float = -10.0 # 고변동 종목 손절 -10%
|
||
ATR_STOP_MULTIPLIER: float = 2.5 # 2.5 × ATR 손절
|
||
ATR_TRAILING_MULTIPLIER: float = 1.5 # 1.5 × ATR 트레일링
|
||
|
||
# MDD 관리 (섹션 6.3)
|
||
MDD_PAUSE_THRESHOLD: float = -10.0 # -10% MDD: 신규진입 중단 + 30% 축소
|
||
MDD_FORCE_THRESHOLD: float = -20.0 # -20% MDD: 50% 강제 축소
|
||
|
||
# 가격 필터 (섹션 6.4)
|
||
MIN_PRICE_KRW: int = 5000 # 국내 5,000원 이하 매수 금지
|
||
MIN_PRICE_USD: float = 4.0 # 미국 $4 이하 매수 금지
|
||
|
||
# 연속 손실 관리 (섹션 7.4)
|
||
CONSECUTIVE_LOSS_BAN_COUNT: int = 3 # 3회 연속 손실
|
||
CONSECUTIVE_LOSS_BAN_DAYS: int = 30 # 30거래일 진입 금지
|
||
|
||
# 평균회귀 전략 자본 배분 (섹션 7.5)
|
||
MEAN_REVERSION_MAX_CAPITAL: float = 0.30 # 전체 자본의 30% 이내
|
||
|
||
# Trend filter (MA-based)
|
||
TREND_FILTER_ENABLED: bool = False
|
||
TREND_MA_SHORT: int = 20
|
||
TREND_MA_LONG: int = 60
|
||
TREND_TOLERANCE_PCT: float = 0.005
|
||
|
||
# 급등 과열 필터 (고점 추격 매수 방지)
|
||
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% 이상 급락 시 자동 매수 차단
|
||
|
||
# 데이트레이딩 설정 (CLAUDE.md 섹션 8.2)
|
||
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.10 # 종목당 최대 10%
|
||
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.015 # 트레일링 발동 수익률
|
||
|
||
# ML / TensorFlow integration toggle
|
||
ML_TF_ENABLED: bool = True # ML 모델 기반 보조지표 사용
|
||
|
||
# ML Ensemble (LightGBM + LSTM + RF)
|
||
ML_ENSEMBLE_ENABLED: bool = True
|
||
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 Rate Limiting & Retry Settings (모의투자 API 안전 운영)
|
||
API_MAX_RETRIES: int = 3 # 최대 재시도 횟수
|
||
API_RETRY_DELAY: float = 1.5 # 초기 재시도 대기 시간 (초)
|
||
API_RETRY_BACKOFF: float = 2.0 # 지수 백오프 배율
|
||
API_MIN_REQUEST_INTERVAL: float = 0.5 # 최소 요청 간격 (초) - 초당 2회 권장
|
||
API_MAX_CONCURRENT_REQUESTS: int = 4 # 동시 최대 요청 수 (batch와 UI 요청 동시 처리)
|
||
API_TIMEOUT: float = 30.0 # API 타임아웃 (초)
|
||
|
||
# Monitoring & Alerting
|
||
API_ERROR_THRESHOLD: float = 0.3 # 오류율 임계값 (30%)
|
||
API_MONITORING_WINDOW: int = 100 # 모니터링 윈도우 (최근 N개 요청)
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# 데이트레이딩 세부 파라미터 (day_trading_strategy.py)
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
DAY_TRADE_MAX_CAPITAL_RATIO: float = 0.10 # 종목당 최대 10% 자본
|
||
DAY_TRADE_MAX_RATIO_DT: float = 0.25 # 전체 데이트레이딩 최대 25%
|
||
DAY_TRADE_RSI_BREAKEVEN_THRESHOLD: float = 40.0 # RSI_BB 진입 기준 (5분봉 RSI ≤ 40)
|
||
DAY_TRADE_ORB_STOP_LOSS_PCT: float = 0.02 # ORB 손절 -2%
|
||
DAY_TRADE_REVERSION_STOP_LOSS_PCT: float = 0.025 # VWAP/RSI_BB 손절 -2.5%
|
||
DAY_TRADE_TRAILING_TRIGGER: float = 0.015 # 트레일링 발동 +1.5%
|
||
DAY_TRADE_TRAILING_ATR_MULT: float = 1.0 # 트레일링 ATR 배수
|
||
DAY_TRADE_ORB_BREAKOUT_MARGIN: float = 0.002 # ORB 돌파 최소 여유 +0.2%
|
||
DAY_TRADE_COST_PCT: float = 0.005 # 수수료+슬리피지 합산 (매수+매도)
|
||
DAY_TRADE_ENTRY_CUTOFF_HOUR: int = 15 # 신규 진입 마감 시간 (15시)
|
||
DAY_TRADE_ENTRY_CUTOFF_MIN: int = 0 # 신규 진입 마감 분
|
||
DAY_TRADE_EOD_HOUR: int = 15 # EOD 강제 청산 시작 시간
|
||
DAY_TRADE_EOD_MIN: int = 20 # EOD 강제 청산 시작 분
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# 단기 전략 (short_term_strategy.py)
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
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.5 # 거래량 필터 배수 (1.5배 이상)
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# 평균회귀 전략 (mean_reversion_strategy.py)
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
MEAN_REVERSION_RSI_THRESHOLD: float = 30.0 # RSI 과매도 기준 (≤30)
|
||
MEAN_REVERSION_MULTIPLIER_STRONG: float = 1.50 # 강한 신호 배수 (+50%)
|
||
MEAN_REVERSION_MULTIPLIER_NORMAL: float = 1.25 # 중간 신호 배수 (+25%)
|
||
MEAN_REVERSION_MULTIPLIER_WEAK: float = 1.00 # 약한 신호 배수 (기본값)
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# KIS WebSocket & 실시간 스트림 (kis_realtime_stream.py)
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
KIS_WS_ENABLED: bool = True # WebSocket 실시간 스트림 활성화 (True) / 비활성화 (False)
|
||
KIS_SYNC_INTERVAL: int = 10 # 구독 목록 동기화 주기 (초)
|
||
KIS_RECONNECT_WAIT: int = 5 # 재연결 대기 시간 (초)
|
||
KIS_APPROVAL_KEY_TTL: int = 82800 # approval_key 캐시 유효시간 (초, 23시간)
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# 감시 목록 (watchlist.py)
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
WATCHLIST_INIT_SIZE: int = 150 # 아침 배치 초기화 종목 수
|
||
WATCHLIST_MAX_SIZE: int = 150 # 최대 관리 종목 수
|
||
WATCHLIST_ENTRY_SCORE: int = 60 # 장중 신규 진입 기준 점수
|
||
WATCHLIST_EXIT_SCORE: int = 20 # 장중 제거 기준 점수
|
||
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
# 기술적 지표 임계값 (technical_indicators.py)
|
||
# ═════════════════════════════════════════════════════════════════════════════
|
||
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 안정 범위 최대값
|
||
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일)
|
||
RSI_PERIOD: int = 14 # RSI 계산 기간
|
||
ATR_PERIOD: int = 14 # ATR 계산 기간
|
||
|
||
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 미설정"
|
||
)
|
||
|
||
# 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:
|
||
from pydantic_settings import BaseSettings as _BS # lazy import
|
||
# GEMINI_API_KEY는 config에 없으므로 환경변수 직접 확인
|
||
gemini_key = os.environ.get("GEMINI_API_KEY", "")
|
||
if not gemini_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()
|