fix(P2): 주문 멱등성·취소 보호·다중행·v2/수동매도 정합

- korea_investment_api: 주문 계열 엔드포인트는 내부 재시도 금지(_NON_IDEMPOTENT_ENDPOINTS).
  상위 _place_order_with_retry 와 중첩돼 타임아웃 시 동일 주문 최대 12회 전송 가능했음.
  get_todays_orders() 추가.
- trade_executor: 예외 후 재시도 전에 당일 주문내역으로 접수 여부 확인(_find_recent_order)
  → 접수됐으면 재주문하지 않고 기존 주문 반환.
- daytrading/exit_service: 주문 체결 이후 상태 반영을 _finalize_exit 로 분리하고
  asyncio.shield 로 보호 — 스케줄러 단계 타임아웃이 이 구간을 취소하면 브로커는 팔았는데
  인메모리는 그대로라 다음 사이클이 재매도.
- swing/position_manager: active 행이 2개면 MultipleResultsFound 로 해당 종목 청산이
  통째로 스킵됐음(손절 미작동) → 최신 행 사용.
- swing/exit_service: 큐 지연·DLQ 시 같은 분할매도가 반복되지 않도록 180초 인메모리 쿨다운.
- portfolio_v2: 부분매도 ActiveTrade 잔량 갱신 + 전략명 매핑(daytrading→day_trade,
  us_swing→us_mean_reversion) — 매칭 실패로 active 행이 영구 잔존했음.
- portfolio.sell_stock: safe_place_order 경유(세마포어·분산락) + DT 인메모리 잔량 반영.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 10:57:40 +09:00
parent 9cdd12b41e
commit 8971a648e2
8 changed files with 296 additions and 14 deletions

View File

@@ -78,14 +78,20 @@ async def get_swing_active_trade(db: AsyncSession, stock_code: str) -> ActiveTra
Returns:
ActiveTrade | None: 활성 매매 ORM 객체, 없으면 None
"""
# [2026-09-23] scalar_one_or_none() 은 행이 2개 이상이면 MultipleResultsFound 를 던지고,
# 호출측 except 가 이를 삼켜 해당 종목의 청산(손절 포함)이 통째로 스킵됐다.
# db_worker legacy 경로와 동일하게 가장 최근 행을 사용한다.
result = await db.execute(
select(ActiveTrade).where(
select(ActiveTrade)
.where(
ActiveTrade.stock_code == stock_code,
ActiveTrade.status == "active",
ActiveTrade.strategy_name != "day_trade",
)
.order_by(ActiveTrade.id.desc())
.limit(1)
)
return result.scalar_one_or_none()
return result.scalars().first()
async def count_swing_positions(db: AsyncSession) -> int: