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
+10 -10
View File
@@ -946,12 +946,12 @@ namespace YLErp.Modules.SwapModule
if (position.InterestType == (int)InterestTypeEnum.)
{
// 复利计算
CalcDailyCompoundInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, ref interestAmount, ref tdInterestAmount);
CalcDailyCompoundInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, eodFloatRate, 1m, ref interestAmount, ref tdInterestAmount);
}
else
{
// 单利计算
CalcDailySimpleInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, ref interestAmount, ref tdInterestAmount);
CalcDailySimpleInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, eodFloatRate, 1m, ref interestAmount, ref tdInterestAmount);
}
}
@@ -1121,7 +1121,7 @@ namespace YLErp.Modules.SwapModule
// 把上日尚未实现的的利息 按本次平掉的这部分计息基数分给本次平仓 并在重置日并入计息基数
// 它只在当前 endDate 恰好为重置日时使用,避免把同一笔历史利息重复资本化。
var resetCarryInterest = preEodPosition.InterestIncomeSum * remainingPercent;
CalcDailyCompoundInterest(endDate, position, closePosiNotionalValue, interest, annualDays, needPrice,
CalcDailyCompoundInterest(endDate, position, closePosiNotionalValue, interest, annualDays,
floateRate, closePrecent, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount,
consumedInterest, resetCarryInterest);
if (preEodPosition.id != 0 && closePrecent == 1m)
@@ -1149,7 +1149,7 @@ namespace YLErp.Modules.SwapModule
}
// 计算截至本次平仓日的累计利息 amountAtEnd
CalcDailyCompoundInterest(replayEndDate, position, closePosiNotionalValue,
interestAtEnd, annualDays, needPrice, floateRate, closePrecent,
interestAtEnd, annualDays, floateRate, closePrecent,
calcFirst, calcLast, ref amountAtEnd, ref tdAmountAtEnd, consumedInterest);
var interestAtPreviousEod = new swap_flow_event { InterestRate = rate };
decimal amountAtPreviousEod = 0m;
@@ -1158,7 +1158,7 @@ namespace YLErp.Modules.SwapModule
// 因此此处按闭区间包含上一日终当天,避免算头不算尾时重复加入该日利息。
// 计算截至上一日终累积的利息 amountAtPreviousEod
CalcDailyCompoundInterest(preEodPosition.ValueDate, position, closePosiNotionalValue,
interestAtPreviousEod, annualDays, needPrice, floateRate, closePrecent,
interestAtPreviousEod, annualDays, floateRate, closePrecent,
calcFirst, true, ref amountAtPreviousEod, ref tdAmountAtPreviousEod, consumedInterest);
// 例如 00045/18 待实现 -118631.261797,加 5/19 新增约 -4648.912760
// 得到最终应结 -123280.174557,按金额两位落为 Excel BN 的 -123280.17。
@@ -1170,7 +1170,7 @@ namespace YLErp.Modules.SwapModule
}
else
{
CalcDailySimpleInterest(preEodPosition, endDate, position, posiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount);
CalcDailySimpleInterest(preEodPosition, endDate, position, posiNotionalValue, interest, annualDays, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount);
}
interest.InterestAmount = Math.Round(InterestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
@@ -1231,7 +1231,7 @@ namespace YLErp.Modules.SwapModule
/// <param name="annualDays">年化天数</param>
/// <returns></returns>
public void CalcDailyCompoundInterest(DateTime endDate, swap_position position, decimal principal, swap_flow_event flowEvent,
int annualDays, bool needPrice, decimal floateRate, decimal closePercent, bool calcFirst, bool calcLast,
int annualDays, decimal floateRate, decimal closePercent, bool calcFirst, bool calcLast,
ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m, decimal resetCarryInterest = 0m)
{
var startDate = position.PosiStartDate;
@@ -1270,7 +1270,7 @@ namespace YLErp.Modules.SwapModule
/// <summary>
/// 计算单利 盘中(按重置天数分段,每段使用对应浮动利率)
/// </summary>
public void CalcDailySimpleInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m)
public void CalcDailySimpleInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m)
{
var startDate = position.PosiStartDate;
int interestPeriod = position.interest_rest_days ?? 1;
@@ -1320,7 +1320,7 @@ namespace YLErp.Modules.SwapModule
/// <param name="isAnnualized">是否年化</param>
/// <param name="annualDays">年化天数</param>
/// <returns></returns>
public void CalcDailyCompoundInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, ref decimal InterestAmount, ref decimal TdInterestAmount)
public void CalcDailyCompoundInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, decimal floateRate, decimal closePercent, ref decimal InterestAmount, ref decimal TdInterestAmount)
{
int interestPeriod = position.interest_rest_days ?? 1;
var isResetDay = (endDate - tradeDate).Days % interestPeriod == 0;
@@ -1367,7 +1367,7 @@ namespace YLErp.Modules.SwapModule
/// <summary>
/// 计算单利 收盘(按重置天数分段,每段使用对应浮动利率)
/// </summary>
public void CalcDailySimpleInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, ref decimal InterestAmount, ref decimal TdInterestAmount)
public void CalcDailySimpleInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, decimal floateRate, decimal closePercent, ref decimal InterestAmount, ref decimal TdInterestAmount)
{
// 首次操作(preEod.id == 0):计息基数按存量本金初始化——保留旧行为(含对 preEod 的就地修正)。
if (preEodPosition.id == 0)