feat: postgres history-first 계약과 적재 경로 추가

- PostgreSQL history contract와 schema/validator를 추가했습니다.
- .NET history store, snapshot reader, repository, migration을 연결했습니다.
- history-first 운영 모델 문서와 daily signal tracking 문구를 정리했습니다.
This commit is contained in:
2026-06-26 14:17:04 +09:00
parent 7e0c0b6c8f
commit 8f13bb4a48
17 changed files with 707 additions and 17 deletions
@@ -0,0 +1,82 @@
"""PostgreSQL history store for engine provenance tracking.
This module is intentionally thin: it owns connection, table routing, append
operations, and snapshot reads for the history-first operating model.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable
DOMAIN_TABLES = {
"market_raw_history": "engine_history.market_raw_history",
"factor_version_history": "engine_history.factor_version_history",
"factor_output_history": "engine_history.factor_output_history",
"decision_result_history": "engine_history.decision_result_history",
"market_vs_engine_gap_history": "engine_history.market_vs_engine_gap_history",
}
@dataclass(frozen=True)
class HistoryRow:
domain: str
payload: dict[str, Any]
def _is_pg_dsn(value: str) -> bool:
return value.startswith("postgresql://") or value.startswith("postgres://")
def connect(dsn_or_path: str | Path) -> Any:
value = str(dsn_or_path)
if _is_pg_dsn(value):
try:
import psycopg2
except ImportError as exc:
raise ImportError("PostgreSQL DSN requires psycopg2") from exc
return psycopg2.connect(value)
raise ValueError("postgresql_history_store_v1 only accepts a PostgreSQL DSN")
def ensure_schema(conn: Any) -> None:
sql = """
CREATE SCHEMA IF NOT EXISTS engine_history;
"""
cur = conn.cursor()
cur.execute(sql)
conn.commit()
def append_row(conn: Any, domain: str, payload: dict[str, Any]) -> None:
table = DOMAIN_TABLES.get(domain)
if not table:
raise KeyError(f"unknown domain: {domain}")
ensure_schema(conn)
keys = [k for k in payload.keys() if k != "id"]
cols = ", ".join(keys + ["provenance"])
placeholders = ", ".join(["%s"] * (len(keys) + 1))
values = [json.dumps(payload.get(k), ensure_ascii=False, default=str) if isinstance(payload.get(k), (dict, list)) else payload.get(k) for k in keys]
values.append(json.dumps(payload.get("provenance") or {}, ensure_ascii=False, default=str))
sql = f"INSERT INTO {table} ({cols}) VALUES ({placeholders})"
cur = conn.cursor()
cur.execute(sql, values)
conn.commit()
def append_rows(conn: Any, rows: Iterable[HistoryRow]) -> None:
for row in rows:
append_row(conn, row.domain, row.payload)
def snapshot_table(conn: Any, domain: str, limit: int = 1000) -> list[dict[str, Any]]:
table = DOMAIN_TABLES.get(domain)
if not table:
raise KeyError(f"unknown domain: {domain}")
cur = conn.cursor()
cur.execute(f"SELECT * FROM {table} ORDER BY created_at DESC LIMIT %s", (limit,))
columns = [col[0] for col in cur.description]
return [dict(zip(columns, row)) for row in cur.fetchall()]