fix: security, data-integrity, and doc-drift findings from repo audit
Consolidates duplicate KIS API client implementations (governance tests were exercising an unused class instead of the one actually running in production), closes a SQL injection path in the DB admin page, fixes a migration that used MySQL-only syntax and had never actually applied (confirmed against production), resyncs docs/db/quantengine.dbml with all migrations, and removes a duplicate OMS·WMS·ERP frontend tree in favor of src/frontend/. Also corrects several unverifiable/inflated claims in the OMS planning docs and realigns CI/CD and architecture documentation with what's actually in the repo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+249
-2
@@ -1,8 +1,14 @@
|
||||
// =============================================================================
|
||||
// QuantEngine Database Schema (DBML)
|
||||
// DbUp 마이그레이션(V1~V5)과 1:1 동기화 — 마이그레이션 추가 시 이 파일도 반드시 갱신
|
||||
// DbUp 마이그레이션(V1~V8, V003, V004)과 1:1 동기화 — 마이그레이션 추가 시 이 파일도 반드시 갱신
|
||||
// (CLAUDE.md 규칙: schema 변경 → DBML + 문서 동기화)
|
||||
//
|
||||
// 마이그레이션 파일명 규칙 2종 혼재 (2026-07-30 발견, 미해결):
|
||||
// V1__Name.sql .. V8__Name.sql (더블언더스코어, zero-pad 없음)
|
||||
// V003_name.sql, V004_name.sql (싱글언더스코어, zero-pad)
|
||||
// DbUp는 파일명 알파벳순으로 실행하므로 "V003" < "V1" 순서로 적용됨 — 신규 마이그레이션은
|
||||
// 반드시 하나의 규칙(권장: V{n}__Name.sql)만 사용할 것.
|
||||
//
|
||||
// 참고: Hangfire 스키마는 Hangfire.PostgreSql 라이브러리가 자동 생성
|
||||
// (DbUp 마이그레이션으로 관리하지 않음, 여기서도 제외)
|
||||
// =============================================================================
|
||||
@@ -382,7 +388,7 @@ Table engine_history.market_vs_engine_gap_history {
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Schema: engine_history (V5 normalized learning history)
|
||||
// V6: Market Time Series (quantengine schema)
|
||||
// =============================================================================
|
||||
|
||||
Table quantengine.price_history_daily {
|
||||
@@ -415,6 +421,10 @@ Table quantengine.macro_history_daily {
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// V5: Normalized Learning History (engine_history schema, event-sourcing style)
|
||||
// =============================================================================
|
||||
|
||||
Table engine_history.source_observation {
|
||||
observation_id UUID [pk]
|
||||
observed_at TIMESTAMPTZ [not null]
|
||||
@@ -493,6 +503,243 @@ Table engine_history.outcome_evaluation {
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// V003: Audit Trail Tables (quantengine schema, 2026-07-24)
|
||||
//
|
||||
// 2026-07-30 확정 (실제 프로덕션 DB 조회로 검증): 이 마이그레이션은 CREATE TABLE 안에
|
||||
// MySQL 전용 인라인 "INDEX name (cols)" 구문을 사용해 PostgreSQL에서 문법 오류로 실패했다.
|
||||
// quantengine.schemaversions(DbUp 저널)에 V003이 아예 기록되어 있지 않고, 아래 3개 테이블도
|
||||
// 프로덕션에 실제로 존재하지 않음을 직접 확인했다. V003_add_audit_trail_tables.sql의 인라인
|
||||
// INDEX 구문은 이미 별도 CREATE INDEX 문으로 수정됐으므로, 다음 배포 시 DbUp가 이 마이그레이션을
|
||||
// 최초로 실행해 아래 3개 테이블을 생성할 것이다.
|
||||
// =============================================================================
|
||||
|
||||
Table quantengine.kis_collection_runs_audit {
|
||||
id BIGSERIAL [pk]
|
||||
run_id "UUID" [not null]
|
||||
action "VARCHAR(10)" [not null, note: "INSERT/UPDATE/DELETE"]
|
||||
changed_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
|
||||
changed_by "VARCHAR(256)" [default: "CURRENT_USER"]
|
||||
change_reason TEXT
|
||||
old_values JSONB
|
||||
new_values JSONB
|
||||
created_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
|
||||
|
||||
Note: "kis_collection_runs 변경 이력 (트리거 자동 기록) — ⚠️ 마이그레이션 문법 오류로 실제 생성 여부 미확인"
|
||||
}
|
||||
|
||||
Table quantengine.kis_collection_snapshots_audit {
|
||||
id BIGSERIAL [pk]
|
||||
snapshot_id "UUID" [not null]
|
||||
action "VARCHAR(10)" [not null, note: "INSERT/UPDATE/DELETE"]
|
||||
changed_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
|
||||
changed_by "VARCHAR(256)" [default: "CURRENT_USER"]
|
||||
change_reason TEXT
|
||||
old_values JSONB
|
||||
new_values JSONB
|
||||
created_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
|
||||
|
||||
Note: "kis_collection_snapshots 변경 이력 — ⚠️ 마이그레이션 문법 오류로 실제 생성 여부 미확인"
|
||||
}
|
||||
|
||||
Table quantengine.kis_collection_errors_audit {
|
||||
id BIGSERIAL [pk]
|
||||
error_id "UUID" [not null]
|
||||
action "VARCHAR(10)" [not null, note: "INSERT/UPDATE/DELETE"]
|
||||
changed_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
|
||||
changed_by "VARCHAR(256)" [default: "CURRENT_USER"]
|
||||
change_reason TEXT
|
||||
old_values JSONB
|
||||
new_values JSONB
|
||||
created_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
|
||||
|
||||
Note: "kis_collection_errors 변경 이력 — ⚠️ 마이그레이션 문법 오류로 실제 생성 여부 미확인"
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// V004: 3NF Normalization / Star Schema (quantengine schema, Adapter 패턴으로
|
||||
// 기존 kis_collection_snapshots와 병행 운영 — 마이그레이션 자체 주석에 명시됨)
|
||||
// =============================================================================
|
||||
|
||||
Table quantengine.stocks {
|
||||
id SERIAL [pk]
|
||||
ticker "VARCHAR(10)" [unique, not null]
|
||||
name "VARCHAR(255)"
|
||||
sector "VARCHAR(50)"
|
||||
market "VARCHAR(20)" [note: "KOSPI/KOSDAQ 등"]
|
||||
created_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
|
||||
updated_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
|
||||
|
||||
Note: "종목 차원 테이블 (Star Schema dimension)"
|
||||
}
|
||||
|
||||
Table quantengine.sources {
|
||||
id SERIAL [pk]
|
||||
name "VARCHAR(50)" [unique, not null]
|
||||
priority INT [not null, note: "1=주 소스, 2 이상=폴백"]
|
||||
fallback_to_id INT [ref: > quantengine.sources.id]
|
||||
created_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
|
||||
|
||||
Note: "데이터 소스 차원 테이블 (KIS→Naver→Yahoo→OpenDART 폴백 체인)"
|
||||
}
|
||||
|
||||
Table quantengine.market_data {
|
||||
id BIGSERIAL [pk]
|
||||
stock_id INT [not null, ref: > quantengine.stocks.id]
|
||||
source_id INT [not null, ref: > quantengine.sources.id]
|
||||
price DECIMAL [not null]
|
||||
bid DECIMAL
|
||||
ask DECIMAL
|
||||
volume BIGINT
|
||||
collected_at TIMESTAMPTZ [not null]
|
||||
created_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
|
||||
collection_run_id "UUID" [note: "kis_collection_runs 추적용"]
|
||||
|
||||
Note: "정규화된 시장 데이터 팩트 테이블 (Star Schema fact)"
|
||||
}
|
||||
|
||||
Table quantengine.kis_collection_snapshots_v2 {
|
||||
id "UUID" [pk]
|
||||
run_id "UUID" [not null, ref: > quantengine.kis_collection_runs.run_id]
|
||||
stock_id INT [not null, ref: > quantengine.stocks.id]
|
||||
market_data_id BIGINT [ref: > quantengine.market_data.id, note: "조회 성능을 위한 의도적 역정규화"]
|
||||
created_at TIMESTAMPTZ [not null, default: "CURRENT_TIMESTAMP"]
|
||||
|
||||
Note: "정규화된 kis_collection_snapshots — 레거시 kis_collection_snapshots와 Adapter 패턴으로 병행 운영, 완전 전환 여부 미확인"
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// V8: PostgreSQL History-First Operating Model (quantengine schema)
|
||||
//
|
||||
// ⚠️ 스키마 충돌 주의: 아래 4개 테이블(market_raw_history, factor_version_history,
|
||||
// factor_output_history, decision_result_history)은 engine_history 스키마(V3, 위 참고)에
|
||||
// 이미 동일한 이름으로 존재한다. 컬럼 구조를 대조한 결과 같은 테이블의 재적용이 아니라
|
||||
// 서로 다른 두 가지 설계다:
|
||||
// - engine_history.*: EAV형 원본 관측 이력 (field_name/field_value 페어)
|
||||
// - quantengine.*(이 섹션): OHLCV 와이드 테이블 / 팩터ID-스코어 구조
|
||||
// 둘 다 실제 마이그레이션 파일에 존재하므로 DBML에는 두 스키마 버전을 모두 남긴다.
|
||||
// 어느 쪽이 정본인지, 혹은 통합이 필요한지는 별도 아키텍처 결정 필요 (이번 작업 범위 밖).
|
||||
// =============================================================================
|
||||
|
||||
Table quantengine.market_raw_history {
|
||||
id BIGSERIAL [pk]
|
||||
ticker "VARCHAR(32)" [not null]
|
||||
as_of_date "VARCHAR(10)" [not null]
|
||||
open_price "NUMERIC(18,4)"
|
||||
high_price "NUMERIC(18,4)"
|
||||
low_price "NUMERIC(18,4)"
|
||||
close_price "NUMERIC(18,4)" [not null]
|
||||
volume BIGINT
|
||||
nav_price "NUMERIC(18,4)"
|
||||
disparate_ratio "NUMERIC(10,6)"
|
||||
tracking_error "NUMERIC(10,6)"
|
||||
aum_krw "NUMERIC(20,2)"
|
||||
raw_payload JSONB [not null]
|
||||
provenance JSONB [not null]
|
||||
created_at TIMESTAMPTZ [default: "NOW()"]
|
||||
|
||||
indexes {
|
||||
(ticker, as_of_date) [unique, name: "uk_market_raw_ticker_date"]
|
||||
}
|
||||
|
||||
Note: "OHLCV 와이드 테이블 — engine_history.market_raw_history(EAV형)와는 별개 설계"
|
||||
}
|
||||
|
||||
Table quantengine.factor_version_history {
|
||||
factor_id "VARCHAR(64)" [pk]
|
||||
formula_name "VARCHAR(128)" [not null]
|
||||
version "VARCHAR(32)" [not null]
|
||||
category "VARCHAR(64)" [not null]
|
||||
calibration_state "VARCHAR(32)" [not null, default: "'UNTESTED'"]
|
||||
threshold_params JSONB [not null]
|
||||
description TEXT
|
||||
updated_at TIMESTAMPTZ [default: "NOW()"]
|
||||
|
||||
Note: "팩터 정의 — engine_history.factor_version_history와는 별개 설계 (PK가 factor_id 단독, 버전 이력 미보존)"
|
||||
}
|
||||
|
||||
Table quantengine.factor_output_history {
|
||||
id BIGSERIAL [pk]
|
||||
run_id "VARCHAR(64)" [not null]
|
||||
ticker "VARCHAR(32)" [not null]
|
||||
as_of_date "VARCHAR(10)" [not null]
|
||||
factor_id "VARCHAR(64)" [not null, ref: > quantengine.factor_version_history.factor_id]
|
||||
score "NUMERIC(10,4)"
|
||||
calculation_state "VARCHAR(32)" [not null]
|
||||
provenance JSONB [not null]
|
||||
created_at TIMESTAMPTZ [default: "NOW()"]
|
||||
|
||||
Note: "팩터 계산 결과 — engine_history.factor_output_history와는 별개 설계"
|
||||
}
|
||||
|
||||
Table quantengine.decision_result_history {
|
||||
id BIGSERIAL [pk]
|
||||
run_id "VARCHAR(64)" [unique, not null]
|
||||
as_of_date "VARCHAR(10)" [not null]
|
||||
market_regime "VARCHAR(32)" [not null]
|
||||
portfolio_health "VARCHAR(32)" [not null]
|
||||
rebalance_required BOOLEAN [not null, default: "false"]
|
||||
mid_check_required BOOLEAN [not null, default: "false"]
|
||||
total_asset_krw "NUMERIC(20,2)" [not null]
|
||||
d2_cash_krw "NUMERIC(20,2)" [not null]
|
||||
decision_packet_json JSONB [not null]
|
||||
created_at TIMESTAMPTZ [default: "NOW()"]
|
||||
|
||||
Note: "의사결정 패킷 이력 — engine_history.decision_result_history와는 별개 설계"
|
||||
}
|
||||
|
||||
Table quantengine.order_waterfall_execution_history {
|
||||
id BIGSERIAL [pk]
|
||||
run_id "VARCHAR(64)" [not null, ref: > quantengine.decision_result_history.run_id]
|
||||
ticker "VARCHAR(32)" [not null]
|
||||
sell_priority_rank INT [not null]
|
||||
waterfall_stage "VARCHAR(64)" [not null]
|
||||
action "VARCHAR(16)" [not null]
|
||||
target_qty INT [not null]
|
||||
executed_qty INT [default: "0"]
|
||||
target_price "NUMERIC(18,4)"
|
||||
executed_price "NUMERIC(18,4)"
|
||||
bid_ask_spread_bps "NUMERIC(10,2)"
|
||||
slippage_bps "NUMERIC(10,2)"
|
||||
status "VARCHAR(32)" [not null]
|
||||
rationale TEXT
|
||||
created_at TIMESTAMPTZ [default: "NOW()"]
|
||||
|
||||
Note: "매도 워터폴 실행 이력"
|
||||
}
|
||||
|
||||
Table quantengine.shadow_ledger_history {
|
||||
id BIGSERIAL [pk]
|
||||
run_id "VARCHAR(64)" [not null, ref: > quantengine.decision_result_history.run_id]
|
||||
ticker "VARCHAR(32)" [not null]
|
||||
blocked_gate "VARCHAR(64)" [not null]
|
||||
blocked_reason TEXT [not null]
|
||||
shadow_price "NUMERIC(18,4)" [not null]
|
||||
shadow_qty INT [not null]
|
||||
shadow_tp_price "NUMERIC(18,4)"
|
||||
shadow_sl_price "NUMERIC(18,4)"
|
||||
created_at TIMESTAMPTZ [default: "NOW()"]
|
||||
|
||||
Note: "게이트에 막힌 주문의 가상 체결 감사 기록 (Shadow Ledger)"
|
||||
}
|
||||
|
||||
Table quantengine.scheduler_state_history {
|
||||
id BIGSERIAL [pk]
|
||||
task_name "VARCHAR(64)" [not null]
|
||||
execution_id "VARCHAR(64)" [unique, not null]
|
||||
state "VARCHAR(32)" [not null]
|
||||
started_at TIMESTAMPTZ [not null, default: "NOW()"]
|
||||
finished_at TIMESTAMPTZ
|
||||
error_message TEXT
|
||||
lock_token "VARCHAR(64)"
|
||||
|
||||
indexes {
|
||||
(task_name, state) [name: "idx_scheduler_state_task"]
|
||||
}
|
||||
|
||||
Note: "스케줄러 작업 상태 머신 이력"
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Relationships (Logical, not enforced as FKs in DDL)
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user