From 2d7f23f6373133cc167edbf4e1dfddaa0fd5e9ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=94=A6=E9=BA=9F=20=E7=8E=8B?= Date: Fri, 28 Aug 2026 10:58:47 +0800 Subject: [PATCH] =?UTF-8?q?#EQD-7194=20=E4=BC=B0=E5=80=BC=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E9=A1=B5=E9=9D=A2=EF=BC=9A=E8=BF=BD=E6=8A=A5=E9=87=91?= =?UTF-8?q?=E9=A2=9D=EF=BC=8C=E5=8F=AF=E5=8F=96=E8=B5=84=E9=87=91=EF=BC=8C?= =?UTF-8?q?=E5=8F=AF=E7=94=A8=E8=B5=84=E9=87=91=E4=B8=8D=E6=AD=A3=E7=A1=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SwapModule/SwapSpanBalanceCalcTest.cs | 88 ++++++++++++------- .../BLL/EodSettlement/ClientBalanceUtility.cs | 27 ++++-- .../RealTimeClientBanlanceService.cs | 27 ++++-- .../SwapModule/Margin/SwapSpanBalanceCalc.cs | 43 ++++++--- 4 files changed, 134 insertions(+), 51 deletions(-) diff --git a/UnitTestProject/Modules/SwapModule/SwapSpanBalanceCalcTest.cs b/UnitTestProject/Modules/SwapModule/SwapSpanBalanceCalcTest.cs index fda8849a..e1f487d9 100644 --- a/UnitTestProject/Modules/SwapModule/SwapSpanBalanceCalcTest.cs +++ b/UnitTestProject/Modules/SwapModule/SwapSpanBalanceCalcTest.cs @@ -3,9 +3,11 @@ using YLErp.Modules.SwapModule.Margin; namespace YLErp.Modules.SwapModule { /// - /// R2 阶段三 §3.2 可用资金公式测试(需求拆分 R2 口径,实时/EOD/报告三处共用 SwapSpanBalanceCalc): - /// 客户维度 = Max(现金结存 + 授信 − 已使用授信 + 初始保证金 − 维持保证金, 0); - /// 合约维度 = Max(现金结存 + 授信 − 已使用授信 − 交易维度追加保证金合计, 0)。 + /// R2 阶段三 §3.2 可用资金/追保/可取资金公式测试(定稿 2026-08-28 口径,实时/EOD/报告三处共用 SwapSpanBalanceCalc): + /// 客户维度可用 = Max(现金结存 + 授信 − 已使用授信 + 初始保证金 − 维持保证金, 0); + /// 合约维度可用 = Max(现金结存 + 授信 − 已使用授信, 0)(定稿删除"−交易维度追加保证金",避免与现金结存/已使用授信双重扣减); + /// 追保金额两维度均 Max(...,0) 截断、恒 ≥ 0(只追不退); + /// 可取资金只算现金部分:合约维度 = Max(现金结存 + min(持仓盈亏,0), 0),客户维度 = Max(现金结存 + 初始 − 维持 + min(持仓盈亏,0), 0)。 /// 授信额度为 credit.Credit 合计(保存时已折算比例),现金结存=期末结存(阶段二起授信不进资金)。 /// [TestClass] @@ -35,40 +37,27 @@ namespace YLErp.Modules.SwapModule cashBalance: 20, totalCredit: 100, usedCredit: 20, initialMargin: 100, maintenanceMargin: 130), 1e-6); } + /// + /// 合约维度可用资金(定稿 2026-08-28):Max(现金结存 + 授信 − 已使用授信, 0),不再减交易维度追加保证金。 + /// [TestMethod] - public void SB_004_合约维度_按交易追加合计扣减() + public void SB_004_合约维度_可用资金() { - //现金20 + 授信100 − 已使用20 − 追加合计30 = 70 - Assert.AreEqual(70, SwapSpanBalanceCalc.CalcContractDimensionAvailable( - cashBalance: 20, totalCredit: 100, usedCredit: 20, tradeAdditionalMarginSum: 30), 1e-6); + //现金20 + 授信100 − 已使用20 = 100 + Assert.AreEqual(100, SwapSpanBalanceCalc.CalcContractDimensionAvailable( + cashBalance: 20, totalCredit: 100, usedCredit: 20), 1e-6); } [TestMethod] public void SB_005_合约维度_扣尽截断为0() { + //现金20 + 授信0 − 已使用30 = −10 → 0 Assert.AreEqual(0, SwapSpanBalanceCalc.CalcContractDimensionAvailable( - cashBalance: 20, totalCredit: 0, usedCredit: 0, tradeAdditionalMarginSum: 30), 1e-6); + cashBalance: 20, totalCredit: 0, usedCredit: 30), 1e-6); } /// - /// 阶段三口径一致性:无追加保证金流水时(阶段四前),两维度公式数值一致—— - /// 交易追加合计 = Σ(维持−累计) = 维持 − 初始,与客户维度的 (初始−维持) 项互相抵消。 - /// - [TestMethod] - public void SB_006_阶段三两维度数值一致() - { - var cash = 20d; - var credit = 100d; - var used = 20d; - var initial = 100d; - var maintenance = 130d; - var clientDimension = SwapSpanBalanceCalc.CalcClientDimensionAvailable(cash, credit, used, initial, maintenance); - var contractDimension = SwapSpanBalanceCalc.CalcContractDimensionAvailable(cash, credit, used, maintenance - initial); - Assert.AreEqual(clientDimension, contractDimension, 1e-9); - } - - /// - /// 阶段四 §4.2 客户维度双向追保:正数=需追保,不以 0 截断。 + /// 客户维度追保金额(定稿 2026-08-28:Max(...,0) 截断,恒 ≥ 0,只追不退)。 /// [TestMethod] public void SB_010_客户维度追保金额_正数需追保() @@ -79,15 +68,15 @@ namespace YLErp.Modules.SwapModule } [TestMethod] - public void SB_011_客户维度追保金额_负数可返还() + public void SB_011_客户维度追保金额_盈余截断为0() { - //差额 (维持130−初始100)=30,资金 20+100−20=100 → 追保 = 30 − 100 = −70(可返还,不截断为 0) - Assert.AreEqual(-70, SwapSpanBalanceCalc.CalcClientDimensionCallMargin( + //差额 (维持130−初始100)=30,资金 20+100−20=100 → 30 − 100 = −70 → 截断为 0(资金富余不展示负数) + Assert.AreEqual(0, SwapSpanBalanceCalc.CalcClientDimensionCallMargin( cashBalance: 20, totalCredit: 100, usedCredit: 20, initialMargin: 100, maintenanceMargin: 130), 1e-6); } /// - /// 客户维度追保金额与可用资金公式互为反向(去 Max 截断):追保 = −(未截断可用资金)。 + /// 客户维度追保金额与可用资金公式互为反向(各加 Max 截断):追保 = Max(−(未截断可用资金), 0)。 /// [TestMethod] public void SB_012_客户维度追保与可用资金反向一致() @@ -100,7 +89,7 @@ namespace YLErp.Modules.SwapModule var available = SwapSpanBalanceCalc.CalcClientDimensionAvailable(cash, credit, used, initial, maintenance); var callMargin = SwapSpanBalanceCalc.CalcClientDimensionCallMargin(cash, credit, used, initial, maintenance); var availableUnfloored = cash + credit - used + initial - maintenance; - Assert.AreEqual(-availableUnfloored, callMargin, 1e-9); + Assert.AreEqual(Math.Max(-availableUnfloored, 0), callMargin, 1e-9); //可用资金被 0 截断时追保为正(需追保),两者不矛盾 if (availableUnfloored < 0) { @@ -127,5 +116,42 @@ namespace YLErp.Modules.SwapModule Assert.AreEqual(0, SwapSpanBalanceCalc.CalcContractDimensionCallMargin( cashBalance: 50, totalCredit: 100, usedCredit: 20), 1e-6); } + + /// + /// 可取资金(定稿 2026-08-28):只算现金部分(授信不可取现);min(持仓盈亏,0) 只扣浮亏、浮盈不放行。 + /// + [TestMethod] + public void SB_015_客户维度可取资金_浮亏扣减() + { + //现金200 + 初始100 − 维持150 + min(−30,0) = 120 + Assert.AreEqual(120, SwapSpanBalanceCalc.CalcClientDimensionDesirableFund( + cashBalance: 200, initialMargin: 100, maintenanceMargin: 150, positionPnl: -30), 1e-6); + } + + [TestMethod] + public void SB_016_客户维度可取资金_浮盈不放行() + { + //现金200 + 初始100 − 维持150 + min(50,0)=0 → 150 + Assert.AreEqual(150, SwapSpanBalanceCalc.CalcClientDimensionDesirableFund( + cashBalance: 200, initialMargin: 100, maintenanceMargin: 150, positionPnl: 50), 1e-6); + } + + [TestMethod] + public void SB_017_客户维度可取资金_截断为0() + { + //现金20 + 初始100 − 维持150 + min(−40,0) = −70 → 0 + Assert.AreEqual(0, SwapSpanBalanceCalc.CalcClientDimensionDesirableFund( + cashBalance: 20, initialMargin: 100, maintenanceMargin: 150, positionPnl: -40), 1e-6); + } + + [TestMethod] + public void SB_018_合约维度可取资金() + { + //Max(100 + min(−30,0), 0) = 70;浮亏超过现金时截断为 0 + Assert.AreEqual(70, SwapSpanBalanceCalc.CalcContractDimensionDesirableFund(cashBalance: 100, positionPnl: -30), 1e-6); + Assert.AreEqual(0, SwapSpanBalanceCalc.CalcContractDimensionDesirableFund(cashBalance: 20, positionPnl: -50), 1e-6); + //浮盈不放行:Max(100 + min(80,0), 0) = 100 + Assert.AreEqual(100, SwapSpanBalanceCalc.CalcContractDimensionDesirableFund(cashBalance: 100, positionPnl: 80), 1e-6); + } } } diff --git a/YLErpDAL/BLL/EodSettlement/ClientBalanceUtility.cs b/YLErpDAL/BLL/EodSettlement/ClientBalanceUtility.cs index a65ba73f..c5de8504 100644 --- a/YLErpDAL/BLL/EodSettlement/ClientBalanceUtility.cs +++ b/YLErpDAL/BLL/EodSettlement/ClientBalanceUtility.cs @@ -387,8 +387,9 @@ namespace YLErp.BLL.EodSettlement // 未配置(MarginWatchRule=NULL,存量客户)维持旧口径:期末结存-追保账户余额 if (client?.MarginWatchRule == 0) { + //定稿 2026-08-28:Max(现金结存 + 授信额度 − 已使用授信, 0),不再减交易维度追加保证金(避免双重扣减) balance.AvailableAmount = SwapSpanBalanceCalc.CalcContractDimensionAvailable( - balance.AmountFund, balance.TotalCredit, balance.UsedCredit, balance.SwapAdditionalMarginTotal); + balance.AmountFund, balance.TotalCredit, balance.UsedCredit); } else if (client?.MarginWatchRule == 1) { @@ -401,8 +402,8 @@ namespace YLErp.BLL.EodSettlement { balance.AvailableAmount = balance.MarginBalance - (balance.VmInFundSum - balance.VmOutFundSum); } - // 是否追保/追保金额(阶段四 §4.2 按维度分流,允许负值=双向,不以 0 截断): - // 客户维度(==1)= (维持−初始) − (现金+授信−已使用),负=可返还; + // 是否追保/追保金额(按维度分流,定稿 2026-08-28 起均 Max(...,0) 截断、恒 ≥ 0,只追不退): + // 客户维度(==1)= Max((维持−初始) − (现金+授信−已使用), 0); // 合约维度(==0)= Max(−(现金+授信−已使用), 0)(需求原文公式,盈余截断为 0); // 未配置(NULL 存量)维持旧口径:盯市低于维持时 = 初始保证金金额−盯市金额,否则 0 if (client?.MarginWatchRule == 1) @@ -422,8 +423,24 @@ namespace YLErp.BLL.EodSettlement balance.NeedAddMargin = balance.SwapMarketAmount < balance.MaintenanceMargin; balance.MarginByPayableMarginTotal = balance.NeedAddMargin ? (balance.MySideMargin - balance.SwapMarketAmount) : 0; } - // 可取资金=Math.Max(期末结存-min(持仓盈亏,0)-初始保证金,0) - balance.DesirableFund = Math.Max(balance.MarginBalance + Math.Min(balance.RoundedPositionPnl, 0), 0); + // 可取资金(定稿 2026-08-28 分维度口径,只算现金部分——授信不可取现;min(持仓盈亏,0) 只扣浮亏、浮盈不放行): + // 合约维度(==0)= Max(现金结存 + min(持仓盈亏,0), 0)(追保已落账进现金结存,无需再叠加保证金约束); + // 客户维度(==1)= Max(现金结存 + 初始保证金 − 维持保证金 + min(持仓盈亏,0), 0)(追保不产现金流,显式叠加保证金约束); + // 未配置(NULL 存量)维持旧口径 + if (client?.MarginWatchRule == 0) + { + balance.DesirableFund = SwapSpanBalanceCalc.CalcContractDimensionDesirableFund( + balance.AmountFund, balance.RoundedPositionPnl); + } + else if (client?.MarginWatchRule == 1) + { + balance.DesirableFund = SwapSpanBalanceCalc.CalcClientDimensionDesirableFund( + balance.AmountFund, balance.SwapInitMargin, -balance.MySideMargin, balance.RoundedPositionPnl); + } + else + { + balance.DesirableFund = Math.Max(balance.MarginBalance + Math.Min(balance.RoundedPositionPnl, 0), 0); + } } } diff --git a/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs b/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs index f1740039..563dd71f 100644 --- a/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs +++ b/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs @@ -286,8 +286,9 @@ namespace YLErp.BLL.Eod var ruleClient = DataCacheProvider.GetClientDataSource().GetData(clientId); if (ruleClient?.MarginWatchRule == 0) { + //定稿 2026-08-28:Max(现金结存 + 授信额度 − 已使用授信, 0),不再减交易维度追加保证金(避免双重扣减) item.AvailableAmount = SwapSpanBalanceCalc.CalcContractDimensionAvailable( - item.AmountFund, item.TotalCredit, item.UsedCredit, item.SwapAdditionalMarginTotal); + item.AmountFund, item.TotalCredit, item.UsedCredit); } else if (ruleClient?.MarginWatchRule == 1) { @@ -300,8 +301,8 @@ namespace YLErp.BLL.Eod { item.AvailableAmount = item.MarginBalance - item.FrozenMarginMoney; } - // 是否追保/追保金额(按维度分流,与 ClientBalanceUtility 报告口径一致): - // 客户维度(==1)双向追保 = (维持−初始) − (现金+授信−已使用),允许负值(负=可返还); + // 是否追保/追保金额(按维度分流,与 ClientBalanceUtility 报告口径一致,定稿 2026-08-28 起均 Max(...,0) 截断): + // 客户维度(==1)= Max((维持−初始) − (现金+授信−已使用), 0); // 合约维度(==0)= Max(−(现金+授信−已使用), 0),盈余截断为 0;NULL 存量维持旧口径 if (ruleClient?.MarginWatchRule == 1) { @@ -322,8 +323,24 @@ namespace YLErp.BLL.Eod // 追保金额=初始保证金金额-盯市金额 item.MarginByPayableMarginTotal = item.NeedAddMargin ? (item.MySideMargin - item.SwapMarketAmount) : 0; } - // 可取资金=max(期末结存+min(持仓盈亏,0)-初始保证金,0) - item.DesirableFund =Math.Max( item.MarginBalance - item.FrozenMarginMoney + Math.Min(item.RoundedPositionPnl, 0),0); + // 可取资金(定稿 2026-08-28 分维度口径,只算现金部分;min(持仓盈亏,0) 只扣浮亏、浮盈不放行): + // 合约维度(==0)= Max(现金结存 + min(持仓盈亏,0), 0); + // 客户维度(==1)= Max(现金结存 + 初始保证金 − 维持保证金 + min(持仓盈亏,0), 0); + // NULL 存量维持旧口径(含冻结保证金扣减) + if (ruleClient?.MarginWatchRule == 0) + { + item.DesirableFund = SwapSpanBalanceCalc.CalcContractDimensionDesirableFund( + item.AmountFund, item.RoundedPositionPnl); + } + else if (ruleClient?.MarginWatchRule == 1) + { + item.DesirableFund = SwapSpanBalanceCalc.CalcClientDimensionDesirableFund( + item.AmountFund, item.SwapInitMargin, -item.MySideMargin, item.RoundedPositionPnl); + } + else + { + item.DesirableFund = Math.Max(item.MarginBalance - item.FrozenMarginMoney + Math.Min(item.RoundedPositionPnl, 0), 0); + } } return _clientBalanceDic.Values; diff --git a/YLErpDAL/Modules/SwapModule/Margin/SwapSpanBalanceCalc.cs b/YLErpDAL/Modules/SwapModule/Margin/SwapSpanBalanceCalc.cs index 3450b796..2374f39a 100644 --- a/YLErpDAL/Modules/SwapModule/Margin/SwapSpanBalanceCalc.cs +++ b/YLErpDAL/Modules/SwapModule/Margin/SwapSpanBalanceCalc.cs @@ -5,7 +5,7 @@ namespace YLErp.Modules.SwapModule.Margin /// 授信额度取 credit.Credit 合计(阶段一 §1.1 已在保存时折算 原始授信值×最大授信可用比例,消费方不再乘比例); /// 现金结存 = 期末结存 AmountFund(阶段二起授信不进资金,流水天然不含授信部分,无需排除); /// 已使用授信 = 授信出入表 Σ(amount); - /// 初始保证金(净收取为正)= 应付预付金流水收付净额(平仓返还自动冲减); + /// 初始保证金(净收取为正)= 初始预付金的和 = 应付预付金流水收付净额 + 初始预付金授信占用净额(2026-08-28 业务裁定:与页面"初始保证金金额"字段同口径); /// 维持保证金(净收取为正)= −MySideMargin(client_span 维持保证金写入 trade_span 后经 CalcClientMargin 反号聚合)。 /// 公式整体待业务校验(EQD-6948),参数化集中在此便于校验后调整。 /// @@ -24,25 +24,25 @@ namespace YLErp.Modules.SwapModule.Margin /// /// 可用资金(合约维度,MarginWatchRule=0): - /// Max(现金结存 + 授信额度 − 已使用授信 − 交易维度追加保证金合计, 0); - /// 交易维度追加保证金合计 = Σ(维持保证金 − 累计保证金)(阶段三:累计=应付预付金净额;阶段四含追加保证金流水)。 + /// Max(现金结存 + 授信额度 − 已使用授信, 0)(定稿 2026-08-28:删除"−交易维度追加保证金"—— + /// 每日追保落账后现金部分已进现金结存、授信部分已进已使用授信,再减属双重扣减)。 /// - public static double CalcContractDimensionAvailable(double cashBalance, double totalCredit, double usedCredit, - double tradeAdditionalMarginSum) + public static double CalcContractDimensionAvailable(double cashBalance, double totalCredit, double usedCredit) { - return Math.Max(cashBalance + totalCredit - usedCredit - tradeAdditionalMarginSum, 0); + return Math.Max(cashBalance + totalCredit - usedCredit, 0); } /// - /// 追保金额(客户维度,MarginWatchRule=1,阶段四 §4.2 双向——不以 0 截断): - /// (维持保证金 − 初始保证金) − (现金结存 + 授信额度 − 已使用授信),正=需追保、负=可返还。 - /// 即客户维度可用资金公式的反向值(去 Max 截断)——与需求原文"现金结存+授信额度−(维持−初始)"数值互为相反数, + /// 追保金额(客户维度,MarginWatchRule=1,定稿 2026-08-28:Max(...,0) 截断,恒 ≥ 0,只追不退): + /// Max((维持保证金 − 初始保证金) − (现金结存 + 授信额度 − 已使用授信), 0),正=需追保; + /// 资金富余时为 0(富余状态由可用资金/可取资金为正表达)。 + /// 即客户维度可用资金公式反向值的 Max 截断——与需求原文"现金结存+授信额度−(维持−初始)"数值互为相反数, /// 此处按 MarginByPayableMarginTotal 字段既有口径(正数=应追加,估值报告"应追加预付金X元")定向。 /// public static double CalcClientDimensionCallMargin(double cashBalance, double totalCredit, double usedCredit, double initialMargin, double maintenanceMargin) { - return Math.Round(maintenanceMargin - initialMargin - (cashBalance + totalCredit - usedCredit), 2, MidpointRounding.AwayFromZero); + return Math.Max(Math.Round(maintenanceMargin - initialMargin - (cashBalance + totalCredit - usedCredit), 2, MidpointRounding.AwayFromZero), 0); } /// @@ -53,5 +53,28 @@ namespace YLErp.Modules.SwapModule.Margin { return Math.Max(Math.Round(-(cashBalance + totalCredit - usedCredit), 2, MidpointRounding.AwayFromZero), 0); } + + /// + /// 可取资金(客户维度,MarginWatchRule=1,定稿 2026-08-28 新增): + /// Max(现金结存 + 初始保证金 − 维持保证金 + min(持仓盈亏, 0), 0)。 + /// 可用资金含授信但授信不可取现,可取资金只算现金部分;min(持仓盈亏,0) 只扣未实现亏损、浮盈不放行; + /// 客户维度追保不产生现金流,需显式叠加保证金约束(+初始−维持)。 + /// initialMargin/maintenanceMargin 均为净收取为正。 + /// + public static double CalcClientDimensionDesirableFund(double cashBalance, double initialMargin, + double maintenanceMargin, double positionPnl) + { + return Math.Max(cashBalance + initialMargin - maintenanceMargin + Math.Min(positionPnl, 0), 0); + } + + /// + /// 可取资金(合约维度,MarginWatchRule=0,定稿 2026-08-28 新增): + /// Max(现金结存 + min(持仓盈亏, 0), 0)。 + /// 合约维度追保已通过资金记录反映在现金结存中,无需再叠加保证金约束。 + /// + public static double CalcContractDimensionDesirableFund(double cashBalance, double positionPnl) + { + return Math.Max(cashBalance + Math.Min(positionPnl, 0), 0); + } } }