Merge pull request 'feat: L7 실시간 컨텍스트 시장별 기록 (issue #85)' (#116) from feature/issue-85-l7-context-write into main
Some checks failed
CI / test (push) Has been cancelled

Reviewed-on: #116
This commit was merged in pull request #116.
This commit is contained in:
2026-02-10 04:22:57 +09:00
4 changed files with 101 additions and 15 deletions

View File

@@ -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],

View File

@@ -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,

View File

@@ -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,

View File

@@ -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