Files
QuantEngineByItz/src/dotnet/QuantEngine.Application/Services/DecisionLearningService.cs
T
kjh2064 5b9f870ad6
Validators (Pushes and Pull Requests) / validate-ui-and-storage (push) Successful in 17s
Validators (Pushes and Pull Requests) / validate-core (push) Failing after 1m43s
refactor(dotnet): normalize decision learning records
2026-07-13 01:19:55 +09:00

138 lines
4.8 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)
{
decisionKey = RequireValue(decisionKey, nameof(decisionKey));
instrumentId = RequireValue(instrumentId, nameof(instrumentId));
action = RequireValue(action, nameof(action));
gate = RequireValue(gate, nameof(gate));
sourceVersion = RequireValue(sourceVersion, nameof(sourceVersion));
var decisionId = await _store.AppendDecisionAsync(new DecisionEventRecord(
decisionKey,
decidedAt,
instrumentId,
action,
gate,
score,
sourceVersion,
SerializeJson(trace),
SerializeJson(provenance)));
foreach (var factor in factors ?? throw new ArgumentNullException(nameof(factors)))
{
var normalizedFactor = NormalizeFactor(factor);
var observationId = await _store.AppendSourceObservationAsync(new SourceObservationRecord(
normalizedFactor.ObservedAt,
instrumentId,
normalizedFactor.SourceName,
sourceVersion,
normalizedFactor.PayloadJson,
normalizedFactor.ProvenanceJson));
var factorObservationId = await _store.AppendFactorObservationAsync(new FactorObservationRecord(
observationId,
normalizedFactor.FactorObservationId,
normalizedFactor.FactorId,
normalizedFactor.FactorVersion,
normalizedFactor.ObservedAt,
normalizedFactor.NumericValue,
normalizedFactor.TextValue,
normalizedFactor.Gate,
normalizedFactor.ProvenanceJson));
await _store.AppendDecisionFactorEvidenceAsync(decisionId, factorObservationId, normalizedFactor.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,
SerializeJson(provenance)));
}
private static string SerializeJson(object? value)
=> JsonSerializer.Serialize(value ?? new { });
private static string RequireValue(string value, string parameterName)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Value is required.", parameterName);
}
return value.Trim();
}
private static FactorEvidenceInput NormalizeFactor(FactorEvidenceInput factor)
{
ArgumentNullException.ThrowIfNull(factor);
return factor with
{
FactorId = RequireValue(factor.FactorId, nameof(factor.FactorId)),
FactorVersion = RequireValue(factor.FactorVersion, nameof(factor.FactorVersion)),
SourceName = RequireValue(factor.SourceName, nameof(factor.SourceName)),
PayloadJson = RequireValue(factor.PayloadJson, nameof(factor.PayloadJson)),
ProvenanceJson = RequireValue(factor.ProvenanceJson, nameof(factor.ProvenanceJson)),
Gate = RequireValue(factor.Gate, nameof(factor.Gate)),
Role = RequireValue(factor.Role, nameof(factor.Role))
};
}
}
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);