fix: 감사 잔여 항목 — 잔량 출처 DB화·ORB 영속화·미국 부분청산·듀얼전략 태깅·NULL 가격

- portfolio_cache_bridge: 잔량 산출을 Portfolio(DB) 기준으로. 캐시는 sync 실패 시 최대
  5분 낡아 실제와 다른 잔량이 기록될 수 있었음(감사 #14). 조회 실패 시 캐시 폴백.
- ORB 캐시 Redis 영속화(save/load_orb_cache) — 재배포 시 인메모리가 날아가 당일 ORB
  전략이 통째로 불가능했음. 수집 후 저장, 스케줄러 기동·수집 시 복원.
- us_market_scheduler: 부분청산이 Portfolio.quantity 를 갱신하지 않던 문제 → reflect_sell 연동.
- 듀얼 전략(MOMENTUM_BREAKOUT/PULLBACK_BUY)이 ActiveTrade.strategy_name 에 저장되지 않아
  CLAUDE.md §8.4 모멘텀 전용 청산(+10%/+20% 분할)이 한 번도 실행된 적 없었음 → 태깅 전달.
  매도 시에는 실제 행의 strategy_name 으로 close (swing 으로 닫으면 유령 행 잔존).
- 수동매도: current_price NULL 시 TypeError 로 주문 후 상태 갱신이 통째로 누락되던 엣지 수정.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 11:11:20 +09:00
parent 8971a648e2
commit 85cb5cbc09
10 changed files with 241 additions and 8 deletions

View File

@@ -1077,7 +1077,9 @@ async def _execute_buy(
"stock_name": stock_name,
"quantity": quantity,
"entry_price": order_price,
"strategy_name": stock.get("strategy_type", "swing"),
# 듀얼 전략(MOMENTUM_BREAKOUT/PULLBACK_BUY)은 strategy_name 으로 전달돼
# 청산 분기를 결정한다. 없으면 strategy_type 폴백.
"strategy_name": stock.get("strategy_name") or stock.get("strategy_type", "swing"),
"stop_loss": stock.get("stop_loss", -float(settings.STOP_LOSS_PERCENT)),
"take_profit": stock.get("take_profit", 5.0),
"entry_reason": entry_reason,
@@ -1320,9 +1322,26 @@ async def _execute_sell(
# [C2] ActiveTrade 종료/갱신
try:
# 실제 행의 strategy_name 을 사용한다. strategy_type("swing")으로 닫으면
# 듀얼 전략 행(MOMENTUM_BREAKOUT/PULLBACK_BUY)과 매칭되지 않아 유령 행이 남는다.
_at_name = stock.get("strategy_name")
if not _at_name:
try:
from app.models.models import ActiveTrade as _AT
_at_row = (await db.execute(
select(_AT)
.where(_AT.stock_code == stock_code, _AT.status == "active")
.order_by(_AT.id.desc())
.limit(1)
)).scalars().first()
_at_name = _at_row.strategy_name if _at_row else None
except Exception:
_at_name = None
_at_name = _at_name or stock.get("strategy_type", "swing")
if sell_pct >= 1.0:
from app.services.db_worker import enqueue_close_active_trade
await enqueue_close_active_trade(stock_code, strategy_name=stock.get("strategy_type", "swing"))
await enqueue_close_active_trade(stock_code, strategy_name=_at_name)
else:
from app.services.db_worker import enqueue_active_trade_upsert
# [BUG FIX] 기존 remaining_pct에서 sell_pct를 차감해야 누적 분할매도가 올바르게 반영됨
@@ -1331,7 +1350,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"),
"strategy_name": _at_name,
"remaining_pct": new_remaining,
"quantity": max(0, total_qty - quantity), # 잔량 갱신(과다매도 방지)
"highest_since_entry": current_price,