Compare commits

..

2 Commits

Author SHA1 Message Date
36fcd1ef28 Merge branch 'dev' into main — 외부 유입 포지션 자동 등록 2026-09-23 10:42:42 +09:00
6207d13487 feat: 외부 유입 포지션 ActiveTrade 자동 등록 — 손절·분할익절 관리 편입
다른 인스턴스/증권사 앱에서 매수돼 동기화로 들어온 포지션은 ActiveTrade 행이 없어
손절·분할익절·트레일링 관리를 전혀 받지 못했다(2026-09-22 카카오뱅크가 -13%까지
방치되다 청산). 동기화 시 보유 중인데 active 행이 없으면 평균단가를 진입가로 등록하고
손절가를 STOP_LOSS_PERCENT 기준으로 설정한다. 데이트레이딩 태깅 종목은 제외
(DT 엔진이 당일 청산 대상으로 복원해버리므로).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 10:42:40 +09:00
2 changed files with 49 additions and 0 deletions

View File

@@ -799,6 +799,7 @@ async def sync_portfolio(db: AsyncSession = Depends(get_db), force: bool = True)
# ActiveTrade 잔량 정합 — 같은 종목에 active 행이 1개뿐일 때만 실제 보유량으로 교정.
# (분할매도·수동매도 누락으로 DB 잔량이 실제와 달라지면 다음 청산이 과다매도)
try:
from app.core.config import settings as _cfg # 모듈 상단 미임포트(함수 지역 사용)
_held = {h.stock_code: h.quantity for h in new_holdings if h.quantity > 0}
_at_rows = (await db.execute(
select(ActiveTrade).where(ActiveTrade.status == "active")
@@ -819,6 +820,41 @@ async def sync_portfolio(db: AsyncSession = Depends(get_db), force: bool = True)
f"🧹 ActiveTrade 잔량 교정: {_code} {_row.quantity} → {_actual}"
)
_row.quantity = _actual
# 외부 유입 포지션(다른 인스턴스/증권사 앱 매수 등)은 ActiveTrade 행이 없어
# 손절·분할익절·트레일링 관리를 전혀 받지 못한다(2026-09-22 카카오뱅크 -13%).
# 보유 중인데 active 행이 없으면 현재 평균단가를 진입가로 보고 생성한다.
# 데이트레이딩 태깅 종목은 제외 — 엔진이 당일 청산 대상으로 복원해버린다.
_managed = set(_by_code)
for _h in new_holdings:
if _h.quantity <= 0 or _h.stock_code in _managed:
continue
if (_h.strategy_type or "swing") == "daytrading":
continue
_avg = float(_h.average_price or 0)
if _avg <= 0:
continue
_cur = float(_h.current_price or _avg)
db.add(ActiveTrade(
user_id=getattr(_h, "user_id", None),
stock_code=_h.stock_code,
stock_name=_h.stock_name,
quantity=_h.quantity,
entry_price=Decimal(str(_avg)),
current_price=Decimal(str(_cur)),
stop_loss=Decimal(str(round(_avg * (1 - abs(_cfg.STOP_LOSS_PERCENT) / 100), 2))),
status="active",
remaining_pct=Decimal("1.0"),
highest_since_entry=Decimal(str(max(_avg, _cur))),
strategy_name=_h.strategy_type or "swing",
entry_reason="외부 유입 포지션 — 동기화 시 자동 등록(진입가=평균단가)",
currency_code=_h.currency_code or "KRW",
exchange_code=_h.exchange_code,
))
logger.info(
f"🆕 ActiveTrade 자동 등록: {_h.stock_name}({_h.stock_code}) "
f"{_h.quantity}주 @ {_avg:,.0f} — 외부 유입 포지션"
)
except Exception as _rec_err:
logger.warning(f"ActiveTrade 정합 실패(무시): {_rec_err}")

View File

@@ -154,3 +154,16 @@ def test_active_trade_patch_supports_quantity():
assert "quantity" in pf.ActiveTradeStatusUpdateRequest.model_fields
src = inspect.getsource(pf.update_active_trade_status)
assert "active_trade.quantity = request.quantity" in src
def test_sync_seeds_active_trade_for_external_positions():
"""외부 유입 보유 종목(ActiveTrade 없음)은 동기화 시 자동 등록되어야 손절 관리를 받는다."""
import inspect
from app.api.endpoints import portfolio as pf
src = inspect.getsource(pf.sync_portfolio)
assert "외부 유입 포지션 — 동기화 시 자동 등록" in src
# 데이트레이딩 태깅 종목은 제외 (엔진이 당일 청산 대상으로 복원해버림)
assert '(_h.strategy_type or "swing") == "daytrading"' in src
# 평균단가를 진입가로, 손절가는 STOP_LOSS_PERCENT 기준
assert "_cfg.STOP_LOSS_PERCENT" in src
assert "entry_price=Decimal(str(_avg))" in src