Merge branch 'glms/feature/1.4.2' into glms/feature/0812_zmr_divPower
# Conflicts: # YLErpDAL/Modules/EodModule/BondPaymentService.cs # YLErpDAL/Modules/SwapModule/SwapDealService.cs
This commit is contained in:
@@ -65,6 +65,13 @@ namespace YLErp.DBModels
|
||||
[DisplayName("实际付息(兑付)日")]
|
||||
[Column("pay_date_act")]
|
||||
public DateTime? payment_date { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 债权登记日(除息/归属截止日)——票息归属按此判定,而非支付日
|
||||
/// </summary>
|
||||
[DisplayName("债权登记日")]
|
||||
[Column("reg_date")]
|
||||
public DateTime? reg_date { get; set; }
|
||||
/// <summary>
|
||||
/// 每张兑付利息额
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace YLErp.DBModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 客户黑名单审批及操作日志。
|
||||
/// </summary>
|
||||
[Table("client_blacklog")]
|
||||
public class ClientBlackLog
|
||||
{
|
||||
public long id { get; set; }
|
||||
|
||||
public int ClientBlackId { get; set; }
|
||||
|
||||
public string Changes { get; set; }
|
||||
|
||||
public string OptType { get; set; }
|
||||
|
||||
public string DataType { get; set; }
|
||||
|
||||
public int OptId { get; set; }
|
||||
|
||||
public string OptName { get; set; }
|
||||
|
||||
public DateTime OptDate { get; set; }
|
||||
}
|
||||
|
||||
[NotMapped]
|
||||
public class ClientBlackLogDto : ClientBlackLog
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,13 @@ namespace YLErp.Model
|
||||
[Table("client_black")]
|
||||
public class client_black : DBModelWithOperator, IDataEntity, IDataTraceV2, IClonable<client_black>
|
||||
{
|
||||
public const string 未提交 = "未提交";
|
||||
public const string 新增审批中 = "新增审批中";
|
||||
public const string 新增已拒绝 = "新增已拒绝";
|
||||
public const string 已加入 = "已加入";
|
||||
public const string 删除审批中 = "删除审批中";
|
||||
public const string 删除已拒绝 = "删除已拒绝";
|
||||
|
||||
/// <summary>
|
||||
/// 客户名称
|
||||
/// </summary>
|
||||
@@ -25,6 +32,26 @@ namespace YLErp.Model
|
||||
[DataChange]
|
||||
public string Remarks { get; set; }
|
||||
|
||||
[DisplayName("提交审批时间")]
|
||||
public DateTime? ApprovalOptDate { get; set; }
|
||||
|
||||
[DisplayName("提交审批人")]
|
||||
public string ApprovalOptName { get; set; }
|
||||
|
||||
public int ApprovalProcess { get; set; }
|
||||
|
||||
[DisplayName("状态")]
|
||||
public string State { get; set; } = "";
|
||||
|
||||
[DisplayName("创建人")]
|
||||
public int? creator_id { get; set; }
|
||||
|
||||
[DisplayName("创建人")]
|
||||
public string creator_name { get; set; }
|
||||
|
||||
[DisplayName("创建时间")]
|
||||
public DateTime? creator_time { get; set; }
|
||||
|
||||
public client_black Clone()
|
||||
{
|
||||
return (client_black)MemberwiseClone();
|
||||
|
||||
@@ -205,7 +205,7 @@ namespace YLErp.DBModels
|
||||
[DataChange]
|
||||
public int InterestDirection { get; set; }
|
||||
/// <summary>
|
||||
/// 计息基本类型 1:固定值,2:合约名义本金规模,3:持仓名义本金,4:持仓市值,5:初始预付金,6:追加预付金
|
||||
/// 计息基本类型 1:固定值,2:合约名义本金规模,5:初始预付金,6:追加预付金,9:标的期初全价
|
||||
/// </summary>
|
||||
[DisplayName("计息基本类型")]
|
||||
[DataChange]
|
||||
|
||||
@@ -149,6 +149,11 @@ namespace YLErp.DBModels
|
||||
/// </summary>
|
||||
public decimal ClosePercent { get; set; }
|
||||
/// <summary>
|
||||
/// 是否计罚息(EQD-6977):提前终止平仓时利息端按持有至到期计息。默认 false=否。
|
||||
/// 由平仓页“是否罚息”下拉写入,经 GetUnwindInterests 透传至罚息接缝层。
|
||||
/// </summary>
|
||||
public bool IsPenaltyInterest { get; set; }
|
||||
/// <summary>
|
||||
/// 平仓名义本金
|
||||
/// </summary>
|
||||
public decimal CloseNotionalValue { get; set; }
|
||||
|
||||
@@ -280,7 +280,7 @@ namespace YLErp.DBModels
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 计息方式 1:固定值,2:合约名义本金规模,3:持仓名义本金,4:持仓市值
|
||||
/// 计息方式 1:固定值,2:合约名义本金规模,5:初始预付金,6:追加预付金,9:标的期初全价
|
||||
/// </summary>
|
||||
[DisplayName("计息方式")]
|
||||
[DataChange]
|
||||
|
||||
@@ -163,7 +163,7 @@ namespace YLErp.DBModels
|
||||
/// </summary>
|
||||
public string FloatRateUnderlyingCode { get; set; }
|
||||
/// <summary>
|
||||
/// 计息基本类型 1:固定值,2:合约名义本金规模,3:持仓名义本金,4:持仓市值,5:初始预付金,6:追加预付金
|
||||
/// 计息基本类型 1:固定值,2:合约名义本金规模,5:初始预付金,6:追加预付金,9:标的期初全价
|
||||
/// </summary>
|
||||
[DisplayName("计息基本类型")]
|
||||
[DataChange]
|
||||
|
||||
@@ -89,6 +89,21 @@ namespace YLErp.DBModels
|
||||
/// </summary>
|
||||
public string InterestCalcMode { get; set; } = "11";
|
||||
|
||||
/// <summary>算头:InterestCalcMode 首位为 1;null/缺省视为算头。收口原 GetInterests/InitInterestDate/EOD三处 StartsWith 解析。</summary>
|
||||
[JsonIgnore]
|
||||
public bool CalcFirst => InterestCalcMode?.StartsWith("1") ?? true;
|
||||
|
||||
/// <summary>算尾:InterestCalcMode 末位为 1;null/缺省视为算尾(EQD-6968 取价依赖判定的核心开关之一)。</summary>
|
||||
[JsonIgnore]
|
||||
public bool CalcLast => InterestCalcMode?.EndsWith("1") ?? true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否罚息(EQD-6977):提前终止平仓时利息端按持有至到期计息的簿记默认值。
|
||||
/// 平仓页默认带出此值、可修改,以平仓时选择为准(当次选择经 UnwindData.IsPenaltyInterest 走请求,不回写)。
|
||||
/// 存量 JSON 无此键 → 反序列化默认 false("否"),零回填。
|
||||
/// </summary>
|
||||
public bool IsPenaltyInterest { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///多空组合浮动端 收取方向 1:收取,2:支付
|
||||
/// </summary>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
namespace YLErp.Core.Interest;
|
||||
|
||||
/// <summary>
|
||||
/// 计息执行上下文:把"与具体金额/利率无关"的横向参数(年化天数、精度、trace 收集器)
|
||||
/// 打包成一个<b>只读值对象</b>,避免每个计息方法都重复携带这些参数。
|
||||
///
|
||||
/// <para><b>为何 trace 是"成员"而非散落参数</b>:利息纯函数(AccrueSimple / AccrueCompoundInArrears)
|
||||
/// 的核心职责是算账,trace 只是可观测性的旁路。把 trace 作为上下文的成员传入,
|
||||
/// 调用点只需传一个 ctx,签名更干净;同时 ctx 是只读值对象,不破坏纯函数
|
||||
/// (无共享可变状态 → 线程安全、可重入、可测)。<b>切勿</b>把 trace 设成类的实例/静态字段,
|
||||
/// 那会让并发的两笔交易共用同一 trace、并使函数带隐藏状态。</para>
|
||||
///
|
||||
/// <para>与 AccrualState(跨日滚动本金状态)/ AccrualPolicy(EOD 会计政策)正交:
|
||||
/// 本上下文只描述"如何算 + 往哪记",不持有任何交易进度。</para>
|
||||
/// </summary>
|
||||
public readonly struct AccrualContext
|
||||
{
|
||||
/// <summary>年化天数(365 / 360)。</summary>
|
||||
public int AnnualDays { get; }
|
||||
|
||||
/// <summary>舍入精度位数。默认 11(保证金腿);资金腿调用方应显式传 FundingLegPrecision=12。</summary>
|
||||
public int Precision { get; }
|
||||
|
||||
/// <summary>可选 trace 收集器;为 null 时不记录(纯计算场景直接传 null,与开关无关)。</summary>
|
||||
public AccrualTrace? Trace { get; }
|
||||
|
||||
public AccrualContext(int annualDays, int precision = 11, AccrualTrace? trace = null)
|
||||
=> (AnnualDays, Precision, Trace) = (annualDays, precision, trace);
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace YLErp.Core.Interest;
|
||||
|
||||
/// <summary>
|
||||
/// 计息过程追踪收集器(值对象,非日志)。
|
||||
///
|
||||
/// <para><b>为什么是收集器而不是日志调用</b>:计息数学(SwapInterest / FundingLegAccrual)必须保持纯函数、
|
||||
/// 可单测、不依赖 NLog;但按工程铁律,关键路径日志须<b>无条件常驻落盘</b>(出问题时事后翻日志定位,不能依赖开关)。
|
||||
/// 折中:纯函数把"发生了什么"记录为结构化条目写入本收集器,由<b>适配器(IO 边界)</b>统一经
|
||||
/// <c>SwapCalcTrace.Write</c> 常驻落盘。落盘职责归一处,计息代码零日志依赖、保持干净。</para>
|
||||
///
|
||||
/// <para><b>可 diff</b>:<see cref="ToString"/> 产出稳定、有序、与 SwapCalcTrace.Day 对齐的逐行文本,
|
||||
/// 新旧引擎对同一笔交易跑出的 trace 可直接 diff,定位"是计算变了还是重构引入了漂移"。</para>
|
||||
///
|
||||
/// <para>所有记录方法均为语义化命名(Day / ResetBefore / Rollover …),调用点一眼即懂,不污染数学可读性。</para>
|
||||
/// </summary>
|
||||
public sealed class AccrualTrace
|
||||
{
|
||||
private readonly List<AccrualTraceEntry> _entries = new();
|
||||
|
||||
/// <summary>已记录的追踪条目(只读)。</summary>
|
||||
public IReadOnlyList<AccrualTraceEntry> Entries => _entries;
|
||||
|
||||
/// <summary>计息区间起点:标记本次计算的整体边界与年化口径。</summary>
|
||||
public void MarkStart(DateTime start, DateTime end, AccrualBoundary boundary, int annualDays, bool annualized)
|
||||
=> Add(AccrualTraceEvent.Start, start,
|
||||
$"START 区间[{start:yyyy-MM-dd},{end:yyyy-MM-dd}] {boundary} annualDays={annualDays} annualized={annualized}");
|
||||
|
||||
/// <summary>逐日明细:当日生效利率、计息基数、当日利息、累计利息。这是"为何 accrued N 天而非 M 天"的直接证据。</summary>
|
||||
public void Day(int idx, DateTime date, decimal rate, decimal basePrincipal, decimal dayInterest, decimal accumulated)
|
||||
=> Add(AccrualTraceEvent.DayAccrual, date,
|
||||
$" [{idx}] {date:yyyy-MM-dd} rate={rate:P6} base={basePrincipal:F4} day={dayInterest:F6} acc={accumulated:F6}");
|
||||
|
||||
/// <summary>分段明细(多日计息,按重置日分段):段区间[segStart,segEnd)、天数、生效利率、计息基数、段利息、累计利息。
|
||||
/// 这是分段模型与旧逐日循环对账的核心证据——每段的"前后日期/基数/利率"一目了然。</summary>
|
||||
public void Segment(int idx, DateTime segStart, DateTime segEnd, int days, decimal rate, decimal basis, decimal segInterest, decimal accumulated)
|
||||
=> Add(AccrualTraceEvent.DayAccrual, segStart,
|
||||
$" [seg{idx}] [{segStart:yyyy-MM-dd},{segEnd:yyyy-MM-dd}) days={days} rate={rate:P6} basis={basis:F4} segInterest={segInterest:F6} acc={accumulated:F6}");
|
||||
|
||||
/// <summary>EOD 上下文:计息日、重置日标志、平仓比例、基数各分量。供日终单日计息定位"重置日分支选对了吗"。</summary>
|
||||
public void EodContext(DateTime eodDate, bool isResetDay, decimal unwindFraction, decimal priorAccrued, decimal priorNotional, decimal notional, decimal remainingFraction)
|
||||
=> Add(AccrualTraceEvent.Start, eodDate,
|
||||
$" CTX {eodDate:yyyy-MM-dd} reset={isResetDay} unwind={unwindFraction:P2} priorAccrued={priorAccrued:F4} priorNotional={priorNotional:F4} notional={notional:F4} remainingFrac={remainingFraction:P4}");
|
||||
|
||||
/// <summary>重置日<b>前</b>:生效利率(旧)与计息本金(滚动前)。利率/本金切换的"因"。</summary>
|
||||
public void ResetBefore(DateTime resetDate, decimal rateOld, decimal principalBefore)
|
||||
=> Add(AccrualTraceEvent.ResetBefore, resetDate,
|
||||
$" RESET↓ {resetDate:yyyy-MM-dd} rate(old)={rateOld:P6} principal(before)={principalBefore:F4}");
|
||||
|
||||
/// <summary>重置日<b>后</b>:生效利率(新)与计息本金(滚动后,已并本金)。利率/本金切换的"果"。</summary>
|
||||
public void ResetAfter(DateTime resetDate, decimal rateNew, decimal principalAfter)
|
||||
=> Add(AccrualTraceEvent.ResetAfter, resetDate,
|
||||
$" RESET↑ {resetDate:yyyy-MM-dd} rate(new)={rateNew:P6} principal(after)={principalAfter:F4}");
|
||||
|
||||
/// <summary>本金增加(利息滚入计息基数):复利段末并本金的瞬间,记录滚入额与并本金后的新基数。</summary>
|
||||
public void Rollover(DateTime resetDate, decimal accruedRolled, decimal newBasis)
|
||||
=> Add(AccrualTraceEvent.Rollover, resetDate,
|
||||
$" ROLLOVER {resetDate:yyyy-MM-dd} accrued(rolled)={accruedRolled:F6} newBasis={newBasis:F4}");
|
||||
|
||||
/// <summary>平仓缩放:平仓比例、累计已实现、剩余未实现。</summary>
|
||||
public void Unwind(DateTime date, decimal unwindPercent, decimal realized, decimal remainingUnrealized)
|
||||
=> Add(AccrualTraceEvent.Unwind, date,
|
||||
$" UNWIND {date:yyyy-MM-dd} pct={unwindPercent:P2} realized={realized:F6} remaining={remainingUnrealized:F6}");
|
||||
|
||||
/// <summary>收尾:最终累计利息与当日利息。</summary>
|
||||
public void MarkEnd(decimal totalAccrued, decimal totalToday)
|
||||
=> Add(AccrualTraceEvent.End, default,
|
||||
$"END accrued={totalAccrued:F6} today={totalToday:F6}");
|
||||
|
||||
private void Add(AccrualTraceEvent step, DateTime date, string line)
|
||||
=> _entries.Add(new AccrualTraceEntry(step, date, line));
|
||||
|
||||
/// <summary>稳定可 diff 的逐行文本(与 SwapCalcTrace.Day 格式对齐)。</summary>
|
||||
public override string ToString()
|
||||
=> _entries.Count == 0 ? "<empty trace>" : string.Join(Environment.NewLine, _entries.Select(e => e.Line));
|
||||
}
|
||||
|
||||
/// <summary>追踪条目的语义类别(对应 QuantLib/Strata 的"事件"概念),便于程序化筛选(如"只看重置日")。</summary>
|
||||
public enum AccrualTraceEvent
|
||||
{
|
||||
Start, DayAccrual, ResetBefore, ResetAfter, Rollover, Unwind, End
|
||||
}
|
||||
|
||||
/// <summary>单条追踪记录:类别 + 日期 + 已渲染文本。</summary>
|
||||
public readonly record struct AccrualTraceEntry(AccrualTraceEvent Step, DateTime Date, string Line);
|
||||
@@ -1,71 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace YLErp.Core.Interest;
|
||||
|
||||
/// <summary>
|
||||
/// 利率 + 计息方式(单利 / 复利 / 连续复利)。
|
||||
///
|
||||
/// <para><b>通用金融原语,与互换、衍生品、任何具体业务均无耦合</b>——谁需要算利息都能用。
|
||||
/// 利息计算不是互换特有的,所以它不住在 SwapModule,也不带任何 swap 词汇。</para>
|
||||
///
|
||||
/// <para>用法(年化时间 t,如 30天/365):</para>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>计息因子 = <see cref="CompoundFactor(decimal)"/>;含息额 = 本金 × 因子;</description></item>
|
||||
/// <item><description>利息 = 本金 × (因子 − 1) = <see cref="Interest(decimal, decimal)"/>。</description></item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>与 QuantLib 模型一致:单利 / 复利 / 连续复利只是 <see cref="Compounding"/> 的一个分支,
|
||||
/// 不是三套独立方法。TRS 的"重置日并本金"属于离散复利,用 <see cref="Compounding.Simple"/>
|
||||
/// 按段计息、段末把利息滚入本金即可(见 SwapInterest.AccrueCompoundInArrears),无需 Pow/Exp,decimal 精度无损。</para>
|
||||
///
|
||||
/// <para>互换特有的会计态(每日先舍入再乘天数、平仓缩放、跨日滚动本金)不属于本原语,
|
||||
/// 请在各自的 accrual 层处理。</para>
|
||||
/// </summary>
|
||||
public enum Compounding
|
||||
{
|
||||
/// <summary>单利:因子 = 1 + r·t。</summary>
|
||||
Simple,
|
||||
/// <summary>复利(理想化闭式):因子 = (1 + r/f)^(f·t),f 为年复利频次。</summary>
|
||||
Compounded,
|
||||
/// <summary>连续复利:因子 = e^(r·t)。</summary>
|
||||
Continuous
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 不可变利率值对象。构造即完整,无副作用。
|
||||
/// </summary>
|
||||
public readonly struct InterestRate
|
||||
{
|
||||
/// <summary>年化利率 r。</summary>
|
||||
public decimal Rate { get; }
|
||||
|
||||
/// <summary>计息方式。</summary>
|
||||
public Compounding Compounding { get; }
|
||||
|
||||
/// <summary>年复利频次(仅 <see cref="Compounding.Compounded"/> 使用,其余忽略,默认 1)。</summary>
|
||||
public int Frequency { get; }
|
||||
|
||||
public InterestRate(decimal rate, Compounding compounding, int frequency = 1)
|
||||
=> (Rate, Compounding, Frequency) = (rate, compounding, frequency);
|
||||
|
||||
/// <summary>
|
||||
/// 计息因子(输入年化时间 t)。
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see cref="Compounding.Simple"/>:decimal 精确运算。</description></item>
|
||||
/// <item><description><see cref="Compounding.Compounded"/> / <see cref="Compounding.Continuous"/>:闭式(double 计算后回 decimal),
|
||||
/// 满足通用定价 / 保证金场景;若要 decimal 精度的离散重置日复利,请用 Simple 按段计息并滚动本金。</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public decimal CompoundFactor(decimal t)
|
||||
=> Compounding switch
|
||||
{
|
||||
Compounding.Simple => 1m + Rate * t,
|
||||
Compounding.Compounded => (decimal)Math.Pow((double)(1m + Rate / Frequency), (double)(Frequency * t)),
|
||||
Compounding.Continuous => (decimal)Math.Exp((double)(Rate * t)),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(Compounding))
|
||||
};
|
||||
|
||||
/// <summary>利息 = 本金 × (因子 − 1)。</summary>
|
||||
public decimal Interest(decimal principal, decimal t)
|
||||
=> principal * (CompoundFactor(t) - 1m);
|
||||
}
|
||||
@@ -1,291 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using YLErp.Core.Interest;
|
||||
|
||||
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)利息腿计算——纯函数。
|
||||
///
|
||||
/// <para><b>层级关系</b>:计息数学(单利/复利/连续复利)是通用金融原语,已抽到
|
||||
/// <see cref="InterestRate"/>(<c>YLErp.Core.Interest</c>,与互换无关,谁都能用)。
|
||||
/// 本类只负责 TRS 特有的<b>会计态</b>:每日先舍入再乘天数的对账口径、平仓缩放、
|
||||
/// 跨日滚动本金、预付金/授信模式——这些不是"利率数学",不应塞进通用原语。</para>
|
||||
///
|
||||
/// <para>设计约束:
|
||||
/// 1. 无副作用——不读写 flowEvent、不取利率、不连库、不碰任何共享可变状态;
|
||||
/// 2. 同 input → 同 output,结果仅通过返回值流出;
|
||||
/// 3. 正交轴(算头算尾 / 单利复利 / 平仓 / 待实现收益)各自独立,互不耦合;
|
||||
/// 4. 调用方负责「取利率 + 构造日期区间 + 落库」,本类只算账。
|
||||
/// 由此,corp action 调整价格 / 数量时只需把新的 principal 与 rate 喂入,计息逻辑一行不动。</para>
|
||||
///
|
||||
/// <para>领域口径:本系统利息腿是单边融资腿,任一时点只有一个生效利率(见 SwapDealService 的
|
||||
/// floateRate 单一入参),<b>不存在</b> IRS 那种 fixedRate − floatingRate 轧差;
|
||||
/// 权益腿盈亏与平仓费用属三腿汇总层,不在本类职责内。</para>
|
||||
///
|
||||
/// <para>TRS 的"复利"是<b>离散重置日复利</b>:按重置日切段,每段用 <see cref="InterestRate.Simple"/>
|
||||
/// 计息、段末把利息滚入本金——本质就是单利按段叠加,decimal 精度无损,无需 Pow/Exp
|
||||
/// (见 <see cref="AccrueCompoundInArrears"/>)。所以本类不另立复利方法,计息只有一种,区别在于"是否滚动本金"。</para>
|
||||
///
|
||||
/// 为何不复用 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>资金腿计息精度(生产口径)。资金腿所有落库/对账均以 12 位为准,
|
||||
/// 与保证金腿的 Precision=11 不同。提升至公共常量,消除 SwapDealService 与 FundingLegAccrual 的重复定义。</summary>
|
||||
public const int FundingLegPrecision = 12;
|
||||
|
||||
/// <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>把 TRS 年化利率收敛为通用利率原语。
|
||||
/// TRS 计息按段均为单利——离散重置日复利靠"段末把利息滚入本金"实现,不引入 Compounded 闭式。</summary>
|
||||
public static InterestRate ToInterestRate(decimal annualRate)
|
||||
=> new(annualRate, Compounding.Simple);
|
||||
|
||||
/// <summary>单利:计息基数固定,每日利息相同,无逐日循环。</summary>
|
||||
public static InterestResult AccrueSimple(
|
||||
AccrualContext ctx,
|
||||
decimal principal,
|
||||
decimal rate,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
AccrualBoundary boundary)
|
||||
{
|
||||
var days = AccrualDays(startDate, endDate, boundary);
|
||||
var daily = Round(principal * rate / ctx.AnnualDays, ctx.Precision);
|
||||
return new InterestResult(Round(daily * days, ctx.Precision), daily);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 离散重置日<b>复利(compounded-in-arrears)</b>:按重置日切段,段间把累计利息并入计息基数(滚动本金)。
|
||||
/// 每段计息即 <see cref="ToInterestRate"/> 得到的 <see cref="InterestRate.Simple"/>(无逐日循环);
|
||||
/// 重置日是唯一并本金的地方。复利与单利只有"是否滚动本金"这一个区别。
|
||||
///
|
||||
/// <para>此模型即 OIS / SOFR / FR007 的 <b>compounded-in-arrears</b>:每个子区间取一次定盘 rᵢ、增长因子
|
||||
/// 1 + rᵢ·yfᵢ,段末把 accrued 折进下一期本金——比闭式 <see cref="InterestRate.Compounding.Compounded"/>
|
||||
/// 更贴合 FR007 约定且 decimal 无损。<b>注意:它<b>不是</b> InterestRate 的 Compounded 闭式分支(TRS 下该分支为死路径)。</para>
|
||||
///
|
||||
/// <para>每段可有<b>独立利率</b>(FR007 浮动逐段不同),由适配器按段取定盘后封装为
|
||||
/// <paramref name="resetSchedule"/> 传入——取价永远在编排层,原语只吃一个数(与 QuantLib/Strata 同范)。
|
||||
/// <paramref name="resetSchedule"/> 必须含一条 <c>ResetDate ≤ startDate</c> 的起始利率。</para>
|
||||
///
|
||||
/// <para>trace:经 <see cref="AccrualContext.Trace"/> 发射 Start / ResetBefore·ResetAfter(利率切换时) /
|
||||
/// Rollover(段末并本金) / End,完整记录"重置日前后、利率切换、本金增加前后"。纯函数保持无日志依赖。</para>
|
||||
/// </summary>
|
||||
/// <param name="resetSchedule">重置日 → 该段生效利率(段起点 = 重置日)。</param>
|
||||
public static InterestResult AccrueCompoundInArrears(
|
||||
AccrualContext ctx,
|
||||
decimal principal,
|
||||
IReadOnlyList<(DateTime ResetDate, decimal Rate)> resetSchedule,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
AccrualBoundary boundary)
|
||||
{
|
||||
var trace = ctx.Trace;
|
||||
trace?.MarkStart(startDate, endDate, boundary, ctx.AnnualDays, annualized: false);
|
||||
|
||||
var basis = principal;
|
||||
decimal accrued = 0m, accruedToday = 0m;
|
||||
|
||||
var segEnds = (resetSchedule ?? Array.Empty<(DateTime, decimal)>())
|
||||
.Select(s => s.ResetDate)
|
||||
.Where(d => d > startDate && d < endDate)
|
||||
.OrderBy(d => d)
|
||||
.Append(endDate)
|
||||
.ToArray();
|
||||
|
||||
// 段起点生效利率:取"不晚于该段起点"的最近一次重置利率。
|
||||
decimal RateAt(DateTime segStart)
|
||||
=> (resetSchedule ?? Array.Empty<(DateTime, decimal)>())
|
||||
.Where(s => s.ResetDate <= segStart)
|
||||
.OrderByDescending(s => s.ResetDate)
|
||||
.Select(s => s.Rate)
|
||||
.FirstOrDefault();
|
||||
|
||||
var segStart = startDate;
|
||||
var segIncludeStart = boundary.IncludeStart;
|
||||
var prevRate = RateAt(startDate);
|
||||
|
||||
foreach (var segEnd in segEnds)
|
||||
{
|
||||
var segRate = RateAt(segStart);
|
||||
var rateSwitched = segStart != startDate && segRate != prevRate;
|
||||
if (rateSwitched) trace?.ResetBefore(segStart, prevRate, basis);
|
||||
|
||||
var segBoundary = AccrualBoundary.Of(segIncludeStart, segEnd == endDate && boundary.IncludeEnd);
|
||||
var seg = AccrueSimple(ctx, basis, segRate, segStart, segEnd, segBoundary);
|
||||
|
||||
accrued += seg.Accrued;
|
||||
accruedToday = seg.AccruedToday;
|
||||
var newBasis = basis + seg.Accrued; // 仅在重置日并本金
|
||||
// 重置日本身不动本金:RESET↑ 的本金应是"重置边界基数"(basis),与 RESET↓ 一致;
|
||||
// 段末并本金后的 newBasis 由下方的 ROLLOVER 单独表达,避免重复/误导。
|
||||
if (rateSwitched) trace?.ResetAfter(segStart, segRate, basis);
|
||||
|
||||
trace?.Rollover(segEnd, seg.Accrued, newBasis);
|
||||
basis = newBasis;
|
||||
prevRate = segRate;
|
||||
segStart = segEnd;
|
||||
segIncludeStart = false; // 后续段不算头
|
||||
}
|
||||
|
||||
var result = new InterestResult(accrued, accruedToday);
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 固定利率复利便捷重载(每段同一 rate),向后兼容旧调用方。
|
||||
/// 内部把 resetDates 展平为"每段同率"的 schedule 后委托主方法。
|
||||
/// </summary>
|
||||
public static InterestResult AccrueCompoundInArrears(
|
||||
AccrualContext ctx,
|
||||
decimal principal,
|
||||
decimal rate,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
AccrualBoundary boundary,
|
||||
IReadOnlyList<DateTime>? resetDates = null)
|
||||
{
|
||||
var schedule = new List<(DateTime, decimal)> { (startDate, rate) };
|
||||
if (resetDates != null)
|
||||
foreach (var d in resetDates)
|
||||
if (d > startDate && d < endDate)
|
||||
schedule.Add((d, rate));
|
||||
return AccrueCompoundInArrears(ctx, principal, schedule, startDate, endDate, boundary);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="precision">舍入精度。⚠️ 默认 11(Precision),资金腿务必显式传 <see cref="FundingLegPrecision"/>=12。</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);
|
||||
|
||||
/// <summary>统一舍入:MidpointRounding.AwayFromZero。所有计息路径收口到此处,避免散落的 Math.Round 不一致。</summary>
|
||||
public static decimal Round(decimal value, int precision)
|
||||
=> Math.Round(value, precision, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
Reference in New Issue
Block a user