test: add session-boundary risk reload e2e regressions (#376) #386
@@ -1,6 +1,6 @@
|
|||||||
<!--
|
<!--
|
||||||
Doc-ID: DOC-REQ-001
|
Doc-ID: DOC-REQ-001
|
||||||
Version: 1.0.6
|
Version: 1.0.7
|
||||||
Status: active
|
Status: active
|
||||||
Owner: strategy
|
Owner: strategy
|
||||||
Updated: 2026-03-02
|
Updated: 2026-03-02
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ Updated: 2026-03-02
|
|||||||
| REQ-ID | 요구사항 | 상태 | 비고 |
|
| REQ-ID | 요구사항 | 상태 | 비고 |
|
||||||
|--------|----------|------|------|
|
|--------|----------|------|------|
|
||||||
| REQ-V3-001 | 모든 신호/주문/로그에 session_id 포함 | ⚠️ 부분 | 큐 intent에 `session_id` 누락 (`#375`) |
|
| REQ-V3-001 | 모든 신호/주문/로그에 session_id 포함 | ⚠️ 부분 | 큐 intent에 `session_id` 누락 (`#375`) |
|
||||||
| REQ-V3-002 | 세션 전환 훅 + 리스크 파라미터 재로딩 | ⚠️ 부분 | 구현 존재, 세션 경계 E2E 회귀 보강 필요 (`#376`) |
|
| REQ-V3-002 | 세션 전환 훅 + 리스크 파라미터 재로딩 | ✅ 완료 | 세션 경계 E2E 회귀(override 적용/해제 + 재로딩 실패 폴백) 보강 (`#376`) |
|
||||||
| REQ-V3-003 | 블랙아웃 윈도우 정책 | ✅ 완료 | `src/core/blackout_manager.py` |
|
| REQ-V3-003 | 블랙아웃 윈도우 정책 | ✅ 완료 | `src/core/blackout_manager.py` |
|
||||||
| REQ-V3-004 | 블랙아웃 큐 + 복구 시 재검증 | ⚠️ 부분 | 큐 포화는 oldest-drop 정책으로 정합화 (`#371`), 재검증 강화는 `#328` 추적 |
|
| REQ-V3-004 | 블랙아웃 큐 + 복구 시 재검증 | ⚠️ 부분 | 큐 포화는 oldest-drop 정책으로 정합화 (`#371`), 재검증 강화는 `#328` 추적 |
|
||||||
| REQ-V3-005 | 저유동 세션 시장가 금지 | ✅ 완료 | `src/core/order_policy.py` |
|
| REQ-V3-005 | 저유동 세션 시장가 금지 | ✅ 완료 | `src/core/order_policy.py` |
|
||||||
@@ -80,13 +80,13 @@ Updated: 2026-03-02
|
|||||||
- **해소**: #326 머지 — `log_trade()` 호출 시 런타임 `session_id` 명시적 전달
|
- **해소**: #326 머지 — `log_trade()` 호출 시 런타임 `session_id` 명시적 전달
|
||||||
- **요구사항**: REQ-V3-001
|
- **요구사항**: REQ-V3-001
|
||||||
|
|
||||||
### GAP-3: 세션 전환 시 리스크 파라미터 재로딩 없음 → ⚠️ 부분 해소 (#327)
|
### GAP-3: 세션 전환 시 리스크 파라미터 재로딩 없음 → ✅ 해소 (#327, #376)
|
||||||
|
|
||||||
- **위치**: `src/main.py`, `src/config.py`
|
- **위치**: `src/main.py`, `src/config.py`
|
||||||
- **해소 내용**: #327 머지 — `SESSION_RISK_PROFILES_JSON` 기반 세션별 파라미터 재로딩 메커니즘 구현
|
- **해소 내용**: #327 머지 — `SESSION_RISK_PROFILES_JSON` 기반 세션별 파라미터 재로딩 메커니즘 구현
|
||||||
- `SESSION_RISK_RELOAD_ENABLED=true` 시 세션 경계에서 파라미터 재로딩
|
- `SESSION_RISK_RELOAD_ENABLED=true` 시 세션 경계에서 파라미터 재로딩
|
||||||
- 재로딩 실패 시 기존 파라미터 유지 (안전 폴백)
|
- 재로딩 실패 시 기존 파라미터 유지 (안전 폴백)
|
||||||
- **잔여 갭**: 세션 경계 실시간 전환 E2E 통합 테스트 보강 필요 (`test_main.py`에 설정 오버라이드/폴백 단위 테스트는 존재)
|
- **해소**: 세션 경계 E2E 회귀 테스트를 추가해 override 적용/해제, 재로딩 실패 시 폴백 유지를 검증함 (`#376`)
|
||||||
- **요구사항**: REQ-V3-002
|
- **요구사항**: REQ-V3-002
|
||||||
|
|
||||||
### GAP-4: 블랙아웃 복구 DB 기록 + 재검증 → ⚠️ 부분 해소 (#324, #328, #371)
|
### GAP-4: 블랙아웃 복구 DB 기록 + 재검증 → ⚠️ 부분 해소 (#324, #328, #371)
|
||||||
@@ -394,8 +394,7 @@ Phase 3 (중기): v3 세션 최적화
|
|||||||
|
|
||||||
### 테스트 미존재 (잔여)
|
### 테스트 미존재 (잔여)
|
||||||
|
|
||||||
- ❌ 세션 전환 훅 콜백 (GAP-3 잔여)
|
- ✅ 세션 전환 훅 콜백/세션 경계 리스크 재로딩 E2E 회귀 (`#376`)
|
||||||
- ❌ 세션 경계 리스크 파라미터 재로딩 단위 테스트 (GAP-3 잔여)
|
|
||||||
- ❌ 실거래 경로 ↔ v2 상태기계 통합 테스트 (피처 공급 포함)
|
- ❌ 실거래 경로 ↔ v2 상태기계 통합 테스트 (피처 공급 포함)
|
||||||
- ❌ FX PnL 운영 활성화 검증 (GAP-6)
|
- ❌ FX PnL 운영 활성화 검증 (GAP-6)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Tests for main trading loop integration."""
|
"""Tests for main trading loop integration."""
|
||||||
|
|
||||||
from datetime import UTC, date, datetime
|
from datetime import UTC, date, datetime
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import ANY, AsyncMock, MagicMock, patch
|
from unittest.mock import ANY, AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -6378,6 +6379,225 @@ async def test_us_min_price_filter_not_applied_to_kr_market() -> None:
|
|||||||
broker.send_order.assert_called_once()
|
broker.send_order.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_session_boundary_reloads_us_min_price_override_in_trading_cycle() -> None:
|
||||||
|
db_conn = init_db(":memory:")
|
||||||
|
decision_logger = DecisionLogger(db_conn)
|
||||||
|
|
||||||
|
broker = MagicMock()
|
||||||
|
broker.get_balance = AsyncMock(return_value={"output1": [], "output2": [{}]})
|
||||||
|
|
||||||
|
overseas_broker = MagicMock()
|
||||||
|
overseas_broker.get_overseas_price = AsyncMock(
|
||||||
|
return_value={"output": {"last": "7.0", "rate": "0.0"}}
|
||||||
|
)
|
||||||
|
overseas_broker.get_overseas_balance = AsyncMock(
|
||||||
|
return_value={
|
||||||
|
"output1": [],
|
||||||
|
"output2": [{"frcr_evlu_tota": "10000", "frcr_buy_amt_smtl": "0"}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
overseas_broker.get_overseas_buying_power = AsyncMock(
|
||||||
|
return_value={"output": {"ovrs_ord_psbl_amt": "10000"}}
|
||||||
|
)
|
||||||
|
overseas_broker.send_overseas_order = AsyncMock(return_value={"rt_cd": "0", "msg1": "OK"})
|
||||||
|
|
||||||
|
market = MagicMock()
|
||||||
|
market.name = "NASDAQ"
|
||||||
|
market.code = "US_NASDAQ"
|
||||||
|
market.exchange_code = "NASD"
|
||||||
|
market.is_domestic = False
|
||||||
|
|
||||||
|
telegram = MagicMock()
|
||||||
|
telegram.notify_trade_execution = AsyncMock()
|
||||||
|
telegram.notify_fat_finger = AsyncMock()
|
||||||
|
telegram.notify_circuit_breaker = AsyncMock()
|
||||||
|
telegram.notify_scenario_matched = AsyncMock()
|
||||||
|
|
||||||
|
settings = Settings(
|
||||||
|
KIS_APP_KEY="k",
|
||||||
|
KIS_APP_SECRET="s",
|
||||||
|
KIS_ACCOUNT_NO="12345678-01",
|
||||||
|
GEMINI_API_KEY="g",
|
||||||
|
MODE="paper",
|
||||||
|
PAPER_OVERSEAS_CASH=50000.0,
|
||||||
|
US_MIN_PRICE=5.0,
|
||||||
|
USD_BUFFER_MIN=1000.0,
|
||||||
|
SESSION_RISK_RELOAD_ENABLED=True,
|
||||||
|
SESSION_RISK_PROFILES_JSON=(
|
||||||
|
'{"US_PRE": {"US_MIN_PRICE": 8.0}, "US_DAY": {"US_MIN_PRICE": 5.0}}'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
current_session = {"id": "US_PRE"}
|
||||||
|
|
||||||
|
def _session_info(_: Any) -> MagicMock:
|
||||||
|
return MagicMock(session_id=current_session["id"])
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("src.main.get_open_position", return_value=None),
|
||||||
|
patch("src.main.get_session_info", side_effect=_session_info),
|
||||||
|
):
|
||||||
|
await trading_cycle(
|
||||||
|
broker=broker,
|
||||||
|
overseas_broker=overseas_broker,
|
||||||
|
scenario_engine=MagicMock(evaluate=MagicMock(return_value=_make_buy_match("AAPL"))),
|
||||||
|
playbook=_make_playbook("US_NASDAQ"),
|
||||||
|
risk=MagicMock(validate_order=MagicMock(), check_circuit_breaker=MagicMock()),
|
||||||
|
db_conn=db_conn,
|
||||||
|
decision_logger=decision_logger,
|
||||||
|
context_store=MagicMock(
|
||||||
|
get_latest_timeframe=MagicMock(return_value=None),
|
||||||
|
set_context=MagicMock(),
|
||||||
|
),
|
||||||
|
criticality_assessor=MagicMock(
|
||||||
|
assess_market_conditions=MagicMock(return_value=MagicMock(value="NORMAL")),
|
||||||
|
get_timeout=MagicMock(return_value=5.0),
|
||||||
|
),
|
||||||
|
telegram=telegram,
|
||||||
|
market=market,
|
||||||
|
stock_code="AAPL",
|
||||||
|
scan_candidates={},
|
||||||
|
settings=settings,
|
||||||
|
)
|
||||||
|
assert overseas_broker.send_overseas_order.call_count == 0
|
||||||
|
|
||||||
|
current_session["id"] = "US_DAY"
|
||||||
|
await trading_cycle(
|
||||||
|
broker=broker,
|
||||||
|
overseas_broker=overseas_broker,
|
||||||
|
scenario_engine=MagicMock(evaluate=MagicMock(return_value=_make_buy_match("AAPL"))),
|
||||||
|
playbook=_make_playbook("US_NASDAQ"),
|
||||||
|
risk=MagicMock(validate_order=MagicMock(), check_circuit_breaker=MagicMock()),
|
||||||
|
db_conn=db_conn,
|
||||||
|
decision_logger=decision_logger,
|
||||||
|
context_store=MagicMock(
|
||||||
|
get_latest_timeframe=MagicMock(return_value=None),
|
||||||
|
set_context=MagicMock(),
|
||||||
|
),
|
||||||
|
criticality_assessor=MagicMock(
|
||||||
|
assess_market_conditions=MagicMock(return_value=MagicMock(value="NORMAL")),
|
||||||
|
get_timeout=MagicMock(return_value=5.0),
|
||||||
|
),
|
||||||
|
telegram=telegram,
|
||||||
|
market=market,
|
||||||
|
stock_code="AAPL",
|
||||||
|
scan_candidates={},
|
||||||
|
settings=settings,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert overseas_broker.send_overseas_order.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_session_boundary_falls_back_when_profile_reload_fails() -> None:
|
||||||
|
db_conn = init_db(":memory:")
|
||||||
|
decision_logger = DecisionLogger(db_conn)
|
||||||
|
|
||||||
|
broker = MagicMock()
|
||||||
|
broker.get_balance = AsyncMock(return_value={"output1": [], "output2": [{}]})
|
||||||
|
|
||||||
|
overseas_broker = MagicMock()
|
||||||
|
overseas_broker.get_overseas_price = AsyncMock(
|
||||||
|
return_value={"output": {"last": "7.0", "rate": "0.0"}}
|
||||||
|
)
|
||||||
|
overseas_broker.get_overseas_balance = AsyncMock(
|
||||||
|
return_value={
|
||||||
|
"output1": [],
|
||||||
|
"output2": [{"frcr_evlu_tota": "10000", "frcr_buy_amt_smtl": "0"}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
overseas_broker.get_overseas_buying_power = AsyncMock(
|
||||||
|
return_value={"output": {"ovrs_ord_psbl_amt": "10000"}}
|
||||||
|
)
|
||||||
|
overseas_broker.send_overseas_order = AsyncMock(return_value={"rt_cd": "0", "msg1": "OK"})
|
||||||
|
|
||||||
|
market = MagicMock()
|
||||||
|
market.name = "NASDAQ"
|
||||||
|
market.code = "US_NASDAQ"
|
||||||
|
market.exchange_code = "NASD"
|
||||||
|
market.is_domestic = False
|
||||||
|
|
||||||
|
telegram = MagicMock()
|
||||||
|
telegram.notify_trade_execution = AsyncMock()
|
||||||
|
telegram.notify_fat_finger = AsyncMock()
|
||||||
|
telegram.notify_circuit_breaker = AsyncMock()
|
||||||
|
telegram.notify_scenario_matched = AsyncMock()
|
||||||
|
|
||||||
|
settings = Settings(
|
||||||
|
KIS_APP_KEY="k",
|
||||||
|
KIS_APP_SECRET="s",
|
||||||
|
KIS_ACCOUNT_NO="12345678-01",
|
||||||
|
GEMINI_API_KEY="g",
|
||||||
|
MODE="paper",
|
||||||
|
PAPER_OVERSEAS_CASH=50000.0,
|
||||||
|
US_MIN_PRICE=5.0,
|
||||||
|
USD_BUFFER_MIN=1000.0,
|
||||||
|
SESSION_RISK_RELOAD_ENABLED=True,
|
||||||
|
SESSION_RISK_PROFILES_JSON='{"US_PRE": {"US_MIN_PRICE": 8.0}}',
|
||||||
|
)
|
||||||
|
|
||||||
|
current_session = {"id": "US_PRE"}
|
||||||
|
|
||||||
|
def _session_info(_: Any) -> MagicMock:
|
||||||
|
return MagicMock(session_id=current_session["id"])
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("src.main.get_open_position", return_value=None),
|
||||||
|
patch("src.main.get_session_info", side_effect=_session_info),
|
||||||
|
):
|
||||||
|
await trading_cycle(
|
||||||
|
broker=broker,
|
||||||
|
overseas_broker=overseas_broker,
|
||||||
|
scenario_engine=MagicMock(evaluate=MagicMock(return_value=_make_buy_match("AAPL"))),
|
||||||
|
playbook=_make_playbook("US_NASDAQ"),
|
||||||
|
risk=MagicMock(validate_order=MagicMock(), check_circuit_breaker=MagicMock()),
|
||||||
|
db_conn=db_conn,
|
||||||
|
decision_logger=decision_logger,
|
||||||
|
context_store=MagicMock(
|
||||||
|
get_latest_timeframe=MagicMock(return_value=None),
|
||||||
|
set_context=MagicMock(),
|
||||||
|
),
|
||||||
|
criticality_assessor=MagicMock(
|
||||||
|
assess_market_conditions=MagicMock(return_value=MagicMock(value="NORMAL")),
|
||||||
|
get_timeout=MagicMock(return_value=5.0),
|
||||||
|
),
|
||||||
|
telegram=telegram,
|
||||||
|
market=market,
|
||||||
|
stock_code="AAPL",
|
||||||
|
scan_candidates={},
|
||||||
|
settings=settings,
|
||||||
|
)
|
||||||
|
assert overseas_broker.send_overseas_order.call_count == 0
|
||||||
|
|
||||||
|
settings.SESSION_RISK_PROFILES_JSON = "{invalid-json"
|
||||||
|
current_session["id"] = "US_DAY"
|
||||||
|
await trading_cycle(
|
||||||
|
broker=broker,
|
||||||
|
overseas_broker=overseas_broker,
|
||||||
|
scenario_engine=MagicMock(evaluate=MagicMock(return_value=_make_buy_match("AAPL"))),
|
||||||
|
playbook=_make_playbook("US_NASDAQ"),
|
||||||
|
risk=MagicMock(validate_order=MagicMock(), check_circuit_breaker=MagicMock()),
|
||||||
|
db_conn=db_conn,
|
||||||
|
decision_logger=decision_logger,
|
||||||
|
context_store=MagicMock(
|
||||||
|
get_latest_timeframe=MagicMock(return_value=None),
|
||||||
|
set_context=MagicMock(),
|
||||||
|
),
|
||||||
|
criticality_assessor=MagicMock(
|
||||||
|
assess_market_conditions=MagicMock(return_value=MagicMock(value="NORMAL")),
|
||||||
|
get_timeout=MagicMock(return_value=5.0),
|
||||||
|
),
|
||||||
|
telegram=telegram,
|
||||||
|
market=market,
|
||||||
|
stock_code="AAPL",
|
||||||
|
scan_candidates={},
|
||||||
|
settings=settings,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert overseas_broker.send_overseas_order.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_overnight_policy_prioritizes_killswitch_over_exception() -> None:
|
def test_overnight_policy_prioritizes_killswitch_over_exception() -> None:
|
||||||
market = MagicMock()
|
market = MagicMock()
|
||||||
with patch("src.main.get_session_info", return_value=MagicMock(session_id="US_AFTER")):
|
with patch("src.main.get_session_info", return_value=MagicMock(session_id="US_AFTER")):
|
||||||
|
|||||||
Reference in New Issue
Block a user