refactor(phase2): Add Scanner module and strategy README

This commit is contained in:
2026-03-06 10:55:19 +09:00
parent 8920699eee
commit a322745cf3
2 changed files with 407 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
"""
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