feat: inject self-market scorecard into planner prompt (issue #94)
Some checks failed
CI / test (pull_request) Has been cancelled
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:
@@ -95,10 +95,17 @@ class PreMarketPlanner:
|
|||||||
try:
|
try:
|
||||||
# 1. Gather context
|
# 1. Gather context
|
||||||
context_data = self._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)
|
cross_market = self.build_cross_market_context(market, today)
|
||||||
|
|
||||||
# 2. Build prompt
|
# 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
|
# 3. Call Gemini
|
||||||
market_data = {
|
market_data = {
|
||||||
@@ -176,6 +183,37 @@ class PreMarketPlanner:
|
|||||||
lessons=scorecard_data.get("lessons", []),
|
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]:
|
def _gather_context(self) -> dict[str, Any]:
|
||||||
"""Gather strategic context using ContextSelector."""
|
"""Gather strategic context using ContextSelector."""
|
||||||
layers = self._context_selector.select_layers(
|
layers = self._context_selector.select_layers(
|
||||||
@@ -189,6 +227,7 @@ class PreMarketPlanner:
|
|||||||
market: str,
|
market: str,
|
||||||
candidates: list[ScanCandidate],
|
candidates: list[ScanCandidate],
|
||||||
context_data: dict[str, Any],
|
context_data: dict[str, Any],
|
||||||
|
self_market_scorecard: dict[str, Any] | None,
|
||||||
cross_market: CrossMarketContext | None,
|
cross_market: CrossMarketContext | None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Build a structured prompt for Gemini to generate scenario JSON."""
|
"""Build a structured prompt for Gemini to generate scenario JSON."""
|
||||||
@@ -212,6 +251,18 @@ class PreMarketPlanner:
|
|||||||
if cross_market.lessons:
|
if cross_market.lessons:
|
||||||
cross_market_text += f"- Lessons: {'; '.join(cross_market.lessons[:3])}\n"
|
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 = ""
|
context_text = ""
|
||||||
if context_data:
|
if context_data:
|
||||||
context_text = "\n## Strategic Context\n"
|
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"You are a pre-market trading strategist for the {market} market.\n"
|
||||||
f"Generate structured trading scenarios for today.\n\n"
|
f"Generate structured trading scenarios for today.\n\n"
|
||||||
f"## Candidates (from volatility scanner)\n{candidates_text}\n"
|
f"## Candidates (from volatility scanner)\n{candidates_text}\n"
|
||||||
|
f"{self_market_text}"
|
||||||
f"{cross_market_text}"
|
f"{cross_market_text}"
|
||||||
f"{context_text}\n"
|
f"{context_text}\n"
|
||||||
f"## Instructions\n"
|
f"## Instructions\n"
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ def _make_planner(
|
|||||||
token_count: int = 200,
|
token_count: int = 200,
|
||||||
context_data: dict | None = None,
|
context_data: dict | None = None,
|
||||||
scorecard_data: dict | None = None,
|
scorecard_data: dict | None = None,
|
||||||
|
scorecard_map: dict[tuple[str, str, str], dict | None] | None = None,
|
||||||
) -> PreMarketPlanner:
|
) -> PreMarketPlanner:
|
||||||
"""Create a PreMarketPlanner with mocked dependencies."""
|
"""Create a PreMarketPlanner with mocked dependencies."""
|
||||||
if not gemini_response:
|
if not gemini_response:
|
||||||
@@ -106,7 +107,14 @@ def _make_planner(
|
|||||||
|
|
||||||
# Mock ContextStore
|
# Mock ContextStore
|
||||||
store = MagicMock()
|
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
|
# Mock ContextSelector
|
||||||
selector = MagicMock()
|
selector = MagicMock()
|
||||||
@@ -282,6 +290,30 @@ class TestGeneratePlaybook:
|
|||||||
)
|
)
|
||||||
planner._context_selector.get_context_data.assert_called_once()
|
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
|
# _parse_response
|
||||||
@@ -481,6 +513,32 @@ class TestBuildCrossMarketContext:
|
|||||||
assert ctx is None
|
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
|
# _build_prompt
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -491,7 +549,7 @@ class TestBuildPrompt:
|
|||||||
planner = _make_planner()
|
planner = _make_planner()
|
||||||
candidates = [_candidate(code="005930", name="Samsung")]
|
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 "005930" in prompt
|
||||||
assert "Samsung" in prompt
|
assert "Samsung" in prompt
|
||||||
@@ -505,7 +563,7 @@ class TestBuildPrompt:
|
|||||||
win_rate=60, index_change_pct=0.8, lessons=["Cut losses early"],
|
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 "Other Market (US)" in prompt
|
||||||
assert "+1.50%" in prompt
|
assert "+1.50%" in prompt
|
||||||
@@ -515,7 +573,7 @@ class TestBuildPrompt:
|
|||||||
planner = _make_planner()
|
planner = _make_planner()
|
||||||
context = {"L6_DAILY": {"win_rate": 0.65, "total_pnl": 2.5}}
|
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 "Strategic Context" in prompt
|
||||||
assert "L6_DAILY" in prompt
|
assert "L6_DAILY" in prompt
|
||||||
@@ -523,15 +581,30 @@ class TestBuildPrompt:
|
|||||||
|
|
||||||
def test_prompt_contains_max_scenarios(self) -> None:
|
def test_prompt_contains_max_scenarios(self) -> None:
|
||||||
planner = _make_planner()
|
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
|
assert f"Max {planner._settings.MAX_SCENARIOS_PER_STOCK} scenarios" in prompt
|
||||||
|
|
||||||
def test_prompt_market_name(self) -> None:
|
def test_prompt_market_name(self) -> None:
|
||||||
planner = _make_planner()
|
planner = _make_planner()
|
||||||
prompt = planner._build_prompt("US", [_candidate()], {}, None)
|
prompt = planner._build_prompt("US", [_candidate()], {}, None, None)
|
||||||
assert "US market" in prompt
|
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
|
# _extract_json
|
||||||
|
|||||||
Reference in New Issue
Block a user