Merge remote-tracking branch 'origin/glms/feature/1.4.2' into glms/feature/1.4.2
This commit is contained in:
@@ -356,6 +356,22 @@ namespace YLErp
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否债券指数资产类型(永续,无到期日)。
|
||||
/// 注:ConvertCalcType 将其映射为 CommodityFutures 以复用计算路径,
|
||||
/// 但保存校验不应据此强制要求到期日。
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsBondIndex(string instType)
|
||||
=> BondIndex.Equals(instType, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// 是否利率收益率曲线资产类型(无到期日)。
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsRateYield(string instType)
|
||||
=> RateYield.Equals(instType, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
//------------CalcType-----------------------
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -488,6 +488,19 @@ namespace YLErp.DBModels
|
||||
return ConsGlobal.InstrumentType.CalcTypeIsFutures(UnderlyingInstrumentType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标的是否需要在保存时填写到期日。
|
||||
/// 仅真正的期货合约需要;BondIndex(债券指数)/ RateYield(利率曲线)虽被
|
||||
/// ConvertCalcType 映射为 CommodityFutures 以复用计算路径,但二者永续无到期日,
|
||||
/// 不应强制要求。集中此处作为唯一判定,避免 IsFutures() 判断在多处被重复收窄。
|
||||
/// </summary>
|
||||
public bool RequiresMaturityDate()
|
||||
{
|
||||
return ConsGlobal.InstrumentType.CalcTypeIsFutures(UnderlyingInstrumentType)
|
||||
&& !ConsGlobal.InstrumentType.IsBondIndex(UnderlyingInstrumentType)
|
||||
&& !ConsGlobal.InstrumentType.IsRateYield(UnderlyingInstrumentType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标的是否债券类型
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
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>重置日<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);
|
||||
@@ -0,0 +1,71 @@
|
||||
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,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using YLErp.Core.Interest;
|
||||
|
||||
namespace YLErp.Derivatives.Interest;
|
||||
|
||||
@@ -77,16 +78,25 @@ public readonly struct InterestResult
|
||||
/// <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 喂入,计息逻辑一行不动。
|
||||
/// 由此,corp action 调整价格 / 数量时只需把新的 principal 与 rate 喂入,计息逻辑一行不动。</para>
|
||||
///
|
||||
/// 领域口径:本系统利息腿是单边融资腿,任一时点只有一个生效利率(见 SwapDealService 的
|
||||
/// <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 是写死的半开区间,只能表达四种算头算尾中的一种;
|
||||
@@ -96,9 +106,13 @@ public readonly struct InterestResult
|
||||
/// </summary>
|
||||
public static class SwapInterest
|
||||
{
|
||||
/// <summary>系统统一价格精度位数。</summary>
|
||||
/// <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;
|
||||
|
||||
@@ -113,60 +127,122 @@ public static class SwapInterest
|
||||
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,
|
||||
int annualDays,
|
||||
int precision = Precision)
|
||||
AccrualBoundary boundary)
|
||||
{
|
||||
var days = AccrualDays(startDate, endDate, boundary);
|
||||
var daily = Round(principal * rate / annualDays, precision);
|
||||
return new InterestResult(Round(daily * days, precision), daily);
|
||||
var daily = Round(principal * rate / ctx.AnnualDays, ctx.Precision);
|
||||
return new InterestResult(Round(daily * days, ctx.Precision), daily);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复利:按重置日切段,段间把累计利息并入计息基数。
|
||||
/// 段内复用 AccrueSimple(仍无逐日循环);重置日是唯一并本金的地方。
|
||||
/// 离散重置日<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>
|
||||
public static InterestResult AccrueCompound(
|
||||
/// <param name="resetSchedule">重置日 → 该段生效利率(段起点 = 重置日)。</param>
|
||||
public static InterestResult AccrueCompoundInArrears(
|
||||
AccrualContext ctx,
|
||||
decimal principal,
|
||||
decimal rate,
|
||||
IReadOnlyList<(DateTime ResetDate, decimal Rate)> resetSchedule,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
AccrualBoundary boundary,
|
||||
IReadOnlyList<DateTime> resetDates,
|
||||
int annualDays,
|
||||
int precision = Precision)
|
||||
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 = (resetDates ?? Array.Empty<DateTime>())
|
||||
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(basis, rate, segStart, segEnd, segBoundary, annualDays, precision);
|
||||
var seg = AccrueSimple(ctx, basis, segRate, segStart, segEnd, segBoundary);
|
||||
|
||||
accrued += seg.Accrued;
|
||||
accruedToday = seg.AccruedToday;
|
||||
basis += seg.Accrued; // 仅在重置日并本金
|
||||
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; // 后续段不算头
|
||||
segIncludeStart = false; // 后续段不算头
|
||||
}
|
||||
|
||||
return new InterestResult(accrued, accruedToday);
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Newtonsoft.Json;
|
||||
using YLErp;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Modules.SwapModule;
|
||||
using YLErp.Modules.SwapModule.Accrual;
|
||||
using YLErp.Derivatives.Interest;
|
||||
using YLErp.Core.Interest;
|
||||
|
||||
namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
{
|
||||
/// <summary>
|
||||
/// 影子测试:CalcDailyCompoundInterestByEod(旧逐日循环)vs FundingLegAccrual.AccrueCompoundEod(新纯函数)。
|
||||
/// 构造同一组参数,两套实现并行跑,断言结果一致(到分)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class CompoundEodShadowTest
|
||||
{
|
||||
private const decimal Notional = 100_000_000m;
|
||||
private const decimal FixedRate = 0.03m;
|
||||
private const int AnnualDays = 365;
|
||||
private static readonly DateTime TradeDate = new(2026, 4, 21);
|
||||
private static readonly DateTime EodDate = new(2026, 4, 28); // 第7天=重置日
|
||||
|
||||
private static trade CreateTrade()
|
||||
{
|
||||
return new trade
|
||||
{
|
||||
id = 1, TradeNumber = "UT-SHADOW", ClientId = 999998,
|
||||
TradeType = "收益互换", TradeDate = TradeDate, StartDate = TradeDate,
|
||||
ExerciseDate = TradeDate.AddYears(1), TradeStatus = "确认成交", ValidState = "Valid",
|
||||
trade_extend = new trade_extend
|
||||
{
|
||||
TradeId = 1,
|
||||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||||
{
|
||||
AnnualDays = AnnualDays, InterestCalcMode = "11", SettlementRules = 0
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static swap_position CreatePosition(int interestMode, int interestType, int resetDays)
|
||||
{
|
||||
return new swap_position
|
||||
{
|
||||
id = 1001, SwapTradeId = 1,
|
||||
PosiDirection = 0,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = interestMode,
|
||||
InterestRateDefault = FixedRate,
|
||||
InterestPrincipalFix = Notional,
|
||||
PosiStartDate = TradeDate,
|
||||
PosiMatuirityDate = TradeDate.AddYears(1),
|
||||
IsInitial = true, Invalid = false,
|
||||
InterestType = interestType,
|
||||
IsAnnualized = true,
|
||||
interest_rest_days = resetDays,
|
||||
interest_rule = 0,
|
||||
FloatRateUnderlyingCode = null,
|
||||
InterestSwapInterval = "[]"
|
||||
};
|
||||
}
|
||||
|
||||
private static eod_swap_position CreatePreEod(decimal tdPrincipal, decimal unrealized)
|
||||
{
|
||||
return new eod_swap_position
|
||||
{
|
||||
id = 1, SwapTradeId = 1, PositionId = 1001,
|
||||
ValueDate = EodDate.AddDays(-1),
|
||||
TdInterestPrincipal = tdPrincipal,
|
||||
InterestProfitSum = unrealized,
|
||||
PosiNotionalValue = Notional,
|
||||
FloatRate = 0m
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置日场景:EOD 恰为重置日(7天周期,第7天)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void 影子_重置日_旧新一致()
|
||||
{
|
||||
var position = CreatePosition((int)InterestModeEnum.合约名义本金规模, (int)InterestTypeEnum.复利, 7);
|
||||
var preEod = CreatePreEod(Notional, 50_000m);
|
||||
var flowEvent = new swap_flow_event { InterestRate = FixedRate };
|
||||
|
||||
// 旧方法
|
||||
decimal oldInterest = 0, oldTd = 0;
|
||||
var svc = new StubSvc();
|
||||
svc.CalcDailyCompoundInterestByEod(preEod, EodDate, TradeDate, position,
|
||||
Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m, Notional,
|
||||
ref oldInterest, ref oldTd);
|
||||
|
||||
// 新方法
|
||||
var rate = FundingLegRate.Fixed(FixedRate);
|
||||
var policy = new AccrualPolicy(AccrualBoundary.Both, true, 7, AnnualDays, true);
|
||||
var remainingPercent = Math.Max(0m, Math.Min(1m, Notional / Notional));
|
||||
var result = FundingLegAccrual.AccrueCompoundEod(
|
||||
50_000m, Notional, Notional, 1m, Notional, rate, policy,
|
||||
isResetDay: true, remainingPercent, EodDate);
|
||||
|
||||
Console.WriteLine($"重置日: 旧 InterestAmount={oldInterest} Td={oldTd}");
|
||||
Console.WriteLine($"重置日: 新 Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
|
||||
Assert.AreEqual((double)oldInterest, (double)result.Accrued, 0.01, "InterestAmount 一致");
|
||||
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 非重置日场景:第3天(非7的倍数)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void 影子_非重置日_旧新一致()
|
||||
{
|
||||
var nonResetDate = new DateTime(2026, 4, 24); // 第3天
|
||||
var position = CreatePosition((int)InterestModeEnum.合约名义本金规模, (int)InterestTypeEnum.复利, 7);
|
||||
var preEod = CreatePreEod(Notional, 30_000m);
|
||||
preEod.ValueDate = nonResetDate.AddDays(-1);
|
||||
var flowEvent = new swap_flow_event { InterestRate = FixedRate };
|
||||
|
||||
// 旧方法
|
||||
decimal oldInterest = 0, oldTd = 0;
|
||||
var svc = new StubSvc();
|
||||
svc.CalcDailyCompoundInterestByEod(preEod, nonResetDate, TradeDate, position,
|
||||
Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m, Notional,
|
||||
ref oldInterest, ref oldTd);
|
||||
|
||||
// 新方法
|
||||
var rate = FundingLegRate.Fixed(FixedRate);
|
||||
var policy = new AccrualPolicy(AccrualBoundary.Both, true, 7, AnnualDays, true);
|
||||
var result = FundingLegAccrual.AccrueCompoundEod(
|
||||
30_000m, Notional, Notional, 1m, Notional, rate, policy,
|
||||
isResetDay: false, 0m, nonResetDate);
|
||||
|
||||
Console.WriteLine($"非重置日: 旧 InterestAmount={oldInterest} Td={oldTd}");
|
||||
Console.WriteLine($"非重置日: 新 Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
|
||||
Assert.AreEqual((double)oldInterest, (double)result.Accrued, 0.01, "InterestAmount 一致");
|
||||
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
|
||||
}
|
||||
|
||||
private sealed class StubSvc : SwapDealService
|
||||
{
|
||||
public StubSvc() : base(new OptUserInfo(0, nameof(CompoundEodShadowTest), OptUserFrom.UnitTest)) { }
|
||||
public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate) => 0m;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Newtonsoft.Json;
|
||||
using YLErp;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Modules.SwapModule;
|
||||
using YLErp.Modules.SwapModule.Accrual;
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
{
|
||||
/// <summary>
|
||||
/// 影子测试:CalcDailySimpleInterest(旧逐日循环)vs FundingLegAccrual.AccrueSimplePeriod(新分段纯函数)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SimplePeriodShadowTest
|
||||
{
|
||||
private const decimal Notional = 100_000_000m;
|
||||
private const decimal Spread = 0.0025m;
|
||||
private const int AnnualDays = 365;
|
||||
private static readonly DateTime StartDate = new(2026, 4, 21);
|
||||
private static readonly DateTime EndDate = new(2026, 5, 11); // 21天, 7天周期→重置日 4/28, 5/5
|
||||
|
||||
private static trade CreateTrade()
|
||||
{
|
||||
return new trade
|
||||
{
|
||||
id = 1, TradeNumber = "UT-SIMPLE-SHADOW", ClientId = 999998,
|
||||
TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate,
|
||||
ExerciseDate = StartDate.AddYears(1), TradeStatus = "确认成交", ValidState = "Valid",
|
||||
trade_extend = new trade_extend
|
||||
{
|
||||
TradeId = 1,
|
||||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||||
{
|
||||
AnnualDays = AnnualDays, InterestCalcMode = "10", SettlementRules = 0
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static swap_position CreatePosition()
|
||||
{
|
||||
return new swap_position
|
||||
{
|
||||
id = 1001, SwapTradeId = 1, PosiDirection = 0,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = (int)InterestModeEnum.标的期初全价,
|
||||
InterestRateDefault = Spread,
|
||||
InterestPrincipalFix = Notional,
|
||||
PosiStartDate = StartDate, PosiMatuirityDate = StartDate.AddYears(1),
|
||||
IsInitial = true, Invalid = false,
|
||||
InterestType = (int)InterestTypeEnum.单利,
|
||||
IsAnnualized = true, interest_rest_days = 7, interest_rule = 0,
|
||||
FloatRateUnderlyingCode = null,
|
||||
InterestSwapInterval = "[]"
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class StubSvc : SwapDealService
|
||||
{
|
||||
public StubSvc() : base(new OptUserInfo(0, nameof(SimplePeriodShadowTest), OptUserFrom.UnitTest)) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 固定利率(无FR007)算头不算尾,全平,无历史归档。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void 影子_固定利率_无归档_旧新一致()
|
||||
{
|
||||
var td = CreateTrade();
|
||||
var position = CreatePosition();
|
||||
var flowEvent = new swap_flow_event { InterestRate = Spread };
|
||||
var preEod = new eod_swap_position { id = 0, TdInterestPrincipal = 0, InterestProfitSum = 0 };
|
||||
|
||||
// 旧方法
|
||||
decimal oldI = 0, oldTd = 0;
|
||||
var svc = new StubSvc();
|
||||
svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent,
|
||||
AnnualDays, false, 0m, 1m, Notional, true, false, ref oldI, ref oldTd);
|
||||
|
||||
// 新方法:固定利率全段相同
|
||||
// 旧代码差分: dynomicPrincipal = preEod.TdInterestPrincipal(=0) + posiPrincipal - orginPv = 0
|
||||
var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) };
|
||||
var accrualPrincipal = 0m + Notional - Notional; // 差分 = 0
|
||||
var result = FundingLegAccrual.AccrueSimplePeriod(
|
||||
priorUnrealized: 0m,
|
||||
accrualPrincipal: 0m, // 差分=0(无归档时 preEod.TdInterestPrincipal=0)
|
||||
closeRatio: 1m,
|
||||
segmentRates: segRates,
|
||||
startDate: StartDate,
|
||||
endDate: EndDate,
|
||||
priorValueDate: DateTime.MinValue,
|
||||
boundary: AccrualBoundary.StartOnly,
|
||||
annualDays: AnnualDays,
|
||||
isAnnualized: true);
|
||||
|
||||
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
|
||||
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
|
||||
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 有历史归档(preEod.id != 0),续接上一日终。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void 影子_有归档_续接_旧新一致()
|
||||
{
|
||||
var position = CreatePosition();
|
||||
var preEodDate = new DateTime(2026, 5, 4); // 上一日终 = 第14天
|
||||
var preEod = new eod_swap_position
|
||||
{
|
||||
id = 1, SwapTradeId = 1, PositionId = 1001,
|
||||
ValueDate = preEodDate,
|
||||
TdInterestPrincipal = Notional,
|
||||
InterestProfitSum = 200_000m,
|
||||
PosiNotionalValue = Notional, FloatRate = 0m
|
||||
};
|
||||
var flowEvent = new swap_flow_event { InterestRate = Spread };
|
||||
|
||||
// 旧方法
|
||||
decimal oldI = 0, oldTd = 0;
|
||||
var svc = new StubSvc();
|
||||
svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent,
|
||||
AnnualDays, false, 0m, 0.5m, Notional, true, false, ref oldI, ref oldTd);
|
||||
|
||||
// 新方法
|
||||
// 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv
|
||||
var accrualPrincipal = Notional + Notional - Notional;
|
||||
var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) };
|
||||
var result = FundingLegAccrual.AccrueSimplePeriod(
|
||||
priorUnrealized: 200_000m * 0.5m, // InterestProfitSum × closePercent
|
||||
accrualPrincipal: accrualPrincipal,
|
||||
closeRatio: 0.5m,
|
||||
segmentRates: segRates,
|
||||
startDate: StartDate,
|
||||
endDate: EndDate,
|
||||
priorValueDate: preEodDate,
|
||||
boundary: AccrualBoundary.StartOnly,
|
||||
annualDays: AnnualDays,
|
||||
isAnnualized: true);
|
||||
|
||||
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
|
||||
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
|
||||
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using YLErp.Modules.SwapModule.ReturnLegs;
|
||||
|
||||
namespace UnitTestProject.Modules.SwapModule.ReturnLegs
|
||||
{
|
||||
[TestClass]
|
||||
public class PositionValueCalcTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void 无方向因子_利息加浮动()
|
||||
=> Assert.AreEqual(1500m, PositionValueCalc.Calc(1000m, 500m));
|
||||
|
||||
[TestMethod]
|
||||
public void 收取方向_利息乘1()
|
||||
=> Assert.AreEqual(1500m, PositionValueCalc.Calc(1000m, 500m, 1));
|
||||
|
||||
[TestMethod]
|
||||
public void 支付方向_利息乘负1()
|
||||
=> Assert.AreEqual(-500m, PositionValueCalc.Calc(1000m, 500m, -1));
|
||||
|
||||
[TestMethod]
|
||||
public void 零利息_等于浮动端()
|
||||
=> Assert.AreEqual(500m, PositionValueCalc.Calc(0m, 500m));
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ namespace YLErp.Modules.ClientModule
|
||||
//针对法人,授权协议签署人才做唯一性判断,有效期结束后可新增
|
||||
if ((contactType == contactypelist.FirstOrDefault(x => x.ContactType == "法人")?.id || (contactType == contactypelist.FirstOrDefault(x => x.ContactType == "授权协议签署人")?.id && PS.Config.Company == Configuration.CompanyEnum.国泰君安))
|
||||
&& DbContext.clientduty.Any(x => x.ApprovalOrder < 1 && x.ClientId == req.ClientId && x.ContactTypeId.Contains(contactType.ToString()) && (x.DeadLine >= DateTime.Now || !x.DeadLine.HasValue))
|
||||
&& !PS.Config.Is浙期 && PS.Config.Company != Configuration.CompanyEnum.国联)
|
||||
&& PS.Config.Company != Configuration.CompanyEnum.国联)
|
||||
{
|
||||
throw new ServiceException("法人在有效期内必须唯一,授权协议签署人必须唯一");
|
||||
}
|
||||
@@ -113,7 +113,7 @@ namespace YLErp.Modules.ClientModule
|
||||
//针对法人,授权协议签署人才做唯一性判断,有效期结束后可新增
|
||||
if ((contactType == contactypelist.FirstOrDefault(x => x.ContactType == "法人")?.id || contactType == contactypelist.FirstOrDefault(x => x.ContactType == "授权协议签署人")?.id)
|
||||
&& DbContext.clientduty.Any(x => x.ApprovalOrder < 1 && x.ClientId == req.ClientId && x.ContactTypeId.Contains(contactType.ToString()) && x.id != req.id && (x.DeadLine >= DateTime.Now || !x.DeadLine.HasValue))
|
||||
&& !PS.Config.Is浙期 && PS.Config.Company != Configuration.CompanyEnum.国联)
|
||||
&& PS.Config.Company != Configuration.CompanyEnum.国联)
|
||||
{
|
||||
throw new ServiceException("法人在有效期内必须唯一,授权协议签署人必须唯一");
|
||||
}
|
||||
@@ -209,7 +209,7 @@ namespace YLErp.Modules.ClientModule
|
||||
if (dbmodel.ApprovalOrder == 0)
|
||||
{
|
||||
if (clientCount > 0 && client.ProcessStatus == "已开户"
|
||||
&& (PS.Config.Is浙期 || PS.Config.Company == Configuration.CompanyEnum.国联))
|
||||
&& PS.Config.Company == Configuration.CompanyEnum.国联)
|
||||
{
|
||||
if (!isAdd)
|
||||
{
|
||||
@@ -399,7 +399,7 @@ namespace YLErp.Modules.ClientModule
|
||||
var clientCount = yldb.approvalprocess.Where(x => x.processType == "ClientProcess").Count();
|
||||
|
||||
if (clientCount > 0 && client.ProcessStatus == "已开户"
|
||||
&& (PS.Config.Is浙期 || PS.Config.Company == Configuration.CompanyEnum.国联)
|
||||
&& PS.Config.Company == Configuration.CompanyEnum.国联
|
||||
&& clientDuty.ApprovalOrder == 0)
|
||||
{
|
||||
clientDuty.ApprovalOrder = (int)ApprovalOrderEnum.删除;
|
||||
|
||||
@@ -1103,7 +1103,7 @@ namespace YLErp.Modules.ClientModule
|
||||
public bool resetClientProcess(client_file file, ApprovalOrderEnum orderEnum)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(file.ProtocolNumber) && file.ApprovalOrder == (int)ApprovalOrderEnum.无需审批
|
||||
&& !PS.Config.Is浙期 && PS.Config.Company != Configuration.CompanyEnum.国联)
|
||||
&& PS.Config.Company != Configuration.CompanyEnum.国联)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1778,7 +1778,7 @@ namespace YLErp.Modules.ClientModule
|
||||
var processlist = yldb.approvalprocess.Where(x => x.processType == "ClientProcess");
|
||||
|
||||
if (processlist.Count() > 0 && client.ProcessStatus == "已开户"
|
||||
&& (PS.Config.Is浙期 || PS.Config.Company == Configuration.CompanyEnum.国联))
|
||||
&& PS.Config.Company == Configuration.CompanyEnum.国联)
|
||||
{
|
||||
clientDuty.ApprovalOrder = (int)ApprovalOrderEnum.导入;
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# SwapModule 架构说明
|
||||
|
||||
> 本文档描述 2026-08 重构后的模块结构。所有改动均通过编译 + 全量测试验证(零回归)。
|
||||
|
||||
## 三大限界上下文
|
||||
|
||||
TRS 的三类业务各自独立,通过 SwapInterest 纯函数库共享计息能力:
|
||||
|
||||
```
|
||||
SwapInterest (Core 纯函数)
|
||||
"本金 × 利率 × 天数 / 年化"
|
||||
不依附于任何腿
|
||||
↑
|
||||
┌──────────────┼──────────────┐
|
||||
│ 谁需要算利息就调它 │
|
||||
│ │
|
||||
┌─────┴──────┐ ┌──────┴───────┐
|
||||
│ FundingLeg │ │ Margin │
|
||||
│ 融资腿 │ │ 保证金 │
|
||||
│ │ │ │
|
||||
│ 客户付券商 │ │ 客户交的抵押品│
|
||||
│ 的融资成本 │ │ │
|
||||
│ spread+FR007│ │ 余额/追保/返还│
|
||||
│ mode 1/2/9 │ │ mode 5/6 │
|
||||
└─────────────┘ └──────────────┘
|
||||
|
||||
┌─────────────┐
|
||||
│ ReturnLeg │
|
||||
│ 标的端 │
|
||||
│ │
|
||||
│ 标的总回报 │
|
||||
│ 价格+分红 │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
**命名规则(防歧义)**:
|
||||
- `Funding` = 融资成本(不用 Interest,避免和通用"利息"混)
|
||||
- `Return` = 标的总回报(不用 Float,避免和 FR007 浮动利率混)
|
||||
- `Margin` = 保证金(客户交的抵押品,有余额和返息,不走融资腿的计息基数框架)
|
||||
- `FloatRate` = FR007 浮动利率(唯一含义)
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
SwapModule/
|
||||
├── FundingLegs/ 融资腿(mode 1/2/9)
|
||||
│ ├── IFundingLegStrategy 策略接口 + NotionalResult 值对象
|
||||
│ ├── FixedAmountLeg mode 1 固定值(恒=Fix 不随比例变)
|
||||
│ ├── ContractNotionalLeg mode 2 合约名义本金规模
|
||||
│ ├── UnderlyingEntryFullPriceLeg mode 9 标的期初全价
|
||||
│ └── FundingLegStrategyFactory 按 mode 分发
|
||||
│
|
||||
├── Margin/ 保证金(mode 5/6)
|
||||
│ ├── MarginModes mode 判断(含 ForLinq for EF Core)
|
||||
│ ├── MarginBalance 保证金余额(值对象)
|
||||
│ ├── MarginAccount 余额管理 + AccrueInterest 计息入口
|
||||
│ ├── MarginCalc 纯函数(PreviousBalance/FlipDirection/AccumulateSettlement)
|
||||
│ ├── IMarginResolver 保证金形态接口
|
||||
│ └── Cash/Credit/Guarantee 三种形态实现
|
||||
│
|
||||
├── ReturnLegs/ 标的端
|
||||
│ ├── ReturnLegSummary 标的端汇总值
|
||||
│ ├── QtyRollforward 数量递推(预留 corpActionDeltaQty 给公司行为)
|
||||
│ ├── MtmCalc 盯市(MarketValue + UnrealizedPnl)
|
||||
│ ├── DividendCalc 增值税后票息(AfterTax + AfterTaxRaw)
|
||||
│ ├── DirectionRatio 方向因子(LongShort + ReceivePay)
|
||||
│ └── PositionValueCalc 持仓价值汇总(利息端 + 浮动端)
|
||||
│
|
||||
├── SwapDealService.cs 盘中平仓/互换主逻辑
|
||||
├── SwapEodPositionService.cs EOD 日终归档主逻辑
|
||||
├── SwapDealIndexFixer.cs SwapDealService 专用取价器(委托 TryGetFloatRate)
|
||||
└── Fr007IndexFixer.cs FR007 取价生产实现
|
||||
```
|
||||
|
||||
## Core 层(Framework/YLErp.Core/Interest/)
|
||||
|
||||
```
|
||||
Interest/
|
||||
├── SwapInterest.cs 纯函数库(AccrueSimple/AccrueCompound/ApplyUnwind)
|
||||
├── IIndexFixer.cs 取价接口
|
||||
├── IndexFixerBase.cs 取价日计算工具
|
||||
└── Fr007IndexFixer.cs FR007 取价生产实现(调 EodPriceQueryService)
|
||||
```
|
||||
|
||||
## InterestModeEnum(显式赋值,DB 契约)
|
||||
|
||||
```
|
||||
Unknown = 0
|
||||
固定值 = 1 → FundingLeg
|
||||
合约名义本金规模 = 2 → FundingLeg
|
||||
初始预付金 = 5 → Margin
|
||||
追加预付金 = 6 → Margin
|
||||
标的期初全价 = 9 → FundingLeg
|
||||
|
||||
已删除(无历史数据):
|
||||
3 = 持仓名义本金(死代码)
|
||||
4 = 持仓市值(死代码)
|
||||
7 = 多头存续名义本金(界面已禁用)
|
||||
8 = 空头存续名义本金(界面已禁用)
|
||||
```
|
||||
|
||||
## 已消除的 inline 重复
|
||||
|
||||
| 公式 | 原重复 | 收敛到 |
|
||||
|---|---|---|
|
||||
| FR007 取价 | 6处 | IIndexFixer |
|
||||
| 保证金 mode 判断 | 10处 | MarginModes |
|
||||
| UnderlyingMarketValue | 4处 | MtmCalc.MarketValue |
|
||||
| PosiMtmPnL | 4处 | MtmCalc.UnrealizedPnl |
|
||||
| PosiQuantity 递推 | 2行 | QtyRollforward.Calc |
|
||||
| 增值税公式 | 5处 | DividendCalc |
|
||||
| 方向因子三元式 | 7处 | DirectionRatio |
|
||||
| SwapPositionValue | 8处 | PositionValueCalc |
|
||||
| CalcNotionalByMode | 整个方法 | 已删除(融资腿走工厂,保证金内联) |
|
||||
|
||||
## 方向因子类型规范
|
||||
|
||||
所有方向因子(多空 +1/-1、收付 +1/-1)统一用 **int**,不用 decimal:
|
||||
|
||||
| 纯函数 | 参数 | 类型 |
|
||||
|---|---|---|
|
||||
| MtmCalc.MarketValue | shortRatio | int |
|
||||
| MtmCalc.UnrealizedPnl | shortRatio, ratio | int |
|
||||
| DirectionRatio.LongShort | 返回值 | int |
|
||||
| DirectionRatio.ReceivePay | 返回值 | int |
|
||||
| MarginCalc.FlipDirection | 返回值 | int |
|
||||
| PositionValueCalc.Calc | ratio | int |
|
||||
|
||||
## 待后续改造
|
||||
|
||||
| 项目 | 依赖 | 接缝已预留 |
|
||||
|---|---|---|
|
||||
| 公司行为(送股/拆股) | QtyRollforward.corpActionDeltaQty | ✅ |
|
||||
| 公司行为(登记日快照) | DividendCalc + BondPayment | 见 corp-action-refactor-proposal.md |
|
||||
| 保证金配置/规则/占用 | MarginAccount + MarginCalc | ✅ |
|
||||
| RecordMarginCashFlow 迁入 Margin | AddClientCash 加 virtual | 待做 |
|
||||
| EOD 编排拆分 | SwapPositionCompose | 待业务需求驱动 |
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
/// 计息政策(不可变配置)。把"算头算尾 / 单复利率 / 重置频率 / 年化天数"收敛为一处,
|
||||
/// 取代旧代码里散落各处的 calcFirst/calcLast 布尔对与魔法数字。
|
||||
///
|
||||
/// 单/复利不再另立枚举——直接复用既有 DB 枚举 InterestTypeEnum(单利=0 / 复利=1),
|
||||
/// 通过 <see cref="IsCompound"/> 暴露为类型安全的 bool。
|
||||
/// </summary>
|
||||
public sealed class AccrualPolicy
|
||||
{
|
||||
/// <summary>算头算尾约定(复用 SwapInterest 已有的 AccrualBoundary,物理上杜绝 calcFirst/calcLast 传反)。</summary>
|
||||
public AccrualBoundary Convention { get; }
|
||||
|
||||
/// <summary>是否复利(利滚利)。来自 DB 的 InterestTypeEnum;单利=false,复利=true。</summary>
|
||||
public bool IsCompound { get; }
|
||||
|
||||
/// <summary>利率重置周期(天)。FR007 通常为 7;复利时亦为"利息并入本金"的周期。</summary>
|
||||
public int ResetPeriodDays { get; }
|
||||
|
||||
/// <summary>年化基数(365 / 360)。</summary>
|
||||
public int AnnualDays { get; }
|
||||
|
||||
/// <summary>是否年化(position.IsAnnualized)。决定利息是否再除以 <see cref="AnnualDays"/>;
|
||||
/// 与 <see cref="AnnualDays"/> 一同收敛 daycount 语义,不再作为裸 bool 散落在计息签名里。</summary>
|
||||
public bool IsAnnualized { get; }
|
||||
|
||||
public AccrualPolicy(AccrualBoundary convention, bool isCompound, int resetPeriodDays, int annualDays, bool isAnnualized = false)
|
||||
=> (Convention, IsCompound, ResetPeriodDays, AnnualDays, IsAnnualized) = (convention, isCompound, resetPeriodDays, annualDays, isAnnualized);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
/// 融资腿逐日计息的跨日状态(不可变值对象)。
|
||||
/// 这是"待实现利息"在日间滚动的快照,区别于已落库的 <c>swap_flow_event</c>。
|
||||
///
|
||||
/// 旧字段 → 领域命名映射(DB 列不可改,仅在边界处适配;本类内部一律用下列自描述名):
|
||||
/// <list type="table">
|
||||
/// <item><term>TdInterestPrincipal</term><description>逐日滚动的计息本金 → <see cref="AccrualPrincipal"/></description></item>
|
||||
/// <item><term>InterestIncomeSum</term><description>累计待实现利息 → <see cref="UnrealizedInterest"/></description></item>
|
||||
/// <item><term>consumedInterest</term><description>历史已实现利息(legacy) → <see cref="RealizedInterest"/></description></item>
|
||||
/// <item><term>ValueDate</term><description>快照截至日 → <see cref="ValueDate"/>(EOD 续接起算日,Bug C / 5-11 跳过需据此判断从哪天接续)。</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public readonly struct AccrualState
|
||||
{
|
||||
/// <summary>用于计算当日利息的计息本金。单利=名义本金基数;复利=本金+累计利息。</summary>
|
||||
public decimal AccrualPrincipal { get; }
|
||||
|
||||
/// <summary>累计待实现(未平仓)利息。</summary>
|
||||
public decimal UnrealizedInterest { get; }
|
||||
|
||||
/// <summary>历史各次平仓已确认的已实现利息,从剩余待实现中扣除。</summary>
|
||||
public decimal RealizedInterest { get; }
|
||||
|
||||
/// <summary>快照截至日(来自 eod_swap_position.ValueDate)。编排层据此判断计息区间起点,避免 5-11 等"跳过日"误重算。</summary>
|
||||
public DateTime ValueDate { get; }
|
||||
|
||||
public AccrualState(decimal accrualPrincipal, decimal unrealizedInterest, decimal realizedInterest, DateTime valueDate)
|
||||
=> (AccrualPrincipal, UnrealizedInterest, RealizedInterest, ValueDate) = (accrualPrincipal, unrealizedInterest, realizedInterest, valueDate);
|
||||
|
||||
/// <summary>向后兼容:未携带快照日期时(如纯内存构造)用默认日。</summary>
|
||||
public AccrualState(decimal accrualPrincipal, decimal unrealizedInterest, decimal realizedInterest)
|
||||
: this(accrualPrincipal, unrealizedInterest, realizedInterest, default) { }
|
||||
|
||||
/// <summary>空状态(新开仓首个计息日之前)。</summary>
|
||||
public static readonly AccrualState Zero = new(0m, 0m, 0m);
|
||||
|
||||
/// <summary>
|
||||
/// 从上一日日终归档 <see cref="eod_swap_position"/> 适配(边界适配:DB 列名 → 领域名)。
|
||||
/// 仅映射计息状态;名义本金基数 / 平仓比例 / 已实现利息等由调用方另行传入。
|
||||
/// </summary>
|
||||
public static AccrualState FromPreviousEod(eod_swap_position previousEod)
|
||||
=> previousEod == null || previousEod.id == 0
|
||||
? Zero
|
||||
: new AccrualState(previousEod.TdInterestPrincipal, previousEod.InterestIncomeSum, 0m, previousEod.ValueDate);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
using System;
|
||||
using YLErp.Core.Interest;
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
/// 融资腿计息编排层——纯数学部分(替换 SwapDealService 内 CalcDaily* 家族的纯计算)。
|
||||
///
|
||||
/// <para>职责边界(与 SwapInterest 原语、SwapDealService 适配器三者正交):</para>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>本类:持有已解析的 <see cref="FundingLegRate"/> 与 <see cref="AccrualPolicy"/>,执行单利/复利日终计息纯函数。</description></item>
|
||||
/// <item><description>SwapInterest:原子 "本金×利率×天数/年化" 纯函数,无状态。</description></item>
|
||||
/// <item><description>SwapDealService:负责 DB 读、取率、swap_flow_event 构造与落库(IO)。</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static class FundingLegAccrual
|
||||
{
|
||||
/// <summary>
|
||||
/// 单利日终计息(纯函数,替换 SwapDealService.CalcDailySimpleInterestByEod 的"纯数学"部分)。
|
||||
/// </summary>
|
||||
public static InterestResult AccrueSimpleEod(
|
||||
decimal priorUnrealized,
|
||||
decimal priorAccrualPrincipal,
|
||||
decimal positionPrincipal,
|
||||
decimal closeRatio,
|
||||
decimal originalPv,
|
||||
FundingLegRate rate,
|
||||
AccrualPolicy policy,
|
||||
DateTime eodDate,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
var baseTdInterestPrincipal = priorAccrualPrincipal + positionPrincipal - originalPv;
|
||||
var baseInterestPrincipal = baseTdInterestPrincipal * closeRatio;
|
||||
|
||||
var combinedRate = rate.AllInRate;
|
||||
var dayInterest = baseInterestPrincipal * combinedRate;
|
||||
var tdInterest = baseTdInterestPrincipal * combinedRate;
|
||||
if (policy.IsAnnualized)
|
||||
{
|
||||
dayInterest /= policy.AnnualDays;
|
||||
tdInterest /= policy.AnnualDays;
|
||||
}
|
||||
|
||||
var totalUnrealized = priorUnrealized + dayInterest;
|
||||
var result = new InterestResult(
|
||||
Math.Round(totalUnrealized, SwapInterest.FundingLegPrecision, MidpointRounding.AwayFromZero),
|
||||
Math.Round(tdInterest, SwapInterest.FundingLegPrecision, MidpointRounding.AwayFromZero));
|
||||
|
||||
trace?.Day(0, eodDate, combinedRate, baseInterestPrincipal, dayInterest, totalUnrealized);
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复利日终计息(纯函数,替换 CalcDailyCompoundInterestByEod 的"纯数学"部分)。
|
||||
///
|
||||
/// EOD 只算一天,按当日是否为重置日分两条路径:
|
||||
/// - 重置日(isResetDay=true):计息本金 = positionPrincipal + priorUnrealized × remainingPercent(利息并入本金)
|
||||
/// - 非重置日:计息本金 = priorAccrualPrincipal + positionPrincipal - originalPv(差分)
|
||||
///
|
||||
/// remainingPercent = principal / posiPrincipal(由调用方算好传入,对应旧代码 :1505-1508)。
|
||||
/// </summary>
|
||||
public static InterestResult AccrueCompoundEod(
|
||||
decimal priorUnrealized,
|
||||
decimal priorAccrualPrincipal,
|
||||
decimal positionPrincipal,
|
||||
decimal closeRatio,
|
||||
decimal originalPv,
|
||||
FundingLegRate rate,
|
||||
AccrualPolicy policy,
|
||||
bool isResetDay,
|
||||
decimal remainingPercent,
|
||||
DateTime eodDate,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
// 重置日:利息并入本金;非重置日:差分本金
|
||||
var baseTdInterestPrincipal = isResetDay
|
||||
? positionPrincipal + priorUnrealized * remainingPercent
|
||||
: priorAccrualPrincipal + positionPrincipal - originalPv;
|
||||
var baseInterestPrincipal = baseTdInterestPrincipal * closeRatio;
|
||||
|
||||
var combinedRate = rate.AllInRate;
|
||||
var dayInterest = baseInterestPrincipal * combinedRate;
|
||||
var tdInterest = baseTdInterestPrincipal * combinedRate;
|
||||
if (policy.IsAnnualized)
|
||||
{
|
||||
dayInterest /= policy.AnnualDays;
|
||||
tdInterest /= policy.AnnualDays;
|
||||
}
|
||||
|
||||
// 旧代码(两分支相同):InterestAmount = priorUnrealized × closePercent + dayInterest
|
||||
// TdInterestAmount = dayInterest(不含 priorUnrealized)
|
||||
var totalUnrealized = priorUnrealized * closeRatio + dayInterest;
|
||||
var result = new InterestResult(
|
||||
Math.Round(totalUnrealized, SwapInterest.FundingLegPrecision, MidpointRounding.AwayFromZero),
|
||||
Math.Round(tdInterest, SwapInterest.FundingLegPrecision, MidpointRounding.AwayFromZero));
|
||||
|
||||
trace?.Day(0, eodDate, combinedRate, baseInterestPrincipal, dayInterest, totalUnrealized);
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单利多日计息(纯函数,替换 CalcDailySimpleInterest 的"纯数学"部分)。
|
||||
///
|
||||
/// 单利特征:计息本金全程恒定(差分公式 = priorAccrualPrincipal + positionPrincipal - originalPv)。
|
||||
/// 按重置日分段,每段用对应利率算天数×日利息(无逐日循环,等价于 SwapInterest.AccrueSimple 分段累加)。
|
||||
///
|
||||
/// 利率变化点由调用方通过 segmentRates 传入(已取好 FR007),本方法不取价。
|
||||
/// </summary>
|
||||
/// <param name="priorUnrealized">上一日终累计待实现利息(preEod.InterestProfitSum × closeRatio)。</param>
|
||||
/// <param name="accrualPrincipal">计息本金(差分,全程恒定)。</param>
|
||||
/// <param name="closeRatio">平仓比例。</param>
|
||||
/// <param name="segmentRates">分段利率表:(段起日, all-in利率),按日期升序。</param>
|
||||
/// <param name="startDate">计息开始日(PosiStartDate)。</param>
|
||||
/// <param name="endDate">计息结束日(平仓日)。</param>
|
||||
/// <param name="priorValueDate">上一日终归档日(只算此日之后的利息)。</param>
|
||||
/// <param name="boundary">算头算尾。</param>
|
||||
/// <param name="annualDays">年化天数。</param>
|
||||
/// <param name="isAnnualized">是否年化。</param>
|
||||
public static InterestResult AccrueSimplePeriod(
|
||||
decimal priorUnrealized,
|
||||
decimal accrualPrincipal,
|
||||
decimal closeRatio,
|
||||
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
DateTime priorValueDate,
|
||||
AccrualBoundary boundary,
|
||||
int annualDays,
|
||||
bool isAnnualized)
|
||||
{
|
||||
var displayPrincipal = accrualPrincipal * closeRatio;
|
||||
decimal interest = priorUnrealized;
|
||||
decimal tdInterest = priorUnrealized;
|
||||
var precision = SwapInterest.FundingLegPrecision;
|
||||
|
||||
// 按段累加:每段内利率恒定,用 AccrualDays 算天数 × 日利息
|
||||
var segStart = startDate;
|
||||
var segIncludeStart = boundary.IncludeStart;
|
||||
|
||||
for (int si = 0; si < segmentRates.Count; si++)
|
||||
{
|
||||
var (segRateStart, segRate) = segmentRates[si];
|
||||
var segEnd = si < segmentRates.Count - 1
|
||||
? segmentRates[si + 1].StartDate
|
||||
: endDate;
|
||||
|
||||
// 跳过 priorValueDate 之前的日期(续接上一日终)
|
||||
var effectiveStart = segStart > priorValueDate ? segStart : priorValueDate.AddDays(1);
|
||||
if (effectiveStart > segEnd) { segStart = segEnd; continue; }
|
||||
|
||||
// 算头算尾:首段用 boundary.IncludeStart,后续段不算头
|
||||
var segBoundary = AccrualBoundary.Of(segIncludeStart, segEnd == endDate && boundary.IncludeEnd);
|
||||
var days = SwapInterest.AccrualDays(effectiveStart, segEnd, segBoundary);
|
||||
if (days <= 0) { segStart = segEnd; segIncludeStart = false; continue; }
|
||||
|
||||
var dailyRate = isAnnualized ? segRate / annualDays : segRate;
|
||||
var daily = Math.Round(displayPrincipal * dailyRate, precision, MidpointRounding.AwayFromZero);
|
||||
var segInterest = Math.Round(daily * days, precision, MidpointRounding.AwayFromZero);
|
||||
|
||||
interest += segInterest;
|
||||
tdInterest += segInterest;
|
||||
|
||||
segStart = segEnd;
|
||||
segIncludeStart = false;
|
||||
}
|
||||
|
||||
return new InterestResult(
|
||||
Math.Round(interest, precision, MidpointRounding.AwayFromZero),
|
||||
Math.Round(tdInterest, precision, MidpointRounding.AwayFromZero));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
/// 融资腿在某一计息日生效的利率(不可变值对象)。
|
||||
///
|
||||
/// <para>计息只认一个数:<see cref="AllInRate"/>(当日生效年利率)。
|
||||
/// 固定腿与浮动腿的区别是"取率"环节的事,已在 <c>SwapDealService.CalcDailySimpleInterestByEod</c>
|
||||
/// 收敛成 all-in 数;本结构不再为腿型背负四个字段——利息计算不是互换特有的,
|
||||
/// 固定利率就是一个 <see cref="decimal"/>。</para>
|
||||
/// </summary>
|
||||
public readonly struct FundingLegRate
|
||||
{
|
||||
/// <summary>当日生效年利率(all-in)。固定腿=固定利率;浮动腿=加点利差+指数定盘。</summary>
|
||||
public decimal Rate { get; }
|
||||
|
||||
/// <summary>计息用的当日生效年利率。即 <see cref="Rate"/>。</summary>
|
||||
public decimal AllInRate => Rate;
|
||||
|
||||
private FundingLegRate(decimal rate)
|
||||
=> Rate = rate;
|
||||
|
||||
/// <summary>构造固定腿利率。</summary>
|
||||
public static FundingLegRate Fixed(decimal fixedRate)
|
||||
=> new(fixedRate);
|
||||
|
||||
/// <summary>构造浮动腿利率(all-in = 加点利差 + 指数定盘)。</summary>
|
||||
public static FundingLegRate Floating(decimal spread, decimal indexFixing)
|
||||
=> new(spread + indexFixing);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using YLErp.Core.Interest;
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Margin;
|
||||
@@ -38,5 +39,5 @@ public sealed class MarginAccount
|
||||
/// <param name="boundary">算头算尾规则。</param>
|
||||
/// <param name="annualDays">年化天数(365 或 360)。</param>
|
||||
public InterestResult AccrueInterest(decimal rate, System.DateTime startDate, System.DateTime endDate, AccrualBoundary boundary, int annualDays)
|
||||
=> SwapInterest.AccrueSimple(Balance.Balance, rate, startDate, endDate, boundary, annualDays);
|
||||
=> SwapInterest.AccrueSimple(new AccrualContext(annualDays), Balance.Balance, rate, startDate, endDate, boundary);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
/// <summary>
|
||||
/// 保证金计算工具。从 SwapDealService 剥离的保证金纯函数。
|
||||
/// </summary>
|
||||
public static class MarginCalc
|
||||
{
|
||||
/// <summary>
|
||||
/// 取上一日终保证金余额。
|
||||
///
|
||||
/// 三级优先级(均有测试覆盖,不可简化):
|
||||
/// 1. InterestPrincipalFix——EOD 归档赋值时的首选字段
|
||||
/// 2. TdInterestPrincipal——部分 EOD 归档路径未写 InterestPrincipalFix(值为0),
|
||||
/// 此时 TdInterestPrincipal 含正确的保证金本金(生产 eod_swap_position(35774) 即此场景)
|
||||
/// 3. fallback——首次平仓(preEod.id==0, 无归档)时用当日保证金余额
|
||||
/// </summary>
|
||||
public static decimal PreviousBalance(eod_swap_position preEod, decimal fallback)
|
||||
{
|
||||
var previous = preEod.InterestPrincipalFix != 0m
|
||||
? preEod.InterestPrincipalFix
|
||||
: preEod.TdInterestPrincipal;
|
||||
return preEod.id != 0 && previous != 0m ? previous : fallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保证金方向翻转。保证金利息是券商付给客户(方向与融资腿相反)。
|
||||
/// </summary>
|
||||
public static int FlipDirection(int direction)
|
||||
=> direction == (int)SwapDirectionEnum.收取
|
||||
? (int)SwapDirectionEnum.支付
|
||||
: (int)SwapDirectionEnum.收取;
|
||||
|
||||
/// <summary>
|
||||
/// 汇总保证金腿的平仓返还金额(SwapMarginAmount)和保证金返息(SwapMarginRebatePnl)。
|
||||
/// 保证金方向与融资腿相反:interestRatio = InterestDirection==1 ? -1 : 1。
|
||||
/// </summary>
|
||||
public static void AccumulateSettlement(List<swap_flow_event> interestList, UnwindData unwindData)
|
||||
{
|
||||
foreach (var x in interestList)
|
||||
{
|
||||
if (!MarginModes.Contains(x.InterestMode)) continue;
|
||||
var interestRatio = x.InterestDirection == 1 ? -1m : 1m;
|
||||
unwindData.SwapMarginRebatePnl += x.InterestClosePnL;
|
||||
unwindData.SwapMarginAmount += x.InterestPrincipal * interestRatio;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace YLErp.Modules.SwapModule.ReturnLegs;
|
||||
|
||||
/// <summary>
|
||||
/// 持仓价值(SwapPositionValue)汇总。
|
||||
///
|
||||
/// SwapPositionValue = 利息端收益 + 浮动端收益。
|
||||
/// 利息端可能带方向因子(收取/支付),浮动端不含方向因子。
|
||||
/// 原代码在 SwapEodPositionService 8处重复此公式(1115/1258/1519/1647/1746/1830/1938/2113)。
|
||||
/// </summary>
|
||||
public static class PositionValueCalc
|
||||
{
|
||||
/// <summary>持仓价值 = 利息端 × 方向因子 + 浮动端。ratio: 收取=1, 支付=-1。</summary>
|
||||
public static decimal Calc(decimal interestProfitSum, decimal posiProfitSum, int ratio = 1)
|
||||
=> interestProfitSum * ratio + posiProfitSum;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using YLErp.Core.Interest;
|
||||
using YLErp.Helpers;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
@@ -83,5 +84,17 @@ namespace YLErp.Modules.SwapModule
|
||||
public static string Dump() => string.Join(Environment.NewLine, GlobalLines);
|
||||
|
||||
public static string DumpForRequest() => _reqBuf.Value?.ToString() ?? "";
|
||||
|
||||
/// <summary>
|
||||
/// 把纯函数产出的 <see cref="AccrualTrace"/> 常驻落盘(关键路径日志)。
|
||||
/// 每条目经 <see cref="Critical"/> 写出——<b>无条件</b>落盘,与开关无关;
|
||||
/// 开关打开时同时进内存 buffer 供实时查看 / 单测断言。这是事后 diff 新旧引擎的主通道。
|
||||
/// </summary>
|
||||
public static void Write(AccrualTrace? trace)
|
||||
{
|
||||
if (trace == null) return;
|
||||
foreach (var entry in trace.Entries)
|
||||
Critical(entry.Line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ using Newtonsoft.Json;
|
||||
using YLErp.BLL;
|
||||
using YLErp.BLL.Eod;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Core.Interest;
|
||||
using YLErp.Derivatives.Interest;
|
||||
using YLErp.Helpers;
|
||||
using YLErp.Modules.DataProviderModule;
|
||||
using YLErp.Modules.EodModule;
|
||||
using YLErp.Modules.SwapModule.Accrual;
|
||||
using YLErp.Modules.SwapModule.FundingLegs;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
using YLErp.Modules.SwapModule.ReturnLegs;
|
||||
@@ -113,8 +115,8 @@ namespace YLErp.Modules.SwapModule
|
||||
return unwindData.ClosePercent == 1 || (remainingNotional == 0 && remainingQuantity == 0);
|
||||
}
|
||||
|
||||
// 待实现利息会进入 decimal(30,12) 日终快照
|
||||
private const int InterestCalculationPrecision = 12;
|
||||
// 待实现利息会进入 decimal(30,12) 日终快照;精度常量统一引用 SwapInterest.FundingLegPrecision,消除重复定义。
|
||||
private const int InterestCalculationPrecision = SwapInterest.FundingLegPrecision;
|
||||
|
||||
/// <summary>
|
||||
/// 手工平仓、手工互换及收益结算的利息事件按金额两位落库。
|
||||
@@ -866,7 +868,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
if (MarginModes.Contains(position.InterestMode))
|
||||
{
|
||||
positionClone.InterestDirection = FlipMarginDirection(position.InterestDirection);
|
||||
positionClone.InterestDirection = MarginCalc.FlipDirection(position.InterestDirection);
|
||||
}
|
||||
|
||||
// 获取利率
|
||||
@@ -1136,55 +1138,28 @@ namespace YLErp.Modules.SwapModule
|
||||
///
|
||||
/// 待迁入 Margin 模块:保证金独立计息入口建好后,此方法移入 MarginAccount/MarginService。
|
||||
/// </summary>
|
||||
private decimal ResolveMarginOrginPv(swap_position position, eod_swap_position preEodPosition, decimal fallback)
|
||||
{
|
||||
var previousBalance = preEodPosition.InterestPrincipalFix != 0m
|
||||
? preEodPosition.InterestPrincipalFix
|
||||
: preEodPosition.TdInterestPrincipal;
|
||||
return preEodPosition.id != 0 && previousBalance != 0m
|
||||
? previousBalance
|
||||
: fallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保证金腿的利息方向翻转。保证金利息是券商付给客户(方向与融资腿相反)。
|
||||
/// 待迁入 Margin 模块。
|
||||
/// </summary>
|
||||
private static int FlipMarginDirection(int direction)
|
||||
=> direction == (int)SwapDirectionEnum.收取
|
||||
? (int)SwapDirectionEnum.支付
|
||||
: (int)SwapDirectionEnum.收取;
|
||||
|
||||
/// <summary>
|
||||
/// 汇总保证金腿的平仓返还金额(SwapMarginAmount)和保证金返息(SwapMarginRebatePnl)。
|
||||
/// 保证金方向与融资腿相反:interestRatio = InterestDirection==1 ? -1 : 1。
|
||||
/// 待迁入 Margin 模块。
|
||||
/// </summary>
|
||||
private static void AccumulateMarginSettlement(List<swap_flow_event> interestList, UnwindData unwindData)
|
||||
{
|
||||
foreach (var x in interestList)
|
||||
{
|
||||
if (!MarginModes.Contains(x.InterestMode)) continue;
|
||||
var interestRatio = x.InterestDirection == 1 ? -1m : 1m;
|
||||
unwindData.SwapMarginRebatePnl += x.InterestClosePnL;
|
||||
unwindData.SwapMarginAmount += x.InterestPrincipal * interestRatio;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入保证金的资金记录:应付预付金(SwapMarginAmount)和预付金返息(SwapMarginRebatePnl)。
|
||||
/// 待迁入 Margin 模块。
|
||||
/// 依赖实例方法 AddClientCash/AddClientCashInCashOut,暂留此处。
|
||||
/// </summary>
|
||||
private void RecordMarginCashFlow(trade td, UnwindData unwindData)
|
||||
=> RecordMarginCashFlow(td, unwindData.ValueDate,
|
||||
unwindData.SwapMarginAmount, unwindData.SwapMarginRebatePnl,
|
||||
AddClientCashInCashOut);
|
||||
|
||||
/// <summary>
|
||||
/// 写入保证金资金记录的通用重载,接受资金写入委托。
|
||||
/// AddClientCash(virtual,测试可stub) 和 AddClientCashInCashOut(非virtual,直接写库)
|
||||
/// 都可通过此重载统一。
|
||||
/// </summary>
|
||||
private void RecordMarginCashFlow(trade td, DateTime valueDate,
|
||||
decimal marginAmount, decimal marginRebate,
|
||||
Func<trade, double, string, DateTime, int> writeCash)
|
||||
{
|
||||
if (unwindData.SwapMarginAmount != 0)
|
||||
{
|
||||
AddClientCashInCashOut(td, Convert.ToDouble(unwindData.SwapMarginAmount), ClientCashInCashOut.系统操作_应付预付金, unwindData.ValueDate);
|
||||
}
|
||||
if (unwindData.SwapMarginRebatePnl != 0)
|
||||
{
|
||||
AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate);
|
||||
}
|
||||
if (marginAmount != 0)
|
||||
writeCash(td, Convert.ToDouble(marginAmount), ClientCashInCashOut.系统操作_应付预付金, valueDate);
|
||||
if (marginRebate != 0)
|
||||
writeCash(td, Convert.ToDouble(-marginRebate), ClientCashInCashOut.系统操作_预付金返息, valueDate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1243,7 +1218,7 @@ namespace YLErp.Modules.SwapModule
|
||||
// 保证金腿的 orginPv 对齐到保证金本金维度,避免差分公式维度不匹配算出巨负值
|
||||
if (MarginModes.Contains(position.InterestMode))
|
||||
{
|
||||
orginPv = ResolveMarginOrginPv(position, preEodPosition, position.InterestPrincipalFix);
|
||||
orginPv = MarginCalc.PreviousBalance(preEodPosition, position.InterestPrincipalFix);
|
||||
}
|
||||
|
||||
if (swap)
|
||||
@@ -1585,49 +1560,59 @@ namespace YLErp.Modules.SwapModule
|
||||
/// </summary>
|
||||
public void CalcDailySimpleInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, ref decimal InterestAmount, ref decimal TdInterestAmount)
|
||||
{
|
||||
decimal interestProfitSum = preEodPosition.InterestProfitSum;
|
||||
int interestPeriod = position.interest_rest_days ?? 1;
|
||||
double floatRate = Convert.ToDouble(floateRate);
|
||||
var calcDays = (endDate - tradeDate).Days;
|
||||
// 修复:首次操作时(preEodPosition.id == 0),TdInterestPrincipal 需要正确初始化
|
||||
// 首次操作(preEod.id == 0):计息基数按存量本金初始化——保留旧行为(含对 preEod 的就地修正)。
|
||||
if (preEodPosition.id == 0)
|
||||
{
|
||||
preEodPosition.TdInterestPrincipal = posiPrincipal;
|
||||
}
|
||||
|
||||
// 检查是否到达重置周期
|
||||
if (calcDays % interestPeriod == 0)
|
||||
// 取率:重置日按 interest_rule 重新定盘浮动利率(GLMS-JIATT-20260805 根因——
|
||||
// 重置日=平仓日必须用新利率,否则沿用旧周期利率并污染后续 EOD)。
|
||||
decimal effectiveFloat = floateRate;
|
||||
int interestPeriod = position.interest_rest_days ?? 1;
|
||||
if ((endDate - tradeDate).Days % interestPeriod == 0
|
||||
&& !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
|
||||
{
|
||||
// 获取新的浮动利率
|
||||
if (!string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
|
||||
var fixingDate = IndexFixerBase.GetFixingDate(endDate, position.interest_rule);
|
||||
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
|
||||
{
|
||||
var fixingDate = IndexFixerBase.GetFixingDate(endDate, position.interest_rule);
|
||||
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
|
||||
{
|
||||
if (fixing != 0m) floatRate = Convert.ToDouble(fixing);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
|
||||
}
|
||||
if (fixing != 0m) effectiveFloat = fixing;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
|
||||
}
|
||||
}
|
||||
|
||||
flowEvent.FloatRate = Convert.ToDecimal(floatRate);
|
||||
var baseTdInterestPrincipal = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv;
|
||||
var baseInterestPrincipal = baseTdInterestPrincipal * closePercent;
|
||||
flowEvent.FloatRate = effectiveFloat;
|
||||
|
||||
// 修复:正确计算本次利息(基于实际持仓本金)
|
||||
decimal interest = baseInterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
|
||||
decimal tdinterest = baseTdInterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
|
||||
if (position.IsAnnualized)
|
||||
{
|
||||
interest /= annualDays;
|
||||
tdinterest /= annualDays;
|
||||
}
|
||||
|
||||
InterestAmount = Math.Round(interestProfitSum + interest, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
|
||||
TdInterestAmount = Math.Round(tdinterest, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
|
||||
// 纯数学下沉至 FundingLegAccrual(DDD 命名 + 末位生产精度 12 舍入),行为与上版逐字对齐。
|
||||
// 利率构成按腿型封装:固定腿 → FixedRate;浮动腿 → Spread + IndexFixing(沿用旧实现 InterestRate+浮动利率 的口径)。
|
||||
var isFixedLeg = string.IsNullOrEmpty(position.FloatRateUnderlyingCode);
|
||||
var legRate = isFixedLeg
|
||||
? FundingLegRate.Fixed(flowEvent.InterestRate)
|
||||
: FundingLegRate.Floating(flowEvent.InterestRate, effectiveFloat);
|
||||
var accrualPolicy = new AccrualPolicy(
|
||||
convention: AccrualBoundary.Both,
|
||||
isCompound: false,
|
||||
resetPeriodDays: position.interest_rest_days ?? 1,
|
||||
annualDays: annualDays,
|
||||
isAnnualized: position.IsAnnualized);
|
||||
// 完整计息 trace:收集器由适配器创建,随后经 SwapCalcTrace 常驻落盘(关键路径日志,无条件)。
|
||||
var interestTrace = new AccrualTrace();
|
||||
var result = FundingLegAccrual.AccrueSimpleEod(
|
||||
priorUnrealized: preEodPosition.InterestProfitSum,
|
||||
priorAccrualPrincipal: preEodPosition.TdInterestPrincipal,
|
||||
positionPrincipal: posiPrincipal,
|
||||
closeRatio: closePercent,
|
||||
originalPv: orginPv,
|
||||
rate: legRate,
|
||||
policy: accrualPolicy,
|
||||
eodDate: endDate,
|
||||
trace: interestTrace);
|
||||
InterestAmount = result.Accrued;
|
||||
TdInterestAmount = result.AccruedToday;
|
||||
SwapCalcTrace.Write(interestTrace);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1658,14 +1643,7 @@ namespace YLErp.Modules.SwapModule
|
||||
ExecuteInTransaction(() =>
|
||||
{
|
||||
int clientCashId = AddClientCash(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut.系统操作_平仓费, unwindData.ValueDate);
|
||||
if (unwindData.SwapMarginAmount != 0)
|
||||
{
|
||||
AddClientCash(td, Convert.ToDouble(unwindData.SwapMarginAmount), ClientCashInCashOut.系统操作_应付预付金, unwindData.ValueDate);
|
||||
}
|
||||
//if (unwindData.SwapMarginRebatePnl != 0)
|
||||
//{
|
||||
// AddClientCash(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate);
|
||||
//}
|
||||
RecordMarginCashFlow(td, unwindData.ValueDate, unwindData.SwapMarginAmount, 0m, AddClientCash);
|
||||
DealFloatPosition(unwindData);
|
||||
var flowList = new List<swap_flow_event>(unwindData.FlowEvents);
|
||||
var eventId = SaveSwapDeal(unwindData, (int)SwapEventTypeEnum.平仓, clientCashId, "系统操作_平仓");
|
||||
@@ -1941,7 +1919,7 @@ namespace YLErp.Modules.SwapModule
|
||||
else if (MarginModes.Contains(item.InterestMode))
|
||||
{
|
||||
_closePosiNotionalValue = 0;
|
||||
positionClone.InterestDirection = FlipMarginDirection(position.InterestDirection);
|
||||
positionClone.InterestDirection = MarginCalc.FlipDirection(position.InterestDirection);
|
||||
}
|
||||
decimal rate = item.InterestRateDefault;
|
||||
if (swapIntervalToday != null)//当日无适用观察日
|
||||
@@ -1981,14 +1959,7 @@ namespace YLErp.Modules.SwapModule
|
||||
private void DealUnwind(UnwindData unwindData, trade td, string actionMsg = "系统操作_自动平仓")
|
||||
{
|
||||
int clientCashId = AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut.系统操作_平仓费, unwindData.ValueDate);
|
||||
if (unwindData.SwapMarginAmount != 0)
|
||||
{
|
||||
AddClientCashInCashOut(td, Convert.ToDouble(unwindData.SwapMarginAmount), ClientCashInCashOut.系统操作_应付预付金, unwindData.ValueDate);
|
||||
}
|
||||
//if (unwindData.SwapMarginRebatePnl != 0)
|
||||
//{
|
||||
// AddClientCashInCashOut(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate);
|
||||
//}
|
||||
RecordMarginCashFlow(td, unwindData.ValueDate, unwindData.SwapMarginAmount, 0m, AddClientCashInCashOut);
|
||||
var flowList = new List<swap_flow_event>(unwindData.FlowEvents);
|
||||
var eventId = SaveSwapDeal(unwindData, (int)SwapEventTypeEnum.平仓, clientCashId, actionMsg);
|
||||
if (unwindData.CloseMethod == (int)CloseMethodEnum.全部平仓)
|
||||
@@ -2029,7 +2000,7 @@ namespace YLErp.Modules.SwapModule
|
||||
unwindData.SwapMarginAmount = 0;
|
||||
if (interestList != null)
|
||||
{
|
||||
AccumulateMarginSettlement(interestList.ToList(), unwindData);
|
||||
MarginCalc.AccumulateSettlement(interestList.ToList(), unwindData);
|
||||
interestList.ForEach(x =>
|
||||
{
|
||||
unwindData.SwapRealizedPnL += x.InterestClosePnL;
|
||||
@@ -2137,10 +2108,7 @@ namespace YLErp.Modules.SwapModule
|
||||
ExecuteInTransaction(() =>
|
||||
{
|
||||
int clientCashId = AddClientCash(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut.系统操作_互换, unwindData.ValueDate);
|
||||
if (unwindData.SwapMarginRebatePnl != 0)
|
||||
{
|
||||
AddClientCash(td, Convert.ToDouble(-unwindData.SwapMarginRebatePnl), ClientCashInCashOut.系统操作_预付金返息, unwindData.ValueDate);
|
||||
}
|
||||
RecordMarginCashFlow(td, unwindData.ValueDate, 0m, unwindData.SwapMarginRebatePnl, AddClientCash);
|
||||
foreach (var item in unwindData.FlowEvents)
|
||||
{
|
||||
item.OptLog = "手工操作";
|
||||
@@ -2207,9 +2175,9 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
string action = eventType == (int)SwapEventTypeEnum.互换 ? ClientCashInCashOut.系统操作_互换 : ClientCashInCashOut.系统操作_平仓费;
|
||||
int clientCashId = AddClientCash(td, Convert.ToDouble(-swapEvent.unwindData.SwapRealizedPnL), action, swapEvent.unwindData.ValueDate);
|
||||
if (eventType == (int)SwapEventTypeEnum.平仓 && swapEvent.unwindData.SwapMarginAmount != 0)
|
||||
if (eventType == (int)SwapEventTypeEnum.平仓)
|
||||
{
|
||||
AddClientCash(td, Convert.ToDouble(swapEvent.unwindData.SwapMarginAmount), ClientCashInCashOut.系统操作_应付预付金, swapEvent.unwindData.ValueDate);
|
||||
RecordMarginCashFlow(td, swapEvent.unwindData.ValueDate, swapEvent.unwindData.SwapMarginAmount, 0m, AddClientCash);
|
||||
}
|
||||
swapEvent.ClientCashId = clientCashId;
|
||||
td.UnWindDate = swapEvent.unwindData.UnwindDate;
|
||||
|
||||
@@ -1112,7 +1112,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum;
|
||||
//持仓价值
|
||||
newEodPayPosition.SwapPositionValue = newEodPayPosition.InterestProfitSum * ratio + newEodPayPosition.PosiProfitSum;
|
||||
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio);
|
||||
|
||||
//累计已实现
|
||||
newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio;
|
||||
@@ -1255,7 +1255,7 @@ namespace YLErp.Modules.SwapModule
|
||||
: RoundEodInterest(eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee);
|
||||
newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum;
|
||||
//持仓价值
|
||||
newEodPayPosition.SwapPositionValue = newEodPayPosition.InterestProfitSum * ratio + newEodPayPosition.PosiProfitSum;
|
||||
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio);
|
||||
|
||||
//累计已实现
|
||||
newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio;
|
||||
@@ -1516,7 +1516,7 @@ namespace YLErp.Modules.SwapModule
|
||||
// InterestProfitSum 是利息腿待实现总额,包含利息和费用;无费用时等于 InterestIncomeSum。
|
||||
newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum;
|
||||
//持仓价值
|
||||
newEodPayPosition.SwapPositionValue = newEodPayPosition.InterestProfitSum * ratio + newEodPayPosition.PosiProfitSum;
|
||||
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio);
|
||||
|
||||
Log.Info($"InterestIncomeSum is {eodPayPosition.InterestIncomeSum},TdInterestIncome is {newEodPayPosition.TdInterestIncome}" +
|
||||
$",TdCloseInterest is {newEodPayPosition.TdCloseInterest}");
|
||||
@@ -1644,7 +1644,7 @@ namespace YLErp.Modules.SwapModule
|
||||
newEodPayPosition.InterestFeeSum = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee;
|
||||
newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum;
|
||||
//持仓价值
|
||||
newEodPayPosition.SwapPositionValue = newEodPayPosition.InterestProfitSum * ratio + newEodPayPosition.PosiProfitSum;
|
||||
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio);
|
||||
|
||||
//累计已实现
|
||||
newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio;
|
||||
@@ -1738,12 +1738,12 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
//持仓内容-浮动收益腿-损益统计(本方视角
|
||||
newEodPayPosition.TdPosiDividend = Math.Round(dividendIn * ratio, 2);
|
||||
newEodPayPosition.PosiMtmPnL = MtmCalc.UnrealizedPnl(newEodPayPosition.UnderlyingPrice, newEodPayPosition.PosiGrossPrice, newEodPayPosition.PosiQuantity, newEodPayPosition.ContractSize, shortRatio, ratio);
|
||||
newEodPayPosition.PosiMtmPnL = MtmCalc.UnrealizedPnl(newEodPayPosition.UnderlyingPrice, newEodPayPosition.PosiGrossPrice, newEodPayPosition.PosiQuantity, newEodPayPosition.ContractSize, shortRatio, (int)ratio);
|
||||
newEodPayPosition.PosiDividendSum = Math.Round(newEodPayPosition.TdPosiDividend - newEodPayPosition.TdCloseDividend, 2);
|
||||
newEodPayPosition.PosiProfitSum = newEodPayPosition.PosiMtmPnL + newEodPayPosition.PosiDividendSum + newEodPayPosition.PosiFeePending;
|
||||
|
||||
//持仓价值
|
||||
newEodPayPosition.SwapPositionValue = newEodPayPosition.InterestProfitSum + newEodPayPosition.PosiProfitSum;
|
||||
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum);
|
||||
//累计已实现
|
||||
newEodPayPosition.RealizedFee = closeFee;
|
||||
newEodPayPosition.RealizedMtmPnL = newEodPayPosition.TdCloseMtmPnl;
|
||||
@@ -1827,7 +1827,7 @@ namespace YLErp.Modules.SwapModule
|
||||
, seekPreday: true, currencyRateType: curretEod.PosiDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell);
|
||||
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
|
||||
//持仓价值
|
||||
curretEod.SwapPositionValue = curretEod.InterestProfitSum + curretEod.PosiProfitSum;
|
||||
curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum);
|
||||
UpdateDbOption(curretEod);
|
||||
curretEod.Invalid = false;
|
||||
if (curretEod.id == 0)
|
||||
@@ -1935,7 +1935,7 @@ namespace YLErp.Modules.SwapModule
|
||||
, seekPreday: true, currencyRateType: curretEod.PosiDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell);
|
||||
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
|
||||
//持仓价值
|
||||
curretEod.SwapPositionValue = curretEod.InterestProfitSum + curretEod.PosiProfitSum;
|
||||
curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum);
|
||||
UpdateDbOption(curretEod);
|
||||
curretEod.Invalid = false;
|
||||
if (curretEod.id == 0)
|
||||
@@ -2110,7 +2110,7 @@ namespace YLErp.Modules.SwapModule
|
||||
curretEod.PosiNotionalValue = 0;
|
||||
}
|
||||
//持仓价值
|
||||
curretEod.SwapPositionValue = curretEod.InterestProfitSum + curretEod.PosiProfitSum;
|
||||
curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum);
|
||||
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value
|
||||
, seekPreday: true, currencyRateType: curretEod.PosiDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell);
|
||||
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
|
||||
|
||||
@@ -322,7 +322,7 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
#endregion
|
||||
|
||||
public int AddClientCashInCashOut(OtcTradeBase td, double amount, string action, DateTime valueDate)
|
||||
public virtual int AddClientCashInCashOut(OtcTradeBase td, double amount, string action, DateTime valueDate)
|
||||
{
|
||||
var cl = DataCacheProvider.GetClientDataSource().GetData(td.ClientId);
|
||||
|
||||
|
||||
@@ -943,7 +943,7 @@ namespace YLErp.Modules.SwapModule
|
||||
throw new ServiceException("标的信息不存在:" + req.UnderlyingCode);
|
||||
}
|
||||
|
||||
if (_underlying.IsFutures() && _underlying.MaturityDate == null)
|
||||
if (_underlying.RequiresMaturityDate() && _underlying.MaturityDate == null)
|
||||
{
|
||||
throw new ServiceException("标的到期日不存在:" + req.UnderlyingCode);
|
||||
}
|
||||
@@ -952,7 +952,7 @@ namespace YLErp.Modules.SwapModule
|
||||
req.VarietyId = _underlying.UnderlyingTypeId;
|
||||
req.UnderlyingAssetClass = _underlying.UnderlyingType;
|
||||
req.UnderlyingAssetName = _underlying.UnderlyingName;
|
||||
req.MaturityDate = _underlying.IsFutures() ? _underlying.MaturityDate : null;
|
||||
req.MaturityDate = _underlying.RequiresMaturityDate() ? _underlying.MaturityDate : null;
|
||||
req.UnderlyingInstrumentType = _underlying.GetMainType();
|
||||
req.CountRatio = _underlying.CountRatio;
|
||||
|
||||
|
||||
@@ -142,6 +142,49 @@ curretEod.PosiQuantity = qty < 0 ? 0 : Math.Abs(qty);
|
||||
|
||||
---
|
||||
|
||||
## 4.1 阶段1 落地形态建议:独立 `CorporateActions` 模块(新增建议 · **待深度验证与评审**)
|
||||
|
||||
> ⚠️ 以下为**架构建议草案**,尚未经过逐文件源码复核与评审。仅作方向性参考,落地前须:
|
||||
> 1. 逐文件确认现有 `DividendService` / `BondPaymentService` / `ex_dividend_info` 的调用边,避免重复造轮子;
|
||||
> 2. 确认 `YLErp.Core` 是否合适承载(须被 OMS / 期权 / TRS 多程序集引用,不能反向依赖业务层);
|
||||
> 3. 与**利息核心(复利/部分平仓)**明确划界——见下方"边界警示"。
|
||||
|
||||
### 4.1.1 为什么必须新模块,而不是往现有屎山堆
|
||||
|
||||
- 现有 `SwapDealService` / `SwapEodPositionService` 已高度耦合(计息、平仓、EOD 递推、公司行为全搅在一起),继续往里加 if/else 只会放大"隐式不变量跨函数跨日不可见"的风险(这正是"测试绿但全错"的温床)。
|
||||
- 公司行为域(除权除息 / 复权 / 付息 / 分红 / 拆股 / 送股 / 配股)在 **OMS、期权、TRS** 多个业务都要用,**必须抽到共享核心程序集**,各业务只消费、不各写一份。
|
||||
|
||||
### 4.1.2 推荐目录形态
|
||||
|
||||
```
|
||||
YLErp.Core / CorporateActions/ ← 共享核心,被 OMS/期权/TRS 引用,不反向依赖业务层
|
||||
CorporateAction.cs # 统一实体:actionType + record/ex/effective/payment 四日期 + factor/splitRatio + 金额
|
||||
ICorporateActionSource.cs # 上游数据源适配(聚源付息日历已含全日期,仅做映射)
|
||||
ActionType.cs # 枚举:CashDividend / StockDividend / Split / BondCoupon / RightsIssue / Merger ...
|
||||
handlers/ # 每种行为一个 typed handler(新增行为 = 加类,不动旧代码)
|
||||
CashDividendHandler.cs # 现金分红(含债券 ETF 分红)
|
||||
BondCouponHandler.cs # 债券付息(record 日快照归属)
|
||||
SplitHandler.cs # 拆股
|
||||
StockDividendHandler.cs # 送股
|
||||
RightsIssueHandler.cs # 配股
|
||||
IAdjustmentFactorProvider.cs # 前复权 / 后复权 / 累计 CAF(t)=1(t≥ex)/=ratio(t<ex)
|
||||
EodHooks/ # EOD 收盘链上的接入口(与现有 SwapEodPositionService 解耦的薄适配层)
|
||||
IRecordDateHandler.cs # 登记日收盘:按快照确认 dividendin/accruedCash(金额钉死)
|
||||
IExDateHandler.cs # 除息日:价格自动剔息 + 穿越持仓因子(开盘前第一步,绝不可收盘后补)
|
||||
IPaymentDateHandler.cs # 支付日:纯现金划付,不重算归属
|
||||
```
|
||||
|
||||
### 4.1.3 边界警示(重要,避免域混淆)
|
||||
|
||||
- **债券 ETF 分红 / 付息 = 公司行为域** → 走 `CorporateActions` 模块。
|
||||
- **复利 / 部分平仓 / T+1 本金继承 / 重置日动态本金** = **利息计息域**,现居 `SwapDealService.cs`(`InitSwapDealInterest` / `CalcDailyCompoundInterest` / `CalcUnwindInterest`)+ `SwapEodPositionService.cs`(`SaveAutoEodWithCloseInterestPosition`)。这是**另一回事**,与"公司行为"正交:
|
||||
- 付息(coupon)的**金额**由 corp action 决定(按登记日快照);
|
||||
- 付息的**利息滚存/复利/部分平仓结算**由利息核心决定。
|
||||
- 两者通过 `swap_flow_event`(付息流水)衔接,**不要在 corp action 模块里实现复利逻辑**,也不要在利息核心里硬编码某种公司行为的日期语义。
|
||||
- 当前 `_0808` 分支的 4 个复利部分平仓修复(见分支对比分析)属于**利息核心**,与本模块无关;合并时利息核心以 `_0808` 为准,corp action 模块独立演进。
|
||||
|
||||
---
|
||||
|
||||
## 5. 待确认 / 下一步
|
||||
|
||||
- [ ] 阶段 0 是否现在落地(加字段 + 改 `DealDividends` + 补「登记日快照」回归测试)?
|
||||
|
||||
Reference in New Issue
Block a user