improve: batch pipeline - market regime detection + DB scanner + watchlist priority
This commit is contained in:
@@ -3,8 +3,14 @@
|
||||
|
||||
아침 배치(batch_daily_analysis)와 장중 배치(batch_intraday_analysis)가 공유하는
|
||||
초기화 함수. 각 배치 스크립트가 자신의 AsyncSession을 전달해 사용한다.
|
||||
|
||||
[승률 개선]
|
||||
- scanner_tickers가 제공되면 해당 종목을 워치리스트 앞쪽에 우선 배치
|
||||
- 나머지 슬롯은 score 상위 순으로 채움
|
||||
- added_reason: "scanner_signal" | "morning_batch" 로 구분되어 UI 에서도 식별 가능
|
||||
"""
|
||||
from datetime import datetime, date
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
from sqlalchemy import select, update
|
||||
@@ -15,20 +21,38 @@ from app.models.models import WatchlistStock, StockAnalysis
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 분석종목 관련 상수
|
||||
WATCHLIST_INIT_SIZE: int = 50 # 아침 배치 초기화 종목 수
|
||||
WATCHLIST_MAX_SIZE: int = 100 # 최대 관리 종목 수
|
||||
WATCHLIST_ENTRY_SCORE: int = 60 # 장중 신규 진입 기준 점수
|
||||
WATCHLIST_EXIT_SCORE: int = 20 # 장중 제거 기준 점수
|
||||
WATCHLIST_INIT_SIZE: int = 50 # 아침 배치 초기화 종목 수
|
||||
WATCHLIST_MAX_SIZE: int = 100 # 최대 관리 종목 수
|
||||
WATCHLIST_ENTRY_SCORE: int = 60 # 장중 신규 진입 기준 점수
|
||||
WATCHLIST_EXIT_SCORE: int = 20 # 장중 제거 기준 점수
|
||||
|
||||
# 스캐너 신호 종목에 배정하는 최대 슬롯 수 (WATCHLIST_INIT_SIZE 중 이 수만큼 우선)
|
||||
SCANNER_PRIORITY_SLOTS: int = 20
|
||||
|
||||
|
||||
async def initialize_watchlist(session: AsyncSession, analysis_date: date) -> int:
|
||||
async def initialize_watchlist(
|
||||
session: AsyncSession,
|
||||
analysis_date: date,
|
||||
scanner_tickers: Optional[set[str]] = None,
|
||||
) -> int:
|
||||
"""아침 배치 완료 후 분석종목 초기화.
|
||||
|
||||
기존 활성 종목을 전부 비활성화하고,
|
||||
오늘 stock_analysis score 상위 WATCHLIST_INIT_SIZE 개로 새로 채운다.
|
||||
워치리스트 구성 우선순위:
|
||||
1순위: scanner_tickers ∩ score 상위에 동시 포함된 종목 (added_reason="scanner_signal")
|
||||
2순위: scanner_tickers에 있지만 score 랭킹 밖 종목 (added_reason="scanner_signal")
|
||||
3순위: score 상위 에서 남은 슬롯 커버 (added_reason="morning_batch")
|
||||
|
||||
scanner_tickers는 SCANNER_PRIORITY_SLOTS(20)개까지만 우선 배치하여
|
||||
스캐너 도미노 현상을 방지한다.
|
||||
|
||||
Args:
|
||||
session: AsyncSession
|
||||
analysis_date: 오늘 일자
|
||||
scanner_tickers: DB 스캐너에서 추출된 신호 종목 코드 set
|
||||
|
||||
Returns: 추가된 종목 수
|
||||
"""
|
||||
scanner_tickers = scanner_tickers or set()
|
||||
now = datetime.now()
|
||||
|
||||
# 1. 기존 활성 종목 전부 비활성화
|
||||
@@ -46,18 +70,74 @@ async def initialize_watchlist(session: AsyncSession, analysis_date: date) -> in
|
||||
.limit(WATCHLIST_INIT_SIZE)
|
||||
)
|
||||
top_stocks = result.all()
|
||||
top_codes = {row.stock_code for row in top_stocks}
|
||||
|
||||
# 3. 신규 활성 종목 등록
|
||||
added = 0
|
||||
added_codes: set[str] = set()
|
||||
scanner_slots = SCANNER_PRIORITY_SLOTS
|
||||
|
||||
# --- 1순위: score 상위 + 스캐너 동시 포함 종목 (스캐너 신호로 마크) ---
|
||||
for row in top_stocks:
|
||||
if row.stock_code not in scanner_tickers:
|
||||
continue
|
||||
if scanner_slots <= 0:
|
||||
break
|
||||
session.add(WatchlistStock(
|
||||
stock_code=row.stock_code,
|
||||
stock_name=row.stock_name,
|
||||
added_at=now,
|
||||
added_reason="morning_batch",
|
||||
score_at_add=row.score,
|
||||
is_active=True,
|
||||
stock_code=row.stock_code, stock_name=row.stock_name,
|
||||
added_at=now, added_reason="scanner_signal",
|
||||
score_at_add=row.score, is_active=True,
|
||||
))
|
||||
added_codes.add(row.stock_code)
|
||||
scanner_slots -= 1
|
||||
added += 1
|
||||
|
||||
# --- 2순위: 스캐너에만 있고 score 랭킹에는 없는 종목 (최대 scanner_slots개) ---
|
||||
extra = list(scanner_tickers - top_codes)
|
||||
if extra and scanner_slots > 0:
|
||||
extra_result = await session.execute(
|
||||
select(StockAnalysis.stock_code, StockAnalysis.stock_name, StockAnalysis.score)
|
||||
.where(
|
||||
StockAnalysis.analysis_date == analysis_date,
|
||||
StockAnalysis.stock_code.in_(extra),
|
||||
)
|
||||
.order_by(StockAnalysis.score.desc())
|
||||
.limit(scanner_slots)
|
||||
)
|
||||
for row in extra_result.all():
|
||||
if row.stock_code in added_codes:
|
||||
continue
|
||||
session.add(WatchlistStock(
|
||||
stock_code=row.stock_code, stock_name=row.stock_name,
|
||||
added_at=now, added_reason="scanner_signal",
|
||||
score_at_add=row.score, is_active=True,
|
||||
))
|
||||
added_codes.add(row.stock_code)
|
||||
added += 1
|
||||
|
||||
# --- 3순위: 나머지 슬롯 — score 상위 순 ---
|
||||
remaining = WATCHLIST_INIT_SIZE - added
|
||||
for row in top_stocks:
|
||||
if remaining <= 0:
|
||||
break
|
||||
if row.stock_code in added_codes:
|
||||
continue
|
||||
session.add(WatchlistStock(
|
||||
stock_code=row.stock_code, stock_name=row.stock_name,
|
||||
added_at=now, added_reason="morning_batch",
|
||||
score_at_add=row.score, is_active=True,
|
||||
))
|
||||
added_codes.add(row.stock_code)
|
||||
remaining -= 1
|
||||
added += 1
|
||||
|
||||
await session.commit()
|
||||
logger.info(f"📋 분석종목 초기화 완료: {len(top_stocks)}개 (기준일: {analysis_date})")
|
||||
return len(top_stocks)
|
||||
|
||||
scanner_added = added - remaining if remaining >= 0 else added
|
||||
logger.info(
|
||||
"📋 워치리스트 초기화: %d개 (scanner_signal=%d, morning_batch=%d, 기준일: %s)",
|
||||
added,
|
||||
len([c for c in added_codes]), # 로깅용 (simplified)
|
||||
remaining if remaining >= 0 else 0,
|
||||
analysis_date,
|
||||
)
|
||||
return added
|
||||
|
||||
Reference in New Issue
Block a user