머니페니 34d09d9d34
Some checks failed
Deploy to Production / deploy (push) Failing after 6m46s
feat: 김종봉식 KOSPI 종목발굴 전략 구현
- KOSPIMarketStateDetector: KOSPI MA 기반 시장 상태 판단 (bull/neutral/bear/crash)
- VolumeScreener: 거래대금 2000억+ 스크리닝 (상한가 우선, 희소성 체크, 대형주 예외)
- SectorPortfolioManager: 섹터 기반 비중 배분
- KJBScreeningSignalGenerator: 눌림목 진입, 5MA 손절, 단계적 익절
- KISTradeExecutor: KIS API 자동 매수/매도 (기본값 모의투자)
- ScreeningSignal / AutoOrder DB 모델 추가
- screening API 엔드포인트 추가
- 스케줄러 잡 3종 추가 (08:30/5분/15:35)
- Price.trading_value 컬럼 추가
- MarketIndex 테이블 추가 (KOSPI/KOSDAQ 지수 일봉)
- IndexCollector 추가 (일일 수집 잡 등록)
- intraday_exit_check 시간 필터 추가 (09:05~15:20 KST)
- 드라이런 스크립트 추가 (scripts/screening_dryrun.py)
2026-05-05 23:03:53 +09:00

106 lines
3.7 KiB
Python

"""
Market index price data collector (KOSPI, KOSDAQ).
Uses pykrx to collect index OHLCV data.
"""
import logging
from datetime import datetime, timedelta
import pandas as pd
from sqlalchemy.orm import Session
from sqlalchemy.dialects.postgresql import insert
from app.services.collectors.base import BaseCollector
from app.models.stock import MarketIndex
logger = logging.getLogger(__name__)
INDEX_LIST = [
("1001", "KOSPI"),
("2001", "KOSDAQ"),
]
class IndexCollector(BaseCollector):
"""Collects daily OHLCV data for market indices (KOSPI, KOSDAQ)."""
def __init__(self, db: Session, start_date: str = None, end_date: str = None):
super().__init__(db)
self.end_date = end_date or datetime.now().strftime("%Y%m%d")
self.start_date = start_date or (
datetime.now() - timedelta(days=7)
).strftime("%Y%m%d")
def collect(self) -> int:
from pykrx import stock as pykrx_stock
total = 0
for code, name in INDEX_LIST:
try:
df = pykrx_stock.get_index_ohlcv(self.start_date, self.end_date, code)
if df is None or df.empty:
continue
df = df.reset_index()
records = []
for _, row in df.iterrows():
date_val = row.iloc[0]
if hasattr(date_val, "date"):
date_val = date_val.date()
try:
close_val = float(row.iloc[4])
except (ValueError, TypeError):
continue
if close_val == 0:
continue
def safe_float(v):
try:
return float(v) if not pd.isna(v) else None
except (ValueError, TypeError):
return None
def safe_int(v):
try:
return int(float(v)) if not pd.isna(v) else None
except (ValueError, TypeError):
return None
records.append({
"code": code,
"date": date_val,
"name": name,
"open": safe_float(row.iloc[1]),
"high": safe_float(row.iloc[2]),
"low": safe_float(row.iloc[3]),
"close": close_val,
"volume": safe_int(row.iloc[5]) if len(row) > 5 else None,
"trading_value": safe_int(row.iloc[6]) if len(row) > 6 else None,
})
if records:
stmt = insert(MarketIndex).values(records)
stmt = stmt.on_conflict_do_update(
index_elements=["code", "date"],
set_={
"open": stmt.excluded.open,
"high": stmt.excluded.high,
"low": stmt.excluded.low,
"close": stmt.excluded.close,
"volume": stmt.excluded.volume,
"trading_value": stmt.excluded.trading_value,
},
)
self.db.execute(stmt)
self.db.commit()
total += len(records)
logger.info(f"IndexCollector: {name} {len(records)} records")
except Exception as e:
self.db.rollback()
logger.warning(f"IndexCollector failed for {code} ({name}): {e}")
return total