fix: Remove Stock model dependency, use StockDailyPrice only

This commit is contained in:
2026-03-06 22:22:33 +09:00
parent 83cf8330b5
commit 5f84897aa6

View File

@@ -14,7 +14,7 @@ from sqlalchemy import select, and_, func
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from app.core.database import AsyncSessionLocal from app.core.database import AsyncSessionLocal
from app.models.models import Stock, StockDailyPrice from app.models.models import StockDailyPrice
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -40,36 +40,16 @@ class StockUniverse:
market: Optional[str] = None, market: Optional[str] = None,
active_only: bool = True active_only: bool = True
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
"""DB에서 전체 종목 리스트 조회 """FinanceDataReader로 전체 종목 리스트 조회 (DB 대신)
Args: Args:
market: 'KOSPI' | 'KOSDAQ' | None market: 'KOSPI' | 'KOSDAQ' | None
active_only: 활성 종목만 조회 active_only: 활성 종목만 조회 (FDR은 자동으로 활성종목만)
Returns: Returns:
[{'code': '005930', 'name': '삼성전자', 'market': 'KOSPI'}, ...] [{'code': '005930', 'name': '삼성전자', 'market': 'KOSPI'}, ...]
""" """
async with AsyncSessionLocal() as session: return await get_all_krx_stocks_from_fdr(market=market)
stmt = select(Stock)
if active_only:
stmt = stmt.where(Stock.is_active == True)
if market:
stmt = stmt.where(Stock.market == market)
result = await session.execute(stmt)
stocks = result.scalars().all()
return [
{
'code': s.stock_code,
'name': s.stock_name,
'market': s.market,
'sector': getattr(s, 'sector', None),
}
for s in stocks
]
@staticmethod @staticmethod
async def get_filtered_stocks( async def get_filtered_stocks(
@@ -88,26 +68,25 @@ class StockUniverse:
if reference_date is None: if reference_date is None:
reference_date = datetime.now().date() - timedelta(days=1) reference_date = datetime.now().date() - timedelta(days=1)
async with AsyncSessionLocal() as session: # 1. FDR로 전체 종목 조회
# 1. 기본 종목 필터 all_stocks = await get_all_krx_stocks_from_fdr(market=filter_config.market)
stmt = select(Stock).where(Stock.is_active == True)
if filter_config.market: if not all_stocks:
stmt = stmt.where(Stock.market == filter_config.market) logger.warning("FDR 조회 실패, 빈 리스트 반환")
return []
if filter_config.exclude_admin: # 관리종목 제외
# 종목명에 '관리' 포함 제외 if filter_config.exclude_admin:
stmt = stmt.where(~Stock.stock_name.like('%관리%')) all_stocks = [s for s in all_stocks if '관리' not in s['name']]
result = await session.execute(stmt) stock_codes = [s['code'] for s in all_stocks]
stocks = result.scalars().all()
stock_codes = [s.stock_code for s in stocks]
if not stock_codes: if not stock_codes:
return [] return []
# 2. 가격/거래량 필터 (최근 20일 평균) # 2. DB에서 가격/거래량 필터 (최근 20일 평균)
if any([filter_config.min_volume, filter_config.min_price, filter_config.max_price]): if any([filter_config.min_volume, filter_config.min_price, filter_config.max_price]):
async with AsyncSessionLocal() as session:
start_date = reference_date - timedelta(days=30) start_date = reference_date - timedelta(days=30)
# 종목별 평균 계산 # 종목별 평균 계산
@@ -145,12 +124,12 @@ class StockUniverse:
stock_codes = filtered_codes stock_codes = filtered_codes
# 3. Top N 제한 # 3. Top N 제한
if filter_config.top_n and len(stock_codes) > filter_config.top_n: if filter_config.top_n and len(stock_codes) > filter_config.top_n:
stock_codes = stock_codes[:filter_config.top_n] stock_codes = stock_codes[:filter_config.top_n]
logger.info(f"필터링 완료: {len(stock_codes)}개 종목") logger.info(f"필터링 완료: {len(stock_codes)}개 종목")
return stock_codes return stock_codes
@staticmethod @staticmethod
async def get_liquid_stocks( async def get_liquid_stocks(
@@ -193,63 +172,81 @@ class StockUniverse:
return await StockUniverse.get_filtered_stocks(filter_config) return await StockUniverse.get_filtered_stocks(filter_config)
@staticmethod @staticmethod
def get_preset_universes() -> Dict[str, StockFilter]: def get_preset_universes() -> Dict[str, Dict[str, Any]]:
"""사전 정의된 유니버스 프리셋 """사전 정의된 유니버스 프리셋
Returns: Returns:
{'large_cap': StockFilter(...), ...} {'large_cap': {'description': '...', 'filter': StockFilter(...)}, ...}
""" """
return { return {
'all': StockFilter( 'all': {
exclude_admin=True 'description': 'KOSPI+KOSDAQ 전체 (관리종목 제외)',
), 'filter': StockFilter(exclude_admin=True)
'kospi_all': StockFilter( },
market='KOSPI', 'kospi_all': {
exclude_admin=True 'description': 'KOSPI 전체',
), 'filter': StockFilter(market='KOSPI', exclude_admin=True)
'kosdaq_all': StockFilter( },
market='KOSDAQ', 'kosdaq_all': {
exclude_admin=True 'description': 'KOSDAQ 전체',
), 'filter': StockFilter(market='KOSDAQ', exclude_admin=True)
'large_cap': StockFilter( },
market='KOSPI', 'large_cap': {
min_market_cap=10000, # 1조 이상 'description': 'KOSPI 대형주 (거래량 상위 100개)',
min_volume=100000, 'filter': StockFilter(
exclude_admin=True, market='KOSPI',
top_n=100 min_volume=100000,
), min_price=10000,
'mid_cap': StockFilter( exclude_admin=True,
market='KOSPI', top_n=100
min_market_cap=3000, # 3천억 이상 )
max_price=100000, # 10만원 이하 },
min_volume=50000, 'mid_cap': {
exclude_admin=True, 'description': 'KOSPI 중형주 (상위 200개)',
top_n=200 'filter': StockFilter(
), market='KOSPI',
'liquid_kosdaq': StockFilter( min_price=5000,
market='KOSDAQ', max_price=100000,
min_volume=200000, # 20만주 이상 min_volume=50000,
min_price=3000, # 3천원 이상 exclude_admin=True,
exclude_admin=True, top_n=200
top_n=100 )
), },
'penny_stocks': StockFilter( 'liquid_kosdaq': {
min_price=1000, 'description': 'KOSDAQ 유동성 높은 종목 (상위 100개)',
max_price=5000, # 1천~5천원 'filter': StockFilter(
min_volume=500000, # 거래량 많은 것만 market='KOSDAQ',
exclude_admin=True, min_volume=200000,
top_n=50 min_price=3000,
), exclude_admin=True,
top_n=100
)
},
'penny_stocks': {
'description': '저가주 (1천~5천원, 거래량 많은 50개)',
'filter': StockFilter(
min_price=1000,
max_price=5000,
min_volume=500000,
exclude_admin=True,
top_n=50
)
},
} }
# ── FinanceDataReader 연동 (API 없이 전체 종목 조회) ───────────────────────────────── # ── FinanceDataReader 연동 (API 없이 전체 종목 조회) ─────────────────────────────────
async def get_all_krx_stocks_from_fdr() -> List[Dict[str, Any]]: async def get_all_krx_stocks_from_fdr(
market: Optional[str] = None
) -> List[Dict[str, Any]]:
"""FinanceDataReader로 KRX 전체 종목 조회 """FinanceDataReader로 KRX 전체 종목 조회
DB에 없어도 사용 가능! DB에 없어도 사용 가능!
Args:
market: 'KOSPI' | 'KOSDAQ' | None (전체)
Returns: Returns:
[{'code': '005930', 'name': '삼성전자', 'market': 'KOSPI'}, ...] [{'code': '005930', 'name': '삼성전자', 'market': 'KOSPI'}, ...]
""" """
@@ -259,6 +256,10 @@ async def get_all_krx_stocks_from_fdr() -> List[Dict[str, Any]]:
# KRX 전체 종목 # KRX 전체 종목
df_krx = fdr.StockListing('KRX') df_krx = fdr.StockListing('KRX')
# 마켓 필터
if market:
df_krx = df_krx[df_krx['Market'] == market]
stocks = [] stocks = []
for _, row in df_krx.iterrows(): for _, row in df_krx.iterrows():
stocks.append({ stocks.append({
@@ -269,7 +270,7 @@ async def get_all_krx_stocks_from_fdr() -> List[Dict[str, Any]]:
'listing_date': row.get('ListingDate', None), 'listing_date': row.get('ListingDate', None),
}) })
logger.info(f"FDR에서 {len(stocks)}개 종목 조회") logger.info(f"FDR에서 {len(stocks)}개 종목 조회 (market={market})")
return stocks return stocks
except ImportError: except ImportError: