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:
@@ -200,6 +200,9 @@ async def check_and_execute_exit_v2(db: AsyncSession) -> list[dict]:
|
||||
|
||||
# ── ActiveTrade 조회 (분할 청산 상태) ────────────────────
|
||||
active_trade = await get_swing_active_trade(db, stock_code)
|
||||
# ActiveTrade upsert/close 시 항상 전략명을 명시해야 같은 종목의 DT 행을
|
||||
# 잘못 건드리지 않는다(db_worker 는 전략명이 없으면 최신 행을 매칭).
|
||||
_at_strategy = (active_trade.strategy_name if active_trade else None) or "swing"
|
||||
remaining_pct = float(active_trade.remaining_pct) if active_trade and active_trade.remaining_pct is not None else 1.0
|
||||
trailing_high = float(active_trade.highest_since_entry) if active_trade and active_trade.highest_since_entry else None
|
||||
|
||||
@@ -209,12 +212,13 @@ async def check_and_execute_exit_v2(db: AsyncSession) -> list[dict]:
|
||||
from app.services.db_worker import enqueue_active_trade_upsert
|
||||
await enqueue_active_trade_upsert({
|
||||
"stock_code": stock_code,
|
||||
"strategy_name": _at_strategy, # DT 행 오염 방지
|
||||
"highest_since_entry": trailing_high,
|
||||
})
|
||||
|
||||
# ── 전략별 청산 분기 ────────────────────────────────────
|
||||
exit_signal = None
|
||||
_strategy = active_trade.strategy_name if active_trade else "swing"
|
||||
_strategy = _at_strategy
|
||||
|
||||
if domestic:
|
||||
try:
|
||||
@@ -341,13 +345,18 @@ async def check_and_execute_exit_v2(db: AsyncSession) -> list[dict]:
|
||||
|
||||
# 전량 청산이면 ActiveTrade 종료, 분할 매도면 remaining_pct 갱신
|
||||
if sell_qty >= quantity:
|
||||
await enqueue_close_active_trade(stock_code, strategy_name=None)
|
||||
# strategy_name 을 비우면 db_worker 가 '가장 최근 active 행'을 닫는다
|
||||
# → 같은 종목에 DT 행이 있으면 그쪽이 닫히고 스윙 행은 영구 잔존.
|
||||
await enqueue_close_active_trade(
|
||||
stock_code, strategy_name=_strategy or "swing"
|
||||
)
|
||||
else:
|
||||
# 분할 매도 후 remaining_pct 갱신 (반복 트리거 방지)
|
||||
new_remaining = max(0.0, remaining_pct - sell_pct)
|
||||
from app.services.db_worker import enqueue_active_trade_upsert
|
||||
_at_data = {
|
||||
"stock_code": stock_code,
|
||||
"strategy_name": _strategy or "swing",
|
||||
"remaining_pct": new_remaining,
|
||||
# 잔량도 함께 갱신 — 누락 시 재기동 복원이 원래 수량으로 되살아나
|
||||
# 보유량보다 많이 매도한다(2026-09-23 DT LG씨엔에스 사례와 동일 패턴)
|
||||
|
||||
Reference in New Issue
Block a user