- 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>
839 lines
67 KiB
Python
839 lines
67 KiB
Python
"""
|
||
애플리케이션 설정 모듈
|
||
|
||
- 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()
|