Merge branch 'glms/feature/1.4.2' into glms/feature/0812_zmr_divPower
This commit is contained in:
@@ -11,7 +11,9 @@ namespace UnitTestProject.Modules.SwapModule.Penalty
|
||||
/// ② 重置日前一日平仓(② 几乎整段、窗口首段 0 天);
|
||||
/// ③ 到期日恰为重置日(末段 [到期,到期] 1 天);
|
||||
/// ④ 锚点偏离(td.StartDate=7/31 但腿 PosiStartDate=8/3 的延期/存续腿——重置网格整体不同);
|
||||
/// ⑤ 起息日当天平仓(无 preEod)。
|
||||
/// ⑤ 起息日当天平仓(无 preEod);
|
||||
/// ⑥ 部分平仓 share<1 + 无 preEod 兜底——钉 merger 复刻 GetInterests 本金口径的接缝
|
||||
/// (现有用例全部 closePercent=1m,重放基数与复刻本金的口径偏差在 share=1 下不可见)。
|
||||
///
|
||||
/// 一致性前提(与现实世界对齐):冻结利率 = 当前重置区间(含 unwind-1 的区间)的在役利率,
|
||||
/// 即"历史末段利率 = 冻结利率";历史各段定盘不同(体现真实 FR007 利率历史)。
|
||||
@@ -67,7 +69,7 @@ namespace UnitTestProject.Modules.SwapModule.Penalty
|
||||
|
||||
private static decimal RunFee(trade td, swap_position p, decimal settledAmount,
|
||||
eod_swap_position? preEod, DateTime unwind, bool settled, decimal spread,
|
||||
decimal interestPrincipal = 0m, bool maturityCalcLast = true)
|
||||
decimal interestPrincipal = 0m, bool maturityCalcLast = true, decimal closePercent = 1m)
|
||||
{
|
||||
var e = new swap_flow_event
|
||||
{
|
||||
@@ -78,7 +80,8 @@ namespace UnitTestProject.Modules.SwapModule.Penalty
|
||||
PenaltyInterestFeeMerger.Merge(
|
||||
td, new List<swap_position> { p }, new List<swap_flow_event> { e },
|
||||
unwind, AnnualDays, settled, maturityCalcLast: maturityCalcLast,
|
||||
posiNotionalValue: Notional, closePosiNotionalValue: Notional, closePercent: 1m,
|
||||
posiNotionalValue: Notional, closePosiNotionalValue: Notional * closePercent,
|
||||
closePercent: closePercent,
|
||||
getSpread: _ => spread, getPreEod: _ => preEod, tryGetFixing: (d, c) => spread);
|
||||
return e.InterestFee;
|
||||
}
|
||||
@@ -192,6 +195,30 @@ namespace UnitTestProject.Modules.SwapModule.Penalty
|
||||
"无preEod+已有重置:兜底取事件基数后 ① 精确,全期=实结+罚息(修复前差≈3.17元)");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 部分平仓无preEod兜底_share对齐本金口径_恒等式成立()
|
||||
{
|
||||
// 接缝守卫:merger 的 closePrincipal 走 CalcNotional 复刻 GetInterests 口径
|
||||
// (标的期初全价 = posiNotional×closePercent),而重放基数由调用方以
|
||||
// closePosiNotionalValue 缩放——两处口径若有偏差,share=1 时不可见、
|
||||
// share<1 时 ① 里会混入本金差。本用例以 50% 平仓钉死该对齐。
|
||||
var start = new DateTime(2026, 8, 5); var unwind = new DateTime(2026, 8, 20); var maturity = new DateTime(2026, 9, 30);
|
||||
var hist = new decimal[] { 0.0216m, 0.0144m }; // 8/5 段 2.16% / 8/19 段 1.44%(=冻结),14 天重置
|
||||
var share = 0.5m;
|
||||
var closedNotional = Notional * share;
|
||||
// 被平份额的实结与重放基数:复利对 notional 线性,直接按半额本金重放
|
||||
var elapsed = AccrueOnGrid(start, unwind, AccrualBoundary.StartOnly, hist, notional: closedNotional, period: 14);
|
||||
var replayFinalBasis = closedNotional + AccrueOnGrid(start, new DateTime(2026, 8, 18), AccrualBoundary.Both, hist, notional: closedNotional, period: 14);
|
||||
|
||||
var fee = RunFee(CreateTrade(start, maturity), CompoundLeg(start, maturity, hist[^1], periodDays: 14), elapsed,
|
||||
preEod: null, unwind: unwind, settled: false, spread: hist[^1],
|
||||
interestPrincipal: replayFinalBasis, maturityCalcLast: false, closePercent: share);
|
||||
|
||||
var full = AccrueOnGrid(start, maturity, AccrualBoundary.StartOnly, hist, notional: closedNotional, period: 14);
|
||||
Assert.AreEqual((double)full, (double)(elapsed + fee), 0.01,
|
||||
"部分平仓+无preEod:兜底①按被平份额缩放精确,全期(被平份额)=实结+罚息(口径漂移时此式必挂)");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 起息日当天平仓_无preEod_恒等式成立()
|
||||
{
|
||||
|
||||
@@ -50,7 +50,8 @@ namespace UnitTestProject.Modules.SwapModule.Penalty
|
||||
|
||||
private static void RunMerge(
|
||||
swap_position p, swap_flow_event normalEvent, eod_swap_position? preEod,
|
||||
Func<swap_position, decimal>? getSpread = null, Func<DateTime, string, decimal?>? tryGetFixing = null)
|
||||
Func<swap_position, decimal>? getSpread = null, Func<DateTime, string, decimal?>? tryGetFixing = null,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
getSpread ??= _ => Rate;
|
||||
tryGetFixing ??= (d, code) => Rate;
|
||||
@@ -61,7 +62,8 @@ namespace UnitTestProject.Modules.SwapModule.Penalty
|
||||
posiNotionalValue: Notional, closePosiNotionalValue: Notional, closePercent: 1m,
|
||||
getSpread: getSpread,
|
||||
getPreEod: _ => preEod,
|
||||
tryGetFixing: tryGetFixing);
|
||||
tryGetFixing: tryGetFixing,
|
||||
trace: trace);
|
||||
}
|
||||
|
||||
/// <summary>复利重放 [StartDate, endDate],重置段=每 7 天;分段利率由 rates 决定(rates.Count=1 时为常率)。</summary>
|
||||
@@ -152,6 +154,45 @@ namespace UnitTestProject.Modules.SwapModule.Penalty
|
||||
Assert.IsTrue(e.InterestFee > 0m, "无 preEod(首日平仓等)仍可计算罚息");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 无preEod复利段中兜底为零且账龄超重置周期_留退化告警trace()
|
||||
{
|
||||
// 场景:无日终快照 + 复利 + 段中平仓,事件 InterestPrincipal 仍是种子值(=平仓本金)→兜底已并复利本金=0。
|
||||
// 账龄 25 天 ≥ 7 天重置周期:复利每周期并本理应>0,已并复利本金=0 属退化——
|
||||
// 典型成因=interestWindowEmpty(当日已结息)早退未重放覆盖种子、或日终归档缺失。
|
||||
var e = NormalEvent(settledAmount: 50_000m);
|
||||
e.InterestPrincipal = Notional; // GetInterests 种子值:interestWindowEmpty 早退路径不会用重放基数覆盖它
|
||||
var trace = new AccrualTrace();
|
||||
RunMerge(Leg(InterestTypeEnum.复利), e, preEod: null, trace: trace);
|
||||
|
||||
StringAssert.Contains(trace.ToString(), "无preEod兜底已并复利本金=0",
|
||||
"已并复利本金=0 且账龄超周期必须留告警,供事后核对日终归档/计息窗口根因");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 无preEod兜底为正_不留退化告警()
|
||||
{
|
||||
var e = NormalEvent(settledAmount: 50_000m);
|
||||
e.InterestPrincipal = Notional + 100_000m; // 重放末次并本金后基数 → 已并复利本金=100000 正常路径
|
||||
var trace = new AccrualTrace();
|
||||
RunMerge(Leg(InterestTypeEnum.复利), e, preEod: null, trace: trace);
|
||||
|
||||
Assert.IsFalse(trace.ToString().Contains("兜底已并复利本金=0"), "已并复利本金>0 是正常兜底路径,不得告警");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 无preEod真首日兜底为零_不留退化告警()
|
||||
{
|
||||
var p = Leg(InterestTypeEnum.复利);
|
||||
p.PosiStartDate = UnwindDate; // 起息日当天平仓:账龄 0 < 重置周期,已并复利本金=0 是设计内约定(类头注)
|
||||
var e = NormalEvent(settledAmount: 50_000m);
|
||||
e.InterestPrincipal = Notional;
|
||||
var trace = new AccrualTrace();
|
||||
RunMerge(p, e, preEod: null, trace: trace);
|
||||
|
||||
Assert.IsFalse(trace.ToString().Contains("兜底已并复利本金=0"), "真首日 已并复利本金=0 合法,不得告警");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 冻结利率解析失败_跳过该腿不阻断()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user