improve: batch pipeline - market regime detection + DB scanner + watchlist priority

This commit is contained in:
2026-02-22 17:46:06 +09:00
parent 0fe933c480
commit 357504a61d
2 changed files with 304 additions and 150 deletions

View File

@@ -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