Files
zszq-trs/UnitTestProject/Modules/SwapModule/Margin/SwapAdditionalMarginLegTest.cs
T
2026-08-27 18:40:46 +08:00

226 lines
12 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>
/// EOD 追保腿化改造测试(纯函数层:腿构造、资金来源分配、幂等识别、跨日水位)。
/// 服务层(SettleAdditionalMargin 的 DB 写入编排)依赖真实库,无内存测试基建——
/// 此处按服务内实际调用顺序组合 SwapAdditionalMarginCalc / ConsFundTag / FundTagCalc 纯函数验证等价语义:
/// 增量>0 → BuildEodMarginLeg 建腿 → PreferCredit(腿标签, 交易级资金来源) 回退 → AllocateByLegPreference 分配
/// = ApplyMarginFundTags 内部分配),拆单守恒走 ApplySaveTimeSplit(与 SplitLeg 同口径倒挤)。
/// </summary>
[TestClass]
public class SwapAdditionalMarginLegTest
{
private static readonly DateTime SettleDate = new(2026, 8, 27);
private static trade NewTrade(string fundSource = ConsFundTag.Credit)
=> new()
{
id = 2543,
TradeNumber = "TRS2026-2543",
StartDate = new DateTime(2026, 8, 20),
ExerciseDate = new DateTime(2027, 8, 20),
SettlementCurrency = "CNY",
MarginFundSource = fundSource
};
/// <summary>
/// 目标>累计 → 生成 1 条 mode6 追保腿:与手工追加预付金同形态
/// (资金腿、IsInitial、收取方向、fix=增量两位 AwayFromZero 舍入、HappenDate=结算日、FundTag=NULL 回退、OptName 打标)。
/// </summary>
[TestMethod]
public void AM_LEG_001_目标大于累计_生成追保腿()
{
var td = NewTrade();
var target = SwapAdditionalMarginCalc.CalcTarget(2_808_000, 2_000_000);
Assert.AreEqual(808_000, target, 1e-6);
var leg = SwapAdditionalMarginService.BuildEodMarginLeg(td, target, SettleDate, optId: 0);
Assert.AreEqual(2543, leg.SwapTradeId);
Assert.AreEqual(0, leg.PositionType);
Assert.AreEqual(0, leg.PosiDirection);
Assert.IsTrue(leg.IsInitial);
Assert.AreEqual((int)InterestModeEnum.追加预付金, leg.InterestMode);
Assert.AreEqual((int)SwapDirectionEnum.收取, leg.InterestDirection);
Assert.AreEqual(808_000m, leg.InterestPrincipalFix);
Assert.AreEqual(SettleDate, leg.HappenDate);
Assert.IsNull(leg.FundTag);
Assert.AreEqual(SwapAdditionalMarginService.EodOptName, leg.OptName);
Assert.AreEqual("EOD追保", SwapAdditionalMarginService.EodOptName);
//两位 AwayFromZero 舍入
var rounded = SwapAdditionalMarginService.BuildEodMarginLeg(td, 1000.005, SettleDate, optId: 0);
Assert.AreEqual(1000.01m, rounded.InterestPrincipalFix);
//币种:取交易结算币种;交易未设置时回退系统默认 CNY(与存量手工腿口径一致,2026-08-27 修复空币种)
Assert.AreEqual("CNY", leg.Currency);
td.SettlementCurrency = null;
Assert.AreEqual(ConsGlobal.Currency.CNY,
SwapAdditionalMarginService.BuildEodMarginLeg(td, 100, SettleDate, optId: 0).Currency);
}
/// <summary>
/// 授信充足(腿未选 → 回退交易级 Credit):整腿定稿授信占用,现金部分 0(不产生现金流水)。
/// 占用 remark 必须带"追加保证金"前缀(累计口径与清理链路的硬性识别点)。
/// </summary>
[TestMethod]
public void AM_LEG_002_授信充足_全额授信零现金流水()
{
var td = NewTrade(ConsFundTag.Credit);
var leg = SwapAdditionalMarginService.BuildEodMarginLeg(td, 1000, SettleDate, optId: 0);
Assert.IsTrue(ConsFundTag.PreferCredit(leg.FundTag, td.MarginFundSource));
var plans = FundTagCalc.AllocateByLegPreference(
new List<LegAmount> { new() { Leg = leg, Amount = 1000, PreferCredit = true } },
creditAvailable: 5_000, ignoreMoneyCheck: false);
Assert.AreEqual(1000, plans[0].CreditAmount);
Assert.AreEqual(0, plans[0].CashAmount);
Assert.IsFalse(plans[0].NeedSplit);
//CashAmount==0 → ApplyMarginFundTags 不写现金流水;占用 remark 前缀断言
Assert.IsTrue((ClientCreditInoutService.AdditionalMarginRemark + "占用")
.StartsWith(ClientCreditInoutService.AdditionalMarginRemark));
Assert.IsTrue((ClientCreditInoutService.AdditionalMarginRemark + "拆单授信部分")
.StartsWith(ClientCreditInoutService.AdditionalMarginRemark));
}
/// <summary>
/// 额度不足 → 跨界拆单:原腿保留授信部分标 Credit,克隆现金差额腿标 Cash,
/// 两腿 fix 倒挤守恒(现金流水绑现金腿 id,Deal=现金腿id 由 ApplyMarginFundTags 保证)。
/// </summary>
[TestMethod]
public void AM_LEG_003_额度不足_拆腿加现金流水()
{
var td = NewTrade(ConsFundTag.Credit);
var leg = SwapAdditionalMarginService.BuildEodMarginLeg(td, 1000, SettleDate, optId: 0);
var legs = new List<LegAmount> { new() { Leg = leg, Amount = 1000, PreferCredit = true } };
var plans = FundTagCalc.AllocateByLegPreference(legs, creditAvailable: 300, ignoreMoneyCheck: false);
Assert.IsTrue(plans[0].NeedSplit);
Assert.AreEqual(300, plans[0].CreditAmount);
Assert.AreEqual(700, plans[0].CashAmount);
//ApplySaveTimeSplit 与 SplitLeg 同口径(原腿=授信部分、克隆现金腿倒挤守恒)
var newLegs = FundTagCalc.ApplySaveTimeSplit(legs, plans);
Assert.AreEqual(1, newLegs.Count);
Assert.AreEqual(300m, leg.InterestPrincipalFix);
Assert.AreEqual(ConsFundTag.Credit, leg.FundTag);
var cashLeg = newLegs[0];
Assert.AreEqual(700m, cashLeg.InterestPrincipalFix);
Assert.AreEqual(ConsFundTag.Cash, cashLeg.FundTag);
Assert.AreEqual(1000m, leg.InterestPrincipalFix + cashLeg.InterestPrincipalFix);
//拆单两腿合计=增量,且现金腿为独立期初腿(流水 Deal 绑它)
Assert.AreEqual(0, cashLeg.id);
Assert.AreEqual(0, cashLeg.PositionId);
Assert.IsTrue(cashLeg.IsInitial);
}
/// <summary>
/// margin_fund_source=Cash:腿未选回退交易级现金 → 整腿定稿现金、全额现金流水、零授信占用。
/// </summary>
[TestMethod]
public void AM_LEG_004_交易级现金来源_全额现金零占用()
{
var td = NewTrade(ConsFundTag.Cash);
var leg = SwapAdditionalMarginService.BuildEodMarginLeg(td, 1000, SettleDate, optId: 0);
Assert.IsFalse(ConsFundTag.PreferCredit(leg.FundTag, td.MarginFundSource));
var plans = FundTagCalc.AllocateByLegPreference(
new List<LegAmount> { new() { Leg = leg, Amount = 1000, PreferCredit = false } },
creditAvailable: 5_000, ignoreMoneyCheck: false);
Assert.AreEqual(0, plans[0].CreditAmount);
Assert.AreEqual(1000, plans[0].CashAmount);
Assert.IsFalse(plans[0].NeedSplit);
}
/// <summary>
/// 增量≤0(追保回落/已补足)→ 不产生新腿:目标<已补足 与 目标=0 两种情形均跳过。
/// </summary>
[TestMethod]
public void AM_LEG_005_增量非正_不产生新腿()
{
//已补足:目标 50,已补足 80 → 增量 -30 → 跳过
var target = SwapAdditionalMarginCalc.CalcTarget(150, 100);
var increment = Math.Round(target - 80, 2, MidpointRounding.AwayFromZero);
Assert.IsTrue(increment <= 0);
//追保回落到应付之下:目标 0 → 增量 ≤0 → 跳过(超付不返还,负缺口走可用资金公式)
var fallen = SwapAdditionalMarginCalc.CalcTarget(50, 100);
Assert.AreEqual(0, fallen, 1e-6);
Assert.IsTrue(Math.Round(fallen - 80, 2, MidpointRounding.AwayFromZero) <= 0);
}
/// <summary>
/// 幂等:EOD 腿识别(OptName 打标 + mode6 + 重跑窗口)——手工腿(OptName=操作员)与
/// 窗口外更早历史腿不受影响;重跑时旧腿金额已计入已补足 → 增量 0 不翻倍。
/// </summary>
[TestMethod]
public void AM_LEG_006_幂等识别与重跑不翻倍()
{
var td = NewTrade();
var eodLeg = SwapAdditionalMarginService.BuildEodMarginLeg(td, 80, SettleDate, optId: 0);
var manualLeg = new swap_position
{
InterestMode = (int)InterestModeEnum.追加预付金,
OptName = "张三",
HappenDate = SettleDate
};
var initLeg = new swap_position
{
InterestMode = (int)InterestModeEnum.初始预付金,
OptName = SwapAdditionalMarginService.EodOptName,
HappenDate = SettleDate
};
var earlierEodLeg = SwapAdditionalMarginService.BuildEodMarginLeg(td, 50, SettleDate.AddDays(-2), optId: 0);
Assert.IsTrue(SwapAdditionalMarginService.IsEodMarginLeg(eodLeg, SettleDate));
Assert.IsFalse(SwapAdditionalMarginService.IsEodMarginLeg(manualLeg, SettleDate));
Assert.IsFalse(SwapAdditionalMarginService.IsEodMarginLeg(initLeg, SettleDate));
//重跑窗口起点防误删更早历史日已归属腿
Assert.IsFalse(SwapAdditionalMarginService.IsEodMarginLeg(earlierEodLeg, SettleDate));
Assert.IsTrue(SwapAdditionalMarginService.IsEodMarginLeg(earlierEodLeg, SettleDate.AddDays(-2)));
//重跑:旧腿簿记已计入已补足(现金 30 + 授信 50 = 80)→ 目标不变、增量 0
var target = SwapAdditionalMarginCalc.CalcTarget(180, 100);
var rerunIncrement = Math.Round(target - 30 - 50, 2, MidpointRounding.AwayFromZero);
Assert.AreEqual(0, rerunIncrement, 1e-6);
}
/// <summary>
/// 跨日序列(映射交易2538实例):首日补足水位后,维保回落不返还、回升只补差额,累计已补足不越目标水位。
/// </summary>
[TestMethod]
public void AM_LEG_007_跨日序列_不越水位()
{
const double payableNet = 2_000_000; //初始预付金全走授信(占用净额)
var fundedLegs = new List<double>();
//D1 维持 2,808,000 → 目标 808,000 → 腿1
var day1Target = SwapAdditionalMarginCalc.CalcTarget(2_808_000, payableNet);
var day1Increment = Math.Round(day1Target - fundedLegs.Sum(), 2, MidpointRounding.AwayFromZero);
Assert.AreEqual(808_000, day1Increment, 1e-6);
fundedLegs.Add(day1Increment);
//D2 维保回落至 2,500,000 → 目标 500,000 < 已补足 → 无新腿(不返还)
var day2Target = SwapAdditionalMarginCalc.CalcTarget(2_500_000, payableNet);
var day2Increment = Math.Round(day2Target - fundedLegs.Sum(), 2, MidpointRounding.AwayFromZero);
Assert.IsTrue(day2Increment <= 0);
//D3 维保回升至 3,000,000 → 目标 1,000,000 已补足 808,000 = 增量 192,000 → 腿2
var day3Target = SwapAdditionalMarginCalc.CalcTarget(3_000_000, payableNet);
var day3Increment = Math.Round(day3Target - fundedLegs.Sum(), 2, MidpointRounding.AwayFromZero);
Assert.AreEqual(192_000, day3Increment, 1e-6);
fundedLegs.Add(day3Increment);
//D3 重跑:已补足=目标 → 增量 0,总量恒等不越水位
var day3Rerun = Math.Round(day3Target - fundedLegs.Sum(), 2, MidpointRounding.AwayFromZero);
Assert.AreEqual(0, day3Rerun, 1e-6);
Assert.AreEqual(day3Target, fundedLegs.Sum(), 1e-6);
}
}
}