Some checks failed
Deploy to Production / deploy (push) Failing after 6m46s
- 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)
106 lines
3.7 KiB
Python
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
|