Files
zszq-trs/UnitTestProject/Modules/SwapModule/InterestEodTailSnapshotTest.cs
T
hjhan 6e2cae7c1f test(swap): EQD-6968 自洽化契约钉死——事件利率确定性+快照再定盘载体+CI_007/008 新口径
- Determinism_NoTail_EventFloatRateIndependentOfPublishTime:尾日价缺(上午) vs
  有(下午)两世界,金额与事件 FloatRate 完全一致(=末段已消费利率0.01425)
- CloseOnly_平仓日为重置日_剩余持仓快照再定盘(+非重置日反例):经 DealInterests
  真路由断言快照 FloatRate=当日新定盘(RefixCalls 计数)——载体职责显式化验收
- CalcFirst 用例去掉 closeRate 免疫 hack:priorValueDate 根治后首重置日恒取价
- CI_007 更名+断言翻转:平仓日=重置日+不算尾 → 事件利率=末段已消费利率(旧);
  原断言(取新定盘)正是被替换的旧口径契约,根因注释留痕 GLMS-JIATT-20260805
- CI_008:本金结转断言保留,利率断言改为末段旧利率

验证:ConsumedInterestScenario 9/9、InterestEodTailSnapshot+Fr007 36/36、
全量 976 例 145 败与基线 diff=0
2026-08-19 11:07:10 +08:00

320 lines
18 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 Newtonsoft.Json;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// L1(类内去重)前置安全网:DealInterests 四分支中无 golden 语料的三格
/// AutoSettle / AutoSettleWithClose / CloseOnly)尾部滚存字段特征化快照。
///
/// - ManualSwap / RollForward 两格已由 DealInterestsGoldenReplayTest 语料钉住
/// (字段集见 GoldenReplayFramework.EodPositionToJson)。
/// - 本测试钉"现状行为":L1 抽共享助手(腿字段拷贝段 + 滚存收尾段)前后,
/// 以下字段必须逐字段不变。变化=去重改了口径。
/// - 同时断言接 seam 指纹(哪个计息接缝 + eventType)与 autoInterests 收集行为,
/// 兼作 L2(按腿拆类)的路由验收。
/// - 计息金额由受控 CalcResult 注入(不连库、不依赖真实计息引擎)。
/// </summary>
[TestClass]
public class InterestEodTailSnapshotTest
{
private const decimal Principal = 10000m;
private const decimal Rate = 0.03m;
private static readonly DateTime StartDate = new(2026, 4, 27);
private static readonly DateTime SettleDate = StartDate.AddDays(10); // 第10天收盘
private const decimal Accrued10d = 8.22m; // 受控:10天理论应结
private const decimal DailyNew = 0.82m; // 受控:当日新增
private const decimal ManualSettled = 3.5m; // 受控:盘中平仓已结
private const decimal Remaining = 7000m; // 平仓后剩余本金
private const decimal ClosedNotional = 3000m; // 本次平掉本金
private sealed class TailStubService : TestableSwapEodPositionService
{
public TailStubService() : base(nameof(InterestEodTailSnapshotTest)) { }
/// <summary>受控计息结果:两个计息 seam 均返回它</summary>
public List<swap_flow_event> CalcResult { get; set; } = new();
public string LastCalcSeam { get; private set; } = "";
public List<int> CalcEventTypes { get; } = new();
protected override List<swap_flow_event> CalcSwapInterests(
trade td, trade_extend tradeExtend,
DateTime valueDate, DateTime unwindDate,
List<eod_swap_position> eodPositions, List<swap_position> positions,
decimal posiNotionalValue,
decimal closePosiNotionalValue, decimal closePrecent,
int eventType, bool tdClose,
decimal orginPv,
bool add = false, bool settment = true, bool newCalcLast = false,
List<swap_flow_event> closeList = null)
{
LastCalcSeam = nameof(CalcSwapInterests);
CalcEventTypes.Add(eventType);
return CalcResult;
}
protected override List<swap_flow_event> CalcEodPostCloseSettleInterests(InterestCalcRequest req)
{
LastCalcSeam = nameof(CalcEodPostCloseSettleInterests);
CalcEventTypes.Add(req.EventType);
return CalcResult;
}
/// <summary>持仓延续腿重置日再定盘接缝:计数并返回受控新定盘(不连库)</summary>
public decimal RefixResult { get; set; }
public int RefixCalls { get; private set; }
protected override decimal ResolveOngoingResetFixing(swap_position position, DateTime valueDate)
{
RefixCalls++;
return RefixResult;
}
public List<swap_flow_event> ExecuteDealInterests(
List<swap_position> interestList, List<eod_swap_position> eodPositions,
DateTime settleDate, trade td, List<swap_flow_event> flowEvents,
decimal posiTotalNotional, decimal closeNational, decimal grossPrice, decimal orginPv)
{
var autoInterests = new List<swap_flow_event>();
DealInterests(interestList, eodPositions, new List<eod_swap_position>(),
settleDate, td, flowEvents, autoInterests, null,
posiTotalNotional, closeNational, grossPrice, orginPv);
return autoInterests;
}
}
private static trade CreateTrade() => new()
{
id = 1, TradeNumber = "TAIL-SNAP-001", ClientId = 999998,
TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate,
ExerciseDate = new DateTime(2027, 4, 27), TradeStatus = "确认成交",
ValidState = "Valid", StructureType = "单标的",
QuoteCurrency = "CNY", SettlementCurrency = "CNY",
trade_extend = new trade_extend
{
TradeId = 1,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson { AnnualDays = 365, InterestCalcMode = "10", SettlementRules = 0 })
}
};
/// <param name="observationDay">true=当日观察日(Settlement=1)false=观察日在别日</param>
private static swap_position CreateInterestPosition(bool observationDay)
{
var interval = observationDay
? new IntervalModel { Date = SettleDate, Rate = Rate, Settlement = 1 }
: new IntervalModel { Date = StartDate, Rate = Rate, Settlement = 1 };
return new swap_position
{
id = 1001, SwapTradeId = 1, InterestDirection = (int)SwapDirectionEnum.收取,
InterestMode = (int)InterestModeEnum.标的期初全价, InterestRateDefault = Rate,
InterestPrincipalFix = Principal, PosiStartDate = StartDate,
PosiMatuirityDate = new DateTime(2027, 4, 27), IsInitial = true,
InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true,
interest_rest_days = 1, interest_rule = 0,
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel> { interval })
};
}
private static eod_swap_position CreatePreEod(decimal accumulated) => new()
{
id = 100, PositionId = 1001, ValueDate = SettleDate.AddDays(-1),
InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价,
InterestIncomeSum = accumulated, InterestProfitSum = accumulated,
InterestRateDefault = Rate, TdInterestPrincipal = Principal,
InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 1
};
private static swap_flow_event CreateCalcResult() => new()
{
EventType = (int)SwapEventTypeEnum.自动互换, PositionId = 1001,
InterestAmount = Accrued10d, TdInterestAmount = DailyNew,
InterestClosePnL = Accrued10d,
InterestPrincipal = Principal, InterestRate = Rate,
InterestDirection = (int)SwapDirectionEnum.收取
};
private static swap_flow_event CreateCloseEvent() => new()
{
EventType = (int)SwapEventTypeEnum.平仓, PositionId = 1001,
InterestAmount = ManualSettled, InterestClosePnL = ManualSettled,
InterestRate = Rate, InterestFee = 0m,
InterestPrincipal = ClosedNotional,
InterestDirection = (int)SwapDirectionEnum.收取,
DataState = (int)SwapFlowDateStateEnum.完成
};
/// <summary>AutoSettle 格:观察日无平仓 → SaveAutoEodInterestPosition,返回值收集进 autoInterests</summary>
[TestMethod]
public void AutoSettle_观察日无平仓_尾部快照()
{
var service = new TailStubService { CalcResult = new List<swap_flow_event> { CreateCalcResult() } };
var autoInterests = service.ExecuteDealInterests(
new List<swap_position> { CreateInterestPosition(observationDay: true) },
new List<eod_swap_position> { CreatePreEod(Accrued10d) },
SettleDate, CreateTrade(), new List<swap_flow_event>(),
Principal, 0m, 100m, Principal);
Assert.AreEqual("CalcSwapInterests", service.LastCalcSeam, "观察日无平仓应走 CalcSwapInterests seam");
Assert.AreEqual((int)SwapEventTypeEnum.自动互换, service.CalcEventTypes.Single(), "eventType 应为自动互换");
Assert.AreEqual(1, autoInterests.Count, "观察日分支应收集返回值进 autoInterests(→资金记录)");
var p = service.PersistedPositions.Single();
// 钉值于 2026-08-17 现状行为(受控输入:应结8.22/新增0.82/本金10000
Assert.AreEqual(8.22m, p.TdCloseInterest);
Assert.AreEqual(0.82m, p.TdInterestIncome);
Assert.AreEqual(10000m, p.TdInterestPrincipal);
Assert.AreEqual(0.03m, p.TdInterestRate);
Assert.AreEqual(0.00m, p.InterestIncomeSum, "应结=结算,待实现清零");
Assert.AreEqual(0m, p.InterestFeeSum);
Assert.AreEqual(0m, p.InterestProfitSum);
Assert.AreEqual(8.22m, p.RealizedInterest);
Assert.AreEqual(0m, p.RealizedInterestFee);
Assert.AreEqual(0m, p.SwapPositionValue);
Assert.AreEqual(1.0m, p.TdCurrency);
}
/// <summary>AutoSettleWithClose 格(TEST-MATRIX §6 最弱格):观察日+平仓 → SaveAutoEodWithCloseInterestPosition(autoSwap:true),补结差额=恒1全额−盘中已结</summary>
[TestMethod]
public void AutoSettleWithClose_观察日加平仓_尾部快照()
{
var service = new TailStubService { CalcResult = new List<swap_flow_event> { CreateCalcResult() } };
var autoInterests = service.ExecuteDealInterests(
new List<swap_position> { CreateInterestPosition(observationDay: true) },
new List<eod_swap_position> { CreatePreEod(Accrued10d) },
SettleDate, CreateTrade(), new List<swap_flow_event> { CreateCloseEvent() },
Remaining, ClosedNotional, 100m, Remaining);
Assert.AreEqual("CalcEodPostCloseSettleInterests", service.LastCalcSeam, "观察日+平仓应走 EodPostCloseSettle seam");
Assert.AreEqual((int)SwapEventTypeEnum.自动互换, service.CalcEventTypes.Single(), "autoSwap=true → eventType=自动互换");
Assert.AreEqual(1, autoInterests.Count, "观察日分支应收集返回值进 autoInterests");
Assert.AreEqual(Accrued10d - ManualSettled, autoInterests[0].InterestAmount, "补结差额=恒1全额8.22−盘中已结3.50");
var p = service.PersistedPositions.Single();
// 钉值于 2026-08-17 现状行为(受控输入:恒1全额8.22/盘中已结3.5/剩余7000/平掉3000
Assert.AreEqual(8.22m, p.TdCloseInterest, "TdCloseInterest=盘中已结3.50+补结4.72");
Assert.AreEqual(0.5753424657534246575342465753m, p.TdInterestIncome, "autoSwap 重算展示应计=剩余7000×3%/365");
Assert.AreEqual(7000m, p.TdInterestPrincipal, "单利部分平仓:跨日本金=剩余");
Assert.AreEqual(0.03m, p.TdInterestRate);
Assert.AreEqual(0.00m, p.InterestIncomeSum, "恒1口径:理论应结8.22−结算8.22=0");
Assert.AreEqual(0m, p.InterestFeeSum);
Assert.AreEqual(0m, p.InterestProfitSum);
Assert.AreEqual(8.22m, p.RealizedInterest);
Assert.AreEqual(0m, p.RealizedInterestFee);
Assert.AreEqual(0m, p.SwapPositionValue);
Assert.AreEqual(1.0m, p.TdCurrency);
}
/// <summary>CloseOnly 格:非观察日平仓 → SaveAutoEodWithCloseInterestPosition(autoSwap:false),返回值不收集,TdCloseInterest=盘中已结</summary>
[TestMethod]
public void CloseOnly_非观察日平仓_尾部快照()
{
var service = new TailStubService { CalcResult = new List<swap_flow_event> { CreateCalcResult() } };
var autoInterests = service.ExecuteDealInterests(
new List<swap_position> { CreateInterestPosition(observationDay: false) },
new List<eod_swap_position> { CreatePreEod(Accrued10d) },
SettleDate, CreateTrade(), new List<swap_flow_event> { CreateCloseEvent() },
Remaining, ClosedNotional, 100m, Remaining);
Assert.AreEqual("CalcEodPostCloseSettleInterests", service.LastCalcSeam, "纯平仓应走 EodPostCloseSettle seam");
Assert.AreEqual((int)SwapEventTypeEnum.平仓, service.CalcEventTypes.Single(), "autoSwap=false → eventType=平仓");
Assert.AreEqual(0, autoInterests.Count, "纯平仓分支不收集返回值(结算已在盘中流水定格)");
var p = service.PersistedPositions.Single();
// 钉值于 2026-08-17 现状行为(受控输入:恒1重算8.22/盘中已结3.5/剩余7000/平掉3000
Assert.AreEqual(ManualSettled, p.TdCloseInterest, "TdCloseInterest 应仅为盘中已结3.50,不叠加恒1重算值");
Assert.AreEqual(0.5753424657534246575342465753m, p.TdInterestIncome, "不算尾路径:剩余7000×3%/365");
Assert.AreEqual(7000m, p.TdInterestPrincipal, "单利部分平仓:跨日本金=剩余");
Assert.AreEqual(0.03m, p.TdInterestRate, "非观察日:利率取平仓流水 InterestRate");
Assert.AreEqual(5.295342465753m, p.InterestIncomeSum, "尾差递推:上日8.22+新增0.575342−已结3.50");
Assert.AreEqual(0m, p.InterestFeeSum);
Assert.AreEqual(5.295342465753m, p.InterestProfitSum);
Assert.AreEqual(3.5m, p.RealizedInterest);
Assert.AreEqual(0m, p.RealizedInterestFee);
Assert.AreEqual(5.295342465753m, p.SwapPositionValue);
Assert.AreEqual(1.0m, p.TdCurrency);
}
#region 持仓延续腿重置日再定盘(EQD-6968 自洽化:快照利率载体)
private const decimal OldFloat = 0.01425m;
private const decimal NewFloat = 0.0143m;
/// <summary>4/27+147 天周期的重置日平仓</summary>
private static readonly DateTime ResetSettle = StartDate.AddDays(14);
/// <summary>4/27+10:非重置日平仓(10%7≠0</summary>
private static readonly DateTime NonResetSettle = StartDate.AddDays(10);
private static swap_position CreateFloatLegPosition() => new()
{
id = 1001, SwapTradeId = 1, InterestDirection = (int)SwapDirectionEnum.收取,
InterestMode = (int)InterestModeEnum.标的期初全价, InterestRateDefault = Rate,
InterestPrincipalFix = Principal, PosiStartDate = StartDate,
PosiMatuirityDate = new DateTime(2027, 4, 27), IsInitial = true,
InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true,
interest_rest_days = 7, interest_rule = 0,
FloatRateUnderlyingCode = "FR007",
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel>
{ new IntervalModel { Date = StartDate, Rate = Rate, Settlement = 1 } })
};
private static eod_swap_position CreatePreEodBefore(DateTime settle, decimal accumulated) => new()
{
id = 100, PositionId = 1001, ValueDate = settle.AddDays(-1),
InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价,
InterestIncomeSum = accumulated, InterestProfitSum = accumulated,
InterestRateDefault = Rate, TdInterestPrincipal = Principal,
InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 7
};
private static swap_flow_event CalcResultWithFloat(decimal floatRate)
{
var e = CreateCalcResult();
e.FloatRate = floatRate;
return e;
}
/// <summary>
/// 平仓日恰为重置日且剩余持仓>0:快照 FloatRate 必须显式再定盘为当日新定盘——
/// 它是后续非重置日(ByEod 沿用 preEod.FloatRate)与当日应计(intersetAcmount)的利率载体。
/// 排除日"纯跳过"后事件利率=末段已消费利率(OldFloat),载体职责与本步骤显式分离。
/// </summary>
[TestMethod]
public void CloseOnly_平仓日为重置日_剩余持仓快照再定盘()
{
var service = new TailStubService
{
CalcResult = new List<swap_flow_event> { CalcResultWithFloat(OldFloat) },
RefixResult = NewFloat,
};
service.ExecuteDealInterests(
new List<swap_position> { CreateFloatLegPosition() },
new List<eod_swap_position> { CreatePreEodBefore(ResetSettle, Accrued10d) },
ResetSettle, CreateTrade(), new List<swap_flow_event> { CreateCloseEvent() },
Remaining, ClosedNotional, 100m, Remaining);
Assert.AreEqual(1, service.RefixCalls, "不算尾+平仓日=重置日+剩余>0:应恰好显式再定盘一次");
Assert.AreEqual(NewFloat, service.PersistedPositions.Single().FloatRate,
"剩余持仓快照利率=当日新定盘(非事件末段旧利率)");
}
[TestMethod]
public void CloseOnly_平仓日非重置日_不再定盘_快照沿用事件利率()
{
var service = new TailStubService
{
CalcResult = new List<swap_flow_event> { CalcResultWithFloat(OldFloat) },
RefixResult = NewFloat,
};
service.ExecuteDealInterests(
new List<swap_position> { CreateFloatLegPosition() },
new List<eod_swap_position> { CreatePreEodBefore(NonResetSettle, Accrued10d) },
NonResetSettle, CreateTrade(), new List<swap_flow_event> { CreateCloseEvent() },
Remaining, ClosedNotional, 100m, Remaining);
Assert.AreEqual(0, service.RefixCalls, "非重置日平仓:无需再定盘");
Assert.AreEqual(OldFloat, service.PersistedPositions.Single().FloatRate,
"快照沿用事件末段已消费利率(周期未切换)");
}
#endregion
}
}