feat: add canonical decision learning service
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user