fix(swap): 修复部分平仓后复利计算问题(算头不算尾合约复利)
- 当calcLast为false时,部分平仓日终新增复利使用剩余本金计算 - 修复最终全平仓时历史终点利息计算逻辑 - 添加测试用例验证部分平仓后复利计算准确性 - 修正日终待实现利息累加时的历史终点处理方式 - 调整复利部分平仓金额计算方式,确保未平仓部分正确参与后续计算
This commit is contained in:
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user