using YLErp.Modules.EodModule;
namespace YLErp.Modules.SwapModule
{
///
/// 自动平仓路径(AuotoSwapUnwind → EnrichDividendIn, SwapDealService.cs:1668-1687)多次部分平仓是否多算的实证。
/// EnrichDividendIn 核心:GetBondPayments(td.StartDate, closeDate) × unwindQty(当次平仓量,非剩余持仓)。
/// 本测试直接驱动真实 BondPaymentService.CalcPayment(与 EnrichDividendIn 等价:GetBondPayments 按 reg_date 过滤 + CalcPayment × unwindQty),
/// 内存注入 reg_date 数据,不连库。完整 AuotoSwapUnwind 链路因 EnrichDividendIn 直接 new BondPaymentService 查库、无内存 seam 注入点,故用计算核心等价验证。
///
/// 结论验证:多次跨越登记日的部分平仓,每次 × 当次平仓量 → 总额 = 各批按登记日持有 × 平仓量分摊,
/// 不自洽多算、不重复计入重叠窗口。
/// (纠正此前"从建仓日重算导致重复计入"的推断:该推断误以为 CalcPayment 乘剩余持仓,实际乘当次 unwindQty。)
///
[TestClass]
public class AutoUnwindMultiPartialDividendTest
{
private const string BondCode = "230004.IB";
private static readonly DateTime StartDate = new(2026, 1, 5);
private static readonly DateTime Reg1 = new(2026, 5, 15); // 每百元付息 10
private static readonly DateTime Reg2 = new(2026, 6, 15); // 每百元付息 12
private sealed class BridgeBps : BondPaymentService
{
public BridgeBps(OptUserInfo u) : base(u) { }
protected override IQueryable QueryBondPayments(string underlyingCode)
=> new List
{
new BondPayment { underlyingCode = BondCode, reg_date = Reg1, payment_date_pl = Reg1, payment_date = Reg1, payment_interest = 10m },
new BondPayment { underlyingCode = BondCode, reg_date = Reg2, payment_date_pl = Reg2, payment_date = Reg2, payment_interest = 12m },
}.Where(x => x.underlyingCode == underlyingCode).AsQueryable();
}
// 等价于 EnrichDividendIn 的数值核心:GetBondPayments(StartDate, closeDate) × unwindQty
private static decimal EnrichOnce(DateTime closeDate, decimal unwindQty)
{
var svc = new BridgeBps(OptUserInfo.UnitTestUser);
return svc.CalcPayment(BondCode, StartDate, closeDate, unwindQty, 1, 1);
}
[TestMethod]
public void 多次部分平仓_自动路径总额按登记日持仓分摊_不自洽多算()
{
decimal totalFace = 10_000m; // 总面额 1 万元
decimal halfFace = totalFace / 2m; // 每次平一半
// 第一次 5/20 平一半:窗口(Start,5/20] 仅含 reg1 → 10 × 5000/100 = 500
var d1 = EnrichOnce(new DateTime(2026, 5, 20), halfFace);
// 第二次 6/20 平一半:窗口(Start,6/20] 含 reg1+reg2 → (10+12) × 5000/100 = 1100
var d2 = EnrichOnce(new DateTime(2026, 6, 20), halfFace);
var total = d1 + d2;
// 经济应得(登记日持有规则):
// 第一批5000元:5/15持有✓(10)、6/15未持有✗ → 10×5000/100 = 500
// 第二批5000元:5/15持有✓(10)、6/15持有✓(12) → 22×5000/100 = 1100
decimal expected = 10m * halfFace / 100m + (10m + 12m) * halfFace / 100m;
Assert.AreEqual(500m, d1, 0.001m, "第一次(5/20)只含 reg1 = 500");
Assert.AreEqual(1100m, d2, 0.001m, "第二次(6/20)含 reg1+reg2 = 1100");
Assert.AreEqual(expected, total, 0.001m,
"两次部分平仓总额 = 按登记日持有×平仓量分摊的应得值,重叠窗口不重复计同量(纠正:乘当次 unwindQty 而非剩余持仓)");
// 反证:若手动路径口径(第一次平仓即给全量待实现 = 两次分红×总面额)会多算
decimal manualFullIfFirst = (10m + 12m) * totalFace / 100m; // 2200
Assert.IsTrue(manualFullIfFirst > total,
"反证:手动全量落袋口径(2200) > 自动分摊口径(1600),多算方是手动路径而非自动路径");
}
}
}