1121 lines
61 KiB
YAML
1121 lines
61 KiB
YAML
# =============================================================================
|
||
# QuantEngine 데이터 실증 기반 퀀트 엔진 로드맵 + WBS (기계 판정)
|
||
# =============================================================================
|
||
# formula_id: QUANT_ENGINE_WBS_V1
|
||
# 원칙: 모든 작업(task)은 "완료 주장"이 아니라 게이트 실행으로만 DONE 판정된다.
|
||
# - BE 실증: pg_query(PostgreSQL 쿼리) + log_pattern(Serilog 로그) + json_gate(아티팩트)
|
||
# - FE 실증: playwright_report(스펙 PASS) + file_exists(스크린샷)
|
||
# 실행:
|
||
# 단일 작업 검증: python tools/verify_wbs_task_v1.py --task <TASK_ID>
|
||
# → Temp/evidence/<TASK_ID>/verdict.json + 원시 증거 보존
|
||
# 전체 WBS 게이트: python tools/validate_quant_engine_wbs_v1.py
|
||
# → Temp/quant_engine_wbs_v1.json (status=DONE 작업의 증거 재검증)
|
||
# 관례: spec/16_data_gaps_roadmap.yaml 의 success_criteria 구조
|
||
# (expected_success_value / evidence_artifacts / verification_commands) 준수.
|
||
# 검증 로직만 하드코딩 → evidence_checks 선언형으로 일반화.
|
||
# =============================================================================
|
||
|
||
meta:
|
||
formula_id: QUANT_ENGINE_WBS_V1
|
||
version: 1
|
||
created: "2026-07-12"
|
||
authority: "governance/authority_matrix.yaml"
|
||
validator: tools/validate_quant_engine_wbs_v1.py
|
||
task_verifier: tools/verify_wbs_task_v1.py
|
||
remote_evidence_collector: tools/collect_remote_wbs_evidence_v1.py
|
||
evidence_root: Temp/evidence
|
||
status_values: [PENDING, IN_PROGRESS, DONE] # DONE = 해당 verdict.json gate=PASS 필수
|
||
db_connection:
|
||
# 검증기의 PostgreSQL 접속 순서:
|
||
# 1) env QE_WBS_PG_DSN (psycopg DSN)
|
||
# 2) env ConnectionStrings__DefaultConnection (.NET 형식 → 자동 변환)
|
||
# 3) src/dotnet/QuantEngine.Web/appsettings.Development.json 의 ConnectionStrings.DefaultConnection
|
||
# (로컬은 SSH 터널 127.0.0.1:5432 전제 — CLAUDE.md "Local Development & Testing")
|
||
dotnet_appsettings: src/dotnet/QuantEngine.Web/appsettings.Development.json
|
||
remote_evidence:
|
||
collector: "python tools/collect_remote_wbs_evidence_v1.py --target <ssh-target>"
|
||
policy: "Collect journal and JSON artifacts only; never copy env files or passwords."
|
||
postgres: "Use QE_WBS_PG_DSN through an approved SSH tunnel; do not embed credentials in evidence."
|
||
execution_convention: >
|
||
각 작업의 execution.haiku_prompt 필드는 Agent(model: haiku)에 그대로 전달 가능한
|
||
자기완결적 지시문이다(파일 경로, 정확한 수정 내용, acceptance 커맨드 포함). 조정자는
|
||
haiku 결과 diff를 반드시 리뷰한 뒤 verify_wbs_task_v1.py 게이트 PASS 시에만
|
||
status: DONE 으로 갱신한다. execution.mode: manual_user_action 작업(예: QE-M1-07)은
|
||
Gitea Actions workflow_dispatch 등 에이전트가 트리거할 수 없는 행위이므로 haiku_prompt
|
||
없이 instructions 필드만 갖는다. haiku_prompt는 실행 직전 작업에만 채우며 미리 전부
|
||
작성하지 않는다(과설계 방지).
|
||
|
||
# -----------------------------------------------------------------------------
|
||
# 검증 체크 타입 사전 (verify_wbs_task_v1.py 가 해석하는 선언형 vocabulary)
|
||
# -----------------------------------------------------------------------------
|
||
evidence_check_types:
|
||
pg_query: "PostgreSQL 쿼리 1개 실행, 단일 스칼라 결과를 expect{min,max,equals}와 비교. 원시 결과를 pg_query_<n>.json 으로 보존"
|
||
log_pattern: "file_glob 로그 파일들에서 정규식 매칭. expect{min_matches, max_age_hours(파일 mtime 기준)}. 매칭 라인을 log_excerpt.txt 로 보존"
|
||
json_gate: "path 의 JSON 아티팩트에서 expect 의 키-값 검사 (점 표기 경로 지원, 값 '>=N' 비교 지원)"
|
||
file_exists: "paths 의 모든 파일 존재 (expect.min_bytes 선택)"
|
||
playwright_report: "Playwright JSON 리포트(report)에서 spec_file 의 결과가 expect{passed_min, failed} 충족"
|
||
|
||
# =============================================================================
|
||
# 로드맵 (M0 → M5)
|
||
# =============================================================================
|
||
roadmap:
|
||
scope_note: >
|
||
전통 팩터(모멘텀/거래량/수급/실적/매크로/밸류/재무건전성 = spec/08_scoring_rules.yaml SS001)
|
||
+ ATR 리스크 관리 기본 포함. 최신 기법은 레짐 감지 + 워크포워드 캘리브레이션 + 거래비용
|
||
반영 평가로 한정(사용자 확정, 2026-07-12). 딥러닝/인트라데이/대체데이터/실거래 집행 제외
|
||
(은퇴자산 + read-only KIS 거버넌스: governance/rules/06_no_direct_api_trading.yaml 유지).
|
||
phases:
|
||
M0:
|
||
name: "실증 하네스 + 정직성 정리"
|
||
goal: "완료 주장이 불가능한 구조 확립 — 검증기/증거 규약/CI 편입 + 가짜 검증 제거"
|
||
exit_gate: "validate_quant_engine_wbs 가 release DAG/CI 노드로 PASS; dotnet test + Playwright evidence 스위트 CI 편입; 디버그 스펙 격리·가짜 PASS 제거; 중복/가짜 게이트 재발 없음"
|
||
tasks: [QE-M0-01, QE-M0-02, QE-M0-03, QE-M0-04, QE-M0-05, QE-M0-06, QE-M0-07]
|
||
M1:
|
||
name: "수집 파이프라인 배선"
|
||
goal: "운영 앱이 실제 KIS 데이터를 수집하도록 고아 오케스트레이터 배선 (첫 실데이터 실증)"
|
||
exit_gate: "Hangfire daily-collection + POST /api/collection/run 으로 kis_collection_* 에 실데이터 적재, Admin Collection 페이지 Playwright 실증"
|
||
tasks: [QE-M1-01, QE-M1-02, QE-M1-03, QE-M1-04, QE-M1-05, QE-M1-06, QE-M1-07]
|
||
M2:
|
||
name: "히스토리 시계열 저장소"
|
||
goal: "모멘텀 팩터·백테스트의 전제인 일봉/매크로 시계열 축적 (2년 백필)"
|
||
exit_gate: "price_history_daily/macro_history_daily 에 유니버스 2년치; (ticker,date) 중복 0; 거래일 캘린더 대비 gap 0"
|
||
tasks: [QE-M2-01, QE-M2-02, QE-M2-03, QE-M2-04, QE-M2-05, QE-M2-06]
|
||
M3:
|
||
name: "실데이터 팩터 계산"
|
||
goal: "SS001 전통 팩터를 PG 히스토리에서 계산해 engine_history 에 적재, 파일 개수 골든커버리지를 수치 패리티로 대체"
|
||
exit_gate: "factor_output_history 에 유니버스 전체 스코어(0-100); Python 참조 대비 패리티 ≥20 formula tol 1e-9 PASS"
|
||
tasks: [QE-M3-01, QE-M3-02, QE-M3-03, QE-M3-04, QE-M3-05]
|
||
M4:
|
||
name: "백테스팅 + 검증"
|
||
goal: "point-in-time 데이터만 사용하는 워크포워드 백테스터 + 거래비용 모델 + no-lookahead 게이트 실배선"
|
||
exit_gate: "Sharpe/MDD/턴오버 JSON 산출; no-lookahead 정상 PASS + 오염 픽스처 FAIL 양방향; T+5/T+20 원장 표본 ≥30"
|
||
tasks: [QE-M4-01, QE-M4-02, QE-M4-03, QE-M4-04, QE-M4-05]
|
||
M5:
|
||
name: "포트폴리오 구성 + 최신 기법"
|
||
goal: "레짐 감지 + SS001 가중치 워크포워드 캘리브레이션(제약+shrinkage) + 변동성 타게팅 사이징"
|
||
exit_gate: "백필 전 기간 레짐 라벨; 캘리브레이션 가중치 제약 준수 + OOS Sharpe 정직 보고; 최종 포트폴리오 패킷 캡 준수"
|
||
tasks: [QE-M5-01, QE-M5-02, QE-M5-03, QE-M5-04]
|
||
|
||
# =============================================================================
|
||
# WBS 작업 목록
|
||
# =============================================================================
|
||
tasks:
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# M0 — 실증 하네스 + 정직성 정리
|
||
# ---------------------------------------------------------------------------
|
||
QE-M0-01:
|
||
title: "WBS 스펙(YAML) + 로드맵 작성, 레거시 로드맵 문서에 포인터 추가"
|
||
status: DONE
|
||
depends_on: []
|
||
owner_files:
|
||
- spec/60_quant_engine_wbs.yaml
|
||
- docs/ROADMAP_WBS.md
|
||
success_criteria:
|
||
expected_success_value: { spec_exists: true, legacy_pointer_appended: true }
|
||
evidence_artifacts: [Temp/evidence/QE-M0-01/verdict.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M0-01"]
|
||
evidence_checks:
|
||
- type: file_exists
|
||
paths: [spec/60_quant_engine_wbs.yaml]
|
||
expect: { min_bytes: 10000 }
|
||
- type: log_pattern
|
||
file_glob: docs/ROADMAP_WBS.md
|
||
pattern: "QUANT_ENGINE_WBS_V1"
|
||
expect: { min_matches: 1 }
|
||
|
||
QE-M0-02:
|
||
title: "증거 검증기 2종 구현 (단일 작업 verifier + 전체 WBS validator) + 유닛테스트"
|
||
status: DONE
|
||
depends_on: []
|
||
owner_files:
|
||
- tools/verify_wbs_task_v1.py
|
||
- tools/validate_quant_engine_wbs_v1.py
|
||
- tests/unit/test_validate_quant_engine_wbs_v1.py
|
||
success_criteria:
|
||
expected_success_value: { self_test: PASS, synthetic_pass_fail_bidirectional: true }
|
||
evidence_artifacts: [Temp/evidence/QE-M0-02/verdict.json, Temp/quant_engine_wbs_v1.json]
|
||
verification_commands:
|
||
- "python -m pytest tests/unit/test_validate_quant_engine_wbs_v1.py -q"
|
||
- "python tools/verify_wbs_task_v1.py --task QE-M0-02"
|
||
evidence_checks:
|
||
- type: file_exists
|
||
paths:
|
||
- tools/verify_wbs_task_v1.py
|
||
- tools/validate_quant_engine_wbs_v1.py
|
||
- tests/unit/test_validate_quant_engine_wbs_v1.py
|
||
- type: log_pattern
|
||
file_glob: tools/validate_quant_engine_wbs_v1.py
|
||
pattern: "def main"
|
||
expect: { min_matches: 1 }
|
||
|
||
QE-M0-03:
|
||
title: "Playwright 정직성 정리 + evidence 프로젝트 + npm 스크립트"
|
||
status: DONE
|
||
depends_on: [QE-M0-02]
|
||
owner_files:
|
||
- playwright.config.ts
|
||
- package.json
|
||
- tests/e2e/archive/
|
||
notes: >
|
||
디버그 스펙(~17개: debug-login, html-debug, wasm-test, framework-check, console-check,
|
||
screenshot-diagnosis, inspect-page, login* 변형 등)을 tests/e2e/archive/ 로 이동하고
|
||
testIgnore 로 제외. full-validation.spec.ts 의 assert 없는 가짜 "[PASS]" 배너 테스트
|
||
제거(파일째 archive). evidence 프로젝트: testDir tests/e2e/evidence, screenshot 'on',
|
||
trace 'on', JSON reporter → Temp/evidence/playwright-last-run.json.
|
||
npm 스크립트: verify:task / verify:wbs / test:e2e / test:evidence
|
||
success_criteria:
|
||
expected_success_value: { default_project_specs: ["admin-pages.spec.ts"], fake_pass_removed: true }
|
||
evidence_artifacts: [Temp/evidence/QE-M0-03/verdict.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M0-03"]
|
||
evidence_checks:
|
||
- type: file_exists
|
||
paths: [tests/e2e/archive]
|
||
- type: log_pattern
|
||
file_glob: playwright.config.ts
|
||
pattern: "evidence"
|
||
expect: { min_matches: 1 }
|
||
- type: log_pattern
|
||
file_glob: package.json
|
||
pattern: "verify:task"
|
||
expect: { min_matches: 1 }
|
||
- type: log_pattern
|
||
file_glob: tests/e2e/full-validation.spec.ts
|
||
pattern: ".*"
|
||
expect: { max_matches: 0 } # 파일이 기본 testDir 에 더 이상 존재하지 않아야 함
|
||
|
||
QE-M0-04:
|
||
title: "CI에 dotnet test 편입 + 고아 QuantEngine.Web.Tests 처리"
|
||
status: DONE
|
||
depends_on: []
|
||
owner_files:
|
||
- .gitea/workflows/ci.yml
|
||
- src/dotnet/QuantEngine.Web.Tests/
|
||
notes: >
|
||
QuantEngine.Web.Tests/DashboardComponentTests.cs 는 csproj 없는 고아(폐기된 Blazor 대상).
|
||
현 Razor Pages UI 에 맞지 않으면 삭제. ci.yml 에 dotnet test 스텝 추가.
|
||
success_criteria:
|
||
expected_success_value: { ci_has_dotnet_test: true, core_tests_green: true }
|
||
evidence_artifacts: [Temp/evidence/QE-M0-04/verdict.json]
|
||
verification_commands:
|
||
- "dotnet test src/dotnet/QuantEngine.Core.Tests/QuantEngine.Core.Tests.csproj -c Release --nologo"
|
||
- "python tools/verify_wbs_task_v1.py --task QE-M0-04"
|
||
evidence_checks:
|
||
- type: log_pattern
|
||
file_glob: .gitea/workflows/ci.yml
|
||
pattern: "dotnet test"
|
||
expect: { min_matches: 1 }
|
||
|
||
QE-M0-05:
|
||
title: "release DAG + CI 에 validate_quant_engine_wbs 게이트 노드 등록"
|
||
status: DONE
|
||
depends_on: [QE-M0-02]
|
||
owner_files:
|
||
- spec/41_release_dag.yaml
|
||
- .gitea/workflows/ci.yml
|
||
success_criteria:
|
||
expected_success_value: { dag_node: validate_quant_engine_wbs, ci_step: true }
|
||
evidence_artifacts: [Temp/evidence/QE-M0-05/verdict.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M0-05"]
|
||
evidence_checks:
|
||
- type: log_pattern
|
||
file_glob: spec/41_release_dag.yaml
|
||
pattern: "validate_quant_engine_wbs"
|
||
expect: { min_matches: 1 }
|
||
- type: log_pattern
|
||
file_glob: .gitea/workflows/ci.yml
|
||
pattern: "validate_quant_engine_wbs_v1"
|
||
expect: { min_matches: 1 }
|
||
|
||
QE-M0-06:
|
||
title: "골든커버리지 정직성 표기 (coverage_basis: FILE_COUNT_ONLY)"
|
||
status: DONE
|
||
depends_on: []
|
||
owner_files:
|
||
- tools/validate_golden_coverage_100.py
|
||
notes: >
|
||
골든 테스트 174개는 실행되지 않는 placeholder. 커버리지 판정 출력에
|
||
coverage_basis: FILE_COUNT_ONLY 필드를 추가해 실체를 명시(삭제는 M3 패리티 대체 후).
|
||
success_criteria:
|
||
expected_success_value: { honesty_field: FILE_COUNT_ONLY }
|
||
evidence_artifacts: [Temp/evidence/QE-M0-06/verdict.json]
|
||
verification_commands:
|
||
- "python tools/validate_golden_coverage_100.py"
|
||
- "python tools/verify_wbs_task_v1.py --task QE-M0-06"
|
||
evidence_checks:
|
||
- type: json_gate
|
||
path: Temp/golden_coverage_100_v1.json
|
||
expect: { coverage_basis: FILE_COUNT_ONLY }
|
||
|
||
QE-M0-07:
|
||
title: "schemas/generated + models/generated 중복 스키마 레이어 폐기"
|
||
status: DONE
|
||
depends_on: []
|
||
owner_files:
|
||
- schemas/generated/
|
||
- src/quant_engine/models/generated/
|
||
- .gitea/workflows/ci.yml
|
||
- spec/41_release_dag.yaml
|
||
- docs/ROADMAP_WBS.md
|
||
notes: >
|
||
비판적 재검토(2026-07-12)에서 발견: schemas/generated/(174) + models/generated/(347)가
|
||
기존 runtime/python/core/formulas/generated/(172 stub)와 동일 목적을 범용 wrapper로
|
||
중복 구현, validate_schema_model_generation_v1.py는 파일 개수만 세는 가짜 게이트였음
|
||
(M0가 확립한 "가짜 게이트 금지" 원칙의 재발 사례). 삭제하고 ci.yml/release DAG의
|
||
관련 스텝·노드(build_schema_models, validate_schema_model) 제거.
|
||
schemas/generated/gas_adapter_contract.schema.json 은 별개 목적(GAS 어댑터 계약)이라 보존.
|
||
success_criteria:
|
||
expected_success_value: { duplicate_layer_removed: true, gas_adapter_schema_preserved: true }
|
||
evidence_artifacts: [Temp/evidence/QE-M0-07/verdict.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M0-07"]
|
||
evidence_checks:
|
||
- type: file_exists
|
||
paths: [schemas/generated/gas_adapter_contract.schema.json]
|
||
- type: log_pattern
|
||
file_glob: .gitea/workflows/ci.yml
|
||
pattern: 'validate_schema_model_generation_v1|generate_schema_model_generation_evidence_v1'
|
||
expect: { max_matches: 0 }
|
||
- type: log_pattern
|
||
file_glob: spec/41_release_dag.yaml
|
||
pattern: 'build_schema_models|validate_schema_model'
|
||
expect: { max_matches: 0 }
|
||
- type: log_pattern
|
||
file_glob: docs/ROADMAP_WBS.md
|
||
pattern: '폐기: schemas/generated'
|
||
expect: { min_matches: 1 }
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# M1 — 수집 파이프라인 배선 (첫 실데이터 실증)
|
||
# ---------------------------------------------------------------------------
|
||
QE-M1-01:
|
||
title: "KisDataCollectionOrchestrator DI 등록 + daily-collection Hangfire 잡 실구현"
|
||
status: DONE
|
||
depends_on: [QE-M0-02]
|
||
owner_files:
|
||
- src/dotnet/QuantEngine.Web/Program.cs
|
||
- src/dotnet/QuantEngine.Web/Services/SchedulerService.cs
|
||
notes: >
|
||
Program.cs 에 PriceDataNormalizer / SourcePriorityResolver / ICollectionOrchestrator →
|
||
KisDataCollectionOrchestrator 등록. RunDailyCollectionAsync 의 Task.Delay 시뮬레이션을
|
||
IServiceScopeFactory 스코프 → 오케스트레이터 호출로 교체 (runId "daily-yyyyMMdd-HHmmss").
|
||
완료 로그: "Collection run {RunId} completed: {Snapshots} snapshots, {Errors} errors".
|
||
상태값은 대문자 COMPLETED / COMPLETED_WITH_ERRORS (KisDataCollectionOrchestrator.cs:103).
|
||
success_criteria:
|
||
expected_success_value: { runs_completed_min: 1, snapshots_min: 5, hangfire_job: daily-collection }
|
||
evidence_artifacts: [Temp/evidence/QE-M1-01/verdict.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M1-01"]
|
||
evidence_checks:
|
||
- type: pg_query
|
||
sql: >
|
||
SELECT count(*) FROM quantengine.kis_collection_runs
|
||
WHERE status LIKE 'COMPLETED%' AND total_snapshots >= 5
|
||
AND started_at >= (now() - interval '24 hours')::text
|
||
expect: { min: 1 }
|
||
- type: pg_query
|
||
sql: >
|
||
SELECT count(DISTINCT s.ticker) FROM quantengine.kis_collection_snapshots s
|
||
JOIN quantengine.kis_collection_runs r ON r.run_id = s.run_id
|
||
WHERE r.started_at >= (now() - interval '24 hours')::text
|
||
expect: { min: 5 }
|
||
- type: log_pattern
|
||
file_glob: src/dotnet/QuantEngine.Web/logs/quantengine-*.log
|
||
# 2026-07-12 정정: 수집은 두 경로로 트리거될 수 있다 —
|
||
# (a) SchedulerService.RunDailyCollectionAsync (매일 09:00 cron) → "Collection run {Id} completed: {N} snapshots, {N} errors"
|
||
# (b) POST /api/collection/run → Hangfire enqueue → 오케스트레이터 자체 완료 로그 → "Collection run {Id} finished with status {Status}: {N} ok, {N} errors"
|
||
# 둘 다 동일한 오케스트레이터/DI/PG 쓰기 경로를 타는 동등한 실증이므로 둘 다 인정.
|
||
pattern: 'Collection run .+ (completed: \d+ snapshots|finished with status \w+: \d+ ok)'
|
||
expect: { min_matches: 1, max_age_hours: 24 }
|
||
- type: json_gate
|
||
path: Temp/kis_dotnet_collection_v1.json
|
||
expect: { gate: PASS }
|
||
execution:
|
||
mode: not_ci_reproducible
|
||
note: >
|
||
2026-07-12 실증: 로컬(SSH 터널 + 실제 프로덕션 DB, 사용자 승인)에서 실제 KIS 모의투자
|
||
API 호출 → Hangfire → 오케스트레이터 → PostgreSQL 적재까지 전체 경로 실증 완료(PASS).
|
||
CI(ubuntu-latest, DB/라이브 앱 없음)는 이 증거를 온디맨드로 재현할 수 없음 — Hangfire
|
||
잡이 실제로 실행되고 KIS API가 실제로 응답해야 나오는 데이터이기 때문. QE-M1-07(수동
|
||
배포)과 동일 범주: 코드는 CI에서 빌드/유닛테스트로 검증되고, 데이터 무결성은 이
|
||
로컬 실증 기록으로 남는다.
|
||
|
||
QE-M1-02:
|
||
title: "Admin Collection 페이지 FE 실증 (실제 run 렌더링을 Playwright 로 증명)"
|
||
status: DONE
|
||
depends_on: [QE-M1-01, QE-M0-03]
|
||
owner_files:
|
||
- tests/e2e/evidence/qe-m1-02-collection-run.spec.ts
|
||
notes: >
|
||
필수 3요소: (a) 기대값을 /api/collection/runs API 에서 조회(하드코딩 금지),
|
||
(b) /Admin/Collection DOM 에서 run_id·스냅샷 수·상태 배지를 기대값과 assert,
|
||
(c) assert 시점 스크린샷 → Temp/evidence/QE-M1-02/screenshots/{01-collection-page,02-run-detail}.png
|
||
success_criteria:
|
||
expected_success_value: { spec_passed: 1, screenshots: 2, dom_equals_api: true }
|
||
evidence_artifacts: [Temp/evidence/QE-M1-02/verdict.json]
|
||
verification_commands:
|
||
- "npx playwright test --project=evidence tests/e2e/evidence/qe-m1-02-collection-run.spec.ts"
|
||
- "python tools/verify_wbs_task_v1.py --task QE-M1-02"
|
||
evidence_checks:
|
||
- type: playwright_report
|
||
report: Temp/evidence/playwright-last-run.json
|
||
spec_file: qe-m1-02-collection-run.spec.ts
|
||
expect: { passed_min: 1, failed: 0 }
|
||
- type: file_exists
|
||
paths:
|
||
- Temp/evidence/QE-M1-02/screenshots/01-collection-page.png
|
||
- Temp/evidence/QE-M1-02/screenshots/02-run-detail.png
|
||
expect: { min_bytes: 10000 }
|
||
execution:
|
||
mode: not_ci_reproducible
|
||
note: >
|
||
2026-07-12 로컬 실증(Playwright, 실제 DOM=API 대조 + 스크린샷 2장) PASS.
|
||
라이브 앱 + 실제 수집 run 데이터가 전제라 CI에서 온디맨드 재현 불가 (QE-M1-01 참조).
|
||
|
||
QE-M1-03:
|
||
title: "POST /api/collection/run 실구현 (BackgroundJob.Enqueue + 인증 필수화)"
|
||
status: DONE
|
||
depends_on: [QE-M1-01]
|
||
owner_files:
|
||
- src/dotnet/QuantEngine.Web/Endpoints/CollectionEndpoints.cs
|
||
notes: >
|
||
202 no-op 스텁을 Hangfire BackgroundJob.Enqueue(오케스트레이터 실행)로 교체,
|
||
응답에 {runId} 포함. AllowAnonymous 제거(쿠키 인증).
|
||
로그: "Collection run {RunId} enqueued".
|
||
success_criteria:
|
||
expected_success_value: { returns_run_id: true, auth_required: true }
|
||
evidence_artifacts: [Temp/evidence/QE-M1-03/verdict.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M1-03"]
|
||
evidence_checks:
|
||
- type: log_pattern
|
||
file_glob: src/dotnet/QuantEngine.Web/logs/quantengine-*.log
|
||
pattern: 'Collection run .+ enqueued'
|
||
expect: { min_matches: 1, max_age_hours: 24 }
|
||
- type: pg_query
|
||
sql: >
|
||
SELECT count(*) FROM quantengine.kis_collection_runs
|
||
WHERE run_id LIKE 'api-%' AND started_at >= (now() - interval '24 hours')::text
|
||
expect: { min: 1 }
|
||
execution:
|
||
mode: not_ci_reproducible
|
||
note: >
|
||
2026-07-12 로컬 실증: POST /api/collection/run 202 Accepted, run_id 실제 PG 기록 확인.
|
||
인증된 라이브 앱 세션이 전제라 CI에서 온디맨드 재현 불가 (QE-M1-01 참조).
|
||
|
||
QE-M1-04:
|
||
title: "오케스트레이터 로깅 복원 + 출력 아티팩트 표준화 + 멀티소스 폴백 배선"
|
||
status: DONE
|
||
depends_on: [QE-M1-01]
|
||
owner_files:
|
||
- src/dotnet/QuantEngine.Application/Services/KisDataCollectionOrchestrator.cs
|
||
notes: >
|
||
"// Log: skipped" → ILogger<KisDataCollectionOrchestrator> 복원.
|
||
출력 경로 Path.GetTempPath() → <repo>/Temp/kis_dotnet_collection_v1.json,
|
||
형식 {formula_id: KIS_DOTNET_COLLECTION_V1, gate, summary{success_count, error_count, source_counts}}.
|
||
SourcePriorityResolver 를 통해 Naver/Yahoo 폴백 경로 활성화.
|
||
success_criteria:
|
||
expected_success_value: { gate: PASS, source_counts_min: 1, error_rows_on_bad_ticker: true }
|
||
evidence_artifacts: [Temp/evidence/QE-M1-04/verdict.json, Temp/kis_dotnet_collection_v1.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M1-04"]
|
||
evidence_checks:
|
||
- type: json_gate
|
||
path: Temp/kis_dotnet_collection_v1.json
|
||
expect: { formula_id: KIS_DOTNET_COLLECTION_V1, gate: PASS }
|
||
- type: log_pattern
|
||
file_glob: src/dotnet/QuantEngine.Web/logs/quantengine-*.log
|
||
pattern: 'Collecting ticker'
|
||
expect: { min_matches: 1, max_age_hours: 24 }
|
||
execution:
|
||
mode: not_ci_reproducible
|
||
note: >
|
||
2026-07-12 로컬 실증: Temp/kis_dotnet_collection_v1.json gate=PASS, "Collecting ticker"
|
||
로그 확인. 실제 오케스트레이터 실행이 전제라 CI에서 온디맨드 재현 불가 (QE-M1-01 참조).
|
||
|
||
QE-M1-05:
|
||
title: "티커 유니버스를 GatherTradingData 파서/DB 설정에서 로드 (하드코딩 제거)"
|
||
status: DONE
|
||
depends_on: []
|
||
# 2026-07-12 정정: 원래 [QE-M1-01] 로 선언했으나, 이 작업은 M1-01의 "코드"(이미 병합됨)만
|
||
# 필요했지 M1-01의 "실증 완료(DONE)"까지는 필요 없었다. M1-01은 여전히 PENDING(프로덕션
|
||
# 재배포 차단, QE-M1-07 참조)이지만 M1-05는 코드 레벨 게이트로 독립적으로 PASS했다.
|
||
owner_files:
|
||
- src/dotnet/QuantEngine.Web/Services/SchedulerService.cs
|
||
success_criteria:
|
||
expected_success_value: { distinct_tickers_equals_universe: true }
|
||
evidence_artifacts: [Temp/evidence/QE-M1-05/verdict.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M1-05"]
|
||
evidence_checks:
|
||
- type: log_pattern
|
||
file_glob: src/dotnet/QuantEngine.Web/Services/SchedulerService.cs
|
||
pattern: '005930.+000660.+051910'
|
||
expect: { max_matches: 0 } # 하드코딩 티커 배열 부재
|
||
execution:
|
||
haiku_prompt: |
|
||
Repo: C:\Temp\data_feed, .NET solution at src/dotnet (net10.0). Task: WBS QE-M1-05 —
|
||
remove the hardcoded 6-ticker array in SchedulerService.RunDailyCollectionAsync and load
|
||
the ticker universe from GatherTradingData.json via the existing GatherTradingDataParser.
|
||
|
||
READ first:
|
||
- src/dotnet/QuantEngine.Web/Services/SchedulerService.cs (RunDailyCollectionAsync, ~line 91:
|
||
`var tickers = new[] { "005930", "000660", "051910", "005380", "010140", "005490" };`)
|
||
- src/dotnet/QuantEngine.Application/Services/GatherTradingDataParser.cs (public API:
|
||
`List<Dictionary<string,object>> ParseGatherTradingData(string jsonFilePath)`; each row has
|
||
a `"Ticker"` key)
|
||
- src/dotnet/QuantEngine.Application/Services/KisDataCollectionOrchestrator.cs, method
|
||
`GetOutputPath()` (~line 188): the exact "walk up from AppContext.BaseDirectory looking for
|
||
a `.git` directory or `GatherTradingData.json`" pattern already used elsewhere in this repo —
|
||
reuse this same pattern to locate `GatherTradingData.json`'s absolute path, don't invent a
|
||
new one.
|
||
- src/dotnet/QuantEngine.Web/Program.cs (confirm whether GatherTradingDataParser is already
|
||
DI-registered; if not, register it `AddScoped<GatherTradingDataParser>()` near the other
|
||
collection-pipeline registrations).
|
||
|
||
Changes (SchedulerService.cs only, plus Program.cs DI registration if needed):
|
||
1. Inject `GatherTradingDataParser` via constructor (keep existing params).
|
||
2. Add a private method (or reuse the walk-up pattern inline) that locates
|
||
`<repoRoot>/GatherTradingData.json`; if not found, fall back to the current hardcoded
|
||
6-ticker array with a LogWarning ("GatherTradingData.json not found, falling back to
|
||
default universe") — do not throw and break the daily job.
|
||
3. In `RunDailyCollectionAsync`, replace the hardcoded array: call
|
||
`_parser.ParseGatherTradingData(path)`, extract each row's `"Ticker"` value (cast to
|
||
string, skip null/empty), de-duplicate, and use that as `tickers`. Log the resolved
|
||
count: `_logger.LogInformation("Loaded {Count} tickers from GatherTradingData.json", tickers.Count);`
|
||
|
||
Acceptance (run from repo root, report output):
|
||
1. `dotnet build src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj -c Release --nologo` → 0 errors.
|
||
2. `grep -n "005930.+000660.+051910" src/dotnet/QuantEngine.Web/Services/SchedulerService.cs`
|
||
(or equivalent) should find NOTHING (the literal hardcoded sequence must be gone — a
|
||
fallback array is fine as long as it isn't reached in the normal path, but simplest is to
|
||
just not have that exact 6-ticker literal sequence in the file at all — e.g. keep a fallback
|
||
of a single default ticker or move the fallback list to configuration).
|
||
3. `git diff --stat`.
|
||
Do not modify CollectionEndpoints.cs, KisDataCollectionOrchestrator.cs, or any other file.
|
||
Match existing code style (minimal comments, file-scoped namespace if already used).
|
||
|
||
QE-M1-06:
|
||
title: "LogLineageEvent 침묵 예외 수정 + 신규 캐싱/lineage 로직 유닛테스트"
|
||
status: DONE
|
||
depends_on: []
|
||
# 2026-07-12 정정: QE-M1-05와 동일한 사유로 [QE-M1-01] 의존성 제거 — 코드 레벨
|
||
# 하드닝 작업이라 M1-01의 실증 완료를 전제하지 않는다.
|
||
owner_files:
|
||
- src/dotnet/QuantEngine.Application/Services/KisDataCollectionOrchestrator.cs
|
||
- src/dotnet/QuantEngine.Core.Tests/
|
||
notes: >
|
||
비판적 재검토(2026-07-12)에서 발견: 캐시 히트(IsMarketClosed 기반)와 LogLineageEvent가
|
||
외부에서 추가됐으나 테스트 0건. LogLineageEvent의 catch{ /* Robust fallback */ }가 예외를
|
||
완전 침묵 처리(로그도 안 남김) — lineage 무결성 실패가 운영에서 보이지 않는 사각지대.
|
||
LogLineageEvent는 현재 private static이라 인스턴스 필드 _logger에 접근 불가 — 인스턴스
|
||
메서드로 전환 필요.
|
||
success_criteria:
|
||
expected_success_value: { lineage_exception_logged: true, new_tests_min: 3 }
|
||
evidence_artifacts: [Temp/evidence/QE-M1-06/verdict.json]
|
||
verification_commands:
|
||
- "dotnet test src/dotnet/QuantEngine.Core.Tests/QuantEngine.Core.Tests.csproj -c Release --filter FullyQualifiedName~IsMarketClosed|FullyQualifiedName~LogLineage|FullyQualifiedName~CacheHit"
|
||
- "python tools/verify_wbs_task_v1.py --task QE-M1-06"
|
||
evidence_checks:
|
||
- type: log_pattern
|
||
file_glob: src/dotnet/QuantEngine.Application/Services/KisDataCollectionOrchestrator.cs
|
||
pattern: '_logger\.LogWarning\(ex, "Failed to write lineage event'
|
||
expect: { min_matches: 1 }
|
||
# 참고: verify_wbs_task_v1.py 의 log_pattern 은 라인 단위 매칭이라 catch/{ 를
|
||
# 포함한 멀티라인 패턴은 매치되지 않는다(2026-07-12 QE-M1-06 실행 중 발견,
|
||
# 원본 패턴으로 수정). LogWarning 호출 한 줄만 대상으로 검사.
|
||
- type: log_pattern
|
||
file_glob: src/dotnet/QuantEngine.Core.Tests/**/*.cs
|
||
pattern: 'IsMarketClosed|LogLineageEvent|Cached'
|
||
expect: { min_matches: 3 }
|
||
execution:
|
||
haiku_prompt: |
|
||
Repo: C:\Temp\data_feed, .NET solution at src/dotnet (net10.0, xUnit tests in
|
||
QuantEngine.Core.Tests). Task: WBS QE-M1-06 — fix a silent-exception bug and add missing
|
||
unit tests for recently-added logic in KisDataCollectionOrchestrator.
|
||
|
||
READ FIRST (whole file):
|
||
src/dotnet/QuantEngine.Application/Services/KisDataCollectionOrchestrator.cs
|
||
|
||
You'll find (as of now, ~264 lines):
|
||
- `IsMarketClosed()` (private static bool, ~line 208): true if KST weekend, or KST time
|
||
outside 09:00:00–15:30:00 (KST = `DateTime.UtcNow.AddHours(9)`, no DST — fine for Korea).
|
||
- A cache-hit branch inside `RunCollectionAsync`'s per-ticker loop (~line 60-69): when
|
||
`IsMarketClosed()` is true, calls `_repository.GetLatestSnapshotsForTickerAsync(ticker, 1)`
|
||
and reuses that day's snapshot (source name suffixed `" (Cached)"`) instead of hitting the
|
||
live KIS API.
|
||
- `LogLineageEvent(string runId, string status, int successCount, int errorCount)` (private
|
||
**static** void, ~line 225): walks up from AppContext.BaseDirectory to find the repo root
|
||
(a `.git` directory), appends one JSON line to `<repoRoot>/runtime/lineage_events.jsonl`.
|
||
Wrapped in `try { ... } catch { /* Robust fallback */ }` — **any exception (I/O error, no
|
||
repo root found, etc.) is silently swallowed with zero logging.**
|
||
|
||
## Fix 1 — stop swallowing the exception silently
|
||
Change `LogLineageEvent` from `private static void` to a private **instance** method (so it
|
||
can use the instance field `_logger`). Update its single call site (~line 171,
|
||
`LogLineageEvent(runId, result.Status, result.SuccessCount, result.ErrorCount);`) — it's
|
||
already called from an instance method (`RunCollectionAsync`), so removing `static` from the
|
||
signature only and calling it the same way (`LogLineageEvent(...)` — implicit `this`) is a
|
||
pure signature change, no call-site edit needed beyond confirming it still compiles. In the
|
||
`catch { /* Robust fallback */ }` block, replace with:
|
||
```csharp
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning(ex, "Failed to write lineage event for run {RunId}", runId);
|
||
}
|
||
```
|
||
(must catch `Exception ex` by name, not a bare `catch {}` — a WBS log-pattern gate greps for
|
||
`catch\s*\(Exception ex\)\s*\{\s*_logger\.LogWarning`).
|
||
|
||
## Fix 2 — add unit test coverage (≥3 new `[Fact]`/`[Theory]` tests)
|
||
Add tests in `src/dotnet/QuantEngine.Core.Tests/` (create a new file, e.g.
|
||
`KisDataCollectionOrchestratorTests.cs`, following the style of existing test files in that
|
||
directory — check `SchedulerServiceTests.cs` for constructor/mocking conventions, likely
|
||
using a mocking library already referenced by the test project, e.g. Moq or NSubstitute —
|
||
check the .csproj for what's available). `IsMarketClosed` is private static, so either:
|
||
(a) test it indirectly through `RunCollectionAsync`'s observable behavior (mock
|
||
`ICollectionRepository.GetLatestSnapshotsForTickerAsync` to return a same-day snapshot and
|
||
assert the KIS client is NOT called when run at a time you control — if the orchestrator
|
||
doesn't allow injecting a clock, it's acceptable to test the always-current-time behavior
|
||
conditionally, e.g. skip/assert differently based on `DateTime.UtcNow`), or (b) if a test
|
||
already has reflection-based private-static-method testing conventions elsewhere in this
|
||
test project, follow that pattern. Prioritize simplicity: at minimum, write tests that
|
||
exercise (1) the cache-hit path returns without invoking `IKisApiClient` when a same-day
|
||
cached snapshot exists, (2) the cache-miss path (no same-day snapshot, or market open) does
|
||
invoke the KIS client, (3) `LogLineageEvent`/the run-completion path does not throw even when
|
||
the lineage file write fails (e.g. point at an unwritable path via a mocked repo-root
|
||
resolution, or simply assert `RunCollectionAsync` completes and returns a result even under
|
||
a forced I/O condition if you can simulate one — if truly impractical to simulate a file I/O
|
||
failure cleanly, it is acceptable to instead assert that a warning-level log call happens via
|
||
a mocked `ILogger` when you can trigger the catch path, using whatever mocking library the
|
||
test project already uses). Use your judgment on the exact test shape — the WBS gate only
|
||
requires ≥3 matches of `IsMarketClosed|LogLineageEvent|Cached` across test files, so name
|
||
tests/comments to naturally include these terms.
|
||
|
||
Acceptance (run from repo root, report output):
|
||
1. `dotnet build src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj -c Release --nologo` → 0 errors.
|
||
2. `dotnet test src/dotnet/QuantEngine.Core.Tests/QuantEngine.Core.Tests.csproj -c Release --nologo`
|
||
→ all green, including your new tests.
|
||
3. `git diff --stat`.
|
||
Do not modify SchedulerService.cs, CollectionEndpoints.cs, or Program.cs. Match existing
|
||
code style (minimal comments).
|
||
|
||
QE-M1-07:
|
||
title: "(수동) 프로덕션 재배포 — Gitea Actions prepare-release.yml + deploy-prod.yml"
|
||
status: PENDING
|
||
depends_on: [QE-M1-01, QE-M1-03, QE-M1-04, QE-M1-05, QE-M1-06]
|
||
owner_files: []
|
||
notes: >
|
||
비판적 재검토(2026-07-12)에서 발견: 운영 서버 journal에 구버전 로그 문자열
|
||
("Daily data collection completed at...")이 남아있어 로컬 소스가 실제 배포본보다
|
||
앞서있음을 확인. CLAUDE.md "CI/CD-Only Deployment Mandate"에 따라 수동 SSH 배포는
|
||
금지 — Gitea Actions UI에서 prepare-release.yml(workflow_dispatch) → deploy-prod.yml
|
||
(workflow_dispatch)을 사용자가 직접 트리거해야 함. 에이전트가 자동 실행할 수 없는
|
||
작업이므로 status는 PENDING으로 유지, verification_commands 없음(수동 확인 전용).
|
||
success_criteria:
|
||
expected_success_value: { manual_action_required: true }
|
||
evidence_artifacts: []
|
||
verification_commands: []
|
||
evidence_checks: []
|
||
execution:
|
||
mode: manual_user_action
|
||
instructions: >
|
||
1) https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/actions 에서 prepare-release.yml
|
||
실행(버전 태그 입력) → 2) 생성된 Release로 deploy-prod.yml 실행 → 3) 배포 후
|
||
`python tools/collect_remote_wbs_evidence_v1.py --target kjh2064@178.104.200.7`로
|
||
원격 journal에 신버전 로그("Collecting ticker", "Collection run .+ completed")가
|
||
나타나는지 확인 → 4) `npm run verify:task -- QE-M1-01` 재실행으로 M1-01 실증 완료.
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# M2 — 히스토리 시계열 저장소
|
||
# ---------------------------------------------------------------------------
|
||
QE-M2-01:
|
||
title: "V6 마이그레이션: price_history_daily + macro_history_daily"
|
||
status: DONE
|
||
depends_on: [QE-M1-01]
|
||
owner_files:
|
||
- src/dotnet/QuantEngine.Infrastructure/Migrations/V6__Add_Market_Time_Series.sql
|
||
notes: >
|
||
price_history_daily(ticker, trade_date, open/high/low/close numeric, volume bigint,
|
||
source text, collected_at timestamptz, PK(ticker, trade_date));
|
||
macro_history_daily(symbol, trade_date, value numeric, source, PK(symbol, trade_date)).
|
||
DbUp 마이그레이션 추가 시 docs/db/quantengine.dbml 동기화 필수 (CLAUDE.md 규칙 — 아래 체크로 강제).
|
||
success_criteria:
|
||
expected_success_value: { tables_created: 2 }
|
||
evidence_artifacts: [Temp/evidence/QE-M2-01/verdict.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M2-01"]
|
||
evidence_checks:
|
||
- type: pg_query
|
||
sql: >
|
||
SELECT count(*) FROM information_schema.tables
|
||
WHERE table_schema='quantengine' AND table_name IN ('price_history_daily','macro_history_daily')
|
||
expect: { equals: 2 }
|
||
- type: log_pattern
|
||
file_glob: docs/db/quantengine.dbml
|
||
pattern: 'price_history_daily'
|
||
expect: { min_matches: 1 } # DBML 동기화 강제
|
||
|
||
QE-M2-02:
|
||
title: "일봉 OHLCV 시계열 적재 (daily run 마다 upsert, 재실행 중복 0)"
|
||
status: DONE
|
||
depends_on: [QE-M2-01]
|
||
# 2026-07-12 실증 메모: 005930 1행 실적재 확인(2026-07-10, OHLCV 실제값). Dapper가
|
||
# System.DateOnly 파라미터를 지원하지 않는 버그를 발견·수정(CollectionRepository.cs,
|
||
# DateOnly→DateTime 변환). 나머지 5개 티커는 KIS 모의투자 토큰 발급이 403으로 거부됨
|
||
# (외부 자격증명/레이트리밋 이슈, 코드 결함 아님) — QE-M2-03(2년 백필)은 동일 이슈로
|
||
# 대량 API 호출 시 악화될 위험이 있어 이번엔 착수하지 않고 보류.
|
||
owner_files:
|
||
- src/dotnet/QuantEngine.Application/Services/KisDataCollectionOrchestrator.cs
|
||
success_criteria:
|
||
expected_success_value: { rows_per_ticker_min: 1, duplicate_on_rerun: 0 }
|
||
evidence_artifacts: [Temp/evidence/QE-M2-02/verdict.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M2-02"]
|
||
evidence_checks:
|
||
- type: pg_query
|
||
sql: "SELECT count(*) FROM quantengine.price_history_daily WHERE collected_at >= now() - interval '24 hours'"
|
||
expect: { min: 1 }
|
||
- type: pg_query
|
||
sql: >
|
||
SELECT count(*) FROM (SELECT ticker, trade_date, count(*) c
|
||
FROM quantengine.price_history_daily GROUP BY 1,2 HAVING count(*) > 1) d
|
||
expect: { equals: 0 }
|
||
execution:
|
||
mode: not_ci_reproducible
|
||
note: >
|
||
2026-07-12 로컬 실증: 005930 실데이터 1행 적재 확인. 실제 KIS API 응답이 전제라
|
||
CI에서 온디맨드 재현 불가 (QE-M1-01 참조).
|
||
|
||
QE-M2-03:
|
||
title: "2년치 백필 툴 (KIS chart API 페이지네이션 + rate-limit, 매크로는 yfinance→PG)"
|
||
status: PENDING
|
||
depends_on: [QE-M2-01]
|
||
owner_files:
|
||
- src/dotnet/QuantEngine.Tools/
|
||
- src/quant_engine/macro_index_collection_v1.py
|
||
success_criteria:
|
||
expected_success_value: { bars_per_ticker_min: 480, macro_bars_min: 480 }
|
||
evidence_artifacts: [Temp/evidence/QE-M2-03/verdict.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M2-03"]
|
||
evidence_checks:
|
||
- type: pg_query
|
||
sql: "SELECT coalesce(min(c),0) FROM (SELECT count(*) c FROM quantengine.price_history_daily GROUP BY ticker) t"
|
||
expect: { min: 480 }
|
||
- type: pg_query
|
||
sql: "SELECT count(*) FROM quantengine.macro_history_daily WHERE symbol IN ('KOSPI','KOSDAQ')"
|
||
expect: { min: 960 }
|
||
|
||
QE-M2-04:
|
||
title: "시계열 무결성 게이트 (거래일 캘린더 대비 gap 0, 가격 sanity)"
|
||
status: DONE
|
||
depends_on: [QE-M2-02]
|
||
# 2026-07-12 정정: 원래 [QE-M2-03](2년 백필) 의존 — 그러나 이 게이트는 "수집된 범위 내"
|
||
# gap-freeness(각 티커의 min~max trade_date 사이 결측 거래일 수)를 검증하는 것으로,
|
||
# 전체 2년 커버리지를 전제하지 않는다. 백필 전에도 코드 완성·정직한 결과 산출 가능.
|
||
owner_files:
|
||
- tools/validate_price_history_integrity_v1.py
|
||
success_criteria:
|
||
expected_success_value: { gap_count: 0, invalid_price_rows: 0 }
|
||
evidence_artifacts: [Temp/evidence/QE-M2-04/verdict.json, Temp/price_history_integrity_v1.json]
|
||
verification_commands:
|
||
- "python tools/validate_price_history_integrity_v1.py"
|
||
- "python tools/verify_wbs_task_v1.py --task QE-M2-04"
|
||
evidence_checks:
|
||
- type: json_gate
|
||
path: Temp/price_history_integrity_v1.json
|
||
expect: { gate: PASS, gap_count: 0 }
|
||
execution:
|
||
mode: not_ci_reproducible
|
||
note: >
|
||
2026-07-12 로컬 실증: gap_count=0, invalid_price_rows=0 (005930 실데이터 기준).
|
||
price_history_daily 실데이터가 전제라 CI에서 온디맨드 재현 불가 (QE-M1-01 참조).
|
||
|
||
QE-M2-05:
|
||
title: "히스토리 현황 FE (per-ticker bar 수/기간/gap — API 값과 DOM 대조)"
|
||
status: DONE
|
||
depends_on: [QE-M2-02, QE-M0-03]
|
||
# 2026-07-12 정정: QE-M2-04와 동일 사유로 [QE-M2-03] 의존 제거 — FE는 현재 존재하는
|
||
# 데이터(설사 희소하더라도)를 정직하게 표시하면 되고 풀 백필을 전제하지 않는다.
|
||
owner_files:
|
||
- src/dotnet/QuantEngine.Web/Pages/Admin/Collection/
|
||
- tests/e2e/evidence/qe-m2-05-history-tab.spec.ts
|
||
success_criteria:
|
||
expected_success_value: { spec_passed: 1, screenshots_min: 1 }
|
||
evidence_artifacts: [Temp/evidence/QE-M2-05/verdict.json]
|
||
verification_commands:
|
||
- "npx playwright test --project=evidence tests/e2e/evidence/qe-m2-05-history-tab.spec.ts"
|
||
- "python tools/verify_wbs_task_v1.py --task QE-M2-05"
|
||
evidence_checks:
|
||
- type: playwright_report
|
||
report: Temp/evidence/playwright-last-run.json
|
||
spec_file: qe-m2-05-history-tab.spec.ts
|
||
expect: { passed_min: 1, failed: 0 }
|
||
execution:
|
||
mode: not_ci_reproducible
|
||
note: >
|
||
2026-07-12 로컬 실증: 스크린샷 + DOM=API 대조 PASS. 라이브 앱 + 실데이터가 전제라
|
||
CI에서 온디맨드 재현 불가 (QE-M1-01 참조).
|
||
|
||
QE-M2-06:
|
||
title: "market_time_series 게이트 아키텍처 정합화 (정직한 라벨링 + release DAG 편입)"
|
||
status: DONE
|
||
depends_on: [QE-M0-07]
|
||
owner_files:
|
||
- tools/validate_market_time_series_schema_v1.py
|
||
- spec/41_release_dag.yaml
|
||
- spec/64_market_time_series_schema.yaml
|
||
notes: >
|
||
비판적 재검토(2026-07-12)에서 발견: validate_market_time_series_schema_v1.py 가
|
||
마이그레이션/DBML 존재 여부만 정규식으로 확인하면서 출력에
|
||
"runtime_database_query": "DATA_GATED" 라고 자기선언 — DB 연결이 전혀 없는데
|
||
마치 실데이터를 검증한 것처럼 오인될 수 있는 라벨. 또한 spec/41_release_dag.yaml에
|
||
노드가 없어 ci.yml에서만 직접 호출되고 lineage 시스템(runtime/lineage_events.jsonl)을
|
||
우회. 실데이터 검증의 진짜 권위는 QE-M2-01(spec/60, 실제 pg_query 사용)이 담당 —
|
||
이 검증기는 구조적/오프라인 사전 체크로만 정직하게 재정의한다(파일 삭제는 하지 않음 —
|
||
DB 없이 PR 단계에서 마이그레이션+DBML 동기화를 빠르게 잡아내는 정당한 역할이 있음).
|
||
success_criteria:
|
||
expected_success_value: { honest_label: true, dag_node_present: true }
|
||
evidence_artifacts: [Temp/evidence/QE-M2-06/verdict.json]
|
||
verification_commands:
|
||
- "python tools/validate_market_time_series_schema_v1.py"
|
||
- "python tools/verify_wbs_task_v1.py --task QE-M2-06"
|
||
evidence_checks:
|
||
- type: json_gate
|
||
path: Temp/market_time_series_schema_v1.json
|
||
expect: { check_scope: STATIC_STRUCTURAL_ONLY }
|
||
- type: log_pattern
|
||
file_glob: spec/41_release_dag.yaml
|
||
pattern: 'validate_market_time_series_schema'
|
||
expect: { min_matches: 1 }
|
||
- type: log_pattern
|
||
file_glob: spec/64_market_time_series_schema.yaml
|
||
pattern: 'QE-M2-01'
|
||
expect: { min_matches: 1 }
|
||
execution:
|
||
haiku_prompt: |
|
||
Repo: C:\Temp\data_feed. Task: WBS QE-M2-06 — fix an "honesty" and architecture-consistency
|
||
problem in one validator, found during a critical re-review of the QuantEngine WBS evidence
|
||
system. Three small, independent edits.
|
||
|
||
## Edit 1 — tools/validate_market_time_series_schema_v1.py (relabel the misleading field)
|
||
Read the whole file first (41 lines). It's a pure file-existence/regex validator (checks
|
||
the V6 migration SQL contains `CREATE TABLE IF NOT EXISTS quantengine.price_history_daily`
|
||
etc., and that docs/db/quantengine.dbml declares matching tables) — it never opens a
|
||
database connection. Yet its output payload (line ~31) has:
|
||
```python
|
||
"runtime_database_query": "DATA_GATED",
|
||
```
|
||
This is misleading — "DATA_GATED" elsewhere in this repo (e.g. spec/16) means "intentionally
|
||
deferred pending real data," but here it could be misread as "a live DB query happened and
|
||
the data just isn't there yet," when actually NO DB query happens at all. Replace that key
|
||
with:
|
||
```python
|
||
"check_scope": "STATIC_STRUCTURAL_ONLY",
|
||
"check_scope_note": "No database connection — verifies migration SQL + DBML text only. Live-data verification is QE-M2-01's pg_query evidence gate in spec/60_quant_engine_wbs.yaml.",
|
||
```
|
||
Keep everything else in the file identical (same checks, same gate logic, same REPORT path
|
||
`Temp/market_time_series_schema_v1.json`).
|
||
|
||
## Edit 2 — spec/41_release_dag.yaml (wire this validator into the release DAG)
|
||
This validator currently runs ONLY as a direct step in `.gitea/workflows/ci.yml` — it bypasses
|
||
the release-DAG lineage/caching system that every other validator in this repo goes through
|
||
(`runtime/lineage_events.jsonl`, `Temp/release_dag_run_v3.json`). Add a new node under the
|
||
`dag.nodes` mapping (look at the existing `validate_quant_engine_wbs` node — grep for it —
|
||
as your template for exact YAML shape: `artifact_policy`, `cache_key`, `command`, `depends_on`,
|
||
`id`, `inputs`, `outputs`, `strict`, `timeout_sec`). Add:
|
||
```yaml
|
||
validate_market_time_series_schema:
|
||
artifact_policy: keep
|
||
cache_key: validate_market_time_series_schema_v1
|
||
command:
|
||
- python
|
||
- tools/validate_market_time_series_schema_v1.py
|
||
depends_on: []
|
||
id: validate_market_time_series_schema
|
||
inputs:
|
||
- tools/validate_market_time_series_schema_v1.py
|
||
- src/dotnet/QuantEngine.Infrastructure/Migrations/V6__Add_Market_Time_Series.sql
|
||
- docs/db/quantengine.dbml
|
||
outputs:
|
||
- Temp/market_time_series_schema_v1.json
|
||
strict: true
|
||
timeout_sec: 30
|
||
```
|
||
Insert it alphabetically among the other `validate_*` node entries under `dag.nodes` (the file
|
||
is organized alphabetically by node id within that mapping — find the right spot, e.g. near
|
||
`validate_market_regime` or wherever alphabetical order puts it). Then add its id
|
||
`validate_market_time_series_schema` to the appropriate wave list under the top-level
|
||
`execution_order:` key (any node with `depends_on: []` can go in `wave_0` — find that list
|
||
and insert alphabetically, following the existing pattern, e.g. next to
|
||
`validate_low_capability` / `validate_metric_alias_collision` depending on exact alphabetical
|
||
position).
|
||
After editing, verify the file still parses and has no dangling references:
|
||
```
|
||
python -c "
|
||
import yaml
|
||
d = yaml.safe_load(open('spec/41_release_dag.yaml', encoding='utf-8'))
|
||
nodes = set(d['dag']['nodes'].keys())
|
||
missing = [(n,dep) for n,node in d['dag']['nodes'].items() for dep in (node.get('depends_on') or []) if dep not in nodes]
|
||
eo = [x for wave in d['execution_order'].values() for x in wave]
|
||
dangling = [x for x in eo if x not in nodes]
|
||
print('nodes:', len(nodes), 'dangling depends_on:', missing, 'dangling execution_order:', dangling)
|
||
print('validate_market_time_series_schema in nodes:', 'validate_market_time_series_schema' in nodes)
|
||
print('validate_market_time_series_schema in execution_order:', 'validate_market_time_series_schema' in eo)
|
||
"
|
||
```
|
||
All four printed values must show the new node present with zero dangling references.
|
||
|
||
## Edit 3 — spec/64_market_time_series_schema.yaml (cross-reference comment)
|
||
Read this file (it's the declarative contract this validator implements). Add a short comment
|
||
or note field near the top (follow whatever structure the file already uses — a top-level
|
||
`note:` key or a comment line) stating in Korean: "이 계약은 구조적 검증만 수행한다(DB 미연결).
|
||
실데이터(테이블 존재/행 도달 가능) 검증의 권위는 QE-M2-01(spec/60_quant_engine_wbs.yaml)의
|
||
pg_query 게이트다." — must literally contain the substring "QE-M2-01" (a WBS log-pattern gate
|
||
checks for it).
|
||
|
||
Acceptance (run from repo root, report full output of each):
|
||
1. `python tools/validate_market_time_series_schema_v1.py` → still exits 0 (gate PASS), and
|
||
`Temp/market_time_series_schema_v1.json` now has `"check_scope": "STATIC_STRUCTURAL_ONLY"`
|
||
instead of the old `runtime_database_query` key.
|
||
2. The yaml-parse verification snippet from Edit 2, showing the new node present and zero
|
||
dangling references.
|
||
3. `python tools/validate_specs.py` → exit 0 (confirms nothing else broke).
|
||
4. `git diff --stat`.
|
||
Do not modify any other file, and do not touch the `validate_quant_engine_wbs` node itself
|
||
(only use it as a formatting reference).
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# M3 — 실데이터 팩터 계산
|
||
# ---------------------------------------------------------------------------
|
||
QE-M3-01:
|
||
title: "Point-in-time 리더 (GetBarsAsOf — lookahead 구조적 차단 + xUnit 증명)"
|
||
status: DONE
|
||
depends_on: [QE-M2-02]
|
||
# 2026-07-12 정정: [QE-M2-03](2년 백필) 의존 제거 — 리더의 lookahead 차단 정확성은
|
||
# 코드 레벨 유닛테스트(mock/합성 데이터)로 증명 가능하며 실제 2년치 데이터 존재를
|
||
# 전제하지 않는다. price_history_daily 쓰기 경로(QE-M2-02)만 있으면 충분.
|
||
owner_files:
|
||
- src/dotnet/QuantEngine.Infrastructure/Repositories/
|
||
- src/dotnet/QuantEngine.Core.Tests/
|
||
success_criteria:
|
||
expected_success_value: { asof_leak_tests_green: true }
|
||
evidence_artifacts: [Temp/evidence/QE-M3-01/verdict.json]
|
||
verification_commands:
|
||
- "dotnet test src/dotnet/QuantEngine.Core.Tests/QuantEngine.Core.Tests.csproj -c Release --filter PriceHistoryReader"
|
||
- "python tools/verify_wbs_task_v1.py --task QE-M3-01"
|
||
evidence_checks:
|
||
- type: log_pattern
|
||
file_glob: src/dotnet/QuantEngine.Core.Tests/**/*.cs
|
||
pattern: 'GetBarsAsOf'
|
||
expect: { min_matches: 1 }
|
||
|
||
QE-M3-02:
|
||
title: "전통 팩터 계산기 (모멘텀 20/60/120d·RS, 저변동성 ATR%·stdev·beta, 밸류/퀄리티)"
|
||
status: DONE
|
||
depends_on: [QE-M3-01]
|
||
owner_files:
|
||
- src/dotnet/QuantEngine.Core/Domain/
|
||
- tools/validate_factor_parity_v1.py
|
||
success_criteria:
|
||
expected_success_value: { parity_formulas_min: 20, tolerance: 1e-9 }
|
||
evidence_artifacts: [Temp/evidence/QE-M3-02/verdict.json, Temp/factor_parity_v1.json]
|
||
verification_commands:
|
||
- "python tools/validate_factor_parity_v1.py"
|
||
- "python tools/verify_wbs_task_v1.py --task QE-M3-02"
|
||
evidence_checks:
|
||
- type: json_gate
|
||
path: Temp/factor_parity_v1.json
|
||
expect: { gate: PASS, compared_count: ">=20" }
|
||
|
||
QE-M3-03:
|
||
title: "SS001 합성 스코어 + HF001-09 → engine_history.factor_output_history 적재"
|
||
status: DONE
|
||
depends_on: [QE-M3-02]
|
||
owner_files:
|
||
- src/dotnet/QuantEngine.Application/Services/
|
||
success_criteria:
|
||
expected_success_value: { scored_universe_full: true, score_range_0_100: true }
|
||
evidence_artifacts: [Temp/evidence/QE-M3-03/verdict.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M3-03"]
|
||
evidence_checks:
|
||
- type: pg_query
|
||
sql: "SELECT count(*) FROM engine_history.factor_output_history WHERE created_at >= now() - interval '24 hours'"
|
||
expect: { min: 5 }
|
||
|
||
QE-M3-04:
|
||
title: "PipelineOrchestrator 정직화 (1-2단계 실구현, 나머지 STUBBED 표기 — mock PASS 금지)"
|
||
status: DONE
|
||
depends_on: [QE-M3-03]
|
||
owner_files:
|
||
- src/dotnet/QuantEngine.Application/Services/PipelineOrchestrator.cs
|
||
success_criteria:
|
||
expected_success_value: { computed_steps_min: 2, stub_steps_marked: STUBBED }
|
||
evidence_artifacts: [Temp/evidence/QE-M3-04/verdict.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M3-04"]
|
||
evidence_checks:
|
||
- type: log_pattern
|
||
file_glob: src/dotnet/QuantEngine.Application/Services/PipelineOrchestrator.cs
|
||
pattern: 'STUBBED'
|
||
expect: { min_matches: 1 }
|
||
|
||
QE-M3-05:
|
||
title: "스코어 FE (SS001 테이블 — factor_output_history 값과 DOM 대조)"
|
||
status: DONE
|
||
depends_on: [QE-M3-03, QE-M0-03]
|
||
owner_files:
|
||
- tests/e2e/evidence/qe-m3-05-scores.spec.ts
|
||
success_criteria:
|
||
expected_success_value: { spec_passed: 1 }
|
||
evidence_artifacts: [Temp/evidence/QE-M3-05/verdict.json]
|
||
verification_commands:
|
||
- "npx playwright test --project=evidence tests/e2e/evidence/qe-m3-05-scores.spec.ts"
|
||
- "python tools/verify_wbs_task_v1.py --task QE-M3-05"
|
||
evidence_checks:
|
||
- type: playwright_report
|
||
report: Temp/evidence/playwright-last-run.json
|
||
spec_file: qe-m3-05-scores.spec.ts
|
||
expect: { passed_min: 1, failed: 0 }
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# M4 — 백테스팅 + 검증
|
||
# ---------------------------------------------------------------------------
|
||
QE-M4-01:
|
||
title: "백테스터 + 거래비용 모델 (Sharpe/MDD/턴오버/비용 드래그 JSON)"
|
||
status: PENDING
|
||
depends_on: [QE-M3-03]
|
||
owner_files:
|
||
- src/dotnet/QuantEngine.Core/Domain/Backtester.cs
|
||
- src/dotnet/QuantEngine.Tools/
|
||
success_criteria:
|
||
expected_success_value: { metrics_populated: [sharpe, mdd, turnover, cost_drag] }
|
||
evidence_artifacts: [Temp/evidence/QE-M4-01/verdict.json, Temp/backtest_result_v1.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M4-01"]
|
||
evidence_checks:
|
||
- type: json_gate
|
||
path: Temp/backtest_result_v1.json
|
||
expect: { gate: PASS }
|
||
|
||
QE-M4-02:
|
||
title: "no-lookahead 게이트 실배선 (정상 PASS + 오염 픽스처 FAIL 양방향 검증)"
|
||
status: PENDING
|
||
depends_on: [QE-M4-01]
|
||
owner_files:
|
||
- tools/validate_no_lookahead_bias_v1.py
|
||
success_criteria:
|
||
expected_success_value: { real_run: PASS, corrupted_fixture: FAIL }
|
||
evidence_artifacts: [Temp/evidence/QE-M4-02/verdict.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M4-02"]
|
||
evidence_checks:
|
||
- type: json_gate
|
||
path: Temp/no_lookahead_bias_v1.json
|
||
expect: { gate: PASS }
|
||
|
||
QE-M4-03:
|
||
title: "워크포워드 하네스 (24m train / 6m test 롤링, 윈도우 ≥4)"
|
||
status: PENDING
|
||
depends_on: [QE-M4-01]
|
||
owner_files:
|
||
- src/dotnet/QuantEngine.Tools/
|
||
success_criteria:
|
||
expected_success_value: { windows_min: 4, oos_metrics_nonnull: true }
|
||
evidence_artifacts: [Temp/evidence/QE-M4-03/verdict.json, Temp/walk_forward_v1.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M4-03"]
|
||
evidence_checks:
|
||
- type: json_gate
|
||
path: Temp/walk_forward_v1.json
|
||
expect: { gate: PASS, windows: ">=4" }
|
||
|
||
QE-M4-04:
|
||
title: "T+5/T+20 성과 원장 (prediction_accuracy 실표본 재계산, t5_sample≥30)"
|
||
status: PENDING
|
||
depends_on: [QE-M2-03]
|
||
owner_files:
|
||
- tools/
|
||
success_criteria:
|
||
expected_success_value: { t5_sample_min: 30 }
|
||
evidence_artifacts: [Temp/evidence/QE-M4-04/verdict.json, Temp/prediction_accuracy_harness_v2.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M4-04"]
|
||
evidence_checks:
|
||
- type: json_gate
|
||
path: Temp/prediction_accuracy_harness_v2.json
|
||
expect: { t5_sample: ">=30" }
|
||
|
||
QE-M4-05:
|
||
title: "백테스트 결과 FE (에쿼티커브/Sharpe/MDD — backtest_result_v1.json 값과 DOM 대조)"
|
||
status: PENDING
|
||
depends_on: [QE-M4-01, QE-M0-03]
|
||
owner_files:
|
||
- tests/e2e/evidence/qe-m4-05-backtest.spec.ts
|
||
success_criteria:
|
||
expected_success_value: { spec_passed: 1 }
|
||
evidence_artifacts: [Temp/evidence/QE-M4-05/verdict.json]
|
||
verification_commands:
|
||
- "npx playwright test --project=evidence tests/e2e/evidence/qe-m4-05-backtest.spec.ts"
|
||
- "python tools/verify_wbs_task_v1.py --task QE-M4-05"
|
||
evidence_checks:
|
||
- type: playwright_report
|
||
report: Temp/evidence/playwright-last-run.json
|
||
spec_file: qe-m4-05-backtest.spec.ts
|
||
expect: { passed_min: 1, failed: 0 }
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# M5 — 포트폴리오 구성 + 최신 기법
|
||
# ---------------------------------------------------------------------------
|
||
QE-M5-01:
|
||
title: "레짐 감지기 (spec/11_market_regime.yaml — 실제 매크로 시계열, 전 거래일 라벨)"
|
||
status: PENDING
|
||
depends_on: [QE-M2-03]
|
||
owner_files:
|
||
- src/dotnet/QuantEngine.Core/Domain/
|
||
success_criteria:
|
||
expected_success_value: { regime_labels_full_window: true }
|
||
evidence_artifacts: [Temp/evidence/QE-M5-01/verdict.json, Temp/market_regime_v1.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M5-01"]
|
||
evidence_checks:
|
||
- type: json_gate
|
||
path: Temp/market_regime_v1.json
|
||
expect: { gate: PASS }
|
||
|
||
QE-M5-02:
|
||
title: "SS001 가중치 워크포워드 캘리브레이션 (±50% 제약 + shrinkage λ=0.5, 정직 보고)"
|
||
status: PENDING
|
||
depends_on: [QE-M4-03, QE-M5-01]
|
||
owner_files:
|
||
- src/dotnet/QuantEngine.Tools/
|
||
notes: "게이트는 방법론 필드(제약 준수, OOS 비교 존재)를 검증 — 캘리브레이션이 '이겨야' PASS 가 아님"
|
||
success_criteria:
|
||
expected_success_value: { weights_within_bounds: true, oos_comparison_reported: true }
|
||
evidence_artifacts: [Temp/evidence/QE-M5-02/verdict.json, Temp/weight_calibration_v1.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M5-02"]
|
||
evidence_checks:
|
||
- type: json_gate
|
||
path: Temp/weight_calibration_v1.json
|
||
expect: { gate: PASS }
|
||
|
||
QE-M5-03:
|
||
title: "변동성 타게팅 사이징 + heat/집중도 캡 합성 → 최종 목표 포트폴리오 패킷"
|
||
status: PENDING
|
||
depends_on: [QE-M5-02]
|
||
owner_files:
|
||
- src/dotnet/QuantEngine.Application/Services/
|
||
success_criteria:
|
||
expected_success_value: { weights_sum_lte_100: true, all_caps_satisfied: true }
|
||
evidence_artifacts: [Temp/evidence/QE-M5-03/verdict.json, Temp/target_portfolio_v1.json]
|
||
verification_commands: ["python tools/verify_wbs_task_v1.py --task QE-M5-03"]
|
||
evidence_checks:
|
||
- type: json_gate
|
||
path: Temp/target_portfolio_v1.json
|
||
expect: { gate: PASS }
|
||
|
||
QE-M5-04:
|
||
title: "포트폴리오·레짐 대시보드 FE (레짐 배지·목표 가중치 — API 값과 DOM 대조)"
|
||
status: PENDING
|
||
depends_on: [QE-M5-03, QE-M0-03]
|
||
owner_files:
|
||
- tests/e2e/evidence/qe-m5-04-portfolio.spec.ts
|
||
success_criteria:
|
||
expected_success_value: { spec_passed: 1 }
|
||
evidence_artifacts: [Temp/evidence/QE-M5-04/verdict.json]
|
||
verification_commands:
|
||
- "npx playwright test --project=evidence tests/e2e/evidence/qe-m5-04-portfolio.spec.ts"
|
||
- "python tools/verify_wbs_task_v1.py --task QE-M5-04"
|
||
evidence_checks:
|
||
- type: playwright_report
|
||
report: Temp/evidence/playwright-last-run.json
|
||
spec_file: qe-m5-04-portfolio.spec.ts
|
||
expect: { passed_min: 1, failed: 0 }
|