fix: address PR review — inject today param, remove unused imports, fix lint
Some checks failed
CI / test (pull_request) Has been cancelled

Review findings addressed:
- Finding 1 (ImportError): false positive — ContextLayer is re-exported from
  src.context.store, import works correctly at runtime
- Finding 2 (timezone): generate_playbook() and build_cross_market_context()
  now accept optional today parameter for market-local date injection
- Finding 3 (lint): removed unused imports (UTC, datetime, PlaybookStatus),
  fixed line-too-long in prompt template
- Tests simplified: replaced date patching with direct today= parameter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
agentson
2026-02-08 21:57:39 +09:00
parent 6471e66d89
commit be695a5d7c
2 changed files with 32 additions and 57 deletions

View File

@@ -8,7 +8,7 @@ from __future__ import annotations
import json import json
import logging import logging
from datetime import UTC, date, datetime from datetime import date
from typing import Any from typing import Any
from src.analysis.smart_scanner import ScanCandidate from src.analysis.smart_scanner import ScanCandidate
@@ -21,7 +21,6 @@ from src.strategy.models import (
DayPlaybook, DayPlaybook,
GlobalRule, GlobalRule,
MarketOutlook, MarketOutlook,
PlaybookStatus,
ScenarioAction, ScenarioAction,
StockCondition, StockCondition,
StockPlaybook, StockPlaybook,
@@ -74,17 +73,20 @@ class PreMarketPlanner:
self, self,
market: str, market: str,
candidates: list[ScanCandidate], candidates: list[ScanCandidate],
today: date | None = None,
) -> DayPlaybook: ) -> DayPlaybook:
"""Generate a DayPlaybook for a market using Gemini. """Generate a DayPlaybook for a market using Gemini.
Args: Args:
market: Market code ("KR" or "US") market: Market code ("KR" or "US")
candidates: Stock candidates from SmartVolatilityScanner candidates: Stock candidates from SmartVolatilityScanner
today: Override date (defaults to date.today()). Use market-local date.
Returns: Returns:
DayPlaybook with scenarios. Empty/defensive if no candidates or failure. DayPlaybook with scenarios. Empty/defensive if no candidates or failure.
""" """
today = date.today() if today is None:
today = date.today()
if not candidates: if not candidates:
logger.info("No candidates for %s — returning empty playbook", market) logger.info("No candidates for %s — returning empty playbook", market)
@@ -93,7 +95,7 @@ class PreMarketPlanner:
try: try:
# 1. Gather context # 1. Gather context
context_data = self._gather_context() context_data = self._gather_context()
cross_market = self.build_cross_market_context(market) 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, cross_market)
@@ -128,14 +130,21 @@ class PreMarketPlanner:
return self._defensive_playbook(today, market, candidates) return self._defensive_playbook(today, market, candidates)
return self._empty_playbook(today, market) return self._empty_playbook(today, market)
def build_cross_market_context(self, target_market: str) -> CrossMarketContext | None: def build_cross_market_context(
self, target_market: str, today: date | None = None,
) -> CrossMarketContext | None:
"""Build cross-market context from the other market's L6 data. """Build cross-market context from the other market's L6 data.
KR planner → reads US scorecard from previous night. KR planner → reads US scorecard from previous night.
US planner → reads KR scorecard from today. US planner → reads KR scorecard from today.
Args:
target_market: The market being planned ("KR" or "US")
today: Override date (defaults to date.today()). Use market-local date.
""" """
other_market = "US" if target_market == "KR" else "KR" other_market = "US" if target_market == "KR" else "KR"
today = date.today() if today is None:
today = date.today()
timeframe = today.isoformat() timeframe = today.isoformat()
scorecard_key = f"scorecard_{other_market}" scorecard_key = f"scorecard_{other_market}"
@@ -220,9 +229,11 @@ class PreMarketPlanner:
f"## Instructions\n" f"## Instructions\n"
f"Return a JSON object with this exact structure:\n" f"Return a JSON object with this exact structure:\n"
f'{{\n' f'{{\n'
f' "market_outlook": "bullish|neutral_to_bullish|neutral|neutral_to_bearish|bearish",\n' f' "market_outlook": "bullish|neutral_to_bullish|neutral'
f'|neutral_to_bearish|bearish",\n'
f' "global_rules": [\n' f' "global_rules": [\n'
f' {{"condition": "portfolio_pnl_pct < -2.0", "action": "REDUCE_ALL", "rationale": "..."}}\n' f' {{"condition": "portfolio_pnl_pct < -2.0",'
f' "action": "REDUCE_ALL", "rationale": "..."}}\n'
f' ],\n' f' ],\n'
f' "stocks": [\n' f' "stocks": [\n'
f' {{\n' f' {{\n'

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import json import json
from datetime import date from datetime import date
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock
import pytest import pytest
@@ -135,10 +135,7 @@ class TestGeneratePlaybook:
planner = _make_planner() planner = _make_planner()
candidates = [_candidate()] candidates = [_candidate()]
with patch("src.strategy.pre_market_planner.date") as mock_date: pb = await planner.generate_playbook("KR", candidates, today=date(2026, 2, 8))
mock_date.today.return_value = date(2026, 2, 8)
mock_date.side_effect = lambda *a, **kw: date(*a, **kw)
pb = await planner.generate_playbook("KR", candidates)
assert isinstance(pb, DayPlaybook) assert isinstance(pb, DayPlaybook)
assert pb.market == "KR" assert pb.market == "KR"
@@ -151,10 +148,7 @@ class TestGeneratePlaybook:
async def test_empty_candidates_returns_empty_playbook(self) -> None: async def test_empty_candidates_returns_empty_playbook(self) -> None:
planner = _make_planner() planner = _make_planner()
with patch("src.strategy.pre_market_planner.date") as mock_date: pb = await planner.generate_playbook("KR", [], today=date(2026, 2, 8))
mock_date.today.return_value = date(2026, 2, 8)
mock_date.side_effect = lambda *a, **kw: date(*a, **kw)
pb = await planner.generate_playbook("KR", [])
assert pb.stock_count == 0 assert pb.stock_count == 0
assert pb.scenario_count == 0 assert pb.scenario_count == 0
@@ -166,10 +160,7 @@ class TestGeneratePlaybook:
planner._gemini.decide = AsyncMock(side_effect=RuntimeError("API timeout")) planner._gemini.decide = AsyncMock(side_effect=RuntimeError("API timeout"))
candidates = [_candidate()] candidates = [_candidate()]
with patch("src.strategy.pre_market_planner.date") as mock_date: pb = await planner.generate_playbook("KR", candidates, today=date(2026, 2, 8))
mock_date.today.return_value = date(2026, 2, 8)
mock_date.side_effect = lambda *a, **kw: date(*a, **kw)
pb = await planner.generate_playbook("KR", candidates)
assert pb.default_action == ScenarioAction.HOLD assert pb.default_action == ScenarioAction.HOLD
assert pb.market_outlook == MarketOutlook.NEUTRAL_TO_BEARISH assert pb.market_outlook == MarketOutlook.NEUTRAL_TO_BEARISH
@@ -184,10 +175,7 @@ class TestGeneratePlaybook:
planner._gemini.decide = AsyncMock(side_effect=RuntimeError("fail")) planner._gemini.decide = AsyncMock(side_effect=RuntimeError("fail"))
candidates = [_candidate()] candidates = [_candidate()]
with patch("src.strategy.pre_market_planner.date") as mock_date: pb = await planner.generate_playbook("KR", candidates, today=date(2026, 2, 8))
mock_date.today.return_value = date(2026, 2, 8)
mock_date.side_effect = lambda *a, **kw: date(*a, **kw)
pb = await planner.generate_playbook("KR", candidates)
assert pb.stock_count == 0 assert pb.stock_count == 0
@@ -220,10 +208,7 @@ class TestGeneratePlaybook:
planner = _make_planner(gemini_response=_gemini_response_json(stocks=stocks)) planner = _make_planner(gemini_response=_gemini_response_json(stocks=stocks))
candidates = [_candidate(), _candidate(code="AAPL", name="Apple")] candidates = [_candidate(), _candidate(code="AAPL", name="Apple")]
with patch("src.strategy.pre_market_planner.date") as mock_date: pb = await planner.generate_playbook("US", candidates, today=date(2026, 2, 8))
mock_date.today.return_value = date(2026, 2, 8)
mock_date.side_effect = lambda *a, **kw: date(*a, **kw)
pb = await planner.generate_playbook("US", candidates)
assert pb.stock_count == 2 assert pb.stock_count == 2
codes = [sp.stock_code for sp in pb.stock_playbooks] codes = [sp.stock_code for sp in pb.stock_playbooks]
@@ -245,10 +230,7 @@ class TestGeneratePlaybook:
planner = _make_planner(gemini_response=_gemini_response_json(stocks=stocks)) planner = _make_planner(gemini_response=_gemini_response_json(stocks=stocks))
candidates = [_candidate()] # Only 005930 candidates = [_candidate()] # Only 005930
with patch("src.strategy.pre_market_planner.date") as mock_date: pb = await planner.generate_playbook("KR", candidates, today=date(2026, 2, 8))
mock_date.today.return_value = date(2026, 2, 8)
mock_date.side_effect = lambda *a, **kw: date(*a, **kw)
pb = await planner.generate_playbook("KR", candidates)
assert pb.stock_count == 1 assert pb.stock_count == 1
assert pb.stock_playbooks[0].stock_code == "005930" assert pb.stock_playbooks[0].stock_code == "005930"
@@ -258,10 +240,7 @@ class TestGeneratePlaybook:
planner = _make_planner() planner = _make_planner()
candidates = [_candidate()] candidates = [_candidate()]
with patch("src.strategy.pre_market_planner.date") as mock_date: pb = await planner.generate_playbook("KR", candidates, today=date(2026, 2, 8))
mock_date.today.return_value = date(2026, 2, 8)
mock_date.side_effect = lambda *a, **kw: date(*a, **kw)
pb = await planner.generate_playbook("KR", candidates)
assert len(pb.global_rules) == 1 assert len(pb.global_rules) == 1
assert pb.global_rules[0].action == ScenarioAction.REDUCE_ALL assert pb.global_rules[0].action == ScenarioAction.REDUCE_ALL
@@ -271,10 +250,7 @@ class TestGeneratePlaybook:
planner = _make_planner(token_count=450) planner = _make_planner(token_count=450)
candidates = [_candidate()] candidates = [_candidate()]
with patch("src.strategy.pre_market_planner.date") as mock_date: pb = await planner.generate_playbook("KR", candidates, today=date(2026, 2, 8))
mock_date.today.return_value = date(2026, 2, 8)
mock_date.side_effect = lambda *a, **kw: date(*a, **kw)
pb = await planner.generate_playbook("KR", candidates)
assert pb.token_count == 450 assert pb.token_count == 450
@@ -429,10 +405,7 @@ class TestBuildCrossMarketContext:
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)
with patch("src.strategy.pre_market_planner.date") as mock_date: ctx = planner.build_cross_market_context("KR", today=date(2026, 2, 8))
mock_date.today.return_value = date(2026, 2, 8)
mock_date.side_effect = lambda *a, **kw: date(*a, **kw)
ctx = planner.build_cross_market_context("KR")
assert ctx is not None assert ctx is not None
assert ctx.market == "US" assert ctx.market == "US"
@@ -449,10 +422,7 @@ class TestBuildCrossMarketContext:
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}
planner = _make_planner(scorecard_data=scorecard) planner = _make_planner(scorecard_data=scorecard)
with patch("src.strategy.pre_market_planner.date") as mock_date: ctx = planner.build_cross_market_context("US", today=date(2026, 2, 8))
mock_date.today.return_value = date(2026, 2, 8)
mock_date.side_effect = lambda *a, **kw: date(*a, **kw)
ctx = planner.build_cross_market_context("US")
assert ctx is not None assert ctx is not None
assert ctx.market == "KR" assert ctx.market == "KR"
@@ -465,20 +435,14 @@ class TestBuildCrossMarketContext:
def test_no_scorecard_returns_none(self) -> None: def test_no_scorecard_returns_none(self) -> None:
planner = _make_planner(scorecard_data=None) planner = _make_planner(scorecard_data=None)
with patch("src.strategy.pre_market_planner.date") as mock_date: ctx = planner.build_cross_market_context("KR", today=date(2026, 2, 8))
mock_date.today.return_value = date(2026, 2, 8)
mock_date.side_effect = lambda *a, **kw: date(*a, **kw)
ctx = planner.build_cross_market_context("KR")
assert ctx is None assert ctx is None
def test_invalid_scorecard_returns_none(self) -> None: def test_invalid_scorecard_returns_none(self) -> None:
planner = _make_planner(scorecard_data="not a dict and not json") planner = _make_planner(scorecard_data="not a dict and not json")
with patch("src.strategy.pre_market_planner.date") as mock_date: ctx = planner.build_cross_market_context("KR", today=date(2026, 2, 8))
mock_date.today.return_value = date(2026, 2, 8)
mock_date.side_effect = lambda *a, **kw: date(*a, **kw)
ctx = planner.build_cross_market_context("KR")
assert ctx is None assert ctx is None