138 lines
7.6 KiB
C#
138 lines
7.6 KiB
C#
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&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;
|
||
}
|
||
}
|