Files
zszq-trs/YLErpDAL/Modules/SwapModule/Penalty/PenaltyInterestFeeMerger.cs
T

136 lines
7.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using YLErp.Modules.SwapModule.Accrual;
using YLErp.Modules.SwapModule.FundingLegs;
using YLErp.Modules.SwapModule.ReturnLegs;
namespace YLErp.Modules.SwapModule.Penalty;
/// <summary>
/// EQD-6977 罚息接缝(纯函数,无 DB 依赖):把罚息金额**并入既有平仓利息流的 InterestFee(其他费用含罚息)**。
/// 不产生独立罚息事件——前端其他费用列/盈亏公式(含 InterestFee)与日终 TdCloseInterestFee 链路天然承接。
///
/// 设计:上帝类(SwapDealService.GetIntradayUnwindInterests)仅注入三个外部依赖委托——
/// getSpread(加点利差)/ getPreEod(上一日终快照行)/ tryGetFixing(定盘取价),本类零 DB 耦合、可 headless 单测。
///
/// 复利承接量(精确续接口径的关键)**必须取实际计息状态**,严禁冻结利率重放推导:
/// 承接① capitalized = max(0, preEod.TdInterestPrincipal×份额 closePrincipal) —— 实际滚动复利基数中已并入部分;
/// 承接② carryIn = 正常平仓流实结 InterestAmount ① —— 最近重置日后实际已计利息;
/// 无 preEod(首日平仓):①=0、②=实结金额。
/// 逐腿全程 trace 落盘(SwapCalcTrace),供计算过程分析与错误定位。
/// </summary>
public static class PenaltyInterestFeeMerger
{
/// <summary>
/// 对每条融资腿:解析冻结利率 → 以实际计息状态推导承接量 → 计算罚息 → 并入该腿正常平仓利息事件的 InterestFee。
/// 取不到冻结利率(浮动腿缺价且无 preEod)时跳过该腿(不阻断正常平仓),留 trace。
/// </summary>
public static void Merge(
trade td,
List<swap_position> fundingPositions,
List<swap_flow_event> interests,
DateTime unwindDate,
int annualDays,
bool unwindDaySettled,
bool maturityCalcLast,
decimal posiNotionalValue,
decimal closePosiNotionalValue,
decimal closePercent,
Func<swap_position, decimal> getSpread,
Func<swap_position, eod_swap_position?> getPreEod,
Func<DateTime, string, decimal?> tryGetFixing,
AccrualTrace? trace = null)
{
if (td.ExerciseDate == null)
{
trace?.Note("PENALTY|跳过 交易无到期日(ExerciseDate=null)");
return;
}
var maturityDate = td.ExerciseDate.Value;
foreach (var position in fundingPositions)
{
// 正常平仓利息流(GetInterests 刚产出)——承接②的事实源与罚息并入目标
var normalEvent = interests.FirstOrDefault(x => x.PositionId == position.id);
if (normalEvent == null)
{
trace?.Note($"PENALTY|p{position.id} 跳过 无正常平仓利息流(意外:融资腿应有对应事件)");
continue;
}
// 复用 GetInterests 的本金口径(mode2 无条件覆盖 / mode9 全平兜底,见其根因位置注释)
var mode = (InterestModeEnum)position.InterestMode;
var r = FundingLegStrategyFactory.Get(mode)
.CalcNotional(position.InterestPrincipalFix, posiNotionalValue, closePercent);
decimal closePrincipal = r.ClosePrincipal;
if (mode == InterestModeEnum.合约名义本金规模
|| (mode == InterestModeEnum.标的期初全价 && posiNotionalValue == 0m))
{
closePrincipal = closePosiNotionalValue;
}
var share = r.PosiPrincipal > 0m ? Math.Min(1m, closePrincipal / r.PosiPrincipal) : 1m;
var isCompound = position.InterestType == (int)InterestTypeEnum.复利;
// 冻结利率:前一晚收盘在役利率优先(preEod.FloatRate),无快照再按取价日=unwindDate-1 所在区间取定盘
FundingLegRate frozenRate;
string rateSource;
var preEod = getPreEod(position);
try
{
frozenRate = PenaltyLegRateResolver.ResolveFrozenRate(
position, getSpread(position), preEod?.FloatRate, unwindDate,
d => tryGetFixing(d, position.FloatRateUnderlyingCode));
rateSource = preEod != null
? $"preEod.FloatRate@{preEod.ValueDate:yyyy-MM-dd}"
: "定盘取价(unwindDate-1区间)";
}
catch (Exception ex)
{
trace?.Note($"PENALTY|p{position.id} 跳过 冻结利率解析失败:{ex.Message}");
continue;
}
// 复利承接:实际滚动基数中已并入部分(①)+ 段内实际已计利息(②)。单利无并本金语义恒 0。
// ① 的取值依赖平仓日是否为重置日(数据契约):
// 段中平仓:昨日快照 TdInterestPrincipal 即当前段滚动基数(=本金+①),直接作差;
// 重置日当天平仓:快照基数仍是【上一段】的(今日并入尚未发生),须改取
// preEod.InterestIncomeSum(昨日全部待实现利息 = 今日并入新段基数的那部分)。
decimal capitalized = 0m, carryIn = 0m;
if (isCompound)
{
var periodDays = position.interest_rest_days ?? 1;
var unwindOnResetDay = SwapDealService.IsResetDay(unwindDate, position.PosiStartDate, periodDays);
capitalized = unwindOnResetDay
? (preEod?.InterestIncomeSum ?? 0m) * share
: Math.Max(0m, (preEod?.TdInterestPrincipal ?? 0m) * share - closePrincipal);
// ① 不得超过实结金额(数据异常时钳制并留痕,避免负②进入计息)
if (capitalized > Math.Max(0m, normalEvent.InterestAmount))
{
trace?.Note($"PENALTY|p{position.id} 注意 承接①钳制:推导 {capitalized:F4} > 实结 {normalEvent.InterestAmount:F4}(快照/事件数据异常,请核对 preEod.TdInterestPrincipal/InterestIncomeSum");
capitalized = Math.Max(0m, normalEvent.InterestAmount);
}
carryIn = normalEvent.InterestAmount - capitalized;
}
var policy = AccrualPolicy.BuildEod(position, annualDays, isCompound);
// 锚点 = PosiStartDate:与正常计息重放(CalcDailyCompoundInterest 的分段网格)一致,延期腿勿用 td.StartDate
var penalty = SwapPenaltyInterestCalculator.CalcPenaltyAmount(
position, closePrincipal, unwindDate, maturityDate,
unwindDaySettled, maturityCalcLast,
capitalized, carryIn,
frozenRate, policy, position.PosiStartDate, trace);
penalty = InterestMath.Round(penalty, InterestMath.FundingLegPrecision);
var feeBefore = normalEvent.InterestFee;
normalEvent.InterestFee += penalty;
normalEvent.InterestClosePnL += penalty * DirectionRatio.ReceivePay(position.InterestDirection);
trace?.Note(
$"PENALTY|p{position.id} 完成 mode={mode} {(isCompound ? "复利" : "单利")} " +
$"窗口=[{unwindDate:yyyy-MM-dd}→{maturityDate:yyyy-MM-dd}] 平仓日已结={unwindDaySettled} 到期算尾={maturityCalcLast} | " +
$"本金 close={closePrincipal:F2} posi={r.PosiPrincipal:F2} share={share:P4} | " +
$"冻结利率={frozenRate.AllInRate:P6} 来源={rateSource} | " +
$"承接①={capitalized:F4} ②={carryIn:F4} 实结={normalEvent.InterestAmount:F4} | " +
$"罚息={penalty:F2} → InterestFee {feeBefore:F2}→{normalEvent.InterestFee:F2} PnL含罚息={normalEvent.InterestClosePnL:F2}");
}
}
}