test(dotnet): implement FormulaEngine parity tests and generate dotnet_formula_parity_v1.json (WBS-10.4)
Deploy to Production / Build Release Package (push) Failing after 12s
Snapshot Admin Deployment / build-and-deploy (push) Failing after 31s
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 4s
Deploy to Production / Deploy to Production Server (push) Has been skipped
Deploy to Production / Post-Deployment Checks (push) Has been skipped
Quant Engine CI/CD Pipeline / validate-core (push) Failing after 2m15s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (push) Has been skipped

This commit is contained in:
2026-06-29 10:22:49 +09:00
parent 4b32cd2d43
commit d417d6325e
2 changed files with 321 additions and 74 deletions
@@ -1,91 +1,338 @@
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 FormulaEngineTests
namespace QuantEngine.Core.Tests
{
[Fact]
public void TestTimingDecisionNeutral()
public class FormulaParityFixture : IDisposable
{
var ctx = new Dictionary<string, object>
{
{ "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 }
};
public int TotalTests = 0;
public int PassedTests = 0;
private readonly object _lock = new object();
var result = FormulaEngine.ComputeTimingDecision(ctx);
Assert.NotNull(result);
Assert.Equal("BUY_BREAKOUT_PILOT_ONLY", result.Action);
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 }));
}
}
[Fact]
public void ComputeSellDecisionProducesExitTrimWhenRiskWindowIsOpen()
public class FormulaEngineTests : IClassFixture<FormulaParityFixture>
{
var ctx = new Dictionary<string, object>
private readonly FormulaParityFixture _fixture;
public FormulaEngineTests(FormulaParityFixture fixture)
{
{ "close", 100.0 },
{ "profitPct", 31.0 },
{ "tp1Price", 108.0 },
{ "tp2Price", 112.0 },
{ "timingAction", "BUY_STAGE1_READY" },
{ "atr20", 4.0 }
};
_fixture = fixture;
}
var result = FormulaEngine.ComputeSellDecision(ctx);
Assert.Equal("PROFIT_TRIM_35", result.Action);
Assert.Equal(35, result.RatioPct);
Assert.Equal("SIGNAL_CONFIRMED", result.Validation);
}
[Fact]
public void ComputeFinalDecisionPromotesSellReadyWhenSellSignalIsConfirmed()
{
var ctx = new Dictionary<string, object>
[Fact]
public void TestTimingDecisionNeutral()
{
{ "sellAction", "TRIM_35" },
{ "sellValidation", "SIGNAL_CONFIRMED" },
{ "timingScoreEntry", 72.0 },
{ "timingScoreExit", 15.0 }
};
bool success = false;
try
{
var ctx = new Dictionary<string, object>
{
{ "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.ComputeFinalDecision(ctx);
var result = FormulaEngine.ComputeTimingDecision(ctx);
Assert.NotNull(result);
Assert.Equal("BUY_BREAKOUT_PILOT_ONLY", result.Action);
success = true;
}
finally
{
_fixture.RegisterResult(success);
}
}
Assert.Equal("SELL_READY", result.FinalAction);
Assert.Equal(10, result.ActionPriority);
Assert.Equal("RULE_ENGINE", result.DecisionSource);
}
[Fact]
public void ComputeCashShortfallHarnessCalculatesTargetAndShortfall()
{
var asResult = new Dictionary<string, object>
[Fact]
public void ComputeSellDecisionProducesExitTrimWhenRiskWindowIsOpen()
{
{ "settlementCashD2Krw", 10_000_000.0 }
};
var cashFloor = new Dictionary<string, object>
bool success = false;
try
{
var ctx = new Dictionary<string, object>
{
{ "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()
{
{ "minPct", 15.0 }
};
bool success = false;
try
{
var ctx = new Dictionary<string, object>
{
{ "sellAction", "TRIM_35" },
{ "sellValidation", "SIGNAL_CONFIRMED" },
{ "timingScoreEntry", 72.0 },
{ "timingScoreExit", 15.0 }
};
var result = FormulaEngine.ComputeCashShortfallHarness(asResult, 100_000_000.0, cashFloor, 6.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);
}
}
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);
[Fact]
public void ComputeCashShortfallHarnessCalculatesTargetAndShortfall()
{
bool success = false;
try
{
var asResult = new Dictionary<string, object>
{
{ "settlementCashD2Krw", 10_000_000.0 }
};
var cashFloor = new Dictionary<string, object>
{
{ "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<Dictionary<string, object>>
{
new Dictionary<string, object>
{
{ "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);
}
}
}
}