fix: 로그 분석으로 찾은 버그 4건 (과다매도·수량 0 오염·LSTM 무력화·sync 충돌)
1) DT 분할매도가 ActiveTrade.quantity를 갱신하지 않아 재기동 복원 시 원래 수량이 되살아남 → 잔량 4주인데 7주 매도되어 같은 종목 스윙 보유분 3주까지 청산됨(2026-09-23 LG씨엔에스). 2) reflect_sell이 '전략 포지션 잔량'을 Portfolio 행 잔량으로 사용 → DT 전량청산이 스윙 보유분까지 0으로 만듦. 행 잔량은 항상 (행 수량 - 매도 수량)으로 계산. 3) LSTM 체크포인트에 numpy 배열이 들어가 weights_only=True 로드가 매번 실패 → 앙상블에서 LSTM이 조용히 빠진 채 예측 중이었음. 저장을 list로 바꾸고, 기존 파일은 MODEL_DIR 내부일 때만 완화 재시도. 4) 수동 매도(행 삭제)와 sync 동시 실행 시 StaleDataError로 sync 전체 실패 → 전량 매도는 행 삭제 대신 수량 0, sync는 충돌 시 실패 기록 없이 다음 주기 재시도. 추가로 비강제 sync는 락 점유 중이면 30초 대기 없이 캐시 반환. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
- POST /sell: 수동 매도 (청산)
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm.exc import StaleDataError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from typing import List, Optional
|
||||
@@ -477,6 +478,11 @@ async def sync_portfolio(db: AsyncSession = Depends(get_db), force: bool = True)
|
||||
if not force and not portfolio_cache.should_sync():
|
||||
logger.debug("Skip sync: cache fresh & no change flag")
|
||||
return {"message": "Skipped (cache fresh)", "cached": True}
|
||||
# 다른 sync가 진행 중이면 비강제 호출은 기다리지 않고 캐시 반환
|
||||
# (KIS 지연 시 요청이 30초씩 쌓여 화면 전체가 느려지던 문제, 2026-09-23)
|
||||
if not force and portfolio_cache.sync_lock.locked():
|
||||
return {"message": "Sync in progress (using cache)", "cached": True}
|
||||
|
||||
# 락 획득에 30초 타임아웃 적용 (이전 sync hang 시 데드락 방지)
|
||||
try:
|
||||
await asyncio.wait_for(portfolio_cache.sync_lock.acquire(), timeout=30)
|
||||
@@ -798,6 +804,13 @@ async def sync_portfolio(db: AsyncSession = Depends(get_db), force: bool = True)
|
||||
portfolio_cache.last_trade_id = await _get_latest_executed_trade_id(db)
|
||||
logger.info(f"Portfolio synced: {len(portfolio_cache.holdings)} holdings")
|
||||
return {"message": "Portfolio synchronized", "count": len(portfolio_cache.holdings), "cached": False}
|
||||
except StaleDataError as e:
|
||||
# 수동 매도(행 삭제)와 동시 실행 시 발생 — 실패로 기록하지 않고 다음 주기에 재시도
|
||||
# (2026-09-22 14:33/14:42 수동 청산 중 sync 전체가 실패하던 문제)
|
||||
await db.rollback()
|
||||
portfolio_cache.mark_changed()
|
||||
logger.warning(f"Portfolio sync 충돌(동시 매도) — 다음 주기 재시도: {e}")
|
||||
return {"message": "Sync skipped (concurrent update)", "cached": True}
|
||||
except Exception as e:
|
||||
portfolio_cache.record_failure(e)
|
||||
logger.error(f"Portfolio sync failed: {e}")
|
||||
@@ -1433,8 +1446,10 @@ async def sell_stock(
|
||||
|
||||
# ✅ Portfolio DB 즉시 반영 (매도 후 보유종목에서 즉시 제거)
|
||||
if quantity >= original_holding_qty:
|
||||
# 전량 매도 → 레코드 삭제
|
||||
await db.delete(holding)
|
||||
# 전량 매도 → 수량 0 (행 삭제는 동시 실행 중인 sync 와 StaleDataError 충돌을
|
||||
# 일으킨다. 수량 0 행은 조회에서 제외되고 다음 sync 가 KIS 기준으로 정리한다)
|
||||
holding.quantity = 0
|
||||
holding.current_price = Decimal(str(executed_price))
|
||||
else:
|
||||
# 분할 매도 → 수량 차감
|
||||
holding.quantity -= quantity
|
||||
|
||||
Reference in New Issue
Block a user