146 lines
5.2 KiB
Python
146 lines
5.2 KiB
Python
"""
|
|
분석종목(Watchlist) 관리 공용 서비스
|
|
|
|
아침 배치(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
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
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 중 이 수만큼 우선)
|
|
SCANNER_PRIORITY_SLOTS: int = 20
|
|
|
|
|
|
async def initialize_watchlist(
|
|
session: AsyncSession,
|
|
analysis_date: date,
|
|
scanner_tickers: Optional[set[str]] = None,
|
|
) -> int:
|
|
"""아침 배치 완료 후 분석종목 초기화.
|
|
|
|
워치리스트 구성 우선순위:
|
|
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. 기존 활성 종목 전부 비활성화
|
|
await session.execute(
|
|
update(WatchlistStock)
|
|
.where(WatchlistStock.is_active == True)
|
|
.values(is_active=False, removed_at=now, removed_reason="daily_reset")
|
|
)
|
|
|
|
# 2. 오늘 score 상위 N개 조회
|
|
result = await session.execute(
|
|
select(StockAnalysis.stock_code, StockAnalysis.stock_name, StockAnalysis.score)
|
|
.where(StockAnalysis.analysis_date == analysis_date)
|
|
.order_by(StockAnalysis.score.desc())
|
|
.limit(WATCHLIST_INIT_SIZE)
|
|
)
|
|
top_stocks = result.all()
|
|
top_codes = {row.stock_code for row in top_stocks}
|
|
|
|
added = 0
|
|
scanner_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="scanner_signal",
|
|
score_at_add=row.score, is_active=True,
|
|
))
|
|
added_codes.add(row.stock_code)
|
|
scanner_slots -= 1
|
|
added += 1
|
|
scanner_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
|
|
scanner_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(
|
|
"📋 워치리스트 초기화: %d개 (scanner_signal=%d, morning_batch=%d, 기준일: %s)",
|
|
added,
|
|
scanner_added,
|
|
added - scanner_added,
|
|
analysis_date,
|
|
)
|
|
return added
|