fix(swap): 修复部分平仓后复利计算问题(算头不算尾合约复利)

- 当calcLast为false时,部分平仓日终新增复利使用剩余本金计算
- 修复最终全平仓时历史终点利息计算逻辑
- 添加测试用例验证部分平仓后复利计算准确性
- 修正日终待实现利息累加时的历史终点处理方式
- 调整复利部分平仓金额计算方式,确保未平仓部分正确参与后续计算
This commit is contained in:
张名锐
2026-08-08 16:11:53 +08:00
parent 6fdc7d80b2
commit 54a86fccc1
3 changed files with 168 additions and 9 deletions
@@ -1447,6 +1447,164 @@ namespace YLErp.Modules.SwapModule
}
[TestMethod]
public void DI_GLMS_20260421_0005_EodPartialCloseUsesRemainingCompoundAccrualWhenCalcLastFalse()
{
const decimal originalNotional = 303139117.8m;
const decimal partialNotional = 90941735.34m;
const decimal remainingNotional = 212197382.46m;
const decimal spread = 0.0025m;
var partialCloseDate = new DateTime(2026, 5, 11);
var finalCloseDate = new DateTime(2026, 5, 19);
var td = new trade
{
id = 1828,
TradeNumber = "GLMS-20260421-0005",
ClientId = 999998,
TradeType = "收益互换",
TradeDate = new DateTime(2026, 4, 21),
StartDate = new DateTime(2026, 4, 21),
ExerciseDate = finalCloseDate,
TradeStatus = "确认成交",
ValidState = "Valid",
StructureType = "单标的",
QuoteCurrency = "CNY",
SettlementCurrency = "CNY",
trade_extend = new trade_extend
{
TradeId = 1828,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays,
InterestCalcMode = "10",
SettlementRules = 0
})
}
};
var position = new swap_position
{
id = 18281,
SwapTradeId = td.id,
PositionType = (int)PositionTypeFlag.Unknown,
InterestDirection = (int)SwapDirectionEnum.,
InterestMode = (int)InterestModeEnum.,
InterestRateDefault = spread,
InterestPrincipalFix = originalNotional,
PosiStartDate = td.StartDate.Value,
PosiMatuirityDate = finalCloseDate,
IsInitial = true,
Invalid = false,
InterestType = (int)InterestTypeEnum.,
IsAnnualized = true,
interest_rest_days = 7,
interest_rule = 0,
FloatRateUnderlyingCode = "FR007",
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel>
{
new IntervalModel { Date = finalCloseDate, Rate = spread, Settlement = 0 }
})
};
var dealService = new StubCompoundSwapDealService(new Dictionary<DateTime, double>
{
[new DateTime(2026, 4, 21)] = 0.0132,
[new DateTime(2026, 4, 28)] = 0.0136,
[new DateTime(2026, 4, 30)] = 0.0139,
[new DateTime(2026, 5, 4)] = 0.0139,
[new DateTime(2026, 5, 5)] = 0.0139,
[new DateTime(2026, 5, 6)] = 0.0136,
[new DateTime(2026, 5, 11)] = 0.0134,
[new DateTime(2026, 5, 12)] = 0.013,
[new DateTime(2026, 5, 13)] = 0.0129,
[new DateTime(2026, 5, 19)] = 0.0131
});
var eodService = new StubEodPositionService { DealService = dealService };
var previousEod = new eod_swap_position
{
id = 18282,
SwapTradeId = td.id,
PositionId = position.id,
ValueDate = new DateTime(2026, 5, 10),
InterestDirection = position.InterestDirection,
InterestMode = position.InterestMode,
InterestType = position.InterestType,
InterestRateDefault = spread,
InterestIncomeSum = 266674.349853521170m,
InterestProfitSum = 266674.349853521170m,
TdInterestPrincipal = 303324019.318344137434m,
PosiNotionalValue = originalNotional,
FloatRate = 0.0139m,
IsAnnualized = true,
interest_rest_days = 7,
interest_rule = 0
};
var partialCloseFlow = new swap_flow_event
{
SwapTradeId = td.id,
PositionId = position.id,
EventType = (int)SwapFlowEventTypeEnum.,
EventDate = partialCloseDate,
UnwindDate = partialCloseDate,
InterestDirection = position.InterestDirection,
InterestRate = spread,
InterestPrincipal = partialNotional,
InterestAmount = 80002.30m,
InterestClosePnL = 80002.30m,
DataState = (int)SwapFlowDateStateEnum.
};
var partialEod = eodService.ExecuteSaveAutoEodWithCloseInterestPosition(
previousEod, position, td, partialCloseDate, null, remainingNotional, 0m,
new List<swap_flow_event> { partialCloseFlow }, partialNotional, false);
AssertDecimal(9540.163676094769m, partialEod.TdInterestIncome,
"0005 计算不算尾时,部分平仓日终新增复利必须按剩余70%本金计提");
AssertDecimal(196212.213529615939m, partialEod.InterestIncomeSum,
"0005 部分平仓后日终待实现复利必须扣除实际80002.30结算");
var intermediateDate = new DateTime(2026, 5, 18);
var intermediateInterest = dealService.GetInterests(
td, td.trade_extend, intermediateDate, intermediateDate,
new List<eod_swap_position> { partialEod }, new List<swap_position> { position },
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
(int)SwapEventTypeEnum., false, false, 0m, originalNotional,
settment: false, newCalcLast: true).Single();
Assert.IsTrue(Math.Abs(259348.386714765m - intermediateInterest.InterestAmount) <= 0.01m,
$"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]
public void DI_GLMS_20260421_0006_FinalCloseCarriesPreviousEodInterest()
{