Files
zszq-trs/UnitTestProject/Modules/SwapModule/GLMS20260703CloseInterestTest.cs
T
hjhan 4a3fee9292 refactor(swap)+test: 删 GetInterests/CalcSwapInterests 死参数 needPrice/grossPrice;补工厂→接缝映射钉子
死参数收口(另一半):
- SwapDealService.GetInterests 删 needPrice/grossPrice(体内零消费,2026-08 验证);
  InitSwapDealInterest.needPrice 同为死参数一并删
- SwapEodPositionService.CalcSwapInterests 签名+转发同步;两个 EOD 生产调用点
  (SaveAutoEodInterestPosition/SaveEodInterestPositionCopy) 重排实参;
  CalcEodPostCloseSettleInterests/GetIntradayUnwindInterests 委托同步
- 14 个测试文件 ~44 处直调点机械更新(8 处 override 签名 + 36 处调用实参)
- 注意:EOD 编排链(DealInterests→Save*家族)的 grossPrice(期初不含费价)有真实用途,保留未动

新增钉子:CalcEodPostCloseSettleInterests 工厂→接缝参数映射测试——
CalcSwapInterestsCapture 捕获 stub 断言 EodPostCloseSettle 的完整转发契约
(posi=平仓后剩余/closePosi=平掉额/恒1/settment:false/orginPv 等 11 项)。
该段位置转发含三个相邻同型 decimal,编译器不查错位,此测试兜底。

验证:定向 241 测试通过(含 T0/T1 Excel 验证期望值、EntrySemantics 精确值钉子——
任何 decimal 错位即红);全量 903=145失败/746通过/12跳过,与基线逐位一致。
2026-08-14 16:57:56 +08:00

317 lines
15 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;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// GLMS-20260703-0002 债券TRS 平仓利息差异 - 仅调用系统函数复现
/// ============================================================================
/// 客户测试环境这笔债券TRS(截图确认):
/// 成交/持仓名义本金 = 279,486,108.21
/// 起息日(StartDate=SettleDate) = 2026-07-06,平仓日(UnwindDate) = 2026-07-20
/// 年化天数 = 365,计息模式 = 算头不算尾("10"),计息天数 = 14 天
/// 利率 = FR007-1.55%(浮动利率腿),重置频率 = 7 天,interest_rule = 0(当前营业日)
/// 截图 FR00707-06=1.42%, 07-13=1.425%(重置点取当前营业日)
///
/// 观察结果:
/// 系统计算的平仓盈亏(利息端)= -13,667.85
/// 实际应得平仓盈亏(利息端)= -13,668.02
/// 差异 = 0.17 元
///
/// 代码走查结论(SwapDealService):
/// 平仓路径:GetInterests → CalcSwapDealInterest → CalcUnwindInterest
/// → InitSwapDealInterest → CalcDailyCompoundInterest / CalcDailySimpleInterest
/// FR007 取数规则:只在"重置日"(i % interest_rest_days == 0) 取一次 FR007,非重置日沿用上一重置日。
/// 本例 07-06~07-20 跨 2 个重置周期,取 2 次 FR007:
/// - 第一段(07-06~07-12):取 07-06 当前营业日 FR007 = 1.42%
/// - 第二段(07-13~07-19):取 07-13 当前营业日 FR007 = 1.425%
/// 差异根因:系统配置走 <复利>,导致 07-13 重置日把前 7 天累计利息并入本金,
/// 第二段计息本金降为 279,479,140.20,最终利息绝对值比单利少 0.17 元。
/// 若按业务口径走 <单利>,则本金全程保持 279,486,108.21,利息 = -13,668.02。
///
/// 本测试不再做任何手工计算(手算/验算见同目录 Excel:GLMS20260703_平仓利息验算.xlsx),
/// 只做:构造输入 → 调用系统真实函数 GetInterests → 与截图已知结果断言。
/// ============================================================================
[TestClass]
public class GLMS20260703CloseInterestTest
{
#region 内部类:固定利率模拟服务(不触碰 DB,但按日期返回 FR007 截图值)
private sealed class StubSwapDealService : SwapDealService
{
public StubSwapDealService(OptUserInfo optUser) : base(optUser) { }
/// <summary>单元测试记录:生产代码每次调用 TryGetFloatRate 的日期与结果。</summary>
public readonly List<(DateTime RequestDate, double Rate)> FloatRateCalls = new();
/// <summary>
/// 按截图 eod_commodity_future_price.ValueDate 返回 FR007 ReferencePrice(小数)。
/// 生产环境由 TryGetFloatRate 去行情/DB 取数;单元测试用截图硬编码快照替代。
/// </summary>
protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate)
{
rate = 0d;
if (underlyingCode != "FR007") return false;
var fr007 = new Dictionary<DateTime, double>
{
[new DateTime(2026, 7, 3)] = 0.0143,
[new DateTime(2026, 7, 6)] = 0.0142,
[new DateTime(2026, 7, 7)] = 0.0143,
[new DateTime(2026, 7, 8)] = 0.0143,
[new DateTime(2026, 7, 9)] = 0.0143,
[new DateTime(2026, 7, 10)] = 0.0142,
[new DateTime(2026, 7, 13)] = 0.01425,
[new DateTime(2026, 7, 14)] = 0.0143,
[new DateTime(2026, 7, 15)] = 0.0144,
[new DateTime(2026, 7, 16)] = 0.0144,
[new DateTime(2026, 7, 17)] = 0.0144,
[new DateTime(2026, 7, 20)] = 0.0143,
};
if (fr007.TryGetValue(valueDate.Date, out rate))
{
FloatRateCalls.Add((valueDate.Date, rate));
return true;
}
// 若请求日期不在硬编码表(如 interest_rule=-1 调到周末),返回最近有值日的 FR007
var nearest = fr007.Keys.OrderByDescending(d => d)
.FirstOrDefault(d => d <= valueDate.Date);
if (nearest != default)
{
rate = fr007[nearest];
FloatRateCalls.Add((valueDate.Date, rate));
return true;
}
return false;
}
}
#endregion
#region 测试常量(来自客户测试环境截图 GLMS-20260703-0002
/// <summary>成交/持仓名义本金</summary>
private const decimal Notional = 279486108.21m;
/// <summary>年化天数</summary>
private const int AnnualDays = 365;
/// <summary>固定利差 -1.55%FR007-1.55%</summary>
private const decimal Spread = -0.0155m;
/// <summary>起息日(td.StartDate = SettleDate,不是成交日)</summary>
private static readonly DateTime StartDate = new(2026, 7, 6);
/// <summary>成交日(td.TradeDate,仅作对照;不参与计息起点)</summary>
private static readonly DateTime TradeDate = new(2026, 7, 3);
/// <summary>平仓日(valueDate / unwindDate</summary>
private static readonly DateTime CloseDate = new(2026, 7, 20);
/// <summary>系统实际计算的利息绝对值</summary>
private const decimal SystemInterestAmount = 13667.85m;
/// <summary>实际应得利息绝对值</summary>
private const decimal ActualInterestAmount = 13668.02m;
#endregion
private SwapDealService _service;
[TestInitialize]
public void Init()
{
_service = new StubSwapDealService(
new OptUserInfo(0, nameof(GLMS20260703CloseInterestTest), OptUserFrom.UnitTest));
}
#region 测试数据构建器(仅构造输入,不计算利息)
private static trade CreateTrade()
{
var extend = new trade_extend
{
TradeId = 1,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays,
InterestCalcMode = "10", // 算头不算尾
SettlementRules = 0
})
};
return new trade
{
id = 1,
TradeNumber = "GLMS-20260703-0002",
ClientId = 999998,
TradeType = "债券TRS",
TradeDate = TradeDate,
StartDate = StartDate,
ExerciseDate = CloseDate.AddDays(1), // 必须 > valueDate,否则 InitInterestDate 会多减一天
TradeStatus = "已平仓",
ValidState = "Valid",
trade_extend = extend
};
}
/// <summary>
/// 构造债券TRS浮动利率本金腿。FloatRateUnderlyingCode=FR007,利率=FR007-1.55%。
/// 仅设置输入字段;利息由系统函数 CalcDailyCompoundInterest / CalcDailySimpleInterest 计算。
/// </summary>
private static swap_position CreateBondPosition(InterestTypeEnum interestType, int interestRule = 0)
{
var intervalModels = new List<IntervalModel>
{
new IntervalModel { Date = CloseDate, Rate = Spread, Settlement = 0 }
};
return new swap_position
{
id = 1001,
SwapTradeId = 1,
PositionType = (int)PositionTypeFlag.Unknown,
// 债券本金腿:标的期初全价(closePrincipal = posiNotional × closePercent
InterestDirection = (int)SwapDirectionEnum.支付,
InterestMode = (int)InterestModeEnum.标的期初全价,
InterestRateDefault = Spread,
InterestPrincipalFix = Notional,
PosiStartDate = StartDate,
PosiMatuirityDate = CloseDate,
IsInitial = true,
Invalid = false,
InterestType = (int)interestType, // 复利 or 单利
IsAnnualized = true,
interest_rest_days = 7, // 7天重置
interest_rule = interestRule, // 0=当前营业日, -1=前一营业日
FloatRateUnderlyingCode = "FR007", // 真实浮动利率腿
FloatRate = 0m,
PosiNotionalValue = Notional,
UnderlyingCode = "2500002.IB",
InterestSwapInterval = JsonConvert.SerializeObject(intervalModels)
};
}
/// <summary>
/// 驱动真实平仓利息路径(settment=false → CalcUnwindInterest → InitSwapDealInterest
/// → CalcDailyCompoundInterest / CalcDailySimpleInterest)。
/// eodPositions 传空 → 等效"无前日日终快照",日循环从起始日重算。
/// </summary>
private swap_flow_event CalcCloseInterest(InterestTypeEnum interestType, int interestRule = 0)
{
var td = CreateTrade();
var position = CreateBondPosition(interestType, interestRule);
// 每次计算前清空取数记录,避免同一测试中多次调用互相污染
((StubSwapDealService)_service).FloatRateCalls.Clear();
var interests = _service.GetInterests(
td, td.trade_extend,
CloseDate, CloseDate, // valueDate / unwindDate
new List<eod_swap_position>(), // eodPositions(空)
new List<swap_position> { position },
Notional, Notional, Notional, Notional, // posiNotional / long / short / closePosiNotional
1m, // closePercent
(int)SwapEventTypeEnum.平仓,
false, Notional, // tdClose / orginPv
false, settment: false, newCalcLast: false, closeList: null);
Assert.AreEqual(1, interests.Count);
return interests[0];
}
#endregion
#region 复现用例(仅调用系统函数 + 对照已知结果断言)
/// <summary>
/// [GLMS20260703_REPRO_001] 系统路径:InterestType=复利 + 真实FR007 → 复现系统值 -13,667.85
/// 与生产代码完全一致:7天重置,重置日取当前营业日 FR007,利息并入本金。
/// </summary>
[TestMethod]
public void Reproduce_SystemValue_13667_85_WithCompoundInterest()
{
var interest = CalcCloseInterest(InterestTypeEnum.复利);
var amount2 = Math.Round(interest.InterestAmount, 2, MidpointRounding.AwayFromZero);
var closePnl2 = Math.Round(interest.InterestClosePnL, 2, MidpointRounding.AwayFromZero);
Console.WriteLine("系统路径(复利):");
Console.WriteLine($" 利息金额(2位显示)={amount2}");
Console.WriteLine($" 平仓盈亏(利息端,2位)={closePnl2}");
// 截图给的是"平仓盈亏(利息端)"的绝对值口径,故比较绝对值。
Assert.AreEqual(SystemInterestAmount, Math.Abs(amount2),
$"系统复利路径应得到利息绝对值 {SystemInterestAmount},实际 {amount2}");
Assert.AreEqual(SystemInterestAmount, Math.Abs(closePnl2),
$"支付方向平仓盈亏绝对值应为 {SystemInterestAmount},实际 {closePnl2}");
}
/// <summary>
/// [GLMS20260703_REPRO_002] 实际口径:InterestType=单利 + 真实FR007 → 复现实际值 -13,668.02
/// 同一 position、同一 FR007、同一计息天数,仅把 InterestType 改为单利,
/// 利息不并入本金,全程用名义本金 279,486,108.21 计息。
/// </summary>
[TestMethod]
public void Reproduce_ActualValue_13668_02_WithSimpleInterest()
{
var interest = CalcCloseInterest(InterestTypeEnum.单利);
var amount2 = Math.Round(interest.InterestAmount, 2, MidpointRounding.AwayFromZero);
var closePnl2 = Math.Round(interest.InterestClosePnL, 2, MidpointRounding.AwayFromZero);
Console.WriteLine("实际口径(单利):");
Console.WriteLine($" 利息金额(2位显示)={amount2}");
Console.WriteLine($" 平仓盈亏(利息端,2位)={closePnl2}");
Assert.AreEqual(ActualInterestAmount, Math.Abs(amount2),
$"单利路径应得到利息绝对值 {ActualInterestAmount},实际 {amount2}");
Assert.AreEqual(ActualInterestAmount, Math.Abs(closePnl2),
$"支付方向平仓盈亏绝对值应为 {ActualInterestAmount},实际 {closePnl2}");
}
/// <summary>
/// [GLMS20260703_REPRO_003] 差异定位:0.17 元 = 复利 vs 单利
/// 同一笔交易、同一 FR007 取值、同一计息天数,唯一区别是 InterestType
/// 系统(复利)比实际(单利)少 0.17 元。
/// </summary>
[TestMethod]
public void PrecisionGap_Is_0_17_ComplexVsSimple()
{
var compound = CalcCloseInterest(InterestTypeEnum.复利).InterestAmount;
var simple = CalcCloseInterest(InterestTypeEnum.单利).InterestAmount;
// 两者都是负数(支付方向),取绝对值差异
var gap = Math.Round(Math.Abs(simple) - Math.Abs(compound), 2, MidpointRounding.AwayFromZero);
Console.WriteLine($"复利 |利息|={Math.Abs(compound):F11}");
Console.WriteLine($"单利 |利息|={Math.Abs(simple):F11}");
Console.WriteLine($"差异(2位)={gap}");
Assert.AreEqual(0.17m, gap, "单利与复利的利息绝对值差异应为 0.17 元");
}
/// <summary>
/// [GLMS20260703_REPRO_004] 验证真实函数确实在 07-06、07-13 两个重置点取了 FR007
/// (不手算利息,仅检查系统函数实际发起了哪几次取数)。
/// </summary>
[TestMethod]
public void Trace_FloatRate_Taken_Dates()
{
var fe = CalcCloseInterest(InterestTypeEnum.复利, interestRule: 0);
var calls = ((StubSwapDealService)_service).FloatRateCalls;
Console.WriteLine("TryGetFloatRate 实际调用记录(按调用顺序):");
foreach (var (date, rate) in calls)
{
Console.WriteLine($" 请求日期={date:yyyy-MM-dd} 返回 FR007={rate:P4}");
}
Assert.IsTrue(calls.Any(c => c.RequestDate == new DateTime(2026, 7, 6)), "应取 07-06 的 FR007");
Assert.IsTrue(calls.Any(c => c.RequestDate == new DateTime(2026, 7, 13)), "应取 07-13 的 FR007");
Assert.AreEqual(SystemInterestAmount, Math.Abs(Math.Round(fe.InterestAmount, 2, MidpointRounding.AwayFromZero)),
"interest_rule=0 复利应复现系统值 -13,667.85");
}
#endregion
}
}