Merge branch 'dev' into main — ActiveTrade 정합
This commit is contained in:
@@ -796,6 +796,32 @@ async def sync_portfolio(db: AsyncSession = Depends(get_db), force: bool = True)
|
||||
# 이뤄져야 하므로(Portfolio SELECT → UPDATE/DELETE → commit) 큐로 분리가 어려움.
|
||||
# sync_portfolio()는 API 엔드포인트(get_db 의존성) 경로에서만 호출되므로
|
||||
# 실시간 콜백/스케줄러 직접 호출과는 성격이 다름.
|
||||
# ActiveTrade 잔량 정합 — 같은 종목에 active 행이 1개뿐일 때만 실제 보유량으로 교정.
|
||||
# (분할매도·수동매도 누락으로 DB 잔량이 실제와 달라지면 다음 청산이 과다매도)
|
||||
try:
|
||||
_held = {h.stock_code: h.quantity for h in new_holdings if h.quantity > 0}
|
||||
_at_rows = (await db.execute(
|
||||
select(ActiveTrade).where(ActiveTrade.status == "active")
|
||||
)).scalars().all()
|
||||
_by_code: dict[str, list] = {}
|
||||
for _row in _at_rows:
|
||||
_by_code.setdefault(_row.stock_code, []).append(_row)
|
||||
for _code, _rows in _by_code.items():
|
||||
if len(_rows) != 1:
|
||||
continue # 스윙+DT 혼재 등 → 자동 교정하지 않음
|
||||
_row = _rows[0]
|
||||
_actual = _held.get(_code, 0)
|
||||
if _actual == 0:
|
||||
_row.status = "closed"
|
||||
logger.info(f"🧹 ActiveTrade 정리: {_code} 보유 없음 → closed")
|
||||
elif int(_row.quantity or 0) != _actual:
|
||||
logger.info(
|
||||
f"🧹 ActiveTrade 잔량 교정: {_code} {_row.quantity} → {_actual}"
|
||||
)
|
||||
_row.quantity = _actual
|
||||
except Exception as _rec_err:
|
||||
logger.warning(f"ActiveTrade 정합 실패(무시): {_rec_err}")
|
||||
|
||||
await db.commit()
|
||||
portfolio_cache.holdings = [h for h in new_holdings if h.quantity > 0]
|
||||
await portfolio_cache.cache_summary(balance_data, overseas_balance_data) # 국내·해외 잔고 전달
|
||||
@@ -1528,6 +1554,7 @@ class ActiveTradeStatusUpdateRequest(BaseModel):
|
||||
reason: Optional[str] = ""
|
||||
updated_at: Optional[str] = None
|
||||
remaining_pct: Optional[float] = None # 부분 매도 후 잔여 비율 (예: 0.5). 제공 시 ActiveTrade.remaining_pct 갱신
|
||||
quantity: Optional[int] = None # 실제 보유 수량으로 교정 (DB 잔량 드리프트 정정용)
|
||||
|
||||
|
||||
@router.patch("/active-trades/{stock_code}/status")
|
||||
@@ -1594,6 +1621,15 @@ async def update_active_trade_status(
|
||||
if request.remaining_pct is not None:
|
||||
active_trade.remaining_pct = Decimal(str(round(request.remaining_pct, 4)))
|
||||
|
||||
if request.quantity is not None:
|
||||
if request.quantity < 0:
|
||||
raise HTTPException(status_code=400, detail="quantity는 0 이상이어야 합니다")
|
||||
logger.info(
|
||||
f"[TradeMonitor API] {stock_code} ActiveTrade 수량 교정: "
|
||||
f"{active_trade.quantity} → {request.quantity}"
|
||||
)
|
||||
active_trade.quantity = request.quantity
|
||||
|
||||
db.add(active_trade)
|
||||
await db.commit()
|
||||
await db.refresh(active_trade)
|
||||
|
||||
@@ -136,3 +136,21 @@ def test_manual_sell_closes_active_trade():
|
||||
src = inspect.getsource(pf.sell_stock)
|
||||
assert "_enq_close_at(stock_code" in src
|
||||
assert '"quantity": original_holding_qty - quantity' in src
|
||||
|
||||
|
||||
def test_sync_reconciles_active_trade_quantity():
|
||||
"""동기화가 ActiveTrade 잔량을 실제 보유량으로 정합해야 드리프트가 누적되지 않는다."""
|
||||
import inspect
|
||||
from app.api.endpoints import portfolio as pf
|
||||
src = inspect.getsource(pf.sync_portfolio)
|
||||
assert "ActiveTrade 잔량 교정" in src
|
||||
assert "len(_rows) != 1" in src, "스윙+DT 혼재 종목은 자동 교정 대상에서 제외해야 함"
|
||||
assert "_row.status = \"closed\"" in src, "보유 없음 → closed 처리 누락"
|
||||
|
||||
|
||||
def test_active_trade_patch_supports_quantity():
|
||||
import inspect
|
||||
from app.api.endpoints import portfolio as pf
|
||||
assert "quantity" in pf.ActiveTradeStatusUpdateRequest.model_fields
|
||||
src = inspect.getsource(pf.update_active_trade_status)
|
||||
assert "active_trade.quantity = request.quantity" in src
|
||||
|
||||
Reference in New Issue
Block a user