feat: cross-market date fix and strategic context selector (issue #88)
Some checks failed
CI / test (pull_request) Has been cancelled
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:
@@ -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(
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user