refactor(dotnet): normalize formula service inputs
This commit is contained in:
@@ -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?>()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user