using System; using System.Collections.Generic; using System.IO; using System.Text.Json; using Xunit; using QuantEngine.Core.Domain; namespace QuantEngine.Core.Tests { public class FormulaParityFixture : IDisposable { public int TotalTests = 0; public int PassedTests = 0; private readonly object _lock = new object(); public void RegisterResult(bool passed) { lock (_lock) { TotalTests++; if (passed) PassedTests++; } } public void Dispose() { var tempDir = @"C:\Temp\data_feed\Temp"; if (!Directory.Exists(tempDir)) { Directory.CreateDirectory(tempDir); } var outputPath = Path.Combine(tempDir, "dotnet_formula_parity_v1.json"); var result = new { gate = PassedTests == TotalTests && TotalTests >= 37 ? "PASS" : "FAIL", total = TotalTests, passed = PassedTests }; File.WriteAllText(outputPath, JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true })); } } public class FormulaEngineTests : IClassFixture { private readonly FormulaParityFixture _fixture; public FormulaEngineTests(FormulaParityFixture fixture) { _fixture = fixture; } [Fact] public void TestTimingDecisionNeutral() { bool success = false; try { var ctx = new Dictionary { { "entryModeGate", "PASS" }, { "entryMode", "BREAKOUT" }, { "leaderGate", "PASS" }, { "acGate", "CLEAR" }, { "leaderTotal", 4.0 }, { "flowCredit", 0.8 }, { "ma20Slope", 1.0 }, { "disparity", 0.0 }, { "rsi14", 50.0 }, { "avgTradeValue5D", 100.0 }, { "spreadPct", 0.1 }, { "priceStatus", "PRICE_OK" }, { "atr20", 10.0 } }; var result = FormulaEngine.ComputeTimingDecision(ctx); Assert.NotNull(result); Assert.Equal("BUY_BREAKOUT_PILOT_ONLY", result.Action); success = true; } finally { _fixture.RegisterResult(success); } } [Fact] public void ComputeSellDecisionProducesExitTrimWhenRiskWindowIsOpen() { bool success = false; try { var ctx = new Dictionary { { "close", 100.0 }, { "profitPct", 31.0 }, { "tp1Price", 108.0 }, { "tp2Price", 112.0 }, { "timingAction", "BUY_STAGE1_READY" }, { "atr20", 4.0 } }; var result = FormulaEngine.ComputeSellDecision(ctx); Assert.Equal("PROFIT_TRIM_35", result.Action); Assert.Equal(35, result.RatioPct); Assert.Equal("SIGNAL_CONFIRMED", result.Validation); success = true; } finally { _fixture.RegisterResult(success); } } [Fact] public void ComputeFinalDecisionPromotesSellReadyWhenSellSignalIsConfirmed() { bool success = false; try { var ctx = new Dictionary { { "sellAction", "TRIM_35" }, { "sellValidation", "SIGNAL_CONFIRMED" }, { "timingScoreEntry", 72.0 }, { "timingScoreExit", 15.0 } }; var result = FormulaEngine.ComputeFinalDecision(ctx); Assert.Equal("SELL_READY", result.FinalAction); Assert.Equal(10, result.ActionPriority); Assert.Equal("RULE_ENGINE", result.DecisionSource); success = true; } finally { _fixture.RegisterResult(success); } } [Fact] public void ComputeCashShortfallHarnessCalculatesTargetAndShortfall() { bool success = false; try { var asResult = new Dictionary { { "settlementCashD2Krw", 10_000_000.0 } }; var cashFloor = new Dictionary { { "minPct", 15.0 } }; var result = FormulaEngine.ComputeCashShortfallHarness(asResult, 100_000_000.0, cashFloor, 6.0); Assert.Equal(10.0, result.CashCurrentPctD2); Assert.Equal(15.0, result.CashTargetPct); Assert.Equal(5_000_000.0, result.CashShortfallMinKrw); Assert.Equal(5_000_000.0, result.CashShortfallTargetKrw); success = true; } finally { _fixture.RegisterResult(success); } } [Theory] [InlineData(1.0, "CLEAR", "PASS")] [InlineData(2.0, "PULLBACK_WAIT", "WAIT")] [InlineData(4.0, "BLOCK_CHASE", "BLOCKED")] public void Formula_10_4_1_Velocity_And_10_4_3_AntiChasing(double vel, string expectedVerdict, string expectedStatus) { bool success = false; try { var res = AntiChasingCalculator.ComputeAntiChasing(vel); Assert.Equal(expectedVerdict, res.AntiChasingVerdict); Assert.Equal(expectedStatus, res.AntiChasingVelocityStatus); success = true; } finally { _fixture.RegisterResult(success); } } [Theory] [InlineData(-5.0, "NORMAL")] [InlineData(5.0, "BREAKEVEN_RATCHET")] [InlineData(15.0, "PROFIT_LOCK_10")] [InlineData(25.0, "PROFIT_LOCK_20")] [InlineData(35.0, "PROFIT_LOCK_30")] [InlineData(45.0, "APEX_TRAILING")] [InlineData(65.0, "APEX_SUPER")] public void Formula_10_4_2_ProfitLockStage(double profit, string expectedStage) { bool success = false; try { var res = ProfitLockCalculator.ClassifyProfitLockStage(profit); Assert.Equal(expectedStage, res); success = true; } finally { _fixture.RegisterResult(success); } } [Theory] [InlineData(100000.0, 100000.0, 3000.0, "PULLBACK_ZONE", "PASS")] [InlineData(105000.0, 100000.0, 3000.0, "ABOVE_PULLBACK_ZONE", "BLOCKED")] [InlineData(102000.0, 100000.0, 3000.0, "PULLBACK_ZONE", "PASS")] public void Formula_10_4_4_PullbackTrigger(double close, double ma, double atr, string expectedVerdict, string expectedState) { bool success = false; try { var res = PullbackTriggerCalculator.ComputePullbackTrigger(close, ma, atr); Assert.Equal(expectedVerdict, res.PullbackEntryVerdict); Assert.Equal(expectedState, res.PullbackState); success = true; } finally { _fixture.RegisterResult(success); } } [Theory] [InlineData(100000.0, 95000.0, 100000.0, "PASS")] [InlineData(90000.0, 95000.0, 100000.0, "INVALID_PRICE_INVERSION")] [InlineData(140000.0, 95000.0, 100000.0, "INVALID_UNREALISTIC_PRICE")] public void Formula_10_4_5_SellPriceSanity(double sell, double stop, double prev, string expectedStatus) { bool success = false; try { var res = SellPriceSanityChecker.CheckSellPriceSanity(sell, stop, prev); Assert.Equal(expectedStatus, res.SellPriceSanityStatus); success = true; } finally { _fixture.RegisterResult(success); } } [Theory] [InlineData(1500.0, 1)] [InlineData(4500.0, 5)] [InlineData(15000.0, 10)] [InlineData(45000.0, 50)] [InlineData(150000.0, 100)] [InlineData(450000.0, 500)] [InlineData(1000000.0, 1000)] [InlineData(3000000.0, 1000)] public void Formula_10_4_6_TickNormalizer(double price, int expectedTick) { bool success = false; try { int tick = KrxTickNormalizer.GetTickUnit(price); Assert.Equal(expectedTick, tick); success = true; } finally { _fixture.RegisterResult(success); } } [Theory] [InlineData(5000000.0, true)] [InlineData(10000000.0, false)] [InlineData(0.0, true)] public void Formula_10_4_7_CashRecoveryOptimizer(double shortfall, bool expectedShortfallMet) { bool success = false; try { var candidates = new List> { new Dictionary { { "Ticker", "005930" }, { "Name", "삼성전자" }, { "Sell_Qty", 100 }, { "Sell_Limit_Price", 80000.0 }, { "Cash_Preserve_Ratio", 100.0 }, { "Cash_Preserve_Style", "FULL" } } }; var res = FormulaEngine.ComputeCashRecoveryOptimizer(candidates, shortfall); Assert.NotNull(res); Assert.Equal(expectedShortfallMet, res.ShortfallMet); success = true; } finally { _fixture.RegisterResult(success); } } [Theory] [InlineData(65.0, "APEX_SUPER")] [InlineData(45.0, "APEX_TRAILING")] [InlineData(35.0, "PROFIT_LOCK_30")] [InlineData(25.0, "PROFIT_LOCK_20")] [InlineData(15.0, "PROFIT_LOCK_10")] [InlineData(5.0, "BREAKEVEN_RATCHET")] [InlineData(-5.0, "NORMAL")] public void Formula_10_4_8_ProfitRatchetTiered(double profitPct, string expectedStage) { bool success = false; try { var res = ProfitLockCalculator.ComputeTrailingStop( profitPct, 100000, 3000, 90000, 80000 ); Assert.Equal(expectedStage, res.RatchetStage); success = true; } finally { _fixture.RegisterResult(success); } } } }