fix(P1): MDD 축소 수량 반영·ActiveTrade 전략명 필수화·스윙 매도에서 DT 보유분 제외

- mdd_manager: 강제 축소가 TradeHistory/remaining_pct 만 갱신하고 실제 수량은 어디도
  줄이지 않아 다음 청산이 축소 전 수량으로 매도했음 → _reflect_reduction()으로
  Portfolio·DT 인메모리 반영, ActiveTrade.quantity 도 잔량으로 갱신 (두 경로 모두)
- swing/exit_service: close/upsert 에 strategy_name 미전달 시 db_worker 가 최신 active
  행(=DT)을 닫거나 트레일링 고점을 덮어씀 → 항상 전략명 명시
- trade_executor._execute_sell: Portfolio 행(스윙+DT 합계)을 그대로 매도 대상으로 삼아
  DT 보유분까지 팔았음 → 엔진 보유 수량을 제외한 자기 전략 보유분만 대상, upsert 에 전략명
- portfolio.sync: DT 태깅이 해외 루프에만 적용돼 국내 DT 매수분이 swing 으로 저장되면
  스윙 엔진이 손절 → 국내에도 적용. _dt_qty 정의를 output1 블록 밖으로 올려 NameError 방지

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 10:53:13 +09:00
parent 70def8fe0b
commit 9cdd12b41e
5 changed files with 138 additions and 13 deletions

View File

@@ -1134,7 +1134,30 @@ async def _execute_sell(
"""
# [CLAUDE.md 5.2] 분할매도: sell_pct에 따라 매도 수량 결정
# [2026-09-23] Portfolio 행은 stock_code UNIQUE 라 같은 종목의 스윙+데이트레이딩
# 수량이 합산돼 있다. 행 전체를 대상으로 잡으면 다른 전략 보유분까지 매도된다
# → 데이트레이딩 엔진이 들고 있는 수량은 제외한 '이 전략의 보유분'만 대상으로 한다.
total_qty = holding.quantity
_strategy_tag = stock.get("strategy_type", "swing")
if _strategy_tag != "daytrading":
try:
from app.services.daytrading.position_manager import day_trading_state as _dts
_dt_pos = _dts.positions.get(stock_code)
_dt_qty = int((_dt_pos or {}).get("quantity", 0) or 0)
except Exception:
_dt_qty = 0
if _dt_qty > 0:
_own = max(0, total_qty - _dt_qty)
if _own <= 0:
logger.info(
f"[SELL] {stock_name}: 보유 {total_qty}주 전량이 데이트레이딩 포지션 — 스윙 청산 스킵"
)
return {"action": "SKIP", "reason": "owned_by_daytrading", "stock": stock_name}
logger.info(
f"[SELL] {stock_name}: 보유 {total_qty}주 중 DT {_dt_qty}주 제외 → {_own}주 대상"
)
total_qty = _own
quantity = max(1, int(total_qty * sell_pct)) # 최소 1주
if sell_pct >= 1.0:
quantity = total_qty # 전량매도 시 잔여분 없도록
@@ -1255,6 +1278,7 @@ async def _execute_sell(
new_remaining = max(0.0, remaining_pct - sell_pct)
await enqueue_active_trade_upsert({
"stock_code": stock_code,
"strategy_name": stock.get("strategy_type", "swing"),
"remaining_pct": new_remaining,
"quantity": max(0, total_qty - quantity), # 잔량 갱신(과다매도 방지)
"highest_since_entry": current_price,