From ec02f2720012f230d4c6103a3ed13a3eb08d1d44 Mon Sep 17 00:00:00 2001 From: shuim Date: Wed, 25 Feb 2026 16:56:34 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A0=88=EB=94=94=EC=8A=A4=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EC=88=98=EC=A0=95=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 265 ++++++++++++++++-- backend/app/core/config.py | 64 +++++ backend/app/main.py | 22 +- backend/app/services/day_trading_strategy.py | 63 +++-- backend/app/services/kis_realtime_stream.py | 7 +- .../app/services/mean_reversion_strategy.py | 18 +- backend/app/services/short_term_strategy.py | 35 +-- backend/app/services/technical_indicators.py | 45 +-- backend/app/services/watchlist.py | 22 +- mcp-server/gemini_client.py | 18 +- 10 files changed, 428 insertions(+), 131 deletions(-) diff --git a/.env.example b/.env.example index 304e078..2bb1ead 100644 --- a/.env.example +++ b/.env.example @@ -1,45 +1,248 @@ -# 한국투자증권 API +# ═══════════════════════════════════════════════════════════════════════════════ +# 자동 주식 매매 시스템 설정 파일 (.env) +# ═══════════════════════════════════════════════════════════════════════════════ +# +# 모든 설정이 .env 파일 변수로 제어 가능합니다. +# 기본값은 config.py에 정의되어 있고, 여기서 오버라이드할 수 있습니다. +# +# 참조: CLAUDE.md - 섹션 1.2 환경 변수 참고 + +# ═══════════════════════════════════════════════════════════════════════════════ +# 기본 애플리케이션 설정 +# ═══════════════════════════════════════════════════════════════════════════════ +ENVIRONMENT=development +DEBUG=false +LOG_LEVEL=INFO + +# 백엔드 서버 +BACKEND_HOST=0.0.0.0 +BACKEND_PORT=8000 +SECRET_KEY=your-secret-key-here + +# ═══════════════════════════════════════════════════════════════════════════════ +# 데이터베이스 및 캐시 +# ═══════════════════════════════════════════════════════════════════════════════ +DATABASE_URL=postgresql://autostock:autostock_password@localhost:5432/autostock_db +REDIS_URL=redis://localhost:6379/0 +RABBITMQ_URL=amqp://autostock:autostock_password@localhost:5672/ + +# PostgreSQL (도커 환경) +POSTGRES_USER=autostock +POSTGRES_PASSWORD=autostock_password +POSTGRES_DB=autostock + +# ═══════════════════════════════════════════════════════════════════════════════ +# 한국투자증권 (KIS) API 설정 +# ═══════════════════════════════════════════════════════════════════════════════ + +# 모드 선택: "virtual" (모의투자) 또는 "real" (실전투자) +KIS_MODE=virtual + +# 모의투자 (Paper Trading) 설정 +KIS_VIRTUAL_APP_KEY=your_virtual_app_key +KIS_VIRTUAL_APP_SECRET=your_virtual_app_secret +KIS_VIRTUAL_ACCOUNT_NO=00000000-01 +KIS_VIRTUAL_URL=https://openapivts.koreainvestment.com:29443 + +# 실전투자 설정 +KIS_REAL_APP_KEY=your_real_app_key +KIS_REAL_APP_SECRET=your_real_app_secret +KIS_REAL_ACCOUNT_NO=00000000-01 +KIS_REAL_URL=https://openapi.koreainvestment.com:9443 + +# WebSocket 접속키 (수동 지정 시 우선, 비어있으면 자동 발급) +KIS_WS_APPROVAL_KEY= + +# WebSocket URL (커스토마이징 필요 시만 수정) +KIS_WS_REAL_URL=wss://openapi.koreainvestment.com:21000/websocket/realtime +KIS_WS_VIRTUAL_URL=wss://openapiwss.koreainvestment.com:29443/websocket/realtime + +# KIS 실시간 스트림 타이밍 (초 단위) +KIS_SYNC_INTERVAL=10 # 구독 목록 동기화 주기 +KIS_RECONNECT_WAIT=5 # 재연결 대기 시간 +KIS_APPROVAL_KEY_TTL=82800 # approval_key 캐시 유효시간 (초, 23시간) + +# 레거시 호환 (직접 지정 시 우선) KOREA_INVESTMENT_APP_KEY=your_app_key_here KOREA_INVESTMENT_APP_SECRET=your_app_secret_here KOREA_INVESTMENT_ACCOUNT_NUMBER=your_account_number KOREA_INVESTMENT_API_BASE_URL=https://openapi.koreainvestment.com:9443 -# Claude AI -ANTHROPIC_API_KEY=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 +# ═══════════════════════════════════════════════════════════════════════════════ +# AI 프로바이더 설정 +# ═══════════════════════════════════════════════════════════════════════════════ +AI_PROVIDER=claude # "claude" 또는 "gemini" +ANTHROPIC_API_KEY=your_anthropic_api_key -# Database -POSTGRES_USER=autostock -POSTGRES_PASSWORD=autostock_password -POSTGRES_DB=autostock_db -DATABASE_URL=postgresql://autostock:autostock_password@postgres:5432/autostock_db +# MCP 서버 설정 +MCP_SERVER_URL=http://localhost:8001 +MCP_VALIDATION_ENABLED=true # AI 검증 활성화 +MCP_CONFIDENCE_THRESHOLD=0.5 # AI 신뢰도 최소값 (0.0~1.0) -# Redis -REDIS_URL=redis://redis:6379/0 +# ═══════════════════════════════════════════════════════════════════════════════ +# 거래 설정 (CLAUDE.md 섹션 5-8 기반) +# ═══════════════════════════════════════════════════════════════════════════════ -# RabbitMQ -RABBITMQ_DEFAULT_USER=autostock -RABBITMQ_DEFAULT_PASS=autostock_password -RABBITMQ_URL=amqp://autostock:autostock_password@rabbitmq:5672/ +# 기본 거래 파라미터 +AUTO_TRADE_ENABLED=true # 자동매매 활성화 +ANALYSIS_STOCK_COUNT=150 # 배치 분석 종목 수 +AUTO_TRADE_TOP_N_PER_RULE=2 # 규칙당 자동매매 최대 종목 수 +MAX_DAILY_TRADES=5 -# Backend -BACKEND_HOST=0.0.0.0 -BACKEND_PORT=8000 -SECRET_KEY=your_secret_key_change_this_in_production +# 포지션 규칙 (섹션 5.1) +MAX_POSITION_SIZE=0.04 # 1종목당 최대 3-4% 자본 +MAX_HOLDINGS_COUNT=20 # 동시 보유 최대 20종목 +LEVERAGE_ETF_MAX_RATIO=0.20 # 레버리지/인버스 ETF 20% 이내 -# Frontend +# 손절/익절 설정 (섹션 5.2) +STOP_LOSS_PERCENT=-6.0 # 저변동 종목 손절 -6% +STOP_LOSS_HIGH_VOL=-10.0 # 고변동 종목 손절 -10% +TAKE_PROFIT_PERCENT=5.0 # 익절 기본값 + +# ATR 기반 손절/트레일링 (섹션 5.2) +ATR_STOP_MULTIPLIER=2.5 # 2.5 × ATR 손절 +ATR_TRAILING_MULTIPLIER=1.5 # 1.5 × ATR 트레일링 + +# MDD (최대 낙폭) 관리 (섹션 5.3) +MDD_PAUSE_THRESHOLD=-10.0 # -10% MDD: 신규진입 중단 + 30% 축소 +MDD_FORCE_THRESHOLD=-20.0 # -20% MDD: 50% 강제 축소 + +# 가격 필터 (섹션 5.4) +MIN_PRICE_KRW=5000 # 국내 5,000원 이하 매수 금지 +MIN_PRICE_USD=4.0 # 미국 $4 이하 매수 금지 + +# 연속 손실 관리 (섹션 7.4) +CONSECUTIVE_LOSS_BAN_COUNT=3 # 3회 연속 손실 +CONSECUTIVE_LOSS_BAN_DAYS=30 # 30거래일 진입 금지 + +# ═══════════════════════════════════════════════════════════════════════════════ +# 전략 A: RSI + 볼린저 밴드 평균회귀 (섹션 6) +# ═══════════════════════════════════════════════════════════════════════════════ +MEAN_REVERSION_MAX_CAPITAL=0.30 # 전체 자본의 30% 이내 (섹션 7.5) +MEAN_REVERSION_RSI_THRESHOLD=30.0 # RSI 과매도 기준 (≤30) +MEAN_REVERSION_MULTIPLIER_STRONG=1.50 # RSI ≤ 20: 강한 신호 배수 (+50%) +MEAN_REVERSION_MULTIPLIER_NORMAL=1.25 # RSI ≤ 25: 중간 신호 배수 (+25%) +MEAN_REVERSION_MULTIPLIER_WEAK=1.00 # 기본 신호 배수 (4%) + +# 시장 국면 필터 (7.5) +BEAR_MARKET_FILTER_ENABLED=true +BEAR_MARKET_MIN_SCORE=5 +BEAR_MARKET_MAX_TRADES=3 +BEAR_MARKET_POSITION_RATIO=0.5 + +# 급등 과열 필터 (섹션 5.4) +OVERHEAT_FILTER_ENABLED=true +OVERHEAT_3DAY_MAX=15.0 # 3일 최대 상승률 15% +OVERHEAT_5DAY_MAX=20.0 # 5일 최대 상승률 20% +OVERHEAT_10DAY_MAX=30.0 # 10일 최대 상승률 30% +OVERHEAT_DISPARITY_MAX=108.0 # 괴리율 최대 108% + +# 트렌드 필터 (7.1) +TREND_FILTER_ENABLED=false +TREND_MA_SHORT=20 # 단기 MA 기간 +TREND_MA_LONG=60 # 장기 MA 기간 +TREND_TOLERANCE_PCT=0.005 # 허용 오차 0.5% + +# ═══════════════════════════════════════════════════════════════════════════════ +# 전략 B: 데이트레이딩 (섹션 8.2) +# ═══════════════════════════════════════════════════════════════════════════════ +DAY_TRADE_ENABLED=true # 데이트레이딩 활성화 +DAY_TRADE_MAX_POSITIONS=3 # 동시 최대 3종목 +DAY_TRADE_CAPITAL_RATIO=0.25 # 전체 자본 대비 최대 25% +DAY_TRADE_MAX_RATIO_DT=0.25 # 전체 데이트레이딩 최대 25% +DAY_TRADE_PER_STOCK_RATIO=0.10 # 종목당 최대 10% +DAY_TRADE_MAX_CAPITAL_RATIO=0.10 # 종목당 최대 10% + +# 데이트레이딩 손절/익절 +DAY_TRADE_ORB_STOP_LOSS_PCT=0.02 # ORB 손절 -2% +DAY_TRADE_REVERSION_STOP_LOSS_PCT=0.025 # VWAP/RSI_BB 손절 -2.5% +DAY_TRADE_TRAILING_TRIGGER=0.015 # 트레일링 발동 +1.5% +DAY_TRADE_TRAILING_ATR_MULT=1.0 # 트레일링 ATR 배수 +DAY_TRADE_ORB_BREAKOUT_MARGIN=0.002 # ORB 돌파 최소 여유 +0.2% +DAY_TRADE_COST_PCT=0.005 # 수수료+슬리피지 0.5% + +# 데이트레이딩 일일/시간 제한 +DAY_TRADE_DAILY_LOSS_LIMIT=-0.03 # 일일 손실 한도 -3% +DAY_TRADE_ENTRY_CUTOFF_HOUR=15 # 신규 진입 마감 시간 (15시) +DAY_TRADE_ENTRY_CUTOFF_MIN=0 # 신규 진입 마감 분 +DAY_TRADE_EOD_HOUR=15 # EOD 강제 청산 시작 시간 +DAY_TRADE_EOD_MIN=20 # EOD 강제 청산 시작 분 +DAY_TRADE_RSI_BREAKEVEN_THRESHOLD=40.0 # RSI_BB 진입 기준 (5분봉 RSI ≤ 40) + +# ═══════════════════════════════════════════════════════════════════════════════ +# 단기 전략 (15분봉, 섹션 8.2) +# ═══════════════════════════════════════════════════════════════════════════════ +SHORT_TERM_MIN_ATR_RATIO=0.008 # ATR/close ≥ 0.8% (거래비용 커버) +SHORT_TERM_RSI_LOOKBACK=3 # RSI 반등 확인 과거 봉 수 +SHORT_TERM_VOLUME_MULTIPLIER=1.5 # 거래량 필터 배수 (1.5배) + +# ═══════════════════════════════════════════════════════════════════════════════ +# 감시 목록 관리 (watchlist.py) +# ═══════════════════════════════════════════════════════════════════════════════ +WATCHLIST_INIT_SIZE=50 # 아침 배치 초기화 종목 수 +WATCHLIST_MAX_SIZE=100 # 최대 관리 종목 수 +WATCHLIST_ENTRY_SCORE=60 # 장중 신규 진입 기준 점수 +WATCHLIST_EXIT_SCORE=20 # 장중 제거 기준 점수 + +# ═══════════════════════════════════════════════════════════════════════════════ +# 기술적 지표 설정 (technical_indicators.py) +# ═══════════════════════════════════════════════════════════════════════════════ + +# RSI (Relative Strength Index) +RSI_PERIOD=14 # RSI 계산 기간 (14일) +RSI_OVERSOLD_LEVEL=30.0 # RSI 과매도 수준 +RSI_OVERBOUGHT_LEVEL=70.0 # RSI 과매수 수준 +RSI_RECOVERY_RANGE_MIN=30.0 # RSI 회복 범위 최소값 (30~45) +RSI_RECOVERY_RANGE_MAX=45.0 # RSI 회복 범위 최대값 +RSI_STABLE_RANGE_MIN=45.0 # RSI 안정 범위 최소값 (45~60) +RSI_STABLE_RANGE_MAX=60.0 # RSI 안정 범위 최대값 + +# 볼린저 밴드 +BOLLINGER_PERIOD=20 # 볼린저 밴드 기간 (20일) +BOLLINGER_STDDEV=2.0 # 표준편차 배수 + +# 이동평균 (Moving Average) +MA_SHORT_PERIOD=5 # 단기 MA (5일) +MA_MEDIUM_PERIOD=20 # 중기 MA (20일) +MA_LONG_PERIOD=60 # 장기 MA (60일) + +# ATR (Average True Range) +ATR_PERIOD=14 # ATR 계산 기간 (14일) + +# ═══════════════════════════════════════════════════════════════════════════════ +# API 레이트 제한 및 안전 운영 +# ═══════════════════════════════════════════════════════════════════════════════ +API_MAX_RETRIES=3 # 최대 재시도 횟수 +API_RETRY_DELAY=1.5 # 초기 재시도 대기 시간 (초) +API_RETRY_BACKOFF=2.0 # 지수 백오프 배율 +API_MIN_REQUEST_INTERVAL=0.5 # 최소 요청 간격 (초) +API_MAX_CONCURRENT_REQUESTS=4 # 동시 최대 요청 수 +API_TIMEOUT=30.0 # API 타임아웃 (초) + +# API 모니터링 +API_ERROR_THRESHOLD=0.3 # 오류율 임계값 (30%) +API_MONITORING_WINDOW=100 # 모니터링 윈도우 (최근 100개 요청) + +# ═══════════════════════════════════════════════════════════════════════════════ +# ML (머신러닝) 설정 +# ═══════════════════════════════════════════════════════════════════════════════ +ML_TF_ENABLED=true # ML 모델 기반 보조지표 사용 +ML_ENSEMBLE_ENABLED=true # ML 앙상블 활성화 +ML_ENSEMBLE_MODEL_DIR=models/ensemble +ML_ENSEMBLE_RULE_WEIGHT=0.3 # 룰베이스 스코어 비중 (30%) +ML_ENSEMBLE_ML_WEIGHT=0.7 # ML 스코어 비중 (70%) +ML_ENSEMBLE_BUY_THRESHOLD=60 # 매수 최소 앙상블 스코어 (0~100) +ML_ENSEMBLE_CONFIDENCE_MIN=0.5 # 최소 신뢰도 + +# ═══════════════════════════════════════════════════════════════════════════════ +# 프론트엔드 (선택사항) +# ═══════════════════════════════════════════════════════════════════════════════ VITE_API_URL=http://localhost:8000 VITE_WS_URL=ws://localhost:8000/ws -# MCP Server -MCP_SERVER_HOST=0.0.0.0 -MCP_SERVER_PORT=8001 +# ═══════════════════════════════════════════════════════════════════════════════ +# 기타 설정 +# ═══════════════════════════════════════════════════════════════════════════════ +TOKEN_SAVE_MODE=true # JSON 압축, 차트 데이터 30일 제한 +NEWS_API_KEY=your_news_api_key -# News API (선택) -NEWS_API_KEY=your_news_api_key_here - -# 환경 -ENVIRONMENT=development -DEBUG=true - -# 로그 레벨 -LOG_LEVEL=INFO diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 9232529..71309aa 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -206,6 +206,70 @@ class Settings(BaseSettings): # Monitoring & Alerting API_ERROR_THRESHOLD: float = 0.3 # 오류율 임계값 (30%) API_MONITORING_WINDOW: int = 100 # 모니터링 윈도우 (최근 N개 요청) + + # ═════════════════════════════════════════════════════════════════════════════ + # 데이트레이딩 세부 파라미터 (day_trading_strategy.py) + # ═════════════════════════════════════════════════════════════════════════════ + DAY_TRADE_MAX_CAPITAL_RATIO: float = 0.10 # 종목당 최대 10% 자본 + DAY_TRADE_MAX_RATIO_DT: float = 0.25 # 전체 데이트레이딩 최대 25% + DAY_TRADE_RSI_BREAKEVEN_THRESHOLD: float = 40.0 # RSI_BB 진입 기준 (5분봉 RSI ≤ 40) + DAY_TRADE_ORB_STOP_LOSS_PCT: float = 0.02 # ORB 손절 -2% + DAY_TRADE_REVERSION_STOP_LOSS_PCT: float = 0.025 # VWAP/RSI_BB 손절 -2.5% + DAY_TRADE_TRAILING_TRIGGER: float = 0.015 # 트레일링 발동 +1.5% + DAY_TRADE_TRAILING_ATR_MULT: float = 1.0 # 트레일링 ATR 배수 + DAY_TRADE_ORB_BREAKOUT_MARGIN: float = 0.002 # ORB 돌파 최소 여유 +0.2% + DAY_TRADE_COST_PCT: float = 0.005 # 수수료+슬리피지 합산 (매수+매도) + DAY_TRADE_ENTRY_CUTOFF_HOUR: int = 15 # 신규 진입 마감 시간 (15시) + DAY_TRADE_ENTRY_CUTOFF_MIN: int = 0 # 신규 진입 마감 분 + DAY_TRADE_EOD_HOUR: int = 15 # EOD 강제 청산 시작 시간 + DAY_TRADE_EOD_MIN: int = 20 # EOD 강제 청산 시작 분 + + # ═════════════════════════════════════════════════════════════════════════════ + # 단기 전략 (short_term_strategy.py) + # ═════════════════════════════════════════════════════════════════════════════ + 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.5 # 거래량 필터 배수 (1.5배 이상) + + # ═════════════════════════════════════════════════════════════════════════════ + # 평균회귀 전략 (mean_reversion_strategy.py) + # ═════════════════════════════════════════════════════════════════════════════ + MEAN_REVERSION_RSI_THRESHOLD: float = 30.0 # RSI 과매도 기준 (≤30) + MEAN_REVERSION_MULTIPLIER_STRONG: float = 1.50 # 강한 신호 배수 (+50%) + MEAN_REVERSION_MULTIPLIER_NORMAL: float = 1.25 # 중간 신호 배수 (+25%) + MEAN_REVERSION_MULTIPLIER_WEAK: float = 1.00 # 약한 신호 배수 (기본값) + + # ═════════════════════════════════════════════════════════════════════════════ + # KIS WebSocket & 실시간 스트림 (kis_realtime_stream.py) + # ═════════════════════════════════════════════════════════════════════════════ + KIS_SYNC_INTERVAL: int = 10 # 구독 목록 동기화 주기 (초) + KIS_RECONNECT_WAIT: int = 5 # 재연결 대기 시간 (초) + KIS_APPROVAL_KEY_TTL: int = 82800 # approval_key 캐시 유효시간 (초, 23시간) + + # ═════════════════════════════════════════════════════════════════════════════ + # 감시 목록 (watchlist.py) + # ═════════════════════════════════════════════════════════════════════════════ + WATCHLIST_INIT_SIZE: int = 50 # 아침 배치 초기화 종목 수 + WATCHLIST_MAX_SIZE: int = 100 # 최대 관리 종목 수 + WATCHLIST_ENTRY_SCORE: int = 60 # 장중 신규 진입 기준 점수 + WATCHLIST_EXIT_SCORE: int = 20 # 장중 제거 기준 점수 + + # ═════════════════════════════════════════════════════════════════════════════ + # 기술적 지표 임계값 (technical_indicators.py) + # ═════════════════════════════════════════════════════════════════════════════ + 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 안정 범위 최대값 + 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일) + RSI_PERIOD: int = 14 # RSI 계산 기간 + ATR_PERIOD: int = 14 # ATR 계산 기간 class Config: env_file = str(BACKEND_ENV_FILE) if BACKEND_ENV_FILE.exists() else ".env" diff --git a/backend/app/main.py b/backend/app/main.py index a950f93..138830e 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -59,13 +59,25 @@ def _normalize_recommendations(rec: dict) -> dict: """ if not rec: return rec - # 이미 recommendations_by_rule 키가 있으면 그대로 사용 - if rec.get("recommendations_by_rule"): + + # Case 1: Already normalized (dict) + if isinstance(rec.get("recommendations_by_rule"), dict): return rec - # recommendations 리스트를 rule 기준으로 그룹화 - raw_list = rec.get("recommendations", []) + + # Determine the source of the recommendation list + raw_list = [] + if "recommendations" in rec and isinstance(rec["recommendations"], list): + raw_list = rec["recommendations"] + elif "recommendations_by_rule" in rec and isinstance(rec["recommendations_by_rule"], list): + # This is the case that causes the bug + raw_list = rec["recommendations_by_rule"] + if not raw_list: + # if there is no list to process, make sure the output has a dict + rec["recommendations_by_rule"] = {} return rec + + # Group the list by rule by_rule: dict = {} for item in raw_list: rule = ( @@ -75,6 +87,8 @@ def _normalize_recommendations(rec: dict) -> dict: or "general" ) by_rule.setdefault(rule, []).append(item) + + # Return the original dict with the new/replaced 'recommendations_by_rule' return {**rec, "recommendations_by_rule": by_rule} diff --git a/backend/app/services/day_trading_strategy.py b/backend/app/services/day_trading_strategy.py index e1a0fd9..9ff8494 100644 --- a/backend/app/services/day_trading_strategy.py +++ b/backend/app/services/day_trading_strategy.py @@ -11,34 +11,43 @@ - 일일 손실 한도: 전체 자본 -3% 초과 시 신규 진입 중단 - 동시 최대 3종목, 종목당 최대 10% 자본 - 전체 데이트레이딩 자본: 총자산의 최대 25% (CLAUDE.md 8.2) - - 수수료+슬리피지 포함 기준: 0.5% (매수 0.015% + 매도 0.3% + 슬리피지 0.2%) + +[.env 설정] + DAY_TRADE_MAX_CAPITAL_RATIO + DAY_TRADE_MAX_RATIO_DT + DAY_TRADE_MAX_POSITIONS (== MAX_DAY_TRADE_POSITIONS) + DAY_TRADE_DAILY_LOSS_LIMIT + DAY_TRADE_ORB_STOP_LOSS_PCT + DAY_TRADE_REVERSION_STOP_LOSS_PCT + DAY_TRADE_TRAILING_TRIGGER + DAY_TRADE_TRAILING_ATR_MULT + DAY_TRADE_ORB_BREAKOUT_MARGIN + DAY_TRADE_COST_PCT + DAY_TRADE_ENTRY_CUTOFF_HOUR, DAY_TRADE_ENTRY_CUTOFF_MIN + DAY_TRADE_EOD_HOUR, DAY_TRADE_EOD_MIN """ import logging from typing import Optional, Dict, List, Tuple from dataclasses import dataclass from datetime import time +from app.core.config import settings logger = logging.getLogger(__name__) # ── 장 시간 상수 (KST) ────────────────────────────────────────────────────── MARKET_OPEN = time(9, 0) ORB_END = time(9, 30) # Opening Range 집계 종료 -ENTRY_CUTOFF = time(15, 0) # 신규 진입 마감 (이후는 청산만) -EOD_FLAT = time(15, 20) # EOD 강제 청산 시작 -MARKET_CLOSE = time(15, 30) -# ── 리스크 파라미터 ─────────────────────────────────────────────────────────── -MAX_CAPITAL_RATIO = 0.10 # 종목당 최대 10% -MAX_DAY_TRADE_RATIO = 0.25 # 전체 데이트레이딩 최대 25% -MAX_POSITIONS = 3 # 동시 최대 3종목 -DAILY_LOSS_LIMIT = -0.03 # 일일 손실 한도 -3% -STOP_LOSS_PCT = 0.02 # ORB 손절 -2% -STOP_LOSS_PCT_REVERSION = 0.025 # VWAP/RSI_BB 평균회귀 전략 손절 -2.5% -# ↑ ORB(-2%)보다 약간 넓게 잡되, 일일 손실 한도(-3%) 이내에서 단일 포지션이 소화 가능하도록 설정 -TRAILING_TRIGGER = 0.015 # 트레일링 발동 +1.5% -TRAILING_ATR_MULT = 1.0 # 트레일링 ATR 배수 -ORB_BREAKOUT_MARGIN = 0.002 # ORB 돌파 최소 여유 +0.2% -COST_PCT = 0.005 # 수수료+슬리피지 합산 (매수+매도) +# 동적 시간 설정 (settings에서 읽음) +def _get_entry_cutoff() -> time: + """신규 진입 마감 시간 (.env: DAY_TRADE_ENTRY_CUTOFF_HOUR, DAY_TRADE_ENTRY_CUTOFF_MIN)""" + return time(settings.DAY_TRADE_ENTRY_CUTOFF_HOUR, settings.DAY_TRADE_ENTRY_CUTOFF_MIN) + +def _get_eod_flat() -> time: + """EOD 강제 청산 시작 시간 (.env: DAY_TRADE_EOD_HOUR, DAY_TRADE_EOD_MIN)""" + return time(settings.DAY_TRADE_EOD_HOUR, settings.DAY_TRADE_EOD_MIN) + +MARKET_CLOSE = time(15, 30) # ── 데이터 클래스 ───────────────────────────────────────────────────────────── @@ -226,7 +235,7 @@ def check_orb_signal( if not orb or current_price <= 0: return None - if current_price < orb.high * (1 + ORB_BREAKOUT_MARGIN): + if current_price < orb.high * (1 + settings.DAY_TRADE_ORB_BREAKOUT_MARGIN): return None # 최근 1분봉 거래량 체크 @@ -243,7 +252,7 @@ def check_orb_signal( if candles_5m[-2].close < candles_5m[-2].open: return None # 직전 5분봉 음봉 → 스킵 - stop_loss = max(orb.low, current_price * (1 - STOP_LOSS_PCT)) + stop_loss = max(orb.low, current_price * (1 - settings.DAY_TRADE_ORB_STOP_LOSS_PCT)) risk = current_price - stop_loss # 수수료 고려: 목표 = 진입가 + 2R + 수수료 보전 target = current_price + max(risk * 2.0, current_price * 0.03) @@ -302,8 +311,8 @@ def check_vwap_bounce_signal( return None atr = _atr(candles_5min, 14) - raw_stop = (current_price - 1.5 * atr) if atr else current_price * (1 - STOP_LOSS_PCT) - stop_loss = max(raw_stop, current_price * (1 - STOP_LOSS_PCT_REVERSION), 1.0) # 하드 스탑 -2.5% (일일 손실 한도 -3% 이내) + raw_stop = (current_price - 1.5 * atr) if atr else current_price * (1 - settings.DAY_TRADE_ORB_STOP_LOSS_PCT) + stop_loss = max(raw_stop, current_price * (1 - settings.DAY_TRADE_REVERSION_STOP_LOSS_PCT), 1.0) # 하드 스탑 -2.5% (일일 손실 한도 -3% 이내) target = (current_price + 2.0 * atr) if atr else vwap * 1.03 return DayTradeSignal( @@ -360,8 +369,8 @@ def check_rsi_bb_signal( return None atr = _atr(candles_5min, 14) - raw_stop = (current_price - 1.5 * atr) if atr else current_price * (1 - STOP_LOSS_PCT) - stop_loss = max(raw_stop, current_price * (1 - STOP_LOSS_PCT_REVERSION), 1.0) # 하드 스탑 -2.5% (일일 손실 한도 -3% 이내) + raw_stop = (current_price - 1.5 * atr) if atr else current_price * (1 - settings.DAY_TRADE_ORB_STOP_LOSS_PCT) + stop_loss = max(raw_stop, current_price * (1 - settings.DAY_TRADE_REVERSION_STOP_LOSS_PCT), 1.0) # 하드 스탑 -2.5% (일일 손실 한도 -3% 이내) # 최소 1.5R 보장: bb_middle이 너무 가까우면 R:R 확보를 위해 목표 상향 risk = current_price - stop_loss target = max(bb_middle, current_price + risk * 1.5) @@ -403,10 +412,10 @@ def check_day_trade_exit( 5. 5분봉 RSI ≥ 70 (과매수 청산) """ # 1. EOD 강제 청산 - if current_time >= EOD_FLAT: + if current_time >= _get_eod_flat(): return DayTradeExit( action="SELL_ALL", - reason=f"EOD 강제 청산 ({current_time.strftime('%H:%M')} ≥ 15:20)", + reason=f"EOD 강제 청산 ({current_time.strftime('%H:%M')} ≥ {_get_eod_flat().strftime('%H:%M')})", ) # 2. 손절 @@ -427,10 +436,10 @@ def check_day_trade_exit( # 4. 트레일링 스탑 (1.5% 이상 수익 시 발동) profit_pct = (highest_since_entry - entry_price) / entry_price - if profit_pct >= TRAILING_TRIGGER: + if profit_pct >= settings.DAY_TRADE_TRAILING_TRIGGER: atr = _atr(candles_5min, 14) if atr: - trailing_stop = highest_since_entry - TRAILING_ATR_MULT * atr + trailing_stop = highest_since_entry - settings.DAY_TRADE_TRAILING_ATR_MULT * atr else: trailing_stop = highest_since_entry * (1 - 0.02) # fallback: 최고가 -2% (5분봉 데이터 부족 시) if current_price <= trailing_stop: @@ -446,7 +455,7 @@ def check_day_trade_exit( rsi = _rsi(candles_5min, 14) if rsi is not None and rsi >= 70: gain_pct = (current_price - entry_price) / entry_price * 100 - if gain_pct > COST_PCT * 100: # 수수료 이상 수익인 경우만 + if gain_pct > settings.DAY_TRADE_COST_PCT * 100: # 수수료 이상 수익인 경우만 return DayTradeExit( action="SELL_ALL", reason=f"과매수 청산: RSI={rsi:.1f}≥70, 수익={gain_pct:.1f}%", diff --git a/backend/app/services/kis_realtime_stream.py b/backend/app/services/kis_realtime_stream.py index 654c4b7..373e772 100644 --- a/backend/app/services/kis_realtime_stream.py +++ b/backend/app/services/kis_realtime_stream.py @@ -46,9 +46,10 @@ TR_TICK = "H0STCNT0" # 주식 현재가 (체결 틱) TR_QUOTE = "H0STASP0" # 주식 호가 # ── 타이밍 상수 ────────────────────────────────────────────────────────────── -_SYNC_INTERVAL = 10 # 구독 목록 동기화 주기 (초) -_RECONNECT_WAIT = 5 # 재연결 대기 (초) -_APPROVAL_KEY_TTL = 82800 # approval_key 캐시 유효시간 (23시간, 초) +# (.env 설정: KIS_SYNC_INTERVAL, KIS_RECONNECT_WAIT, KIS_APPROVAL_KEY_TTL) +_SYNC_INTERVAL = settings.KIS_SYNC_INTERVAL # 구독 목록 동기화 주기 (초) +_RECONNECT_WAIT = settings.KIS_RECONNECT_WAIT # 재연결 대기 (초) +_APPROVAL_KEY_TTL = settings.KIS_APPROVAL_KEY_TTL # approval_key 캐시 유효시간 (초) # ── 모듈 레벨 approval_key 캐시 ────────────────────────────────────────────── diff --git a/backend/app/services/mean_reversion_strategy.py b/backend/app/services/mean_reversion_strategy.py index 02b1bc2..f74775c 100644 --- a/backend/app/services/mean_reversion_strategy.py +++ b/backend/app/services/mean_reversion_strategy.py @@ -7,12 +7,20 @@ RSI + 볼린저 밴드 평균회귀 전략 (CLAUDE.md 섹션 7.1-7.5) ① R:R 개선: 손절 -8%→-6%, 1차 익절 RSI 45~50→55~65, 트레일링 스탑 도입 ② 진입 기회 확대: BULL RSI ≤25 → ≤30 (2배 증가) ③ 동적 포지션 사이징: RSI/ADX 기반 4~8% 다이나믉 비중 + +[.env 설정] + MEAN_REVERSION_RSI_THRESHOLD + MEAN_REVERSION_MULTIPLIER_STRONG + MEAN_REVERSION_MULTIPLIER_NORMAL + MEAN_REVERSION_MULTIPLIER_WEAK """ import logging from typing import Optional, Dict, List, Tuple from dataclasses import dataclass from enum import Enum +from app.core.config import settings + logger = logging.getLogger(__name__) @@ -208,7 +216,7 @@ def check_buy_signal( return None if regime == MarketRegime.BULL: - rsi_ok = rsi <= 30 # ← 25 → 30으로 완화: 진입 기회 ~2배 증가 + rsi_ok = rsi <= settings.MEAN_REVERSION_RSI_THRESHOLD # ← 30 (또는 .env 설정값) vol_ok = current_volume >= avg_volume_20d * 2 # 거래량 20일평균 200% 이상 if rsi_ok or (vol_ok and rsi <= 50): # vol_ok 단독 시 RSI 50 상한 (과열 진입 차단) bb_low_minus = bb_lower * 0.97 # -3% @@ -357,13 +365,13 @@ def calculate_position_size( # RSI 기반 동적 변수 스케일링 if rsi is not None: if rsi <= 20: - multiplier = 1.50 # 군도 과매도 → 6% + multiplier = settings.MEAN_REVERSION_MULTIPLIER_STRONG # 강한 과매도 신호 elif rsi <= 25: - multiplier = 1.25 # 강한 신호 → 5% + multiplier = settings.MEAN_REVERSION_MULTIPLIER_NORMAL # 강한 신호 else: - multiplier = 1.00 # 표준 → 4% + multiplier = settings.MEAN_REVERSION_MULTIPLIER_WEAK # 표준 신호 else: - multiplier = 1.00 + multiplier = settings.MEAN_REVERSION_MULTIPLIER_WEAK # ADX < 15: 특돉한 회황장 → +0.25× 비중 보너스 (최대 2.0× = 8%) if adx is not None and adx < 15: diff --git a/backend/app/services/short_term_strategy.py b/backend/app/services/short_term_strategy.py index 978608e..612e9bc 100644 --- a/backend/app/services/short_term_strategy.py +++ b/backend/app/services/short_term_strategy.py @@ -4,16 +4,11 @@ 전체 자본의 최대 25% 이내에서 운용. 일봉 RSI ≤ 40 관심 종목에 한해 15분봉 신호를 감지한다. -[승률 개선 항목] - 1. 가격 하한 필터 → ATR 비율 필터 (ATR/close ≥ 0.8%) - — 거래비용을 커버할 수 있는 변동성 종목만 선별 - 2. 거래량 배수 1.3 → 1.5 (허위 신호 감소) - 3. RSI 방향 확인 — RSI가 바닥에서 반등 중인 경우만 진입 - 4. 반전 캔들 확인 — 양봉 or 긴 하단 꼬리 (optional) - 5. 손절 조건 추가 — entry - 1.0×ATR (CRITICAL: 기존 코드에 누락되어 있던 것) - 6. 장 초반 30분 / 마감 15분 진입 금지 (노이즈 구간) - -⚠️ 슬리피지 주의: 수수료 0.3% + 슬리피지 0.2% 포함 백테스트 필수 +[.env 설정] + SHORT_TERM_MIN_ATR_RATIO (거래비용 커버용 ATR 비율 ≥ 0.8%) + SHORT_TERM_RSI_LOOKBACK (RSI 반등 확인 과거 봉 수) + SHORT_TERM_VOLUME_MULTIPLIER (거래량 배수) + DAY_TRADE_ENABLED (단기 전략 활성화 여부) """ import logging from dataclasses import dataclass, field @@ -21,22 +16,17 @@ from datetime import time as dtime from enum import Enum from typing import List, Optional +from app.core.config import settings from .mean_reversion_strategy import calculate_rsi, calculate_bollinger logger = logging.getLogger(__name__) -# 자본 비율 상한 -MAX_CAPITAL_RATIO = 0.25 # 비용 가정 — 백테스트 시 왕복으로 적용 COMMISSION_RATE = 0.003 SLIPPAGE_RATE = 0.002 TOTAL_COST_RATE = COMMISSION_RATE + SLIPPAGE_RATE -# 승률 개선 파라미터 -MIN_ATR_RATIO = 0.008 # ATR/close ≥ 0.8% : 거래비용 커버 보장 -RSI_DIRECTION_LOOKBACK = 3 # RSI 반등 확인용 과거 봉 수 - # 장 운영 시간 필터 _SKIP_OPEN_UNTIL = dtime(9, 30) # 장 초반 30분 제외 (개장 직후 변동성 과다) _SKIP_CLOSE_FROM = dtime(15, 15) # 마감 15분 전 제외 (종가 청산 압력) @@ -89,11 +79,10 @@ class ShortTermStrategy: DAILY_RSI_THRESHOLD = 40.0 M15_RSI_ENTRY = 30.0 M15_RSI_EXIT = 55.0 - VOLUME_MULTIPLIER = 1.5 # 1.3 → 1.5 강화 - MIN_ATR_RATIO = MIN_ATR_RATIO - def __init__(self, max_capital_ratio: float = MAX_CAPITAL_RATIO): - self.max_capital_ratio = max_capital_ratio + def __init__(self, max_capital_ratio: float = None): + # .env에서 DAY_TRADE_CAPITAL_RATIO 읽기 (없으면 기본값 0.25) + self.max_capital_ratio = max_capital_ratio or settings.DAY_TRADE_CAPITAL_RATIO # ------------------------------------------------------------------ # Public API @@ -137,7 +126,7 @@ class ShortTermStrategy: if not (_SKIP_OPEN_UNTIL <= trade_time < _SKIP_CLOSE_FROM): return None - min_len = 25 + RSI_DIRECTION_LOOKBACK + min_len = 25 + settings.SHORT_TERM_RSI_LOOKBACK if len(m15_closes) < min_len or len(m15_volumes) < 20: return None @@ -171,14 +160,14 @@ class ShortTermStrategy: # 조건 6: 거래량 급증 (1.5배 강화) vol_ma20 = sum(m15_volumes[-20:]) / 20 - if vol_ma20 == 0 or current_volume <= vol_ma20 * self.VOLUME_MULTIPLIER: + if vol_ma20 == 0 or current_volume <= vol_ma20 * settings.SHORT_TERM_VOLUME_MULTIPLIER: return None # 조건 7: ATR 비율 필터 — 5,000원 필터 대체 atr_ratio = 0.0 if atr_15m is not None: atr_ratio = atr_15m / close if close > 0 else 0.0 - if atr_ratio < self.MIN_ATR_RATIO: + if atr_ratio < settings.SHORT_TERM_MIN_ATR_RATIO: # 변동성 부족 → 1.5×ATR 익절 목표가 왕복 비용에 도달안함 return None diff --git a/backend/app/services/technical_indicators.py b/backend/app/services/technical_indicators.py index adfed7b..bc0ce58 100644 --- a/backend/app/services/technical_indicators.py +++ b/backend/app/services/technical_indicators.py @@ -158,10 +158,11 @@ def analyze_macd(chart_data: list) -> dict: def analyze_rsi(chart_data: list) -> dict: - if len(chart_data) < 15: + rsi_period = settings.RSI_PERIOD # 기본값 14 + if len(chart_data) < rsi_period + 1: return {"signal": None, "description": "데이터 부족"} - prices = [float(item.get("stck_clpr", 0)) for item in chart_data[-15:]] + prices = [float(item.get("stck_clpr", 0)) for item in chart_data[-(rsi_period+1):]] gains = [] losses = [] for i in range(1, len(prices)): @@ -173,8 +174,8 @@ def analyze_rsi(chart_data: list) -> dict: gains.append(0) losses.append(abs(change)) - avg_gain = sum(gains[-14:]) / 14 - avg_loss = sum(losses[-14:]) / 14 + avg_gain = sum(gains[-rsi_period:]) / rsi_period + avg_loss = sum(losses[-rsi_period:]) / rsi_period if avg_loss == 0: rsi = 100 @@ -182,8 +183,8 @@ def analyze_rsi(chart_data: list) -> dict: rs = avg_gain / avg_loss rsi = 100 - (100 / (1 + rs)) - if len(chart_data) >= 16: - prev_prices = [float(item.get("stck_clpr", 0)) for item in chart_data[-16:-1]] + if len(chart_data) >= rsi_period + 2: + prev_prices = [float(item.get("stck_clpr", 0)) for item in chart_data[-(rsi_period+2):-(1)]] prev_gains = [] prev_losses = [] for i in range(1, len(prev_prices)): @@ -195,8 +196,8 @@ def analyze_rsi(chart_data: list) -> dict: prev_gains.append(0) prev_losses.append(abs(change)) - prev_avg_gain = sum(prev_gains[-14:]) / 14 - prev_avg_loss = sum(prev_losses[-14:]) / 14 + prev_avg_gain = sum(prev_gains[-rsi_period:]) / rsi_period + prev_avg_loss = sum(prev_losses[-rsi_period:]) / rsi_period if prev_avg_loss == 0: rsi_prev = 100 else: @@ -218,16 +219,18 @@ def analyze_rsi(chart_data: list) -> dict: def analyze_bollinger(chart_data: list) -> dict: - if len(chart_data) < 20: + bb_period = settings.BOLLINGER_PERIOD # 기본값 20 + bb_stddev = settings.BOLLINGER_STDDEV # 기본값 2.0 + if len(chart_data) < bb_period: return {"signal": None, "description": "데이터 부족"} - prices = [float(item.get("stck_clpr", 0)) for item in chart_data[-20:]] - ma20 = sum(prices) / len(prices) - variance = sum((p - ma20) ** 2 for p in prices) / len(prices) + prices = [float(item.get("stck_clpr", 0)) for item in chart_data[-bb_period:]] + ma_bb = sum(prices) / len(prices) + variance = sum((p - ma_bb) ** 2 for p in prices) / len(prices) std_dev = variance ** 0.5 - upper_band = ma20 + (2 * std_dev) - lower_band = ma20 - (2 * std_dev) + upper_band = ma_bb + (bb_stddev * std_dev) + lower_band = ma_bb - (bb_stddev * std_dev) current_price = prices[-1] if current_price < lower_band: @@ -239,13 +242,17 @@ def analyze_bollinger(chart_data: list) -> dict: def analyze_moving_average(chart_data: list) -> dict: - if len(chart_data) < 60: + ma_long = settings.MA_LONG_PERIOD # 기본값 60 + ma_short = settings.MA_MEDIUM_PERIOD # 기본값 20 + ma_quick = settings.MA_SHORT_PERIOD # 기본값 5 + + if len(chart_data) < ma_long: return {"signal": None, "description": "데이터 부족"} - prices = [float(item.get("stck_clpr", 0)) for item in chart_data[-60:]] - ma5 = sum(prices[-5:]) / 5 - ma20 = sum(prices[-20:]) / 20 - ma60 = sum(prices[-60:]) / 60 + prices = [float(item.get("stck_clpr", 0)) for item in chart_data[-ma_long:]] + ma5 = sum(prices[-ma_quick:]) / ma_quick + ma20 = sum(prices[-ma_short:]) / ma_short + ma60 = sum(prices[-ma_long:]) / ma_long current_price = prices[-1] if ma5 > ma20 and current_price > ma5: diff --git a/backend/app/services/watchlist.py b/backend/app/services/watchlist.py index f3b4ce2..28c8b29 100644 --- a/backend/app/services/watchlist.py +++ b/backend/app/services/watchlist.py @@ -4,10 +4,11 @@ 아침 배치(batch_daily_analysis)와 장중 배치(batch_intraday_analysis)가 공유하는 초기화 함수. 각 배치 스크립트가 자신의 AsyncSession을 전달해 사용한다. -[승률 개선] - - scanner_tickers가 제공되면 해당 종목을 워치리스트 앞쪽에 우선 배치 - - 나머지 슬롯은 score 상위 순으로 채움 - - added_reason: "scanner_signal" | "morning_batch" 로 구분되어 UI 에서도 식별 가능 +[.env 설정] + WATCHLIST_INIT_SIZE (아침 배치 초기화 종목 수) + WATCHLIST_MAX_SIZE (최대 관리 종목 수) + WATCHLIST_ENTRY_SCORE (장중 신규 진입 기준 점수) + WATCHLIST_EXIT_SCORE (장중 제거 기준 점수) """ from datetime import datetime, date from typing import Optional @@ -17,17 +18,12 @@ import pytz from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession +from app.core.config import settings from app.models.models import WatchlistStock, StockAnalysis logger = logging.getLogger(__name__) -# 분석종목 관련 상수 -WATCHLIST_INIT_SIZE: int = 50 # 아침 배치 초기화 종목 수 -WATCHLIST_MAX_SIZE: int = 100 # 최대 관리 종목 수 -WATCHLIST_ENTRY_SCORE: int = 60 # 장중 신규 진입 기준 점수 -WATCHLIST_EXIT_SCORE: int = 20 # 장중 제거 기준 점수 - -# 스캐너 신호 종목에 배정하는 최대 슬롯 수 (WATCHLIST_INIT_SIZE 중 이 수만큼 우선) +# 스캐너 신호 종목에 배정하는 최대 슬롯 수 (settings.WATCHLIST_INIT_SIZE 중 이 수만큼 우선) SCANNER_PRIORITY_SLOTS: int = 20 @@ -68,7 +64,7 @@ async def initialize_watchlist( select(StockAnalysis.stock_code, StockAnalysis.stock_name, StockAnalysis.score) .where(StockAnalysis.analysis_date == analysis_date) .order_by(StockAnalysis.score.desc()) - .limit(WATCHLIST_INIT_SIZE) + .limit(settings.WATCHLIST_INIT_SIZE) ) top_stocks = result.all() top_codes = {row.stock_code for row in top_stocks} @@ -119,7 +115,7 @@ async def initialize_watchlist( scanner_added += 1 # --- 3순위: 나머지 슬롯 — score 상위 순 --- - remaining = WATCHLIST_INIT_SIZE - added + remaining = settings.WATCHLIST_INIT_SIZE - added for row in top_stocks: if remaining <= 0: break diff --git a/mcp-server/gemini_client.py b/mcp-server/gemini_client.py index 8c49382..349d5be 100644 --- a/mcp-server/gemini_client.py +++ b/mcp-server/gemini_client.py @@ -13,8 +13,11 @@ class GeminiClient: """Gemini AI 클라이언트 (ClaudeClient와 동일한 인터페이스)""" def __init__(self): - self.client = genai.Client(api_key=settings.GEMINI_API_KEY) - self.model_name = settings.GEMINI_MODEL + if not settings.GEMINI_API_KEY: + raise ValueError("GEMINI_API_KEY is not set") + genai.configure(api_key=settings.GEMINI_API_KEY) + + self.model = genai.GenerativeModel(settings.GEMINI_MODEL) self.generate_config = types.GenerateContentConfig( max_output_tokens=settings.MAX_TOKENS, temperature=settings.TEMPERATURE, @@ -28,10 +31,13 @@ class GeminiClient: ) -> Dict[str, Any]: """Gemini를 통한 분석 실행""" try: - response = await self.client.aio.models.generate_content( - model=self.model_name, - contents=f"{system_prompt}\n\n{user_message}", - config=self.generate_config, + model_with_system_prompt = genai.GenerativeModel( + model_name=self.model.model_name, + system_instruction=system_prompt + ) + response = await model_with_system_prompt.generate_content_async( + user_message, + generation_config=self.generate_config, ) text = response.text if response.text else ""