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));
}
///
/// 层间空隙不落最深层(2026-08-27 修正,交易2538实证):
/// 真实模板空头档位 (−∞,0.99]∪(1.00,1.03],(0.99,1.00] 为价格在期初附近小幅波动的"未触发追保"空隙——
/// 此前兜底一律按最深层计,ratio≈0.99999 被错误收取最深档(0.04×基数)。
/// 修正后:空隙返回 null(追加保证金按0);仅穿出最深层边界(ratio>最深上界)才按最深层计。
///
[TestMethod]
public void SS_008_空头层间空隙_不落最深层_追加为零()
{
//模板"保证金测试01"实际配置形态:第1层 (−∞,0.99],第2~4层 (1.00,1.03]
var tiers = new List
{
Tier(null, 0.99, 0.01),
Tier(1.00, 1.01, 0.02),
Tier(1.01, 1.02, 0.03),
Tier(1.02, 1.03, 0.04),
};
//交易2538 08-24:净价100 → ratio = 100/100.0011 ≈ 0.99999 落 (0.99,1.00] 空隙 → 不追保
Assert.IsNull(SwapSpanMarginCalc.MatchTier(tiers, false, 0.99999));
Assert.IsNull(SwapSpanMarginCalc.MatchTier(tiers, false, 0.995));
//空隙边界归第1层
Assert.AreSame(tiers[0], SwapSpanMarginCalc.MatchTier(tiers, false, 0.99));
//空隙上方正常落档
Assert.AreSame(tiers[1], SwapSpanMarginCalc.MatchTier(tiers, false, 1.005));
Assert.AreSame(tiers[2], SwapSpanMarginCalc.MatchTier(tiers, false, 1.015));
Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, false, 1.025));
//穿出最深层上界 → 按最深层计(原有语义不变)
Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, false, 1.0301));
}
///
/// 多头层间空隙同理:空隙返回 null(追加0),仅跌破最深层下界才按最深层计。
///
[TestMethod]
public void SS_009_多头层间空隙_不落最深层()
{
var tiers = new List
{
Tier(0.98, null, 0.01),
Tier(0.96, 0.97, 0.03), //留出 (0.97,0.98) 空隙
Tier(0.95, 0.96, 0.04),
};
Assert.IsNull(SwapSpanMarginCalc.MatchTier(tiers, true, 0.975));
Assert.AreSame(tiers[1], SwapSpanMarginCalc.MatchTier(tiers, true, 0.9699));
Assert.AreSame(tiers[2], SwapSpanMarginCalc.MatchTier(tiers, true, 0.955));
//穿出最深层下界 → 按最深层计(原有语义不变)
Assert.AreSame(tiers[2], SwapSpanMarginCalc.MatchTier(tiers, true, 0.94));
}
[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);
}
}
}