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