fix(swap): 修复部分平仓后复利本金计算逻辑(修复T+1)
- 修复部分平仓后下一日复利本金未按剩余比例继承的问题 - 添加对最终日不算尾时历史差分重放的日期边界控制 - 修复算头不算尾快速路径中已平部分进入下一日复利的错误 - 确保重置日动态本金不会被二次缩放导致剩余本金计算错误 - 添加单元测试验证部分平仓后复利本金按剩余70%本金计提的逻辑 - 补充最终全平重放时历史终点包含当日利息后再做差额的测试用例
This commit is contained in:
@@ -1562,6 +1562,9 @@ namespace YLErp.Modules.SwapModule
|
|||||||
"0005 计算不算尾时,部分平仓日终新增复利必须按剩余70%本金计提");
|
"0005 计算不算尾时,部分平仓日终新增复利必须按剩余70%本金计提");
|
||||||
AssertDecimal(196212.213529615939m, partialEod.InterestIncomeSum,
|
AssertDecimal(196212.213529615939m, partialEod.InterestIncomeSum,
|
||||||
"0005 部分平仓后日终待实现复利必须扣除实际80002.30结算");
|
"0005 部分平仓后日终待实现复利必须扣除实际80002.30结算");
|
||||||
|
AssertDecimal(previousEod.TdInterestPrincipal * (1m - partialNotional / originalNotional),
|
||||||
|
partialEod.TdInterestPrincipal,
|
||||||
|
"0005 部分平仓后,下一日复利本金必须只继承剩余70%本金");
|
||||||
|
|
||||||
var intermediateDate = new DateTime(2026, 5, 18);
|
var intermediateDate = new DateTime(2026, 5, 18);
|
||||||
var intermediateInterest = dealService.GetInterests(
|
var intermediateInterest = dealService.GetInterests(
|
||||||
@@ -1573,6 +1576,39 @@ namespace YLErp.Modules.SwapModule
|
|||||||
Assert.IsTrue(Math.Abs(259348.386714765m - intermediateInterest.InterestAmount) <= 0.01m,
|
Assert.IsTrue(Math.Abs(259348.386714765m - intermediateInterest.InterestAmount) <= 0.01m,
|
||||||
$"0005 5/18 复利应承接部分平仓后的累计利息 Expected approximately 259348.386714765, Actual: {intermediateInterest.InterestAmount}");
|
$"0005 5/18 复利应承接部分平仓后的累计利息 Expected approximately 259348.386714765, Actual: {intermediateInterest.InterestAmount}");
|
||||||
|
|
||||||
|
var intermediateEod = partialEod.Clone();
|
||||||
|
intermediateEod.id = 18283;
|
||||||
|
intermediateEod.ValueDate = intermediateDate;
|
||||||
|
intermediateEod.InterestIncomeSum = intermediateInterest.InterestAmount;
|
||||||
|
intermediateEod.InterestProfitSum = intermediateInterest.InterestAmount;
|
||||||
|
intermediateEod.TdInterestPrincipal = remainingNotional;
|
||||||
|
intermediateEod.PosiNotionalValue = remainingNotional;
|
||||||
|
|
||||||
|
var expectedEndFlow = new swap_flow_event { InterestRate = spread };
|
||||||
|
decimal expectedAmountAtEnd = 0m;
|
||||||
|
decimal expectedTdAmountAtEnd = 0m;
|
||||||
|
dealService.CalcDailyCompoundInterest(
|
||||||
|
finalCloseDate, position, remainingNotional, expectedEndFlow, AnnualDays, false,
|
||||||
|
intermediateEod.FloatRate, 1m, originalNotional, 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,
|
||||||
|
intermediateEod.FloatRate, 1m, originalNotional, true, true,
|
||||||
|
ref expectedAmountAtPreviousEod, ref expectedTdAmountAtPreviousEod);
|
||||||
|
var expectedFinalInterest = intermediateEod.InterestIncomeSum
|
||||||
|
+ expectedAmountAtEnd - expectedAmountAtPreviousEod;
|
||||||
|
var finalInterest = dealService.GetInterests(
|
||||||
|
td, td.trade_extend, finalCloseDate, finalCloseDate,
|
||||||
|
new List<eod_swap_position> { intermediateEod }, new List<swap_position> { position },
|
||||||
|
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||||||
|
(int)SwapEventTypeEnum.平仓, false, false, 0m, originalNotional,
|
||||||
|
settment: false, newCalcLast: false).Single();
|
||||||
|
AssertDecimal(expectedFinalInterest, finalInterest.InterestAmount,
|
||||||
|
"0005 最终全平重放时,历史5/18终点必须包含当日利息后再做差额");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
|
|||||||
@@ -1243,7 +1243,19 @@ namespace YLErp.Modules.SwapModule
|
|||||||
var interestAtEnd = new swap_flow_event { InterestRate = rate };
|
var interestAtEnd = new swap_flow_event { InterestRate = rate };
|
||||||
decimal amountAtEnd = 0m;
|
decimal amountAtEnd = 0m;
|
||||||
decimal tdAmountAtEnd = 0m;
|
decimal tdAmountAtEnd = 0m;
|
||||||
CalcDailyCompoundInterest(endDate, position, closePosiNotionalValue,
|
// InitInterestDate 在最终日不算尾时会先把 endDate 回拨一天;
|
||||||
|
// 历史差分的 amountAtEnd 需补回该日,但计算器仍使用交易 calcLast,
|
||||||
|
// 并将重放日期限制在合约到期日,避免提前全平或超期重复计息。
|
||||||
|
var replayEndDate = endDate;
|
||||||
|
if (!calcLast && endDate < valueDate)
|
||||||
|
{
|
||||||
|
replayEndDate = endDate.AddDays(1);
|
||||||
|
if (replayEndDate > td.ExerciseDate.Value)
|
||||||
|
{
|
||||||
|
replayEndDate = td.ExerciseDate.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CalcDailyCompoundInterest(replayEndDate, position, closePosiNotionalValue,
|
||||||
interestAtEnd, annualDays, needPrice, floateRate, closePrecent, orginPv,
|
interestAtEnd, annualDays, needPrice, floateRate, closePrecent, orginPv,
|
||||||
calcFirst, calcLast, ref amountAtEnd, ref tdAmountAtEnd, consumedInterest);
|
calcFirst, calcLast, ref amountAtEnd, ref tdAmountAtEnd, consumedInterest);
|
||||||
var interestAtPreviousEod = new swap_flow_event { InterestRate = rate };
|
var interestAtPreviousEod = new swap_flow_event { InterestRate = rate };
|
||||||
|
|||||||
@@ -1427,6 +1427,18 @@ namespace YLErp.Modules.SwapModule
|
|||||||
var fullPrincipal = lastTdInterestPrincipal > 0m
|
var fullPrincipal = lastTdInterestPrincipal > 0m
|
||||||
? lastTdInterestPrincipal
|
? lastTdInterestPrincipal
|
||||||
: oriPosiNotionalValue;
|
: oriPosiNotionalValue;
|
||||||
|
// CalcSwapInterests 的算头不算尾非重置日快速路径会返回上一 EOD 的全额本金;
|
||||||
|
// 只有该路径需要按剩余比例缩放,避免已平部分进入下一日复利。
|
||||||
|
// 重置日动态本金已包含累计复利,calcLast=true 也已返回当前剩余本金,
|
||||||
|
// 两者都不能再次缩放,否则会把剩余本金二次打折。
|
||||||
|
var usesFullPreviousEodPrincipal = !calcLast
|
||||||
|
&& hasPreviousEod
|
||||||
|
&& (valueDate - eodPayPosition.ValueDate).Days == 1
|
||||||
|
&& (valueDate - position.PosiStartDate).Days % (position.interest_rest_days ?? 1) != 0;
|
||||||
|
if (usesFullPreviousEodPrincipal)
|
||||||
|
{
|
||||||
|
newEodPayPosition.TdInterestPrincipal *= 1m - closePercent;
|
||||||
|
}
|
||||||
var accrualPrincipal = calcLast
|
var accrualPrincipal = calcLast
|
||||||
? fullPrincipal
|
? fullPrincipal
|
||||||
: fullPrincipal * (1m - closePercent);
|
: fullPrincipal * (1m - closePercent);
|
||||||
|
|||||||
Reference in New Issue
Block a user