fix(swap): 修复重置日期利息计算逻辑
- 修正了 calcLast=false 时重置日取价被跳过的问题 - 确保重置日之前的复利能够正确并入本金 - 调整了计息日判断逻辑的位置以保证本金重置正常执行 - 修复了重置日当天利息不应被计入的逻辑错误
This commit is contained in:
@@ -369,6 +369,67 @@ namespace YLErp.Modules.SwapModule
|
||||
Assert.IsTrue(Math.Abs((result.FloatRate ?? 0) - (decimal)rateNew) < 0.0001m,
|
||||
$"平仓日=重置日时 FloatRate 应={rateNew}(取到新利率)。" +
|
||||
$"实际={result.FloatRate},若={rateOld} 说明 calcLast=false 跳过了重置日取价(GLMS-JIATT-20260805 根因)");
|
||||
|
||||
var interestBeforeResetDate = Principal * (FixedRate + (decimal)rateOld) * 7m / AnnualDays;
|
||||
AssertDecimal(Principal + interestBeforeResetDate, result.InterestPrincipal,
|
||||
"calcLast=false 的重置日仍应将前 7 天复利并入本金");
|
||||
AssertDecimal(interestBeforeResetDate, result.InterestAmount,
|
||||
"calcLast=false 不应计入重置日当天利息");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CI_008_ResetDayPartialCloseCarriesRemainingInterestIntoPrincipal()
|
||||
{
|
||||
const decimal previousPrincipal = 50061728.39m;
|
||||
const decimal remainingPrincipal = 30037037.04m;
|
||||
const decimal previousInterest = 7425.050203320057m;
|
||||
const decimal fixedRate = 0.001234m;
|
||||
const double oldFloatRate = 0.0123;
|
||||
const double newFloatRate = 0.0213;
|
||||
var startDate = new DateTime(2026, 7, 28);
|
||||
var resetDate = new DateTime(2026, 8, 4);
|
||||
var service = new StubSwapDealService(
|
||||
new OptUserInfo(0, nameof(ConsumedInterestScenarioTest), OptUserFrom.UnitTest),
|
||||
d => d >= resetDate ? newFloatRate : oldFloatRate);
|
||||
var td = CreateTrade();
|
||||
td.StartDate = startDate;
|
||||
td.TradeDate = startDate;
|
||||
var position = new swap_position
|
||||
{
|
||||
id = 1001, SwapTradeId = td.id,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = (int)InterestModeEnum.标的期初全价,
|
||||
InterestType = (int)InterestTypeEnum.复利,
|
||||
InterestRateDefault = fixedRate,
|
||||
PosiStartDate = startDate, PosiMatuirityDate = ExerciseDate,
|
||||
IsAnnualized = true, interest_rest_days = 7, interest_rule = 0,
|
||||
FloatRateUnderlyingCode = "FR007",
|
||||
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel>
|
||||
{
|
||||
new IntervalModel { Date = ExerciseDate, Rate = fixedRate, Settlement = 0 }
|
||||
})
|
||||
};
|
||||
var preEod = new eod_swap_position
|
||||
{
|
||||
id = 1, PositionId = position.id, ValueDate = resetDate.AddDays(-1),
|
||||
TdInterestPrincipal = previousPrincipal,
|
||||
InterestIncomeSum = previousInterest,
|
||||
InterestProfitSum = previousInterest,
|
||||
FloatRate = (decimal)oldFloatRate
|
||||
};
|
||||
|
||||
var result = service.GetInterests(td, td.trade_extend, resetDate, resetDate,
|
||||
new List<eod_swap_position> { preEod }, new List<swap_position> { position },
|
||||
remainingPrincipal, remainingPrincipal, 0m, remainingPrincipal, 1m,
|
||||
(int)SwapEventTypeEnum.平仓, true, false, 0m, remainingPrincipal,
|
||||
add: false, settment: false, newCalcLast: false).Single();
|
||||
|
||||
var remainingInterest = previousInterest * remainingPrincipal / previousPrincipal;
|
||||
var expectedPrincipal = remainingPrincipal + remainingInterest;
|
||||
var expectedDailyInterest = expectedPrincipal * (fixedRate + (decimal)newFloatRate) / AnnualDays;
|
||||
AssertDecimal(expectedPrincipal, result.InterestPrincipal);
|
||||
AssertDecimal(expectedDailyInterest,
|
||||
result.InterestPrincipal * (result.InterestRate + result.FloatRate.Value) / AnnualDays);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1205,7 +1205,14 @@ namespace YLErp.Modules.SwapModule
|
||||
var floateRate = preEodPosition.FloatRate;
|
||||
if (position.InterestType == (int)InterestTypeEnum.复利)
|
||||
{
|
||||
CalcDailyCompoundInterest( endDate, position, closePosiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount, consumedInterest);
|
||||
var remainingPercent = preEodPosition.TdInterestPrincipal > 0m
|
||||
? closePosiNotionalValue / preEodPosition.TdInterestPrincipal
|
||||
: 1m;
|
||||
remainingPercent = Math.Max(0m, Math.Min(1m, remainingPercent));
|
||||
var resetCarryInterest = preEodPosition.InterestIncomeSum * remainingPercent;
|
||||
CalcDailyCompoundInterest(endDate, position, closePosiNotionalValue, interest, annualDays, needPrice,
|
||||
floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount,
|
||||
consumedInterest, resetCarryInterest);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1233,7 +1240,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <param name="isAnnualized">是否年化</param>
|
||||
/// <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, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m)
|
||||
public void CalcDailyCompoundInterest(DateTime endDate, swap_position position, decimal principal, 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, decimal resetCarryInterest = 0m)
|
||||
{
|
||||
var startDate = position.PosiStartDate;
|
||||
decimal interestProfitSum = 0;
|
||||
@@ -1265,15 +1272,14 @@ namespace YLErp.Modules.SwapModule
|
||||
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fr007RateDate:yyyy年MM月dd日}的价格");
|
||||
}
|
||||
}
|
||||
if (!calcFirst && accrueDate == startDate) continue; // 首日不算头
|
||||
if (!calcLast && accrueDate == endDate) continue; // 到期日不算尾(只跳过计息,取价已在上方完成)
|
||||
if (accrueDate >= startDate)
|
||||
{
|
||||
if (i % interestPeriod == 0)
|
||||
{
|
||||
// 复利时:利息并入本金(FR007 取价已提前到 calcFirst/calcLast 跳过之前完成)
|
||||
dynomicPrincipal = principal + interest;
|
||||
tdDynomicPrincipal = principal + interest;
|
||||
var interestToReset = i == 0 || resetCarryInterest == 0m ? interest : resetCarryInterest;
|
||||
dynomicPrincipal = principal + interestToReset;
|
||||
tdDynomicPrincipal = principal + interestToReset;
|
||||
flowEvent.InterestPrincipal = tdDynomicPrincipal;
|
||||
TdInterestPrincipal = tdDynomicPrincipal;
|
||||
}
|
||||
@@ -1283,6 +1289,11 @@ namespace YLErp.Modules.SwapModule
|
||||
flowEvent.InterestPrincipal = tdDynomicPrincipal;
|
||||
TdInterestPrincipal = tdDynomicPrincipal;
|
||||
}
|
||||
}
|
||||
if (!calcFirst && accrueDate == startDate) continue; // 首日不算头
|
||||
if (!calcLast && accrueDate == endDate) continue; // 到期日不算尾(只跳过计息,重置本金已在上方完成)
|
||||
if (accrueDate >= startDate)
|
||||
{
|
||||
flowEvent.FloatRate = Convert.ToDecimal(floatRate);
|
||||
var interest1 = flowEvent.InterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
|
||||
var tdinterest1 = TdInterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
|
||||
@@ -1391,7 +1402,11 @@ namespace YLErp.Modules.SwapModule
|
||||
var days = (endDate - tradeDate).Days;
|
||||
if (days % interestPeriod == 0)
|
||||
{
|
||||
tdDynomicPrincipal = tdDynomicPrincipal + interestProfitSum;
|
||||
var remainingPercent = preEodPosition.TdInterestPrincipal > 0m
|
||||
? principal / preEodPosition.TdInterestPrincipal
|
||||
: 1m;
|
||||
remainingPercent = Math.Max(0m, Math.Min(1m, remainingPercent));
|
||||
tdDynomicPrincipal = tdDynomicPrincipal + interestProfitSum * remainingPercent;
|
||||
if (!string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
|
||||
{
|
||||
// 获取合适的 rateDate
|
||||
|
||||
Reference in New Issue
Block a user