refactor(accrual): 接线SimpleInterest/CompoundInterest内部 + 分段修复 + 去重

CalcDailySimpleInterest/CalcDailyCompoundInterest内部逐日循环→分段纯函数(签名不变):
- AccrueSimplePeriod: AccruedToday改未缩放累计(TdInterestAmount口径)
- AccrueCompoundPeriod: 新增out finalBasis供flowEvent.InterestPrincipal精确赋值
- 修复5处分段边界bug(includeStart/includeEnd/resetCarryInterest interestPeriod=1场景)
- 提取BuildSegmentRates消除SimpleInterest/CompoundInterest取率重复
- 影子测试补AccruedToday断言关闭测试盲区

SwapModule零回归(7基线/510通过)
This commit is contained in:
hjhan
2026-08-12 13:09:17 +08:00
parent 85746d7ee9
commit c86633479d
4 changed files with 224 additions and 147 deletions
@@ -93,11 +93,16 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
endDate: EndDate,
boundary: AccrualBoundary.StartOnly,
annualDays: AnnualDays,
isAnnualized: true);
isAnnualized: true,
resetCarryInterest: 0m,
realizedInterest: 0m,
unwindFraction: 1m,
finalBasis: out _);
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
}
/// <summary>
@@ -138,11 +143,13 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
isAnnualized: true,
resetCarryInterest: carry,
realizedInterest: consumed,
unwindFraction: closePct);
unwindFraction: closePct,
finalBasis: out _);
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
}
/// <summary>
@@ -177,11 +184,16 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
endDate: EndDate,
boundary: AccrualBoundary.Both,
annualDays: AnnualDays,
isAnnualized: true);
isAnnualized: true,
resetCarryInterest: 0m,
realizedInterest: 0m,
unwindFraction: 1m,
finalBasis: out _);
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
}
}
}
@@ -64,6 +64,27 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
public StubSvc() : base(new OptUserInfo(0, nameof(SimplePeriodShadowTest), OptUserFrom.UnitTest)) { }
}
/// <summary>带浮动率 stub 的 SwapDealServiceoverride IndexFixer 注入预设 FR007 取价。</summary>
private sealed class FloatStubSvc : SwapDealService
{
private readonly IIndexFixer _fixer;
public FloatStubSvc(IIndexFixer fixer) : base(new OptUserInfo(0, nameof(SimplePeriodShadowTest), OptUserFrom.UnitTest))
=> _fixer = fixer;
protected override IIndexFixer IndexFixer => _fixer;
}
/// <summary>Stub IIndexFixer:对所有查询返回固定 fixing(不依赖日期匹配,规避 QDP 日历差异)。</summary>
private sealed class StubIndexFixer : IIndexFixer
{
private readonly decimal _rate;
public StubIndexFixer(decimal rate) => _rate = rate;
public bool TryGetFixing(DateTime fixingDate, string underlyingCode, out decimal rate)
{
rate = _rate;
return true;
}
}
/// <summary>
/// 固定利率(无FR007)算头不算尾,全平,无历史归档。
/// </summary>
@@ -100,6 +121,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
}
/// <summary>
@@ -145,6 +167,70 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
}
/// <summary>
/// 单利 + FR007 浮动利率 + 多重置日 + 部分平仓 + 有历史归档。
/// 验证旧方法内部取价循环生成的 segmentRates 与手算一致——为抽取共享 SegmentRateBuilder 做安全网。
/// 场景:preEod.ValueDate=4/30,重置日 4/21(跳过取价), 4/28(跳过取价), 5/5(取 FR007 fixing)。
/// </summary>
[TestMethod]
public void _单利浮动_FR007_部分平仓_旧新一致()
{
const decimal floatRateIn = 0.0150m; // 入参 floateRate(上一次取到的浮动率 1.50%)
const decimal fixingAtReset = 0.0125m; // 5/5 重置日取到的 FR007 fixing1.25%
const decimal closePct = 0.5m;
var preEodDate = new DateTime(2026, 4, 30); // 上一日终=4/305/5 > 4/30 触发取价
var position = CreatePosition();
position.FloatRateUnderlyingCode = "FR007";
var preEod = new eod_swap_position
{
id = 1, SwapTradeId = 1, PositionId = 1001,
ValueDate = preEodDate,
TdInterestPrincipal = Notional,
InterestProfitSum = 200_000m,
PosiNotionalValue = Notional, FloatRate = 0m
};
var flowEvent = new swap_flow_event { InterestRate = Spread };
// 旧方法(通过 stub IndexFixer 注入 FR007 取价)
decimal oldI = 0, oldTd = 0;
var svc = new FloatStubSvc(new StubIndexFixer(fixingAtReset));
svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent,
AnnualDays, false, floatRateIn, closePct, Notional, true, false, ref oldI, ref oldTd);
// 新方法:手算 segmentRates(对齐旧代码取价循环的逻辑)
// 4/21 <= preEodDate(4/30) → 跳过取价,currentFloat 保持入参 floatRateIn
// 4/28 <= preEodDate(4/30) → 跳过取价,currentFloat 保持入参 floatRateIn
// 5/5 > preEodDate(4/30) → 取价,currentFloat 更新为 fixingAtReset
var segRates = new List<(DateTime, decimal)>
{
(StartDate, Spread + floatRateIn), // (4/21, 0.0175)
(StartDate.AddDays(7), Spread + floatRateIn), // (4/28, 0.0175)
(StartDate.AddDays(14), Spread + fixingAtReset), // (5/5, 0.0150)
};
// 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv
var accrualPrincipal = Notional + Notional - Notional;
var result = FundingLegAccrual.AccrueSimplePeriod(
priorAccrued: 200_000m * closePct,
notional: accrualPrincipal,
unwindFraction: closePct,
segmentRates: segRates,
startDate: StartDate,
endDate: EndDate,
priorValueDate: preEodDate,
boundary: AccrualBoundary.StartOnly,
annualDays: AnnualDays,
isAnnualized: true);
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
}
}
}