배치 수정
This commit is contained in:
@@ -31,7 +31,6 @@ sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import AsyncSessionLocal # 🔧 공통 DB 모듈 사용
|
||||
import aiohttp
|
||||
from sqlalchemy import select as sa_select
|
||||
from app.models.models import StockDailyPrice
|
||||
from app.services.analysis_db_service import bulk_upsert_analysis, passes_price_filter, run_db_scanner # 🔧 단일화
|
||||
@@ -120,146 +119,28 @@ async def detect_and_log_market_regime() -> str:
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
async def get_all_tradable_stocks(limit: int = 500) -> list[tuple[str, str]]:
|
||||
"""거래 가능 종목 조회 (네이버 금융 sise 사용)
|
||||
"""KIS 거래량 순위 API로 거래 가능 종목 조회 (KOSPI+KOSDAQ 각 top 40).
|
||||
|
||||
네이버 금융 전체 시세 페이지에서 KOSPI+KOSDAQ 종목 수집.
|
||||
(data.krx.co.kr IP 차단 우회 — 네이버 서버는 별도)
|
||||
KIS FHPST01710000: 거래량 순위 — 5,000원 이상 활성 종목.
|
||||
ETF / 레버리지 / 스팩 등 특수종목 자동 제외.
|
||||
|
||||
Raises:
|
||||
RuntimeError: 조회 결과가 MIN_STOCK_COUNT(50) 미만인 경우 배치 중단
|
||||
"""
|
||||
from bs4 import BeautifulSoup
|
||||
from app.services.market_analyzer import get_all_tradable_stocks as _kis_get_stocks
|
||||
|
||||
SKIP_KEYWORDS = [
|
||||
"ETF", "레버리지", "인버스", "KODEX", "TIGER", "ARIRANG",
|
||||
"KBSTAR", "HANARO", "SOL", "ACE", "KOSEF", "KINDEX", "RISE",
|
||||
"선물", "스팩", "리츠", "SPAC",
|
||||
]
|
||||
HEADERS = {"User-Agent": "Mozilla/5.0 (compatible; autostock-batch/1.0)"}
|
||||
logger.info("📡 KIS 거래량 순위 종목 조회 (KOSPI+KOSDAQ 각 top 40)")
|
||||
stocks = await _kis_get_stocks()
|
||||
|
||||
logger.info("📡 네이버 금융 종목 조회 (KOSPI+KOSDAQ)")
|
||||
|
||||
async def _fetch_market_naver(
|
||||
session: aiohttp.ClientSession, sosok: int, market_name: str
|
||||
) -> list[tuple[str, str, str]]:
|
||||
"""sosok=0: KOSPI, sosok=1: KOSDAQ"""
|
||||
base_url = "https://finance.naver.com/sise/sise_market_sum.naver"
|
||||
results: list[tuple[str, str, str]] = []
|
||||
|
||||
# 첫 페이지로 총 페이지 수 파악
|
||||
async with session.get(
|
||||
base_url, params={"sosok": sosok, "page": 1}, headers=HEADERS,
|
||||
timeout=aiohttp.ClientTimeout(total=30)
|
||||
) as resp:
|
||||
if resp.status != 200:
|
||||
logger.warning(" 네이버 %s HTTP %s", market_name, resp.status)
|
||||
return []
|
||||
html = await resp.text(encoding="euc-kr", errors="replace")
|
||||
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
# 총 페이지 수 추출
|
||||
pager = soup.select("td.pgRR a")
|
||||
if pager:
|
||||
import re
|
||||
m = re.search(r"page=(\d+)", pager[-1].get("href", ""))
|
||||
total_pages = int(m.group(1)) if m else 1
|
||||
else:
|
||||
total_pages = 1
|
||||
total_pages = min(total_pages, 30) # 최대 30페이지 (약 1,500종목)
|
||||
|
||||
# 첫 페이지 파싱
|
||||
def _parse_page(s: BeautifulSoup) -> list[tuple[str, str, str]]:
|
||||
rows = s.select("table.type_2 tr")
|
||||
items = []
|
||||
for row in rows:
|
||||
a_tag = row.select_one("a.tltle")
|
||||
if not a_tag:
|
||||
continue
|
||||
name = a_tag.get_text(strip=True)
|
||||
href = a_tag.get("href", "")
|
||||
import re as _re
|
||||
m2 = _re.search(r"code=(\d+)", href)
|
||||
if not m2:
|
||||
continue
|
||||
code = m2.group(1)
|
||||
if any(kw in name for kw in SKIP_KEYWORDS):
|
||||
continue
|
||||
items.append((code, name, market_name))
|
||||
return items
|
||||
|
||||
results.extend(_parse_page(soup))
|
||||
|
||||
# 나머지 페이지 병렬 수집
|
||||
sem = asyncio.Semaphore(5)
|
||||
|
||||
async def _fetch_page(page: int):
|
||||
async with sem:
|
||||
try:
|
||||
async with session.get(
|
||||
base_url, params={"sosok": sosok, "page": page},
|
||||
headers=HEADERS, timeout=aiohttp.ClientTimeout(total=30)
|
||||
) as r:
|
||||
if r.status != 200:
|
||||
return []
|
||||
h = await r.text(encoding="euc-kr", errors="replace")
|
||||
return _parse_page(BeautifulSoup(h, "html.parser"))
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
tasks = [_fetch_page(p) for p in range(2, total_pages + 1)]
|
||||
pages_data = await asyncio.gather(*tasks)
|
||||
for page_items in pages_data:
|
||||
results.extend(page_items)
|
||||
|
||||
# 중복 제거
|
||||
seen: set[str] = set()
|
||||
unique = []
|
||||
for item in results:
|
||||
if item[0] not in seen:
|
||||
seen.add(item[0])
|
||||
unique.append(item)
|
||||
|
||||
logger.info(" %s: %d개 선정 (총 %d페이지)", market_name, len(unique), total_pages)
|
||||
return unique
|
||||
|
||||
kospi_stocks: list[tuple[str, str, str]] = []
|
||||
kosdaq_stocks: list[tuple[str, str, str]] = []
|
||||
async with aiohttp.ClientSession() as session:
|
||||
kospi_stocks = await _fetch_market_naver(session, 0, "KOSPI")
|
||||
kosdaq_stocks = await _fetch_market_naver(session, 1, "KOSDAQ")
|
||||
|
||||
# 네이버 스크래핑 실패 시 KIS API로 폴백
|
||||
naver_total = len(kospi_stocks) + len(kosdaq_stocks)
|
||||
if naver_total < MIN_STOCK_COUNT:
|
||||
logger.warning(
|
||||
"⚠️ 네이버 금융 조회 부족(%d개) — KIS API로 폴백", naver_total
|
||||
)
|
||||
try:
|
||||
from app.services.market_analyzer import get_all_tradable_stocks as _kis_stocks
|
||||
kis_result = await _kis_stocks()
|
||||
if len(kis_result) >= MIN_STOCK_COUNT:
|
||||
logger.info("✅ KIS API 폴백 성공: %d개", len(kis_result))
|
||||
return kis_result[:limit]
|
||||
except Exception as _exc:
|
||||
logger.error("❌ KIS API 폴백도 실패: %s", _exc)
|
||||
if len(stocks) < MIN_STOCK_COUNT:
|
||||
raise RuntimeError(
|
||||
f"종목 조회 실패: 네이버({naver_total}개) + KIS API 모두 불가. "
|
||||
f"네트워크 및 API 설정을 확인하세요."
|
||||
f"종목 조회 실패: KIS API 결과 부족 ({len(stocks)}개 < {MIN_STOCK_COUNT}개). "
|
||||
f"KIS API 설정을 확인하세요."
|
||||
)
|
||||
|
||||
# KOSPI 60% / KOSDAQ 40% 비율로 합산
|
||||
kospi_n = int(limit * 0.6)
|
||||
kosdaq_n = limit - kospi_n
|
||||
all_stocks = kospi_stocks[:kospi_n] + kosdaq_stocks[:kosdaq_n]
|
||||
top_with_mkt = all_stocks[:limit]
|
||||
top_stocks = [(code, name) for code, name, _ in top_with_mkt]
|
||||
|
||||
kospi_n = sum(1 for *_, mkt in top_with_mkt if mkt == "KOSPI")
|
||||
kosdaq_n = sum(1 for *_, mkt in top_with_mkt if mkt == "KOSDAQ")
|
||||
logger.info("✅ 선정 %d개 (KOSPI %d / KOSDAQ %d)", len(top_stocks), kospi_n, kosdaq_n)
|
||||
|
||||
return top_stocks
|
||||
result = stocks[:limit]
|
||||
logger.info("✅ 선정 %d개", len(result))
|
||||
return result
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
@@ -420,7 +301,7 @@ async def batch_analyze_all_stocks(limit: int = None):
|
||||
# Step 1: 시장 레짐 판별
|
||||
regime = await detect_and_log_market_regime()
|
||||
|
||||
# Step 2: 종목 목록 조회 (KRX OpenAPI)
|
||||
# Step 2: 종목 목록 조회 (KIS 거래량 순위 API)
|
||||
all_stocks = await get_all_tradable_stocks(limit=500)
|
||||
filtered_stocks = all_stocks[:limit] if limit else all_stocks
|
||||
total = len(filtered_stocks)
|
||||
|
||||
Reference in New Issue
Block a user