188 lines
9.5 KiB
C#
188 lines
9.5 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|