refactor(dotnet): normalize formula service inputs
Validators (Pushes and Pull Requests) / validate-ui-and-storage (push) Successful in 17s
Validators (Pushes and Pull Requests) / validate-core (push) Has been cancelled

This commit is contained in:
2026-07-13 01:28:05 +09:00
parent e745cfb0ae
commit cb814b8aa2
2 changed files with 77 additions and 12 deletions
@@ -16,14 +16,14 @@ namespace QuantEngine.Application.Services
_learningService = learningService;
}
public TimingDecisionResult ComputeTimingDecision(Dictionary<string, object> ctx)
=> FormulaEngine.ComputeTimingDecision(ctx);
public TimingDecisionResult ComputeTimingDecision(Dictionary<string, object> ctx)
=> FormulaEngine.ComputeTimingDecision(RequireContext(ctx));
public SellDecisionResult ComputeSellDecision(Dictionary<string, object> ctx)
=> FormulaEngine.ComputeSellDecision(ctx);
public SellDecisionResult ComputeSellDecision(Dictionary<string, object> ctx)
=> FormulaEngine.ComputeSellDecision(RequireContext(ctx));
public FinalDecisionResult ComputeFinalDecision(Dictionary<string, object> ctx)
=> FormulaEngine.ComputeFinalDecision(ctx);
public FinalDecisionResult ComputeFinalDecision(Dictionary<string, object> ctx)
=> FormulaEngine.ComputeFinalDecision(RequireContext(ctx));
public async Task<Guid> ComputeAndRecordFinalDecisionAsync(
Dictionary<string, object> ctx,
@@ -32,17 +32,18 @@ namespace QuantEngine.Application.Services
string sourceVersion,
IEnumerable<FactorEvidenceInput> factorEvidence)
{
var decision = ComputeFinalDecision(ctx);
var normalizedContext = RequireContext(ctx);
var decision = ComputeFinalDecision(normalizedContext);
return await _learningService.RecordDecisionAsync(
decisionKey,
RequireValue(decisionKey, nameof(decisionKey)),
DateTimeOffset.UtcNow,
instrumentId,
RequireValue(instrumentId, nameof(instrumentId)),
decision.FinalAction,
"PASS",
Convert.ToDecimal(decision.PriorityScore),
sourceVersion,
RequireValue(sourceVersion, nameof(sourceVersion)),
factorEvidence,
new { context_keys = ctx.Keys.OrderBy(key => key).ToArray() },
new { context_keys = normalizedContext.Keys.OrderBy(key => key).ToArray() },
new { formula = "FormulaEngine.ComputeFinalDecision", source_version = sourceVersion });
}
@@ -59,6 +60,22 @@ namespace QuantEngine.Application.Services
=> FormulaEngine.ComputeCashRecoveryOptimizer(sellCandidates, cashShortfallMinKrw);
public Task<int> AppendFormulaRunAsync(string formulaName, Dictionary<string, object?> payload)
=> _historyStore.AppendAsync($"formula_{formulaName}_history", payload);
=> _historyStore.AppendAsync($"formula_{RequireValue(formulaName, nameof(formulaName))}_history", payload);
private static Dictionary<string, object> RequireContext(Dictionary<string, object> ctx)
{
ArgumentNullException.ThrowIfNull(ctx);
return ctx;
}
private static string RequireValue(string value, string parameterName)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Value is required.", parameterName);
}
return value.Trim();
}
}
}
@@ -0,0 +1,48 @@
using Moq;
using QuantEngine.Application.Services;
using QuantEngine.Core.Domain;
using QuantEngine.Core.Interfaces;
namespace QuantEngine.Core.Tests;
public class FormulaServiceTests
{
[Fact]
public async Task ComputeAndRecordFinalDecisionAsync_NormalizesInputs()
{
var historyStore = new Mock<IPostgresqlHistoryStore>(MockBehavior.Strict);
var learningStore = new Mock<INormalizedLearningStore>(MockBehavior.Strict);
learningStore.Setup(s => s.AppendDecisionAsync(It.IsAny<DecisionEventRecord>()))
.ReturnsAsync(Guid.NewGuid());
var service = new FormulaService(historyStore.Object, new DecisionLearningService(learningStore.Object));
var result = await service.ComputeAndRecordFinalDecisionAsync(
new Dictionary<string, object>
{
["entryModeGate"] = "PASS",
["entryMode"] = "PULLBACK",
["leaderGate"] = "PASS",
["acGate"] = "CLEAR",
["priceStatus"] = "PRICE_OK",
["atr20"] = 1.5
},
" decision-1 ",
" 005930 ",
" v1 ",
[]);
Assert.NotEqual(Guid.Empty, result);
learningStore.VerifyAll();
}
[Fact]
public async Task AppendFormulaRunAsync_RejectsBlankName()
{
var service = new FormulaService(new Mock<IPostgresqlHistoryStore>().Object, new DecisionLearningService(new Mock<INormalizedLearningStore>().Object));
await Assert.ThrowsAsync<ArgumentException>(() =>
service.AppendFormulaRunAsync(" ", new Dictionary<string, object?>()));
}
}