Files
auto-stock/backend/app/core/config.py
shuim 45677c8fbb fix: 리스크 결함 일괄 수정 — 누적 4% 상한·스윙 소유권·일일한도 분리·DT 순손익/체결가·시간손절 밴 제외·체결조회 경로
trade_executor
- 종목당 4% 상한을 기존 보유분 포함 누적으로 (sizing_core.remaining_position_room)
- 추천 경로는 스윙 소유 행만 매수·매도 (external/manual/daytrading/us_swing skip, 대칭), 고아 DT 행 warning
- 일일 매수한도는 스윙 소유 매수만 집계, 집계 실패 시 fail-closed
- 부분매도 반복 차단에서만 DT 매도 제외 (재매수 쿨다운은 유지)
- 체결 확인: 부분체결 잔량 자동 재주문 제거(가드 전부 우회·취소/거부 시 발동), PARTIAL→COMPLETED+체결수량, 초과체결 캡

korea_investment_api
- get_order_execution 이 존재하지 않는 /trading/inquire-ccnl 을 호출하던 결함 → inquire-daily-ccld
  (TTTC0081R 우선, 구 TR 폴백, 페이지네이션, ODNO 매칭, EXCG_ID_DVSN_CD=KRX), 실패 로그 warning

daytrading
- 시간손절은 loss_tracker 중립 (42일 밴이 스윙까지 막던 문제)
- 실현손익을 수수료·거래세 차감 순손익으로 (trade_cost_core), 체결가 백그라운드 보정 (fill_reconciler)
- legacy _record_trade strategy_type=daytrading 명시

db_worker
- update_trade_history_by_order_id 를 당일(KST)+최신 1행으로 한정 (일별 재사용 ODNO 로 과거 행 덮어쓰기 방지)

alembic 0013: trade_journals strategy_name='unknown' 을 trade_history.strategy_type 으로 보정 (백업 테이블 포함)
CLAUDE.md: 변경 동작 반영, MT 예약 리비전 0013→0014

998 passed

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

653 lines
51 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
# ── 멀티테넌트(사용자별 매매) 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 = "" # 비어있으면 부트스트랩 관리자 시드 건너뜀
# ═════════════════════════════════════════════════════════════════════════════
# 데이터베이스 & 메시지 큐
# ═════════════════════════════════════════════════════════════════════════════
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
# 자본 배분 글로벌 가드 — 전 전략 합산 현금 사용 상한
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
# 가격 필터 (섹션 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 경로 그대로
# 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% 미만 시 청산 (기존 +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% → 노이즈 손절 방지)
# ═════════════════════════════════════════════════════════════════════════════
# 박스권(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 # 종목당 일일 최대 매수 횟수
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"
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()