"""DT 진입 하드닝 라운드 8 회귀 테스트 (2026-10-10). [LOW] DT 매수 TradeHistory payload 에 executed_at 이 없어(운영 10/01 이후 DT 58행 전부 NULL) sync_portfolio 의 executed_at 시간창 조회(ghost 보존·팬텀 close 유예)가 DT 체결을 보지 못하던 결함 → payload 에 executed_at(KST naive, 진입 시각)을 싣는다. 목킹 원칙(§4.1): 주문 공통 종단(safe_place_order)은 도달 시 BaseException. """ from datetime import datetime, timedelta from types import SimpleNamespace from unittest.mock import AsyncMock, patch import pytest import pytz from app.models.models import TradeHistory from app.services.daytrading import entry_service as ens from app.services.daytrading.position_manager import day_trading_state KST = pytz.timezone("Asia/Seoul") CODE = "777778" ODNO = "0000888" class MustNotBeCalled(BaseException): pass def _boom(name): async def _f(*a, **k): raise MustNotBeCalled(f"호출 금지 경로 도달: {name}") return _f @pytest.fixture(autouse=True) def _isolate(monkeypatch): import app.services.trade_executor as te import app.services.portfolio_cache_bridge as pcb saved = dict(day_trading_state.positions) day_trading_state.positions.clear() ens._pending_entries.clear() monkeypatch.setattr(te, "safe_place_order", _boom("safe_place_order")) monkeypatch.setattr(pcb, "reflect_buy", AsyncMock()) monkeypatch.setattr(ens, "enqueue_active_trade_upsert", AsyncMock()) monkeypatch.setattr(ens, "_schedule_buy_fill_reconcile", lambda *a, **k: None) with patch.object(day_trading_state, "save_daily_state", AsyncMock(return_value=True)): yield day_trading_state.positions.clear() day_trading_state.positions.update(saved) ens._pending_entries.clear() def _signal(): return SimpleNamespace(strategy="RSI_BB", stop_loss=9_700.0, target_price=10_450.0, reason="t") @pytest.mark.asyncio async def test_accepted_entry_trade_history_has_executed_at_kst_now(monkeypatch): rows = [] async def _th(p): rows.append(p) monkeypatch.setattr(ens, "enqueue_trade_history", _th) before = datetime.now(KST).replace(tzinfo=None) await ens._record_accepted_entry_impl(CODE, "테스트", _signal(), 10, 10_000.0, ODNO) after = datetime.now(KST).replace(tzinfo=None) assert len(rows) == 1 ex = rows[0]["executed_at"] assert isinstance(ex, datetime) and ex.tzinfo is None # KST naive (created_at 규약과 동일) assert before <= ex <= after # 인메모리 포지션 entry_time 과 일치 assert day_trading_state.positions[CODE]["entry_time"] == ex # db_worker INSERT 경로(TradeHistory(**data))가 받아들이는 키인지 assert TradeHistory(**rows[0]).executed_at == ex @pytest.mark.asyncio async def test_unknown_confirmed_entry_uses_order_send_time(monkeypatch): rows = [] async def _th(p): rows.append(p) monkeypatch.setattr(ens, "enqueue_trade_history", _th) since = datetime.now(KST).replace(tzinfo=None) - timedelta(minutes=3) await ens._record_accepted_entry_impl( CODE, "테스트", _signal(), 10, 10_000.0, ODNO, entry_time=since, reason_suffix=" [x]", ) assert rows[0]["executed_at"] == since