d417d6325e
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
339 lines
11 KiB
C#
339 lines
11 KiB
C#
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<FormulaParityFixture>
|
|
{
|
|
private readonly FormulaParityFixture _fixture;
|
|
|
|
public FormulaEngineTests(FormulaParityFixture fixture)
|
|
{
|
|
_fixture = fixture;
|
|
}
|
|
|
|
[Fact]
|
|
public void TestTimingDecisionNeutral()
|
|
{
|
|
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.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<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()
|
|
{
|
|
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.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<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);
|
|
}
|
|
}
|
|
}
|
|
}
|