Files
auto-stock/backend/app/core/config.py
shuim a45007cbb2 feat: TradingView 스타일 대시보드 + 차트 오버레이 API, Crisis 손절 소급 차단 플래그(기본 OFF)
- frontend: 대시보드를 TradingView 레이아웃으로 재구성(components/tv/*) — 캔들+MA/BB/거래량,
  평단·손절·목표·트레일링 가격선, 체결 마커, RSI 기준선은 실효 설정값, 하단 탭(보유·체결·DT 카드·
  비용 후 손익곡선/낙폭·저널), 디자인 토큰·한국식 색·최소 11px, 모바일 가로 스크롤 제거
- backend(GET 전용, require_api_key): /positions/overlay, /risk/equity-history,
  /strategy-config/display, /trades 응답 필드(side·strategy_type·commission·net_profit_loss),
  /risk/mdd-status thresholds, /market-data/chart 1m|5m(캐시·in-flight 공유)
- mean_reversion_strategy: RSI 청산 임계를 resolve_swing_exit_rsi_v2 로 추출(동작 동일) —
  청산과 화면 표시가 같은 값을 쓴다
- §8.5-39: SWING_STOP_IGNORE_CRISIS_OVERLAY(기본 False) — 반사실 재생에서 Crisis 조기 손절이
  오히려 손실을 줄여 기본 OFF 유지, 정식 백테스트 전 켜지 말 것
- 유닛 스위트 2816 passed / 0 failed, tsc·build 통과

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:47:00 +09:00

839 lines
67 KiB
Python
Raw Permalink 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 typing import Optional
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
# ── 멀티테넌트(사용자별 매매) 1단계 ─────────────────────────────────
MULTI_TENANT_ENABLED: bool = False # True 전환 시 사용자별 KIS 계좌·리스크 상태 분리 (기본 False = 기존 단일 계좌 동작)
CREDENTIALS_ENCRYPTION_KEY: str = "" # user_kis_credentials 암복호화 키 파생 소스. 비어있으면 SECRET_KEY 폴백
CREDENTIALS_ENCRYPTION_KEY_PREVIOUS: str = "" # 키 로테이션 중 구 키(복호화만 허용)
BOOTSTRAP_ADMIN_USERNAME: str = "admin" # 앱 최초 기동 시 users 테이블이 비어있으면 시드할 관리자 계정명
BOOTSTRAP_ADMIN_PASSWORD: str = "" # 비어있으면 부트스트랩 관리자 시드 건너뜀
# ── 인증·보안 강화 (2026-10-02 검증 루프 2회차) ─────────────────────
# 세션 토큰의 사용자 is_active/is_admin 재확인 캐시 TTL(초). revoke 후 최대 이 시간만 통과.
SESSION_ACTIVE_CACHE_SECONDS: int = 30
# SECRET_KEY/API_KEY 최소 길이 (CLAUDE.md §4: 64자+ 랜덤)
MIN_SECRET_KEY_LENGTH: int = 64
# True 이고 DEBUG=False 이면 약한/기본 키로 기동을 중단한다(fail-closed).
# 기본 False: 운영 .env 상태 확인 전에는 error 로그 + /health "security":"warn" 만 낸다.
SECURITY_FAIL_CLOSED: bool = False
# X-Real-IP 를 신뢰할 직전 홉(프록시) 대역. 이 대역 밖에서 온 요청은 소켓 주소를 쓴다.
TRUSTED_PROXY_CIDRS: str = "127.0.0.1/32,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
# holidays 패키지에 없는 KRX 임시 휴장일 (YYYY-MM-DD 콤마 구분)
KRX_EXTRA_HOLIDAYS: str = ""
# KIS 토큰 재발급 최소 간격(초) — KIS "1분당 1회" 제한 + 여유. 이 창 안에서는 대기하지 않고 즉시 실패한다.
KIS_TOKEN_REISSUE_MIN_INTERVAL_SEC: int = 70
# KIS 토큰 실제 유효기간(시간). 로컬 만료(23h)보다 길다 — 재발급 실패 시 이 한도 안에서만 기존 토큰을 연장한다.
KIS_TOKEN_HARD_TTL_HOURS: int = 24
# 재발급 실패 시 기존 토큰 연장 폭(분) — 짧게 잡아 실제 만료 전에 재발급을 다시 시도하게 한다.
KIS_TOKEN_GRACE_EXTEND_MINUTES: int = 10
# 종료 시 db_worker 를 멈추기 전에 백그라운드 태스크(_spawn_background)를 기다리는 상한(초).
# uvicorn 정리 + 이 값 + db_worker drain(DRAIN_DEADLINE_SECONDS=20) 이 compose stop_grace_period(40s) 안에
# 들어와야 한다 — 기동 시 main.py 가 합계를 CONTAINER_STOP_GRACE_SECONDS 와 비교해 넘으면 error 로그.
SHUTDOWN_BACKGROUND_DRAIN_SECONDS: int = 8
# uvicorn --timeout-graceful-shutdown. backend/Dockerfile CMD 가 같은 이름의 **환경변수**를 읽어
# 적용한다(기본 5) — .env 로 바꾸면 실제 반영된다.
UVICORN_GRACEFUL_SHUTDOWN_SECONDS: int = 5
# docker-compose.yml backend stop_grace_period 와 같은 값(초). 컨테이너 설정을 앱이 읽을 수 없으므로
# 여기 값을 기준으로 종료 예산 합계를 검증한다(테스트가 compose 와 대조).
CONTAINER_STOP_GRACE_SECONDS: int = 40
# 기동 시 DLQ replay 로 재큐잉된 밴이 커밋되기를 기다리는 상한(초) — 그 뒤 loss_tracker 재로드
DLQ_REPLAY_SETTLE_SECONDS: int = 10
# /health 결과 캐시(초) — 폴링 폭주 시 DB 커넥션 풀 경쟁 방지
HEALTH_CACHE_SECONDS: float = 1.5
# Redis 소켓 타임아웃(초) — 없으면 Redis 가 응답 없이 멈출 때 호출자(DT finalize·분산락)가 무기한 대기.
# 코드베이스에 블로킹 명령(BLPOP/pubsub)이 없어 읽기 타임아웃이 정상 동작을 끊지 않는다(2026-10-02 확인).
REDIS_SOCKET_TIMEOUT: float = 5.0
REDIS_SOCKET_CONNECT_TIMEOUT: float = 3.0
# ═════════════════════════════════════════════════════════════════════════════
# 데이터베이스 & 메시지 큐
# ═════════════════════════════════════════════════════════════════════════════
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.04 # 종목당 4% (6%→4%, 2026-09-23: 종목 수를 늘려 표본 확보)
MAX_HOLDINGS_COUNT: int = 20 # 동시 보유 최대 20종목 (섹션 5.1)
LEVERAGE_ETF_MAX_RATIO: float = 0.20 # 레버리지/인버스 ETF 최대 20% (섹션 5.1)
# [HIGH-4] 섹터 미매핑 종목에 "UNKNOWN" 단일 버킷으로 섹터당 최대 3종목 한도를 적용할지.
# True로 켜면 §5.1 섹터캡이 전 종목에 적용되지만, _STOCK_CODE_SECTOR_MAP 커버리지가
# 낮은 현재 상태에서는 총 보유가 ~3종목으로 묶여 MAX_HOLDINGS_COUNT(20)에 도달할 수 없다.
# → 매핑 테이블 확충 + 백테스트 검증 후 활성화할 것 (correlation_filter 참고).
SECTOR_CAP_UNMAPPED_ENABLED: bool = False
# 상관 필터 (§5.1) — 기본값은 현행 동작 유지. 켜기 전 운영 DB 로 일봉 결손 빈도 확인 필요.
MAX_STOCKS_PER_SECTOR: int = 3
CORRELATION_THRESHOLD: float = 0.7
# 보유종목 일봉 부족/공통 거래일 부족으로 비교 제외가 생기면 후보 보류(fail-closed)
CORRELATION_FAIL_CLOSED_ON_MISSING_DATA: bool = False
# 후보 종목 일봉 부족 / 상관 계산 불가 시 보류(fail-closed). 거래량 영향 -> 사용자 결정 전 False
CORRELATION_FAIL_CLOSED_ON_CANDIDATE_DATA: bool = False
# 자본 배분 글로벌 가드 — 전 전략 합산 현금 사용 상한
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% 강제 축소
# [항목7 2026-09-27] MDD 갱신 실패 시 fail-safe (신규 진입 차단)
# swing/scheduler 의 mdd_manager.update() 가 peak/drawdown 을 갱신하는 유일한 실효
# 경로다. KIS 잔고 조회가 계속 실패하면 peak/drawdown 이 그 시점에 얼어붙고
# -10%/-20% 게이트는 "무증상으로" 영구 미발동한다 → 리스크 게이트가 눈이 먼 상태에서
# 계속 진입하게 되고, 실제 낙폭이 -20% 를 넘어가도 추가 진입이 무제한 허용된다.
# 거래 기회를 잃는 손실(<진입 몇 건)보다 게이트 없는 매매의 손실이 압도적으로 크므로
# 기본값을 True(차단)로 둔다. 청산은 절대 막지 않는다(can_open_new_position 한정).
MDD_BLIND_BLOCK_ENTRY: bool = True
# 몇 회 연속 갱신 실패부터 차단할지. 1~2회는 KIS 순간 오류로 흔하므로
# 알림 임계값(swing/scheduler.MDD_UPDATE_FAILURE_ALERT)과 동일한 5회로 맞춘다.
MDD_BLIND_FAILURE_THRESHOLD: int = 5
# [C-1 2026-10-02] MDD 강제축소 종목별 재시도 상한 (KST 하루 단위).
# 거래정지·주문가능 0 등으로 한 종목이 계속 실패해도 이미 축소된 종목은 재매도하지
# 않는다(레벨별 완료 집합). 실패 종목만 재시도하고, 이 횟수에 도달하면 당일 포기 +
# error 로그. 날짜가 바뀌면 시도 횟수만 초기화된다(완료 집합은 peak 갱신 시에만 초기화).
# order_lock_busy(다른 경로가 같은 종목 주문 중)는 실패로 세지 않는다.
MDD_REDUCTION_MAX_RETRIES: int = 5
# 가격 필터 (섹션 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 = 150_000 # 최소 주문금액 (500,000→150,000, 2026-09-23)
# 50만원은 종목당 예산(총자산×6%≈59만)과 거의 같아, 신뢰도·현금 보정으로 조금만
# 깎여도 전부 탈락했다(2026-09-23 하루 141건 차단). 계좌 대비 1.5~3% 주문을 허용한다.
# 연속 손실 관리 (섹션 5.5)
CONSECUTIVE_LOSS_BAN_COUNT: int = 3 # 3회 연속 손실
# [HIGH-3 수정] 기존 30은 어디서도 쓰이지 않는 죽은 값이었고, 실제 ban 기간은
# loss_tracker의 하드코딩 42캘린더일이었다. 문서 §5.1("3연패 → 42캘린더일 ≈ 30거래일")과
# 실동작에 맞춰 42(캘린더일)로 정정하고, loss_tracker가 이 값을 읽도록 연결했다.
CONSECUTIVE_LOSS_BAN_DAYS: int = 42 # 진입 금지 기간(캘린더일) — 30거래일 ≈ 42캘린더일
# ═════════════════════════════════════════════════════════════════════════════
# 평균회귀(스윙) 전략 (섹션 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 → 추세 강함
# ── BEAR 진입 점수제 v2 (§8.5-9) ──────────────────────────────────────────
# 기존 BEAR 분기는 RSI<35 AND %B≤0.20~0.30 AND 거래량>1.5×20MA AND 반전캔들의
# 4중 AND 여서 사실상 미발화했다. v2는 %B/거래량/반전캔들을 가중 점수로 바꾸고
# RSI 반등(rsi>rsi_prev)과 RSI 상한만 필수(AND)로 유지한다.
# ⚠️ 백테스트 미검증 — True 전환 금지.
# (1) 실전과 동일한 지표(Wilder RSI/ADX, indicators_core)로 재산출한 백테스트,
# (2) BEAR 국면 구간만 분리한 승률·Profit Factor·MDD,
# (3) 최대 연속 손실 횟수가 기존 경로 대비 악화되지 않음
# — 3개를 모두 통과한 뒤에만 켠다. 하락장 진입을 늘리는 변경이므로 특히 위험.
MEAN_REVERSION_BEAR_V2_ENABLED: bool = False # 기본 False = 기존 4중 AND 경로 그대로
# [C1 2026-10-02] 추천 스캐너(recommendations.is_mean_reversion_candidate)의 RSI 임계
# 출처 스위치. False(기본) = 현행: trading_rules(DB/JSON) conditions 의
# rsi_bull_max/rsi_bear_max(없으면 40/30). True = CLAUDE.md §0 우선순위
# (adaptive rsi_entry/bear_rsi_entry → strategy_cfg → settings 46/35).
# 2026-10-02 사용자 결정(거래 확대 우선)으로 True — 진입 RSI 40/30 → 46/35.
# ⚠️ 백테스트 미검증, 모의계좌 기준 결정. KIS_MODE=real 전환 전 재검토.
MEAN_REVERSION_CANONICAL_PARAMS_ENABLED: bool = True
# RSI 상한(필수 AND): 기존 임계값(35) + relax, 단 hard cap(40) 이하.
# 근거: 40은 BULL 진입 임계(46)보다 낮게 유지해 "하락장은 더 보수적"이라는 원칙을 깨지 않는다.
MEAN_REVERSION_BEAR_V2_RSI_RELAX: float = 5.0
MEAN_REVERSION_BEAR_V2_RSI_HARD_CAP: float = 40.0
# 진입 임계 점수. 가중치 합은 %B 0.40 + 반전 0.35 + 거래량 0.25 = 1.00 (+RSI깊이 보너스 0.15).
# 0.60 = "핵심 조건(%B) + 보조 1개" 또는 "보조 2개 + 깊은 과매도" 수준.
MEAN_REVERSION_BEAR_V2_MIN_SCORE: float = 0.60
# %B: 평균회귀 엣지의 직접 근거(하단 이탈 후 되돌림) → 최대 가중치.
MEAN_REVERSION_BEAR_V2_W_PCT_B: float = 0.40
# 반전캔들: 낙하 중 진입 방지 대리변수. RSI 반등과 상호보완이나 도지/데이터부족 시
# 판정 불가라 필수화하면 다시 미발화한다 → 높은 가중치의 점수 항목으로만 둔다.
MEAN_REVERSION_BEAR_V2_W_REVERSAL: float = 0.35
# 거래량: 확인 지표. 하락장 대량 거래는 투매(추가 하락)일 수도 있어 단독 예측력이 약함 → 최소 가중치.
MEAN_REVERSION_BEAR_V2_W_VOLUME: float = 0.25
# RSI 깊이 보너스: RSI<20 → 만점, RSI<25 → 절반. 깊은 과매도의 반등 확률 프리미엄.
MEAN_REVERSION_BEAR_V2_W_RSI_DEPTH: float = 0.15
# %B 부분점수 배수: pct_b <= pct_b_max*1.6 이면 절반 점수.
MEAN_REVERSION_BEAR_V2_PCT_B_RELAX_MULT: float = 1.6
# %B 하드 상한(필수 AND 성격의 잔존 하한): BB 중심선(0.5) 위는 평균회귀 진입이 아니다.
MEAN_REVERSION_BEAR_V2_PCT_B_HARD_CAP: float = 0.50
# 거래량 중간 등급 배수: 1.2×20MA 이상이면 부분 점수(0.6배).
MEAN_REVERSION_BEAR_V2_VOLUME_MID_MULT: float = 1.2
# ═════════════════════════════════════════════════════════════════════════════
# 단기(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 = 5 # 동시 최대 종목 수 (8→5, 2026-09-23 30% 배분: 5×6%=30%)
# 감시종목 수 (populate_daytrading_watchlist 의 상한). 20→30, 2026-09-30.
# 용도: ORB 돌파 후보 풀 확대 — 진입 임계값을 낮추지 않고 표본만 늘린다.
# ⚠️ 리스크 노출과 무관하다: 동시 보유는 DAY_TRADE_MAX_POSITIONS(5), 종목당 예산은
# DAY_TRADE_MAX_CAPITAL_RATIO(6%), DT 합산은 DAY_TRADE_MAX_RATIO_DT(30%) 가 각각
# 독립적으로 상한을 건다. 늘어나는 것은 기회(스캔 후보)뿐이다.
# 상한 근거(2026-09-30 산정, daytrading/entry_service 스캔 예산):
# 선조회 wall = 종목수 × 분봉 페이지수 × 페이지비용 ÷ _SCAN_PREFETCH_CONCURRENCY(8)
# - 페이지비용 3.2초 = 운영 실측 요청 3.0초(monitoring/api-stats avg_duration,
# 재시도 포함) + in-loop sleep 0.2초(API_MIN_REQUEST_INTERVAL+0.05)
# - 페이지수는 시간대 비례(당일 1분봉÷30): 09:30 4장 / 12:00 6장 / 14:00 10장 / 15:00 12장
# - 예산 = _SCAN_PREFETCH_TOTAL_BUDGET(120초, 선조회 단계 데드라인)
# ⇒ 14:00 기준 상한 = 120×8/(10×3.2) = **30종목** (오전 기준으로는 60종목)
# 15:00 직전(12장)에는 30종목이 144초를 필요로 해 약 17% 가 잘리지만, 라운드로빈
# 오프셋이 다음 사이클에 이어받으므로 특정 종목이 영구히 누락되지는 않는다.
# 진입 마감이 15:00·EOD 청산이 15:20 이라 마지막 1시간의 부분 커버리지는 감수한다.
# ORB 수집(09:00~09:29)은 종목당 1페이지·0.3초 간격이라 상한이 수백 종목으로 훨씬 느슨하다.
# 후보 공급 측 제약: 운영 실측 당일 분석 종목이 53개뿐이다(2026-09-30) —
# 40~50 으로 올리면 사실상 전량 편입이 되어 점수 기반 선별이 사라진다.
DAY_TRADE_WATCHLIST_SIZE: int = 30
DAY_TRADE_CAPITAL_RATIO: float = 0.30 # 전체 자본 대비 최대 30% (0.50→0.30, 2026-09-23 스윙 70%)
DAY_TRADE_PER_STOCK_RATIO: float = 0.06 # ⚠️ 소비자 0건 — 실효 없음(2026-09-28 확인).
# DT 종목당 한도의 정본은 아래 DAY_TRADE_MAX_CAPITAL_RATIO 다. 이 키는 strategy_configs
# DB 행이 존재하므로 삭제하지 않고 남겨 둔다(삭제 시 UI/리셋 경로가 깨진다).
DAY_TRADE_MAX_CAPITAL_RATIO: float = 0.06 # 종목당 최대 6% 자본 (0.04→0.06, 2026-09-22)
DAY_TRADE_MAX_RATIO_DT: float = 0.30 # 전체 데이트레이딩 최대 30% (스윙:DT = 70:30, 2026-09-23)
SWING_MAX_CAPITAL_RATIO: float = 0.70 # 스윙 전략 합산 최대 70% (0.50→0.70, 2026-09-23 — 표본 확보)
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 # 수수료+슬리피지 합산 (매수+매도)
# [2026-10-01] DT 실제 체결가 확인 (주문 후 백그라운드, 실시간 루프 비차단).
# 실패/시간초과 시 신호가 기록 유지 + warning. 판정 임계값이 아니라 기록 정확도용.
DAY_TRADE_FILL_CHECK_ENABLED: bool = True
DAY_TRADE_FILL_CHECK_DELAY_SEC: float = 2.0 # 주문 직후 첫 조회까지 대기
DAY_TRADE_FILL_CHECK_ATTEMPTS: int = 3 # 체결 조회 최대 횟수
DAY_TRADE_FILL_CHECK_INTERVAL_SEC: float = 2.0 # 조회 간격
DAY_TRADE_FILL_CHECK_TIMEOUT_SEC: float = 15.0 # 전체 상한 (토큰 rate-limit sleep 차단)
# DT 종목의 시장 구분을 모를 때 거래세 적용 시장 (스윙 trade_executor 기본값과 동일)
DAY_TRADE_DEFAULT_TAX_MARKET: str = "KOSPI"
DAY_TRADE_ENTRY_CUTOFF_HOUR: int = 15 # 신규 진입 마감 시간 (14:30→15:00, 2026-09-22 시간대 제한 완화)
DAY_TRADE_ENTRY_CUTOFF_MIN: int = 0 # 신규 진입 마감 분
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% 미만 시 청산. 2026-10-09 -0.7% 변경 원복 — 운영 실측(163건) DT 순손익 +31,233원으로 현행이 이익, 변경 효과는 백테스트 전 미확인
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% → 노이즈 손절 방지)
# ═════════════════════════════════════════════════════════════════════════════
# 박스권(SIDEWAYS) 레인지 매매 (CLAUDE.md §8.5 항목 9)
# ═════════════════════════════════════════════════════════════════════════════
# 전략 자동 최적화(strategy_optimizer)의 **DB 파라미터 자동 반영** 스위치.
#
# ⚠️ **기본 False 유지 — True 로 바꾸기 전에 CLAUDE.md §8.4 백테스트 요건 7개를
# 충족해야 한다.** False 여도 최적화 계산·IS/OOS 평가·`strategy_optimization_logs`
# 기록은 그대로 돌아간다. 막는 것은 `strategy_optimizer.apply_params()` →
# `strategy_cfg.upsert()` 뿐이다(= §0 우선순위 2위인 DB `strategy_configs` 쓰기).
#
# 왜 필요한가: 16:00 배치의 자동 적용 게이트는 "최근 7일 SELL ≥ 5건"이라
# **5거래 표본의 그리드 서치가 RSI 진입·손절·포지션 크기를 덮어쓴다.**
# §8.4 는 파라미터당 최소 100거래를 요구하고, §8.5-1 은 현행 산출물 전부를
# 폐기 상태로 둔다. 게다가 alembic 0012 로 방금 정합시킨 값(RSI 46 / 손절 -7%)이
# 조용히 바뀌어 §0 의 3중 소스가 다시 어긋난다.
# (이 경로는 `schedulers.py` 의 now_kst 섀도잉 TypeError 때문에 실행된 적이
# 없었고, 2026-09-29 섀도잉 수정으로 되살아났다.)
#
# 차단 시 무엇이 적용될 뻔했는지 `logger.warning` 으로 키·현재값·제안값·Sharpe 를
# 남긴다 — 적용은 운영자가 전략설정 UI 에서 수동으로 한다.
STRATEGY_AUTO_APPLY_ENABLED: bool = False
# 장중 매매일지 보충 생성 — 체결된 SELL 중 일지 없는 건을 N분 주기로 일지화한다.
# (16:00 배치만 있으면 장중 매도는 최대 6시간 뒤에야 일지가 생긴다. 2026-09-28~29 에
# 장중 일지가 있었던 것은 스케줄러가 아니라 수동 "일지 생성" 버튼 호출 때문이었다.)
JOURNAL_INTRADAY_ENABLED: bool = True
JOURNAL_INTRADAY_INTERVAL_MIN: int = 10
# 한 번에 훑는 과거 일수 (uncovered 판정은 trade_id 기준이라 멱등)
JOURNAL_LOOKBACK_DAYS: int = 7
# 장중 태스크는 created_at 이 이 시간(분) 이내인 SELL 을 건너뛴다 (즉시 생성 경로와의 경합 방지)
JOURNAL_INTRADAY_GRACE_MIN: int = 3
# in-flight 등록 기록 만료(분) — 큐 INSERT 가 DLQ 로 갔어도 이 시간 뒤엔 재시도된다
JOURNAL_ENQUEUE_TTL_MIN: int = 30
# ═════════════════════════════════════════════════════════════════════════════
# ⚠️ **백테스트 미검증 — True 로 바꾸지 말 것 (CLAUDE.md §8.1-4).**
# 이 전략은 SIDEWAYS 국면 전용이며, market_analyzer 의 SIDEWAYS 전면 차단(§6.1)을
# 의도적으로 우회한다. False 인 동안 swing/entry_service.scan_range_trading_signals()
# 는 함수 진입 즉시 [] 를 반환하므로 기존 동작에 어떤 영향도 없다.
# 켜기 전 통과해야 할 검증: SIDEWAYS 표본 ≥ 60거래일 확보 / in-sample 승률 ≥ 50% ·
# PF ≥ 1.3 · MDD ≥ -8% / walk-forward 4구간 전부 PF ≥ 1.0 · MDD -10% 초과 없음 /
# 파라미터 민감도 그리드 과반 통과. 상세는 range_trading_strategy.py docstring 참조.
RANGE_TRADE_ENABLED: bool = False
RANGE_TRADE_LOOKBACK_DAYS: int = 20 # 박스 판정 구간(일). SIDEWAYS 기울기 비교 기간(20일)과 정합
RANGE_TRADE_MIN_WIDTH_PCT: float = 6.0 # 박스 최소 폭. 왕복 이익이 거래비용(왕복 ~0.5%)+손절폭을 못 넘으면 무의미
RANGE_TRADE_MAX_WIDTH_PCT: float = 20.0 # 박스 최대 폭. 이상이면 박스가 아니라 추세/고변동 구간
RANGE_TRADE_TOUCH_TOLERANCE_PCT: float = 2.0 # 상·하단 접촉 허용 오차. 정확히 같은 값을 찍는 경우는 드뭄
RANGE_TRADE_MIN_TOUCHES: int = 2 # 상단·하단 각 최소 접촉 봉 수 (지지/저항 확인)
RANGE_TRADE_MIN_ALTERNATIONS: int = 2 # 상단↔하단 접촉 교대 최소 횟수 = 레인지 내 왕복 이력
RANGE_TRADE_MAX_ADX: float = 20.0 # ADX 상한. SIDEWAYS 히스테리시스(진입 18/해제 22)의 중앙값
RANGE_TRADE_ENTRY_ZONE_PCT: float = 25.0 # 박스 하단 25% 구간에서만 진입 (지지대 매수)
RANGE_TRADE_BB_PCT_B_MAX: float = 0.25 # %B 상한. 평균회귀 BULL(0.20~0.35)보다 보수적 중간값
RANGE_TRADE_RSI_ENTRY: float = 40.0 # 진입 RSI. 평균회귀 BULL(46)보다 타이트 — 미검증 전략이라 보수적
RANGE_TRADE_VOLUME_RATIO: float = 1.0 # 거래량 ≥ 20일 평균 × 배수 (지지 반등 수급 확인, 과도한 요구는 표본 소멸)
RANGE_TRADE_RSI_PARTIAL_EXIT: float = 55.0 # 1차 익절 RSI (§8.2 MEAN_REVERSION_RSI_EXIT_THRESHOLD 와 동일)
RANGE_TRADE_RSI_FULL_EXIT: float = 65.0 # 전량 청산 RSI. 박스 상단에서 오래 버티지 않음 → 스윙(70)보다 빠르게
RANGE_TRADE_EXIT_ZONE_PCT: float = 80.0 # 박스 상단 20% 진입 시 전량 익절 (상단 정확히 기다리면 미체결 위험)
RANGE_TRADE_BOX_BREAK_PCT: float = 1.0 # 박스 하단 -1% 하향 이탈 → 즉시 전량 손절 (노이즈 여유 1%)
RANGE_TRADE_ATR_STOP_MULTIPLIER: float = 2.0 # ATR 손절 배수. 스윙(2.5)보다 타이트 — 박스 폭이 좁아 여유가 불필요
RANGE_TRADE_STOP_LOSS_PERCENT: float = 5.0 # 기본 손절 -5%. 스윙(-7%)보다 타이트, 박스 폭 하한(6%)과 정합
RANGE_TRADE_TRAILING_ATR_MULT: float = 1.5 # 트레일링 = 고점 − 1.5×ATR (§5.2 동일)
RANGE_TRADE_MIN_PROFIT_PCT: float = 0.5 # 익절 최소 수익률 (수수료+세금 ~0.4% 커버)
RANGE_TRADE_MAX_POSITIONS: int = 3 # 동시 보유 상한 (미검증 전략 노출 최소화)
RANGE_TRADE_MAX_CAPITAL_RATIO: float = 0.10 # 스윙 몫(SWING_MAX_CAPITAL_RATIO=0.70) **내부** 비율 → 총자본 7%
RANGE_TRADE_POSITION_MULTIPLIER: float = 0.5 # 종목당 = MAX_POSITION_SIZE(4%) × 0.5 = 2% (미검증 전략)
RANGE_TRADE_SCAN_LIMIT: int = 10 # 사이클당 차트 조회 종목 수 상한 (KIS 레이트 리밋)
# ═════════════════════════════════════════════════════════════════════════════
# 코스닥 스캐너 (섹션 7.1)
# ═════════════════════════════════════════════════════════════════════════════
# (코스닥 스캐너 파라미터 추가 필요 시 여기에 추가)
# ═════════════════════════════════════════════════════════════════════════════
# 미국 주식 매매 (섹션 7.4)
# ═════════════════════════════════════════════════════════════════════════════
US_TRADING_ENABLED: bool = False # 미국 주식 자동매매 활성화 (기본 OFF)
US_MAX_CAPITAL_RATIO: float = 0.30 # 전체 자본 대비 미국 주식 최대 비중 (30%)
US_SCAN_INTERVAL: int = 300 # 미국장 스캔 간격 (초, 기본 5분)
# ── [§8.5-8 상수 편입 2026-09-27] us_stock_strategy / us_market_scheduler ──
# 아래 키는 두 모듈에 하드코딩돼 있던 숫자를 **값 변경 없이** 그대로 옮긴 것이다
# (CLAUDE.md §8.1-4: 백테스트 없이 전략값 변경 금지 — 이번 작업은 "출처 단일화"뿐).
# US_MAX_CAPITAL_RATIO(위)는 모듈 상수와 값이 완전히 같았으므로 이 키를 정본으로 쓰고
# 모듈 상수는 제거했다.
# 국내 키와 값이 겹치는 것(US_MIN_PRICE_USD/US_STOP_LOSS_PCT/US_ATR_STOP_MULT)은
# 국내 키를 재사용하지 않고 US 전용 키를 새로 뒀다 — 미국은 거래비용·틱·변동성이
# 달라 앞으로 갈라질 값이고, 공유하면 국내 튜닝이 미국에 조용히 전파된다.
US_MAX_POSITION_SIZE: float = 0.05 # 미국 종목당 최대 5% (국내 MAX_POSITION_SIZE=0.04 와 별개, 의도적으로 더 큼)
US_RSI_BUY_THRESHOLD: float = 35.0 # RSI(14) 매수 상한 (국내 평균회귀 46 대비 보수적)
US_BB_LOWER_MARGIN_LOW: float = 0.97 # 진입 허용 구간 하한 = BB하단 × 0.97 (-3%)
US_BB_LOWER_MARGIN_HIGH: float = 1.02 # 진입 허용 구간 상한 = BB하단 × 1.02 (+2%)
US_MIN_PRICE_USD: float = 4.0 # 페니스탁 필터(이 값 "이하" 진입 불가). 현재 MIN_PRICE_USD(4.0)와 값 동일 — 별 키로 분리 보관
US_STOP_LOSS_PCT: float = -7.0 # 기본 손절 **음수 규약**(국내 STOP_LOSS_PERCENT=7.0 양수와 부호 규약 다름, 절대값은 현재 동일). us_backtest_engine 이 `pnl_pct <= US_STOP_LOSS_PCT` 로 음수 그대로 소비하므로 음수 유지
US_ATR_STOP_MULT: float = 2.5 # ATR 손절 배수. 현재 ATR_STOP_MULTIPLIER(2.5)와 값 동일 — 별 키로 분리 보관
US_FIRST_EXIT_RSI: float = 55.0 # 1차 익절 RSI 기준
US_FIRST_EXIT_SELL_PCT: float = 0.30 # 1차 익절 매도 비율 30%
US_TRAILING_STOP_PCT: float = 0.04 # 트레일링 스탑 고점 대비 -4%
US_MAX_NEW_ENTRIES_PER_CYCLE: int = 3 # 스캔 사이클당 신규 진입 종목 수 상한
# 환율 폴백: KIS 해외잔고로 환율을 역산할 수 없을 때(잔고 조회 실패, 또는 USD 평가액+예수금이 0)
# 사용된다. 이 값은 포지션 사이징(KRW예산 ÷ 환율 → USD → 수량)과 TradeHistory.fx_rate 에
# 그대로 들어가므로 **실제 주문 수량·기록 금액에 직접 영향**을 준다. 실환율과 크게 벌어지면
# 주문 수량이 그만큼 어긋난다(예: 실제 1450 인데 1350 사용 → 예산 대비 약 7% 과다 주문).
US_DEFAULT_FX_RATE: float = 1350.0
# ═════════════════════════════════════════════════════════════════════════════
# 필터 & 시장 국면 (섹션 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 # 종목당 일일 최대 매수 횟수
# [M-1 3회차 2026-10-02] 자동매매 1사이클 후보 순회. 매수 성공 건수가 한도
# (MAX_DAILY_TRADES 잔여)에 도달할 때까지 후보를 계속 평가한다 — SKIP/HOLD 는 슬롯을
# 쓰지 않는다. 아래는 KIS 호출량 보호용 사이클당 '신규 후보' 평가 상한(보유 종목의
# 청산 판정은 상한과 무관하게 계속).
AUTO_TRADE_MAX_EVALUATIONS_PER_CYCLE: int = 30
# KIS 업무 거부(rt_cd "1": 잔고·호가·종목 상태 등) 매수 종목은 이 시간 동안 재주문하지
# 않는다(인메모리, 재기동 시 초기화). 하루 차단이 아니라 같은 사이클 반복 주문 방지용.
AUTO_TRADE_ORDER_REJECT_COOLDOWN_MIN: float = 15.0
# [L-2 4회차 2026-10-02] 계좌 단위 거부 msg_cd — 종목이 아니라 계좌 문제라 다른 종목도
# 같은 결과다. 받으면 그 사이클 신규 매수 중단 + 위 쿨다운 동안 전 종목 매수 보류(청산 무관).
# 40910000 = 모의투자 계좌 교체/만료(메모리: kis-virtual-account-rotation).
KIS_ACCOUNT_REJECT_MSG_CODES: list[str] = ["40910000"]
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 # 최소 신뢰도
# 룰베이스 스코어의 최대값 (market_analyzer 의 signal 가점 합 상한).
# 0~RULE_SCORE_MAX → 0~100 정규화에 사용. 하드코딩 15.0 을 대체한다.
RULE_SCORE_MAX: int = 15
# ML 게이트 점수 스케일 정규화 (기본 False = 기존 계산 그대로).
# False: trade_executor 의 ML 게이트가 0~15 룰점수와 0~100 ML점수를 그대로
# 가중합 (룰 기여도가 문서상 30% 가 아니라 실질 7.5%, ML 이중 계산).
# True : StockAnalysis.score 가 이미 rule+ML 블렌딩된 0~RULE_SCORE_MAX 값이므로
# 재블렌딩 없이 0~100 으로 환산해 임계와 직접 비교.
# 진입 판정이 바뀌는 변경이므로 백테스트 검증 전 활성화 금지.
ML_SCORE_NORMALIZE_ENABLED: bool = False
# ═════════════════════════════════════════════════════════════════════════════
# 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"
# ═════════════════════════════════════════════════════════════════════════════
# 스윙 전략 플래그 (2026-10-02) — 사용자 결정("거래가 늘어나는 방향")으로 모멘텀·
# 시간손절은 기본 ON. ⚠️ CLAUDE.md §8.1-5(백테스트 없이 실전 투입 금지) 미충족 상태.
# ═════════════════════════════════════════════════════════════════════════════
# 모멘텀 돌파: 오늘(장중) 봉을 제외한 20일 고점/평균거래량으로 판정하는 _v2 사용.
# False 면 기존 v1(오늘 봉 포함 → current_close > max(highs[-20:]) 이 구조적으로 불가,
# 실전 발화 0)을 호출한다.
MOMENTUM_BREAKOUT_ENABLED: bool = True
# 스윙 시간손절(check_time_stop_exit) 배선. 기간·최소수익은 아래 두 키.
SWING_TIME_STOP_ENABLED: bool = True
SWING_TIME_STOP_DAYS: int = 20
SWING_TIME_STOP_MIN_PROFIT_PCT: float = 3.0
# [L-6 2026-10-02] 스윙 손절 파라미터 합리 범위(resolve_swing_stop_params_v2 의 소스 값 검증).
# 범위 밖 값(strategy_cfg·적응형)은 warning 후 하위 소스(최종 config)로 폴백한다.
# 손절률은 크기(abs, %) 기준, ATR 배수는 배수 그대로.
# [§8.5-39] True 면 적응형 모드가 crisis 일 때 스윙 손절 오버레이(stop_loss_pct/high_vol/
# atr_stop_multiplier)를 적용하지 않는다(strategy_cfg→config). 진입 차단은 무변경. 기본 OFF.
SWING_STOP_IGNORE_CRISIS_OVERLAY: bool = False
SWING_STOP_LOSS_PCT_MIN: float = 1.0
SWING_STOP_LOSS_PCT_MAX: float = 30.0
SWING_ATR_STOP_MULT_MIN: float = 0.5
SWING_ATR_STOP_MULT_MAX: float = 10.0
# [M-2 2026-10-02 5회차] 결과 불명 스윙 매도 후 재매도 전 KIS 잔고 확인이 연속 이 횟수
# 이상 실패하면 warning → error 로 승격(그동안 재매도는 보류).
SWING_PENDING_SELL_CONFIRM_ERROR_AFTER: int = 3
# [H-1/M-1 2026-10-02] 현재가 조회 실패 시 손절 판정용 대체 가격(직전 실시세·Portfolio 평가가)의
# 최대 나이(초). 초과한 값은 쓰지 않는다 — 둘 다 초과면 손절 판정 skip + error(수동 확인).
SWING_FALLBACK_PRICE_MAX_AGE_SEC: float = 900.0
# [C-1 2026-10-02 6회차] 국내 스윙 매도 직전 KIS 잔고 확인이 종목별로 연속 이 횟수 이상 실패하면
# warning → error 로 승격(그동안 매도는 보류 — DT 물량 매도 방지가 우선).
SWING_PRESELL_HOLDING_CHECK_ERROR_AFTER: int = 3
# [M-4 2026-10-02 6회차] 현재가·대체가 모두 없어 손절 판정을 연속 이 횟수 skip 한 종목은
# 텔레그램 시스템 경고(종목별)를 보낸다.
SWING_FALLBACK_SKIP_ALERT_AFTER: int = 5
# [7회차 H-A] get_balance 첫 페이지 행 수(모의 20 / 실전 50). 종목 미발견 + 이 행 수 이상이면 페이지 초과로 보고 매도 보류.
SWING_BALANCE_FULL_PAGE_ROWS: int = 20
# [9회차 D-1, §8.5-37] korea_investment_api.get_balance_all_v2 잔고 연속조회 최대 페이지 수
# (모의 20행 / 실전 50행 × 5). 상한 도달 후에도 남은 페이지가 있으면 결과에 _partial=True.
KIS_BALANCE_MAX_PAGES: int = 5
# ML 단독 SELL 트리거(신뢰도≥0.5). 운영 LSTM 재학습 전 난수(§8.5-20)이므로 OFF.
ML_SELL_SIGNAL_ENABLED: bool = False
# ═════════════════════════════════════════════════════════════════════════════
# 데이트레이딩 하드코딩 이관 (2026-10-02) — 값은 기존 리터럴과 동일(동작 무변경)
# ═════════════════════════════════════════════════════════════════════════════
# ORB 갭 필터 상한(비율). 기본 판정은 |현재가−당일시가|/당일시가 (오버나이트 갭 아님).
DAY_TRADE_ORB_MAX_GAP_PCT: float = 0.04
# ORB 레인지 최소 폭(비율) — (고−저)/저 가 이 값 미만이면 노이즈 돌파로 보고 거부.
DAY_TRADE_ORB_MIN_RANGE_PCT: float = 0.003
# [기본 OFF] 갭 필터를 §7 문서대로 '전일 종가 대비 오버나이트 갭'으로 판정.
# ON 이면 진입 경로가 워밍업(전일 포함) 1분봉을 ORB 판정에 넘기고, 전일 종가를 못 구하면
# 기존 당일시가 판정으로 폴백(warning). 진입 감소 방향이라 사용자 결정으로 OFF 출고.
DAY_TRADE_ORB_OVERNIGHT_GAP_ENABLED: bool = False
# [기본 OFF] ORB 레인지를 09:00~09:29 전체로 확정. OFF(현행 실제 동작)는 첫 수집 성공
# 시점(1분봉 ≥5개 → 대개 09:05 전후)의 레인지로 하루가 고정된다 — 돌파 신호가 더 많다.
# ON: 09:30 이전엔 매 수집마다 재계산하고(Redis 저장분도 09:30 전엔 확정본으로 보지 않음),
# 09:30 이후 수집분만 확정본으로 둔다.
DAY_TRADE_ORB_FULL_RANGE: bool = False
# 5분봉 RSI 과매수 청산 임계(기존 리터럴 70).
DAY_TRADE_RSI_EXIT_OVERBOUGHT: float = 70.0
# VWAP 반등 신호에서 ATR 이 없을 때 목표가 = VWAP × 이 값(기존 리터럴 1.03).
DAY_TRADE_VWAP_TARGET_FALLBACK_MULT: float = 1.03
# DT 청산 체크 처리량·상한 (exit_service 모듈 상수에서 이관, 값 동일).
DAY_TRADE_EXIT_CHECK_CONCURRENCY: int = 4 # 포지션 동시 판정 수
DAY_TRADE_EXIT_FAST_STOP_TIMEOUT_SEC: float = 15.0 # 손절 선판정 현재가·잔고 조회 상한
DAY_TRADE_EXIT_CHECK_TIMEOUT_SEC: float = 45.0 # 포지션별 분봉 판정 상한
DAY_TRADE_TOTAL_ASSETS_REFRESH_SEC: float = 300.0 # 청산 측 총자산 갱신 주기
# 전일(stale) 포지션 매도 실패 시 재시도 (당일·프로세스 단위).
# MAX_RETRIES 는 '업무 거부'만 센다(레이트리밋·전송 전 실패·전송 불명은 미산정).
DAY_TRADE_STALE_SELL_MAX_RETRIES: int = 3
DAY_TRADE_STALE_SELL_RETRY_BACKOFF_SEC: float = 60.0
# 업무 거부가 MAX_RETRIES 에 도달한 뒤에도 당일 포기하지 않고 이 간격으로 계속 재시도.
DAY_TRADE_STALE_SELL_EXHAUSTED_RETRY_SEC: float = 600.0
# [2026-10-08] DT 고아 잔고 감지 — Portfolio.strategy_type='daytrading' 수량이 있는데
# 인메모리 포지션·active ActiveTrade(day_trade) 가 없는 종목(예: 10/06 KB금융 EOD 매도
# 접수 후 0체결 → ActiveTrade 만 closed). close_stale_positions 경로로 전량 청산한다.
DAY_TRADE_ORPHAN_DETECT_ENABLED: bool = True
# DT 청산 반영 직후엔 Portfolio 행·KIS 잔고가 아직 이전 수량일 수 있다 → 이 시간 동안 판정 보류
DAY_TRADE_ORPHAN_GRACE_SEC: float = 600.0
# 고아로 판정하려면 이 기간 안에 DT ActiveTrade 행(상태 무관)이 있어야 한다 — 근거 없는
# 'daytrading' 태그(수동 매수가 섞인 행 등)를 팔지 않게. 매도 수량도 그 행 수량이 상한.
DAY_TRADE_ORPHAN_LOOKBACK_DAYS: int = 7
# 청산 주문 1건 응답 대기 상한. 초과 = 결과 불명 → 당일 주문내역으로 접수 확인.
# ⚠️ 이 상한에는 전송 전 대기(분산락·종목 세마포어·토큰)도 포함된다(전송 시점 표식은
# trade_executor/korea_investment_api 쪽 훅이 필요 — 미구현).
DAY_TRADE_EXIT_ORDER_TIMEOUT_SEC: float = 60.0
# 결과 불명이고 접수 확인도 안 될 때 같은 종목 재매도 보류 시간(재시도 직전에 다시 확인).
# 2026-10-02: 120 → 30 — 해제 직전 재확인 + 실보유 상한 도입에 맞춰 단축(손절 지연 축소).
DAY_TRADE_EXIT_UNKNOWN_HOLD_SEC: float = 30.0
# 결과 불명 주문을 당일 주문내역에서 찾을 때 주문 시각 허용 오차(초, 서버·KIS 시계 차).
DAY_TRADE_EXIT_UNKNOWN_MATCH_WINDOW_SEC: float = 60.0
# 체결 후 반영 꼬리 단계(Redis 저장·loss_tracker·포트폴리오 캐시) 단계별 상한(초).
DAY_TRADE_EXIT_FINALIZE_STEP_TIMEOUT_SEC: float = 10.0
# 결과 불명 매도 이후 확인용 조회(KIS 잔고·비DT 보유 수량)가 연속 이 횟수만큼 실패하면
# 손절이 보류된 상태이므로 텔레그램으로 수동 청산 경보를 보낸다(종목·일 1회).
DAY_TRADE_EXIT_UNKNOWN_LOOKUP_ALERT_COUNT: int = 3
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
# ── [R9 E-5] 소비처가 getattr(settings, KEY, default) 로만 읽던 키의 선언 ──────────────
# 기본값은 각 소비처의 기존 기본값과 동일(동작 무변경). 선언 목적은 .env 로 조정 가능하게 하는 것.
# (CORS_ORIGINS 는 의도적으로 제외: .env 에 값이 이미 있어 선언하면 지금껏 무시되던 값이 적용돼 CORS 동작이 바뀐다.)
# [보류] MDD_*/SWING_*(알림·타임아웃)/DAY_TRADE_* 키는 선언하지 않았다: 기존 테스트 ~40건이 모듈 기본값(_DEFAULT_*)
# 을 monkeypatch 하는데, 선언하면 settings 값이 우선해 그 목킹이 무력화된다(테스트 이관 후 선언할 것).
# None 기본값은 "소비처 모듈 상수 사용" 의미라 Optional 로 둔다.
HB_FIRST_BEAT_GRACE_SEC: float = 600.0 # main: 장중 첫 하트비트 없음 정체 판정 유예(가동 초)
SWING_ADAPTIVE_TRAILING_ENABLED: bool = False
MEAN_REVERSION_RULE_MERGE_ENABLED: bool = False
KIS_WS_MAX_REGISTRATIONS: int = 41
KIS_WS_ORIGIN: Optional[str] = None
KIS_WS_SUBPROTOCOLS: Optional[str] = None
NEWS_EMPTY_ALERT_THRESHOLD: int = 5
RISK_MAX_DAILY_LOSS_RATIO: float = 0.03
RISK_MAX_CONSECUTIVE_LOSSES: int = 3
BATCH_ANALYSIS_TIMEOUT_SEC: Optional[float] = None # None → schedulers 안전 기본값
ZERO_BUY_ALERT_HOURS: Optional[float] = None # None → schedulers 기본값
@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()