feat: wire normalized learning store into dotnet
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
// QuantEngine Database Schema (DBML)
|
// QuantEngine Database Schema (DBML)
|
||||||
// DbUp 마이그레이션(V1~V3)과 1:1 동기화 — 마이그레이션 추가 시 이 파일도 반드시 갱신
|
// DbUp 마이그레이션(V1~V5)과 1:1 동기화 — 마이그레이션 추가 시 이 파일도 반드시 갱신
|
||||||
// (CLAUDE.md 규칙: schema 변경 → DBML + 문서 동기화)
|
// (CLAUDE.md 규칙: schema 변경 → DBML + 문서 동기화)
|
||||||
//
|
//
|
||||||
// 참고: Hangfire 스키마는 Hangfire.PostgreSql 라이브러리가 자동 생성
|
// 참고: Hangfire 스키마는 Hangfire.PostgreSql 라이브러리가 자동 생성
|
||||||
@@ -277,6 +277,12 @@ TableGroup "engine_history" {
|
|||||||
factor_output_history
|
factor_output_history
|
||||||
decision_result_history
|
decision_result_history
|
||||||
market_vs_engine_gap_history
|
market_vs_engine_gap_history
|
||||||
|
source_observation
|
||||||
|
factor_definition
|
||||||
|
factor_observation
|
||||||
|
decision_event
|
||||||
|
decision_factor_evidence
|
||||||
|
outcome_evaluation
|
||||||
}
|
}
|
||||||
|
|
||||||
Table engine_history.market_raw_history {
|
Table engine_history.market_raw_history {
|
||||||
@@ -375,6 +381,88 @@ Table engine_history.market_vs_engine_gap_history {
|
|||||||
Note: "시장 데이터 vs 엔진 계산 갭 분석 이력"
|
Note: "시장 데이터 vs 엔진 계산 갭 분석 이력"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Schema: engine_history (V5 normalized learning history)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
Table engine_history.source_observation {
|
||||||
|
observation_id UUID [pk]
|
||||||
|
observed_at TIMESTAMPTZ [not null]
|
||||||
|
instrument_id TEXT [not null]
|
||||||
|
source_name TEXT [not null]
|
||||||
|
source_version TEXT [not null]
|
||||||
|
payload JSONB [not null]
|
||||||
|
provenance JSONB [not null, default: "'{}'::jsonb"]
|
||||||
|
created_at TIMESTAMPTZ [not null, default: "NOW()"]
|
||||||
|
}
|
||||||
|
|
||||||
|
Table engine_history.factor_definition {
|
||||||
|
factor_id TEXT [not null]
|
||||||
|
factor_version TEXT [not null]
|
||||||
|
formula_id TEXT [not null]
|
||||||
|
effective_from TIMESTAMPTZ [not null]
|
||||||
|
effective_to TIMESTAMPTZ
|
||||||
|
definition JSONB [not null, default: "'{}'::jsonb"]
|
||||||
|
provenance JSONB [not null, default: "'{}'::jsonb"]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(factor_id, factor_version) [pk]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Table engine_history.factor_observation {
|
||||||
|
factor_observation_id UUID [pk]
|
||||||
|
observation_id UUID [not null]
|
||||||
|
factor_id TEXT [not null]
|
||||||
|
factor_version TEXT [not null]
|
||||||
|
observed_at TIMESTAMPTZ [not null]
|
||||||
|
numeric_value NUMERIC
|
||||||
|
text_value TEXT
|
||||||
|
gate TEXT [not null]
|
||||||
|
provenance JSONB [not null, default: "'{}'::jsonb"]
|
||||||
|
}
|
||||||
|
|
||||||
|
Table engine_history.decision_event {
|
||||||
|
decision_id UUID [pk]
|
||||||
|
decision_key TEXT [not null, unique]
|
||||||
|
decided_at TIMESTAMPTZ [not null]
|
||||||
|
instrument_id TEXT [not null]
|
||||||
|
action TEXT [not null]
|
||||||
|
gate TEXT [not null]
|
||||||
|
score NUMERIC
|
||||||
|
source_version TEXT [not null]
|
||||||
|
trace JSONB [not null, default: "'{}'::jsonb"]
|
||||||
|
provenance JSONB [not null, default: "'{}'::jsonb"]
|
||||||
|
created_at TIMESTAMPTZ [not null, default: "NOW()"]
|
||||||
|
}
|
||||||
|
|
||||||
|
Table engine_history.decision_factor_evidence {
|
||||||
|
decision_id UUID [not null]
|
||||||
|
factor_observation_id UUID [not null]
|
||||||
|
role TEXT [not null]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(decision_id, factor_observation_id) [pk]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Table engine_history.outcome_evaluation {
|
||||||
|
evaluation_id UUID [pk]
|
||||||
|
decision_id UUID [not null]
|
||||||
|
horizon_days INT [not null]
|
||||||
|
evaluated_at TIMESTAMPTZ [not null]
|
||||||
|
realized_return NUMERIC
|
||||||
|
benchmark_return NUMERIC
|
||||||
|
excess_return NUMERIC
|
||||||
|
outcome_class TEXT [not null]
|
||||||
|
evaluation_gate TEXT [not null]
|
||||||
|
provenance JSONB [not null, default: "'{}'::jsonb"]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(decision_id, horizon_days) [unique]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Relationships (Logical, not enforced as FKs in DDL)
|
// Relationships (Logical, not enforced as FKs in DDL)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -390,3 +478,9 @@ Ref: quantengine.kis_collection_errors.run_id > quantengine.kis_collection_runs.
|
|||||||
Ref: quantengine.workspace_session.username > quantengine.workspace_account.username {
|
Ref: quantengine.workspace_session.username > quantengine.workspace_account.username {
|
||||||
// logical relationship: session belongs to a user
|
// logical relationship: session belongs to a user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ref: engine_history.factor_observation.observation_id > engine_history.source_observation.observation_id
|
||||||
|
Ref: engine_history.factor_observation.(factor_id, factor_version) > engine_history.factor_definition.(factor_id, factor_version)
|
||||||
|
Ref: engine_history.decision_factor_evidence.decision_id > engine_history.decision_event.decision_id
|
||||||
|
Ref: engine_history.decision_factor_evidence.factor_observation_id > engine_history.factor_observation.factor_observation_id
|
||||||
|
Ref: engine_history.outcome_evaluation.decision_id > engine_history.decision_event.decision_id
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
namespace QuantEngine.Core.Interfaces;
|
||||||
|
|
||||||
|
public interface INormalizedLearningStore
|
||||||
|
{
|
||||||
|
Task<Guid> AppendSourceObservationAsync(SourceObservationRecord record);
|
||||||
|
Task<Guid> AppendFactorObservationAsync(FactorObservationRecord record);
|
||||||
|
Task<Guid> AppendDecisionAsync(DecisionEventRecord record);
|
||||||
|
Task AppendDecisionFactorEvidenceAsync(Guid decisionId, Guid factorObservationId, string role);
|
||||||
|
Task AppendOutcomeAsync(OutcomeEvaluationRecord record);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record SourceObservationRecord(
|
||||||
|
DateTimeOffset ObservedAt,
|
||||||
|
string InstrumentId,
|
||||||
|
string SourceName,
|
||||||
|
string SourceVersion,
|
||||||
|
string PayloadJson,
|
||||||
|
string ProvenanceJson);
|
||||||
|
|
||||||
|
public sealed record FactorObservationRecord(
|
||||||
|
Guid ObservationId,
|
||||||
|
Guid FactorObservationId,
|
||||||
|
string FactorId,
|
||||||
|
string FactorVersion,
|
||||||
|
DateTimeOffset ObservedAt,
|
||||||
|
decimal? NumericValue,
|
||||||
|
string? TextValue,
|
||||||
|
string Gate,
|
||||||
|
string ProvenanceJson);
|
||||||
|
|
||||||
|
public sealed record DecisionEventRecord(
|
||||||
|
string DecisionKey,
|
||||||
|
DateTimeOffset DecidedAt,
|
||||||
|
string InstrumentId,
|
||||||
|
string Action,
|
||||||
|
string Gate,
|
||||||
|
decimal? Score,
|
||||||
|
string SourceVersion,
|
||||||
|
string TraceJson,
|
||||||
|
string ProvenanceJson);
|
||||||
|
|
||||||
|
public sealed record OutcomeEvaluationRecord(
|
||||||
|
Guid DecisionId,
|
||||||
|
int HorizonDays,
|
||||||
|
DateTimeOffset EvaluatedAt,
|
||||||
|
decimal? RealizedReturn,
|
||||||
|
decimal? BenchmarkReturn,
|
||||||
|
decimal? ExcessReturn,
|
||||||
|
string OutcomeClass,
|
||||||
|
string EvaluationGate,
|
||||||
|
string ProvenanceJson);
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using Dapper;
|
||||||
|
using QuantEngine.Core.Interfaces;
|
||||||
|
using QuantEngine.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace QuantEngine.Infrastructure.Repositories;
|
||||||
|
|
||||||
|
public sealed class NormalizedLearningStore : INormalizedLearningStore
|
||||||
|
{
|
||||||
|
private readonly IDbConnectionFactory _connectionFactory;
|
||||||
|
|
||||||
|
public NormalizedLearningStore(IDbConnectionFactory connectionFactory) => _connectionFactory = connectionFactory;
|
||||||
|
|
||||||
|
public async Task<Guid> AppendSourceObservationAsync(SourceObservationRecord record)
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
using var conn = _connectionFactory.CreateConnection();
|
||||||
|
await conn.ExecuteAsync(@"INSERT INTO engine_history.source_observation
|
||||||
|
(observation_id, observed_at, instrument_id, source_name, source_version, payload, provenance)
|
||||||
|
VALUES (@Id, @ObservedAt, @InstrumentId, @SourceName, @SourceVersion,
|
||||||
|
CAST(@PayloadJson AS jsonb), CAST(@ProvenanceJson AS jsonb))", new { Id = id, record.ObservedAt, record.InstrumentId, record.SourceName, record.SourceVersion, record.PayloadJson, record.ProvenanceJson });
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Guid> AppendFactorObservationAsync(FactorObservationRecord record)
|
||||||
|
{
|
||||||
|
using var conn = _connectionFactory.CreateConnection();
|
||||||
|
await conn.ExecuteAsync(@"INSERT INTO engine_history.factor_observation
|
||||||
|
(factor_observation_id, observation_id, factor_id, factor_version, observed_at,
|
||||||
|
numeric_value, text_value, gate, provenance)
|
||||||
|
VALUES (@FactorObservationId, @ObservationId, @FactorId, @FactorVersion, @ObservedAt,
|
||||||
|
@NumericValue, @TextValue, @Gate, CAST(@ProvenanceJson AS jsonb))", new
|
||||||
|
{
|
||||||
|
record.FactorObservationId,
|
||||||
|
record.ObservationId,
|
||||||
|
record.FactorId,
|
||||||
|
record.FactorVersion,
|
||||||
|
record.ObservedAt,
|
||||||
|
record.NumericValue,
|
||||||
|
record.TextValue,
|
||||||
|
record.Gate,
|
||||||
|
record.ProvenanceJson
|
||||||
|
});
|
||||||
|
return record.FactorObservationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Guid> AppendDecisionAsync(DecisionEventRecord record)
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
using var conn = _connectionFactory.CreateConnection();
|
||||||
|
await conn.ExecuteAsync(@"INSERT INTO engine_history.decision_event
|
||||||
|
(decision_id, decision_key, decided_at, instrument_id, action, gate, score,
|
||||||
|
source_version, trace, provenance)
|
||||||
|
VALUES (@Id, @DecisionKey, @DecidedAt, @InstrumentId, @Action, @Gate, @Score,
|
||||||
|
@SourceVersion, CAST(@TraceJson AS jsonb), CAST(@ProvenanceJson AS jsonb))", new { Id = id, record.DecisionKey, record.DecidedAt, record.InstrumentId, record.Action, record.Gate, record.Score, record.SourceVersion, record.TraceJson, record.ProvenanceJson });
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AppendDecisionFactorEvidenceAsync(Guid decisionId, Guid factorObservationId, string role)
|
||||||
|
{
|
||||||
|
using var conn = _connectionFactory.CreateConnection();
|
||||||
|
await conn.ExecuteAsync(@"INSERT INTO engine_history.decision_factor_evidence
|
||||||
|
(decision_id, factor_observation_id, role) VALUES (@DecisionId, @FactorObservationId, @Role)", new { decisionId, factorObservationId, role });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AppendOutcomeAsync(OutcomeEvaluationRecord record)
|
||||||
|
{
|
||||||
|
using var conn = _connectionFactory.CreateConnection();
|
||||||
|
await conn.ExecuteAsync(@"INSERT INTO engine_history.outcome_evaluation
|
||||||
|
(decision_id, horizon_days, evaluated_at, realized_return, benchmark_return,
|
||||||
|
excess_return, outcome_class, evaluation_gate, provenance)
|
||||||
|
VALUES (@DecisionId, @HorizonDays, @EvaluatedAt, @RealizedReturn, @BenchmarkReturn,
|
||||||
|
@ExcessReturn, @OutcomeClass, @EvaluationGate, CAST(@ProvenanceJson AS jsonb))
|
||||||
|
ON CONFLICT (decision_id, horizon_days) DO UPDATE SET
|
||||||
|
evaluated_at = EXCLUDED.evaluated_at,
|
||||||
|
realized_return = EXCLUDED.realized_return,
|
||||||
|
benchmark_return = EXCLUDED.benchmark_return,
|
||||||
|
excess_return = EXCLUDED.excess_return,
|
||||||
|
outcome_class = EXCLUDED.outcome_class,
|
||||||
|
evaluation_gate = EXCLUDED.evaluation_gate,
|
||||||
|
provenance = EXCLUDED.provenance", record);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -96,6 +96,7 @@ try
|
|||||||
// Repository Services
|
// Repository Services
|
||||||
builder.Services.AddScoped<IWorkspaceRepository, WorkspaceRepository>();
|
builder.Services.AddScoped<IWorkspaceRepository, WorkspaceRepository>();
|
||||||
builder.Services.AddScoped<IPostgresqlHistoryStore, PostgresqlHistoryStore>();
|
builder.Services.AddScoped<IPostgresqlHistoryStore, PostgresqlHistoryStore>();
|
||||||
|
builder.Services.AddScoped<INormalizedLearningStore, NormalizedLearningStore>();
|
||||||
builder.Services.AddScoped<IPostgresqlHistorySnapshotReader, PostgresqlHistorySnapshotReader>();
|
builder.Services.AddScoped<IPostgresqlHistorySnapshotReader, PostgresqlHistorySnapshotReader>();
|
||||||
builder.Services.AddScoped<HistoryIngestionService>();
|
builder.Services.AddScoped<HistoryIngestionService>();
|
||||||
builder.Services.AddScoped<ICollectionRepository, CollectionRepository>();
|
builder.Services.AddScoped<ICollectionRepository, CollectionRepository>();
|
||||||
|
|||||||
Reference in New Issue
Block a user