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
+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;
//持仓价值