test(swap): EQD-6968 补算尾依赖守卫——重置日缺价仍拦+newCalcLast覆盖算尾仍拦

防'不算尾放宽'误扩到依赖场景(方案一场景4):
- Guard_TailCalced×2:算尾(11)+当日重置日+当日缺价 → 必须仍抛(复利/单利)
- Baseline_TailCalced:算尾+当日有价 → 成功(对照)
- Guard_NewCalcLast:交易10不算尾但本次平仓指定算尾 → effectiveCalcLast=true 仍拦
- Baseline_NewCalcLast:指定算尾+有价 → 成功
Run 运行器扩参:calcMode/interestRule/newCalcLast/market(本提交仅用 calcMode/newCalcLast)

验证:GLMS20260817Fr007UnwindMorningTest 17/17 内存全绿
This commit is contained in:
hjhan
2026-08-19 09:26:30 +08:00
parent e94c8c25e2
commit f069c38d48
@@ -14,6 +14,11 @@ namespace YLErp.Modules.SwapModule
///
/// 核心语义:算头不算尾(calcLast=false)时 endDate 当天不计息,其 FR007 利率不参与计息。
/// 缺价时跳过取价(currentFloat 保持不变),不回退取其他日期利率,不告警。
///
/// 守卫矩阵(防"放宽过头",对应 EQD-6968 方案一四场景):
/// Guard_*:算尾("11"或newCalcLast=true)+当日重置日+当日缺价 → 必须仍拦截(正确依赖);
/// PrevBizDay_*interest_rule=-1(前一营业日基准)→ 取价日回拨,当日未发布也放行(场景2);
/// TailCalced_NonResetDay_*:算尾+当日非重置日 → 当日价未消费,缺价放行(场景3)。
/// </summary>
[TestClass]
public class GLMS20260817Fr007UnwindMorningTest
@@ -38,20 +43,23 @@ namespace YLErp.Modules.SwapModule
{
private readonly HashSet<DateTime> _omit;
private readonly double _closeRate;
private readonly Dictionary<DateTime, double> _market;
public readonly List<DateTime> PricedDates = new();
public StubSwapDealService(OptUserInfo optUser, IEnumerable<DateTime> omit, double closeRate = 0.0143)
public StubSwapDealService(OptUserInfo optUser, IEnumerable<DateTime> omit, double closeRate = 0.0143,
Dictionary<DateTime, double> market = null)
: base(optUser)
{
_omit = new HashSet<DateTime>(omit.Select(d => d.Date));
_closeRate = closeRate;
_market = market;
}
protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate)
{
rate = 0d;
if (underlyingCode != "FR007") return false;
var map = new Dictionary<DateTime, double>(Fr007Market) { [CloseDate] = _closeRate };
var map = new Dictionary<DateTime, double>(_market ?? Fr007Market) { [CloseDate] = _closeRate };
if (_omit.Contains(valueDate.Date)) return false;
if (map.TryGetValue(valueDate.Date, out rate))
{
@@ -75,7 +83,7 @@ namespace YLErp.Modules.SwapModule
private static OptUserInfo MakeOptUser() =>
new(0, nameof(GLMS20260817Fr007UnwindMorningTest), OptUserFrom.UnitTest);
private static trade BuildTrade(DateTime closeDate)
private static trade BuildTrade(DateTime closeDate, string calcMode = "10")
{
var extend = new trade_extend
{
@@ -83,7 +91,7 @@ namespace YLErp.Modules.SwapModule
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays,
InterestCalcMode = "10",
InterestCalcMode = calcMode,
SettlementRules = 0
})
};
@@ -102,7 +110,8 @@ namespace YLErp.Modules.SwapModule
};
}
private static swap_position BuildPosition(InterestTypeEnum interestType, DateTime closeDate, int restDays = 7)
private static swap_position BuildPosition(InterestTypeEnum interestType, DateTime closeDate, int restDays = 7,
int interestRule = 0)
{
var intervalModels = new List<IntervalModel>
{
@@ -124,7 +133,7 @@ namespace YLErp.Modules.SwapModule
InterestType = (int)interestType,
IsAnnualized = true,
interest_rest_days = restDays,
interest_rule = 0,
interest_rule = interestRule,
FloatRateUnderlyingCode = "FR007",
FloatRate = 0m,
PosiNotionalValue = Notional,
@@ -155,16 +164,20 @@ namespace YLErp.Modules.SwapModule
eod_swap_position preEod = null,
decimal closePrecent = 1m,
DateTime? omitDate = null,
double closeRate = 0.0143)
double closeRate = 0.0143,
string calcMode = "10",
int interestRule = 0,
bool newCalcLast = false,
Dictionary<DateTime, double> market = null)
{
var cd = closeDate ?? CloseDate;
var omit = new HashSet<DateTime>();
if (omitDate.HasValue) omit.Add(omitDate.Value.Date);
else if (!includeCloseDate) omit.Add(cd.Date);
var svc = new StubSwapDealService(MakeOptUser(), omit, closeRate);
var td = BuildTrade(cd);
var position = BuildPosition(interestType, cd, restDays);
var svc = new StubSwapDealService(MakeOptUser(), omit, closeRate, market);
var td = BuildTrade(cd, calcMode);
var position = BuildPosition(interestType, cd, restDays, interestRule);
var eodList = preEod == null
? new List<eod_swap_position>()
: new List<eod_swap_position> { preEod };
@@ -175,7 +188,7 @@ namespace YLErp.Modules.SwapModule
new List<swap_position> { position },
Notional, Notional, closePrecent,
(int)SwapEventTypeEnum., false, Notional,
false, settment: false, newCalcLast: false, closeList: null);
false, settment: false, newCalcLast: newCalcLast, closeList: null);
Assert.AreEqual(1, interests.Count, "应返回恰好 1 条利息事件");
return new Outcome { Fe = interests[0] };
}
@@ -283,5 +296,53 @@ namespace YLErp.Modules.SwapModule
AssertNoThrow(Run(InterestTypeEnum., includeCloseDate: true, closeDate: NonIntCloseDate, preEod: BuildPreEod(StartDate)),
"单利-非整倍数-有7/20价-应成功");
}
// ── 算尾守卫(EQD-6968 方案一场景4:算尾+当前营业日+当日重置日+当日缺价 → 必须仍拦截)──
// 放宽只针对"该日利率不参与计息"的场景;算尾时当日利率被消费,缺价拦截是正确依赖,不得误放。
[TestMethod]
public void Guard_TailCalced_ResetDayFr007Missing_StillThrows()
{
var o = Run(InterestTypeEnum., includeCloseDate: false, calcMode: "11",
preEod: BuildPreEod(new DateTime(2026, 7, 13)));
Assert.IsTrue(o.Threw, "算尾(11)+当日重置日+当日缺价 → 应拦截(该日利率被消费)");
StringAssert.Contains(o.Ex.Message, "FR007");
}
[TestMethod]
public void Guard_TailCalced_Simple_ResetDayFr007Missing_StillThrows()
{
var o = Run(InterestTypeEnum., includeCloseDate: false, calcMode: "11",
preEod: BuildPreEod(StartDate));
Assert.IsTrue(o.Threw, "单利 算尾(11)+当日重置日+当日缺价 → 应拦截");
StringAssert.Contains(o.Ex.Message, "FR007");
}
[TestMethod]
public void Baseline_TailCalced_ResetDayFr007Present_Succeeds()
{
AssertNoThrow(Run(InterestTypeEnum., includeCloseDate: true, calcMode: "11",
preEod: BuildPreEod(new DateTime(2026, 7, 13))),
"算尾(11)+当日重置日+当日有价 → 应成功");
}
// ── newCalcLast 守卫:交易本身"10"不算尾,但本次平仓显式指定算尾 → effectiveCalcLast=true → 缺价仍拦截 ──
[TestMethod]
public void Guard_NewCalcLast_OverridesToTail_MissingPrice_StillThrows()
{
var o = Run(InterestTypeEnum., includeCloseDate: false, newCalcLast: true,
preEod: BuildPreEod(new DateTime(2026, 7, 13)));
Assert.IsTrue(o.Threw, "不算尾(10)+本次平仓指定算尾+当日缺价 → 应按算尾拦截");
StringAssert.Contains(o.Ex.Message, "FR007");
}
[TestMethod]
public void Baseline_NewCalcLast_OverridesToTail_WithPrice_Succeeds()
{
AssertNoThrow(Run(InterestTypeEnum., includeCloseDate: true, newCalcLast: true,
preEod: BuildPreEod(new DateTime(2026, 7, 13))),
"不算尾(10)+本次平仓指定算尾+当日有价 → 应成功");
}
}
}