refactor: 提取融资腿计息领域模型 (AccrualPolicy/FundingLegRate/AccrualState/FundingLegAccrual) - 将 CalcDailySimpleInterestByEod 纯数学下沉至 FundingLegAccrual.AccrueSimpleEod, 消除 double 往返转换, 引入值对象收敛散落参数
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
using YLErp.Derivatives.Interest;
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
/// 计息政策(不可变配置)。把"算头算尾 / 单复利率 / 重置频率 / 年化天数"收敛为一处,
|
||||
/// 取代旧代码里散落各处的 calcFirst/calcLast 布尔对与魔法数字。
|
||||
///
|
||||
/// 单/复利不再另立枚举——直接复用既有 DB 枚举 <see cref="InterestTypeEnum"/>(单利=0 / 复利=1),
|
||||
/// 通过 <see cref="IsCompound"/> 暴露为类型安全的 bool,避免与 SwapInterest 已有的
|
||||
/// AccrueSimple/AccrueCompound 方法分裂出"第三种单复利表达"(这是第一版草稿犯过的重复)。
|
||||
/// </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);
|
||||
|
||||
/// <summary>
|
||||
/// 从交易扩展解析(边界适配)。调用方负责从 trade_extend.ExtendObj 取出
|
||||
/// InterestCalcMode / AnnualDays / interest_rest_days 与计息方式后传入;
|
||||
/// 这里只做"布尔对 → AccrualBoundary"与"InterestTypeEnum → bool"的归一。
|
||||
/// </summary>
|
||||
/// <param name="includeStart">算头(InterestCalcMode 首位为 '1')。</param>
|
||||
/// <param name="includeEnd">算尾(InterestCalcMode 末位为 '1')。</param>
|
||||
/// <param name="annualDays">年化天数(ExtendObj.AnnualDays)。</param>
|
||||
/// <param name="interestType">DB 计息方式枚举(单利 / 复利)。</param>
|
||||
/// <param name="resetPeriodDays">重置周期天数(interest_rest_days,缺省 1)。</param>
|
||||
public static AccrualPolicy FromLegacy(bool includeStart, bool includeEnd, int annualDays, InterestTypeEnum interestType, int resetPeriodDays, bool isAnnualized = false)
|
||||
=> new(AccrualBoundary.Of(includeStart, includeEnd),
|
||||
interestType == InterestTypeEnum.复利,
|
||||
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,184 @@
|
||||
using System;
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
/// 融资腿计息编排层(NEW,替换 SwapDealService 内 CalcDaily* 家族的"纯数学"部分)。
|
||||
///
|
||||
/// <para>职责边界(与 SwapInterest 原语、SwapDealService 适配器三者正交):</para>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>本类:持有跨日 <see cref="AccrualState"/>,逐日循环,经 IIndexFixer 取当日率并封装为 <see cref="FundingLegRate"/>,
|
||||
/// 调用 SwapInterest 原子原语算账,处理平仓缩放 / 已实现扣除,replay 求 Δ。</description></item>
|
||||
/// <item><description>SwapInterest:原子 "本金×利率×天数/年化" 纯函数,无状态。</description></item>
|
||||
/// <item><description>SwapDealService.GetInterests:仅做 DB 读、swap_flow_event 构造与落库(IO)。</description></item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>命名规范:本文件内所有概念一律使用自描述英文名。旧代码 typo 一律不出现:</para>
|
||||
/// <list type="table">
|
||||
/// <item><term>closePrecent</term><description>→ <see cref="ApplyPartialClose"/> 的 closeRatio(且语义固定为"占剩余持仓比例")</description></item>
|
||||
/// <item><term>orginPv</term><description>→ OriginalPv(原始名义本金,用于保证金腿差分基数)</description></item>
|
||||
/// <item><term>floateRate</term><description>→ FloatRate(FR007 浮动利率,拼写修正)</description></item>
|
||||
/// <item><term>dynomicPrincipal</term><description>→ AccrualPrincipal(逐日滚动计息本金)</description></item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>重要——以下 <see cref="AccruePeriod"/> 当前为"结构骨架":</para>
|
||||
/// 精确的跨日不变量(T+1 本金缩到剩余、重置日本金保留、consumedInterest 扣除、保证金腿差分公式)
|
||||
/// 依 branch-merge-analysis §7 的 DI_* 不变量,必须在把 <c>_0808</c>(人工已过)的利息核心并入后,
|
||||
/// 从 CalcDaily* 迁移而来并以 Excel oracle 验收。骨架故意只跑"朴素逐日累加",
|
||||
/// <b>不等价于已验证口径——切勿在未迁移前接入生产</b>。
|
||||
/// </summary>
|
||||
public static class FundingLegAccrual
|
||||
{
|
||||
// 中性锚点日期:AccrueDay 只需"同日起止 → 1 天"的语义,不依赖真实时钟,保持纯函数。
|
||||
private static readonly DateTime Epoch = new(2000, 1, 1);
|
||||
|
||||
// 资金腿计息精度(生产口径)。与 SwapDealService.InterestCalculationPrecision 一致。
|
||||
// 注意:SwapInterest.Precision=11 仅服务于保证金腿(MarginAccount),与资金腿 12 不一致属已知 TODO;
|
||||
// 资金腿所有落库/对账均以 12 为准,此处显式锁定,避免静默引入尾差。
|
||||
private const int ProductionPrecision = 12;
|
||||
|
||||
/// <summary>
|
||||
/// 单日原子计息("AccrueDay")。委托 SwapInterest.AccrueSimple(同一天、Convention 边界 → 1 天),
|
||||
/// 直接复用 SwapInterest 已有的 <see cref="InterestResult"/>(Accrued/AccruedToday)作为返回,
|
||||
/// 不再另立结果类型。编排层按日把"当天本金 + 当天率"喂入此入口,使浮动利率逐日不同也能正确累积。
|
||||
/// 复利时收盘本金 = 当日本金 + 当日利息(利滚利),该推导在 <see cref="AccruePeriod"/> 内完成,
|
||||
/// 不塞进返回结构,保持与 SwapInterest 单一结果类型的契约一致。
|
||||
/// </summary>
|
||||
/// <param name="accrualPrincipal">当日计息本金(来自 <see cref="AccrualState.AccrualPrincipal"/>)。</param>
|
||||
/// <param name="dailyRate">当日生效年利率(由调用方经 IIndexFixer + 合约利差构造后传入,已含 spread)。</param>
|
||||
/// <param name="policy">计息政策(含算头算尾 / 年化天数 / 单复利)。</param>
|
||||
public static InterestResult AccrueDay(
|
||||
decimal accrualPrincipal,
|
||||
decimal dailyRate,
|
||||
AccrualPolicy policy,
|
||||
int precision = SwapInterest.Precision)
|
||||
{
|
||||
// 单日计息:start == end,按 policy.Convention 决定首日是否计;Both 时恰 1 天。
|
||||
return SwapInterest.AccrueSimple(accrualPrincipal, dailyRate, Epoch, Epoch, policy.Convention, policy.AnnualDays, precision);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单利日终计息(纯函数,替换 SwapDealService.CalcDailySimpleInterestByEod 的"纯数学"部分)。
|
||||
///
|
||||
/// <para>口径与旧实现逐字对齐(仅命名 DDD 化):</para>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>计息基数 baseTdInterestPrincipal = priorAccrualPrincipal + positionPrincipal − originalPv;</description></item>
|
||||
/// <item><description>当日利息 = baseTdInterestPrincipal × closeRatio × rate.AllInRate,年化则再 ÷ AnnualDays;</description></item>
|
||||
/// <item><description>累计未实现 = priorUnrealized + 当日利息;末位按资金腿精度 12 舍入。</description></item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>取率与重置日重取浮动利率由适配器(CalcDailySimpleInterestByEod)负责,并封装为 <see cref="FundingLegRate"/> 传入;
|
||||
/// daycount 语义(年化 / 年化天数)由 <see cref="AccrualPolicy"/> 提供。本方法保持纯函数、可独立单测,不连库、不取价。</para>
|
||||
/// </summary>
|
||||
/// <param name="priorUnrealized">上一日日终累计未实现利息(preEod.InterestProfitSum)。</param>
|
||||
/// <param name="priorAccrualPrincipal">上一日日终计息本金(preEod.TdInterestPrincipal)。</param>
|
||||
/// <param name="positionPrincipal">存量名义本金(posiPrincipal)。</param>
|
||||
/// <param name="closeRatio">平仓比例(closePercent,EOD 恒为 1)。</param>
|
||||
/// <param name="originalPv">原始名义本金(orginPv),用于保证金腿差分基数。</param>
|
||||
/// <param name="rate">当日生效利率(已由适配器按腿型封装:固定腿=FixedRate,浮动腿=Spread+IndexFixing)。</param>
|
||||
/// <param name="policy">计息政策(daycount:是否年化 / 年化天数)。</param>
|
||||
/// <returns><see cref="InterestResult"/>:Accrued=累计未实现(对应 InterestAmount),AccruedToday=当日利息(对应 TdInterestAmount)。</returns>
|
||||
public static InterestResult AccrueSimpleEod(
|
||||
decimal priorUnrealized,
|
||||
decimal priorAccrualPrincipal,
|
||||
decimal positionPrincipal,
|
||||
decimal closeRatio,
|
||||
decimal originalPv,
|
||||
FundingLegRate rate,
|
||||
AccrualPolicy policy)
|
||||
{
|
||||
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;
|
||||
return new InterestResult(
|
||||
Math.Round(totalUnrealized, ProductionPrecision, MidpointRounding.AwayFromZero),
|
||||
Math.Round(tdInterest, ProductionPrecision, MidpointRounding.AwayFromZero));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 逐日计息编排(骨架)。从 <paramref name="startDate"/> 到 <paramref name="endDate"/> 逐日循环,
|
||||
/// 推进 <see cref="AccrualState"/>,并返回期末状态。
|
||||
/// </summary>
|
||||
/// <param name="opening">期初状态(通常来自上一日日终,见 <see cref="AccrualState.FromPreviousEod"/>)。</param>
|
||||
/// <param name="startDate">计息区间起点。</param>
|
||||
/// <param name="endDate">计息区间终点。</param>
|
||||
/// <param name="dailyRate">取计息日生效年利率的委托(适配器处由 IIndexFixer + 合约利差构造;测试可传 day => 0.0134m)。</param>
|
||||
/// <param name="policy">计息政策。</param>
|
||||
/// <param name="closeRatio">可选平仓比例(占剩余,0~1)。非空则在期末应用平仓缩放。</param>
|
||||
public static AccrualState AccruePeriod(
|
||||
AccrualState opening,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
Func<DateTime, decimal> dailyRate,
|
||||
AccrualPolicy policy,
|
||||
decimal? closeRatio = null)
|
||||
{
|
||||
var state = opening;
|
||||
|
||||
for (var day = startDate.Date; day <= endDate.Date; day = day.AddDays(1))
|
||||
{
|
||||
var rate = dailyRate(day);
|
||||
var dayResult = AccrueDay(state.AccrualPrincipal, rate, policy);
|
||||
|
||||
// 收盘本金:复利时并入当日利息(利滚利),单利时维持原基数。
|
||||
var nextPrincipal = state.AccrualPrincipal + (policy.IsCompound ? dayResult.AccruedToday : 0m);
|
||||
var nextUnrealized = state.UnrealizedInterest + dayResult.AccruedToday;
|
||||
state = new AccrualState(nextPrincipal, nextUnrealized, state.RealizedInterest);
|
||||
|
||||
// ── PORT(合并 _0808 后从 CalcDaily* 迁移,受 DI_* 不变量 + Excel oracle 验收)──
|
||||
// 1. 重置日(距 StartDate 每 resetPeriodDays 天):复利时累计利息并入本金(已在 ClosingAccrualPrincipal 体现);
|
||||
// 但"重置日部分平仓后 EOD 本金必须保留计算出的复利本金、不得被 closeRatio 二次缩放"。
|
||||
// 2. 部分平仓次日(T+1):AccrualPrincipal 必须缩到剩余(× (1-closeRatio));
|
||||
// 重置日 / 算尾日不得二次缩放,否则剩余本金被打折。
|
||||
// 3. consumedInterest(历史已实现)在平仓日经 SwapInterest.ApplyUnwind 扣除。
|
||||
// 4. 保证金腿差分基数 OriginalPv:dynomicPrincipal = AccrualPrincipal + PositionPrincipal - OriginalPv,此处分支处理。
|
||||
// 5. replay 求 Δ:本 EOD 状态 − 上一 EOD 状态,由调用方(适配器层)负责,不在纯数学内。
|
||||
}
|
||||
|
||||
if (closeRatio.HasValue)
|
||||
{
|
||||
state = ApplyPartialClose(state, closeRatio.Value, state.RealizedInterest, policy);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 部分 / 全部平仓缩放:把剩余待实现按 (1-closeRatio) 保留,并扣除历史已实现;
|
||||
/// 计息本金同步缩到剩余持仓。委托 SwapInterest.ApplyUnwind / AccrueUnrealized(与 Margin 共用同一纯函数)。
|
||||
/// </summary>
|
||||
/// <param name="state">平仓前状态(未实现部分)。</param>
|
||||
/// <param name="closeRatio">平仓比例(占剩余持仓,0~1;1=全平)。</param>
|
||||
/// <param name="realizedInterest">历史已实现利息累计,从剩余未实现中扣除。</param>
|
||||
/// <param name="policy">计息政策(精度取 SwapInterest.Precision)。</param>
|
||||
public static AccrualState ApplyPartialClose(
|
||||
AccrualState state,
|
||||
decimal closeRatio,
|
||||
decimal realizedInterest,
|
||||
AccrualPolicy policy)
|
||||
{
|
||||
// 待实现利息缩放:unrealized × (1-closeRatio) − realizedInterest
|
||||
var scaled = SwapInterest.ApplyUnwind(
|
||||
new InterestResult(state.UnrealizedInterest, state.UnrealizedInterest),
|
||||
closeRatio, realizedInterest, SwapInterest.Precision);
|
||||
|
||||
// 计息本金按剩余持仓缩放(全平 closeRatio=1 → 归零)
|
||||
var principalRemaining = Math.Round(
|
||||
state.AccrualPrincipal * (1m - closeRatio),
|
||||
SwapInterest.Precision,
|
||||
MidpointRounding.AwayFromZero);
|
||||
|
||||
return new AccrualState(principalRemaining, scaled.Accrued, realizedInterest);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
/// 融资腿在某一计息日生效的利率(不可变值对象)。
|
||||
///
|
||||
/// <para>把"固定利率 / 加点利差 / 浮动指数定盘"三种来源收敛为一个概念,避免把它们作为裸 decimal
|
||||
/// 散落在计息方法签名里——这正是初版 <c>AccrueSimpleEod</c> 参数膨胀、可读性差的根因。</para>
|
||||
///
|
||||
/// <para>固定腿与浮动腿是互斥的两种形态:固定腿只设 <see cref="FixedRate"/>(其余为 0);
|
||||
/// 浮动腿设 <see cref="Spread"/> + <see cref="IndexFixing"/>(<see cref="FixedRate"/> 为 0)。
|
||||
/// <see cref="AllInRate"/> 对两种形态统一为三者之和。</para>
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see cref="FixedRate"/>:固定腿的固定年利率;浮动腿为 0。</description></item>
|
||||
/// <item><description><see cref="Spread"/>:浮动腿在指数之上的加点利差(合约侧);固定腿为 0。</description></item>
|
||||
/// <item><description><see cref="IndexFixing"/>:浮动指数定盘(FR007 等);固定腿为 0。</description></item>
|
||||
/// <item><description><see cref="AllInRate"/>:计息用的"当日生效年利率"。固定腿取 <see cref="FixedRate"/>;浮动腿取 <see cref="Spread"/> + <see cref="IndexFixing"/>(二者互斥,非叠加)。</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public readonly struct FundingLegRate
|
||||
{
|
||||
/// <summary>固定腿的固定年利率;浮动腿为 0。</summary>
|
||||
public decimal FixedRate { get; }
|
||||
|
||||
/// <summary>浮动腿在指数之上的加点利差(合约侧);固定腿为 0。</summary>
|
||||
public decimal Spread { get; }
|
||||
|
||||
/// <summary>浮动指数定盘利率(市场侧,FR007 等);固定腿为 0。</summary>
|
||||
public decimal IndexFixing { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 当日生效年利率。固定腿与浮动腿是互斥的两种利率确定方式,不是可叠加分量:
|
||||
/// 固定腿取 <see cref="FixedRate"/>;浮动腿取 <see cref="Spread"/> + <see cref="IndexFixing"/>。
|
||||
/// 约定:固定腿只设 FixedRate(其余为 0),浮动腿只设 Spread + IndexFixing(FixedRate 为 0)。
|
||||
/// </summary>
|
||||
public decimal AllInRate => FixedRate != 0m ? FixedRate : Spread + IndexFixing;
|
||||
|
||||
public FundingLegRate(decimal fixedRate = 0m, decimal spread = 0m, decimal indexFixing = 0m)
|
||||
=> (FixedRate, Spread, IndexFixing) = (fixedRate, spread, indexFixing);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ 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;
|
||||
@@ -1552,49 +1553,54 @@ 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
|
||||
? new FundingLegRate(fixedRate: flowEvent.InterestRate)
|
||||
: new FundingLegRate(spread: flowEvent.InterestRate, indexFixing: effectiveFloat);
|
||||
var accrualPolicy = new AccrualPolicy(
|
||||
convention: AccrualBoundary.Both,
|
||||
isCompound: false,
|
||||
resetPeriodDays: position.interest_rest_days ?? 1,
|
||||
annualDays: annualDays,
|
||||
isAnnualized: position.IsAnnualized);
|
||||
var result = FundingLegAccrual.AccrueSimpleEod(
|
||||
priorUnrealized: preEodPosition.InterestProfitSum,
|
||||
priorAccrualPrincipal: preEodPosition.TdInterestPrincipal,
|
||||
positionPrincipal: posiPrincipal,
|
||||
closeRatio: closePercent,
|
||||
originalPv: orginPv,
|
||||
rate: legRate,
|
||||
policy: accrualPolicy);
|
||||
InterestAmount = result.Accrued;
|
||||
TdInterestAmount = result.AccruedToday;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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