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

138 lines
7.6 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;
namespace YLErp.Modules.SwapModule.Penalty;
/// <summary>
/// EQD-6977 平仓罚息计算器(纯函数)——返回罚息金额。
///
/// 口径(2026-08-20 裁定,评审 11.5):**精确续接**。唯一近似 = 未来 FR007 不可得——
/// 剩余窗口一律用「前一晚收盘在役利率」(preEod.FloatRate,由 PenaltyLegRateResolver 解析);
/// 其余与正常到期计息**丝毫不能差**:每 7 天重置节奏照旧、并本金照旧、单/复利走同一套
/// Accrual 纯函数。金标准恒等式(验收基准):
///
/// 全期利息 = 平仓日已结利息(正常平仓流) + 罚息金额(本方法)
///
/// 边界规格(经金标准恒等式测试钉死):
/// - IncludeStart = !unwindDaySettled:正常结算已计平仓日(算尾)→ 罚息自次日起;不算尾 → 含平仓日;
/// - IncludeEnd = maturityCalcLast:到期日沿用交易自身算尾约定(非本次平仓的 newCalcLast)。
///
/// 复利承接(平仓日落在重置段中间时与全期轨迹逐日对齐的两个量,**必须来自实际计息状态**,
/// 由调用方 PenaltyInterestFeeMerger 从 preEod 快照与正常平仓流实结金额推导——严禁冻结利率重放推导,
/// FR007 有真实利率历史时重放值必偏):
/// - capitalizedInterest = 已并入最近重置日的累计利息 → 窗口首日起即加入计息基数;
/// - carryInInterest = 最近重置日之后已计至平仓日的利息 → 首个窗口重置日并入并持续留在基数。
/// 二者之和 = 被平部分的平仓日实结利息(正常平仓流 InterestAmount)。
///
/// 产物为金额,由接缝并入既有利息事件的 InterestFee(其他费用含罚息);不产生独立罚息事件。
/// 仅融资腿;保证金腿(MarginModes)与浮动端 P&amp;L 不进入本模块。
/// </summary>
public static class SwapPenaltyInterestCalculator
{
/// <summary>
/// 计算罚息窗口 [unwindDate, maturityDate] 的罚息金额。
/// </summary>
/// <param name="position">被平的融资腿</param>
/// <param name="closePrincipal">被平部分计息本金(部分平仓仅算被平份额)</param>
/// <param name="unwindDate">提前终止日(窗口起点)</param>
/// <param name="maturityDate">合约原始到期日(窗口终点,= td.ExerciseDate</param>
/// <param name="unwindDaySettled">正常平仓利息是否已计平仓日(effectiveCalcLast = calcLast || newCalcLast</param>
/// <param name="maturityCalcLast">交易到期日算尾约定(tradeExtend.CalcLast</param>
/// <param name="capitalizedInterest">复利承接①:已并入最近重置日的累计利息(被平份额),窗口首日起即入基数;单利传 0</param>
/// <param name="carryInInterest">复利承接②:最近重置日后已计至平仓日的利息(被平份额),首个窗口重置日并入;单利传 0</param>
/// <param name="frozenRate">冻结利率(PenaltyLegRateResolver.ResolveFrozenRate 产物 = 前一晚收盘在役利率)</param>
/// <param name="policy">计息政策(单复利/重置周期/年化天数;Convention 由本方法覆盖)</param>
/// <param name="resetAnchor">重置日锚点 = position.PosiStartDate(与正常计息重放网格一致,勿用 td.StartDate</param>
/// <param name="trace">计息轨迹(可选,SwapCalcTrace 落盘)</param>
public static decimal CalcPenaltyAmount(
swap_position position,
decimal closePrincipal,
DateTime unwindDate,
DateTime maturityDate,
bool unwindDaySettled,
bool maturityCalcLast,
decimal capitalizedInterest,
decimal carryInInterest,
FundingLegRate frozenRate,
AccrualPolicy policy,
DateTime resetAnchor,
AccrualTrace? trace = null)
{
var boundary = AccrualBoundary.Of(includeStart: !unwindDaySettled, includeEnd: maturityCalcLast);
var allInRate = frozenRate.AllInRate;
return policy.IsCompound
? AccrueCompound(closePrincipal, capitalizedInterest, carryInInterest, unwindDate, maturityDate, boundary, policy, resetAnchor, allInRate, trace)
: AccrueSimple(closePrincipal, unwindDate, maturityDate, boundary, policy, allInRate, trace);
}
/// <summary>
/// 复利罚息计息:即使冻结利率为单值,也必须按重置日分段(并本金发生在分段边界),每段同一冻结利率。
/// notional = 本金 + 已并入最近重置日的利息(capitalizedInterest):全期轨迹中当前重置段的滚动基数,
/// 段内每一天都在其上计息——平仓日落在段中间时与全期逐日对齐的关键。carryInInterest 在首个窗口重置日并入。
/// </summary>
private static decimal AccrueCompound(
decimal closePrincipal, decimal capitalizedInterest, decimal carryInInterest,
DateTime unwindDate, DateTime maturityDate, AccrualBoundary boundary, AccrualPolicy policy,
DateTime resetAnchor, decimal allInRate, AccrualTrace? trace)
{
var segments = BuildFrozenSegments(unwindDate, maturityDate, policy.ResetPeriodDays, resetAnchor, allInRate);
var r = CompoundInterestAccrual.AccruePeriod(
notional: closePrincipal + capitalizedInterest,
segmentRates: segments,
startDate: unwindDate,
endDate: maturityDate,
boundary: boundary,
annualDays: policy.AnnualDays,
isAnnualized: policy.IsAnnualized,
resetCarryInterest: 0m,
realizedInterest: 0m,
unwindFraction: 1m,
finalBasis: out _,
trace: trace,
carryInInterest: carryInInterest);
return r.Accrued;
}
/// <summary>
/// 单利罚息计息:无并本金语义,冻结利率即单段全程(需求 2.2.1 公式:利率 × 名义本金 × 剩余天数 / 计息基准)。
/// </summary>
private static decimal AccrueSimple(
decimal closePrincipal, DateTime unwindDate, DateTime maturityDate,
AccrualBoundary boundary, AccrualPolicy policy, decimal allInRate, AccrualTrace? trace)
{
var r = SimpleInterestAccrual.AccruePeriod(
priorAccrued: 0m,
notional: closePrincipal,
unwindFraction: 1m,
segmentRates: new List<(DateTime StartDate, decimal Rate)> { (unwindDate, allInRate) },
startDate: unwindDate,
endDate: maturityDate,
priorValueDate: unwindDate.AddDays(-1),
boundary: boundary,
annualDays: policy.AnnualDays,
isAnnualized: policy.IsAnnualized,
trace: trace);
return r.Accrued;
}
/// <summary>
/// 复利冻结分段:段边界 = 窗口内重置日((d - anchor) % period == 0,对齐 IsResetDay 公式),
/// 每段填同一冻结利率。首段必为 (unwindDate, rate)[start,end] 含端点的重置日也生成段
/// (末段起点==到期日时由 AccruePeriod 的边界决定是否计息)。
/// </summary>
private static List<(DateTime StartDate, decimal Rate)> BuildFrozenSegments(
DateTime start, DateTime end, int periodDays, DateTime anchor, decimal rate)
{
if (periodDays <= 1)
return new List<(DateTime, decimal)> { (start, rate) };
var segments = new List<(DateTime StartDate, decimal Rate)> { (start, rate) };
for (var d = start.AddDays(1); d <= end; d = d.AddDays(1))
{
if (SwapDealService.IsResetDay(d, anchor, periodDays))
segments.Add((d, rate));
}
return segments;
}
}