test(swap): 增加 GLMS-20260703-0002 平仓利息复现用例(真实FR007+真实函数)
- 调用系统 GetInterests 真实路径(复利/单利两条), 用截图中的真实 FR007 定盘值 在 07-06/07-13 两个重置点真实取数, 复现系统值 -13,667.85 与业务实际值 -13,668.02 - 断言两者差异 0.17 元来自复利第二段本金并入 - 仅保留调用系统函数的断言, 不内嵌手算, 验算见 GLMS20260703_平仓利息验算.xlsx
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
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(当前营业日)
|
||||
/// 截图 FR007:07-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 = "511160.SH",
|
||||
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, false, 0m, Notional, // tdClose / needPrice / grossPrice / 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user