using YLErp.DBModels; using YLErp.Modules.SwapModule.Margin; namespace YLErp.Modules.SwapModule { /// /// R2 阶段三 §3.1 引擎单笔计算场景测试(SwapSpanMarginCalc.CalcTradeMaintenanceMargin,收盘价由调用方解析后注入, /// 覆盖"需要收盘价"的各场景:取到价落档、未取到价兜底、试算初始、期初价缺省、方向与腿缺失)。 /// 收盘价解析(债券中债估值净价/ETF收盘价/取不到置0)在 MarginCalculationBase.CalcSwapSpanMaintenanceMargin 胶水层, /// 价格源本身的读库行为见 SwapSpanPriceSourceTest。 /// [TestClass] public class SwapSpanMarginEngineTest { private static SpanTierConfig Tier(double? lower, double? upper, double? amountRate) => new() { Lower = lower, Upper = upper, AmountRate = amountRate }; private static SpanConfig LongShortCfg() => new() { LongSpans = new List { Tier(0.97, null, 0.00), Tier(0.94, 0.97, 0.03), Tier(0.91, 0.94, 0.06), Tier(0.88, 0.91, 0.09) }, ShortSpans = new List { Tier(null, 1.03, 0.00), Tier(1.03, 1.06, 0.03), Tier(1.06, 1.09, 0.06), Tier(1.09, 1.12, 0.09) } }; /// 标的腿(多空),posiDirection/positionType 组合出方向 private static swap_position UnderlyingLeg(int posiDirection, int positionType, decimal grossPrice, decimal? netPrice, decimal quantity, string code = "240004.IB") => new() { PosiDirection = posiDirection, PositionType = positionType, UnderlyingCode = code, PosiGrossPrice = grossPrice, PosiNetNoFeePrice = netPrice, PosiQuantity = quantity }; /// 初始预付金腿(InterestMode=5),interestDirection 1=收取 2=支付 private static swap_position MarginLeg(int interestDirection, decimal principal) => new() { InterestMode = 5, InterestDirection = interestDirection, InterestPrincipalFix = principal }; // ================================================================ // 收盘价取到 → 落档计算 // ================================================================ /// 债券客户看多:收取端标的腿 PositionType=Short → 客户看多;净价 92.0/期初净价 98.5 落第3层 [TestMethod] public void SE_001_债券客户看多_净价落第3层() { var legs = new List { UnderlyingLeg(posiDirection: 1, positionType: 2, grossPrice: 101.0m, netPrice: 98.5m, quantity: 100000m), MarginLeg(interestDirection: 1, principal: 500000m) }; var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin( tradeInitialMargin: null, spanCfg: LongShortCfg(), legs, isInitialCalc: false, closePrice: 92.0); //追加 = 6% × 期初全价101 × 券面100000 = 606000;维持 = 500000 + 606000 Assert.AreEqual(1106000, maintenance.Value, 1e-6); } /// ETF客户看空:收取端标的腿 PositionType=Long → 客户看空;收盘 1.32/期初净价 1.25 落第2层 [TestMethod] public void SE_002_ETF客户看空_收盘价落第2层() { var legs = new List { UnderlyingLeg(posiDirection: 1, positionType: 1, grossPrice: 1.25m, netPrice: 1.25m, quantity: 1000000m, code: "511010.SH"), MarginLeg(interestDirection: 1, principal: 200000m) }; var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin( tradeInitialMargin: null, spanCfg: LongShortCfg(), legs, isInitialCalc: false, closePrice: 1.32); //ratio=1.056 ∈ (1.03,1.06] → 追加 = 3% × 1.25 × 1000000 = 37500;维持 = 200000 + 37500 Assert.AreEqual(237500, maintenance.Value, 1e-6); } /// 收盘价大幅下跌穿出最深层:按最深层比例计(追加=9%),不叠加也不归零 [TestMethod] public void SE_003_价格穿出最深层_按最深层计() { var legs = new List { UnderlyingLeg(1, 2, grossPrice: 101.0m, netPrice: 98.5m, quantity: 100000m), MarginLeg(1, 500000m) }; var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, false, closePrice: 80.0); Assert.AreEqual(500000 + 0.09 * 101.0 * 100000, maintenance.Value, 1e-6); } // ================================================================ // 收盘价未取到 / 试算 —— 兜底行为 // ================================================================ /// 当天无收盘价(closePrice=0):不抛错,追加按0、维持=初始保证金(引擎侧另记告警日志) [TestMethod] public void SE_004_无收盘价_追加为0维持等于初始() { var legs = new List { UnderlyingLeg(1, 2, 101.0m, 98.5m, 100000m), MarginLeg(1, 500000m) }; var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, isInitialCalc: false, closePrice: 0); Assert.AreEqual(500000, maintenance.Value, 1e-6); } /// 试算初始(isInitialCalc=true):即使有收盘价也只出初始项——追加保证金为收盘后口径,试算不产出 [TestMethod] public void SE_005_试算初始_不参与追加() { var legs = new List { UnderlyingLeg(1, 2, 101.0m, 98.5m, 100000m), MarginLeg(1, 500000m) }; var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, isInitialCalc: true, closePrice: 92.0); Assert.AreEqual(500000, maintenance.Value, 1e-6); } /// 期初净价缺失:比基回落期初全价(close/initGross) [TestMethod] public void SE_006_期初净价缺省_回落期初全价() { var legs = new List { UnderlyingLeg(1, 2, grossPrice: 98.5m, netPrice: null, quantity: 100000m), MarginLeg(1, 500000m) }; //close=92.0/98.5 落第3层 var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, false, closePrice: 92.0); Assert.AreEqual(500000 + 0.06 * 98.5 * 100000, maintenance.Value, 1e-6); } // ================================================================ // 腿数据缺失与方向 // ================================================================ /// 无标的腿(缺期初价):返回 null,引擎跳过该交易不产出 trade_span [TestMethod] public void SE_007_无标的腿_返回null() { var legs = new List { MarginLeg(1, 500000m) }; Assert.IsNull(SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, false, closePrice: 92.0)); Assert.IsNull(SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), new List(), false, 92.0)); } /// 我方支付初始保证金(InterestDirection=支付):方向取 −1,维持保证金为负(客户应收) [TestMethod] public void SE_008_我方支付预付金_维持为负() { var legs = new List { UnderlyingLeg(1, 2, 101.0m, 98.5m, 100000m), MarginLeg(interestDirection: 2, principal: 500000m) }; var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, false, closePrice: 92.0); Assert.AreEqual(-(500000 + 0.06 * 101.0 * 100000), maintenance.Value, 1e-6); } /// 无预付金腿:初始保证金回落交易录入值 trade.InitialMargin(客户应付常态,方向+1) [TestMethod] public void SE_009_无预付金腿_回落交易录入初始保证金() { var legs = new List { UnderlyingLeg(1, 2, 101.0m, 98.5m, 100000m) }; var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(300000, LongShortCfg(), legs, false, closePrice: 92.0); Assert.AreEqual(300000 + 0.06 * 101.0 * 100000, maintenance.Value, 1e-6); } /// 多条预付金腿按收付净额定初始与方向(净支付 → −1) [TestMethod] public void SE_010_多预付金腿_按净收取定方向() { var legs = new List { UnderlyingLeg(1, 2, 101.0m, 98.5m, 100000m), MarginLeg(1, 200000m), //收取 20万 MarginLeg(2, 500000m) //支付 50万 → 净支付 30万 }; var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, false, closePrice: 0); Assert.AreEqual(-300000, maintenance.Value, 1e-6); } } }