死参数收口(另一半): - 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跳过,与基线逐位一致。
317 lines
15 KiB
C#
317 lines
15 KiB
C#
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 = "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
|
||
}
|
||
}
|