refactor(dotnet): normalize history ingestion payloads
This commit is contained in:
@@ -34,25 +34,32 @@ namespace QuantEngine.Application.Services
|
|||||||
string? sourceVersion = null,
|
string? sourceVersion = null,
|
||||||
string? gate = null)
|
string? gate = null)
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(decision);
|
||||||
|
|
||||||
|
var normalizedInstrumentId = NormalizeOptional(instrumentId);
|
||||||
|
var normalizedSourceVersion = NormalizeOptional(sourceVersion) ?? RequireValue(decision.DecisionSource, nameof(decision.DecisionSource));
|
||||||
|
var normalizedGate = NormalizeOptional(gate) ?? (string.IsNullOrWhiteSpace(sellDecision?.Validation) ? "PASS" : sellDecision.Validation!.Trim());
|
||||||
|
var normalizedAction = RequireValue(decision.FinalAction, nameof(decision.FinalAction));
|
||||||
|
|
||||||
var payload = new Dictionary<string, object?>
|
var payload = new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["decision_id"] = Guid.NewGuid().ToString("N"),
|
["decision_id"] = Guid.NewGuid().ToString("N"),
|
||||||
["decided_at"] = DateTimeOffset.UtcNow,
|
["decided_at"] = DateTimeOffset.UtcNow,
|
||||||
["instrument_id"] = instrumentId ?? string.Empty,
|
["instrument_id"] = normalizedInstrumentId ?? string.Empty,
|
||||||
["action"] = decision.FinalAction,
|
["action"] = normalizedAction,
|
||||||
["gate"] = gate ?? (string.IsNullOrWhiteSpace(sellDecision?.Validation) ? "PASS" : sellDecision.Validation),
|
["gate"] = normalizedGate,
|
||||||
["score"] = decision.PriorityScore,
|
["score"] = decision.PriorityScore,
|
||||||
["source_version"] = sourceVersion ?? decision.DecisionSource,
|
["source_version"] = normalizedSourceVersion,
|
||||||
["provenance"] = new Dictionary<string, object?>
|
["provenance"] = new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["final_action"] = decision.FinalAction,
|
["final_action"] = normalizedAction,
|
||||||
["action_priority"] = decision.ActionPriority,
|
["action_priority"] = decision.ActionPriority,
|
||||||
["priority_score"] = decision.PriorityScore,
|
["priority_score"] = decision.PriorityScore,
|
||||||
["decision_source"] = decision.DecisionSource,
|
["decision_source"] = decision.DecisionSource,
|
||||||
["sell_action"] = sellDecision?.Action,
|
["sell_action"] = NormalizeOptional(sellDecision?.Action),
|
||||||
["sell_validation"] = sellDecision?.Validation,
|
["sell_validation"] = NormalizeOptional(sellDecision?.Validation),
|
||||||
["timing_action"] = timingDecision?.Action,
|
["timing_action"] = NormalizeOptional(timingDecision?.Action),
|
||||||
["timing_reason"] = timingDecision?.Reason
|
["timing_reason"] = NormalizeOptional(timingDecision?.Reason)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,6 +74,11 @@ namespace QuantEngine.Application.Services
|
|||||||
string? sourceVersion = null,
|
string? sourceVersion = null,
|
||||||
DateTimeOffset? observedAt = null)
|
DateTimeOffset? observedAt = null)
|
||||||
{
|
{
|
||||||
|
factorId = RequireValue(factorId, nameof(factorId));
|
||||||
|
factorVersion = RequireValue(factorVersion, nameof(factorVersion));
|
||||||
|
outputGate = RequireValue(outputGate, nameof(outputGate));
|
||||||
|
sourceVersion = NormalizeOptional(sourceVersion) ?? factorVersion;
|
||||||
|
|
||||||
var payload = new Dictionary<string, object?>
|
var payload = new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["factor_output_id"] = Guid.NewGuid().ToString("N"),
|
["factor_output_id"] = Guid.NewGuid().ToString("N"),
|
||||||
@@ -75,7 +87,7 @@ namespace QuantEngine.Application.Services
|
|||||||
["factor_version"] = factorVersion,
|
["factor_version"] = factorVersion,
|
||||||
["output_value"] = outputValue,
|
["output_value"] = outputValue,
|
||||||
["output_gate"] = outputGate,
|
["output_gate"] = outputGate,
|
||||||
["source_version"] = sourceVersion ?? factorVersion,
|
["source_version"] = sourceVersion,
|
||||||
["provenance"] = new Dictionary<string, object?>
|
["provenance"] = new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["factor_id"] = factorId,
|
["factor_id"] = factorId,
|
||||||
@@ -88,5 +100,18 @@ namespace QuantEngine.Application.Services
|
|||||||
|
|
||||||
return _store.AppendAsync("factor_output_history", payload);
|
return _store.AppendAsync("factor_output_history", payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string RequireValue(string value, string parameterName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Value is required.", parameterName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeOptional(string? value)
|
||||||
|
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using Moq;
|
||||||
|
using QuantEngine.Application.Services;
|
||||||
|
using QuantEngine.Core.Domain;
|
||||||
|
using QuantEngine.Core.Interfaces;
|
||||||
|
|
||||||
|
namespace QuantEngine.Core.Tests;
|
||||||
|
|
||||||
|
public class HistoryIngestionServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task AppendDecisionAsync_NormalizesTypedPayload()
|
||||||
|
{
|
||||||
|
var store = new Mock<IPostgresqlHistoryStore>(MockBehavior.Strict);
|
||||||
|
store.Setup(s => s.AppendAsync("decision_result_history", It.Is<IDictionary<string, object?>>(payload =>
|
||||||
|
payload["instrument_id"] != null && payload["instrument_id"]!.ToString() == "005930" &&
|
||||||
|
payload["action"] != null && payload["action"]!.ToString() == "BUY" &&
|
||||||
|
payload["gate"] != null && payload["gate"]!.ToString() == "PASS" &&
|
||||||
|
payload["source_version"] != null && payload["source_version"]!.ToString() == "v1"))).ReturnsAsync(1);
|
||||||
|
|
||||||
|
var service = new HistoryIngestionService(store.Object);
|
||||||
|
|
||||||
|
var result = await service.AppendDecisionAsync(
|
||||||
|
new FinalDecisionResult
|
||||||
|
{
|
||||||
|
FinalAction = " BUY ",
|
||||||
|
ActionPriority = 1,
|
||||||
|
PriorityScore = 12.3,
|
||||||
|
DecisionSource = " v1 "
|
||||||
|
},
|
||||||
|
new SellDecisionResult { Action = "SELL", Validation = " PASS " },
|
||||||
|
null,
|
||||||
|
" 005930 ",
|
||||||
|
" ",
|
||||||
|
null);
|
||||||
|
|
||||||
|
Assert.Equal(1, result);
|
||||||
|
store.VerifyAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AppendFactorOutputAsync_RejectsBlankCoreFields()
|
||||||
|
{
|
||||||
|
var service = new HistoryIngestionService(new Mock<IPostgresqlHistoryStore>().Object);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<ArgumentException>(() => service.AppendFactorOutputAsync(
|
||||||
|
" ",
|
||||||
|
"1.0",
|
||||||
|
1.0,
|
||||||
|
"PASS"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user