Files
zszq-trs/UnitTestProject/Modules/SwapModule/SwapSpanMarginCalcTest.cs
T

275 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using YLErp.DBModels;
using YLErp.Modules.SwapModule.Margin;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// R2 阶段三 §3.1 区间追保纯函数测试:落档边界值(恰好等于档界)、看多/看空方向、
/// 追保金额=AmountRate×期初全价×券面总额(累计到所落档位,直取)、
/// 维持保证金=(初始+总追加)×我方净收取方向。
/// 区间结构(docx 确认书追保表同型):多头 第1层[Lower,+∞)、第n层[Lower,Upper);空头 第1层(−∞,Upper]、第n层(Lower,Upper]。
/// </summary>
[TestClass]
public class SwapSpanMarginCalcTest
{
private static SpanTierConfig Tier(double? lower, double? upper, double? amountRate)
=> new() { Lower = lower, Upper = upper, AmountRate = amountRate };
/// <summary>客户看多 4 层(A=3% 口径:97/94/91/88,比例 0/3%/6%/9%</summary>
private static List<SpanTierConfig> 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),
};
/// <summary>客户看空 4 层(A=3% 口径:103/106/109/112,比例 0/3%/6%/9%</summary>
private static List<SpanTierConfig> 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<SpanTierConfig> { new SpanTierConfig(), null },
ShortSpans = new List<SpanTierConfig> { 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<SpanTierConfig> { Tier(0.97, null, 0) } }));
Assert.IsTrue(SwapSpanMarginCalc.HasSpanConfig(new SpanConfig { ShortSpans = new List<SpanTierConfig> { 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));
}
/// <summary>
/// 层间空隙不落最深层(2026-08-27 修正,交易2538实证):
/// 真实模板空头档位 (−∞,0.99](1.00,1.03](0.99,1.00] 为价格在期初附近小幅波动的"未触发追保"空隙——
/// 此前兜底一律按最深层计,ratio≈0.99999 被错误收取最深档(0.04×基数)。
/// 修正后:空隙返回 null(追加保证金按0);仅穿出最深层边界(ratio>最深上界)才按最深层计。
/// </summary>
[TestMethod]
public void SS_008_空头层间空隙_不落最深层_追加为零()
{
//模板"保证金测试01"实际配置形态:第1层 (−∞,0.99],第2~4层 (1.00,1.03]
var tiers = new List<SpanTierConfig>
{
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));
}
/// <summary>
/// 多头层间空隙同理:空隙返回 null(追加0),仅跌破最深层下界才按最深层计。
/// </summary>
[TestMethod]
public void SS_009_多头层间空隙_不落最深层()
{
var tiers = new List<SpanTierConfig>
{
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<SpanTierConfig>(), false, 1.0));
Assert.IsNull(SwapSpanMarginCalc.MatchTier(new List<SpanTierConfig> { 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);
}
/// <summary>docx 数值复算(客户看多,A=3%):期初净价98.5 → 当前净价92.0 落第3层,追加=6%×101×100000</summary>
[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);
}
/// <summary>docx 数值复算(客户看空,A=3%):期初价98.5 → 收盘价104.0 落第2层,追加=3%×101×100000</summary>
[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);
}
/// <summary>
/// BUG-25 回归:配置层未按浅→深排序时,穿出最深层的回落不依赖数组顺序——
/// 多头取最小下界层、空头取最大上界层(Last() 假定有序,乱序时回落错层)。
/// </summary>
[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<SpanTierConfig>
{
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<SpanTierConfig>
{
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);
}
}
}