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;