fix: 감사 잔여 항목 — 잔량 출처 DB화·ORB 영속화·미국 부분청산·듀얼전략 태깅·NULL 가격

- portfolio_cache_bridge: 잔량 산출을 Portfolio(DB) 기준으로. 캐시는 sync 실패 시 최대
  5분 낡아 실제와 다른 잔량이 기록될 수 있었음(감사 #14). 조회 실패 시 캐시 폴백.
- ORB 캐시 Redis 영속화(save/load_orb_cache) — 재배포 시 인메모리가 날아가 당일 ORB
  전략이 통째로 불가능했음. 수집 후 저장, 스케줄러 기동·수집 시 복원.
- us_market_scheduler: 부분청산이 Portfolio.quantity 를 갱신하지 않던 문제 → reflect_sell 연동.
- 듀얼 전략(MOMENTUM_BREAKOUT/PULLBACK_BUY)이 ActiveTrade.strategy_name 에 저장되지 않아
  CLAUDE.md §8.4 모멘텀 전용 청산(+10%/+20% 분할)이 한 번도 실행된 적 없었음 → 태깅 전달.
  매도 시에는 실제 행의 strategy_name 으로 close (swing 으로 닫으면 유령 행 잔존).
- 수동매도: current_price NULL 시 TypeError 로 주문 후 상태 갱신이 통째로 누락되던 엣지 수정.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 11:11:20 +09:00
parent 8971a648e2
commit 85cb5cbc09
10 changed files with 241 additions and 8 deletions

View File

@@ -19,6 +19,23 @@ def _cache():
return portfolio_cache
async def _db_quantity(stock_code: str) -> int | None:
"""Portfolio 테이블의 현재 수량 (읽기 전용). 조회 실패 시 None."""
try:
from sqlalchemy import select
from app.core.database import AsyncSessionLocal
from app.models.models import Portfolio
async with AsyncSessionLocal() as session:
row = (await session.execute(
select(Portfolio.quantity).where(Portfolio.stock_code == stock_code)
)).scalars().first()
return int(row) if row is not None else None
except Exception as e:
logger.debug(f"[cache_bridge] Portfolio 수량 조회 실패 ({stock_code}): {e}")
return None
async def reflect_buy(
stock_code: str,
stock_name: str,
@@ -93,7 +110,13 @@ async def reflect_sell(
cache = _cache()
existing = next((h for h in cache.holdings if h.stock_code == stock_code), None)
avg = float(existing.average_price or price) if existing is not None else price
if existing is not None:
# 잔량 산출 기준은 DB(Portfolio) — 캐시는 sync 실패 시 최대 5분까지 낡을 수 있어
# 캐시 수량으로 절대값을 쓰면 실제와 다른 잔량이 기록된다(2026-09-23 감사 #14).
db_qty = await _db_quantity(stock_code)
if db_qty is not None:
remaining = max(0, db_qty - quantity)
elif existing is not None:
remaining = max(0, int(existing.quantity or 0) - quantity)
elif remaining_qty is not None:
remaining = max(0, int(remaining_qty))