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 即『缺的要素』。 - 修正覆盖结论:浮动端/利息腿已有两段式覆盖,预付金腿两段式此前缺失(缺口 与截图坏『预付金端』一行精确对应)。
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
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 地板)。
|
||||
/// 同时用两个归档 ValueDate(8.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.83(6天计息),且证明 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user