refactor(accrual): 抽取BuildSegmentRates共享分段取率+清理needPrice死参数

- 抽取 BuildSegmentRates:统一单利/复利分段取率循环,参数化 fetchAfterDate (单利传 ValueDate 仅取新段,复利传 null 全程取)

- 修复 BuildSegmentRates static→instance:访问实例属性 IndexFixer

- 清理 CalcDaily 层 needPrice 死参数:4个 CalcDaily* 方法签名移除 needPrice;上层 CalcSwapInterests/GetInterests 保留 (virtual seam/位置参数兼容)

- 修复 6 处调用点 needPrice 参数传递

- 新增影子测试:单利+FR007浮动+部分平仓+历史归档,验证 segmentRates 一致

- 新增 TdCarryInCharacterizationTest:钉死部分平仓 TdInterestAmount carry-in 行为

测试: 511通过 / 7预存在失败(数据依赖) / 9跳过
This commit is contained in:
hjhan
2026-08-12 18:02:36 +08:00
parent c412f918d9
commit f1f7cae832
7 changed files with 87 additions and 21 deletions
@@ -92,7 +92,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
decimal oldInterest = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailyCompoundInterestByEod(preEod, EodDate, TradeDate, position,
Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m,
Notional, Notional, flowEvent, AnnualDays, 0m, 1m,
ref oldInterest, ref oldTd);
// 新方法
@@ -125,7 +125,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
decimal oldInterest = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailyCompoundInterestByEod(preEod, nonResetDate, TradeDate, position,
Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m,
Notional, Notional, flowEvent, AnnualDays, 0m, 1m,
ref oldInterest, ref oldTd);
// 新方法
@@ -74,7 +74,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
decimal oldI = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent,
AnnualDays, false, 0m, 1m, true, false,
AnnualDays, 0m, 1m, true, false,
ref oldI, ref oldTd);
// 新方法:固定利率全段相同,分段点 = PosiStartDate + k×7
@@ -122,7 +122,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
decimal oldI = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailyCompoundInterest(EndDate, position, Notional * closePct, flowEvent,
AnnualDays, false, 0m, closePct, true, false,
AnnualDays, 0m, closePct, true, false,
ref oldI, ref oldTd, consumedInterest: consumed, resetCarryInterest: carry);
// 新方法
@@ -166,7 +166,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
decimal oldI = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent,
AnnualDays, false, 0m, 1m, true, true,
AnnualDays, 0m, 1m, true, true,
ref oldI, ref oldTd);
// 新方法
@@ -100,7 +100,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
decimal oldI = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent,
AnnualDays, false, 0m, 1m, Notional, true, false, ref oldI, ref oldTd);
AnnualDays, 0m, 1m, Notional, true, false, ref oldI, ref oldTd);
// 新方法:固定利率全段相同
// 旧代码差分: dynomicPrincipal = preEod.TdInterestPrincipal(=0) + posiPrincipal - orginPv = 0
@@ -146,7 +146,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
decimal oldI = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent,
AnnualDays, false, 0m, 0.5m, Notional, true, false, ref oldI, ref oldTd);
AnnualDays, 0m, 0.5m, Notional, true, false, ref oldI, ref oldTd);
// 新方法
// 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv
@@ -200,7 +200,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
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);
AnnualDays, floatRateIn, closePct, Notional, true, false, ref oldI, ref oldTd);
// 新方法:手算 segmentRates(对齐旧代码取价循环的逻辑)
// 4/21 <= preEodDate(4/30) → 跳过取价,currentFloat 保持入参 floatRateIn
@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YLErp.Derivatives.Interest; // InterestResult, AccrualBoundary, SwapInterest
using YLErp.Modules.SwapModule.Accrual; // SimpleInterestAccrual
namespace UnitTestProject.Modules.SwapModule.Accrual
{
/// <summary>
/// 性格化测试:部分平仓时 TdInterestAmount 的 carry-in 是否被错误缩放。
/// 调用真实纯函数 SimpleInterestAccrual.AccruePeriod,参数与线上 SwapDealService.cs:1290 完全一致
/// priorAccrued = InterestProfitSum * closePercent)。先钉死“当前行为”,修复后再改断言。
/// </summary>
[TestClass]
public class TdCarryInCharacterizationTest
{
[TestMethod]
public void _历史累计利息_carryIn被缩放_复现当前行为()
{
// ── 例子(教学用整数,非市场真实利率)──
// 昨日(上一EOD)全腿累计利息 InterestProfitSum = 100(不缩放口径,下游 EOD :1300/:1370 当累计用)
// 今日部分平仓 closePercent = 0.3(平 30%
// 计息基数 notional = 1000(全腿)
// 单段、年化=false、利率 0.10、区间 1 天 → 今日未缩放增量 = 1000*0.10*1 = 100
decimal interestProfitSum = 100m;
decimal closePercent = 0.3m;
decimal notional = 1000m;
var segmentRates = new List<(DateTime, decimal)> { (new DateTime(2026, 6, 1), 0.10m) };
var startDate = new DateTime(2026, 6, 1);
var endDate = new DateTime(2026, 6, 2); // 区间 1 天(StartOnly 边界 → days=1
var priorValueDate = new DateTime(2026, 5, 31);
var boundary = AccrualBoundary.StartOnly;
// 线上真实调用(SwapDealService.cs:1290):priorAccrued = InterestProfitSum * closePercent
var result = SimpleInterestAccrual.AccruePeriod(
priorAccrued: interestProfitSum * closePercent, // = 30 ← 已缩放
notional: notional,
unwindFraction: closePercent,
segmentRates: segmentRates,
startDate: startDate,
endDate: endDate,
priorValueDate: priorValueDate,
boundary: boundary,
annualDays: 365,
isAnnualized: false);
Console.WriteLine("==== 当前代码(buggy)实际输出 ====");
Console.WriteLine($"InterestAmount (Accrued) = {result.Accrued}");
Console.WriteLine($"TdInterestAmount (AccruedToday) = {result.AccruedToday}");
// ── 推演(当前代码)──
// InterestAmount = 30 + 300*0.10*1(=30) = 60 (缩放累计,正确)
// TdInterestAmount= 30 + 1000*0.10*1(=100) = 130 ← 应为 200
// 差距 = 100*(1-0.3) = 70,即历史累计被砍掉的那一截。
Assert.AreEqual(60m, result.Accrued);
Assert.AreEqual(130m, result.AccruedToday); // 当前 buggy 值,先钉死现状
// ── 期望值(修复后)──
// TdInterestAmount 应为:InterestProfitSum(100, 不缩放) + 今日未缩放增量(100) = 200
// 修复 = SwapDealService.cs:1290 改传 InterestProfitSum(去 *closePercent
// + SimpleInterestAccrual.cs:68 accrued = priorAccrued * unwindFraction
// 修复后 InterestAmount 仍 = 60(不变,正确),TdInterestAmount = 200。
}
}
}
@@ -509,7 +509,7 @@ namespace YLErp.Modules.SwapModule
decimal tdInterestAmount = 0m;
service.CalcDailyCompoundInterestByEod(preEod, resetDate, startDate, position,
principal, principal, flowEvent, AnnualDays, false, 0.013502m, 1m,
principal, principal, flowEvent, AnnualDays, 0.013502m, 1m,
ref interestAmount, ref tdInterestAmount);
AssertDecimal(principal + pendingInterest, flowEvent.InterestPrincipal,
@@ -1724,14 +1724,14 @@ namespace YLErp.Modules.SwapModule
decimal expectedAmountAtEnd = 0m;
decimal expectedTdAmountAtEnd = 0m;
dealService.CalcDailyCompoundInterest(
finalCloseDate, position, remainingNotional, expectedEndFlow, AnnualDays, false,
finalCloseDate, position, remainingNotional, expectedEndFlow, AnnualDays,
intermediateEod.FloatRate, 1m, true, false,
ref expectedAmountAtEnd, ref expectedTdAmountAtEnd);
var expectedPreviousFlow = new swap_flow_event { InterestRate = spread };
decimal expectedAmountAtPreviousEod = 0m;
decimal expectedTdAmountAtPreviousEod = 0m;
dealService.CalcDailyCompoundInterest(
intermediateDate, position, remainingNotional, expectedPreviousFlow, AnnualDays, false,
intermediateDate, position, remainingNotional, expectedPreviousFlow, AnnualDays,
intermediateEod.FloatRate, 1m, true, true,
ref expectedAmountAtPreviousEod, ref expectedTdAmountAtPreviousEod);
var expectedFinalInterest = intermediateEod.InterestIncomeSum