diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 7c2d44cc..a6c4d9ff 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -104,6 +104,9 @@ jobs: - name: Validate Schema Model Generation run: python3 tools/generate_schema_model_generation_evidence_v1.py && python3 tools/validate_schema_model_generation_v1.py + - name: Validate Market Time Series Schema + run: python3 tools/validate_market_time_series_schema_v1.py + - name: Validate Quant Engine WBS run: python3 tools/validate_quant_engine_wbs_v1.py diff --git a/package.json b/package.json index 78c6083a..781e483d 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "validate:dotnet-cutover": "python tools/validate_dotnet_postgresql_json_cutover_v1.py", "validate:schema-model": "python tools/generate_schema_model_generation_evidence_v1.py && python tools/validate_schema_model_generation_v1.py", "validate:runtime-settings": "python tools/validate_runtime_connection_settings_immutability_v1.py", + "validate:market-schema": "python tools/validate_market_time_series_schema_v1.py", "test:e2e": "playwright test --project=chromium", "test:evidence": "playwright test --project=evidence" }, diff --git a/spec/64_market_time_series_schema.yaml b/spec/64_market_time_series_schema.yaml new file mode 100644 index 00000000..3b75da71 --- /dev/null +++ b/spec/64_market_time_series_schema.yaml @@ -0,0 +1,10 @@ +formula_id: MARKET_TIME_SERIES_SCHEMA_V1 +version: 1 +authority: spec/60_quant_engine_wbs.yaml +migration: src/dotnet/QuantEngine.Infrastructure/Migrations/V6__Add_Market_Time_Series.sql +dbml: docs/db/quantengine.dbml +tables: + - quantengine.price_history_daily + - quantengine.macro_history_daily +runtime_database_status: DATA_GATED +verification: python tools/validate_market_time_series_schema_v1.py diff --git a/tools/validate_market_time_series_schema_v1.py b/tools/validate_market_time_series_schema_v1.py new file mode 100644 index 00000000..04708cf7 --- /dev/null +++ b/tools/validate_market_time_series_schema_v1.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import json +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +MIGRATION = ROOT / "src/dotnet/QuantEngine.Infrastructure/Migrations/V6__Add_Market_Time_Series.sql" +DBML = ROOT / "docs/db/quantengine.dbml" +REPORT = ROOT / "Temp/market_time_series_schema_v1.json" +TABLES = ("quantengine.price_history_daily", "quantengine.macro_history_daily") + + +def main() -> int: + migration = MIGRATION.read_text(encoding="utf-8") if MIGRATION.exists() else "" + dbml = DBML.read_text(encoding="utf-8") if DBML.exists() else "" + checks = { + "migration_exists": MIGRATION.exists(), + "dbml_exists": DBML.exists(), + "migration_tables": all(re.search(rf"CREATE TABLE IF NOT EXISTS {re.escape(t)}\b", migration) for t in TABLES), + "dbml_tables": all(re.search(rf"Table {re.escape(t)}\s*\{{", dbml) for t in TABLES), + "price_primary_key": "PRIMARY KEY (ticker, trade_date)" in migration, + "macro_primary_key": "PRIMARY KEY (symbol, trade_date)" in migration, + "dbup_embedded_glob": "Migrations/**/*.sql" in (ROOT / "src/dotnet/QuantEngine.Infrastructure/QuantEngine.Infrastructure.csproj").read_text(encoding="utf-8"), + } + payload = { + "formula_id": "MARKET_TIME_SERIES_SCHEMA_V1", + "gate": "PASS" if all(checks.values()) else "FAIL", + "expected_tables": list(TABLES), + "checks": checks, + "runtime_database_query": "DATA_GATED", + } + REPORT.parent.mkdir(parents=True, exist_ok=True) + REPORT.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(payload, ensure_ascii=False, indent=2)) + return 0 if payload["gate"] == "PASS" else 1 + + +if __name__ == "__main__": + raise SystemExit(main())