refactor(dotnet): normalize formula service inputs
This commit is contained in:
@@ -16,14 +16,14 @@ namespace QuantEngine.Application.Services
|
|||||||
_learningService = learningService;
|
_learningService = learningService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public TimingDecisionResult ComputeTimingDecision(Dictionary<string, object> ctx)
|
public TimingDecisionResult ComputeTimingDecision(Dictionary<string, object> ctx)
|
||||||
=> FormulaEngine.ComputeTimingDecision(ctx);
|
=> FormulaEngine.ComputeTimingDecision(RequireContext(ctx));
|
||||||
|
|
||||||
public SellDecisionResult ComputeSellDecision(Dictionary<string, object> ctx)
|
public SellDecisionResult ComputeSellDecision(Dictionary<string, object> ctx)
|
||||||
=> FormulaEngine.ComputeSellDecision(ctx);
|
=> FormulaEngine.ComputeSellDecision(RequireContext(ctx));
|
||||||
|
|
||||||
public FinalDecisionResult ComputeFinalDecision(Dictionary<string, object> ctx)
|
public FinalDecisionResult ComputeFinalDecision(Dictionary<string, object> ctx)
|
||||||
=> FormulaEngine.ComputeFinalDecision(ctx);
|
=> FormulaEngine.ComputeFinalDecision(RequireContext(ctx));
|
||||||
|
|
||||||
public async Task<Guid> ComputeAndRecordFinalDecisionAsync(
|
public async Task<Guid> ComputeAndRecordFinalDecisionAsync(
|
||||||
Dictionary<string, object> ctx,
|
Dictionary<string, object> ctx,
|
||||||
@@ -32,17 +32,18 @@ namespace QuantEngine.Application.Services
|
|||||||
string sourceVersion,
|
string sourceVersion,
|
||||||
IEnumerable<FactorEvidenceInput> factorEvidence)
|
IEnumerable<FactorEvidenceInput> factorEvidence)
|
||||||
{
|
{
|
||||||
var decision = ComputeFinalDecision(ctx);
|
var normalizedContext = RequireContext(ctx);
|
||||||
|
var decision = ComputeFinalDecision(normalizedContext);
|
||||||
return await _learningService.RecordDecisionAsync(
|
return await _learningService.RecordDecisionAsync(
|
||||||
decisionKey,
|
RequireValue(decisionKey, nameof(decisionKey)),
|
||||||
DateTimeOffset.UtcNow,
|
DateTimeOffset.UtcNow,
|
||||||
instrumentId,
|
RequireValue(instrumentId, nameof(instrumentId)),
|
||||||
decision.FinalAction,
|
decision.FinalAction,
|
||||||
"PASS",
|
"PASS",
|
||||||
Convert.ToDecimal(decision.PriorityScore),
|
Convert.ToDecimal(decision.PriorityScore),
|
||||||
sourceVersion,
|
RequireValue(sourceVersion, nameof(sourceVersion)),
|
||||||
factorEvidence,
|
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 });
|
new { formula = "FormulaEngine.ComputeFinalDecision", source_version = sourceVersion });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +60,22 @@ namespace QuantEngine.Application.Services
|
|||||||
=> FormulaEngine.ComputeCashRecoveryOptimizer(sellCandidates, cashShortfallMinKrw);
|
=> FormulaEngine.ComputeCashRecoveryOptimizer(sellCandidates, cashShortfallMinKrw);
|
||||||
|
|
||||||
public Task<int> AppendFormulaRunAsync(string formulaName, Dictionary<string, object?> payload)
|
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