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:
83
backend/tests/test_p0_audit_fixes.py
Normal file
83
backend/tests/test_p0_audit_fixes.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""감사 P0 수정 검증: 인증 누락 / 레거시 엔진 병존 / 혼재 종목 재태깅."""
|
||||
import inspect
|
||||
from decimal import Decimal
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.api.endpoints import day_trading as dt
|
||||
from app.api.endpoints.portfolio import portfolio_cache
|
||||
from app.models.models import Portfolio
|
||||
from app.services import portfolio_cache_bridge as bridge
|
||||
|
||||
|
||||
def _deps(fn_name: str):
|
||||
"""해당 라우트에 걸린 의존성 이름 목록 (중첩 포함)."""
|
||||
for r in dt.router.routes:
|
||||
if getattr(r, "endpoint", None) is not None and r.endpoint.__name__ == fn_name:
|
||||
names, stack = [], list(r.dependant.dependencies)
|
||||
while stack:
|
||||
d = stack.pop()
|
||||
if d.call is not None:
|
||||
names.append(getattr(d.call, "__name__", str(d.call)))
|
||||
stack.extend(d.dependencies)
|
||||
return names
|
||||
raise AssertionError(f"route not found: {fn_name}")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fn", ["set_watchlist", "execute_day_trade",
|
||||
"close_all_positions", "manual_cycle"])
|
||||
def test_write_endpoints_require_api_key(fn):
|
||||
"""CLAUDE.md §4: 쓰기 엔드포인트는 require_api_key 필수 (인증 없이 전량청산 가능했음)."""
|
||||
assert "require_api_key" in _deps(fn), f"{fn}: 인증 누락"
|
||||
|
||||
|
||||
def test_legacy_execute_blocks_real_orders():
|
||||
src = inspect.getsource(dt.execute_day_trade)
|
||||
assert "if not req.dry_run:" in src and "410" in src
|
||||
|
||||
|
||||
def test_close_all_delegates_to_engine():
|
||||
"""레거시 인메모리로 청산하면 엔진이 같은 물량을 30초 뒤 재매도한다."""
|
||||
src = inspect.getsource(dt.close_all_positions)
|
||||
assert "force_close_all" in src and "_execute_eod_flat" not in src
|
||||
|
||||
|
||||
def test_startup_does_not_restore_legacy_positions():
|
||||
import app.main as m
|
||||
src = inspect.getsource(m)
|
||||
assert "restore_positions_from_db()" not in src, "레거시 복원이 남아 있으면 이중 매도"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dt_buy_does_not_retag_existing_swing_row():
|
||||
"""스윙 보유 종목을 DT가 추가 매수해도 행 전략 태깅을 바꾸면 안 된다.
|
||||
|
||||
바꾸면 get_swing_holdings 에서 빠져 스윙 보유분의 손절·익절이 완전히 멈춘다.
|
||||
"""
|
||||
portfolio_cache.holdings = [Portfolio(
|
||||
stock_code="064400", stock_name="LG씨엔에스", quantity=8,
|
||||
average_price=Decimal("70148"), current_price=Decimal("70800"),
|
||||
strategy_type="swing",
|
||||
)]
|
||||
portfolio_cache.summary = {"available_cash": 1_000_000, "total_value": 0, "total_investment": 0,
|
||||
"total_assets": 1_000_000, "total_profit_loss": 0,
|
||||
"total_profit_loss_rate": 0, "total_stocks": 1}
|
||||
q = AsyncMock()
|
||||
with patch("app.services.db_worker.enqueue_portfolio_upsert", q):
|
||||
await bridge.reflect_buy("064400", "LG씨엔에스", 4, 70800.0, strategy_type="daytrading")
|
||||
|
||||
assert portfolio_cache.holdings[0].strategy_type == "swing"
|
||||
assert portfolio_cache.holdings[0].quantity == 12
|
||||
assert "strategy_type" not in q.await_args.args[0], "기존 행에 전략 태깅을 덮어쓰면 안 됨"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_row_keeps_strategy_tag():
|
||||
portfolio_cache.holdings = []
|
||||
portfolio_cache.summary = None
|
||||
q = AsyncMock()
|
||||
with patch("app.services.db_worker.enqueue_portfolio_upsert", q):
|
||||
await bridge.reflect_buy("005930", "삼성전자", 3, 70000.0, strategy_type="daytrading")
|
||||
assert q.await_args.args[0]["strategy_type"] == "daytrading"
|
||||
assert portfolio_cache.holdings[0].strategy_type == "daytrading"
|
||||
Reference in New Issue
Block a user