From 6b3960a3a423fe26502292d82c0f92fb491a0d1d Mon Sep 17 00:00:00 2001 From: agentson Date: Sat, 14 Feb 2026 23:27:01 +0900 Subject: [PATCH] feat: inject self-market scorecard into planner prompt (issue #94) Add build_self_market_scorecard() to read previous day's own market performance, and include it in the Gemini planning prompt alongside cross-market context. Co-Authored-By: Claude Opus 4.6 --- src/strategy/pre_market_planner.py | 54 ++++++++++++++++++- tests/test_pre_market_planner.py | 85 +++++++++++++++++++++++++++--- 2 files changed, 132 insertions(+), 7 deletions(-) diff --git a/src/strategy/pre_market_planner.py b/src/strategy/pre_market_planner.py index 4420dae..bb5772f 100644 --- a/src/strategy/pre_market_planner.py +++ b/src/strategy/pre_market_planner.py @@ -95,10 +95,17 @@ class PreMarketPlanner: try: # 1. Gather context context_data = self._gather_context() + self_market_scorecard = self.build_self_market_scorecard(market, today) cross_market = self.build_cross_market_context(market, today) # 2. Build prompt - prompt = self._build_prompt(market, candidates, context_data, cross_market) + prompt = self._build_prompt( + market, + candidates, + context_data, + self_market_scorecard, + cross_market, + ) # 3. Call Gemini market_data = { @@ -176,6 +183,37 @@ class PreMarketPlanner: lessons=scorecard_data.get("lessons", []), ) + def build_self_market_scorecard( + self, market: str, today: date | None = None, + ) -> dict[str, Any] | None: + """Build previous-day scorecard for the same market.""" + if today is None: + today = date.today() + timeframe = (today - timedelta(days=1)).isoformat() + scorecard_key = f"scorecard_{market}" + scorecard_data = self._context_store.get_context( + ContextLayer.L6_DAILY, timeframe, scorecard_key + ) + + if scorecard_data is None: + return None + + if isinstance(scorecard_data, str): + try: + scorecard_data = json.loads(scorecard_data) + except (json.JSONDecodeError, TypeError): + return None + + if not isinstance(scorecard_data, dict): + return None + + return { + "date": timeframe, + "total_pnl": float(scorecard_data.get("total_pnl", 0.0)), + "win_rate": float(scorecard_data.get("win_rate", 0.0)), + "lessons": scorecard_data.get("lessons", []), + } + def _gather_context(self) -> dict[str, Any]: """Gather strategic context using ContextSelector.""" layers = self._context_selector.select_layers( @@ -189,6 +227,7 @@ class PreMarketPlanner: market: str, candidates: list[ScanCandidate], context_data: dict[str, Any], + self_market_scorecard: dict[str, Any] | None, cross_market: CrossMarketContext | None, ) -> str: """Build a structured prompt for Gemini to generate scenario JSON.""" @@ -212,6 +251,18 @@ class PreMarketPlanner: if cross_market.lessons: cross_market_text += f"- Lessons: {'; '.join(cross_market.lessons[:3])}\n" + self_market_text = "" + if self_market_scorecard: + self_market_text = ( + f"\n## My Market Previous Day ({market})\n" + f"- Date: {self_market_scorecard['date']}\n" + f"- P&L: {self_market_scorecard['total_pnl']:+.2f}%\n" + f"- Win Rate: {self_market_scorecard['win_rate']:.0f}%\n" + ) + lessons = self_market_scorecard.get("lessons", []) + if lessons: + self_market_text += f"- Lessons: {'; '.join(lessons[:3])}\n" + context_text = "" if context_data: context_text = "\n## Strategic Context\n" @@ -225,6 +276,7 @@ class PreMarketPlanner: f"You are a pre-market trading strategist for the {market} market.\n" f"Generate structured trading scenarios for today.\n\n" f"## Candidates (from volatility scanner)\n{candidates_text}\n" + f"{self_market_text}" f"{cross_market_text}" f"{context_text}\n" f"## Instructions\n" diff --git a/tests/test_pre_market_planner.py b/tests/test_pre_market_planner.py index 6e6db55..2093208 100644 --- a/tests/test_pre_market_planner.py +++ b/tests/test_pre_market_planner.py @@ -88,6 +88,7 @@ def _make_planner( token_count: int = 200, context_data: dict | None = None, scorecard_data: dict | None = None, + scorecard_map: dict[tuple[str, str, str], dict | None] | None = None, ) -> PreMarketPlanner: """Create a PreMarketPlanner with mocked dependencies.""" if not gemini_response: @@ -106,7 +107,14 @@ def _make_planner( # Mock ContextStore store = MagicMock() - store.get_context = MagicMock(return_value=scorecard_data) + if scorecard_map is not None: + store.get_context = MagicMock( + side_effect=lambda layer, timeframe, key: scorecard_map.get( + (layer.value if hasattr(layer, "value") else layer, timeframe, key) + ) + ) + else: + store.get_context = MagicMock(return_value=scorecard_data) # Mock ContextSelector selector = MagicMock() @@ -282,6 +290,30 @@ class TestGeneratePlaybook: ) planner._context_selector.get_context_data.assert_called_once() + @pytest.mark.asyncio + async def test_generate_playbook_injects_self_and_cross_scorecards(self) -> None: + scorecard_map = { + (ContextLayer.L6_DAILY.value, "2026-02-07", "scorecard_KR"): { + "total_pnl": -1.0, + "win_rate": 40, + "lessons": ["Tighten entries"], + }, + (ContextLayer.L6_DAILY.value, "2026-02-07", "scorecard_US"): { + "total_pnl": 1.5, + "win_rate": 62, + "index_change_pct": 0.9, + "lessons": ["Follow momentum"], + }, + } + planner = _make_planner(scorecard_map=scorecard_map) + + await planner.generate_playbook("KR", [_candidate()], today=date(2026, 2, 8)) + + call_market_data = planner._gemini.decide.call_args.args[0] + prompt = call_market_data["prompt_override"] + assert "My Market Previous Day (KR)" in prompt + assert "Other Market (US)" in prompt + # --------------------------------------------------------------------------- # _parse_response @@ -481,6 +513,32 @@ class TestBuildCrossMarketContext: assert ctx is None +# --------------------------------------------------------------------------- +# build_self_market_scorecard +# --------------------------------------------------------------------------- + + +class TestBuildSelfMarketScorecard: + def test_reads_previous_day_scorecard(self) -> None: + scorecard = {"total_pnl": -1.2, "win_rate": 45, "lessons": ["Reduce overtrading"]} + planner = _make_planner(scorecard_data=scorecard) + + data = planner.build_self_market_scorecard("KR", today=date(2026, 2, 8)) + + assert data is not None + assert data["date"] == "2026-02-07" + assert data["total_pnl"] == -1.2 + assert data["win_rate"] == 45 + assert "Reduce overtrading" in data["lessons"] + planner._context_store.get_context.assert_called_once_with( + ContextLayer.L6_DAILY, "2026-02-07", "scorecard_KR" + ) + + def test_missing_scorecard_returns_none(self) -> None: + planner = _make_planner(scorecard_data=None) + assert planner.build_self_market_scorecard("US", today=date(2026, 2, 8)) is None + + # --------------------------------------------------------------------------- # _build_prompt # --------------------------------------------------------------------------- @@ -491,7 +549,7 @@ class TestBuildPrompt: planner = _make_planner() candidates = [_candidate(code="005930", name="Samsung")] - prompt = planner._build_prompt("KR", candidates, {}, None) + prompt = planner._build_prompt("KR", candidates, {}, None, None) assert "005930" in prompt assert "Samsung" in prompt @@ -505,7 +563,7 @@ class TestBuildPrompt: win_rate=60, index_change_pct=0.8, lessons=["Cut losses early"], ) - prompt = planner._build_prompt("KR", [_candidate()], {}, cross) + prompt = planner._build_prompt("KR", [_candidate()], {}, None, cross) assert "Other Market (US)" in prompt assert "+1.50%" in prompt @@ -515,7 +573,7 @@ class TestBuildPrompt: planner = _make_planner() context = {"L6_DAILY": {"win_rate": 0.65, "total_pnl": 2.5}} - prompt = planner._build_prompt("KR", [_candidate()], context, None) + prompt = planner._build_prompt("KR", [_candidate()], context, None, None) assert "Strategic Context" in prompt assert "L6_DAILY" in prompt @@ -523,15 +581,30 @@ class TestBuildPrompt: def test_prompt_contains_max_scenarios(self) -> None: planner = _make_planner() - prompt = planner._build_prompt("KR", [_candidate()], {}, None) + prompt = planner._build_prompt("KR", [_candidate()], {}, None, None) assert f"Max {planner._settings.MAX_SCENARIOS_PER_STOCK} scenarios" in prompt def test_prompt_market_name(self) -> None: planner = _make_planner() - prompt = planner._build_prompt("US", [_candidate()], {}, None) + prompt = planner._build_prompt("US", [_candidate()], {}, None, None) assert "US market" in prompt + def test_prompt_contains_self_market_scorecard(self) -> None: + planner = _make_planner() + self_scorecard = { + "date": "2026-02-07", + "total_pnl": -0.8, + "win_rate": 45.0, + "lessons": ["Avoid midday entries"], + } + prompt = planner._build_prompt("KR", [_candidate()], {}, self_scorecard, None) + + assert "My Market Previous Day (KR)" in prompt + assert "2026-02-07" in prompt + assert "-0.80%" in prompt + assert "Avoid midday entries" in prompt + # --------------------------------------------------------------------------- # _extract_json