fix: Remove Stock model dependency, use StockDailyPrice only
This commit is contained in:
@@ -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:
|
||||||
# 종목명에 '관리' 포함 제외
|
all_stocks = [s for s in all_stocks if '관리' not in s['name']]
|
||||||
stmt = stmt.where(~Stock.stock_name.like('%관리%'))
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
# 종목별 평균 계산
|
# 종목별 평균 계산
|
||||||
@@ -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(
|
},
|
||||||
|
'kospi_all': {
|
||||||
|
'description': 'KOSPI 전체',
|
||||||
|
'filter': StockFilter(market='KOSPI', exclude_admin=True)
|
||||||
|
},
|
||||||
|
'kosdaq_all': {
|
||||||
|
'description': 'KOSDAQ 전체',
|
||||||
|
'filter': StockFilter(market='KOSDAQ', exclude_admin=True)
|
||||||
|
},
|
||||||
|
'large_cap': {
|
||||||
|
'description': 'KOSPI 대형주 (거래량 상위 100개)',
|
||||||
|
'filter': StockFilter(
|
||||||
market='KOSPI',
|
market='KOSPI',
|
||||||
exclude_admin=True
|
|
||||||
),
|
|
||||||
'kosdaq_all': StockFilter(
|
|
||||||
market='KOSDAQ',
|
|
||||||
exclude_admin=True
|
|
||||||
),
|
|
||||||
'large_cap': StockFilter(
|
|
||||||
market='KOSPI',
|
|
||||||
min_market_cap=10000, # 1조 이상
|
|
||||||
min_volume=100000,
|
min_volume=100000,
|
||||||
|
min_price=10000,
|
||||||
exclude_admin=True,
|
exclude_admin=True,
|
||||||
top_n=100
|
top_n=100
|
||||||
),
|
)
|
||||||
'mid_cap': StockFilter(
|
},
|
||||||
|
'mid_cap': {
|
||||||
|
'description': 'KOSPI 중형주 (상위 200개)',
|
||||||
|
'filter': StockFilter(
|
||||||
market='KOSPI',
|
market='KOSPI',
|
||||||
min_market_cap=3000, # 3천억 이상
|
min_price=5000,
|
||||||
max_price=100000, # 10만원 이하
|
max_price=100000,
|
||||||
min_volume=50000,
|
min_volume=50000,
|
||||||
exclude_admin=True,
|
exclude_admin=True,
|
||||||
top_n=200
|
top_n=200
|
||||||
),
|
)
|
||||||
'liquid_kosdaq': StockFilter(
|
},
|
||||||
|
'liquid_kosdaq': {
|
||||||
|
'description': 'KOSDAQ 유동성 높은 종목 (상위 100개)',
|
||||||
|
'filter': StockFilter(
|
||||||
market='KOSDAQ',
|
market='KOSDAQ',
|
||||||
min_volume=200000, # 20만주 이상
|
min_volume=200000,
|
||||||
min_price=3000, # 3천원 이상
|
min_price=3000,
|
||||||
exclude_admin=True,
|
exclude_admin=True,
|
||||||
top_n=100
|
top_n=100
|
||||||
),
|
)
|
||||||
'penny_stocks': StockFilter(
|
},
|
||||||
|
'penny_stocks': {
|
||||||
|
'description': '저가주 (1천~5천원, 거래량 많은 50개)',
|
||||||
|
'filter': StockFilter(
|
||||||
min_price=1000,
|
min_price=1000,
|
||||||
max_price=5000, # 1천~5천원
|
max_price=5000,
|
||||||
min_volume=500000, # 거래량 많은 것만
|
min_volume=500000,
|
||||||
exclude_admin=True,
|
exclude_admin=True,
|
||||||
top_n=50
|
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:
|
||||||
|
|||||||
Reference in New Issue
Block a user