Merge remote-tracking branch 'origin/glms/feature/1.4.2' into glms/feature/1.4.2
This commit is contained in:
@@ -11,7 +11,7 @@ namespace YLErp.Modules.SwapModule
|
||||
[TestClass]
|
||||
public class FundTagCalcTest
|
||||
{
|
||||
private static LegAmount Leg(long id, double amount, bool preferCredit)
|
||||
private static LegAmount Leg(long id, decimal amount, bool preferCredit)
|
||||
=> new() { Leg = new swap_position { id = id }, Amount = amount, PreferCredit = preferCredit };
|
||||
|
||||
// ================================================================
|
||||
@@ -273,7 +273,7 @@ namespace YLErp.Modules.SwapModule
|
||||
InterestPrincipalFix = fix,
|
||||
FundTag = preferCredit ? ConsFundTag.Credit : ConsFundTag.Cash
|
||||
},
|
||||
Amount = (double)fix,
|
||||
Amount = fix,
|
||||
PreferCredit = preferCredit
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合约维度追保金额(需求原文 现金+授信−已使用 的应追加方向取值):账户透支为正=应补足,盈余为负。
|
||||
/// 合约维度追保金额(Max((现金+授信−已使用)×−1, 0)):账户透支为正=应补足,盈余截断为 0。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SB_013_合约维度追保金额_透支为正()
|
||||
@@ -121,10 +121,10 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SB_014_合约维度追保金额_盈余为负()
|
||||
public void SB_014_合约维度追保金额_盈余截断为0()
|
||||
{
|
||||
//现金50 + 授信100 − 已使用20 = 130 → 追保 = −130(盈余可返还方向)
|
||||
Assert.AreEqual(-130, SwapSpanBalanceCalc.CalcContractDimensionCallMargin(
|
||||
//现金50 + 授信100 − 已使用20 = 130 → 盈余,Max(...,0) 截断 → 追保 = 0
|
||||
Assert.AreEqual(0, SwapSpanBalanceCalc.CalcContractDimensionCallMargin(
|
||||
cashBalance: 50, totalCredit: 100, usedCredit: 20), 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Modules.DataProviderModule;
|
||||
using YLErp.QdpModule;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
@@ -40,17 +41,22 @@ namespace YLErp.Modules.SwapModule
|
||||
public void SP_002_ETF收盘价源_可取()
|
||||
{
|
||||
using var db = DbContextFactory.GetYLDbContext();
|
||||
var latest = db.eod_stock_price
|
||||
.Where(x => x.ClosePrice > 0)
|
||||
.OrderByDescending(x => x.ValueDate)
|
||||
.Select(x => new { x.ValueDate, x.UnderlyingCode })
|
||||
.FirstOrDefault();
|
||||
//join underlying_manager:取数链路 InnerGetEodPrice 从标的表出发 join 价格表,
|
||||
//价格表里未登记为标的的代码(同步进来的非管理标的)取数必返回 null,冒烟样本须限定在已登记标的上
|
||||
var latest = (from ep in db.eod_stock_price
|
||||
where ep.ClosePrice > 0
|
||||
join um in db.underlying_manager on ep.UnderlyingCode equals um.UnderlyingCode
|
||||
orderby ep.ValueDate descending
|
||||
select new { ep.ValueDate, ep.UnderlyingCode }).FirstOrDefault();
|
||||
if (latest == null)
|
||||
{
|
||||
Assert.Inconclusive("dev 库无股票/ETF日终价格数据,跳过");
|
||||
}
|
||||
|
||||
Assert.IsTrue(EodPriceQueryService.TryGetEodPrice(latest.ValueDate, latest.UnderlyingCode, out var price));
|
||||
//价格表可能混有非交易日/未来日期的脏行(如 2026-08-30 周日),而取数链路会按交易日历调整日期
|
||||
//(非国君环境向后滚到下一交易日,脏行日期之后无数据 → 取不到);查询日期回退到最近交易日再验
|
||||
var queryDate = QdpCalendarHelper.GetNonHolidayDefore(latest.ValueDate.Date);
|
||||
Assert.IsTrue(EodPriceQueryService.TryGetEodPrice(queryDate, latest.UnderlyingCode, out var price));
|
||||
Assert.IsTrue(price.GetPrice(SettlementTypeEnum.ClosePrice) > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,10 +141,10 @@ namespace YLErp.Modules.SwapModule
|
||||
return ReleaseMarginByFundTagResult
|
||||
?? new UnwindTagSplit
|
||||
{
|
||||
CashMargin = Convert.ToDouble(marginAmount),
|
||||
CashRebate = Convert.ToDouble(marginLegs.Any()
|
||||
CashMargin = marginAmount,
|
||||
CashRebate = marginLegs.Any()
|
||||
? marginLegs.Sum(x => x.InterestClosePnL)
|
||||
: marginRebate)
|
||||
: marginRebate
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user