Files
zszq-trs/UnitTestProject/Modules/SwapModule/SwapReportInterestSignNormalizeTest.cs
hjhan a153a1d1a6 refactor(swap-eod): 抽出期末头寸纯函数并放宽可见性
SetFixedLegRealizedPnl 替换4处复制粘贴(清理L1296双分号); NormalizeInterestSignForReport 替换16行内联报表分支; CalculateWeightedMarginInterest private->public static。均为纯静态无实例依赖,配套无库单测锁定行为。
2026-07-21 15:28:13 +08:00

213 lines
10 KiB
C#
Raw Permalink 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 YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// SwapEodPositionService.NormalizeInterestSignForReport 的回归测试。
/// -----------------------------------------------------------------
/// 守卫提交 f4ffe710 中报表分支的符号归一化逻辑(原内联于 SearchEodSwapList)。
///
/// 业务口径:
/// - 仅当 InterestDirection > 0 时执行(兼容历史 0 方向脏数据)
/// - 普通利息腿:收取为正、支付为负(interestRatio = Direction==收取 ? 1 : -1
/// - 预付金腿(初始预付金/追加预付金):利息方向与保证金本金方向相反,interestRatio 取反
/// - TdCloseInterest / RealizedInterest 统一按 Math.Abs × interestRatio 重写
/// - RealizedPnl 重算为 RealizedInterest + RealizedInterestFee(兼容历史未同步落库)
///
/// 抽为 public static 纯函数以支持无库单测。
/// </summary>
[TestClass]
public class SwapReportInterestSignNormalizeTest
{
// ================================================================
// 场景1:普通利息腿收取方向 → 利息维持正号
// TdCloseInterest=-1000(历史脏数据符号错) → 归一化为 +1000
// RealizedInterest=-2000 → +2000
// RealizedPnl = 2000 + 100 = 2100
// ================================================================
[TestMethod]
public void 普通利息腿_收取方向_利息归一化为正()
{
var pos = NewPosition(
interestMode: (int)InterestModeEnum.固定值,
interestDirection: (int)SwapDirectionEnum.收取,
tdCloseInterest: -1000m,
realizedInterest: -2000m,
realizedInterestFee: 100m);
SwapEodPositionService.NormalizeInterestSignForReport(pos);
Assert.AreEqual(1000m, pos.TdCloseInterest, 0.0001m, "TdCloseInterest 应归一化为 +1000");
Assert.AreEqual(2000m, pos.RealizedInterest, 0.0001m, "RealizedInterest 应归一化为 +2000");
Assert.AreEqual(2100m, pos.RealizedPnl, 0.0001m, "RealizedPnl = 2000 + 100 = 2100");
}
// ================================================================
// 场景2:普通利息腿支付方向 → 利息归一化为负
// TdCloseInterest=1000(历史脏数据符号错) → 归一化为 -1000
// RealizedInterest=2000 → -2000
// RealizedPnl = -2000 + 100 = -1900
// ================================================================
[TestMethod]
public void 普通利息腿_支付方向_利息归一化为负()
{
var pos = NewPosition(
interestMode: (int)InterestModeEnum.固定值,
interestDirection: (int)SwapDirectionEnum.支付,
tdCloseInterest: 1000m,
realizedInterest: 2000m,
realizedInterestFee: 100m);
SwapEodPositionService.NormalizeInterestSignForReport(pos);
Assert.AreEqual(-1000m, pos.TdCloseInterest, 0.0001m, "TdCloseInterest 应归一化为 -1000");
Assert.AreEqual(-2000m, pos.RealizedInterest, 0.0001m, "RealizedInterest 应归一化为 -2000");
Assert.AreEqual(-1900m, pos.RealizedPnl, 0.0001m, "RealizedPnl = -2000 + 100 = -1900");
}
// ================================================================
// 场景3:预付金腿收取方向 → 利息方向反向为负
// 原因:预付金腿的利息方向与保证金本金方向相反
// TdCloseInterest=1000 → -1000
// RealizedInterest=2000 → -2000
// ================================================================
[TestMethod]
public void 预付金腿_收取方向_利息反向为负()
{
var pos = NewPosition(
interestMode: (int)InterestModeEnum.初始预付金,
interestDirection: (int)SwapDirectionEnum.收取,
tdCloseInterest: 1000m,
realizedInterest: 2000m,
realizedInterestFee: 0m);
SwapEodPositionService.NormalizeInterestSignForReport(pos);
Assert.AreEqual(-1000m, pos.TdCloseInterest, 0.0001m,
"预付金腿收取方向:利息反向为负");
Assert.AreEqual(-2000m, pos.RealizedInterest, 0.0001m,
"预付金腿收取方向:累计利息反向为负");
Assert.AreEqual(-2000m, pos.RealizedPnl, 0.0001m,
"RealizedPnl = -2000 + 0 = -2000");
}
// ================================================================
// 场景4:预付金腿支付方向 → 利息方向反向为正
// ================================================================
[TestMethod]
public void 预付金腿_支付方向_利息反向为正()
{
var pos = NewPosition(
interestMode: (int)InterestModeEnum.追加预付金,
interestDirection: (int)SwapDirectionEnum.支付,
tdCloseInterest: -1000m,
realizedInterest: -2000m,
realizedInterestFee: 50m);
SwapEodPositionService.NormalizeInterestSignForReport(pos);
Assert.AreEqual(1000m, pos.TdCloseInterest, 0.0001m,
"预付金腿支付方向:利息反向为正");
Assert.AreEqual(2000m, pos.RealizedInterest, 0.0001m,
"预付金腿支付方向:累计利息反向为正");
Assert.AreEqual(2050m, pos.RealizedPnl, 0.0001m,
"RealizedPnl = 2000 + 50 = 2050");
}
// ================================================================
// 场景5InterestDirection=0 → 不处理(兼容历史 0 方向脏数据)
// 所有字段保持原值不变
// ================================================================
[TestMethod]
public void 方向为零_不处理_字段保持原值()
{
var pos = NewPosition(
interestMode: (int)InterestModeEnum.固定值,
interestDirection: 0,
tdCloseInterest: -999m,
realizedInterest: -888m,
realizedInterestFee: 77m);
pos.RealizedPnl = 555m; // 预置旧值
SwapEodPositionService.NormalizeInterestSignForReport(pos);
Assert.AreEqual(-999m, pos.TdCloseInterest, 0.0001m, "方向=0TdCloseInterest 不变");
Assert.AreEqual(-888m, pos.RealizedInterest, 0.0001m, "方向=0RealizedInterest 不变");
Assert.AreEqual(555m, pos.RealizedPnl, 0.0001m, "方向=0RealizedPnl 不重算");
}
// ================================================================
// 场景6InterestDirection 为负 → 不处理(防御性,对应原 "> 0" 判断)
// ================================================================
[TestMethod]
public void 方向为负_不处理_字段保持原值()
{
var pos = NewPosition(
interestMode: (int)InterestModeEnum.固定值,
interestDirection: -1,
tdCloseInterest: -999m,
realizedInterest: -888m,
realizedInterestFee: 77m);
pos.RealizedPnl = 555m;
SwapEodPositionService.NormalizeInterestSignForReport(pos);
Assert.AreEqual(-999m, pos.TdCloseInterest, 0.0001m, "方向<0TdCloseInterest 不变");
Assert.AreEqual(555m, pos.RealizedPnl, 0.0001m, "方向<0RealizedPnl 不重算");
}
// ================================================================
// 场景7:利息为零 → Math.Abs(0)=0,归一化后仍为 0RealizedPnl=费用
// ================================================================
[TestMethod]
public void 利息为零_归一化后仍为零_盈亏等于费用()
{
var pos = NewPosition(
interestMode: (int)InterestModeEnum.固定值,
interestDirection: (int)SwapDirectionEnum.收取,
tdCloseInterest: 0m,
realizedInterest: 0m,
realizedInterestFee: 300m);
SwapEodPositionService.NormalizeInterestSignForReport(pos);
Assert.AreEqual(0m, pos.TdCloseInterest, 0.0001m);
Assert.AreEqual(0m, pos.RealizedInterest, 0.0001m);
Assert.AreEqual(300m, pos.RealizedPnl, 0.0001m, "RealizedPnl = 0 + 300 = 300");
}
// ================================================================
// 场景8null 入参 —— 抛 NullReferenceException(现状锚点)
// 生产代码未加 null 检查,InterestDirection 解引用即 NRE。
// 若未来改为 ArgumentNullException,此处需同步更新。
// ================================================================
[TestMethod]
public void Null入参_抛NullReferenceException()
{
Assert.ThrowsException<NullReferenceException>(() =>
SwapEodPositionService.NormalizeInterestSignForReport(null!));
}
// ================================================================
// Helper:构造 eod_swap_position
// ================================================================
private static eod_swap_position NewPosition(
int interestMode,
int interestDirection,
decimal tdCloseInterest,
decimal realizedInterest,
decimal realizedInterestFee)
{
return new eod_swap_position
{
InterestMode = interestMode,
InterestDirection = interestDirection,
TdCloseInterest = tdCloseInterest,
RealizedInterest = realizedInterest,
RealizedInterestFee = realizedInterestFee
};
}
}
}