Files
auto-stock/backend/app/core/config.py
shuim dc128ae4cf feat: DB 계정 기반 회원가입/로그인 (users 테이블) — .env 관리자 계정 방식 대체
- models.User + alembic 0008 (users: username unique, bcrypt password_hash, is_active, is_admin)
- auth.py: POST /auth/register, /auth/login, GET /auth/me, 관리자용 GET /auth/users,
  POST /auth/users/{id}/approve|revoke. bcrypt 는 asyncio.to_thread 로 오프로드,
  미존재 계정도 더미 해시 검증(타이밍 균등화), IP당 실패 잠금 유지
- 정책: 첫 가입자 = 관리자·즉시 활성, 이후 가입자는 관리자 승인 필요
  (SIGNUP_AUTO_APPROVE=true 로 해제, SIGNUP_ENABLED=false 로 가입 차단). API 키는 관리자 취급
- config: ADMIN_USERNAME/ADMIN_PASSWORD(_HASH) 제거 → SIGNUP_ENABLED / SIGNUP_AUTO_APPROVE
- Login.tsx: 로그인/회원가입 탭, 비밀번호 확인, 승인 안내 배너, 상태코드별 메시지,
  첫 가입자는 가입 직후 자동 로그인
- DB 접근: 회원가입/로그인은 sync_portfolio 와 같은 단일 트랜잭션 예외로 엔드포인트 직접 접근
  (컨테이너 Postgres 대상 통합 테스트 19건 통과)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 14:48:05 +09:00

457 lines
32 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 폴백)
# ── 대시보드 로그인 (DB users 테이블, bcrypt) ─────────────────────────
SIGNUP_ENABLED: bool = True # 회원가입 허용. False 면 /auth/register 403
SIGNUP_AUTO_APPROVE: bool = False # True: 가입 즉시 활성. False: 첫 가입자만 자동 관리자, 이후 관리자 승인 필요
SESSION_TOKEN_EXPIRE_HOURS: int = 24 # 로그인 세션 토큰 유효시간
LOGIN_MAX_FAILURES: int = 5 # IP당 연속 실패 허용 횟수
LOGIN_LOCKOUT_SECONDS: int = 300 # 초과 시 잠금 시간(초)
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_QUOTES_VIA_REAL: bool = True # 모의투자 모드에서 시세·차트·호가 조회를 실계좌 시세 서버(초당 20건)로 라우팅. 주문·잔고는 모의서버 유지
KIS_REAL_MIN_REQUEST_INTERVAL: float = 0.1 # 실계좌 시세 인스턴스 최소 요청 간격(초). 한도 20/s 대비 여유 10/s
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_REQUEST_TIMEOUT: float = 15.0 # MCP /analyze 호출 타임아웃(초) — claude_code CLI 모드는 60 권장
MCP_RISK_CACHE_TTL: int = 0 # 동일 종목셋 RISK_ASSESSMENT 결과 재사용(초). 0=매번 호출, 3600=1시간
BATCH_ANALYSIS_INTERVAL: int = 600 # 장중 배치 분석 주기(초). 3600=1시간
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 # 하락장 방어 필터
SWING_INDEX_BREAKER_ENABLED: bool = False # 지수 서킷브레이커(KODEX200<MA20 시 스윙 신규진입 차단). 백테스트 근거 있으나 사용자 결정으로 기본 OFF
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
API_TRIGGERED_AUTO_TRADE: bool = False # GET /recommendations/rule-based?auto_trade=true 가 매매를 실행할지. 기본 OFF — 매매는 스윙 스케줄러 단일 경로
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()