120 lines
3.8 KiB
Python
120 lines
3.8 KiB
Python
"""
|
|
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
|