抽 CalculateSwapRealizedPnl 为 public static 纯函数(用 ConsTrade.InterestMarginModels 替代实例 marginTypes), 新增 8 个单元测试覆盖: 非保证金腿收取/支付原向, 保证金腿(初始预付金/追加预付金)收取/支付反向, 完整 5 字段汇总两种方向, RealizedInterest=0 边界. 锁定 6676b625 修复的保证金腿利息方向反向契约, 防止后续误改回归.
207 lines
9.5 KiB
C#
207 lines
9.5 KiB
C#
using YLErp.DBModels;
|
||
using YLErp.DBModels.Enums;
|
||
|
||
namespace YLErp.Modules.SwapModule
|
||
{
|
||
/// <summary>
|
||
/// SwapEodPositionService.CalculateSwapRealizedPnl 的回归测试。
|
||
/// ---------------------------------------------------------------
|
||
/// 守卫张名锐提交 6676b625 "fix(swap): 修正掉期产品保证金利息计算逻辑"。
|
||
///
|
||
/// 旧 bug:eod_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");
|
||
}
|
||
|
||
// ================================================================
|
||
// 场景8:RealizedInterest=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
|
||
};
|
||
}
|
||
}
|
||
}
|