From aa61465ce0e1d036cc1298b7836e64ec3cf82435 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Mon, 13 Jul 2026 00:48:21 +0900 Subject: [PATCH] refactor(dotnet): centralize domain numeric guards --- .../QuantEngine.Core/Domain/ExitDecisions.cs | 34 ++++++++++------ .../QuantEngine.Core/Domain/FormulaEngine.cs | 39 ++++++++++--------- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/src/dotnet/QuantEngine.Core/Domain/ExitDecisions.cs b/src/dotnet/QuantEngine.Core/Domain/ExitDecisions.cs index 12286869..09d7041a 100644 --- a/src/dotnet/QuantEngine.Core/Domain/ExitDecisions.cs +++ b/src/dotnet/QuantEngine.Core/Domain/ExitDecisions.cs @@ -28,6 +28,9 @@ namespace QuantEngine.Core.Domain public static class ExitDecisions { + private static bool IsValidNumber(double? value) + => value.HasValue && !double.IsNaN(value.Value) && !double.IsInfinity(value.Value); + public static StopPriceResult ComputeStopPriceCore( double? entryPrice, double? atr20, @@ -36,7 +39,7 @@ namespace QuantEngine.Core.Domain { var result = new StopPriceResult(); - if (!entryPrice.HasValue) + if (!IsValidNumber(entryPrice)) { result.StopPrice = null; result.StopPriceStatus = "NO_STOP_PRICE"; @@ -44,38 +47,45 @@ namespace QuantEngine.Core.Domain return result; } - if (!atr20.HasValue && !atrMultiplier.HasValue) + if (!IsValidNumber(atr20) && !IsValidNumber(atrMultiplier)) { - result.StopPrice = entryPrice.Value * 0.92; + result.StopPrice = entryPrice.GetValueOrDefault() * 0.92; result.StopPriceStatus = "DATA_MISSING — 하네스 업데이트 필요"; result.DataMissing.Add("atr20"); return result; } - if (!atrMultiplier.HasValue && (!currentPrice.HasValue || currentPrice.Value == 0)) + var hasCurrentPrice = IsValidNumber(currentPrice) && currentPrice!.Value != 0; + + if (!IsValidNumber(atrMultiplier) && !hasCurrentPrice) { - result.StopPrice = entryPrice.Value * 0.92; + result.StopPrice = entryPrice.GetValueOrDefault() * 0.92; result.StopPriceStatus = "DATA_MISSING — 하네스 업데이트 필요"; - if (!atr20.HasValue) result.DataMissing.Add("atr20"); - if (!currentPrice.HasValue || currentPrice.Value == 0) result.DataMissing.Add("current_price"); + if (!IsValidNumber(atr20)) result.DataMissing.Add("atr20"); + if (!hasCurrentPrice) result.DataMissing.Add("current_price"); return result; } - if (!atrMultiplier.HasValue) + if (!IsValidNumber(atrMultiplier)) { - double atr20Pct = (atr20!.Value / currentPrice!.Value) * 100; + var atr20Value = atr20.GetValueOrDefault(); + var currentPriceValue = currentPrice.GetValueOrDefault(); + double atr20Pct = (atr20Value / currentPriceValue) * 100; atrMultiplier = atr20Pct >= 8 ? 2.0 : 1.5; result.Atr20Pct = atr20Pct; } else { - result.Atr20Pct = (currentPrice.HasValue && currentPrice.Value != 0) - ? (atr20!.Value / currentPrice.Value) * 100 + result.Atr20Pct = hasCurrentPrice + ? (atr20.GetValueOrDefault() / currentPrice.GetValueOrDefault()) * 100 : (double?)null; } + var entryPriceValue = entryPrice.GetValueOrDefault(); + var atr20FinalValue = atr20.GetValueOrDefault(); + var atrMultiplierValue = atrMultiplier.GetValueOrDefault(); result.AtrMultiplier = atrMultiplier; - result.StopPrice = Math.Max(entryPrice.Value * 0.92, entryPrice.Value - atr20!.Value * atrMultiplier.Value); + result.StopPrice = Math.Max(entryPriceValue * 0.92, entryPriceValue - atr20FinalValue * atrMultiplierValue); result.StopPriceStatus = "PASS"; return result; diff --git a/src/dotnet/QuantEngine.Core/Domain/FormulaEngine.cs b/src/dotnet/QuantEngine.Core/Domain/FormulaEngine.cs index 0a525632..29d340eb 100644 --- a/src/dotnet/QuantEngine.Core/Domain/FormulaEngine.cs +++ b/src/dotnet/QuantEngine.Core/Domain/FormulaEngine.cs @@ -65,6 +65,9 @@ namespace QuantEngine.Core.Domain public static class FormulaEngine { + private static bool IsValidNumber(double? value) + => value.HasValue && !double.IsNaN(value.Value) && !double.IsInfinity(value.Value); + public static TimingDecisionResult ComputeTimingDecision(Dictionary ctx) { var reasons = new List(); @@ -98,9 +101,9 @@ namespace QuantEngine.Core.Domain reasons.Add("entry_block"); } - if (leaderTotal.HasValue && !double.IsNaN(leaderTotal.Value) && !double.IsInfinity(leaderTotal.Value)) + if (IsValidNumber(leaderTotal)) { - if (leaderTotal.Value >= 4) + if (leaderTotal!.Value >= 4) { entryScore += 20; reasons.Add("leader_scan>=4"); @@ -116,9 +119,9 @@ namespace QuantEngine.Core.Domain entryScore += 10; } - if (flowCredit.HasValue && !double.IsNaN(flowCredit.Value) && !double.IsInfinity(flowCredit.Value)) + if (IsValidNumber(flowCredit)) { - if (flowCredit.Value >= 0.7) + if (flowCredit!.Value >= 0.7) { entryScore += 20; reasons.Add("flow_strong"); @@ -147,9 +150,9 @@ namespace QuantEngine.Core.Domain reasons.Add("anti_climax_block"); } - if (ma20Slope.HasValue && !double.IsNaN(ma20Slope.Value) && !double.IsInfinity(ma20Slope.Value)) + if (IsValidNumber(ma20Slope)) { - if (ma20Slope.Value > 0) + if (ma20Slope!.Value > 0) { entryScore += 8; } @@ -161,9 +164,9 @@ namespace QuantEngine.Core.Domain } } - if (disparity.HasValue && !double.IsNaN(disparity.Value) && !double.IsInfinity(disparity.Value)) + if (IsValidNumber(disparity)) { - if (disparity.Value >= -5 && disparity.Value <= 4) + if (disparity!.Value >= -5 && disparity.Value <= 4) { entryScore += 10; } @@ -185,9 +188,9 @@ namespace QuantEngine.Core.Domain } } - if (rsi14.HasValue && !double.IsNaN(rsi14.Value) && !double.IsInfinity(rsi14.Value)) + if (IsValidNumber(rsi14)) { - if (rsi14.Value >= 40 && rsi14.Value <= 65) + if (rsi14!.Value >= 40 && rsi14.Value <= 65) { entryScore += 10; } @@ -209,7 +212,7 @@ namespace QuantEngine.Core.Domain } } - if (avgTradeValue5D.HasValue && !double.IsNaN(avgTradeValue5D.Value) && !double.IsInfinity(avgTradeValue5D.Value) && avgTradeValue5D.Value >= 50 && (!spreadPct.HasValue || double.IsNaN(spreadPct.Value) || spreadPct.Value <= 0.8)) + if (IsValidNumber(avgTradeValue5D) && avgTradeValue5D!.Value >= 50 && (!IsValidNumber(spreadPct) || spreadPct!.Value <= 0.8)) { entryScore += 10; } @@ -219,9 +222,9 @@ namespace QuantEngine.Core.Domain reasons.Add("liquidity_or_spread_fail"); } - if (rwPartial.HasValue && !double.IsNaN(rwPartial.Value) && !double.IsInfinity(rwPartial.Value)) + if (IsValidNumber(rwPartial)) { - exitScore += Math.Min(100.0, Math.Max(0.0, (int)rwPartial.Value * 25.0)); + exitScore += Math.Min(100.0, Math.Max(0.0, (int)rwPartial!.Value * 25.0)); } if (!string.IsNullOrEmpty(exitSignal)) @@ -230,13 +233,13 @@ namespace QuantEngine.Core.Domain exitScore += parts.Length * 10; } - if (daysToTimeStop.HasValue && !double.IsNaN(daysToTimeStop.Value) && daysToTimeStop.Value >= 0 && daysToTimeStop.Value <= 7) + if (IsValidNumber(daysToTimeStop) && daysToTimeStop!.Value >= 0 && daysToTimeStop.Value <= 7) { exitScore += 20; reasons.Add("time_stop_near"); } - if (profitPct.HasValue && !double.IsNaN(profitPct.Value) && profitPct.Value >= 10) + if (IsValidNumber(profitPct) && profitPct!.Value >= 10) { exitScore += 15; reasons.Add("profit_protect_zone"); @@ -249,15 +252,15 @@ namespace QuantEngine.Core.Domain double? atr20 = GetNullableDouble(ctx, "atr20"); string priceStatus = GetString(ctx, "priceStatus"); - if (priceStatus != "PRICE_OK" || !atr20.HasValue || double.IsNaN(atr20.Value) || double.IsInfinity(atr20.Value)) + if (priceStatus != "PRICE_OK" || !IsValidNumber(atr20)) { action = "OBSERVE_DATA_MISSING"; } - else if (exitScore >= 75 || (rwPartial.HasValue && rwPartial.Value >= 4)) + else if (exitScore >= 75 || (IsValidNumber(rwPartial) && rwPartial!.Value >= 4)) { action = "STOP_OR_TIME_EXIT_READY"; } - else if (exitScore >= 50 || (rwPartial.HasValue && rwPartial.Value >= 3)) + else if (exitScore >= 50 || (IsValidNumber(rwPartial) && rwPartial!.Value >= 3)) { action = "EXIT_REVIEW"; }