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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user