feat: inject self-market scorecard into planner prompt (issue #94)
Some checks failed
CI / test (pull_request) Has been cancelled

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 <noreply@anthropic.com>
This commit is contained in:
agentson
2026-02-14 23:27:01 +09:00
parent 6cad8e74e1
commit 6b3960a3a4
2 changed files with 132 additions and 7 deletions

View File

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