"""portfolio_cache_bridge — 매매 엔진(데이트레이딩/스윙)에서 포트폴리오 화면 상태를 즉시 갱신. 역할: - 매수 직후: Portfolio 행 upsert(db_worker 큐) + 인메모리 캐시에 즉시 추가 (strategy_type 태깅) - 매도 직후: Portfolio 수량 차감/0 (db_worker 큐) + 캐시에서 즉시 제거/차감 + 최근매도 기록 (KIS 잔고 반영 지연으로 동기화가 되살리는 것 방지) DB 쓰기는 전부 db_worker 큐 경유(CLAUDE.md §3.2). 캐시는 인메모리라 실시간 계층에서 호출 가능. portfolio.py 는 지연 import (순환 import·기동 순서 회피). """ import logging from decimal import Decimal logger = logging.getLogger(__name__) def _cache(): from app.api.endpoints.portfolio import portfolio_cache return portfolio_cache async def reflect_buy( stock_code: str, stock_name: str, quantity: int, price: float, strategy_type: str = "swing", market: str = "KRX", ) -> None: """매수 체결 즉시 Portfolio(큐) + 캐시 반영.""" try: from app.services.db_worker import enqueue_portfolio_upsert from app.models.models import Portfolio cache = _cache() existing = next((h for h in cache.holdings if h.stock_code == stock_code), None) if existing is not None: old_qty = int(existing.quantity or 0) old_avg = float(existing.average_price or price) new_qty = old_qty + quantity new_avg = (old_avg * old_qty + price * quantity) / new_qty if new_qty > 0 else price existing.quantity = new_qty existing.average_price = Decimal(str(round(new_avg, 2))) existing.current_price = Decimal(str(price)) if strategy_type == "daytrading": existing.strategy_type = "daytrading" avg_for_db, qty_for_db = new_avg, new_qty else: cache.holdings.append(Portfolio( stock_code=stock_code, stock_name=stock_name, quantity=quantity, average_price=Decimal(str(price)), current_price=Decimal(str(price)), profit_loss=Decimal("0"), profit_loss_rate=Decimal("0"), market=market, strategy_type=strategy_type, currency_code="KRW" if market == "KRX" else "USD", )) avg_for_db, qty_for_db = price, quantity await enqueue_portfolio_upsert({ "stock_code": stock_code, "stock_name": stock_name, "quantity": qty_for_db, "average_price": round(avg_for_db, 2), "current_price": price, "market": market, "strategy_type": strategy_type, }) await cache.apply_buy(stock_code, stock_name, quantity, price) except Exception as e: logger.warning(f"[cache_bridge] reflect_buy 실패 ({stock_code}): {e}") async def reflect_sell( stock_code: str, quantity: int, price: float, remaining_qty: int | None = None, ) -> None: """매도 체결 즉시 Portfolio(큐) 차감 + 캐시 제거/차감 + 최근매도 기록. remaining_qty: 호출측이 아는 매도 후 잔량(권장). None이면 캐시 보유량 기준으로 계산 (캐시에 없으면 전량 매도로 간주하지 않고 DB 갱신을 건너뜀). """ try: from app.services.db_worker import enqueue_portfolio_upsert 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 remaining_qty is not None: remaining = max(0, int(remaining_qty)) elif existing is not None: remaining = max(0, int(existing.quantity or 0) - quantity) else: remaining = None # 알 수 없음 → DB 수량은 건드리지 않음 if existing is not None and remaining is not None: if remaining <= 0: cache.holdings = [h for h in cache.holdings if h.stock_code != stock_code] else: existing.quantity = remaining existing.current_price = Decimal(str(price)) if remaining is not None: await enqueue_portfolio_upsert({ "stock_code": stock_code, "quantity": remaining, # 0 → 목록에서 제외, 다음 동기화에서 삭제 "current_price": price, }) cache.note_sell(stock_code, quantity) await cache.apply_sell(stock_code, quantity, price, avg) except Exception as e: logger.warning(f"[cache_bridge] reflect_sell 실패 ({stock_code}): {e}")