diff --git a/src/analysis/scanner.py b/src/analysis/scanner.py index 7b82184..50d34ba 100644 --- a/src/analysis/scanner.py +++ b/src/analysis/scanner.py @@ -108,7 +108,7 @@ class MarketScanner: self.context_store.set_context( ContextLayer.L7_REALTIME, timeframe, - f"{market.code}_{stock_code}_volatility", + f"volatility_{market.code}_{stock_code}", { "price": metrics.current_price, "atr": metrics.atr, @@ -179,7 +179,7 @@ class MarketScanner: self.context_store.set_context( ContextLayer.L7_REALTIME, timeframe, - f"{market.code}_scan_result", + f"scan_result_{market.code}", { "total_scanned": len(valid_metrics), "top_movers": [m.stock_code for m in top_movers], diff --git a/src/main.py b/src/main.py index 8baeac4..64c7f28 100644 --- a/src/main.py +++ b/src/main.py @@ -13,7 +13,6 @@ import signal from datetime import UTC, datetime from typing import Any -from src.analysis.scanner import MarketScanner from src.analysis.smart_scanner import ScanCandidate, SmartVolatilityScanner from src.analysis.volatility import VolatilityAnalyzer from src.brain.context_selector import ContextSelector @@ -154,6 +153,38 @@ async def trading_cycle( market_data["rsi"] = candidate.rsi market_data["volume_ratio"] = candidate.volume_ratio + # 1.3. Record L7 real-time context (market-scoped keys) + timeframe = datetime.now(UTC).isoformat() + context_store.set_context( + ContextLayer.L7_REALTIME, + timeframe, + f"volatility_{market.code}_{stock_code}", + { + "momentum_score": 50.0, + "volume_surge": 1.0, + "price_change_1m": 0.0, + }, + ) + context_store.set_context( + ContextLayer.L7_REALTIME, + timeframe, + f"price_{market.code}_{stock_code}", + {"current_price": current_price}, + ) + if candidate: + context_store.set_context( + ContextLayer.L7_REALTIME, + timeframe, + f"rsi_{market.code}_{stock_code}", + {"rsi": candidate.rsi}, + ) + context_store.set_context( + ContextLayer.L7_REALTIME, + timeframe, + f"volume_ratio_{market.code}_{stock_code}", + {"volume_ratio": candidate.volume_ratio}, + ) + # Build portfolio data for global rule evaluation portfolio_data = { "portfolio_pnl_pct": pnl_pct, @@ -171,7 +202,7 @@ async def trading_cycle( volatility_data = context_store.get_context( ContextLayer.L7_REALTIME, latest_timeframe, - f"volatility_{stock_code}", + f"volatility_{market.code}_{stock_code}", ) if volatility_data: volatility_score = volatility_data.get("momentum_score", 50.0) @@ -835,15 +866,6 @@ async def run(settings: Settings) -> None: # Initialize volatility hunter volatility_analyzer = VolatilityAnalyzer(min_volume_surge=2.0, min_price_change=1.0) - market_scanner = MarketScanner( - broker=broker, - overseas_broker=overseas_broker, - volatility_analyzer=volatility_analyzer, - context_store=context_store, - top_n=5, - max_concurrent_scans=1, # Fully serialized to avoid EGW00201 - ) - # Initialize smart scanner (Python-first, AI-last pipeline) smart_scanner = SmartVolatilityScanner( broker=broker, diff --git a/tests/test_main.py b/tests/test_main.py index 0a84b2e..417a74c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,11 +1,12 @@ """Tests for main trading loop integration.""" from datetime import date -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import ANY, AsyncMock, MagicMock, patch import pytest from src.core.risk_manager import CircuitBreakerTripped, FatFingerRejected +from src.context.layer import ContextLayer from src.main import safe_float, trading_cycle from src.strategy.models import ( DayPlaybook, @@ -810,6 +811,69 @@ class TestScenarioEngineIntegration: assert "portfolio_pnl_pct" in portfolio_data assert "total_cash" in portfolio_data + @pytest.mark.asyncio + async def test_trading_cycle_sets_l7_context_keys( + self, mock_broker: MagicMock, mock_market: MagicMock, mock_telegram: MagicMock, + ) -> None: + """Test L7 context is written with market-scoped keys.""" + from src.analysis.smart_scanner import ScanCandidate + + engine = MagicMock(spec=ScenarioEngine) + engine.evaluate = MagicMock(return_value=_make_hold_match()) + playbook = _make_playbook() + context_store = MagicMock(get_latest_timeframe=MagicMock(return_value=None)) + + candidate = ScanCandidate( + stock_code="005930", name="Samsung", price=50000, + volume=1000000, volume_ratio=3.5, rsi=25.0, + signal="oversold", score=85.0, + ) + + with patch("src.main.log_trade"): + await trading_cycle( + broker=mock_broker, + overseas_broker=MagicMock(), + scenario_engine=engine, + playbook=playbook, + risk=MagicMock(), + db_conn=MagicMock(), + decision_logger=MagicMock(), + context_store=context_store, + criticality_assessor=MagicMock( + assess_market_conditions=MagicMock(return_value=MagicMock(value="NORMAL")), + get_timeout=MagicMock(return_value=5.0), + ), + telegram=mock_telegram, + market=mock_market, + stock_code="005930", + scan_candidates={"KR": {"005930": candidate}}, + ) + + context_store.set_context.assert_any_call( + ContextLayer.L7_REALTIME, + ANY, + "volatility_KR_005930", + {"momentum_score": 50.0, "volume_surge": 1.0, "price_change_1m": 0.0}, + ) + context_store.set_context.assert_any_call( + ContextLayer.L7_REALTIME, + ANY, + "price_KR_005930", + {"current_price": 50000.0}, + ) + context_store.set_context.assert_any_call( + ContextLayer.L7_REALTIME, + ANY, + "rsi_KR_005930", + {"rsi": 25.0}, + ) + context_store.set_context.assert_any_call( + ContextLayer.L7_REALTIME, + ANY, + "volume_ratio_KR_005930", + {"volume_ratio": 3.5}, + ) + @pytest.mark.asyncio async def test_scan_candidates_market_scoped( self, mock_broker: MagicMock, mock_market: MagicMock, mock_telegram: MagicMock, diff --git a/tests/test_volatility.py b/tests/test_volatility.py index c2bb18c..02f0234 100644 --- a/tests/test_volatility.py +++ b/tests/test_volatility.py @@ -412,7 +412,7 @@ class TestMarketScanner: scan_result = context_store.get_context( ContextLayer.L7_REALTIME, latest_timeframe, - "KR_scan_result", + "scan_result_KR", ) assert scan_result is not None assert scan_result["total_scanned"] == 3