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:
@@ -156,6 +156,87 @@ namespace QuantEngine.Infrastructure.Data
|
||||
PRIMARY KEY (domain, target_ref)
|
||||
);
|
||||
");
|
||||
|
||||
// 10. engine_history schema and tables
|
||||
conn.Execute(@"
|
||||
CREATE SCHEMA IF NOT EXISTS engine_history;
|
||||
");
|
||||
conn.Execute(@"
|
||||
CREATE TABLE IF NOT EXISTS engine_history.market_raw_history (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
source_id TEXT NOT NULL,
|
||||
observed_at TEXT NOT NULL,
|
||||
source_name TEXT NOT NULL,
|
||||
instrument_id TEXT NOT NULL,
|
||||
field_name TEXT NOT NULL,
|
||||
field_value TEXT NOT NULL,
|
||||
unit TEXT NOT NULL,
|
||||
provenance JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_market_raw_history_created_at ON engine_history.market_raw_history (created_at DESC);
|
||||
");
|
||||
conn.Execute(@"
|
||||
CREATE TABLE IF NOT EXISTS engine_history.factor_version_history (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
factor_id TEXT NOT NULL,
|
||||
factor_version TEXT NOT NULL,
|
||||
effective_from TEXT NOT NULL,
|
||||
effective_to TEXT NOT NULL,
|
||||
formula_id TEXT NOT NULL,
|
||||
source_version TEXT NOT NULL,
|
||||
provenance JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_factor_version_history_created_at ON engine_history.factor_version_history (created_at DESC);
|
||||
");
|
||||
conn.Execute(@"
|
||||
CREATE TABLE IF NOT EXISTS engine_history.factor_output_history (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
factor_output_id TEXT NOT NULL,
|
||||
observed_at TEXT NOT NULL,
|
||||
factor_id TEXT NOT NULL,
|
||||
factor_version TEXT NOT NULL,
|
||||
output_value TEXT NOT NULL,
|
||||
output_gate TEXT NOT NULL,
|
||||
source_version TEXT NOT NULL,
|
||||
provenance JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_factor_output_history_created_at ON engine_history.factor_output_history (created_at DESC);
|
||||
");
|
||||
conn.Execute(@"
|
||||
CREATE TABLE IF NOT EXISTS engine_history.decision_result_history (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
decision_id TEXT NOT NULL,
|
||||
decided_at TEXT NOT NULL,
|
||||
instrument_id TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
gate TEXT NOT NULL,
|
||||
score TEXT NOT NULL,
|
||||
source_version TEXT NOT NULL,
|
||||
provenance JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_decision_result_history_created_at ON engine_history.decision_result_history (created_at DESC);
|
||||
");
|
||||
conn.Execute(@"
|
||||
CREATE TABLE IF NOT EXISTS engine_history.market_vs_engine_gap_history (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
gap_id TEXT NOT NULL,
|
||||
observed_at TEXT NOT NULL,
|
||||
instrument_id TEXT NOT NULL,
|
||||
metric_name TEXT NOT NULL,
|
||||
market_value TEXT NOT NULL,
|
||||
engine_value TEXT NOT NULL,
|
||||
gap_value TEXT NOT NULL,
|
||||
gap_pct TEXT NOT NULL,
|
||||
source_version TEXT NOT NULL,
|
||||
provenance JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_market_vs_engine_gap_history_created_at ON engine_history.market_vs_engine_gap_history (created_at DESC);
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Data;
|
||||
using System.Text.Json;
|
||||
using Dapper;
|
||||
using QuantEngine.Infrastructure.Data;
|
||||
using QuantEngine.Core.Interfaces;
|
||||
|
||||
namespace QuantEngine.Infrastructure.Repositories
|
||||
{
|
||||
public class PostgresqlHistoryStore : IPostgresqlHistoryStore
|
||||
{
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, string[]> DomainColumns = new Dictionary<string, string[]>
|
||||
{
|
||||
["market_raw_history"] = new[] { "source_id", "observed_at", "source_name", "instrument_id", "field_name", "field_value", "unit" },
|
||||
["factor_version_history"] = new[] { "factor_id", "factor_version", "effective_from", "effective_to", "formula_id", "source_version" },
|
||||
["factor_output_history"] = new[] { "factor_output_id", "observed_at", "factor_id", "factor_version", "output_value", "output_gate", "source_version" },
|
||||
["decision_result_history"] = new[] { "decision_id", "decided_at", "instrument_id", "action", "gate", "score", "source_version" },
|
||||
["market_vs_engine_gap_history"] = new[] { "gap_id", "observed_at", "instrument_id", "metric_name", "market_value", "engine_value", "gap_value", "gap_pct", "source_version" }
|
||||
};
|
||||
|
||||
public PostgresqlHistoryStore(IDbConnectionFactory connectionFactory)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public async Task<int> AppendAsync(string domain, IDictionary<string, object?> payload)
|
||||
{
|
||||
if (!DomainColumns.TryGetValue(domain, out var columns))
|
||||
throw new ArgumentException($"Unsupported history domain: {domain}", nameof(domain));
|
||||
|
||||
using var conn = _connectionFactory.CreateConnection();
|
||||
conn.Open();
|
||||
|
||||
var values = new DynamicParameters();
|
||||
var insertColumns = new List<string>(columns.Length + 1);
|
||||
var placeholders = new List<string>(columns.Length + 1);
|
||||
foreach (var column in columns)
|
||||
{
|
||||
insertColumns.Add(column);
|
||||
placeholders.Add($"@{column}");
|
||||
values.Add(column, payload.TryGetValue(column, out var value) ? value : null);
|
||||
}
|
||||
|
||||
insertColumns.Add("provenance");
|
||||
placeholders.Add("@provenance");
|
||||
var provenance = payload.TryGetValue("provenance", out var provenanceValue) ? provenanceValue : new Dictionary<string, object?>();
|
||||
values.Add("provenance", provenance is string s ? s : JsonSerializer.Serialize(provenance));
|
||||
|
||||
var sql = $@"INSERT INTO engine_history.{domain} ({string.Join(", ", insertColumns)}) VALUES ({string.Join(", ", placeholders)})";
|
||||
return await conn.ExecuteAsync(sql, values);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<IDictionary<string, object?>>> SnapshotAsync(string domain, int limit = 500)
|
||||
{
|
||||
if (!DomainColumns.ContainsKey(domain))
|
||||
throw new ArgumentException($"Unsupported history domain: {domain}", nameof(domain));
|
||||
|
||||
using var conn = _connectionFactory.CreateConnection();
|
||||
conn.Open();
|
||||
|
||||
var sql = $@"SELECT * FROM engine_history.{domain} ORDER BY created_at DESC LIMIT @Limit";
|
||||
var rows = await conn.QueryAsync(sql, new { Limit = limit });
|
||||
return rows.Select(row => (IDictionary<string, object?>)row).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user