수정본

This commit is contained in:
2026-02-22 17:23:20 +09:00
parent b147498c31
commit 32e2263f0a
109 changed files with 13979 additions and 4580 deletions

View File

@@ -1,3 +1,9 @@
"""
애플리케이션 설정 모듈
- pydantic-settings 기반 .env 파일 로드
- DB, Redis, KIS API, AI 프로바이더 등 전역 설정 관리
"""
import os
from pathlib import Path
from pydantic_settings import BaseSettings
@@ -12,7 +18,7 @@ class Settings(BaseSettings):
# Application
APP_NAME: str = "Auto Stock Trading System"
ENVIRONMENT: str = "development"
DEBUG: bool = True
DEBUG: bool = False # 운영 기본값 False (.env에서 DEBUG=true로 개발 모드 활성화)
SECRET_KEY: str = "change-this-secret-key"
# Server
@@ -28,35 +34,78 @@ class Settings(BaseSettings):
# RabbitMQ
RABBITMQ_URL: str = "amqp://autostock:autostock_password@localhost:5672/"
# Korea Investment API
# Korea Investment API - 모드 선택
KIS_MODE: str = "virtual" # "virtual" (모의투자) 또는 "real" (실전투자)
# 모의투자 설정
KIS_VIRTUAL_APP_KEY: str = ""
KIS_VIRTUAL_APP_SECRET: str = ""
KIS_VIRTUAL_ACCOUNT_NO: str = ""
KIS_VIRTUAL_URL: str = "https://openapivts.koreainvestment.com:29443"
# 실전투자 설정
KIS_REAL_APP_KEY: str = ""
KIS_REAL_APP_SECRET: str = ""
KIS_REAL_ACCOUNT_NO: str = ""
KIS_REAL_URL: str = "https://openapi.koreainvestment.com:9443"
# 실제 사용되는 KIS API 설정 (KIS_MODE에 따라 자동 결정)
KOREA_INVESTMENT_APP_KEY: str = ""
KOREA_INVESTMENT_APP_SECRET: str = ""
KOREA_INVESTMENT_ACCOUNT_NUMBER: str = ""
KOREA_INVESTMENT_API_BASE_URL: str = "https://openapi.koreainvestment.com:9443"
# Alias for backward compatibility with .env file
# 레거시 호환 (직접 지정 시 우선)
KIS_APP_KEY: str = ""
KIS_APP_SECRET: str = ""
KIS_ACCOUNT_NO: str = ""
KIS_BASE_URL: str = ""
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Use KIS_* values if KOREA_INVESTMENT_* are empty
if not self.KOREA_INVESTMENT_APP_KEY and self.KIS_APP_KEY:
self.KOREA_INVESTMENT_APP_KEY = self.KIS_APP_KEY
if not self.KOREA_INVESTMENT_APP_SECRET and self.KIS_APP_SECRET:
self.KOREA_INVESTMENT_APP_SECRET = self.KIS_APP_SECRET
if not self.KOREA_INVESTMENT_ACCOUNT_NUMBER and self.KIS_ACCOUNT_NO:
self.KOREA_INVESTMENT_ACCOUNT_NUMBER = self.KIS_ACCOUNT_NO
if self.KIS_BASE_URL:
self.KOREA_INVESTMENT_API_BASE_URL = self.KIS_BASE_URL
self._apply_kis_mode()
def _apply_kis_mode(self):
"""KIS_MODE에 따라 모의/실전 API 설정을 자동 선택"""
is_real = self.KIS_MODE.lower() == "real"
# 1순위: KIS_MODE에 따른 VIRTUAL/REAL 변수
if is_real:
mode_key = self.KIS_REAL_APP_KEY
mode_secret = self.KIS_REAL_APP_SECRET
mode_account = self.KIS_REAL_ACCOUNT_NO
mode_url = self.KIS_REAL_URL
else:
mode_key = self.KIS_VIRTUAL_APP_KEY
mode_secret = self.KIS_VIRTUAL_APP_SECRET
mode_account = self.KIS_VIRTUAL_ACCOUNT_NO
mode_url = self.KIS_VIRTUAL_URL
# 2순위: 레거시 KIS_APP_KEY 등 (직접 지정된 경우)
fallback_key = self.KIS_APP_KEY
fallback_secret = self.KIS_APP_SECRET
fallback_account = self.KIS_ACCOUNT_NO
fallback_url = self.KIS_BASE_URL
# 최종 적용 (mode > legacy > KOREA_INVESTMENT_ 기존값)
self.KOREA_INVESTMENT_APP_KEY = mode_key or fallback_key or self.KOREA_INVESTMENT_APP_KEY
self.KOREA_INVESTMENT_APP_SECRET = mode_secret or fallback_secret or self.KOREA_INVESTMENT_APP_SECRET
self.KOREA_INVESTMENT_ACCOUNT_NUMBER = mode_account or fallback_account or self.KOREA_INVESTMENT_ACCOUNT_NUMBER
self.KOREA_INVESTMENT_API_BASE_URL = mode_url or fallback_url or self.KOREA_INVESTMENT_API_BASE_URL
@property
def is_virtual_trading(self) -> bool:
"""모의투자 모드 여부"""
return self.KIS_MODE.lower() != "real"
# Claude AI
ANTHROPIC_API_KEY: str = ""
# MCP Server
MCP_SERVER_URL: str = "http://localhost:8001"
# MCP AI 검증 (자동매매 전 AI 최종 판단)
MCP_VALIDATION_ENABLED: bool = False # 기본 OFF (.env에서 True로 활성화)
MCP_CONFIDENCE_THRESHOLD: float = 0.5 # AI 신뢰도 최소값 (0.0~1.0)
# News API
NEWS_API_KEY: str = ""
@@ -64,11 +113,78 @@ class Settings(BaseSettings):
# Logging
LOG_LEVEL: str = "INFO"
# Trading Settings
MAX_POSITION_SIZE: float = 0.1 # 10% of portfolio
MAX_DAILY_TRADES: int = 10
STOP_LOSS_PERCENT: float = -5.0
TAKE_PROFIT_PERCENT: float = 15.0
# Trading Settings (CLAUDE.md 섹션 6 기반)
MAX_POSITION_SIZE: float = 0.04 # 1종목당 최대 3-4% (섹션 6.1)
MAX_DAILY_TRADES: int = 5
STOP_LOSS_PERCENT: float = -6.0 # 저변동 종목 손절 -6% (섹션 6.2)
TAKE_PROFIT_PERCENT: float = 5.0 # 익절 기본값
AUTO_TRADE_ENABLED: bool = True
# 포지션 규칙 (섹션 6.1)
MAX_HOLDINGS_COUNT: int = 20 # 동시 보유 최대 20종목
LEVERAGE_ETF_MAX_RATIO: float = 0.20 # 레버리지/인버스 ETF 20% 이내
# ATR 기반 손절/트레일링 (섹션 6.2)
STOP_LOSS_HIGH_VOL: float = -10.0 # 고변동 종목 손절 -10%
ATR_STOP_MULTIPLIER: float = 2.5 # 2.5 × ATR 손절
ATR_TRAILING_MULTIPLIER: float = 1.5 # 1.5 × ATR 트레일링
# MDD 관리 (섹션 6.3)
MDD_PAUSE_THRESHOLD: float = -10.0 # -10% MDD: 신규진입 중단 + 30% 축소
MDD_FORCE_THRESHOLD: float = -20.0 # -20% MDD: 50% 강제 축소
# 가격 필터 (섹션 6.4)
MIN_PRICE_KRW: int = 5000 # 국내 5,000원 이하 매수 금지
MIN_PRICE_USD: float = 4.0 # 미국 $4 이하 매수 금지
# 연속 손실 관리 (섹션 7.4)
CONSECUTIVE_LOSS_BAN_COUNT: int = 3 # 3회 연속 손실
CONSECUTIVE_LOSS_BAN_DAYS: int = 30 # 30거래일 진입 금지
# 평균회귀 전략 자본 배분 (섹션 7.5)
MEAN_REVERSION_MAX_CAPITAL: float = 0.30 # 전체 자본의 30% 이내
# Trend filter (MA-based)
TREND_FILTER_ENABLED: bool = False
TREND_MA_SHORT: int = 20
TREND_MA_LONG: int = 60
TREND_TOLERANCE_PCT: float = 0.005
# 급등 과열 필터 (고점 추격 매수 방지)
OVERHEAT_FILTER_ENABLED: bool = True
OVERHEAT_3DAY_MAX: float = 15.0
OVERHEAT_5DAY_MAX: float = 20.0
OVERHEAT_10DAY_MAX: float = 30.0
OVERHEAT_DISPARITY_MAX: float = 108.0
# 하락장 방어 필터
BEAR_MARKET_FILTER_ENABLED: bool = True
BEAR_MARKET_MIN_SCORE: int = 5
BEAR_MARKET_MAX_TRADES: int = 3
BEAR_MARKET_POSITION_RATIO: float = 0.5
# 데이트레이딩 설정 (CLAUDE.md 섹션 8.2)
DAY_TRADE_ENABLED: bool = True # 데이트레이딩 활성화
DAY_TRADE_MAX_POSITIONS: int = 3 # 동시 최대 종목 수
DAY_TRADE_CAPITAL_RATIO: float = 0.25 # 전체 자본 대비 최대 25%
DAY_TRADE_PER_STOCK_RATIO: float = 0.10 # 종목당 최대 10%
DAY_TRADE_STOP_LOSS_PCT: float = 0.02 # 종목 손절 -2%
DAY_TRADE_DAILY_LOSS_LIMIT: float = -0.03 # 일일 손실 한도 -3%
DAY_TRADE_EOD_FLAT_HOUR: int = 15 # EOD 강제 청산 시 (15시)
DAY_TRADE_EOD_FLAT_MIN: int = 20 # EOD 강제 청산 분 (20분)
DAY_TRADE_ORB_VOL_MULT: float = 1.5 # ORB 돌파 거래량 배수
DAY_TRADE_TRAILING_TRIGGER: float = 0.015 # 트레일링 발동 수익률
# ML / TensorFlow integration toggle
ML_TF_ENABLED: bool = True # ML 모델 기반 보조지표 사용
# ML Ensemble (LightGBM + LSTM + RF)
ML_ENSEMBLE_ENABLED: bool = True
ML_ENSEMBLE_MODEL_DIR: str = "models/ensemble"
ML_ENSEMBLE_RULE_WEIGHT: float = 0.3 # 룰베이스 스코어 비중
ML_ENSEMBLE_ML_WEIGHT: float = 0.7 # ML 스코어 비중
ML_ENSEMBLE_BUY_THRESHOLD: int = 60 # 매수 최소 앙상블 스코어 (0~100)
ML_ENSEMBLE_CONFIDENCE_MIN: float = 0.5 # 최소 신뢰도
# API Rate Limiting & Retry Settings (모의투자 API 안전 운영)
API_MAX_RETRIES: int = 3 # 최대 재시도 횟수

View File

@@ -1,3 +1,9 @@
"""
데이터베이스 연결 모듈
- SQLAlchemy async 엔진 및 세션 팩토리 (PostgreSQL + asyncpg)
- Base 모델 선언, get_db 의존성 제공
"""
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
@@ -10,14 +16,19 @@ if DATABASE_URL.startswith("postgresql://"):
engine = create_async_engine(
DATABASE_URL,
echo=settings.DEBUG,
echo=False, # SQL 로그 비활성화 (성능 개선)
future=True,
pool_pre_ping=True,
pool_size=10,
max_overflow=20,
pool_size=20, # 증가
max_overflow=30, # 증가
pool_recycle=3600, # 1시간마다 연결 재생성
pool_timeout=30, # 타임아웃 설정
connect_args={
"timeout": 60,
"command_timeout": 60,
"server_settings": {
"jit": "off" # JIT 비활성화로 성능 개선
}
}
)
@@ -25,14 +36,20 @@ AsyncSessionLocal = sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
autocommit=False,
autoflush=False,
)
Base = declarative_base()
async def get_db():
"""DB 세션 생성 및 관리 (안전한 종료 보장)"""
async with AsyncSessionLocal() as session:
try:
yield session
except Exception:
await session.rollback()
raise
finally:
await session.close()

View File

@@ -0,0 +1,13 @@
"""공용 JSON 직렬화 헬퍼 (CLAUDE.md 코딩 규칙: json.dumps 대신 _dumps() 사용)"""
import json
import os
def _dumps(obj, **kwargs) -> str:
"""토큰 절약 모드에 따라 compact/pretty JSON 직렬화"""
compact = os.getenv("JSON_COMPACT", "false").lower() == "true"
token_save = os.getenv("TOKEN_SAVE_MODE", "false").lower() == "true"
if compact or token_save:
return json.dumps(obj, ensure_ascii=False, separators=(',', ':'), **kwargs)
return json.dumps(obj, ensure_ascii=False, indent=2, **kwargs)

View File

@@ -0,0 +1,59 @@
"""
한국 주식시장 운영시간 관리
- KST 기준 장 시작/종료 시간 판별
- 공휴일, 주말 휴장 체크
"""
from datetime import datetime, time
try:
import pytz
KST = pytz.timezone('Asia/Seoul')
except ImportError:
# pytz가 없으면 기본 timezone 사용 (환경변수에 의존)
from datetime import timezone, timedelta
KST = timezone(timedelta(hours=9))
def is_market_open() -> bool:
"""
현재 한국 증시 정규장 운영 시간인지 확인
(평일 09:00 ~ 15:30, 공휴일 제외 로직은 아직 미포함)
"""
now = datetime.now(KST)
# 1. 주말 체크 (월=0, ..., 일=6)
if now.weekday() >= 5:
return False
# 2. 시간 체크 (09:00 ~ 15:30)
current_time = now.time()
market_start = time(9, 0)
market_end = time(15, 30)
return market_start <= current_time <= market_end
def is_opening_range_period() -> bool:
"""Opening Range 수집 구간 (09:00~09:29)"""
now = datetime.now(KST)
if now.weekday() >= 5:
return False
t = now.time()
return time(9, 0) <= t < time(9, 30)
def is_day_trade_entry_window() -> bool:
"""데이트레이딩 신규 진입 가능 시간 (09:30~14:59)"""
now = datetime.now(KST)
if now.weekday() >= 5:
return False
t = now.time()
return time(9, 30) <= t < time(15, 0)
def is_eod_flat_time() -> bool:
"""EOD 강제 청산 구간 (15:20~15:30)"""
now = datetime.now(KST)
if now.weekday() >= 5:
return False
t = now.time()
return time(15, 20) <= t <= time(15, 30)