using System.Text.Json; using QuantEngine.Core.Interfaces; namespace QuantEngine.Application.Services; /// /// Canonical application path for recording factor evidence, decisions, and /// realized outcomes in the normalized PostgreSQL learning store. /// public sealed class DecisionLearningService { private readonly INormalizedLearningStore _store; public DecisionLearningService(INormalizedLearningStore store) => _store = store; public async Task RecordDecisionAsync( string decisionKey, DateTimeOffset decidedAt, string instrumentId, string action, string gate, decimal? score, string sourceVersion, IEnumerable factors, object? trace = null, object? provenance = null) { var decisionId = await _store.AppendDecisionAsync(new DecisionEventRecord( decisionKey, decidedAt, instrumentId, action, gate, score, sourceVersion, JsonSerializer.Serialize(trace ?? new { }), JsonSerializer.Serialize(provenance ?? new { }))); foreach (var factor in factors) { var observationId = await _store.AppendSourceObservationAsync(new SourceObservationRecord( factor.ObservedAt, instrumentId, factor.SourceName, sourceVersion, factor.PayloadJson, factor.ProvenanceJson)); var factorObservationId = await _store.AppendFactorObservationAsync(new FactorObservationRecord( observationId, factor.FactorObservationId, factor.FactorId, factor.FactorVersion, factor.ObservedAt, factor.NumericValue, factor.TextValue, factor.Gate, factor.ProvenanceJson)); await _store.AppendDecisionFactorEvidenceAsync(decisionId, factorObservationId, factor.Role); } return decisionId; } public Task RecordOutcomeAsync( Guid decisionId, int horizonDays, DateTimeOffset evaluatedAt, decimal? realizedReturn, decimal? benchmarkReturn, string outcomeClass, string evaluationGate, object? provenance = null) { decimal? excessReturn = realizedReturn.HasValue && benchmarkReturn.HasValue ? realizedReturn.Value - benchmarkReturn.Value : null; return _store.AppendOutcomeAsync(new OutcomeEvaluationRecord( decisionId, horizonDays, evaluatedAt, realizedReturn, benchmarkReturn, excessReturn, outcomeClass, evaluationGate, JsonSerializer.Serialize(provenance ?? new { }))); } } public sealed record FactorEvidenceInput( Guid FactorObservationId, string FactorId, string FactorVersion, DateTimeOffset ObservedAt, decimal? NumericValue, string? TextValue, string Gate, string Role, string SourceName, string PayloadJson, string ProvenanceJson);