test(swap): EQD-6968 补不算头不算尾(00)组合——CalcFirst=false 零覆盖盲区

- Red_Compound_NoHeadNoTail:00+缺平仓日价 → 放行(calcLast 同 false,与 10 同口径)
- CalcFirst_Simple_NoHeadDropsExactlyStartDayInterest:单利下 00 与 10 利息差
  恰好=开始日一天利息(精确断言,钉 CalcFirst 回归)。closeRate 取 7/6 定盘同值,
  免疫既有取价细节——排查中发现:无日终快照+00 时 fetchAfter=interestStart-1 会
  跳过开始日取价、首段利率用种子(GetFloatRate 回写的尾日定盘),与 10 的
  fetchAfter=开始日-1 不同(FIX 日志可复现,niche 场景:首个日终前平仓)。
Run 运行器扩参:settment / exerciseDate(供后续 EOD 与到期日用例)。

验证:GLMS20260817Fr007UnwindMorningTest 25/25 内存全绿
This commit is contained in:
hjhan
2026-08-19 10:53:34 +08:00
parent 8701bd8c44
commit 4d68133dcb
@@ -97,7 +97,7 @@ namespace YLErp.Modules.SwapModule
private static OptUserInfo MakeOptUser() =>
new(0, nameof(GLMS20260817Fr007UnwindMorningTest), OptUserFrom.UnitTest);
private static trade BuildTrade(DateTime closeDate, string calcMode = "10")
private static trade BuildTrade(DateTime closeDate, string calcMode = "10", DateTime? exerciseDate = null)
{
var extend = new trade_extend
{
@@ -117,7 +117,7 @@ namespace YLErp.Modules.SwapModule
TradeType = "债券TRS",
TradeDate = TradeDate,
StartDate = StartDate,
ExerciseDate = closeDate.AddDays(1),
ExerciseDate = exerciseDate ?? closeDate.AddDays(1),
TradeStatus = "已平仓",
ValidState = "Valid",
trade_extend = extend
@@ -183,7 +183,9 @@ namespace YLErp.Modules.SwapModule
int interestRule = 0,
bool newCalcLast = false,
Dictionary<DateTime, double> market = null,
DateTime? omitDate2 = null)
DateTime? omitDate2 = null,
bool settment = false,
DateTime? exerciseDate = null)
{
var cd = closeDate ?? CloseDate;
var omit = new HashSet<DateTime>();
@@ -195,7 +197,7 @@ namespace YLErp.Modules.SwapModule
else if (!includeCloseDate) omit.Add(cd.Date);
var svc = new StubSwapDealService(MakeOptUser(), omit, closeRate, market);
var td = BuildTrade(cd, calcMode);
var td = BuildTrade(cd, calcMode, exerciseDate);
var position = BuildPosition(interestType, cd, restDays, interestRule);
var eodList = preEod == null
? new List<eod_swap_position>()
@@ -207,7 +209,7 @@ namespace YLErp.Modules.SwapModule
new List<swap_position> { position },
Notional, Notional, closePrecent,
(int)SwapEventTypeEnum., false, Notional,
false, settment: false, newCalcLast: newCalcLast, closeList: null);
false, settment: settment, newCalcLast: newCalcLast, closeList: null);
Assert.AreEqual(1, interests.Count, "应返回恰好 1 条利息事件");
return new Outcome { Fe = interests[0] };
}
@@ -421,5 +423,35 @@ namespace YLErp.Modules.SwapModule
preEod: BuildPreEod(new DateTime(2026, 7, 13))),
"单利 算尾(11)+当日非重置日+当日缺价 → 应放行");
}
// ── 不算头不算尾(calcMode="00")CalcFirst=false 组合 ──
// 对尾日 FR007 行为与"10"一致(calcLast 同 false);另以单利精确断言钉 CalcFirst 语义——
// "00" 比"10"恰好少计开始日一天的利息(单利无基数效应,差值可精确到分毫)。
[TestMethod]
public void Red_Compound_NoHeadNoTail_WithoutCloseDateFr007_Succeeds()
{
AssertNoThrow(Run(InterestTypeEnum., includeCloseDate: false, calcMode: "00",
preEod: BuildPreEod(new DateTime(2026, 7, 13))),
"不算头不算尾(00)+缺平仓日价 → 应放行(与10同口径,尾日不参与计息)");
}
[TestMethod]
public void CalcFirst_Simple_NoHeadDropsExactlyStartDayInterest()
{
// closeRate=0.0142 与 7/6 定盘相同:使"00"首段种子利率与"10"首段取价利率一致,
// 断言只对"少计开始日一天"敏感,免疫无日终快照时 fetchAfter=interestStart-1
// 跳过 7/6 取价、首段用种子利率的既有取价细节。
var w10 = Run(InterestTypeEnum., includeCloseDate: true, calcMode: "10", closeRate: 0.0142);
var w00 = Run(InterestTypeEnum., includeCloseDate: true, calcMode: "00", closeRate: 0.0142);
Assert.IsFalse(w10.Threw, "10 不应抛:" + w10.Ex?.Message);
Assert.IsFalse(w00.Threw, "00 不应抛:" + w00.Ex?.Message);
// 开始日 7/6 属首段:all-in = spread(-0.0155) + 定盘(0.0142) = -0.0013
// 单利下 00 与 10 的利息差 = 恰好首日一天利息(年化 A365)。
var expectedStartDayInterest = Notional * (Spread + 0.0142m) / AnnualDays;
Assert.AreEqual(expectedStartDayInterest, w10.Fe.InterestAmount - w00.Fe.InterestAmount, 0.0000001m,
"不算头(00)应恰好少计开始日一天利息(CalcFirst 回归锚)");
}
}
}