using System.Text.Json; using QuantEngine.Core.Domain; using QuantEngine.Core.Interfaces; using QuantEngine.Application.Interfaces; namespace QuantEngine.Application.Services; public sealed record FactorComputationAudit( string Ticker, int StockBars, int IndexBars, string State, DateTimeOffset ComputedAt, string? SourceVersion); public sealed class FactorComputationService { private readonly HistoryIngestionService _history; private readonly IRuntimeAuditTrailService _auditTrail; public FactorComputationService(HistoryIngestionService history, IRuntimeAuditTrailService auditTrail) { _history = history; _auditTrail = auditTrail; } public FactorOutputs Compute( string ticker, List stockBars, List indexBars, string? sourceVersion = null) { var computedAt = DateTimeOffset.UtcNow; var outputs = FactorCalculator.CalculateFactors(stockBars, indexBars); _auditTrail.Append("factor_audit", ticker, new FactorComputationAudit(ticker, stockBars.Count, indexBars.Count, "SUCCEEDED", computedAt, sourceVersion)); return outputs; } public async Task AppendFactorOutputsAsync( string ticker, string sourceVersion, FactorOutputs outputs, DateTimeOffset? observedAt = null) { ticker = RequireValue(ticker, nameof(ticker)); sourceVersion = RequireValue(sourceVersion, nameof(sourceVersion)); ArgumentNullException.ThrowIfNull(outputs); var when = observedAt ?? DateTimeOffset.UtcNow; await _history.AppendFactorOutputAsync("momentum_20d", sourceVersion, outputs.Momentum20D, "PASS", sourceVersion, when); await _history.AppendFactorOutputAsync("momentum_60d", sourceVersion, outputs.Momentum60D, "PASS", sourceVersion, when); await _history.AppendFactorOutputAsync("momentum_120d", sourceVersion, outputs.Momentum120D, "PASS", sourceVersion, when); await _history.AppendFactorOutputAsync("atr_20pct", sourceVersion, outputs.Atr20Pct, "PASS", sourceVersion, when); await _history.AppendFactorOutputAsync("stdev_20d", sourceVersion, outputs.StDev20D, "PASS", sourceVersion, when); await _history.AppendFactorOutputAsync("beta_60d", sourceVersion, outputs.Beta60D, "PASS", sourceVersion, when); await _history.AppendFactorOutputAsync("rs_20d", sourceVersion, outputs.Rs20D, "PASS", sourceVersion, when); _auditTrail.Append("factor_audit", ticker, new FactorComputationAudit(ticker, 0, 0, "PERSISTED", when, sourceVersion)); } private static string RequireValue(string value, string parameterName) { if (string.IsNullOrWhiteSpace(value)) { throw new ArgumentException("Value is required.", parameterName); } return value.Trim(); } }