独立于 SwapDealService / SwapEodPositionService 的纯函数计息层:无副作用、 同 input 同 output,正交轴(算头算尾 / 单利复利 / unwind / 待实现收益)各自独立。 术语采用金融标准 Unwind / Realized / Unrealized。纯新增零接入核心计息逻辑, 待 Excel 对账确认"绿而不错"后 pilot 替换。
214 lines
10 KiB
C#
214 lines
10 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
|
||
namespace YLErp.Derivatives.Interest;
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 词汇表(本文件只允许出现下列用词,同一概念不得出现第二种叫法)
|
||
//
|
||
// 概念 唯一用词 与既有代码的对应
|
||
// ───────────────────────────────────────────────────────────────────
|
||
// 区间起点/终点 Start / End startDate / endDate
|
||
// 计息 Accrue CalcDailySimpleInterest / CalcDailyCompoundInterest
|
||
// 平仓 Unwind unwindPercent(既有字段 closePercent)
|
||
// 已实现利息 Realized realizedInterest(legacy 字段 consumedInterest)
|
||
// 待实现收益 Unrealized 预付金模式下的待实现收益余额
|
||
// 计息基数 principal principal / dynomicPrincipal
|
||
// 年化天数 annualDays tradeExtend.ExtendObj.AnnualDays
|
||
//
|
||
// 入参一律沿用既有代码的字段名,调用点两边读起来同名,不产生心智翻译成本。
|
||
// 出参改用自描述名(Accrued / AccruedToday),因为 "Td" 对新读者是黑话。
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 计息区间边界(算头 / 算尾)。
|
||
/// 用具名值取代两个相邻 bool,物理上杜绝 calcFirst / calcLast 传反这一类历史缺陷。
|
||
/// </summary>
|
||
public readonly struct AccrualBoundary
|
||
{
|
||
/// <summary>算头:含 startDate。</summary>
|
||
public bool IncludeStart { get; }
|
||
|
||
/// <summary>算尾:含 endDate。</summary>
|
||
public bool IncludeEnd { get; }
|
||
|
||
private AccrualBoundary(bool includeStart, bool includeEnd)
|
||
=> (IncludeStart, IncludeEnd) = (includeStart, includeEnd);
|
||
|
||
/// <summary>算头算尾 [start, end]。</summary>
|
||
public static readonly AccrualBoundary Both = new(true, true);
|
||
|
||
/// <summary>算头不算尾 [start, end)。</summary>
|
||
public static readonly AccrualBoundary StartOnly = new(true, false);
|
||
|
||
/// <summary>不算头算尾 (start, end]。</summary>
|
||
public static readonly AccrualBoundary EndOnly = new(false, true);
|
||
|
||
/// <summary>不算头不算尾 (start, end)。</summary>
|
||
public static readonly AccrualBoundary None = new(false, false);
|
||
|
||
/// <summary>由既有 calcFirst / calcLast 布尔对构造,供旧调用方渐进迁移。</summary>
|
||
public static AccrualBoundary Of(bool includeStart, bool includeEnd) => new(includeStart, includeEnd);
|
||
|
||
public override string ToString()
|
||
=> $"{(IncludeStart ? "算头" : "不算头")}{(IncludeEnd ? "算尾" : "不算尾")}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计息结果。Accrued → 记账字段 InterestAmount / InterestProfitSum;AccruedToday → TdInterestAmount。
|
||
/// </summary>
|
||
public readonly struct InterestResult
|
||
{
|
||
/// <summary>区间累计应计利息。</summary>
|
||
public decimal Accrued { get; }
|
||
|
||
/// <summary>末日(当日)应计利息。</summary>
|
||
public decimal AccruedToday { get; }
|
||
|
||
public InterestResult(decimal accrued, decimal accruedToday)
|
||
=> (Accrued, AccruedToday) = (accrued, accruedToday);
|
||
|
||
public static readonly InterestResult Zero = new(0m, 0m);
|
||
|
||
public override string ToString() => $"Accrued={Accrued}, AccruedToday={AccruedToday}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 收益互换(TRS)利息腿计算——纯函数。
|
||
///
|
||
/// 设计约束:
|
||
/// 1. 无副作用——不读写 flowEvent、不取利率、不连库、不碰任何共享可变状态;
|
||
/// 2. 同 input → 同 output,结果仅通过返回值流出;
|
||
/// 3. 正交轴(算头算尾 / 单利复利 / 平仓 / 待实现收益)各自独立,互不耦合;
|
||
/// 4. 调用方负责「取利率 + 构造日期区间 + 落库」,本类只算账。
|
||
/// 由此,corp action 调整价格 / 数量时只需把新的 principal 与 rate 喂入,计息逻辑一行不动。
|
||
///
|
||
/// 领域口径:本系统利息腿是单边融资腿,任一时点只有一个生效利率(见 SwapDealService 的
|
||
/// floateRate 单一入参),<b>不存在</b> IRS 那种 fixedRate − floatingRate 轧差;
|
||
/// 权益腿盈亏与平仓费用属三腿汇总层,不在本类职责内。
|
||
///
|
||
/// 为何不复用 Qdp 的 IDayCount:
|
||
/// a. 语义——Qdp 的 DaysInPeriod = end − start 是写死的半开区间,只能表达四种算头算尾中的一种;
|
||
/// b. 精度——Qdp 返回 double 年化系数,本系统 decimal 且日息先 Round 再乘天数,
|
||
/// Round(P*r/365, 11) * n ≠ P*r*(n/365),与 Excel 对账口径不同;
|
||
/// c. 依赖方向——Qdp 用自有 Date 类型,引入会让 YLErp.Core 反向依赖定价库。
|
||
/// </summary>
|
||
public static class SwapInterest
|
||
{
|
||
/// <summary>系统统一价格精度位数。</summary>
|
||
public const int Precision = 11;
|
||
|
||
/// <summary>年化天数常量(合约字段存的是 int,故不用 enum)。</summary>
|
||
public const int Act365 = 365;
|
||
|
||
public const int Act360 = 360;
|
||
|
||
/// <summary>应计天数。边界规则由日期区间表达,计息函数内不再出现 flag 分支。</summary>
|
||
public static int AccrualDays(DateTime startDate, DateTime endDate, AccrualBoundary boundary)
|
||
{
|
||
var s = boundary.IncludeStart ? startDate : startDate.AddDays(1);
|
||
var e = boundary.IncludeEnd ? endDate : endDate.AddDays(-1);
|
||
var days = (int)(e - s).TotalDays + 1; // 含两端
|
||
return days < 0 ? 0 : days;
|
||
}
|
||
|
||
/// <summary>单利:计息基数固定,每日利息相同,无逐日循环。</summary>
|
||
public static InterestResult AccrueSimple(
|
||
decimal principal,
|
||
decimal rate,
|
||
DateTime startDate,
|
||
DateTime endDate,
|
||
AccrualBoundary boundary,
|
||
int annualDays,
|
||
int precision = Precision)
|
||
{
|
||
var days = AccrualDays(startDate, endDate, boundary);
|
||
var daily = Round(principal * rate / annualDays, precision);
|
||
return new InterestResult(Round(daily * days, precision), daily);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 复利:按重置日切段,段间把累计利息并入计息基数。
|
||
/// 段内复用 AccrueSimple(仍无逐日循环);重置日是唯一并本金的地方。
|
||
/// </summary>
|
||
public static InterestResult AccrueCompound(
|
||
decimal principal,
|
||
decimal rate,
|
||
DateTime startDate,
|
||
DateTime endDate,
|
||
AccrualBoundary boundary,
|
||
IReadOnlyList<DateTime> resetDates,
|
||
int annualDays,
|
||
int precision = Precision)
|
||
{
|
||
var basis = principal;
|
||
decimal accrued = 0m, accruedToday = 0m;
|
||
|
||
var segEnds = (resetDates ?? Array.Empty<DateTime>())
|
||
.Where(d => d > startDate && d < endDate)
|
||
.OrderBy(d => d)
|
||
.Append(endDate)
|
||
.ToArray();
|
||
|
||
var segStart = startDate;
|
||
var segIncludeStart = boundary.IncludeStart;
|
||
|
||
foreach (var segEnd in segEnds)
|
||
{
|
||
var segBoundary = AccrualBoundary.Of(segIncludeStart, segEnd == endDate && boundary.IncludeEnd);
|
||
var seg = AccrueSimple(basis, rate, segStart, segEnd, segBoundary, annualDays, precision);
|
||
|
||
accrued += seg.Accrued;
|
||
accruedToday = seg.AccruedToday;
|
||
basis += seg.Accrued; // 仅在重置日并本金
|
||
segStart = segEnd;
|
||
segIncludeStart = false; // 后续段不算头
|
||
}
|
||
|
||
return new InterestResult(accrued, accruedToday);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 平仓(Unwind)缩放——全仓唯一缩放点,物理上杜绝 unwindPercent 被重复相乘。
|
||
/// 全平即 unwindPercent = 1,不另设方法。
|
||
///
|
||
/// 已实现 / 未实现边界:传入的 <paramref name="accrued"/> 是平仓前仍「未实现(unrealized)」的
|
||
/// 累计应计利息;本方法按比例缩放后返回「平仓后剩余未实现」部分,并扣除历史累计「已实现(realized)」
|
||
/// 的 <paramref name="realizedInterest"/>。被平仓比例 unwindPercent 对应的那一份 accrued,
|
||
/// 即在此刻「实现(realized)」,由调用方记入 realizedInterest。
|
||
/// </summary>
|
||
/// <param name="accrued">平仓前累计应计利息(未实现)。</param>
|
||
/// <param name="unwindPercent">
|
||
/// 平仓比例(0~1,实为 ratio 非百分数)。
|
||
/// 对应既有字段 closePercent;分母口径必须与传入 <paramref name="accrued"/> 所依据的持仓数量一致——
|
||
/// 是「本次计算依据的持仓」而非「初始建仓」,历史缺陷正来自这个歧义。
|
||
/// </param>
|
||
/// <param name="realizedInterest">已实现利息累计(legacy 字段 consumedInterest):历史各次 unwind 已确认、应从剩余未实现中扣除的部分。</param>
|
||
public static InterestResult ApplyUnwind(
|
||
InterestResult accrued,
|
||
decimal unwindPercent,
|
||
decimal realizedInterest = 0m,
|
||
int precision = Precision)
|
||
{
|
||
var remaining = 1m - unwindPercent;
|
||
return new InterestResult(
|
||
Round(accrued.Accrued * remaining - realizedInterest, precision),
|
||
Round(accrued.AccruedToday * remaining, precision));
|
||
}
|
||
|
||
/// <summary>待实现收益余额滚动(预付金 / 授信模式)。</summary>
|
||
/// <param name="openingUnrealized">上期待实现收益余额。</param>
|
||
/// <param name="todayIncome">本期新增。</param>
|
||
/// <param name="unwindDeduction">本期 unwind 应扣减(即本期实现的份额)。</param>
|
||
public static decimal AccrueUnrealized(
|
||
decimal openingUnrealized,
|
||
decimal todayIncome,
|
||
decimal unwindDeduction,
|
||
int precision = Precision)
|
||
=> Round(openingUnrealized + todayIncome - unwindDeduction, precision);
|
||
|
||
private static decimal Round(decimal value, int precision)
|
||
=> Math.Round(value, precision, MidpointRounding.AwayFromZero);
|
||
}
|