From dea9c4cba56cea32cd10ca611dc18889d8c978b7 Mon Sep 17 00:00:00 2001 From: hjhan Date: Thu, 13 Aug 2026 10:58:47 +0800 Subject: [PATCH] =?UTF-8?q?docs(margin):=20=E4=BF=AE=E5=A4=8D=E4=BF=9D?= =?UTF-8?q?=E8=AF=81=E9=87=91=E8=AE=A1=E6=81=AF=E9=87=8D=E6=9E=84=E5=90=8E?= =?UTF-8?q?=E7=9A=84=E8=BF=87=E6=97=B6=E6=96=87=E6=A1=A3=E4=B8=8E=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按最新代码(CalcMarginInterest 已接生产、orginPv hack 已删、MarginAccount 未接线、保证金精度=12)修正: - 删除 orginPv 维度重映射的孤儿 summary(方法已删) - SwapDealService 注释:去掉 orginPv(InitSwapDealInterest)/保证金腿 引用 - SwapInterest/AccrualContext/InterestRate:去掉 Precision=11 是"保证金腿"、"保证金场景"等错误归因(保证金实际跑精度12) - MarginAccount/MarginBalance:标注"尚未接线",生产入口指向 CalcMarginInterest,去掉"余额×利率×天数"过度简化 - 测试注释:去掉"无 orginPv/差分"(盘中保留差分)、"提交2 待切换"(已完成) 仅文档/注释,零代码行为变化。 --- .../YLErp.Core/Interest/AccrualContext.cs | 2 +- Framework/YLErp.Core/Interest/InterestRate.cs | 2 +- Framework/YLErp.Core/Interest/SwapInterest.cs | 6 +-- .../Margin/MarginInterestGoldenReplayTest.cs | 9 ++-- .../Margin/MarginInterestShadowTest.cs | 11 ++--- .../SwapModule/Margin/MarginAccount.cs | 15 +++--- .../SwapModule/Margin/MarginBalance.cs | 2 +- .../Modules/SwapModule/SwapDealService.cs | 46 +++++++++++-------- 8 files changed, 49 insertions(+), 44 deletions(-) diff --git a/Framework/YLErp.Core/Interest/AccrualContext.cs b/Framework/YLErp.Core/Interest/AccrualContext.cs index 3cdbf5fb..8e7dd77f 100644 --- a/Framework/YLErp.Core/Interest/AccrualContext.cs +++ b/Framework/YLErp.Core/Interest/AccrualContext.cs @@ -18,7 +18,7 @@ public readonly struct AccrualContext /// 年化天数(365 / 360)。 public int AnnualDays { get; } - /// 舍入精度位数。默认 11(保证金腿);资金腿调用方应显式传 FundingLegPrecision=12。 + /// 舍入精度位数。默认 11(仅未接线的 MarginAccount.AccrueInterest 走此默认;生产融资腿/保证金腿均显式用 FundingLegPrecision=12)。 public int Precision { get; } /// 可选 trace 收集器;为 null 时不记录(纯计算场景直接传 null,与开关无关)。 diff --git a/Framework/YLErp.Core/Interest/InterestRate.cs b/Framework/YLErp.Core/Interest/InterestRate.cs index 716d48b7..b5a6def1 100644 --- a/Framework/YLErp.Core/Interest/InterestRate.cs +++ b/Framework/YLErp.Core/Interest/InterestRate.cs @@ -53,7 +53,7 @@ public readonly struct InterestRate /// /// :decimal 精确运算。 /// / :闭式(double 计算后回 decimal), - /// 满足通用定价 / 保证金场景;若要 decimal 精度的离散重置日复利,请用 Simple 按段计息并滚动本金。 + /// 满足通用定价;若要 decimal 精度的离散重置日复利,请用 Simple 按段计息并滚动本金。 /// /// public decimal CompoundFactor(decimal t) diff --git a/Framework/YLErp.Core/Interest/SwapInterest.cs b/Framework/YLErp.Core/Interest/SwapInterest.cs index d66ce7c4..ddec05bf 100644 --- a/Framework/YLErp.Core/Interest/SwapInterest.cs +++ b/Framework/YLErp.Core/Interest/SwapInterest.cs @@ -106,11 +106,11 @@ public readonly struct InterestResult /// public static class SwapInterest { - /// 系统统一价格精度位数(保证金腿)。 + /// 默认舍入精度位数(历史值;生产融资腿与保证金腿均用 FundingLegPrecision=12)。 public const int Precision = 11; - /// 资金腿计息精度(生产口径)。资金腿所有落库/对账均以 12 位为准, - /// 与保证金腿的 Precision=11 不同。提升至公共常量,消除 SwapDealService 与 FundingLegAccrual 的重复定义。 + /// 资金腿与保证金腿的生产计息精度(落库/对账均以 12 位为准)。 + /// 提升至公共常量,消除 SwapDealService 与 SimpleInterestAccrual 的重复定义。 public const int FundingLegPrecision = 12; /// 年化天数常量(合约字段存的是 int,故不用 enum)。 diff --git a/UnitTestProject/Modules/SwapModule/Margin/MarginInterestGoldenReplayTest.cs b/UnitTestProject/Modules/SwapModule/Margin/MarginInterestGoldenReplayTest.cs index 22c41d27..bb9a65fa 100644 --- a/UnitTestProject/Modules/SwapModule/Margin/MarginInterestGoldenReplayTest.cs +++ b/UnitTestProject/Modules/SwapModule/Margin/MarginInterestGoldenReplayTest.cs @@ -13,12 +13,11 @@ namespace UnitTestProject.Modules.SwapModule.Margin { /// /// 黄金回放验证(连真实测试库 192.168.2.96):对真实保证金交易逐日 EOD 比对 - /// 旧管线 GetInterests(settment=true → CalcEodInterest → CalcDailySimpleInterestByEod → AccrueEod) - /// vs 新方法 CalcMarginInterest(settment=true),验证消除 orginPv/差分后数值仍与旧管线一致。 + /// GetInterests(settment=true,保证金分支现走 CalcMarginInterest) vs 直接调 CalcMarginInterest, + /// 验证 GetInterests→CalcMarginInterest 接线的参数对齐(rate/posiPrincipal/preEod 等)正确。 /// - /// 这是 MarginInterestShadowTest(合成数据)的真实库版本:数据来自 96 库的真实保证金交易, - /// 覆盖追加预付金多行、多次部分平仓(InterestPrincipalFix 下台阶)、跨 EOD 续接等单元测试够不到的边界。 - /// 通过则可安全进入提交2(切换生产 + 删 orginPv hack)。 + /// 数据来自 96 库的真实保证金交易,覆盖追加预付金多行、多次部分平仓(InterestPrincipalFix 下台阶)、 + /// 跨 EOD 续接等单元测试够不到的边界。作为保证金计息迁移后的真实库回归守护。 /// [TestClass] public class MarginInterestGoldenReplayTest diff --git a/UnitTestProject/Modules/SwapModule/Margin/MarginInterestShadowTest.cs b/UnitTestProject/Modules/SwapModule/Margin/MarginInterestShadowTest.cs index 4347a29e..34740fc2 100644 --- a/UnitTestProject/Modules/SwapModule/Margin/MarginInterestShadowTest.cs +++ b/UnitTestProject/Modules/SwapModule/Margin/MarginInterestShadowTest.cs @@ -13,13 +13,12 @@ using YLErp.Derivatives.Interest; namespace UnitTestProject.Modules.SwapModule.Margin { /// - /// 影子对账:保证金腿新方法 CalcMarginInterest(无 orginPv/差分)vs - /// 旧通用管线 CalcDailySimpleInterestByEod/CalcDailySimpleInterest(带差分 + orginPv hack)。 + /// 影子对账:保证金腿方法 CalcMarginInterest(EOD 用昨日终本金、盘中用 accrualBasis 差分)vs + /// 旧通用管线 CalcDailySimpleInterestByEod/CalcDailySimpleInterest。 /// - /// 保证金是纯固定利率单利(FloatRateUnderlyingCode 恒空、InterestType 恒单利、SwapIntervalList 单段), - /// 旧管线差分公式 accrualBasis = TdInterestPrincipal + posiPrincipal - orginPv 对保证金恒等于 posiPrincipal - /// (因 orginPv 经 PreviousBalance 对齐到昨日终保证金余额),故新方法直接用 posiPrincipal/昨日终本金作 - /// notional 应与旧管线严格数值一致。本测试即在多种场景下证明这一等价,为提交2 切换生产路径提供安全网。 + /// 保证金是纯固定利率单利(FloatRateUnderlyingCode 恒空、InterestType 恒单利、SwapIntervalList 单段)。 + /// 本测试在生产切到 CalcMarginInterest 后作为回归守护,确认其 InterestAmount/TdInterestAmount + /// 与旧纯函数(SimpleInterestAccrual)数值一致。覆盖 EOD 续接/首日、盘中全平/部分平仓/互换。 /// [TestClass] public class MarginInterestShadowTest diff --git a/YLErpDAL/Modules/SwapModule/Margin/MarginAccount.cs b/YLErpDAL/Modules/SwapModule/Margin/MarginAccount.cs index fcdf3f14..33a5474f 100644 --- a/YLErpDAL/Modules/SwapModule/Margin/MarginAccount.cs +++ b/YLErpDAL/Modules/SwapModule/Margin/MarginAccount.cs @@ -4,14 +4,12 @@ using YLErp.Derivatives.Interest; namespace YLErp.Modules.SwapModule.Margin; /// -/// 保证金账户。管理保证金余额的变动(追加/释放/返还),并提供计息入口。 +/// 保证金账户。管理保证金余额的变动(追加/释放/返还),并提供计息入口(预留抽象,尚未接线)。 /// -/// 保证金是独立的资金管理概念(初始保证金/维持保证金/保证金余额/追保), -/// 与融资腿(funding leg)完全无关。现有代码把保证金塞进 InterestMode==5/6 -/// 当计息腿处理是错误的,本类是正确建模的起点。 -/// -/// 利息计算委托 SwapInterest 纯函数(余额×利率×天数/年化), -/// 保证金账户只提供余额和计息入口,不自己实现计息算法。 +/// 保证金是独立的资金管理概念(初始保证金/维持保证金/保证金余额/追保),与融资腿(funding leg)无关。 +/// 生产保证金计息入口为 SwapDealService.CalcMarginInterest(仍以 InterestMode 5/6 标识): +/// EOD 用昨日终本金 preEod.TdInterestPrincipal(无差分);盘中用 accrualBasis 差分(orginPv 经 PreviousBalance)。 +/// 本类尚未被生产代码实例化——其扁平"余额×利率×天数"模型无法表达盘中差分与多行分段,留作未来简化抽象。 /// public sealed class MarginAccount { @@ -31,7 +29,8 @@ public sealed class MarginAccount /// /// 按当前余额计算保证金利息。委托 SwapInterest.AccrueSimple。 - /// 保证金利息是券商对客户保证金存款付息(方向与融资腿相反)。 + /// 注意:当前未被生产代码调用——生产保证金计息入口为 SwapDealService.CalcMarginInterest + /// (处理 EOD 昨日终本金与盘中差分;本方法的扁平余额模型不覆盖盘中差分口径)。 /// /// 保证金利率(年化,如 0.03 = 3%)。 /// 计息开始日。 diff --git a/YLErpDAL/Modules/SwapModule/Margin/MarginBalance.cs b/YLErpDAL/Modules/SwapModule/Margin/MarginBalance.cs index 491de03c..abb50ece 100644 --- a/YLErpDAL/Modules/SwapModule/Margin/MarginBalance.cs +++ b/YLErpDAL/Modules/SwapModule/Margin/MarginBalance.cs @@ -4,7 +4,7 @@ namespace YLErp.Modules.SwapModule.Margin; /// 保证金余额。现金、授信、担保等多种保证金形态的统一表达。 /// /// 保证金就是保证金——有余额、有利率、有利息,不存在"计息基数/Notional"概念。 -/// 余额随追加/释放/盈亏变动,利息由 SwapInterest 纯函数按 余额×利率×天数/年化 计算。 +/// 余额随追加/释放/盈亏变动,利息由计息层(SwapDealService.CalcMarginInterest)按 EOD 昨日终本金 / 盘中差分口径计算。 /// public readonly struct MarginBalance { diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 2a2072ae..f0325f0b 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -493,8 +493,8 @@ namespace YLErp.Modules.SwapModule /// 根因(多次部分平仓预付金返还错误):预付金腿(初始/追加)的"当前剩余本金"存于实时持仓 /// realPositions.InterestPrincipalFix,每次平仓由 UpdateInitalPosition 递减;而原始腿 /// origPositions(IsInitial=1)的 InterestPrincipalFix 恒为初始值。GetInterests 算 - /// closePrincipal = Fix × closePercent 与预付金计息基数 orginPv(InitSwapDealInterest) 时都读 - /// position.InterestPrincipalFix,若沿用原始腿,会在多次部分平仓后仍返还/计算初始本金(如始终 99000)。 + /// closePrincipal = Fix × closePercent 时读 position.InterestPrincipalFix,若沿用原始腿, + /// 会在多次部分平仓后仍返还/计算初始本金(如始终 99000)。 /// 修复:迭代源仍用 origPositions(保留 orig.id → eod_swap_position.PositionId 的日终匹配, /// 全库实测 eod 均按 orig.id 归档;若换 realPositions 会破坏 preEod 匹配导致利息重算错误),仅对预付金腿 /// Clone 覆盖其本金值为实时腿的剩余本金。real 与 orig 通过 real.PositionId == orig.id 精确 1:1 关联。 @@ -806,16 +806,33 @@ namespace YLErp.Modules.SwapModule /// protected virtual decimal GetPreEodDividendSum(int tradeId, long positionId, DateTime dealDate) { - var lastEod = DbContext.eod_swap - .Where(x => x.ValueDate < dealDate && x.SwapTradeId == tradeId) - .OrderByDescending(o => o.ValueDate).FirstOrDefault(); - var preEodDate = lastEod == null ? dealDate.AddDays(-1) : lastEod.ValueDate; - var preEod = new SwapEodPositionService(this) - .GetPreEodPositions(tradeId, preEodDate) - .FirstOrDefault(x => x.PositionId == positionId); + var preEod = GetPreEodPositionByDate(tradeId, positionId, dealDate); return preEod == null ? 0m : preEod.PosiDividendSum; } + /// + /// 取 dealDate 对应"上一收盘日"持仓的累计分红快照。 + /// 内部用 ValueDate 严格小于 dealDate 定位上一收盘日(登记日当天手动平仓会因此读到 T-1 快照,见 GLMS-20260105-0006)。 + /// + protected virtual eod_swap_position GetPreEodPositionByDate(int tradeId, long positionId, DateTime dealDate) + { + var lastEod = QueryPreEodSwaps(tradeId) + .Where(x => x.ValueDate < dealDate) + .OrderByDescending(o => o.ValueDate).FirstOrDefault(); + var preEodDate = lastEod == null ? dealDate.AddDays(-1) : lastEod.ValueDate; + return QueryPreEodPosition(tradeId, positionId, preEodDate); + } + + /// 可测性 seam:返回某交易的全部 eod_swap 行(不做日期过滤)。测试可 override 注入内存数据。 + protected virtual IQueryable QueryPreEodSwaps(int tradeId) + => DbContext.eod_swap.Where(x => x.SwapTradeId == tradeId); + + /// 可测性 seam:取指定收盘日的持仓累计分红快照。测试可 override 注入内存数据。 + protected virtual eod_swap_position QueryPreEodPosition(int tradeId, long positionId, DateTime valueDate) + => new SwapEodPositionService(this) + .GetPreEodPositions(tradeId, valueDate) + .FirstOrDefault(x => x.PositionId == positionId); + /// /// 获取固定利率 /// @@ -1062,15 +1079,6 @@ namespace YLErp.Modules.SwapModule orginPv, calcFirst, calcLast, consumedInterest); } /// - /// 保证金腿的 orginPv 维度重映射。 - /// - /// 保证金腿被迫走融资腿的差分公式(dynomicPrincipal = TdInterestPrincipal + posiPrincipal - orginPv), - /// 但 orginPv 对融资腿是"交易名义本金(千万~亿级)",对保证金腿必须是"保证金本金"—— - /// 否则维度不匹配会算出巨负值。本方法把保证金场景的 orginPv 对齐到"上一日保证金本金"。 - /// - /// 待迁入 Margin 模块:保证金独立计息入口建好后,此方法移入 MarginAccount/MarginService。 - /// - /// /// 写入保证金的资金记录:应付预付金(SwapMarginAmount)和预付金返息(SwapMarginRebatePnl)。 /// 依赖实例方法 AddClientCash/AddClientCashInCashOut,暂留此处。 /// @@ -1346,7 +1354,7 @@ namespace YLErp.Modules.SwapModule var startDate = position.PosiStartDate; int interestPeriod = position.interest_rest_days ?? 1; - // orginPv 是路径相关参考本金(资金腿=上一日终浮动端名义本金;保证金腿=上一日终保证金余额)。 + // orginPv 是路径相关参考本金(资金腿=上一日终浮动端名义本金)。保证金腿已走 CalcMarginInterest,不经此方法。 // 单利差分:accrualBasis 全程恒定 = 昨日终滚动基数 + 当日名义本金 - 参考本金。 var accrualBasis = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv;