using YLErp.DBModels; using YLErp.Modules.SwapModule.Margin; namespace YLErp.Modules.SwapModule { /// /// R2 阶段三 §3.1 区间追保纯函数测试:落档边界值(恰好等于档界)、看多/看空方向、 /// 追保金额=AmountRate×期初全价×券面总额(累计到所落档位,直取)、 /// 维持保证金=(初始+总追加)×我方净收取方向。 /// 区间结构(docx 确认书追保表同型):多头 第1层[Lower,+∞)、第n层[Lower,Upper);空头 第1层(−∞,Upper]、第n层(Lower,Upper]。 /// [TestClass] public class SwapSpanMarginCalcTest { private static SpanTierConfig Tier(double? lower, double? upper, double? amountRate) => new() { Lower = lower, Upper = upper, AmountRate = amountRate }; /// 客户看多 4 层(A=3% 口径:97/94/91/88,比例 0/3%/6%/9%) private static List LongTiers() => new() { 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), }; /// 客户看空 4 层(A=3% 口径:103/106/109/112,比例 0/3%/6%/9%) private static List ShortTiers() => new() { 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), }; // ================================================================ // HasSpanConfig:方案B结构判定(存量 x/y 配置回落旧公式) // ================================================================ [TestMethod] public void SS_001_空配置_视为存量xy() { Assert.IsFalse(SwapSpanMarginCalc.HasSpanConfig(null)); Assert.IsFalse(SwapSpanMarginCalc.HasSpanConfig(new SpanConfig())); Assert.IsFalse(SwapSpanMarginCalc.HasSpanConfig(new SpanConfig { LongSpans = new List { new SpanTierConfig(), null }, ShortSpans = new List { new SpanTierConfig() } })); } [TestMethod] public void SS_002_任一方向或线有值_视为新结构() { Assert.IsTrue(SwapSpanMarginCalc.HasSpanConfig(new SpanConfig { WarnLine = 0.91 })); Assert.IsTrue(SwapSpanMarginCalc.HasSpanConfig(new SpanConfig { LongSpans = new List { Tier(0.97, null, 0) } })); Assert.IsTrue(SwapSpanMarginCalc.HasSpanConfig(new SpanConfig { ShortSpans = new List { Tier(null, 1.03, 0) } })); } // ================================================================ // IsCustomerLong:与确认书同规则(收取端同向、支付端反向,客户取反) // ================================================================ [TestMethod] public void SS_003_客户方向判定() { //我方收取端:PositionType 同向 → 我方多头 → 客户看空 Assert.IsFalse(SwapSpanMarginCalc.IsCustomerLong(posiDirection: 1, positionType: 1)); Assert.IsTrue(SwapSpanMarginCalc.IsCustomerLong(posiDirection: 1, positionType: 2)); //我方支付端:PositionType 反向 → 我方空头 → 客户看多 Assert.IsTrue(SwapSpanMarginCalc.IsCustomerLong(posiDirection: 2, positionType: 1)); Assert.IsFalse(SwapSpanMarginCalc.IsCustomerLong(posiDirection: 2, positionType: 2)); } // ================================================================ // MatchTier 多头:[Lower, Upper),第1层上不封顶;边界恰好等于档界 // ================================================================ [TestMethod] public void SS_004_多头落档_区间内与边界值() { var tiers = LongTiers(); //未触发(价格充足)→ 第1层 Assert.AreSame(tiers[0], SwapSpanMarginCalc.MatchTier(tiers, true, 1.00)); Assert.AreSame(tiers[0], SwapSpanMarginCalc.MatchTier(tiers, true, 0.97)); //恰好=第1层下界 //第2层 [0.94, 0.97) Assert.AreSame(tiers[1], SwapSpanMarginCalc.MatchTier(tiers, true, 0.9699)); Assert.AreSame(tiers[1], SwapSpanMarginCalc.MatchTier(tiers, true, 0.94)); //恰好=第2层下界 //第3层 [0.91, 0.94) Assert.AreSame(tiers[2], SwapSpanMarginCalc.MatchTier(tiers, true, 0.9399)); Assert.AreSame(tiers[2], SwapSpanMarginCalc.MatchTier(tiers, true, 0.91)); //第4层 [0.88, 0.91) Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, true, 0.9099)); Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, true, 0.88)); //恰好=最深层下界 } [TestMethod] public void SS_005_多头穿出最深层_按最深层计() { var tiers = LongTiers(); Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, true, 0.8799)); Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, true, 0.50)); } // ================================================================ // MatchTier 空头:(Lower, Upper],第1层下不设限;边界恰好等于档界 // ================================================================ [TestMethod] public void SS_006_空头落档_区间内与边界值() { var tiers = ShortTiers(); Assert.AreSame(tiers[0], SwapSpanMarginCalc.MatchTier(tiers, false, 1.00)); Assert.AreSame(tiers[0], SwapSpanMarginCalc.MatchTier(tiers, false, 1.03)); //恰好=第1层上界 //第2层 (1.03, 1.06] Assert.AreSame(tiers[1], SwapSpanMarginCalc.MatchTier(tiers, false, 1.0301)); Assert.AreSame(tiers[1], SwapSpanMarginCalc.MatchTier(tiers, false, 1.06)); //恰好=第2层上界 //第3层 (1.06, 1.09] Assert.AreSame(tiers[2], SwapSpanMarginCalc.MatchTier(tiers, false, 1.0601)); Assert.AreSame(tiers[2], SwapSpanMarginCalc.MatchTier(tiers, false, 1.09)); //第4层 (1.09, 1.12] Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, false, 1.0901)); Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, false, 1.12)); } [TestMethod] public void SS_007_空头穿出最深层_按最深层计() { var tiers = ShortTiers(); Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, false, 1.1201)); Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, false, 2.00)); } [TestMethod] public void SS_008_无可用层_返回null() { Assert.IsNull(SwapSpanMarginCalc.MatchTier(null, true, 0.9)); Assert.IsNull(SwapSpanMarginCalc.MatchTier(new List(), false, 1.0)); Assert.IsNull(SwapSpanMarginCalc.MatchTier(new List { new SpanTierConfig() }, true, 0.9)); } // ================================================================ // 追保金额与维持保证金公式 // ================================================================ [TestMethod] public void SS_009_追保金额_比例乘期初全价乘券面() { //第2层 3%:0.03 × 期初全价101.5 × 券面10000 = 30450 Assert.AreEqual(30450, SwapSpanMarginCalc.CalcAdditionalMargin(Tier(0.94, 0.97, 0.03), 101.5, 10000), 1e-6); //第1层 0% → 0 Assert.AreEqual(0, SwapSpanMarginCalc.CalcAdditionalMargin(Tier(0.97, null, 0), 101.5, 10000), 1e-6); //未配置比例/null 层 → 0 Assert.AreEqual(0, SwapSpanMarginCalc.CalcAdditionalMargin(null, 101.5, 10000), 1e-6); Assert.AreEqual(0, SwapSpanMarginCalc.CalcAdditionalMargin(Tier(0.94, 0.97, null), 101.5, 10000), 1e-6); } [TestMethod] public void SS_010_维持保证金_初始加追加乘方向() { //我方净收取 +1 Assert.AreEqual(80450, SwapSpanMarginCalc.CalcMaintenanceMargin(50000, 30450, 1), 1e-6); //我方净支付 −1(负值=客户应收) Assert.AreEqual(-80450, SwapSpanMarginCalc.CalcMaintenanceMargin(50000, 30450, -1), 1e-6); } /// docx 数值复算(客户看多,A=3%):期初净价98.5 → 当前净价92.0 落第3层,追加=6%×101×100000 [TestMethod] public void SS_011_客户看多数值复算() { var tiers = LongTiers(); var ratio = 92.0 / 98.5; var tier = SwapSpanMarginCalc.MatchTier(tiers, isCustomerLong: true, priceRatio: ratio); Assert.AreSame(tiers[2], tier); var additional = SwapSpanMarginCalc.CalcAdditionalMargin(tier, initPrice: 101.0, quantity: 100000); Assert.AreEqual(0.06 * 101.0 * 100000, additional, 1e-6); var maintenance = SwapSpanMarginCalc.CalcMaintenanceMargin(500000, additional, direction: 1); Assert.AreEqual(500000 + 0.06 * 101.0 * 100000, maintenance, 1e-6); } /// docx 数值复算(客户看空,A=3%):期初价98.5 → 收盘价104.0 落第2层,追加=3%×101×100000 [TestMethod] public void SS_012_客户看空数值复算() { var tiers = ShortTiers(); var ratio = 104.0 / 98.5; var tier = SwapSpanMarginCalc.MatchTier(tiers, isCustomerLong: false, priceRatio: ratio); Assert.AreSame(tiers[1], tier); var additional = SwapSpanMarginCalc.CalcAdditionalMargin(tier, initPrice: 101.0, quantity: 100000); Assert.AreEqual(0.03 * 101.0 * 100000, additional, 1e-6); } /// /// BUG-25 回归:配置层未按浅→深排序时,穿出最深层的回落不依赖数组顺序—— /// 多头取最小下界层、空头取最大上界层(Last() 假定有序,乱序时回落错层)。 /// [TestMethod] public void SS_013_穿层回落_乱序配置按边界取最深层() { //多头乱序(浅→深:0.97/0.94/0.91/0.88 → 打乱为 0.91/0.97/0.88/0.94) var longShuffled = new List { Tier(0.91, 0.94, 0.06), Tier(0.97, null, 0.00), Tier(0.88, 0.91, 0.09), Tier(0.94, 0.97, 0.03), }; var longDeepest = SwapSpanMarginCalc.MatchTier(longShuffled, isCustomerLong: true, priceRatio: 0.85); Assert.AreEqual(0.88, longDeepest.Lower); Assert.AreEqual(0.09, longDeepest.AmountRate); //空头乱序(浅→深:1.03/1.06/1.09/1.12 → 打乱为 1.09/1.03/1.12/1.06) var shortShuffled = new List { Tier(1.06, 1.09, 0.06), Tier(null, 1.03, 0.00), Tier(1.09, 1.12, 0.09), Tier(1.03, 1.06, 0.03), }; var shortDeepest = SwapSpanMarginCalc.MatchTier(shortShuffled, isCustomerLong: false, priceRatio: 1.15); Assert.AreEqual(1.12, shortDeepest.Upper); Assert.AreEqual(0.09, shortDeepest.AmountRate); } } }