""" Stock Scanner 시장에서 특정 조건에 맞는 종목을 스캔하는 모듈 """ from typing import List, Dict, Any, Optional, Callable import logging import asyncio from app.trading import market_data_provider logger = logging.getLogger(__name__) class Scanner: """ 종목 스캔 엔진 특정 조건을 만족하는 종목을 찾는다. """ def __init__(self): self.filters: List[Callable] = [] def add_filter(self, filter_func: Callable[[Dict[str, Any]], bool]): """ 필터 추가 Args: filter_func: market_data를 받아 True/False 반환하는 함수 """ self.filters.append(filter_func) async def scan( self, stock_codes: List[str], max_concurrent: int = 5 ) -> List[Dict[str, Any]]: """ 종목 목록을 스캔하여 조건에 맞는 종목 반환 Args: stock_codes: 스캔할 종목코드 목록 max_concurrent: 동시 처리 수 Returns: 조건에 맞는 종목 데이터 목록 """ semaphore = asyncio.Semaphore(max_concurrent) async def scan_one(stock_code: str) -> Optional[Dict[str, Any]]: async with semaphore: try: # 시세 데이터 수집 price_data = await market_data_provider.get_current_price(stock_code) if not price_data or price_data.get("rt_cd") != "0": return None output = price_data.get("output", {}) market_data = { "stock_code": stock_code, "stock_name": output.get("hts_kor_isnm", ""), "current_price": float(output.get("stck_prpr", 0)), "change_rate": float(output.get("prdy_ctrt", 0)), "volume": int(output.get("acml_vol", 0)), "market_cap": int(output.get("hts_avls", 0)) * 100000000, # 억 → 원 } # 필터 적용 for filter_func in self.filters: if not filter_func(market_data): return None return market_data except Exception as e: logger.warning(f"⚠️ 스캔 실패 {stock_code}: {e}") return None tasks = [scan_one(code) for code in stock_codes] results = await asyncio.gather(*tasks) matched = [r for r in results if r is not None] logger.info(f"🔍 스캔 완료: {len(matched)}/{len(stock_codes)} 종목 매칭") return matched # 사전 정의 필터 def filter_price_range(min_price: float, max_price: float) -> Callable: """가격 범위 필터""" def _filter(data: Dict[str, Any]) -> bool: price = data.get("current_price", 0) return min_price <= price <= max_price return _filter def filter_volume_min(min_volume: int) -> Callable: """최소 거래량 필터""" def _filter(data: Dict[str, Any]) -> bool: return data.get("volume", 0) >= min_volume return _filter def filter_change_rate(min_rate: float, max_rate: float) -> Callable: """등락률 범위 필터""" def _filter(data: Dict[str, Any]) -> bool: rate = data.get("change_rate", 0) return min_rate <= rate <= max_rate return _filter def filter_market_cap_min(min_cap: int) -> Callable: """최소 시가총액 필터 (원)""" def _filter(data: Dict[str, Any]) -> bool: return data.get("market_cap", 0) >= min_cap return _filter