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" 对新读者是黑话。
// ─────────────────────────────────────────────────────────────────────────────
///
/// 计息区间边界(算头 / 算尾)。
/// 用具名值取代两个相邻 bool,物理上杜绝 calcFirst / calcLast 传反这一类历史缺陷。
///
public readonly struct AccrualBoundary
{
/// 算头:含 startDate。
public bool IncludeStart { get; }
/// 算尾:含 endDate。
public bool IncludeEnd { get; }
private AccrualBoundary(bool includeStart, bool includeEnd)
=> (IncludeStart, IncludeEnd) = (includeStart, includeEnd);
/// 算头算尾 [start, end]。
public static readonly AccrualBoundary Both = new(true, true);
/// 算头不算尾 [start, end)。
public static readonly AccrualBoundary StartOnly = new(true, false);
/// 不算头算尾 (start, end]。
public static readonly AccrualBoundary EndOnly = new(false, true);
/// 不算头不算尾 (start, end)。
public static readonly AccrualBoundary None = new(false, false);
/// 由既有 calcFirst / calcLast 布尔对构造,供旧调用方渐进迁移。
public static AccrualBoundary Of(bool includeStart, bool includeEnd) => new(includeStart, includeEnd);
public override string ToString()
=> $"{(IncludeStart ? "算头" : "不算头")}{(IncludeEnd ? "算尾" : "不算尾")}";
}
///
/// 计息结果。Accrued → 记账字段 InterestAmount / InterestProfitSum;AccruedToday → TdInterestAmount。
///
public readonly struct InterestResult
{
/// 区间累计应计利息。
public decimal Accrued { get; }
/// 末日(当日)应计利息。
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}";
}
///
/// 收益互换(TRS)利息腿计算——纯函数。
///
/// 设计约束:
/// 1. 无副作用——不读写 flowEvent、不取利率、不连库、不碰任何共享可变状态;
/// 2. 同 input → 同 output,结果仅通过返回值流出;
/// 3. 正交轴(算头算尾 / 单利复利 / 平仓 / 待实现收益)各自独立,互不耦合;
/// 4. 调用方负责「取利率 + 构造日期区间 + 落库」,本类只算账。
/// 由此,corp action 调整价格 / 数量时只需把新的 principal 与 rate 喂入,计息逻辑一行不动。
///
/// 领域口径:本系统利息腿是单边融资腿,任一时点只有一个生效利率(见 SwapDealService 的
/// floateRate 单一入参),不存在 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 反向依赖定价库。
///
public static class SwapInterest
{
/// 系统统一价格精度位数。
public const int Precision = 11;
/// 年化天数常量(合约字段存的是 int,故不用 enum)。
public const int Act365 = 365;
public const int Act360 = 360;
/// 应计天数。边界规则由日期区间表达,计息函数内不再出现 flag 分支。
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;
}
/// 单利:计息基数固定,每日利息相同,无逐日循环。
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);
}
///
/// 复利:按重置日切段,段间把累计利息并入计息基数。
/// 段内复用 AccrueSimple(仍无逐日循环);重置日是唯一并本金的地方。
///
public static InterestResult AccrueCompound(
decimal principal,
decimal rate,
DateTime startDate,
DateTime endDate,
AccrualBoundary boundary,
IReadOnlyList resetDates,
int annualDays,
int precision = Precision)
{
var basis = principal;
decimal accrued = 0m, accruedToday = 0m;
var segEnds = (resetDates ?? Array.Empty())
.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);
}
///
/// 平仓(Unwind)缩放——全仓唯一缩放点,物理上杜绝 unwindPercent 被重复相乘。
/// 全平即 unwindPercent = 1,不另设方法。
///
/// 已实现 / 未实现边界:传入的 是平仓前仍「未实现(unrealized)」的
/// 累计应计利息;本方法按比例缩放后返回「平仓后剩余未实现」部分,并扣除历史累计「已实现(realized)」
/// 的 。被平仓比例 unwindPercent 对应的那一份 accrued,
/// 即在此刻「实现(realized)」,由调用方记入 realizedInterest。
///
/// 平仓前累计应计利息(未实现)。
///
/// 平仓比例(0~1,实为 ratio 非百分数)。
/// 对应既有字段 closePercent;分母口径必须与传入 所依据的持仓数量一致——
/// 是「本次计算依据的持仓」而非「初始建仓」,历史缺陷正来自这个歧义。
///
/// 已实现利息累计(legacy 字段 consumedInterest):历史各次 unwind 已确认、应从剩余未实现中扣除的部分。
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));
}
/// 待实现收益余额滚动(预付金 / 授信模式)。
/// 上期待实现收益余额。
/// 本期新增。
/// 本期 unwind 应扣减(即本期实现的份额)。
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);
}