fix(swap): 修复复利计算和部分平仓处理中的多个问题(算头算尾合约复利平仓)

- 修复合约名义本金规模模式下部分平仓时名义本金计算错误
- 修复最终全平时复利利息计算中历史平仓尾差处理问题
- 修复复利重置日处理中利息重复注入历史本金的错误
- 修复部分平仓后日终待实现利息计算中本金比例应用问题
- 修复全平且实际金额覆盖应结利息后待实现利息清零逻辑
- 新增复合复利交换服务用于单元测试验证
- 添加多个测试用例覆盖复利计算边界场景
This commit is contained in:
张名锐
2026-08-08 15:51:29 +08:00
parent c379e31b4d
commit 6fdc7d80b2
3 changed files with 538 additions and 21 deletions
@@ -48,6 +48,8 @@ namespace YLErp.Modules.SwapModule
/// </summary>
public List<swap_flow_event> AutoInterests { get; set; }
public SwapDealService DealService { get; set; }
public eod_swap_position LastInterestCalculationEodPosition { get; private set; }
public StubEodPositionService() : base(nameof(DealInterestsScenarioTest))
@@ -74,7 +76,7 @@ namespace YLErp.Modules.SwapModule
return AutoInterests;
}
return new SwapDealService(this).GetInterests(td, tradeExtend, valueDate, unwindDate,
return (DealService ?? new SwapDealService(this)).GetInterests(td, tradeExtend, valueDate, unwindDate,
eodPositions, positions, posiNotionalValue, posiLongNotionalValue, posiShortNotionalValue,
closePosiNotionalValue, closePrecent, eventType, tdClose, needPrice,
grossPrice, orginPv, add, settment, newCalcLast, closeList);
@@ -126,6 +128,27 @@ namespace YLErp.Modules.SwapModule
}
}
private sealed class StubCompoundSwapDealService : SwapDealService
{
private readonly IReadOnlyDictionary<DateTime, double> _floatRates;
public StubCompoundSwapDealService(IReadOnlyDictionary<DateTime, double> floatRates = null)
: base(new OptUserInfo(0, nameof(StubCompoundSwapDealService), OptUserFrom.UnitTest))
{
_floatRates = floatRates ?? new Dictionary<DateTime, double>();
}
protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate)
{
return _floatRates.TryGetValue(valueDate.Date, out rate);
}
public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate)
{
return 0m;
}
}
#endregion
#region
@@ -1085,6 +1108,437 @@ namespace YLErp.Modules.SwapModule
MidpointRounding.AwayFromZero), "Final close cash interest must be 0.82");
}
[TestMethod]
public void DI_MANUAL_CLOSE_007_CompoundFinalCloseSettlesFirstPartialCloseRoundingTail()
{
const decimal originalNotional = 10012.35m;
const decimal remainingNotional = originalNotional / 2m;
const decimal rate = 0.0299m;
var firstCloseDate = StartDate.AddDays(6);
var finalCloseDate = firstCloseDate.AddDays(6);
var td = CreateTrade();
td.trade_extend.ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays,
InterestCalcMode = "11",
SettlementRules = 0
});
var position = CreateInterestPosition();
position.InterestType = (int)InterestTypeEnum.;
position.InterestRateDefault = rate;
position.InterestPrincipalFix = originalNotional;
position.interest_rest_days = 1;
position.InterestSwapInterval = null;
var dealService = new StubCompoundSwapDealService();
var eodService = new StubEodPositionService { DealService = dealService };
var firstCloseInterest = dealService.GetInterests(
td, td.trade_extend, firstCloseDate, firstCloseDate,
new List<eod_swap_position>(), new List<swap_position> { position },
originalNotional, originalNotional, 0m, remainingNotional, 0.5m,
(int)SwapEventTypeEnum., false, false, 1m, originalNotional,
settment: false).Single();
var firstCloseCash = Math.Round(firstCloseInterest.InterestAmount, ConsGlobal.MoneyRound,
MidpointRounding.AwayFromZero);
var firstCloseRoundingTail = firstCloseInterest.InterestAmount - firstCloseCash;
Assert.AreNotEqual(0m, firstCloseRoundingTail,
$"首次部分平仓高精度利息 {firstCloseInterest.InterestAmount:F12} 按两位实际结算 {firstCloseCash:F2},尾差 {firstCloseRoundingTail:F12}");
var firstCloseFlow = CreateSwapFlowEvent(firstCloseDate, firstCloseCash);
firstCloseFlow.EventType = (int)SwapFlowEventTypeEnum.;
firstCloseFlow.InterestPrincipal = remainingNotional;
var firstCloseEod = eodService.ExecuteSaveAutoEodWithCloseInterestPosition(
null, position, td, firstCloseDate, null, remainingNotional, 0m,
new List<swap_flow_event> { firstCloseFlow }, remainingNotional, false);
var replayAtPreviousEod = dealService.GetInterests(
td, td.trade_extend, firstCloseDate, firstCloseDate,
new List<eod_swap_position>(), new List<swap_position> { position },
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
(int)SwapEventTypeEnum., false, false, 1m, originalNotional,
settment: false).Single();
var replayAtFinalClose = dealService.GetInterests(
td, td.trade_extend, finalCloseDate, finalCloseDate,
new List<eod_swap_position>(), new List<swap_position> { position },
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
(int)SwapEventTypeEnum., false, false, 1m, originalNotional,
settment: false).Single();
var expectedFinalInterest = firstCloseEod.InterestIncomeSum
+ replayAtFinalClose.InterestAmount - replayAtPreviousEod.InterestAmount;
var expectedFinalCash = Math.Round(expectedFinalInterest, ConsGlobal.MoneyRound,
MidpointRounding.AwayFromZero);
var expectedTotalCash = Math.Round(firstCloseCash + firstCloseEod.InterestIncomeSum
+ replayAtFinalClose.InterestAmount - replayAtPreviousEod.InterestAmount,
ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
var finalCloseInterest = dealService.GetInterests(
td, td.trade_extend, finalCloseDate, finalCloseDate,
new List<eod_swap_position> { firstCloseEod }, new List<swap_position> { position },
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
(int)SwapEventTypeEnum., false, false, 1m, originalNotional,
settment: false).Single();
var finalCloseCash = Math.Round(finalCloseInterest.InterestAmount, ConsGlobal.MoneyRound,
MidpointRounding.AwayFromZero);
var finalCloseFlow = CreateSwapFlowEvent(finalCloseDate, finalCloseCash);
finalCloseFlow.EventType = (int)SwapFlowEventTypeEnum.;
finalCloseFlow.InterestPrincipal = remainingNotional;
var finalCloseEod = eodService.ExecuteSaveAutoEodWithCloseInterestPosition(
firstCloseEod, position, td, finalCloseDate, null, 0m, 0m,
new List<swap_flow_event> { finalCloseFlow }, remainingNotional, false);
AssertDecimal(expectedFinalCash, finalCloseCash,
"最终全平现金必须带走上一日日终的待实现利息尾差");
AssertDecimal(expectedTotalCash,
firstCloseCash + finalCloseCash,
"两次实际结算现金必须守恒");
AssertDecimal(0m, finalCloseEod.InterestIncomeSum, "全平且实际金额覆盖应结利息后待实现应清零");
AssertDecimal(firstCloseCash + finalCloseCash, finalCloseEod.RealizedInterest,
"累计已实现利息必须等于历次实际结算金额之和");
var incompleteFinalCloseFlow = CreateSwapFlowEvent(finalCloseDate, finalCloseCash - 0.01m);
incompleteFinalCloseFlow.EventType = (int)SwapFlowEventTypeEnum.;
incompleteFinalCloseFlow.InterestPrincipal = remainingNotional;
var incompleteFinalCloseEod = eodService.ExecuteSaveAutoEodWithCloseInterestPosition(
firstCloseEod, position, td, finalCloseDate, null, 0m, 0m,
new List<swap_flow_event> { incompleteFinalCloseFlow }, remainingNotional, false);
AssertDecimal(expectedFinalInterest - incompleteFinalCloseFlow.InterestAmount,
incompleteFinalCloseEod.InterestIncomeSum,
"最终全平流水少结 0.01 时,日终必须保留未结利息而非清零");
}
[TestMethod]
public void DI_GLMS_20260421_0007_ContractNotionalCompoundPartialCloseScalesAndFinalCloseUsesRemainingPrincipal()
{
const decimal notional = 303139117.8m;
const decimal partialPercent = 0.3m;
const decimal partialNotional = notional * partialPercent;
const decimal remainingNotional = notional - partialNotional;
const decimal spread = 0.0025m;
var startDate = new DateTime(2026, 4, 21);
var maturityDate = new DateTime(2026, 5, 19);
var partialCloseDate = new DateTime(2026, 5, 11);
var td = new trade
{
id = 7007,
TradeNumber = "GLMS-20260421-0007",
ClientId = 999998,
TradeType = "收益互换",
TradeDate = startDate,
StartDate = startDate,
ExerciseDate = maturityDate,
TradeStatus = "确认成交",
ValidState = "Valid",
trade_extend = new trade_extend
{
TradeId = 7007,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays,
InterestCalcMode = "11",
SettlementRules = 0
})
}
};
var position = new swap_position
{
id = 70071,
SwapTradeId = td.id,
PositionType = (int)PositionTypeFlag.Unknown,
InterestDirection = (int)SwapDirectionEnum.,
InterestMode = (int)InterestModeEnum.,
InterestRateDefault = spread,
InterestPrincipalFix = notional,
PosiStartDate = startDate,
PosiMatuirityDate = maturityDate,
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 = maturityDate, Rate = spread, Settlement = 0 }
})
};
var service = new StubCompoundSwapDealService(new Dictionary<DateTime, double>
{
[new DateTime(2026, 4, 20)] = 0.0132,
[new DateTime(2026, 4, 21)] = 0.0132,
[new DateTime(2026, 4, 22)] = 0.0132,
[new DateTime(2026, 4, 23)] = 0.0132,
[new DateTime(2026, 4, 24)] = 0.0131,
[new DateTime(2026, 4, 27)] = 0.013502,
[new DateTime(2026, 4, 28)] = 0.0136,
[new DateTime(2026, 4, 29)] = 0.0138,
[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, 7)] = 0.0136,
[new DateTime(2026, 5, 8)] = 0.0135,
[new DateTime(2026, 5, 9)] = 0.0131,
[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, 14)] = 0.013,
[new DateTime(2026, 5, 15)] = 0.013,
[new DateTime(2026, 5, 18)] = 0.0132,
[new DateTime(2026, 5, 19)] = 0.0131
});
var previousEod = new eod_swap_position
{
id = 70072,
SwapTradeId = td.id,
PositionId = position.id,
ValueDate = new DateTime(2026, 5, 10),
TdInterestPrincipal = 303324019.3183441374m,
InterestIncomeSum = 266674.349853521170m,
InterestProfitSum = 266674.349853521170m,
FloatRate = 0.0139m
};
var partial = service.GetInterests(
td, td.trade_extend, partialCloseDate, partialCloseDate,
new List<eod_swap_position> { previousEod }, new List<swap_position> { position },
notional, notional, 0m, partialNotional, partialPercent,
(int)SwapEventTypeEnum., false, false, 0m, notional,
settment: false).Single();
AssertDecimal(84090.95m, Math.Round(partial.InterestAmount, ConsGlobal.MoneyRound,
MidpointRounding.AwayFromZero),
"GLMS-20260421-0007 30% 复利合约名义本金平仓必须按比例结算");
var final = service.GetInterests(
td, td.trade_extend, maturityDate, maturityDate,
new List<eod_swap_position>(), new List<swap_position> { position },
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
(int)SwapEventTypeEnum., false, false, 0m, remainingNotional,
settment: false, newCalcLast: true).Single();
AssertDecimal(268428.73m, Math.Round(final.InterestAmount, ConsGlobal.MoneyRound,
MidpointRounding.AwayFromZero),
"后续全平必须只结算剩余70%本金的复利,不重复结算原始全额");
Assert.AreNotEqual(280303.16m,
Math.Round(final.InterestAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero),
"后续全平不得再次使用原始全额本金");
}
[TestMethod]
public void DI_GLMS_20260421_0007_EodPartialCloseCarriesRemainingCompoundAccrual()
{
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 intermediateDate = new DateTime(2026, 5, 18);
var finalCloseDate = new DateTime(2026, 5, 19);
var td = new trade
{
id = 7007,
TradeNumber = "GLMS-20260421-0007-EOD",
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 = 7007,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays,
InterestCalcMode = "11",
SettlementRules = 0
})
}
};
var position = new swap_position
{
id = 70071,
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 = 70072,
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.3183441374m,
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 = 84090.95m,
InterestClosePnL = 84090.95m,
DataState = (int)SwapFlowDateStateEnum.
};
var partialEod = eodService.ExecuteSaveAutoEodWithCloseInterestPosition(
previousEod, position, td, partialCloseDate, null, remainingNotional, 0m,
new List<swap_flow_event> { partialCloseFlow }, partialNotional, false);
AssertDecimal(13628.805251563956m, partialEod.TdInterestIncome,
"5/11 EOD 当日新增复利必须按平仓前全额本金计提");
AssertDecimal(196212.205105085126m, partialEod.InterestIncomeSum,
"5/11 EOD 应保留部分平仓后的待实现复利");
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,
$"5/18 复利平仓应承接 5/11 日终剩余本金的累计利息 Expected approximately 259348.386714765, Actual: {intermediateInterest.InterestAmount}");
}
[TestMethod]
public void DI_GLMS_20260421_0006_FinalCloseCarriesPreviousEodInterest()
{
const decimal remainingNotional = 212197382.46m;
const decimal expectedInterest = -119386.71m;
var finalCloseDate = new DateTime(2026, 5, 19);
var td = new trade
{
id = 6006,
TradeNumber = "GLMS-20260421-0006",
ClientId = 999998,
TradeType = "收益互换",
TradeDate = new DateTime(2026, 4, 21),
StartDate = new DateTime(2026, 4, 22),
ExerciseDate = finalCloseDate,
TradeStatus = "确认成交",
ValidState = "Valid",
StructureType = "单标的",
QuoteCurrency = "CNY",
SettlementCurrency = "CNY",
trade_extend = new trade_extend
{
TradeId = 6006,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays,
InterestCalcMode = "10",
SettlementRules = 0
})
}
};
var position = new swap_position
{
id = 60061,
SwapTradeId = td.id,
PositionType = (int)PositionTypeFlag.Unknown,
InterestDirection = (int)SwapDirectionEnum.,
InterestMode = (int)InterestModeEnum.,
InterestRateDefault = -0.021m,
InterestPrincipalFix = remainingNotional,
PosiStartDate = td.StartDate.Value,
PosiMatuirityDate = td.ExerciseDate.Value,
IsInitial = true,
Invalid = false,
InterestType = (int)InterestTypeEnum.,
IsAnnualized = true,
interest_rest_days = 7,
interest_rule = 0,
FloatRateUnderlyingCode = "FR007",
InterestSwapInterval = null
};
var previousEod = new eod_swap_position
{
id = 60062,
SwapTradeId = td.id,
PositionId = position.id,
ValueDate = new DateTime(2026, 5, 18),
InterestDirection = position.InterestDirection,
InterestMode = position.InterestMode,
InterestType = position.InterestType,
InterestRateDefault = position.InterestRateDefault,
FloatRate = 0.0132m,
InterestIncomeSum = expectedInterest,
InterestProfitSum = expectedInterest,
TdInterestPrincipal = remainingNotional,
PosiNotionalValue = remainingNotional,
IsAnnualized = true,
interest_rest_days = 7,
interest_rule = 0
};
var dealService = new StubCompoundSwapDealService(new Dictionary<DateTime, double>
{
[new DateTime(2026, 4, 22)] = 0.0132,
[new DateTime(2026, 4, 29)] = 0.0138,
[new DateTime(2026, 5, 6)] = 0.0136,
[new DateTime(2026, 5, 13)] = 0.0129,
[new DateTime(2026, 5, 19)] = 0.0131
});
var result = dealService.GetInterests(
td, td.trade_extend, finalCloseDate, finalCloseDate,
new List<eod_swap_position> { previousEod }, new List<swap_position> { position },
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
(int)SwapEventTypeEnum., false, false, 0m, remainingNotional,
settment: false).Single();
AssertDecimal(expectedInterest, result.InterestAmount,
"GLMS-20260421-0006 最终全平应承接 5/18 日终待实现利息");
Assert.AreNotEqual(-123072.67m, result.InterestAmount,
"不得回归旧库错误的 -123072.67 最终利息");
}
/// <summary>
/// [DI_MATURITY_SETTLEMENT_001] 到期日存在手动互换但未带齐待实现时不能清零;
/// 当前事件按两位覆盖全部可结金额后,才可视为最终结算并清零。
+34 -2
View File
@@ -830,6 +830,12 @@ namespace YLErp.Modules.SwapModule
// 计算名义本金
var (closePrincipal, posiPrincipal, newClosePercent) = CalcNotionalByMode(position, closePrecent, posiNotionalValue, posiLongNotionalValue, posiShortNotionalValue);
if ((InterestModeEnum)position.InterestMode == InterestModeEnum.
|| (InterestModeEnum)position.InterestMode == InterestModeEnum.
&& posiNotionalValue == 0m)
{
closePrincipal = closePosiNotionalValue;
}
if ((InterestModeEnum)position.InterestMode == InterestModeEnum. || (InterestModeEnum)position.InterestMode == InterestModeEnum.)
{
positionClone.InterestDirection = position.InterestDirection == (int)SwapDirectionEnum. ? (int)SwapDirectionEnum. : (int)SwapDirectionEnum.;
@@ -910,6 +916,9 @@ namespace YLErp.Modules.SwapModule
closePrincipal = posiShort * closePercent;
posiPrincipal = posiShort;
break;
case InterestModeEnum.:
closePrincipal = posiNotional * closePercent;
break;
case InterestModeEnum.:
closePrincipal = posiNotional * closePercent;
break;
@@ -1228,6 +1237,24 @@ namespace YLErp.Modules.SwapModule
CalcDailyCompoundInterest(endDate, position, closePosiNotionalValue, interest, annualDays, needPrice,
floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount,
consumedInterest, resetCarryInterest);
if (preEodPosition.id != 0 && closePrecent == 1m)
{
// 最终全平只重放上一日终之后的新增利息;历史部分平仓的两位结算尾差已在日终待实现中。
var interestAtEnd = new swap_flow_event { InterestRate = rate };
decimal amountAtEnd = 0m;
decimal tdAmountAtEnd = 0m;
CalcDailyCompoundInterest(endDate, position, closePosiNotionalValue,
interestAtEnd, annualDays, needPrice, floateRate, closePrecent, orginPv,
calcFirst, calcLast, ref amountAtEnd, ref tdAmountAtEnd, consumedInterest);
var interestAtPreviousEod = new swap_flow_event { InterestRate = rate };
decimal amountAtPreviousEod = 0m;
decimal tdAmountAtPreviousEod = 0m;
CalcDailyCompoundInterest(preEodPosition.ValueDate, position, closePosiNotionalValue,
interestAtPreviousEod, annualDays, needPrice, floateRate, closePrecent, orginPv,
calcFirst, calcLast, ref amountAtPreviousEod, ref tdAmountAtPreviousEod, consumedInterest);
InterestAmount = preEodPosition.InterestIncomeSum + amountAtEnd - amountAtPreviousEod;
TdInterestAmount = preEodPosition.InterestIncomeSum + tdAmountAtEnd - tdAmountAtPreviousEod;
}
}
else
{
@@ -1291,8 +1318,13 @@ namespace YLErp.Modules.SwapModule
{
if (i % interestPeriod == 0)
{
// 复利时:利息并入本金(FR007 取价已提前到 calcFirst/calcLast 跳过之前完成)
var interestToReset = i == 0 || resetCarryInterest == 0m ? interest : resetCarryInterest;
// resetCarryInterest 是上一日终待实现按本次平仓比例分摊后的存量,
// 只能在 endDate 恰好是当前复利重置日时并入本金。历史重置点必须使用
// 重放到当时的 interest,否则会把上一日终存量反复注入历史本金,
// 例如 0007 的 5/11 部分平仓会由 84,090.95 被多算为 84,114.88。
var interestToReset = i > 0 && accrueDate == endDate && resetCarryInterest != 0m
? resetCarryInterest
: interest;
dynomicPrincipal = principal + interestToReset;
tdDynomicPrincipal = principal + interestToReset;
flowEvent.InterestPrincipal = tdDynomicPrincipal;
@@ -1295,10 +1295,14 @@ namespace YLErp.Modules.SwapModule
// 首次日终结算可能包含当日收盘,因此尚无先前的日终利息持仓。
// 部分平仓仍要续接上一日日终:CalcUnwindInterest 会将 InterestProfitSum
// 加入本次待实现,已实现字段也必须按日累计,不能从新建的临时对象重新开始。
var hasPreviousEod = eodPayPosition != null && eodPayPosition.id != 0;
var lastInterestIncomeSum = eodPayPosition?.InterestIncomeSum ?? 0m;
var lastInterestFeeSum = eodPayPosition?.InterestFeeSum ?? 0m;
var lastRealizedInterest = eodPayPosition?.RealizedInterest ?? 0m;
var lastRealizedInterestFee = eodPayPosition?.RealizedInterestFee ?? 0m;
// 先保留平仓前的复利本金;后面 interests.First().InterestPrincipal 是本次已平部分,
// 不能用它代表平仓前全额本金计算当日总利息。
var lastTdInterestPrincipal = eodPayPosition?.TdInterestPrincipal ?? 0m;
// 保留上一日日终标识和计息上下文,部分平仓只从 ValueDate 之后续算,不能重置到交易起始日。
eodPayPosition = eodPayPosition?.Clone() ?? new eod_swap_position();
eodPayPosition.ClientId = td.ClientId;
@@ -1401,28 +1405,55 @@ namespace YLErp.Modules.SwapModule
{
intersetAcmount /= tradeExtend.AnnualDays;
}
newEodPayPosition.TdInterestIncome = !autoSwap && calcLast
? TdInterestAmount - lastInterestIncomeSum
: intersetAcmount;
newEodPayPosition.TdInterestIncome = autoSwap
? intersetAcmount
: !hasPreviousEod
? interestAmountBeforeSettlement
: posiNotionalValue == 0m
? interestAmountBeforeSettlement - lastInterestIncomeSum
: lastRealizedInterest != 0m || lastRealizedInterestFee != 0m || !calcLast
? intersetAcmount
: TdInterestAmount - lastInterestIncomeSum;
if (!autoSwap
&& calcLast
&& closePercent > 0m && closePercent < 1m
&& posiNotionalValue > 0m
&& position.InterestType == (int)InterestTypeEnum.
&& position.InterestMode == (int)InterestModeEnum.)
{
// 算尾的复利部分平仓:平仓金额只结算“上日待实现 * 平仓比例
// + 已平本金当日利息”,但日终待实现必须按“上日待实现
// + 平仓前全额本金当日利息 - 实际平仓结算”递推。
// 通用路径的 intersetAcmount 此时基于已平本金:0007 只得到 30% 的
// 4,088.64,会漏记剩余 70% 的 9,540.16;因此改用上日终全额复利本金,
// 得到当日总利息 13,628.81,剩余部分才能继续参与后续复利。
var fullPrincipal = lastTdInterestPrincipal > 0m
? lastTdInterestPrincipal
: oriPosiNotionalValue;
newEodPayPosition.TdInterestIncome = fullPrincipal
* (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate);
if (position.IsAnnualized)
{
newEodPayPosition.TdInterestIncome /= tradeExtend.AnnualDays;
}
}
Log.Info($"InterestIncomeSum is {lastInterestIncomeSum},TdInterestIncome is {newEodPayPosition.TdInterestIncome}" +
$",TdCloseInterest is {newEodPayPosition.TdCloseInterest}");
Log.Info($"InterestFeeSum is {eodPayPosition.InterestFeeSum},TdInterestFee is {newEodPayPosition.TdInterestFee}" +
$",TdCloseInterestFee is {newEodPayPosition.TdCloseInterestFee}");
if (closePercent == 1)
{
// 全量平仓后不应把待实现利息或费用带入下一交易日。
newEodPayPosition.InterestIncomeSum = 0;
newEodPayPosition.InterestFeeSum = 0;
}
else
{
var pendingInterestBeforeSettlement = autoSwap
? interestAmountBeforeSettlement
: lastInterestIncomeSum + newEodPayPosition.TdInterestIncome;
newEodPayPosition.InterestIncomeSum = RoundEodInterest(
pendingInterestBeforeSettlement - newEodPayPosition.TdCloseInterest);
newEodPayPosition.InterestFeeSum = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee;
}
var pendingInterestBeforeSettlement = autoSwap
? interestAmountBeforeSettlement
: lastInterestIncomeSum + newEodPayPosition.TdInterestIncome;
var pendingInterestFeeBeforeSettlement = eodPayPosition.InterestFeeSum
+ newEodPayPosition.TdInterestFee;
newEodPayPosition.InterestIncomeSum = closePercent == 1
&& RoundMoney(pendingInterestBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterest)
? 0m
: RoundEodInterest(pendingInterestBeforeSettlement - newEodPayPosition.TdCloseInterest);
newEodPayPosition.InterestFeeSum = closePercent == 1
&& RoundMoney(pendingInterestFeeBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterestFee)
? 0m
: RoundEodInterest(pendingInterestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee);
//持仓内容-利息腿-损益统计(本方视角)
newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum;
//持仓价值