추천로직
This commit is contained in:
@@ -1,26 +1,31 @@
|
||||
"""
|
||||
KRX 히스토리컬 데이터 수집 스크립트
|
||||
|
||||
pykrx를 사용하여 KOSPI/KOSDAQ 종목의 일봉 OHLCV 데이터를 DB에 저장합니다.
|
||||
KRX OpenAPI (data-dbg.krx.co.kr)를 사용하여 KOSPI/KOSDAQ 종목의
|
||||
일봉 OHLCV 데이터를 DB에 저장합니다.
|
||||
|
||||
- KIS API 의존성 완전 제거
|
||||
- 날짜 1건 호출 → 해당일 전 종목 OHLCV 일괄 수신 (효율적)
|
||||
|
||||
사용법:
|
||||
python collect_krx_data.py # 전체 수집 (10년, 주요 200종목)
|
||||
python collect_krx_data.py --limit 5 --start-date 20240101 # 테스트 (5종목, 2024년~)
|
||||
python collect_krx_data.py --update # 증분 업데이트 (마지막 저장일 이후)
|
||||
python collect_krx_data.py --market kospi # KOSPI만
|
||||
python collect_krx_data.py # 전체 수집 (2020-01-01~)
|
||||
python collect_krx_data.py --limit 5 # 최근 5거래일 (테스트)
|
||||
python collect_krx_data.py --start-date 20240101 # 시작일 지정
|
||||
python collect_krx_data.py --update # 증분 업데이트
|
||||
python collect_krx_data.py --market kospi # KOSPI만
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import date, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import aiohttp
|
||||
import pandas as pd
|
||||
from pykrx import stock as pykrx_stock
|
||||
|
||||
# 프로젝트 루트를 Python 경로에 추가
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
@@ -31,131 +36,186 @@ from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
from app.core.database import AsyncSessionLocal, engine, Base
|
||||
from app.models.models import StockDailyPrice
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
# ─── KRX OpenAPI 설정 ─────────────────────────────────────────────
|
||||
# 실제 데이터 엔드포인트 (openapi.krx.co.kr 는 포털, 데이터는 data-dbg)
|
||||
KRX_DATA_BASE = "https://data-dbg.krx.co.kr/svc/apis"
|
||||
|
||||
# ETF/특수 종목 제외 키워드
|
||||
try:
|
||||
from app.core.config import settings as _cfg
|
||||
KRX_API_KEY: str = _cfg.KRX_API_KEY
|
||||
except Exception:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(Path(__file__).parent.parent / ".env")
|
||||
KRX_API_KEY = os.environ.get("KRX_API_KEY", "")
|
||||
|
||||
# ─── ETF/특수 종목 제외 키워드 ────────────────────────────────────
|
||||
SKIP_KEYWORDS = [
|
||||
"ETF", "레버리지", "인버스", "KODEX", "TIGER", "ARIRANG",
|
||||
"KBSTAR", "HANARO", "SOL", "ACE", "KOSEF", "KINDEX",
|
||||
"선물", "스팩", "리츠", "SPAC",
|
||||
]
|
||||
|
||||
# 날짜별 동시 요청 수 (KRX API 쓰로틀링 고려)
|
||||
COLLECT_CONCURRENCY = 3
|
||||
REQUEST_INTERVAL = 0.5 # 요청 간 최소 대기(초)
|
||||
|
||||
def get_stock_list(market: str = "all", limit: int = 0) -> list:
|
||||
"""pykrx로 종목 목록 조회
|
||||
|
||||
Args:
|
||||
market: "kospi", "kosdaq", "all"
|
||||
limit: 0이면 전체, N이면 상위 N개
|
||||
|
||||
Returns:
|
||||
[(stock_code, stock_name), ...]
|
||||
"""
|
||||
today = datetime.now().strftime("%Y%m%d")
|
||||
stocks = []
|
||||
|
||||
markets = []
|
||||
if market in ("kospi", "all"):
|
||||
markets.append("KOSPI")
|
||||
if market in ("kosdaq", "all"):
|
||||
markets.append("KOSDAQ")
|
||||
|
||||
for mkt in markets:
|
||||
try:
|
||||
tickers = pykrx_stock.get_market_ticker_list(today, market=mkt)
|
||||
for ticker in tickers:
|
||||
name = pykrx_stock.get_market_ticker_name(ticker)
|
||||
if any(k in name for k in SKIP_KEYWORDS):
|
||||
continue
|
||||
stocks.append((ticker, name))
|
||||
logger.info(f"{mkt}: {len(tickers)}개 조회, 필터링 후 {len(stocks)}개")
|
||||
except Exception as e:
|
||||
logger.error(f"{mkt} 종목 목록 조회 실패: {e}")
|
||||
|
||||
# 중복 제거
|
||||
seen = set()
|
||||
unique = []
|
||||
for code, name in stocks:
|
||||
if code not in seen:
|
||||
seen.add(code)
|
||||
unique.append((code, name))
|
||||
|
||||
if limit > 0:
|
||||
unique = unique[:limit]
|
||||
|
||||
logger.info(f"총 {len(unique)}개 종목 선정")
|
||||
return unique
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def fetch_ohlcv(stock_code: str, start_date: str, end_date: str) -> pd.DataFrame:
|
||||
"""pykrx로 일봉 OHLCV 데이터 조회
|
||||
# ─── 유틸 ─────────────────────────────────────────────────────────
|
||||
|
||||
Returns:
|
||||
DataFrame with columns: date, open, high, low, close, volume, change_rate, market_cap
|
||||
"""
|
||||
def generate_trading_dates(start: str, end: str) -> list[str]:
|
||||
"""start ~ end 사이 평일(월~금) 날짜 문자열 목록 생성 (YYYYMMDD)."""
|
||||
d = datetime.strptime(start, "%Y%m%d").date()
|
||||
e = datetime.strptime(end, "%Y%m%d").date()
|
||||
dates: list[str] = []
|
||||
while d <= e:
|
||||
if d.weekday() < 5: # 0=Mon … 4=Fri
|
||||
dates.append(d.strftime("%Y%m%d"))
|
||||
d += timedelta(days=1)
|
||||
return dates
|
||||
|
||||
|
||||
def _int(val) -> int:
|
||||
"""문자열/숫자를 int로 안전 변환 (쉼표 포함 처리)."""
|
||||
try:
|
||||
df = pykrx_stock.get_market_ohlcv_by_date(
|
||||
start_date, end_date, stock_code, adjusted=True
|
||||
)
|
||||
if df.empty:
|
||||
return pd.DataFrame()
|
||||
return int(str(val).replace(",", "").strip() or "0")
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
|
||||
# 시가총액 조회
|
||||
try:
|
||||
cap_df = pykrx_stock.get_market_cap_by_date(start_date, end_date, stock_code)
|
||||
if not cap_df.empty:
|
||||
df["시가총액"] = cap_df["시가총액"]
|
||||
except Exception:
|
||||
df["시가총액"] = None
|
||||
|
||||
df = df.reset_index()
|
||||
df.columns = [c.replace("날짜", "date") for c in df.columns]
|
||||
def _float(val) -> float:
|
||||
"""문자열/숫자를 float로 안전 변환."""
|
||||
try:
|
||||
return float(str(val).replace(",", "").strip() or "0")
|
||||
except (ValueError, TypeError):
|
||||
return 0.0
|
||||
|
||||
result = pd.DataFrame({
|
||||
"date": df.iloc[:, 0], # 날짜
|
||||
"open": df["시가"].astype(int),
|
||||
"high": df["고가"].astype(int),
|
||||
"low": df["저가"].astype(int),
|
||||
"close": df["종가"].astype(int),
|
||||
"volume": df["거래량"].astype(int),
|
||||
"change_rate": df["등락률"] / 100.0 if "등락률" in df.columns else 0,
|
||||
"market_cap": df.get("시가총액"),
|
||||
})
|
||||
|
||||
# 거래량 0인 날 제거 (거래정지일)
|
||||
result = result[result["volume"] > 0].reset_index(drop=True)
|
||||
return result
|
||||
# ─── KRX API 호출 ─────────────────────────────────────────────────
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{stock_code} OHLCV 조회 실패: {e}")
|
||||
async def _fetch_krx(
|
||||
session: aiohttp.ClientSession,
|
||||
endpoint: str,
|
||||
bas_dd: str,
|
||||
) -> list:
|
||||
"""KRX OpenAPI 단건 호출 → OutBlock_1 리스트 반환.
|
||||
|
||||
endpoint 예: "sto/stk_bydd_trd"
|
||||
"""
|
||||
url = f"{KRX_DATA_BASE}/{endpoint}"
|
||||
try:
|
||||
async with session.get(
|
||||
url,
|
||||
params={"basDd": bas_dd},
|
||||
headers={"AUTH_KEY": KRX_API_KEY},
|
||||
timeout=aiohttp.ClientTimeout(total=30),
|
||||
) as resp:
|
||||
if resp.status != 200:
|
||||
logger.warning("KRX %s %s → HTTP %s", endpoint, bas_dd, resp.status)
|
||||
return []
|
||||
data = await resp.json(content_type=None)
|
||||
return data.get("OutBlock_1") or []
|
||||
except Exception as exc:
|
||||
logger.error("KRX API 오류 [%s %s]: %s", endpoint, bas_dd, exc)
|
||||
return []
|
||||
|
||||
|
||||
async def fetch_daily_prices(
|
||||
session: aiohttp.ClientSession,
|
||||
market: str,
|
||||
bas_dd: str,
|
||||
) -> pd.DataFrame:
|
||||
"""특정 날짜의 전 종목 OHLCV 조회.
|
||||
|
||||
KRX 응답 필드:
|
||||
ISU_SRT_CD : 단축 종목코드 (e.g. "005930")
|
||||
ISU_ABBRV : 종목 약칭
|
||||
TDD_OPNPRC : 시가
|
||||
TDD_HGPRC : 고가
|
||||
TDD_LWPRC : 저가
|
||||
TDD_CLSPRC : 종가
|
||||
ACC_TRDVOL : 누적 거래량
|
||||
FLUC_RT : 등락률 (%)
|
||||
"""
|
||||
endpoint = "sto/stk_bydd_trd" if market == "kospi" else "sto/ksq_bydd_trd"
|
||||
items = await _fetch_krx(session, endpoint, bas_dd)
|
||||
if not items:
|
||||
return pd.DataFrame()
|
||||
|
||||
trade_date = datetime.strptime(bas_dd, "%Y%m%d").date()
|
||||
rows: list[dict] = []
|
||||
|
||||
for item in items:
|
||||
name = (item.get("ISU_ABBRV") or item.get("ISU_NM") or "").strip()
|
||||
if any(kw in name for kw in SKIP_KEYWORDS):
|
||||
continue
|
||||
|
||||
code = item.get("ISU_SRT_CD", "").strip()
|
||||
if not code:
|
||||
continue
|
||||
|
||||
volume = _int(item.get("ACC_TRDVOL"))
|
||||
if volume == 0:
|
||||
continue # 거래정지일 제외
|
||||
|
||||
close = _int(item.get("TDD_CLSPRC"))
|
||||
if close == 0:
|
||||
continue
|
||||
|
||||
rows.append({
|
||||
"stock_code": code,
|
||||
"stock_name": name,
|
||||
"date": trade_date,
|
||||
"open": _int(item.get("TDD_OPNPRC")),
|
||||
"high": _int(item.get("TDD_HGPRC")),
|
||||
"low": _int(item.get("TDD_LWPRC")),
|
||||
"close": close,
|
||||
"volume": volume,
|
||||
"change_rate": _float(item.get("FLUC_RT")) / 100.0,
|
||||
})
|
||||
|
||||
return pd.DataFrame(rows) if rows else pd.DataFrame()
|
||||
|
||||
|
||||
async def get_stock_codes(
|
||||
session: aiohttp.ClientSession,
|
||||
market: str,
|
||||
bas_dd: str,
|
||||
) -> set[str]:
|
||||
"""특정 날짜 기준 상장 종목 코드 집합 조회 (선택적 필터용)."""
|
||||
endpoint = "sto/stk_isu_base_info" if market == "kospi" else "sto/ksq_isu_base_info"
|
||||
items = await _fetch_krx(session, endpoint, bas_dd)
|
||||
codes: set[str] = set()
|
||||
for item in items:
|
||||
code = item.get("ISU_SRT_CD", "").strip()
|
||||
if code:
|
||||
codes.add(code)
|
||||
return codes
|
||||
|
||||
|
||||
# ─── DB 헬퍼 ──────────────────────────────────────────────────────
|
||||
|
||||
async def create_tables():
|
||||
"""DB 테이블 생성"""
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
logger.info("DB 테이블 확인/생성 완료")
|
||||
|
||||
|
||||
async def get_last_trade_date(session, stock_code: str) -> date | None:
|
||||
"""특정 종목의 DB 내 마지막 거래일 조회"""
|
||||
async def get_last_trade_date(session) -> date | None:
|
||||
"""DB 내 마지막 거래일 조회 (전 종목 공통)."""
|
||||
result = await session.execute(
|
||||
text(
|
||||
"SELECT MAX(trade_date) FROM stock_daily_prices WHERE stock_code = :code"
|
||||
),
|
||||
{"code": stock_code},
|
||||
text("SELECT MAX(trade_date) FROM stock_daily_prices")
|
||||
)
|
||||
row = result.scalar()
|
||||
return row
|
||||
return result.scalar()
|
||||
|
||||
|
||||
async def upsert_daily_prices(session, stock_code: str, stock_name: str, df: pd.DataFrame):
|
||||
"""DB에 일봉 데이터 upsert (중복 시 업데이트)"""
|
||||
async def upsert_daily_prices(session, df: pd.DataFrame) -> int:
|
||||
"""DB에 일봉 데이터 UPSERT (배치 500건)."""
|
||||
if df.empty:
|
||||
return 0
|
||||
|
||||
@@ -164,38 +224,35 @@ async def upsert_daily_prices(session, stock_code: str, stock_name: str, df: pd.
|
||||
trade_date = row["date"]
|
||||
if isinstance(trade_date, pd.Timestamp):
|
||||
trade_date = trade_date.date()
|
||||
|
||||
record = {
|
||||
"stock_code": stock_code,
|
||||
"stock_name": stock_name,
|
||||
"trade_date": trade_date,
|
||||
"open_price": int(row["open"]),
|
||||
"high_price": int(row["high"]),
|
||||
"low_price": int(row["low"]),
|
||||
records.append({
|
||||
"stock_code": row["stock_code"],
|
||||
"stock_name": row["stock_name"],
|
||||
"trade_date": trade_date,
|
||||
"open_price": int(row["open"]),
|
||||
"high_price": int(row["high"]),
|
||||
"low_price": int(row["low"]),
|
||||
"close_price": int(row["close"]),
|
||||
"volume": int(row["volume"]),
|
||||
"volume": int(row["volume"]),
|
||||
"change_rate": float(row["change_rate"]) if pd.notna(row["change_rate"]) else None,
|
||||
"market_cap": int(row["market_cap"]) if pd.notna(row.get("market_cap")) else None,
|
||||
}
|
||||
records.append(record)
|
||||
"market_cap": None,
|
||||
})
|
||||
|
||||
# PostgreSQL ON CONFLICT upsert (batch)
|
||||
batch_size = 500
|
||||
inserted = 0
|
||||
for i in range(0, len(records), batch_size):
|
||||
batch = records[i:i + batch_size]
|
||||
batch = records[i : i + batch_size]
|
||||
stmt = pg_insert(StockDailyPrice).values(batch)
|
||||
stmt = stmt.on_conflict_do_update(
|
||||
constraint="uq_stock_daily",
|
||||
set_={
|
||||
"stock_name": stmt.excluded.stock_name,
|
||||
"open_price": stmt.excluded.open_price,
|
||||
"high_price": stmt.excluded.high_price,
|
||||
"low_price": stmt.excluded.low_price,
|
||||
"stock_name": stmt.excluded.stock_name,
|
||||
"open_price": stmt.excluded.open_price,
|
||||
"high_price": stmt.excluded.high_price,
|
||||
"low_price": stmt.excluded.low_price,
|
||||
"close_price": stmt.excluded.close_price,
|
||||
"volume": stmt.excluded.volume,
|
||||
"volume": stmt.excluded.volume,
|
||||
"change_rate": stmt.excluded.change_rate,
|
||||
"market_cap": stmt.excluded.market_cap,
|
||||
"market_cap": stmt.excluded.market_cap,
|
||||
},
|
||||
)
|
||||
await session.execute(stmt)
|
||||
@@ -205,108 +262,146 @@ async def upsert_daily_prices(session, stock_code: str, stock_name: str, df: pd.
|
||||
return inserted
|
||||
|
||||
|
||||
# ─── 날짜별 수집 ──────────────────────────────────────────────────
|
||||
|
||||
async def collect_date(
|
||||
i: int,
|
||||
total: int,
|
||||
bas_dd: str,
|
||||
market: str,
|
||||
semaphore: asyncio.Semaphore,
|
||||
http_session: aiohttp.ClientSession,
|
||||
) -> tuple[bool, int]:
|
||||
"""단일 날짜 수집 (세마포어 적용)."""
|
||||
async with semaphore:
|
||||
try:
|
||||
markets = []
|
||||
if market in ("kospi", "all"):
|
||||
markets.append("kospi")
|
||||
if market in ("kosdaq", "all"):
|
||||
markets.append("kosdaq")
|
||||
|
||||
frames: list[pd.DataFrame] = []
|
||||
for mkt in markets:
|
||||
df = await fetch_daily_prices(http_session, mkt, bas_dd)
|
||||
if not df.empty:
|
||||
frames.append(df)
|
||||
await asyncio.sleep(REQUEST_INTERVAL)
|
||||
|
||||
if not frames:
|
||||
logger.info("[%d/%d] %s: 데이터 없음 (휴장일?)", i, total, bas_dd)
|
||||
return True, 0 # 휴장일은 성공으로 처리
|
||||
|
||||
combined = pd.concat(frames, ignore_index=True)
|
||||
# 같은 날짜 내 코드 중복 제거 (KOSPI/KOSDAQ 겹침 방지)
|
||||
combined = combined.drop_duplicates(subset=["stock_code", "date"])
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
count = await upsert_daily_prices(db, combined)
|
||||
|
||||
logger.info("[%d/%d] %s: %d종목 저장", i, total, bas_dd, count)
|
||||
return True, count
|
||||
|
||||
except Exception as exc:
|
||||
logger.error("[%d/%d] %s 실패: %s", i, total, bas_dd, exc)
|
||||
return False, 0
|
||||
|
||||
|
||||
# ─── 메인 수집 로직 ───────────────────────────────────────────────
|
||||
|
||||
async def collect_all(
|
||||
market: str = "all",
|
||||
limit: int = 0,
|
||||
start_date: str = "20160101",
|
||||
start_date: str = "20200101",
|
||||
update_mode: bool = False,
|
||||
):
|
||||
"""메인 수집 로직"""
|
||||
"""메인 수집 로직 (KRX OpenAPI 기반, 날짜별 전종목 일괄 수집)."""
|
||||
if not KRX_API_KEY:
|
||||
logger.error(
|
||||
"KRX_API_KEY가 설정되지 않았습니다.\n"
|
||||
" 1) https://openapi.krx.co.kr 에서 API 키 발급\n"
|
||||
" 2) .env 파일에 KRX_API_KEY=<발급받은키> 추가\n"
|
||||
" 3) 다시 실행하세요."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
await create_tables()
|
||||
|
||||
end_date = datetime.now().strftime("%Y%m%d")
|
||||
actual_start = start_date
|
||||
|
||||
# 종목 목록
|
||||
stock_list = get_stock_list(market=market, limit=limit)
|
||||
if not stock_list:
|
||||
logger.error("종목 목록이 비어있습니다")
|
||||
if update_mode:
|
||||
async with AsyncSessionLocal() as db:
|
||||
last_date = await get_last_trade_date(db)
|
||||
if last_date:
|
||||
next_day = last_date + timedelta(days=1)
|
||||
actual_start = next_day.strftime("%Y%m%d")
|
||||
if next_day >= date.today():
|
||||
logger.info("이미 최신 데이터입니다 (마지막: %s)", last_date)
|
||||
return
|
||||
logger.info("증분 업데이트: %s ~", actual_start)
|
||||
|
||||
dates = generate_trading_dates(actual_start, end_date)
|
||||
if not dates:
|
||||
logger.warning("수집할 날짜가 없습니다.")
|
||||
return
|
||||
|
||||
total = len(stock_list)
|
||||
total_inserted = 0
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
# --limit N → 최근 N거래일만 (테스트용)
|
||||
if limit > 0:
|
||||
dates = dates[-limit:]
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
for i, (stock_code, stock_name) in enumerate(stock_list):
|
||||
try:
|
||||
actual_start = start_date
|
||||
total = len(dates)
|
||||
logger.info("수집 날짜: %d일 (%s ~ %s)", total, dates[0], dates[-1])
|
||||
|
||||
if update_mode:
|
||||
last_date = await get_last_trade_date(session, stock_code)
|
||||
if last_date:
|
||||
# 마지막 저장일 다음 날부터
|
||||
next_day = last_date + timedelta(days=1)
|
||||
actual_start = next_day.strftime("%Y%m%d")
|
||||
if next_day >= date.today():
|
||||
logger.info(f"[{i+1}/{total}] {stock_code} ({stock_name}): 이미 최신")
|
||||
success_count += 1
|
||||
continue
|
||||
semaphore = asyncio.Semaphore(COLLECT_CONCURRENCY)
|
||||
conn = aiohttp.TCPConnector(limit=10)
|
||||
|
||||
logger.info(
|
||||
f"[{i+1}/{total}] {stock_code} ({stock_name}): "
|
||||
f"{actual_start} ~ {end_date} 수집 중..."
|
||||
)
|
||||
async with aiohttp.ClientSession(connector=conn) as http_session:
|
||||
results = await asyncio.gather(
|
||||
*[
|
||||
collect_date(i, total, d, market, semaphore, http_session)
|
||||
for i, d in enumerate(dates, 1)
|
||||
],
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
df = fetch_ohlcv(stock_code, actual_start, end_date)
|
||||
if df.empty:
|
||||
logger.warning(f" {stock_code}: 데이터 없음")
|
||||
fail_count += 1
|
||||
continue
|
||||
|
||||
count = await upsert_daily_prices(session, stock_code, stock_name, df)
|
||||
total_inserted += count
|
||||
success_count += 1
|
||||
logger.info(f" {stock_code}: {count}개 행 저장 (총 {len(df)}일)")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f" {stock_code} ({stock_name}) 실패: {e}")
|
||||
fail_count += 1
|
||||
# 에러 시에도 계속 진행
|
||||
try:
|
||||
await session.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# pykrx rate limit 방지
|
||||
time.sleep(0.5)
|
||||
success = sum(1 for r in results if isinstance(r, tuple) and r[0])
|
||||
fail = total - success
|
||||
total_rows = sum(r[1] for r in results if isinstance(r, tuple))
|
||||
|
||||
logger.info("=" * 60)
|
||||
logger.info(f"수집 완료!")
|
||||
logger.info(f" 성공: {success_count}/{total}")
|
||||
logger.info(f" 실패: {fail_count}/{total}")
|
||||
logger.info(f" 총 저장 행: {total_inserted:,}")
|
||||
logger.info("수집 완료!")
|
||||
logger.info(" 성공: %d/%d일", success, total)
|
||||
logger.info(" 실패: %d/%d일", fail, total)
|
||||
logger.info(" 총 저장 행: %s", f"{total_rows:,}")
|
||||
logger.info("=" * 60)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="KRX 히스토리컬 데이터 수집")
|
||||
parser = argparse.ArgumentParser(description="KRX 히스토리컬 데이터 수집 (KRX OpenAPI)")
|
||||
parser.add_argument(
|
||||
"--market",
|
||||
choices=["kospi", "kosdaq", "all"],
|
||||
default="all",
|
||||
"--market", choices=["kospi", "kosdaq", "all"], default="all",
|
||||
help="수집 시장 (기본: all)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--limit", type=int, default=0, help="종목 수 제한 (0=전체, 테스트용)"
|
||||
"--limit", type=int, default=0,
|
||||
help="최근 N거래일만 수집 (0=전체, 테스트용)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--start-date",
|
||||
default="20160101",
|
||||
help="수집 시작일 YYYYMMDD (기본: 20160101)",
|
||||
"--start-date", default="20200101",
|
||||
help="수집 시작일 YYYYMMDD (기본: 20200101)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--update",
|
||||
action="store_true",
|
||||
"--update", action="store_true",
|
||||
help="증분 업데이트 모드 (마지막 저장일 이후만 수집)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
logger.info(f"KRX 데이터 수집 시작")
|
||||
logger.info(f" 시장: {args.market}")
|
||||
logger.info(f" 종목 제한: {'전체' if args.limit == 0 else args.limit}")
|
||||
logger.info(f" 시작일: {args.start_date}")
|
||||
logger.info(f" 증분 모드: {args.update}")
|
||||
logger.info("KRX 데이터 수집 시작 (KRX OpenAPI)")
|
||||
logger.info(" 시장: %s", args.market)
|
||||
logger.info(" 날짜 제한: %s", "전체" if args.limit == 0 else f"최근 {args.limit}거래일")
|
||||
logger.info(" 시작일: %s", args.start_date)
|
||||
logger.info(" 증분 모드: %s", args.update)
|
||||
|
||||
start_time = time.time()
|
||||
asyncio.run(collect_all(
|
||||
@@ -316,7 +411,7 @@ def main():
|
||||
update_mode=args.update,
|
||||
))
|
||||
elapsed = time.time() - start_time
|
||||
logger.info(f"총 소요시간: {elapsed:.1f}초 ({elapsed/60:.1f}분)")
|
||||
logger.info("총 소요시간: %.1f초 (%.1f분)", elapsed, elapsed / 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user