226 lines
11 KiB
C#
226 lines
11 KiB
C#
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));
|
||
}
|
||
|
||
[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);
|
||
}
|
||
}
|
||
}
|