fix: 스윙 부분매도 중복 실행 — ActiveTrade 미존재 포지션 처리

동기화로 유입된 포지션(ActiveTrade 행 없음)에 1차 익절(50%)이 매 사이클
반복 실행돼 전량 청산되는 문제 수정 (2026-09-22 두산에너빌리티 18→9→0주).

- exit_service: 부분매도 시 ActiveTrade 없으면 필수 키 포함해 생성, Portfolio 잔량 즉시 감소
- db_worker: 필수 키 없는 ActiveTrade 신규 upsert는 재시도 대신 경고 후 스킵
- portfolio.py sync 청산 경로 동일 수정
- 회귀 테스트 추가

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-22 10:32:14 +09:00
parent c1deb8caae
commit 8ecedf65a8
4 changed files with 180 additions and 4 deletions

View File

@@ -1139,9 +1139,29 @@ async def check_and_execute_exit_trades(db: AsyncSession):
# 분할 청산 시 ActiveTrade 잔여 비율 업데이트
# TODO: db_worker 마이그레이션 필요 — 읽기+업데이트가 같은 트랜잭션이라 큐 분리 어려움
if exit_signal and exit_signal.action == "SELL_PARTIAL" and active_trade:
if exit_signal and exit_signal.action == "SELL_PARTIAL" and sell_quantity < quantity:
new_remaining = remaining_pct * (1.0 - exit_signal.sell_pct)
active_trade.remaining_pct = Decimal(str(round(new_remaining, 4)))
# 잔여 수량 즉시 반영 (다음 사이클 원래 수량 기준 재매도 방지)
holding.quantity = quantity - sell_quantity
holding.current_price = Decimal(str(latest_price))
if active_trade:
active_trade.remaining_pct = Decimal(str(round(new_remaining, 4)))
else:
# [FIX] ActiveTrade 없는 포지션(동기화 유입)은 remaining_pct를
# 저장할 곳이 없어 부분매도가 매 사이클 반복됨 → 행 생성
db.add(ActiveTrade(
stock_code=stock_code,
stock_name=stock_name,
quantity=quantity - sell_quantity,
entry_price=Decimal(str(avg_price)),
current_price=Decimal(str(latest_price)),
status="active",
remaining_pct=Decimal(str(round(new_remaining, 4))),
highest_since_entry=Decimal(str(latest_price)),
strategy_name="swing",
entry_reason="동기화 유입 포지션 (부분매도 시 자동 생성)",
currency_code="KRW" if domestic else "USD",
))
await db.commit()
else:
# 전량 매도