Files
auto-stock/backend/app/strategy/scanner.py

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