feat: add quant engine WBS verification harness
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
// =============================================================================
|
||||
// QuantEngine Database Schema (DBML)
|
||||
// DbUp 마이그레이션(V1~V3)과 1:1 동기화 — 마이그레이션 추가 시 이 파일도 반드시 갱신
|
||||
// (CLAUDE.md 규칙: schema 변경 → DBML + 문서 동기화)
|
||||
//
|
||||
// 참고: Hangfire 스키마는 Hangfire.PostgreSql 라이브러리가 자동 생성
|
||||
// (DbUp 마이그레이션으로 관리하지 않음, 여기서도 제외)
|
||||
// =============================================================================
|
||||
|
||||
Project quantengine {
|
||||
database_type: 'PostgreSQL'
|
||||
Note: '''
|
||||
QuantEngine v0.1 데이터베이스 스키마.
|
||||
세 개 스키마로 구성:
|
||||
- quantengine: 핵심 KIS API 토큰, 사용자 계정, 수집 파이프라인 데이터
|
||||
- engine_history: 팩터 계산 이력, 시장 데이터 이력, 의사결정 이력
|
||||
- (생략) hangfire: Hangfire 백그라운드 잡 관리 (auto-created)
|
||||
'''
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Schema: quantengine (V1 + V2)
|
||||
// =============================================================================
|
||||
|
||||
TableGroup "quantengine" {
|
||||
kis_tokens
|
||||
workspace_account
|
||||
workspace_session
|
||||
collection_runs
|
||||
collection_snapshots
|
||||
collection_source_errors
|
||||
settings
|
||||
account_snapshot
|
||||
workspace_meta
|
||||
workspace_change_log
|
||||
workspace_approval_v2
|
||||
workspace_lock
|
||||
kis_collection_runs
|
||||
kis_collection_snapshots
|
||||
kis_collection_errors
|
||||
}
|
||||
|
||||
Table quantengine.kis_tokens {
|
||||
account TEXT [pk, note: "KIS 계정 모드 (real/mock)"]
|
||||
access_token TEXT [not null, note: "KIS 토큰"]
|
||||
expires_at TEXT [not null, note: "만료 시각 (ISO 8601)"]
|
||||
updated_at TEXT [not null, note: "마지막 갱신 시각 (ISO 8601)"]
|
||||
|
||||
Note: "KIS Open API 인증 토큰 캐시"
|
||||
}
|
||||
|
||||
Table quantengine.workspace_account {
|
||||
ordinal INT [not null, note: "순서 인덱스"]
|
||||
username TEXT [pk, note: "로그인 ID"]
|
||||
password_hash TEXT [not null, note: "BCrypt 또는 SHA-256 해시 (자동 마이그레이션 가능)"]
|
||||
role TEXT [not null, default: "'Admin'", note: "역할 (Admin)"]
|
||||
is_active TEXT [not null, default: "'true'", note: "활성 상태 (true/false)"]
|
||||
created_at TEXT [not null, note: "생성 시각 (ISO 8601)"]
|
||||
updated_at TEXT [not null, note: "갱신 시각 (ISO 8601)"]
|
||||
|
||||
indexes {
|
||||
(is_active, username) [name: "idx_workspace_account_active"]
|
||||
}
|
||||
|
||||
Note: "Admin UI 사용자 계정"
|
||||
}
|
||||
|
||||
Table quantengine.workspace_session {
|
||||
session_token_hash TEXT [pk, note: "세션 토큰 해시"]
|
||||
username TEXT [not null, note: "사용자명"]
|
||||
role TEXT [not null, default: "'Admin'", note: "역할"]
|
||||
created_at TEXT [not null, note: "세션 생성 시각 (ISO 8601)"]
|
||||
expires_at TEXT [not null, note: "만료 시각 (ISO 8601)"]
|
||||
revoked_at TEXT [note: "취소 시각 (ISO 8601), NULL이면 활성"]
|
||||
|
||||
indexes {
|
||||
(username, expires_at) [name: "idx_workspace_session_username"]
|
||||
}
|
||||
|
||||
Note: "세션 관리 (쿠키 기반 인증)"
|
||||
}
|
||||
|
||||
Table quantengine.collection_runs {
|
||||
run_id TEXT [pk, note: "수집 실행 ID (예: api-20260712-120000)"]
|
||||
collector_name TEXT [not null, note: "수집기 이름"]
|
||||
started_at TEXT [not null, note: "시작 시각 (ISO 8601)"]
|
||||
finished_at TEXT [note: "종료 시각 (ISO 8601)"]
|
||||
status TEXT [not null, note: "상태 (RUNNING/COMPLETED/FAILED)"]
|
||||
input_source TEXT [note: "입력 소스 경로"]
|
||||
output_json_path TEXT [note: "출력 JSON 파일 경로"]
|
||||
output_db_path TEXT [note: "출력 DB 경로"]
|
||||
notes TEXT [note: "메모"]
|
||||
created_at TIMESTAMP [default: "CURRENT_TIMESTAMP", note: "DB 기록 시각"]
|
||||
|
||||
Note: "데이터 수집 실행 기록 (레거시, V2의 kis_collection_runs 참조)"
|
||||
}
|
||||
|
||||
Table quantengine.collection_snapshots {
|
||||
run_id TEXT [not null, note: "수집 실행 ID"]
|
||||
dataset_name TEXT [not null, note: "데이터셋명"]
|
||||
ticker TEXT [not null, note: "종목코드 (예: 005930)"]
|
||||
name TEXT [note: "종목명"]
|
||||
sector TEXT [note: "업종"]
|
||||
as_of_date TEXT [note: "기준 일자"]
|
||||
source_priority TEXT [note: "소스 우선순위"]
|
||||
source_status TEXT [note: "소스 상태"]
|
||||
payload_json TEXT [not null, note: "정규화된 데이터 (JSON)"]
|
||||
provenance_json TEXT [not null, note: "출처 정보 (JSON)"]
|
||||
created_at TIMESTAMP [default: "CURRENT_TIMESTAMP", note: "DB 기록 시각"]
|
||||
|
||||
indexes {
|
||||
(run_id, dataset_name, ticker) [pk]
|
||||
(ticker, created_at) [name: "idx_collection_snapshots_ticker_time"]
|
||||
}
|
||||
|
||||
Note: "수집 스냅샷 (레거시, V2의 kis_collection_snapshots 참조)"
|
||||
}
|
||||
|
||||
Table quantengine.collection_source_errors {
|
||||
run_id TEXT [not null, note: "수집 실행 ID"]
|
||||
ticker TEXT [note: "종목코드"]
|
||||
source_name TEXT [not null, note: "소스명"]
|
||||
error_kind TEXT [not null, note: "에러 타입"]
|
||||
error_message TEXT [not null, note: "에러 메시지"]
|
||||
payload_json TEXT [note: "에러 상세 (JSON)"]
|
||||
created_at TIMESTAMP [default: "CURRENT_TIMESTAMP", note: "DB 기록 시각"]
|
||||
|
||||
indexes {
|
||||
(run_id, source_name) [name: "idx_collection_source_errors_run"]
|
||||
}
|
||||
|
||||
Note: "수집 중 발생한 에러 기록 (레거시)"
|
||||
}
|
||||
|
||||
Table quantengine.settings {
|
||||
ordinal INT [not null, note: "순서 인덱스"]
|
||||
key TEXT [pk, note: "설정 키"]
|
||||
value_json TEXT [not null, note: "값 (JSON)"]
|
||||
note TEXT [not null, default: "''", note: "설명"]
|
||||
updated_at TEXT [not null, note: "갱신 시각 (ISO 8601)"]
|
||||
|
||||
Note: "애플리케이션 설정 저장소"
|
||||
}
|
||||
|
||||
Table quantengine.account_snapshot {
|
||||
ordinal INT [not null, note: "순서 인덱스"]
|
||||
row_json TEXT [not null, note: "계정 데이터 (JSON)"]
|
||||
captured_at TEXT [not null, default: "''", note: "캡처 시각 (ISO 8601)"]
|
||||
account TEXT [not null, default: "''", note: "계정"]
|
||||
account_type TEXT [not null, default: "''", note: "계정 타입"]
|
||||
ticker TEXT [not null, default: "''", note: "종목코드"]
|
||||
name TEXT [not null, default: "''", note: "이름"]
|
||||
parse_status TEXT [not null, default: "''", note: "파싱 상태"]
|
||||
user_confirmed TEXT [not null, default: "''", note: "사용자 확인 여부"]
|
||||
updated_at TEXT [not null, note: "갱신 시각 (ISO 8601)"]
|
||||
|
||||
indexes {
|
||||
(captured_at) [name: "idx_account_snapshot_captured_at"]
|
||||
(ticker) [name: "idx_account_snapshot_ticker"]
|
||||
}
|
||||
|
||||
Note: "계정 스냅샷 저장소"
|
||||
}
|
||||
|
||||
Table quantengine.workspace_meta {
|
||||
key TEXT [pk, note: "메타 키"]
|
||||
value_json TEXT [not null, note: "값 (JSON)"]
|
||||
|
||||
Note: "워크스페이스 메타데이터"
|
||||
}
|
||||
|
||||
Table quantengine.workspace_change_log {
|
||||
id SERIAL [pk, note: "자동 증가 ID"]
|
||||
domain TEXT [not null, note: "도메인"]
|
||||
action TEXT [not null, note: "액션 (create/update/delete)"]
|
||||
target_ref TEXT [not null, default: "''", note: "대상 참조"]
|
||||
actor TEXT [not null, default: "'system'", note: "액터 (사용자/시스템)"]
|
||||
note TEXT [not null, default: "''", note: "메모"]
|
||||
before_json TEXT [not null, default: "'null'", note: "변경 전 값 (JSON)"]
|
||||
after_json TEXT [not null, default: "'null'", note: "변경 후 값 (JSON)"]
|
||||
created_at TEXT [not null, note: "기록 시각 (ISO 8601)"]
|
||||
|
||||
Note: "변경 로그"
|
||||
}
|
||||
|
||||
Table quantengine.workspace_approval_v2 {
|
||||
domain TEXT [not null, note: "도메인"]
|
||||
target_ref TEXT [not null, default: "'*'", note: "대상 참조"]
|
||||
status TEXT [not null, note: "승인 상태"]
|
||||
approved_by TEXT [not null, default: "''", note: "승인자"]
|
||||
approved_at TEXT [not null, default: "''", note: "승인 시각 (ISO 8601)"]
|
||||
note TEXT [not null, default: "''", note: "메모"]
|
||||
updated_at TEXT [not null, note: "갱신 시각 (ISO 8601)"]
|
||||
|
||||
indexes {
|
||||
(domain, target_ref) [pk]
|
||||
}
|
||||
|
||||
Note: "승인 워크플로우"
|
||||
}
|
||||
|
||||
Table quantengine.workspace_lock {
|
||||
domain TEXT [not null, note: "도메인"]
|
||||
target_ref TEXT [not null, default: "''", note: "대상 참조"]
|
||||
locked_by TEXT [not null, default: "''", note: "잠금 사용자"]
|
||||
reason TEXT [not null, default: "''", note: "잠금 사유"]
|
||||
locked_at TEXT [not null, note: "잠금 시각 (ISO 8601)"]
|
||||
|
||||
indexes {
|
||||
(domain, target_ref) [pk]
|
||||
}
|
||||
|
||||
Note: "동시성 제어용 잠금"
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// V2: KIS 수집 파이프라인 (kis_collection_*)
|
||||
// =============================================================================
|
||||
|
||||
Table quantengine.kis_collection_runs {
|
||||
run_id TEXT [pk, note: "수집 실행 ID"]
|
||||
status TEXT [not null, note: "상태: RUNNING / COMPLETED / COMPLETED_WITH_ERRORS / FAILED"]
|
||||
started_at TEXT [not null, note: "시작 시각 (ISO 8601 KST)"]
|
||||
finished_at TEXT [note: "종료 시각 (ISO 8601 KST)"]
|
||||
total_snapshots INTEGER [note: "성공한 스냅샷 수"]
|
||||
total_errors INTEGER [note: "발생한 에러 수"]
|
||||
updated_at TEXT [not null, note: "마지막 갱신 시각 (ISO 8601)"]
|
||||
|
||||
indexes {
|
||||
(started_at) [name: "idx_kis_runs_started_at"]
|
||||
}
|
||||
|
||||
Note: "KIS API 수집 실행 기록"
|
||||
}
|
||||
|
||||
Table quantengine.kis_collection_snapshots {
|
||||
run_id TEXT [not null, note: "수집 실행 ID"]
|
||||
dataset_name TEXT [note: "데이터셋명 (예: data_feed)"]
|
||||
ticker TEXT [not null, note: "종목코드 (예: 005930)"]
|
||||
source_name TEXT [not null, note: "데이터 소스 (kis_open_api 등)"]
|
||||
payload_json TEXT [not null, note: "정규화된 수집 데이터 (JSON)"]
|
||||
captured_at TEXT [not null, note: "캡처 시각 (ISO 8601 KST)"]
|
||||
created_at TEXT [not null, note: "DB 기록 시각 (ISO 8601)"]
|
||||
|
||||
indexes {
|
||||
(run_id, ticker, source_name) [pk]
|
||||
(ticker) [name: "idx_kis_snapshots_ticker"]
|
||||
(captured_at) [name: "idx_kis_snapshots_captured_at"]
|
||||
}
|
||||
|
||||
Note: "KIS API 수집 스냅샷 (시계열 데이터)"
|
||||
}
|
||||
|
||||
Table quantengine.kis_collection_errors {
|
||||
id SERIAL [pk, note: "자동 증가 ID"]
|
||||
run_id TEXT [not null, note: "수집 실행 ID"]
|
||||
source_name TEXT [not null, note: "데이터 소스"]
|
||||
error_kind TEXT [not null, note: "에러 타입 (예: HttpRequestException)"]
|
||||
error_message TEXT [note: "에러 메시지"]
|
||||
ticker TEXT [note: "종목코드 (해당하면)"]
|
||||
created_at TEXT [not null, note: "DB 기록 시각 (ISO 8601)"]
|
||||
|
||||
indexes {
|
||||
(run_id) [name: "idx_kis_errors_run_id"]
|
||||
}
|
||||
|
||||
Note: "KIS API 수집 중 발생한 에러"
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Schema: engine_history (V3)
|
||||
// =============================================================================
|
||||
|
||||
TableGroup "engine_history" {
|
||||
market_raw_history
|
||||
factor_version_history
|
||||
factor_output_history
|
||||
decision_result_history
|
||||
market_vs_engine_gap_history
|
||||
}
|
||||
|
||||
Table engine_history.market_raw_history {
|
||||
id BIGSERIAL [pk, note: "자동 증가 ID"]
|
||||
source_id TEXT [not null, note: "소스 ID"]
|
||||
observed_at TEXT [not null, note: "관측 시각 (ISO 8601)"]
|
||||
source_name TEXT [not null, note: "소스명 (kis_open_api 등)"]
|
||||
instrument_id TEXT [not null, note: "상품 ID (종목코드 등)"]
|
||||
field_name TEXT [not null, note: "필드명 (현재가, 종가 등)"]
|
||||
field_value TEXT [not null, note: "필드값 (문자열)"]
|
||||
unit TEXT [not null, note: "단위 (원, % 등)"]
|
||||
provenance JSONB [not null, default: "'{}'::jsonb", note: "출처 메타데이터 (JSON)"]
|
||||
created_at TIMESTAMPTZ [not null, default: "NOW()", note: "DB 기록 시각 (UTC)"]
|
||||
|
||||
indexes {
|
||||
(created_at) [name: "idx_market_raw_history_created_at"]
|
||||
}
|
||||
|
||||
Note: "시장 데이터 원본 이력 (정규화 전)"
|
||||
}
|
||||
|
||||
Table engine_history.factor_version_history {
|
||||
id BIGSERIAL [pk, note: "자동 증가 ID"]
|
||||
factor_id TEXT [not null, note: "팩터 ID (예: momentum_ss001)"]
|
||||
factor_version TEXT [not null, note: "팩터 버전 (예: v1.0.0)"]
|
||||
effective_from TEXT [not null, note: "유효 시작 일자 (YYYYMMDD)"]
|
||||
effective_to TEXT [not null, note: "유효 종료 일자 (YYYYMMDD)"]
|
||||
formula_id TEXT [not null, note: "계산식 ID"]
|
||||
source_version TEXT [not null, note: "소스 버전"]
|
||||
provenance JSONB [not null, default: "'{}'::jsonb", note: "출처 메타데이터 (JSON)"]
|
||||
created_at TIMESTAMPTZ [not null, default: "NOW()", note: "DB 기록 시각 (UTC)"]
|
||||
|
||||
indexes {
|
||||
(created_at) [name: "idx_factor_version_history_created_at"]
|
||||
}
|
||||
|
||||
Note: "팩터 버전 관리 이력"
|
||||
}
|
||||
|
||||
Table engine_history.factor_output_history {
|
||||
id BIGSERIAL [pk, note: "자동 증가 ID"]
|
||||
factor_output_id TEXT [not null, note: "팩터 출력 ID"]
|
||||
observed_at TEXT [not null, note: "관측 일자 (YYYYMMDD)"]
|
||||
factor_id TEXT [not null, note: "팩터 ID"]
|
||||
factor_version TEXT [not null, note: "팩터 버전"]
|
||||
output_value TEXT [not null, note: "출력값 (문자열)"]
|
||||
output_gate TEXT [not null, note: "게이트 (PASS/FAIL/WARN)"]
|
||||
source_version TEXT [not null, note: "소스 버전"]
|
||||
provenance JSONB [not null, default: "'{}'::jsonb", note: "출처 메타데이터 (JSON)"]
|
||||
created_at TIMESTAMPTZ [not null, default: "NOW()", note: "DB 기록 시각 (UTC)"]
|
||||
|
||||
indexes {
|
||||
(created_at) [name: "idx_factor_output_history_created_at"]
|
||||
}
|
||||
|
||||
Note: "팩터 계산 결과 이력"
|
||||
}
|
||||
|
||||
Table engine_history.decision_result_history {
|
||||
id BIGSERIAL [pk, note: "자동 증가 ID"]
|
||||
decision_id TEXT [not null, note: "의사결정 ID"]
|
||||
decided_at TEXT [not null, note: "의사결정 일자 (YYYYMMDD)"]
|
||||
instrument_id TEXT [not null, note: "상품 ID (종목코드 등)"]
|
||||
action TEXT [not null, note: "액션 (BUY/SELL/HOLD)"]
|
||||
gate TEXT [not null, note: "게이트 (PASS/FAIL)"]
|
||||
score TEXT [not null, note: "스코어 (문자열)"]
|
||||
source_version TEXT [not null, note: "소스 버전"]
|
||||
provenance JSONB [not null, default: "'{}'::jsonb", note: "출처 메타데이터 (JSON)"]
|
||||
created_at TIMESTAMPTZ [not null, default: "NOW()", note: "DB 기록 시각 (UTC)"]
|
||||
|
||||
indexes {
|
||||
(created_at) [name: "idx_decision_result_history_created_at"]
|
||||
}
|
||||
|
||||
Note: "의사결정 결과 이력"
|
||||
}
|
||||
|
||||
Table engine_history.market_vs_engine_gap_history {
|
||||
id BIGSERIAL [pk, note: "자동 증가 ID"]
|
||||
gap_id TEXT [not null, note: "갭 ID"]
|
||||
observed_at TEXT [not null, note: "관측 일자 (YYYYMMDD)"]
|
||||
instrument_id TEXT [not null, note: "상품 ID"]
|
||||
metric_name TEXT [not null, note: "지표명"]
|
||||
market_value TEXT [not null, note: "시장값"]
|
||||
engine_value TEXT [not null, note: "엔진값"]
|
||||
gap_value TEXT [not null, note: "갭값 (절대값)"]
|
||||
gap_pct TEXT [not null, note: "갭 백분율 (%)"]
|
||||
source_version TEXT [not null, note: "소스 버전"]
|
||||
provenance JSONB [not null, default: "'{}'::jsonb", note: "출처 메타데이터 (JSON)"]
|
||||
created_at TIMESTAMPTZ [not null, default: "NOW()", note: "DB 기록 시각 (UTC)"]
|
||||
|
||||
indexes {
|
||||
(created_at) [name: "idx_market_vs_engine_gap_history_created_at"]
|
||||
}
|
||||
|
||||
Note: "시장 데이터 vs 엔진 계산 갭 분석 이력"
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Relationships (Logical, not enforced as FKs in DDL)
|
||||
// =============================================================================
|
||||
|
||||
Ref: quantengine.kis_collection_snapshots.run_id > quantengine.kis_collection_runs.run_id {
|
||||
// logical relationship: snapshots belong to a run
|
||||
}
|
||||
|
||||
Ref: quantengine.kis_collection_errors.run_id > quantengine.kis_collection_runs.run_id {
|
||||
// logical relationship: errors belong to a run
|
||||
}
|
||||
|
||||
Ref: quantengine.workspace_session.username > quantengine.workspace_account.username {
|
||||
// logical relationship: session belongs to a user
|
||||
}
|
||||
Reference in New Issue
Block a user