feat(swap): 添加收益互换利息计算功能及Excel场景测试
- 新增ExecuteSaveEodInterestPositionCopy方法用于保存日终利息持仓副本 - 添加ExcelScenario4Case类和相关测试数据用于验证利息计算准确性 - 实现DI_EXCEL_SCENARIO4_PartialCloseAndFinalCloseMatchBlAndBn测试方法 - 优化SwapDealService中的利息计算逻辑和平仓处理 - 添加详细的注释说明利息计算的核心业务规则 - 完善SwapEodPositionService中的日终持仓处理逻辑 - 修复部分平仓和最终平仓时的利息金额计算问题
This commit is contained in:
@@ -636,11 +636,14 @@ namespace YLErp.Modules.SwapModule
|
||||
List<eod_swap_position> lastEodPositions = new SwapEodPositionService(this).GetPreEodPositions(tradeId, _preSetteDate);//上一交易数据
|
||||
var posiLongNotionalValue = longPositions.Sum(s => s.PosiNotionalValue);// 剩余名义本金
|
||||
var posiShortNotionalValue = shortPositions.Sum(s => s.PosiNotionalValue);// 剩余名义本金
|
||||
var stockEqvNotional = realPostitions.Where(x => x.PosiDirection > 0).Sum(s => s.PosiNotionalValue);
|
||||
var posiNotionalValue = stockEqvNotional * closePercent;//剩余名义本金
|
||||
var orginPv = ResolveUnwindPreviousNotional(lastEod, lastEodPositions, stockEqvNotional);
|
||||
var stockEqvNotional = realPostitions.Where(x => x.PosiDirection > 0).Sum(s => s.PosiNotionalValue); // 当前平仓前的实时剩余本金
|
||||
var posiNotionalValue = stockEqvNotional * closePercent;// 本次平仓名义本金
|
||||
var orginPv = ResolveUnwindPreviousNotional(lastEod, lastEodPositions, stockEqvNotional); // 上一日终的浮动端本金
|
||||
var grossPrice = realPostitions.Where(x => x.PosiDirection > 0).FirstOrDefault()?.PosiGrossPrice;
|
||||
var closeList = DbContext.swap_flow_event.Where(x => x.SwapTradeId == tradeId && x.UnwindDate == unwindDate && eventTypes.Contains(x.EventType) && x.DataState == (int)SwapFlowDateStateEnum.完成).ToList();
|
||||
var closeList = DbContext.swap_flow_event.Where(x => x.SwapTradeId == tradeId
|
||||
&& x.UnwindDate == unwindDate
|
||||
&& eventTypes.Contains(x.EventType)
|
||||
&& x.DataState == (int)SwapFlowDateStateEnum.完成).ToList();
|
||||
bool tdClose = closeList.Count > 0;
|
||||
interests = GetInterests(td, tradeExtend, valueDate, unwindDate, lastEodPositions, positions, stockEqvNotional, posiLongNotionalValue, posiShortNotionalValue, posiNotionalValue, closePercent, eventType, tdClose, false, grossPrice ?? 0, orginPv, true, false,false, closeList);
|
||||
return interests;
|
||||
@@ -826,6 +829,7 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
// 计算计息区间
|
||||
int interestPeriod = position.interest_rest_days ?? 1;
|
||||
// true 跳过 不计利息; false 正常利息
|
||||
bool swap = InitInterestDate(unwindDate, preDealDate, td, tdClose, out DateTime startDate, out DateTime endDate);
|
||||
|
||||
// 计算名义本金
|
||||
@@ -898,9 +902,9 @@ namespace YLErp.Modules.SwapModule
|
||||
/// </summary>
|
||||
private (decimal close, decimal posi, decimal closePct) CalcNotionalByMode(swap_position position, decimal closePercent, decimal posiNotional, decimal posiLong, decimal posiShort)
|
||||
{
|
||||
decimal closePrincipal = posiNotional;
|
||||
decimal posiPrincipal = posiNotional;
|
||||
decimal newClosePercent = closePercent;
|
||||
decimal closePrincipal = posiNotional; // 平仓部分的名义本金
|
||||
decimal posiPrincipal = posiNotional; // 持仓部分的名义本金
|
||||
decimal newClosePercent = closePercent; // 调整后的平仓比例
|
||||
|
||||
switch ((InterestModeEnum)position.InterestMode)
|
||||
{
|
||||
@@ -1201,10 +1205,10 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
if (swap)
|
||||
{
|
||||
interest.InterestAmount = 0; // 利息金额
|
||||
interest.TdInterestAmount = 0; // 当日新增利息
|
||||
interest.InterestAmount = 0;
|
||||
interest.TdInterestAmount = 0;
|
||||
interest.InterestAmount = 0;
|
||||
interest.InterestClosePnL = 0;
|
||||
interest.InterestClosePnL = 0; // 利息端平仓盈亏
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1218,21 +1222,28 @@ namespace YLErp.Modules.SwapModule
|
||||
var daysFromPreEod = preEodPosition.id != 0
|
||||
? (endDate - preEodPosition.ValueDate).Days
|
||||
: 0;
|
||||
// 不算尾 + 当日即新周期首日 + 未到重置日 ==> 说明这一天应归入下一个计息周期 当天无需单独计息
|
||||
if (!calcLast && daysFromPreEod == 1 && daysFromStart % (position.interest_rest_days ?? 1) != 0)
|
||||
{
|
||||
interest.InterestPrincipal = preEodPosition.TdInterestPrincipal * closePrecent;
|
||||
interest.InterestPrincipal = preEodPosition.TdInterestPrincipal * closePrecent; // 计息基数
|
||||
interest.FloatRate = preEodPosition.FloatRate;
|
||||
InterestAmount = preEodPosition.InterestIncomeSum * closePrecent;
|
||||
TdInterestAmount = preEodPosition.InterestIncomeSum;
|
||||
InterestAmount = preEodPosition.InterestIncomeSum * closePrecent; // 利息金额 = 待实现 * 平仓比例
|
||||
TdInterestAmount = preEodPosition.InterestIncomeSum; // 当日新增利息
|
||||
interest.InterestAmount = Math.Round(InterestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
|
||||
interest.TdInterestAmount = Math.Round(TdInterestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
|
||||
interest.InterestClosePnL = interest.InterestAmount * interestRatio;
|
||||
interest.InterestClosePnL = interest.InterestAmount * interestRatio; // 利息端平仓盈亏 = 利息金额 * 方向
|
||||
return interest;
|
||||
}
|
||||
// remainingPercent 只用于把上一日待实现分配给本次计算对应的本金。
|
||||
// 按照利息腿的实际 计息基数 重新计算一个历史待实现利息的 平仓比例。不替代全局的平仓比例
|
||||
// 部分平仓计算关闭 30% 时取 30%;最终全平剩余仓位时取 100%。
|
||||
var remainingPercent = preEodPosition.TdInterestPrincipal > 0m
|
||||
? closePosiNotionalValue / preEodPosition.TdInterestPrincipal
|
||||
: 1m;
|
||||
remainingPercent = Math.Max(0m, Math.Min(1m, remainingPercent));
|
||||
// resetCarryInterest 是重置日并入复利本金的历史待实现,不是当天新增利息。
|
||||
// 把上日尚未实现的的利息 按本次平掉的这部分计息基数分给本次平仓 并在重置日并入计息基数
|
||||
// 它只在当前 endDate 恰好为重置日时使用,避免把同一笔历史利息重复资本化。
|
||||
var resetCarryInterest = preEodPosition.InterestIncomeSum * remainingPercent;
|
||||
CalcDailyCompoundInterest(endDate, position, closePosiNotionalValue, interest, annualDays, needPrice,
|
||||
floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount,
|
||||
@@ -1240,12 +1251,17 @@ namespace YLErp.Modules.SwapModule
|
||||
if (preEodPosition.id != 0 && closePrecent == 1m)
|
||||
{
|
||||
// 最终全平只重放上一日终之后的新增利息;历史部分平仓的两位结算尾差已在日终待实现中。
|
||||
// InterestAmount 是本次最终应结金额;TdInterestAmount 是不按关闭比例缩放的参考累计值。
|
||||
// 二者在全平时都以上一日 InterestIncomeSum 为起点,保证之前攒下的尾差最后一次带走。
|
||||
var interestAtEnd = new swap_flow_event { InterestRate = rate };
|
||||
decimal amountAtEnd = 0m;
|
||||
decimal tdAmountAtEnd = 0m;
|
||||
// InitInterestDate 在最终日不算尾时会先把 endDate 回拨一天;
|
||||
// 历史差分的 amountAtEnd 需补回该日,但计算器仍使用交易 calcLast,
|
||||
// 并将重放日期限制在合约到期日,避免提前全平或超期重复计息。
|
||||
// 如果算尾 重放日 = 正常到期日
|
||||
// 不算尾 且未超过到期日 重放日 = endDate+1 (补齐不算尾那天漏计的利息)
|
||||
// 加1天超过到期日 截断到到期日
|
||||
var replayEndDate = endDate;
|
||||
if (!calcLast && endDate < valueDate)
|
||||
{
|
||||
@@ -1255,6 +1271,7 @@ namespace YLErp.Modules.SwapModule
|
||||
replayEndDate = td.ExerciseDate.Value;
|
||||
}
|
||||
}
|
||||
// 计算截至本次平仓日的累计利息 amountAtEnd
|
||||
CalcDailyCompoundInterest(replayEndDate, position, closePosiNotionalValue,
|
||||
interestAtEnd, annualDays, needPrice, floateRate, closePrecent, orginPv,
|
||||
calcFirst, calcLast, ref amountAtEnd, ref tdAmountAtEnd, consumedInterest);
|
||||
@@ -1263,9 +1280,14 @@ namespace YLErp.Modules.SwapModule
|
||||
decimal tdAmountAtPreviousEod = 0m;
|
||||
// 最终日重放仍遵守交易的 calcLast;上一日终是历史截点而非合约尾日,
|
||||
// 因此此处按闭区间包含上一日终当天,避免算头不算尾时重复加入该日利息。
|
||||
// 计算截至上一日终累积的利息 amountAtPreviousEod
|
||||
CalcDailyCompoundInterest(preEodPosition.ValueDate, position, closePosiNotionalValue,
|
||||
interestAtPreviousEod, annualDays, needPrice, floateRate, closePrecent, orginPv,
|
||||
calcFirst, true, ref amountAtPreviousEod, ref tdAmountAtPreviousEod, consumedInterest);
|
||||
// 例如 0004:5/18 待实现 -118631.261797,加 5/19 新增约 -4648.912760,
|
||||
// 得到最终应结 -123280.174557,按金额两位落为 Excel BN 的 -123280.17。
|
||||
// 上一日终已保存的待实现利息 + 截至平仓日累计利息 - 截至上一日终累计利息
|
||||
// 这样只带走“上一日终以后新增的利息”,同时保留历史部分平仓时因两位金额结算留下的尾差,最终全平一次性结清。
|
||||
InterestAmount = preEodPosition.InterestIncomeSum + amountAtEnd - amountAtPreviousEod;
|
||||
TdInterestAmount = preEodPosition.InterestIncomeSum + tdAmountAtEnd - tdAmountAtPreviousEod;
|
||||
}
|
||||
@@ -1332,6 +1354,7 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
if (i % interestPeriod == 0)
|
||||
{
|
||||
// 每个重置节点 计息基数 = 前日本金 + 本期利息
|
||||
// resetCarryInterest 是上一日终待实现按本次平仓比例分摊后的存量,
|
||||
// 只能在 endDate 恰好是当前复利重置日时并入本金。历史重置点必须使用
|
||||
// 重放到当时的 interest,否则会把上一日终存量反复注入历史本金,
|
||||
|
||||
@@ -1269,8 +1269,13 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动互换用,当日无互换,当日有平仓
|
||||
/// 将平仓/自动互换的盘中利息结果写成当日日终利息腿。
|
||||
/// 字段完整口径和逐日示例见《收益互换日终收盘总流程与当前代码审查》7.2、16.7、16.13 节。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 关键状态链:上日待实现 + 当日新增 - 当日结息 = 当日待实现;
|
||||
/// 上日累计已实现 + 当日结息(按收付方向)= 当日累计已实现。
|
||||
/// </remarks>
|
||||
/// <param name="eodPayPosition">上一日日终持仓</param>
|
||||
/// <param name="newEodPayPosition">当前收盘日日终持仓 不可能为空</param>
|
||||
/// <param name="position">利息腿信息</param>
|
||||
@@ -1285,8 +1290,11 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
Log.Info($"eodPayPosition is {JsonHelper.Serialize(eodPayPosition, false)},newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
|
||||
var tradeExtend = td.trade_extend.ExtendObj;
|
||||
// oriPosiNotionalValue 是平仓前规模,posiNotionalValue 是收盘后剩余规模,closeNational 是本次关闭规模。
|
||||
// 例如 30% 平仓:303139117.80 = 212197382.46 + 90941735.34。
|
||||
decimal oriPosiNotionalValue = posiLongNotional + posiShortNational + closeNational;
|
||||
decimal posiNotionalValue = posiLongNotional + posiShortNational;
|
||||
// ratio 只负责把腿内原始金额转换为本方盈亏方向,不参与计息金额本身的计算。
|
||||
decimal ratio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负
|
||||
if (marginTypes.Contains(position.InterestMode))
|
||||
{
|
||||
@@ -1296,6 +1304,8 @@ namespace YLErp.Modules.SwapModule
|
||||
// 部分平仓仍要续接上一日日终:CalcUnwindInterest 会将 InterestProfitSum
|
||||
// 加入本次待实现,已实现字段也必须按日累计,不能从新建的临时对象重新开始。
|
||||
var hasPreviousEod = eodPayPosition != null && eodPayPosition.id != 0;
|
||||
// InterestIncomeSum 是尚未结算的高精度利息;RealizedInterest 是生命周期累计已结利息。
|
||||
// 二者不能相互替代,也不能在部分平仓后重新从 0 开始。
|
||||
var lastInterestIncomeSum = eodPayPosition?.InterestIncomeSum ?? 0m;
|
||||
var lastInterestFeeSum = eodPayPosition?.InterestFeeSum ?? 0m;
|
||||
var lastRealizedInterest = eodPayPosition?.RealizedInterest ?? 0m;
|
||||
@@ -1332,6 +1342,7 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
orginPv = posiNotionalValue;
|
||||
}
|
||||
// closePercent 描述本次关闭占平仓前仓位的比例;上例为 90941735.34 / 303139117.80 = 30%。
|
||||
decimal closePercent = oriPosiNotionalValue == 0 ? 0 : closeNational / oriPosiNotionalValue;
|
||||
var eventType = autoSwap ? (int)SwapEventTypeEnum.自动互换 : (int)SwapEventTypeEnum.平仓;
|
||||
bool longShort = td.StructureType == ClientMarginTypeEnum.多空组合.ToString();
|
||||
@@ -1349,6 +1360,9 @@ namespace YLErp.Modules.SwapModule
|
||||
preEodPositions.Add(eodPayPosition);
|
||||
var calcLast = tradeExtend?.InterestCalcMode?.EndsWith("1") ?? true;
|
||||
var interests = CalcSwapInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiLongNotional, posiShortNational, closeNational, 1, eventType, false, true, grossPrice, orginPv, true, settment: false, newCalcLast: autoSwap || calcLast);
|
||||
// TdInterestAmount:计息器返回的全腿当日/累计参考值,用于拆出 EOD 的当日新增。
|
||||
// interestAmountBeforeSettlement:本次事件发生前理论应结的高精度利息。
|
||||
// manualSettledInterestAmount:swap_flow_event 实际落库的手工结息,金额已按分处理。
|
||||
decimal TdInterestAmount = interests.Sum(x => x.TdInterestAmount);
|
||||
decimal interestAmountBeforeSettlement = interests.Sum(x => x.InterestAmount);
|
||||
decimal manualSettledInterestAmount = flowEvents.Sum(x => x.InterestAmount);
|
||||
@@ -1382,6 +1396,8 @@ namespace YLErp.Modules.SwapModule
|
||||
newEodPayPosition.interest_rest_days = position.interest_rest_days;
|
||||
newEodPayPosition.interest_rule = position.interest_rule;
|
||||
//利息端估值用信息
|
||||
// TdInterestPrincipal 是“下一日继续计息的收盘后本金”,不是原始合同规模,也不是本次平仓本金。
|
||||
// 模式9单利直接取剩余名义本金;复利还要保留重置时已经并入本金的待实现利息。
|
||||
newEodPayPosition.TdInterestPrincipal = interestModes.Contains(position.InterestMode)
|
||||
? position.InterestPrincipalFix
|
||||
: position.InterestMode == (int)InterestModeEnum.标的期初全价
|
||||
@@ -1399,7 +1415,10 @@ namespace YLErp.Modules.SwapModule
|
||||
//当日已实现,平仓时已处理
|
||||
newEodPayPosition.TdInterestFee = flowEvents.Sum(s => s.InterestFee);
|
||||
newEodPayPosition.TdCloseInterestFee = newEodPayPosition.TdInterestFee;
|
||||
// TdCloseInterest 只表示当天真正结算出去的金额;部分平仓未结部分继续留在 InterestIncomeSum。
|
||||
newEodPayPosition.TdCloseInterest = manualSettledInterestAmount + autoSettledInterestAmount;
|
||||
// intersetAcmount 是收盘后本金的一天应计展示值。算尾部分平仓时,下面的复利分支会改用
|
||||
// 平仓前全额本金重算当天新增,但跨日携带的 TdInterestPrincipal 仍只能是剩余本金。
|
||||
var intersetAcmount = newEodPayPosition.TdInterestPrincipal * (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate);
|
||||
if (position.IsAnnualized)
|
||||
{
|
||||
@@ -1424,6 +1443,7 @@ namespace YLErp.Modules.SwapModule
|
||||
// 模式2(合约名义本金规模)和模式9(标的期初全价)都以名义本金
|
||||
// 作为复利基数;算尾用平仓前全额当日利息再扣实际结算,
|
||||
// 不算尾只计剩余本金,避免已平部分利息进入后续复利。
|
||||
// fullPrincipal 是平仓前动态复利本金,仅用于判断平仓日应按全额还是剩余额计息。
|
||||
var fullPrincipal = lastTdInterestPrincipal > 0m
|
||||
? lastTdInterestPrincipal
|
||||
: oriPosiNotionalValue;
|
||||
@@ -1438,7 +1458,9 @@ namespace YLErp.Modules.SwapModule
|
||||
if (calcLast
|
||||
&& position.InterestMode == (int)InterestModeEnum.合约名义本金规模)
|
||||
{
|
||||
// 模式2的 InterestPrincipal 是已平部分,不是跨日剩余本金。
|
||||
// 模式2的 InterestPrincipal 是已平部分,需反推平仓前全额后再取剩余;
|
||||
// 模式9已直接返回剩余动态本金,再反推会把 30% 平仓后的本金放大 7/3 倍。
|
||||
// 例如模式9的 212135529.97 已是剩余本金,错误反推会变成 494982903.27。
|
||||
newEodPayPosition.TdInterestPrincipal *= (1m - closePercent) / closePercent;
|
||||
}
|
||||
else if (usesFullPreviousEodPrincipal)
|
||||
@@ -1472,11 +1494,15 @@ namespace YLErp.Modules.SwapModule
|
||||
$",TdCloseInterest is {newEodPayPosition.TdCloseInterest}");
|
||||
Log.Info($"InterestFeeSum is {eodPayPosition.InterestFeeSum},TdInterestFee is {newEodPayPosition.TdInterestFee}" +
|
||||
$",TdCloseInterestFee is {newEodPayPosition.TdCloseInterestFee}");
|
||||
// pendingInterestBeforeSettlement 是“扣款前待实现”。普通平仓按上日待实现 + 当日新增;
|
||||
// 自动互换的 interestAmountBeforeSettlement 已经是完整理论应结,不能再加一次上日值。
|
||||
var pendingInterestBeforeSettlement = autoSwap
|
||||
? interestAmountBeforeSettlement
|
||||
: lastInterestIncomeSum + newEodPayPosition.TdInterestIncome;
|
||||
var pendingInterestFeeBeforeSettlement = eodPayPosition.InterestFeeSum
|
||||
+ newEodPayPosition.TdInterestFee;
|
||||
// InterestIncomeSum 是收盘后仍未结算的尾差/剩余利息。
|
||||
// 部分平仓:扣款前待实现 - TdCloseInterest;最终全平且两位金额已覆盖时直接清零。
|
||||
newEodPayPosition.InterestIncomeSum = closePercent == 1
|
||||
&& RoundMoney(pendingInterestBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterest)
|
||||
? 0m
|
||||
@@ -1486,6 +1512,7 @@ namespace YLErp.Modules.SwapModule
|
||||
? 0m
|
||||
: RoundEodInterest(pendingInterestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee);
|
||||
//持仓内容-利息腿-损益统计(本方视角)
|
||||
// InterestProfitSum 是利息腿待实现总额,包含利息和费用;无费用时等于 InterestIncomeSum。
|
||||
newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum;
|
||||
//持仓价值
|
||||
newEodPayPosition.SwapPositionValue = newEodPayPosition.InterestProfitSum * ratio + newEodPayPosition.PosiProfitSum;
|
||||
@@ -1495,6 +1522,8 @@ namespace YLErp.Modules.SwapModule
|
||||
Log.Info($"InterestFeeSum is {eodPayPosition.InterestFeeSum},TdInterestFee is {newEodPayPosition.TdInterestFee}" +
|
||||
$",TdCloseInterestFee is {newEodPayPosition.TdCloseInterestFee}");
|
||||
//累计已实现
|
||||
// RealizedInterest 只增不回滚:上日累计已实现 + 当日结息按方向后的金额。
|
||||
// 收取腿的 -37119.14 会把累计已实现更新为 -37119.14;后续普通 EOD 保持该值。
|
||||
newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio;
|
||||
newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee;
|
||||
SetFixedLegRealizedPnl(newEodPayPosition);
|
||||
|
||||
Reference in New Issue
Block a user