test(swap): 补 6676b625 掉期保证金利息方向反向单元测试

抽 CalculateSwapRealizedPnl 为 public static 纯函数(用 ConsTrade.InterestMarginModels 替代实例 marginTypes), 新增 8 个单元测试覆盖: 非保证金腿收取/支付原向, 保证金腿(初始预付金/追加预付金)收取/支付反向, 完整 5 字段汇总两种方向, RealizedInterest=0 边界. 锁定 6676b625 修复的保证金腿利息方向反向契约, 防止后续误改回归.
This commit is contained in:
hjhan
2026-07-17 08:40:19 +08:00
parent 96a64ae82d
commit 2395aa9856
2 changed files with 209 additions and 2 deletions
@@ -0,0 +1,206 @@
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// SwapEodPositionService.CalculateSwapRealizedPnl 的回归测试。
/// ---------------------------------------------------------------
/// 守卫张名锐提交 6676b625 "fix(swap): 修正掉期产品保证金利息计算逻辑"。
///
/// 旧 bugeod_swap.RealizedPnL 直接 Sum(s.RealizedPnl),未对保证金腿利息做方向反向,
/// 导致"收取对手方保证金"产生的利息被错误计入我方收益(实际是我方支付给对手方的成本),
/// 框架合约已实现收益虚高。
///
/// 修复:新增 CalculateSwapRealizedPnl ——
/// 非保证金腿:interestRatio = Direction==收取 ? 1 : -1(维持数据库方向)
/// 保证金腿(初始预付金 5 / 追加预付金 6):interestRatio 反向
/// 最终:RealizedInterest × interestRatio + 其他 4 字段
///
/// 抽为 public static 纯函数以支持无库单测(marginTypes 等价于 ConsTrade.InterestMarginModels)。
/// 本测试直接锁定方向反向契约,防止后续误改回归。
/// </summary>
[TestClass]
public class SwapEodRealizedPnlCalcTest
{
// ================================================================
// 场景1:非保证金腿收取方向 → RealizedInterest × +1(维持原向)
// ================================================================
[TestMethod]
public void _收取方向_利息维持原向系数为1()
{
var pos = NewPosition(
interestMode: (int)InterestModeEnum.,
interestDirection: (int)SwapDirectionEnum.,
realizedInterest: 1000m);
var result = SwapEodPositionService.CalculateSwapRealizedPnl(pos);
Assert.AreEqual(1000m, result, 0.0001m,
"非保证金腿收取方向:利息 ×(+1)=1000");
}
// ================================================================
// 场景2:非保证金腿支付方向 → RealizedInterest × -1(维持原向)
// ================================================================
[TestMethod]
public void _支付方向_利息维持原向系数为负1()
{
var pos = NewPosition(
interestMode: (int)InterestModeEnum.,
interestDirection: (int)SwapDirectionEnum.,
realizedInterest: 1000m);
var result = SwapEodPositionService.CalculateSwapRealizedPnl(pos);
Assert.AreEqual(-1000m, result, 0.0001m,
"非保证金腿支付方向:利息 ×(-1)=-1000");
}
// ================================================================
// 场景3:保证金腿(初始预付金)收取方向 → 利息反向,系数 -1
// 这是 6676b625 修复的核心场景:收取对手方保证金产生的利息是我方支付成本
// ================================================================
[TestMethod]
public void _初始预付金_收取方向_利息反向系数为负1()
{
var pos = NewPosition(
interestMode: (int)InterestModeEnum.,
interestDirection: (int)SwapDirectionEnum.,
realizedInterest: 1000m);
var result = SwapEodPositionService.CalculateSwapRealizedPnl(pos);
Assert.AreEqual(-1000m, result, 0.0001m,
"保证金腿收取方向:利息应反向 ×(-1)=-1000(修复前会错误得 +1000");
}
// ================================================================
// 场景4:保证金腿(初始预付金)支付方向 → 利息反向,系数 +1
// ================================================================
[TestMethod]
public void _初始预付金_支付方向_利息反向系数为1()
{
var pos = NewPosition(
interestMode: (int)InterestModeEnum.,
interestDirection: (int)SwapDirectionEnum.,
realizedInterest: 1000m);
var result = SwapEodPositionService.CalculateSwapRealizedPnl(pos);
Assert.AreEqual(1000m, result, 0.0001m,
"保证金腿支付方向:利息应反向 ×(+1)=1000");
}
// ================================================================
// 场景5:追加预付金同初始预付金,同样走反向逻辑
// ================================================================
[TestMethod]
public void _追加预付金_收取方向_利息反向()
{
var pos = NewPosition(
interestMode: (int)InterestModeEnum.,
interestDirection: (int)SwapDirectionEnum.,
realizedInterest: 500m);
var result = SwapEodPositionService.CalculateSwapRealizedPnl(pos);
Assert.AreEqual(-500m, result, 0.0001m,
"追加预付金(mode=6)与初始预付金(mode=5)同走反向逻辑");
}
// ================================================================
// 场景6:完整 5 字段汇总(MtmPnL + Dividend + Fee + Interest×ratio + InterestFee
// 保证金腿收取方向,Interest=200, 其他各 100
// 期望:100 + 100 + 100 + 200×(-1) + 100 = 200
// ================================================================
[TestMethod]
public void 5_保证金腿收取方向_利息反向后合计正确()
{
var pos = NewPosition(
interestMode: (int)InterestModeEnum.,
interestDirection: (int)SwapDirectionEnum.,
realizedMtmPnL: 100m,
realizedDividend: 100m,
realizedFee: 100m,
realizedInterest: 200m,
realizedInterestFee: 100m);
var result = SwapEodPositionService.CalculateSwapRealizedPnl(pos);
// 100 + 100 + 100 + 200×(-1) + 100 = 200
Assert.AreEqual(200m, result, 0.0001m,
"5 字段汇总:保证金腿收取方向,Interest×(-1) 后合计=200,验证所有字段都参与计算");
}
// ================================================================
// 场景7:完整 5 字段汇总(非保证金腿收取方向)
// 非保证金腿收取方向,Interest=200, 其他各 100
// 期望:100 + 100 + 100 + 200×(+1) + 100 = 600
// ================================================================
[TestMethod]
public void 5_非保证金腿收取方向_利息原向合计正确()
{
var pos = NewPosition(
interestMode: (int)InterestModeEnum.,
interestDirection: (int)SwapDirectionEnum.,
realizedMtmPnL: 100m,
realizedDividend: 100m,
realizedFee: 100m,
realizedInterest: 200m,
realizedInterestFee: 100m);
var result = SwapEodPositionService.CalculateSwapRealizedPnl(pos);
// 100 + 100 + 100 + 200×(+1) + 100 = 600
Assert.AreEqual(600m, result, 0.0001m,
"5 字段汇总:非保证金腿收取方向,Interest×(+1) 后合计=600");
}
// ================================================================
// 场景8RealizedInterest=0 边界 —— 方向反向无影响,结果为其他 4 字段之和
// ================================================================
[TestMethod]
public void _方向反向无影响_结果为其他4字段之和()
{
var pos = NewPosition(
interestMode: (int)InterestModeEnum.,
interestDirection: (int)SwapDirectionEnum.,
realizedMtmPnL: 100m,
realizedDividend: 50m,
realizedFee: 30m,
realizedInterest: 0m,
realizedInterestFee: 20m);
var result = SwapEodPositionService.CalculateSwapRealizedPnl(pos);
// 100 + 50 + 30 + 0×(-1) + 20 = 200
Assert.AreEqual(200m, result, 0.0001m,
"RealizedInterest=0 时方向反向无影响,结果为其他 4 字段之和");
}
// ================================================================
// Helper:构造 eod_swap_position(只设置参与计算的 7 个字段)
// ================================================================
private static eod_swap_position NewPosition(
int interestMode,
int interestDirection,
decimal realizedMtmPnL = 0m,
decimal realizedDividend = 0m,
decimal realizedFee = 0m,
decimal realizedInterest = 0m,
decimal realizedInterestFee = 0m)
{
return new eod_swap_position
{
InterestMode = interestMode,
InterestDirection = interestDirection,
RealizedMtmPnL = realizedMtmPnL,
RealizedDividend = realizedDividend,
RealizedFee = realizedFee,
RealizedInterest = realizedInterest,
RealizedInterestFee = realizedInterestFee
};
}
}
}