feat: cross-market date fix and strategic context selector (issue #88)
Some checks failed
CI / test (pull_request) Has been cancelled

KR planner now reads US scorecard from previous day (timezone-aware),
and generate_playbook uses STRATEGIC context selection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
agentson
2026-02-14 23:20:24 +09:00
parent 692cb61991
commit 86c94cff62
2 changed files with 44 additions and 9 deletions

View File

@@ -8,7 +8,7 @@ from __future__ import annotations
import json import json
import logging import logging
from datetime import date from datetime import date, timedelta
from typing import Any from typing import Any
from src.analysis.smart_scanner import ScanCandidate from src.analysis.smart_scanner import ScanCandidate
@@ -145,7 +145,8 @@ class PreMarketPlanner:
other_market = "US" if target_market == "KR" else "KR" other_market = "US" if target_market == "KR" else "KR"
if today is None: if today is None:
today = date.today() today = date.today()
timeframe = today.isoformat() timeframe_date = today - timedelta(days=1) if target_market == "KR" else today
timeframe = timeframe_date.isoformat()
scorecard_key = f"scorecard_{other_market}" scorecard_key = f"scorecard_{other_market}"
scorecard_data = self._context_store.get_context( scorecard_data = self._context_store.get_context(

View File

@@ -9,6 +9,7 @@ from unittest.mock import AsyncMock, MagicMock
import pytest import pytest
from src.analysis.smart_scanner import ScanCandidate from src.analysis.smart_scanner import ScanCandidate
from src.brain.context_selector import DecisionType
from src.brain.gemini_client import TradeDecision from src.brain.gemini_client import TradeDecision
from src.config import Settings from src.config import Settings
from src.context.store import ContextLayer from src.context.store import ContextLayer
@@ -16,12 +17,10 @@ from src.strategy.models import (
CrossMarketContext, CrossMarketContext,
DayPlaybook, DayPlaybook,
MarketOutlook, MarketOutlook,
PlaybookStatus,
ScenarioAction, ScenarioAction,
) )
from src.strategy.pre_market_planner import PreMarketPlanner from src.strategy.pre_market_planner import PreMarketPlanner
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Fixtures # Fixtures
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -111,7 +110,9 @@ def _make_planner(
# Mock ContextSelector # Mock ContextSelector
selector = MagicMock() selector = MagicMock()
selector.select_layers = MagicMock(return_value=[ContextLayer.L7_REALTIME, ContextLayer.L6_DAILY]) selector.select_layers = MagicMock(
return_value=[ContextLayer.L7_REALTIME, ContextLayer.L6_DAILY]
)
selector.get_context_data = MagicMock(return_value=context_data or {}) selector.get_context_data = MagicMock(return_value=context_data or {})
settings = Settings( settings = Settings(
@@ -220,11 +221,25 @@ class TestGeneratePlaybook:
stocks = [ stocks = [
{ {
"stock_code": "005930", "stock_code": "005930",
"scenarios": [{"condition": {"rsi_below": 30}, "action": "BUY", "confidence": 85, "rationale": "ok"}], "scenarios": [
{
"condition": {"rsi_below": 30},
"action": "BUY",
"confidence": 85,
"rationale": "ok",
}
],
}, },
{ {
"stock_code": "UNKNOWN", "stock_code": "UNKNOWN",
"scenarios": [{"condition": {"rsi_below": 20}, "action": "BUY", "confidence": 90, "rationale": "bad"}], "scenarios": [
{
"condition": {"rsi_below": 20},
"action": "BUY",
"confidence": 90,
"rationale": "bad",
}
],
}, },
] ]
planner = _make_planner(gemini_response=_gemini_response_json(stocks=stocks)) planner = _make_planner(gemini_response=_gemini_response_json(stocks=stocks))
@@ -254,6 +269,19 @@ class TestGeneratePlaybook:
assert pb.token_count == 450 assert pb.token_count == 450
@pytest.mark.asyncio
async def test_generate_playbook_uses_strategic_context_selector(self) -> None:
planner = _make_planner()
candidates = [_candidate()]
await planner.generate_playbook("KR", candidates, today=date(2026, 2, 8))
planner._context_selector.select_layers.assert_called_once_with(
decision_type=DecisionType.STRATEGIC,
include_realtime=True,
)
planner._context_selector.get_context_data.assert_called_once()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# _parse_response # _parse_response
@@ -402,7 +430,12 @@ class TestParseResponse:
class TestBuildCrossMarketContext: class TestBuildCrossMarketContext:
def test_kr_reads_us_scorecard(self) -> None: def test_kr_reads_us_scorecard(self) -> None:
scorecard = {"total_pnl": 2.5, "win_rate": 65, "index_change_pct": 0.8, "lessons": ["Stay patient"]} scorecard = {
"total_pnl": 2.5,
"win_rate": 65,
"index_change_pct": 0.8,
"lessons": ["Stay patient"],
}
planner = _make_planner(scorecard_data=scorecard) planner = _make_planner(scorecard_data=scorecard)
ctx = planner.build_cross_market_context("KR", today=date(2026, 2, 8)) ctx = planner.build_cross_market_context("KR", today=date(2026, 2, 8))
@@ -415,8 +448,9 @@ class TestBuildCrossMarketContext:
# Verify it queried scorecard_US # Verify it queried scorecard_US
planner._context_store.get_context.assert_called_once_with( planner._context_store.get_context.assert_called_once_with(
ContextLayer.L6_DAILY, "2026-02-08", "scorecard_US" ContextLayer.L6_DAILY, "2026-02-07", "scorecard_US"
) )
assert ctx.date == "2026-02-07"
def test_us_reads_kr_scorecard(self) -> None: def test_us_reads_kr_scorecard(self) -> None:
scorecard = {"total_pnl": -1.0, "win_rate": 40, "index_change_pct": -0.5} scorecard = {"total_pnl": -1.0, "win_rate": 40, "index_change_pct": -0.5}