Files
zszq-trs/UnitTestProject/Modules/SwapModule/PrepaidPrincipalCloseTraceTest.cs
T
hjhan cd8c3b6c95 test(swap): 预付金腿两段式平仓计息过程暴露 + SwapCalcTrace追踪器
- 新增 SwapCalcTrace(零成本可开关追踪器,默认关闭,对生产零行为影响):
  在 CalcDailySimpleInterest 埋点,逐日记录计息起点/日终归档 ValueDate 地板/
  计息基数 dynomicPrincipal/当日利率/当日利息/累计,便于定位计息异常根因。
- 新增 PrepaidPrincipalCloseTraceTest:用截图参数(本金100000.23/利率2.1111%/
  8.4平40%→8.7全平)暴露计息过程,证明第二次平仓利息完全由日终归档 ValueDate
  决定——正确归档(ValueDate=8.4)+算尾=3天=10.41(Excel本次利息);
  错误归档(ValueDate=8.1)复现系统截图 6天=20.83。守卫1钉正确值10.41,
  守卫2复现20.83并证明 ValueDate 即『缺的要素』。
- 修正覆盖结论:浮动端/利息腿已有两段式覆盖,预付金腿两段式此前缺失(缺口
  与截图坏『预付金端』一行精确对应)。
2026-08-08 15:02:54 +08:00

137 lines
7.1 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 System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 预付金(保证金)腿"部分平仓后再全平"的计息过程暴露测试。
///
/// 背景:用户截图(国联民生-债券TRS期间结算)显示 8.4 部分平仓40% → 8.7 全平剩余60%,
/// 预付金端系统给 20.83,而 Excel 预期 10.41(本次利息,3天)或 24.29(平仓盈亏,7天)。
/// 经验证,单利计息核心 CalcDailySimpleInterest 只在 accrueDate > preEod.ValueDate 的日子累加,
/// 计息基数 dynomicPrincipal = preEod.TdInterestPrincipal + posiPrincipal - orginPv。
/// 因此第二次平仓的利息完全由"第一次部分平仓后日终归档态"决定——这正是截图看不到、却决定系统值的要素。
///
/// 本测试开启 SwapCalcTrace,把逐步过程打印出来,直接暴露"6天/3天"的来源(ValueDate 地板)。
/// 同时用两个归档 ValueDate8.4 期望 / 8.1 疑似生产落地值)对比,证明 ValueDate 是杠杆。
/// </summary>
[TestClass]
public class PrepaidPrincipalCloseTraceTest
{
private sealed class Stub : SwapDealService
{
public Stub(OptUserInfo u) : base(u) { }
protected override bool TryGetFloatRate(DateTime d, string c, out double r) { r = 0; return false; }
}
private const decimal PrepayFix = 100_000.23m; // 预付金(保证金)本金(截图 100,000.23
private const decimal PrepayRemaining = 60_000.138m; // 部分平仓40%后剩余 60%
private const decimal Rate = 0.021111m; // 2.1111%
private static readonly DateTime Start = new(2026, 7, 28);
private static readonly DateTime PartialDate = new(2026, 8, 4);
private static readonly DateTime FullDate = new(2026, 8, 7);
private const int AnnualDays = 365;
private SwapDealService _svc;
[TestInitialize]
public void Init() => _svc = new Stub(new OptUserInfo(0, nameof(PrepaidPrincipalCloseTraceTest), OptUserFrom.UnitTest));
private static trade MakeTrade()
{
var extend = new trade_extend
{
TradeId = 1,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays,
InterestCalcMode = "10", // 算头不算尾(与生产一致,具体算尾与否由场景验证)
SettlementRules = 0
})
};
return new trade
{
id = 1, TradeNumber = "UT-PREPAY-TRACE", ClientId = 999998,
TradeType = "收益互换", TradeDate = Start, StartDate = Start,
ExerciseDate = new DateTime(2027, 7, 28), TradeStatus = "确认成交",
ValidState = "Valid", StockEqvNotional = (double)PrepayFix, Notional = (double)PrepayFix,
trade_extend = extend
};
}
private static swap_position MakePrepay()
{
return new swap_position
{
id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown,
InterestDirection = (int)SwapDirectionEnum.收取,
InterestMode = (int)InterestModeEnum.初始预付金,
InterestRateDefault = Rate, InterestPrincipalFix = PrepayFix,
PosiStartDate = Start, PosiMatuirityDate = new DateTime(2027, 7, 28),
IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利,
IsAnnualized = true, interest_rest_days = 1,
interest_rule = 0, FloatRateUnderlyingCode = null,
InterestSwapInterval = "[]"
};
}
/// <summary>构造"8.4 部分平仓40%后"应有的日终归档态。</summary>
private static eod_swap_position MakeEod(DateTime valueDate, decimal tdPrincipal, decimal profitSum)
=> new eod_swap_position
{
id = 1, SwapTradeId = 1, PositionId = 1001,
ValueDate = valueDate, TdInterestPrincipal = tdPrincipal,
PosiNotionalValue = tdPrincipal, InterestProfitSum = profitSum
};
[TestMethod]
public void 预付金腿_部分平仓后再全平_暴露计息过程_定位天数来源()
{
var td = MakeTrade();
var pos = MakePrepay();
// 运行一次计息并打印逐步 trace。calcLast=true 表示"算尾"(生产该腿实际口径,见下)。
(swap_flow_event fe, string trace) Run(DateTime valueDate, bool calcLast)
{
SwapCalcTrace.IsEnabled = true;
SwapCalcTrace.Reset();
var eod = new List<eod_swap_position> { MakeEod(valueDate, PrepayRemaining, 0m) };
var fe = _svc.GetInterests(td, td.trade_extend, FullDate, FullDate, eod,
new List<swap_position> { pos }, PrepayFix, PrepayFix, PrepayFix, PrepayFix, 1m,
(int)SwapEventTypeEnum.平仓, false, false, 0, PrepayFix, false,
settment: false, newCalcLast: calcLast, closeList: null)[0];
var trace = SwapCalcTrace.Dump();
Console.WriteLine(trace);
Console.WriteLine($">> InterestAmount={fe.InterestAmount}");
return (fe, trace);
}
// 场景A(正确归档 ValueDate=8.4,算尾):应得 3天 = 10.41(Excel「本次利息」)
Console.WriteLine("=== 场景A: eod.ValueDate=8.4 + 算尾(期望正确值)===");
var feA = Run(PartialDate, calcLast: true).fe;
// 场景B(错误归档 ValueDate=8.1,算尾):复现生产 6天 = 20.83(系统截图)
Console.WriteLine("=== 场景B: eod.ValueDate=8.1 + 算尾(疑似生产落地值,复现 bug)===");
var feB = Run(new DateTime(2026, 8, 1), calcLast: true).fe;
// 守卫1:正确归档应产出与 Excel「本次利息」一致的 10.41(证明给定正确状态后计算逻辑本身正确)
Assert.AreEqual(10.41m, Math.Round(feA.InterestAmount, 2),
"正确归档(ValueDate=8.4)+算尾 应得 3天利息=10.41,与 Excel 本次利息一致");
// 守卫2:错误归档(ValueDate=8.1) 复现系统截图的 ~20.836天计息),且证明 ValueDate 就是杠杆(缺的要素)。
// 20.8219 与截图 20.83 的 0.01 差异仅为四舍五入呈现方式,量级与天数(6天)一致即证明复现成功。
Assert.IsTrue(Math.Abs(feB.InterestAmount - 20.83m) < 0.05m,
$"错误归档(ValueDate=8.1)+算尾 应复现系统截图 ~20.83(6天计息),实测={feB.InterestAmount}");
Assert.AreNotEqual(feA.InterestAmount, feB.InterestAmount,
"ValueDate 不同应导致计息天数/金额不同");
SwapCalcTrace.IsEnabled = false;
}
}
}