102 lines
3.2 KiB
C#
102 lines
3.2 KiB
C#
using System.Text.Json;
|
|
using QuantEngine.Core.Interfaces;
|
|
|
|
namespace QuantEngine.Application.Services;
|
|
|
|
/// <summary>
|
|
/// Canonical application path for recording factor evidence, decisions, and
|
|
/// realized outcomes in the normalized PostgreSQL learning store.
|
|
/// </summary>
|
|
public sealed class DecisionLearningService
|
|
{
|
|
private readonly INormalizedLearningStore _store;
|
|
|
|
public DecisionLearningService(INormalizedLearningStore store) => _store = store;
|
|
|
|
public async Task<Guid> RecordDecisionAsync(
|
|
string decisionKey,
|
|
DateTimeOffset decidedAt,
|
|
string instrumentId,
|
|
string action,
|
|
string gate,
|
|
decimal? score,
|
|
string sourceVersion,
|
|
IEnumerable<FactorEvidenceInput> 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);
|