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:
@@ -552,6 +552,15 @@ async def db_writer_worker() -> None:
|
||||
if record is None:
|
||||
# remaining_pct_multiply는 신규 생성 시 무시
|
||||
create_data = {k: v for k, v in data.items() if k != "remaining_pct_multiply"}
|
||||
# 필수 컬럼(quantity/entry_price) 없이 신규 생성 요청이 오면
|
||||
# IntegrityError로 3회 재시도만 낭비 → 경고 후 스킵
|
||||
_missing = [k for k in ("quantity", "entry_price") if create_data.get(k) is None]
|
||||
if _missing:
|
||||
logger.warning(
|
||||
"⚠️ [Worker] ActiveTrade upsert 스킵: %s active 행 없음 + 필수 키 누락 %s (keys=%s)",
|
||||
stock_code, _missing, sorted(create_data.keys()),
|
||||
)
|
||||
continue
|
||||
record = ActiveTrade(**create_data)
|
||||
session.add(record)
|
||||
else:
|
||||
|
||||
@@ -341,11 +341,35 @@ async def check_and_execute_exit_v2(db: AsyncSession) -> list[dict]:
|
||||
else:
|
||||
# 분할 매도 후 remaining_pct 갱신 (반복 트리거 방지)
|
||||
new_remaining = max(0.0, remaining_pct - sell_pct)
|
||||
from app.services.db_worker import enqueue_active_trade_upsert
|
||||
await enqueue_active_trade_upsert({
|
||||
from app.services.db_worker import (
|
||||
enqueue_active_trade_upsert, enqueue_portfolio_upsert,
|
||||
)
|
||||
_at_data = {
|
||||
"stock_code": stock_code,
|
||||
"remaining_pct": new_remaining,
|
||||
"highest_since_entry": latest_price if latest_price > (trailing_high or 0) else trailing_high,
|
||||
}
|
||||
if active_trade is None:
|
||||
# [FIX] ActiveTrade 행이 없는 포지션(동기화 유입 등)은 upsert가
|
||||
# NOT NULL(quantity/entry_price) 위반으로 INSERT 실패 → remaining_pct
|
||||
# 미저장 → 다음 사이클에 50% 매도 재실행(2026-09-22 두산에너빌리티).
|
||||
# 신규 생성에 필요한 필수 키를 함께 전달한다.
|
||||
_at_data.update({
|
||||
"stock_name": stock_name,
|
||||
"quantity": quantity - sell_qty,
|
||||
"entry_price": avg_price,
|
||||
"current_price": latest_price,
|
||||
"strategy_name": "swing",
|
||||
"status": "active",
|
||||
"entry_reason": "동기화 유입 포지션 (부분매도 시 자동 생성)",
|
||||
})
|
||||
await enqueue_active_trade_upsert(_at_data)
|
||||
# [FIX] 잔여 수량도 즉시 반영 — 다음 사이클이 동기화 전 원래 수량
|
||||
# 기준으로 매도 수량을 계산하지 않도록 (18주→9주 재매도 방지)
|
||||
await enqueue_portfolio_upsert({
|
||||
"stock_code": stock_code,
|
||||
"quantity": quantity - sell_qty,
|
||||
"current_price": latest_price,
|
||||
})
|
||||
|
||||
# LossTracker 업데이트 — 전량매도 시에만 (분할매도 이중 카운트 방지)
|
||||
|
||||
Reference in New Issue
Block a user