fix(P0): 인증 누락·레거시 DT 엔진 병존·혼재 종목 재태깅·자정 후 스테일 청산 무력화
감사 CRITICAL 4건:
- /daytrading/{watchlist,execute,close-all,cycle} 에 require_api_key 누락 → 인증 없이
전량 청산 가능했음 (CLAUDE.md §4 위반)
- 레거시 day_trading._positions 가 엔진 day_trading_state 와 별도로 기동 시 복원돼
같은 포지션이 두 엔진에 존재 → /close-all 시 이중 매도. 복원 제거 + close-all/cycle 을
엔진에 위임, /execute 실매수는 410(엔진 미관리 포지션 방지)
- reflect_buy 가 기존 행을 daytrading 으로 재태깅 → 같은 종목 스윙 보유분이
get_swing_holdings 에서 빠져 손절·익절 정지. 신규 행일 때만 태깅
- close_stale_positions 가 인메모리만 순회 → 자정 reset 후 대상이 사라져 무력.
ActiveTrade(day_trade/active/전일) 조회 기반으로 변경하고 인메모리가 비어도 실행
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
# MCP risk assessment — 모듈 최상단에서 import (순환 참조 시 별도 util 모듈 분리 고려)
|
||||
from app.api.endpoints.mcp import request_mcp_risk_assessment
|
||||
from app.core.security import require_api_key
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -804,7 +805,7 @@ async def get_watchlist():
|
||||
}
|
||||
|
||||
|
||||
@router.post("/watchlist")
|
||||
@router.post("/watchlist", dependencies=[Depends(require_api_key)])
|
||||
async def set_watchlist(req: WatchlistRequest):
|
||||
"""감시 종목 설정 (최대 20종목) — 실제 엔진과 이 모듈 양쪽에 반영
|
||||
|
||||
@@ -860,9 +861,22 @@ async def get_current_signals():
|
||||
}
|
||||
|
||||
|
||||
@router.post("/execute")
|
||||
@router.post("/execute", dependencies=[Depends(require_api_key)])
|
||||
async def execute_day_trade(req: ExecuteRequest):
|
||||
"""특정 종목 데이트레이딩 신호 스캔 후 진입"""
|
||||
"""특정 종목 데이트레이딩 신호 스캔 후 진입.
|
||||
|
||||
[2026-09-23] 실매수(dry_run=False)는 비활성화. 이 경로로 진입한 포지션은 실제 엔진의
|
||||
`day_trading_state`에 등록되지 않아 손절·트레일링·EOD 청산을 전혀 받지 못한다.
|
||||
신호 확인용 dry_run 만 허용한다.
|
||||
"""
|
||||
if not req.dry_run:
|
||||
raise HTTPException(
|
||||
status_code=410,
|
||||
detail=(
|
||||
"이 엔드포인트의 실매수는 비활성화되었습니다(엔진 미관리 포지션 방지). "
|
||||
"자동 진입은 스케줄러가, 수동 매수는 /portfolio/buy 를 사용하세요."
|
||||
),
|
||||
)
|
||||
_reset_if_new_day()
|
||||
code = req.stock_code
|
||||
name = _watchlist.get(code, code)
|
||||
@@ -975,24 +989,37 @@ async def get_positions():
|
||||
}
|
||||
|
||||
|
||||
@router.post("/close-all")
|
||||
@router.post("/close-all", dependencies=[Depends(require_api_key)])
|
||||
async def close_all_positions():
|
||||
"""전체 포지션 수동 강제 청산 (EOD 대비 수동 실행)"""
|
||||
_reset_if_new_day()
|
||||
await _execute_eod_flat()
|
||||
"""전체 포지션 수동 강제 청산 — 실제 엔진(services/daytrading)에 위임.
|
||||
|
||||
[2026-09-23] 이 모듈의 레거시 인메모리 상태로 청산하면 엔진 상태가 그대로 남아
|
||||
30초 뒤 같은 물량을 다시 매도한다(이중 매도). 엔진 경로만 사용한다.
|
||||
"""
|
||||
from app.services.daytrading.exit_service import force_close_all
|
||||
_es, _st = _engine()
|
||||
_st.reset_if_new_day()
|
||||
if not _st.positions:
|
||||
return {"status": "ok", "message": "활성 포지션 없음", "exits": [], "daily_pnl": round(_st.daily_pnl, 0)}
|
||||
exits = await force_close_all(reason="수동 전량 청산 (API)")
|
||||
return {
|
||||
"status": "ok",
|
||||
"message": "전체 데이트레이딩 포지션 청산 완료",
|
||||
"daily_pnl": round(_daily_pnl, 0),
|
||||
"message": f"데이트레이딩 포지션 {len(exits)}건 청산 완료",
|
||||
"exits": exits,
|
||||
"daily_pnl": round(_st.daily_pnl, 0),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/cycle")
|
||||
@router.post("/cycle", dependencies=[Depends(require_api_key)])
|
||||
async def manual_cycle():
|
||||
"""1분 사이클 수동 실행 (테스트/디버그용)"""
|
||||
_reset_if_new_day()
|
||||
result = await run_day_trading_cycle()
|
||||
return result
|
||||
"""1 사이클 수동 실행 — 실제 엔진의 청산·진입 로직 호출 (테스트/디버그용)."""
|
||||
from app.services.daytrading.exit_service import check_and_exit_positions
|
||||
from app.services.daytrading.entry_service import scan_and_enter
|
||||
_es, _st = _engine()
|
||||
_st.reset_if_new_day()
|
||||
exits = await check_and_exit_positions()
|
||||
entries = await scan_and_enter()
|
||||
return {"exits": exits, "entries": entries, "positions": _st.position_count()}
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
|
||||
Reference in New Issue
Block a user