장중 급등주 포착로직 추가

This commit is contained in:
2026-02-23 00:00:22 +09:00
parent 81c16a8115
commit ae2f9cac6d

View File

@@ -42,6 +42,9 @@ logger = logging.getLogger(__name__)
# 동시 처리 종목 수 (KIS API 세마포어가 추가로 속도 제어)
BATCH_CONCURRENCY = 5
# 워치리스트 외부 후보 재분석 상한 (10분 배치 내에 처리 가능한 수준)
OUTSIDE_RESCORE_LIMIT = 30
# DB 연결
database_url = settings.DATABASE_URL
if database_url.startswith("postgresql://"):
@@ -192,6 +195,103 @@ async def analyze_update_and_manage(
return False, None
async def rescore_outside_candidates(
analysis_date: date,
current_codes: set[str],
) -> int:
"""워치리스트 외부 후보 종목의 score를 실시간 재분석으로 갱신.
아침 배치에서 score 상위권이었던 종목이 장중 거래량·뉴스 급등으로
score가 올라가도 DB에는 오전 값이 그대로 남아 있다.
이를 포착하기 위해 워치리스트 밖 상위 OUTSIDE_RESCORE_LIMIT개를
재분석하여 stock_analysis를 최신화한다.
이후 refresh_watchlist()가 갱신된 score를 보고 신규 추가를 결정한다.
Returns: 갱신 성공 종목 수
"""
# 아침 배치 기준 score 상위 N개 중 워치리스트 미포함 종목 조회
async with AsyncSessionLocal() as session:
exclude = list(current_codes) if current_codes else ["__none__"]
result = await session.execute(
select(StockAnalysis.stock_code, StockAnalysis.stock_name)
.where(
StockAnalysis.analysis_date == analysis_date,
StockAnalysis.stock_code.notin_(exclude),
)
.order_by(StockAnalysis.score.desc())
.limit(OUTSIDE_RESCORE_LIMIT)
)
candidates = result.all()
if not candidates:
logger.info("외부 후보 종목 없음 — 재분석 건너뜀")
return 0
total = len(candidates)
logger.info(f"🔍 워치리스트 외부 후보 {total}개 재분석 시작")
semaphore = asyncio.Semaphore(BATCH_CONCURRENCY)
async def rescore_one(i: int, code: str, name: str) -> bool:
async with semaphore:
try:
res = await analyze_stock_for_batch(code, name)
if not res:
return False
# 저가주 필터 (CLAUDE.md 섹션 6.4)
if res.current_price and res.current_price < 5000:
logger.debug(f"Skip {code}: price={res.current_price:.0f}원 < 5,000원")
return False
async with AsyncSessionLocal() as session:
await session.execute(
delete(StockAnalysis).where(
StockAnalysis.stock_code == code,
StockAnalysis.analysis_date == analysis_date,
)
)
session.add(StockAnalysis(
stock_code=code,
stock_name=name,
analysis_date=analysis_date,
current_price=res.current_price,
change_rate=res.change_rate,
volume=res.volume,
rsi=res.rsi,
ma5=res.ma5,
ma20=res.ma20,
ma60=res.ma60,
stochastic_k=res.stochastic_k,
stochastic_d=res.stochastic_d,
bollinger_upper=res.bollinger_upper,
bollinger_lower=res.bollinger_lower,
bollinger_middle=res.bollinger_middle,
signals=res.signals,
score=res.score,
ml_score=res.ml_score,
ml_confidence=res.ml_confidence,
ml_signal=res.ml_signal,
atr_14=res.atr_14,
adx_14=res.adx_14,
))
await session.commit()
logger.debug(f" [{i}/{total}] {code} ({name}): score={res.score}")
return True
except Exception as e:
logger.error(f"❌ 외부 후보 재분석 실패 {code}: {e}")
return False
tasks = [
rescore_one(i, code, name)
for i, (code, name) in enumerate(candidates, 1)
]
results = await asyncio.gather(*tasks, return_exceptions=True)
success = sum(1 for r in results if r is True)
logger.info(f"✅ 외부 후보 재분석 완료: {success}/{total}개 갱신")
return success
async def refresh_watchlist(analysis_date: date, current_codes: set[str]) -> int:
"""분석종목에 없는 고점수 종목을 신규 추가.
@@ -301,6 +401,9 @@ async def main():
fail = sum(1 for r in results if isinstance(r, tuple) and r[0] is False)
error = sum(1 for r in results if isinstance(r, Exception))
# 워치리스트 외부 후보 재분석 — score DB 최신화 (refresh_watchlist 전에 실행)
await rescore_outside_candidates(analysis_date, current_codes)
# 고점수 신규 종목 추가
added = await refresh_watchlist(analysis_date, current_codes)