Files
zszq-trs/UnitTestProject/Modules/SwapModule/SwapSpanMarginEngineTest.cs

188 lines
9.5 KiB
C#
Raw Permalink 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 引擎单笔计算场景测试(SwapSpanMarginCalc.CalcTradeMaintenanceMargin,收盘价由调用方解析后注入,
/// 覆盖"需要收盘价"的各场景:取到价落档、未取到价兜底、试算初始、期初价缺省、方向与腿缺失)。
/// 收盘价解析(债券中债估值净价/ETF收盘价/取不到置0)在 MarginCalculationBase.CalcSwapSpanMaintenanceMargin 胶水层,
/// 价格源本身的读库行为见 SwapSpanPriceSourceTest。
/// </summary>
[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<SpanTierConfig>
{
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<SpanTierConfig>
{
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)
}
};
/// <summary>标的腿(多空),posiDirection/positionType 组合出方向</summary>
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
};
/// <summary>初始预付金腿(InterestMode=5),interestDirection 1=收取 2=支付</summary>
private static swap_position MarginLeg(int interestDirection, decimal principal)
=> new() { InterestMode = 5, InterestDirection = interestDirection, InterestPrincipalFix = principal };
// ================================================================
// 收盘价取到 → 落档计算
// ================================================================
/// <summary>债券客户看多:收取端标的腿 PositionType=Short → 客户看多;净价 92.0/期初净价 98.5 落第3层</summary>
[TestMethod]
public void SE_001_债券客户看多_净价落第3层()
{
var legs = new List<swap_position>
{
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);
}
/// <summary>ETF客户看空:收取端标的腿 PositionType=Long → 客户看空;收盘 1.32/期初净价 1.25 落第2层</summary>
[TestMethod]
public void SE_002_ETF客户看空_收盘价落第2层()
{
var legs = new List<swap_position>
{
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);
}
/// <summary>收盘价大幅下跌穿出最深层:按最深层比例计(追加=9%),不叠加也不归零</summary>
[TestMethod]
public void SE_003_价格穿出最深层_按最深层计()
{
var legs = new List<swap_position>
{
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);
}
// ================================================================
// 收盘价未取到 / 试算 —— 兜底行为
// ================================================================
/// <summary>当天无收盘价(closePrice=0):不抛错,追加按0、维持=初始保证金(引擎侧另记告警日志)</summary>
[TestMethod]
public void SE_004_无收盘价_追加为0维持等于初始()
{
var legs = new List<swap_position>
{
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);
}
/// <summary>试算初始(isInitialCalc=true):即使有收盘价也只出初始项——追加保证金为收盘后口径,试算不产出</summary>
[TestMethod]
public void SE_005_试算初始_不参与追加()
{
var legs = new List<swap_position>
{
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);
}
/// <summary>期初净价缺失:比基回落期初全价(close/initGross</summary>
[TestMethod]
public void SE_006_期初净价缺省_回落期初全价()
{
var legs = new List<swap_position>
{
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);
}
// ================================================================
// 腿数据缺失与方向
// ================================================================
/// <summary>无标的腿(缺期初价):返回 null,引擎跳过该交易不产出 trade_span</summary>
[TestMethod]
public void SE_007_无标的腿_返回null()
{
var legs = new List<swap_position> { MarginLeg(1, 500000m) };
Assert.IsNull(SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, false, closePrice: 92.0));
Assert.IsNull(SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), new List<swap_position>(), false, 92.0));
}
/// <summary>我方支付初始保证金(InterestDirection=支付):方向取 −1,维持保证金为负(客户应收)</summary>
[TestMethod]
public void SE_008_我方支付预付金_维持为负()
{
var legs = new List<swap_position>
{
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);
}
/// <summary>无预付金腿:初始保证金回落交易录入值 trade.InitialMargin(客户应付常态,方向+1</summary>
[TestMethod]
public void SE_009_无预付金腿_回落交易录入初始保证金()
{
var legs = new List<swap_position> { 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);
}
/// <summary>多条预付金腿按收付净额定初始与方向(净支付 → −1)</summary>
[TestMethod]
public void SE_010_多预付金腿_按净收取定方向()
{
var legs = new List<swap_position>
{
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);
}
}
}