feat: implement pre-market planner with Gemini integration (issue #83) #109

Merged
jihoson merged 3 commits from feature/issue-83-pre-market-planner into main 2026-02-08 22:07:36 +09:00
Collaborator

Summary

  • src/strategy/pre_market_planner.py: PreMarketPlanner 클래스
    • generate_playbook(market, candidates): SmartVolatilityScanner 후보 기반 DayPlaybook 생성
    • build_cross_market_context(target_market): KR↔US 크로스마켓 L6 scorecard 참조
    • 구조화 JSON 프롬프트 (후보 + 크로스마켓 + 전략적 컨텍스트)
    • Gemini JSON 응답 파싱 (마크다운 펜스 제거, 미지 종목 필터, MAX_SCENARIOS 제한)
    • 실패 시 defensive playbook (HOLD + stop-loss -3% + REDUCE_ALL 글로벌 룰)
    • 후보 0개 = 빈 playbook (거래 안 함, 안전)
  • 32 tests, 97% coverage, 전체 480 tests passed

Test plan

  • TestGeneratePlaybook (8): 정상/빈후보/API실패/방어모드/다수후보/미지종목/글로벌룰/토큰
  • TestParseResponse (8): 전체파싱/마크다운/미지outlook/전조건/빈조건/최대제한/잘못된JSON/크로스마켓
  • TestBuildCrossMarketContext (4): KR→US/US→KR/없음/유효하지않음
  • TestBuildPrompt (5): 후보/크로스마켓/컨텍스트/최대시나리오/시장명
  • TestExtractJson (4): 일반/json펜스/일반펜스/공백
  • TestDefensivePlaybook (3): 손절/글로벌룰/빈플레이북

Closes #83

## Summary - `src/strategy/pre_market_planner.py`: PreMarketPlanner 클래스 - `generate_playbook(market, candidates)`: SmartVolatilityScanner 후보 기반 DayPlaybook 생성 - `build_cross_market_context(target_market)`: KR↔US 크로스마켓 L6 scorecard 참조 - 구조화 JSON 프롬프트 (후보 + 크로스마켓 + 전략적 컨텍스트) - Gemini JSON 응답 파싱 (마크다운 펜스 제거, 미지 종목 필터, MAX_SCENARIOS 제한) - 실패 시 defensive playbook (HOLD + stop-loss -3% + REDUCE_ALL 글로벌 룰) - 후보 0개 = 빈 playbook (거래 안 함, 안전) - 32 tests, 97% coverage, 전체 480 tests passed ## Test plan - [x] TestGeneratePlaybook (8): 정상/빈후보/API실패/방어모드/다수후보/미지종목/글로벌룰/토큰 - [x] TestParseResponse (8): 전체파싱/마크다운/미지outlook/전조건/빈조건/최대제한/잘못된JSON/크로스마켓 - [x] TestBuildCrossMarketContext (4): KR→US/US→KR/없음/유효하지않음 - [x] TestBuildPrompt (5): 후보/크로스마켓/컨텍스트/최대시나리오/시장명 - [x] TestExtractJson (4): 일반/json펜스/일반펜스/공백 - [x] TestDefensivePlaybook (3): 손절/글로벌룰/빈플레이북 Closes #83
agentson added 2 commits 2026-02-08 21:50:59 +09:00
PreMarketPlanner generates DayPlaybook via single Gemini API call per market:
- Structured JSON prompt with scan candidates + strategic context
- Cross-market context (KR reads US scorecard, US reads KR scorecard)
- Robust JSON parser with markdown fence stripping
- Unknown stock filtering (only scanner candidates allowed)
- MAX_SCENARIOS_PER_STOCK enforcement
- Defensive playbook on failure (HOLD + stop-loss)
- Empty playbook when no candidates (safe, no trades)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: correct Settings field name in planner tests (KIS_ACCOUNT_NO)
Some checks failed
CI / test (pull_request) Has been cancelled
6471e66d89
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
agentson added 1 commit 2026-02-08 21:57:40 +09:00
fix: address PR review — inject today param, remove unused imports, fix lint
Some checks failed
CI / test (pull_request) Has been cancelled
be695a5d7c
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>
Author
Collaborator

리뷰 반영 (be695a5)

Finding 1: ContextLayer ImportError

거짓 양성 (false positive). ContextLayersrc/context/layer.py에 정의되지만, src/context/store.py에서 re-export합니다:

# src/context/store.py:10
from src.context.layer import LAYER_CONFIG, ContextLayer

from src.context.store import ContextLayer는 런타임에서 정상 동작 확인. 변경 없음.

Finding 2: 날짜/타임존 경계

수정 완료. generate_playbook()build_cross_market_context()today: date | None 파라미터 추가:

  • 기본값: date.today() (기존 동작 유지)
  • caller가 시장 로컬 날짜를 주입 가능 (Issue 1-7에서 markets 모듈의 시장별 날짜 계산 결과를 전달 예정)
  • 테스트에서 patch("date") 제거 → today=date(2026, 2, 8) 직접 전달로 간결화

Finding 3: 미사용 import + lint

수정 완료. UTC, datetime, PlaybookStatus import 제거. 프롬프트 line-too-long 수정. ruff check 통과.

## 리뷰 반영 (`be695a5`) ### Finding 1: ContextLayer ImportError **거짓 양성 (false positive).** `ContextLayer`는 `src/context/layer.py`에 정의되지만, `src/context/store.py`에서 re-export합니다: ```python # src/context/store.py:10 from src.context.layer import LAYER_CONFIG, ContextLayer ``` `from src.context.store import ContextLayer`는 런타임에서 정상 동작 확인. 변경 없음. ### Finding 2: 날짜/타임존 경계 **수정 완료.** `generate_playbook()`과 `build_cross_market_context()`에 `today: date | None` 파라미터 추가: - 기본값: `date.today()` (기존 동작 유지) - caller가 시장 로컬 날짜를 주입 가능 (Issue 1-7에서 `markets` 모듈의 시장별 날짜 계산 결과를 전달 예정) - 테스트에서 `patch("date")` 제거 → `today=date(2026, 2, 8)` 직접 전달로 간결화 ### Finding 3: 미사용 import + lint **수정 완료.** `UTC`, `datetime`, `PlaybookStatus` import 제거. 프롬프트 line-too-long 수정. `ruff check` 통과.
Author
Collaborator

추가 리뷰 응답

Finding 1: ContextLayer re-export 확인

거짓 양성 맞습니다. src/context/store.py:10에 다음이 있습니다:

from src.context.layer import LAYER_CONFIG, ContextLayer

Python에서 module-level import된 이름은 __all__ 없이도 해당 모듈에서 import 가능합니다. 런타임 확인:

>>> from src.context.store import ContextLayer
>>> ContextLayer.L6_DAILY
ContextLayer.L6_DAILY  # 정상 동작

변경 불필요.

Finding 2: today 호출부 전파

동의합니다. Issue 1-7 (main.py 통합)에서 markets 모듈의 시장별 로컬 날짜를 today=로 전달하는 코드를 추가할 예정입니다. 플랜에 이미 반영 완료.

## 추가 리뷰 응답 ### Finding 1: ContextLayer re-export 확인 **거짓 양성 맞습니다.** `src/context/store.py:10`에 다음이 있습니다: ```python from src.context.layer import LAYER_CONFIG, ContextLayer ``` Python에서 module-level import된 이름은 `__all__` 없이도 해당 모듈에서 import 가능합니다. 런타임 확인: ```python >>> from src.context.store import ContextLayer >>> ContextLayer.L6_DAILY ContextLayer.L6_DAILY # 정상 동작 ``` 변경 불필요. ### Finding 2: today 호출부 전파 동의합니다. Issue 1-7 (main.py 통합)에서 `markets` 모듈의 시장별 로컬 날짜를 `today=`로 전달하는 코드를 추가할 예정입니다. 플랜에 이미 반영 완료.
jihoson merged commit 6fba7c7ae8 into main 2026-02-08 22:07:36 +09:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jihoson/The-Ouroboros#109