레디스 및 구조 수정중
This commit is contained in:
265
.env.example
265
.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
|
||||
|
||||
@@ -207,6 +207,70 @@ class Settings(BaseSettings):
|
||||
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"
|
||||
case_sensitive = True
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
|
||||
@@ -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}%",
|
||||
|
||||
@@ -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 캐시 ──────────────────────────────────────────────
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
Reference in New Issue
Block a user