feat(swap): EQD-6953 后端值域闸门补疑似到期债券判定——IsResultAbsurd新增分支:IsMaturedDegenerate(ytm=0且净/全价均为面值100,抽为public纯函数供单测)命中即返回null+真实原因文案,走既有'宁可不回写'守卫链;判定条件与前端swapCalc.js同款闸门一致(注释互指);新增6个单测(命中/正常券180205实测值/零息平价/息票平价/ytm null/IsResultAbsurd分支接线)全过

This commit is contained in:
hjhan
2026-08-21 15:29:17 +08:00
parent cd981527ab
commit b9968dc80a
2 changed files with 82 additions and 1 deletions
@@ -0,0 +1,61 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YLErp.Helpers;
using YLErp.Model;
namespace UnitTestProject.Modules
{
/// <summary>
/// EQD-6953 疑似到期债券值域闸门:IsMaturedDegenerate / IsResultAbsurd 分支覆盖。
/// 场景来源:UAT 060203.IB2006年国债,2026估值日已无剩余现金流)——
/// jquantlib 对空现金流求解得 ytm=0、净/全价均为面值100errCode=0"成功但退化"。
/// 与前端 swapCalc.js::getBondCalcErrorMessage 的同款闸门保持一致口径。
/// </summary>
[TestClass]
public class BondCalcHeplerTest
{
[TestMethod]
public void 退_三条件同时成立_命中()
{
var r = new CalBondResult { cleanPrice = 100m, dirtyPrice = 100m, ytm = 0m };
Assert.IsTrue(BondCalcHepler.IsMaturedDegenerate(r));
}
[TestMethod]
public void _不命中_按UAT实测180205IB()
{
var r = new CalBondResult { cleanPrice = 97.43300000000002m, dirtyPrice = 100.00001369863016m, ytm = 6.738278242318886m };
Assert.IsFalse(BondCalcHepler.IsMaturedDegenerate(r));
}
[TestMethod]
public void ytm为0但净价非面值_不命中_真实零息平价券场景()
{
var r = new CalBondResult { cleanPrice = 99.5m, dirtyPrice = 100m, ytm = 0m };
Assert.IsFalse(BondCalcHepler.IsMaturedDegenerate(r));
}
[TestMethod]
public void ytm非0_不命中_正常息票平价券场景()
{
var r = new CalBondResult { cleanPrice = 100m, dirtyPrice = 100.5m, ytm = 3.2m };
Assert.IsFalse(BondCalcHepler.IsMaturedDegenerate(r));
}
[TestMethod]
public void ytm为null_不命中()
{
var r = new CalBondResult { cleanPrice = 100m, dirtyPrice = 100m, ytm = null };
Assert.IsFalse(BondCalcHepler.IsMaturedDegenerate(r));
}
[TestMethod]
public void IsResultAbsurd_到期退化值_命中并带原因()
{
var r = new CalBondResult { cleanPrice = 100m, dirtyPrice = 100m, ytm = 0m };
var ok = typeof(BondCalcHepler)
.GetMethod("IsResultAbsurd", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)
.Invoke(null, new object[] { r, null });
Assert.IsTrue((bool)ok);
}
}
}
+21 -1
View File
@@ -162,7 +162,8 @@ namespace YLErp.Helpers
/// 命中则上层返回 null、不回写,避免把不可信结果覆盖用户手工输入。规则: /// 命中则上层返回 null、不回写,避免把不可信结果覆盖用户手工输入。规则:
/// - 净价/全价:占面值百分比,正常约 20~300,必须为正且不破千(≤0 或 &gt;1000 视为离谱); /// - 净价/全价:占面值百分比,正常约 20~300,必须为正且不破千(≤0 或 &gt;1000 视为离谱);
/// - 收益率(ytm,百分数口径)|ytm| &gt; 100 视为爆炸(允许负利率债,但量级须合理); /// - 收益率(ytm,百分数口径)|ytm| &gt; 100 视为爆炸(允许负利率债,但量级须合理);
/// - 哨兵值:任一字段命中 -999999 或 -999999×100(如 yieldToCP 不可用哨兵透传) /// - 哨兵值:任一字段命中 -999999 或 -999999×100(如 yieldToCP 不可用哨兵透传)
/// - 疑似到期/无剩余现金流:见 <see cref="IsMaturedDegenerate"/>。
/// 与前端 swapCalc.js::getBondCalcErrorMessage 的值域闸门保持一致口径。 /// 与前端 swapCalc.js::getBondCalcErrorMessage 的值域闸门保持一致口径。
/// </summary> /// </summary>
private static bool IsResultAbsurd(CalBondResult data, out string reason) private static bool IsResultAbsurd(CalBondResult data, out string reason)
@@ -186,8 +187,27 @@ namespace YLErp.Helpers
reason = "债券计算收益率量级异常(" + data.ytm.Value + "),请检查估值日/价格输入或联系管理员核对债券计算服务"; reason = "债券计算收益率量级异常(" + data.ytm.Value + "),请检查估值日/价格输入或联系管理员核对债券计算服务";
return true; return true;
} }
if (IsMaturedDegenerate(data))
{
reason = "该债券疑似已到期或无剩余现金流(收益率=0、净价/全价均为面值100),无法反算,请核对债券到期日或手工填写";
return true;
}
return false; return false;
} }
/// <summary>
/// 疑似到期/无剩余现金流债券:估值日晚于到期日时底层 jquantlib 对空现金流求解得 ytm=0、
/// 净价/全价均为面值100——三者同时成立在现实在市券中几乎不可能(UAT 060203.IB 实证)。
/// 判定条件与前端 swapCalc.js::getBondCalcErrorMessage 内同款闸门保持一致,勿单边改动。
/// public 供单测直接覆盖(BondCalcHeplerTest)。
/// </summary>
public static bool IsMaturedDegenerate(CalBondResult data)
{
return data != null
&& data.ytm == 0m
&& data.cleanPrice == 100m
&& data.dirtyPrice == 100m;
}
} }
} }