test(swap): 复利consumedInterest扣除合成测试(补c6adb3bb覆盖缺口)

c6adb3bb加的复利consumedInterest扣除此前无任何测试覆盖。
现在用4个合成测试完整验证:

改动(SwapDealService.cs):
- GetConsumedInterest加virtual关键字(测试可override注入内存值)

新增(ConsumedInterestScenarioTest.cs):
- StubSwapDealService override TryGetFloatRate+GetConsumedInterest
  (模仿T0/T1的StubSwapDealService模式)
- CI_001: 基线(consumedInterest=0)拿到全程复利利息
- CI_002: 注入consumedInterest后利息=基线-consumed(直接验证cs:793)
- CI_003: 守恒(consumed+利息=基线)×3组不同consumed值
- CI_004: 全部已结(consumed=基线)平仓≈0不为负(防双重扣减)

关键发现:测试必须传settment:false走盘中平仓路径(CalcUnwindInterest),
settment:true(默认)走收盘归档路径(CalcEodInterest)不传consumedInterest。

验证: 89(T0/T1)+7(DealInterests)+4(ConsumedInterest)=100全通过。
This commit is contained in:
hjhan
2026-07-01 18:40:48 +08:00
parent be88442c91
commit f501ee95a7
2 changed files with 251 additions and 1 deletions
@@ -0,0 +1,250 @@
using Newtonsoft.Json;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 复利 consumedInterest 扣除 - 合成单元测试
/// ============================================================================
/// 验证 c6adb3bb 的修复:复利路径平仓时,扣除历史已通过互换结出的利息。
///
/// 核心场景:
/// 一笔复利交易,N天后做了互换结算(已结N天利息),之后再平仓。
/// 平仓默认值应 = 从头算的全程利息 - 已结利息(consumedInterest)。
/// 如果不扣(bug),平仓默认值 = 全程利息(偏大)。
/// 如果多扣(之前单利的错误),平仓默认值 = 0或负(偏小)。
///
/// 模仿 GetInterestsUnitTest_T0 的 StubSwapDealService 模式。
/// ============================================================================
[TestClass]
public class ConsumedInterestScenarioTest
{
#region
private const decimal Principal = 1000m;
private const decimal FixedRate = 0.0025m; // 加点利率
private const double FloatRate = 0.001; // FR007
private const decimal TotalRate = FixedRate + (decimal)FloatRate; // 综合年化利率
private const int AnnualDays = 365;
private const int ResetPeriod = 3;
private static readonly DateTime StartDate = new(2026, 4, 27);
private static readonly DateTime ExerciseDate = new(2027, 4, 27);
#endregion
#region Stub SwapDealService + consumedInterest
/// <summary>
/// 继承 SwapDealServiceoverride 两个虚方法:
/// - TryGetFloatRate:返回固定浮动利率(不连库)
/// - GetConsumedInterest:返回注入的历史已结利息(不连库)
/// </summary>
private sealed class StubSwapDealService : SwapDealService
{
private readonly double _floatRate;
private readonly decimal _consumedInterest;
public StubSwapDealService(OptUserInfo optUser, double floatRate, decimal consumedInterest)
: base(optUser)
{
_floatRate = floatRate;
_consumedInterest = consumedInterest;
}
protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate)
{
rate = _floatRate;
return true; // 始终返回固定浮动利率
}
public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate)
{
return _consumedInterest; // 返回注入值
}
}
#endregion
#region
private static trade CreateTrade()
{
return new trade
{
id = 1, TradeNumber = "UT-CONSUMED-001", ClientId = 999998,
TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate,
ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid",
StructureType = "单标的", QuoteCurrency = "CNY", SettlementCurrency = "CNY",
trade_extend = new trade_extend
{
TradeId = 1,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays,
InterestCalcMode = "10", // 算头不算尾
SettlementRules = 0
})
}
};
}
private static swap_position CreateCompoundPosition()
{
return new swap_position
{
id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown,
InterestDirection = (int)SwapDirectionEnum., InterestMode = (int)InterestModeEnum.,
InterestRateDefault = FixedRate, InterestPrincipalFix = Principal,
PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate,
IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.,
IsAnnualized = true, interest_rest_days = ResetPeriod, interest_rule = 0,
FloatRateUnderlyingCode = "FR007",
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel>
{
new IntervalModel { Date = ExerciseDate, Rate = FixedRate, Settlement = 0 }
})
};
}
/// <summary>调用 GetInterests 获取复利利息(统一调用入口,settment:false走盘中平仓路径)</summary>
private static swap_flow_event CalcCompoundUnwind(StubSwapDealService service, DateTime unwindDate)
{
var td = CreateTrade();
var position = CreateCompoundPosition();
var interests = service.GetInterests(td, td.trade_extend, unwindDate, unwindDate,
new List<eod_swap_position>(), new List<swap_position> { position },
Principal, Principal, Principal, Principal, 1m,
(int)SwapEventTypeEnum., false, false, Principal, Principal,
add: false, settment: false, newCalcLast: false);
Assert.AreEqual(1, interests.Count);
return interests[0];
}
private static StubSwapDealService CreateService(decimal consumedInterest)
{
return new StubSwapDealService(
new OptUserInfo(0, nameof(ConsumedInterestScenarioTest), OptUserFrom.UnitTest),
FloatRate, consumedInterest);
}
private static void AssertDecimal(decimal expected, decimal actual, string message = "")
{
var tolerance = 1m / (decimal)Math.Pow(10, ConsGlobal.PriceRound - 2);
Assert.IsTrue(Math.Abs(expected - actual) <= tolerance,
$"{message} Expected: {expected}, Actual: {actual}, Diff: {expected - actual}");
}
#endregion
// ================================================================
// 场景1:基线——无历史互换(consumedInterest=0),拿到全程复利利息
// ================================================================
/// <summary>
/// [CI_001] 无历史互换结清,复利平仓利息基线
/// ---------------------------------------------------------------
/// consumedInterest=0,平仓利息=从头算的全程复利利息。
/// 此值作为后续场景的参照基线(避免独立复利计算的精度匹配问题)。
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void CI_001_无历史互换平仓利息基线()
{
var unwindDate = StartDate.AddDays(10); // 4/27+10=5/7,算头不算尾约9天
var service = CreateService(consumedInterest: 0m);
var result = CalcCompoundUnwind(service, unwindDate);
Assert.IsTrue(result.InterestAmount > 0, "无互换时复利利息应>0");
Console.WriteLine($"基线(consumedInterest=0): InterestAmount={result.InterestAmount:F6}");
}
// ================================================================
// 场景2consumedInterest>0 → 平仓利息=基线-consumedInterest
// ================================================================
/// <summary>
/// [CI_002] 注入consumedInterest后,平仓利息应=基线-consumedInterest
/// ---------------------------------------------------------------
/// 用相同参数但注入不同的consumedInterest,验证:
/// 利息(有consumed) = 利息(无consumed) - consumedInterest
/// 这是验证cs:793 `interest -= consumedInterest` 的直接方式。
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void CI_002_consumedInterest正确扣除()
{
var unwindDate = StartDate.AddDays(10);
// 基线:consumedInterest=0
var baselineResult = CalcCompoundUnwind(CreateService(0m), unwindDate);
decimal baseline = baselineResult.InterestAmount;
// 注入consumedInterest=基线的50%
decimal consumed = baseline * 0.5m;
var consumedResult = CalcCompoundUnwind(CreateService(consumed), unwindDate);
// 期望 = 基线 - consumed
decimal expected = baseline - consumed;
AssertDecimal(expected, consumedResult.InterestAmount,
$"平仓利息应=基线({baseline:F6})-consumed({consumed:F6})={expected:F6}");
Console.WriteLine($"基线={baseline:F6}, consumed={consumed:F6}");
Console.WriteLine($"平仓利息={consumedResult.InterestAmount:F6}, 期望={expected:F6} ✅");
}
// ================================================================
// 场景3:守恒——consumed + 平仓利息 = 基线
// ================================================================
/// <summary>
/// [CI_003] 守恒:consumedInterest + 平仓利息(扣后) = 基线(无consumed)
/// ---------------------------------------------------------------
/// 注入任意consumedInterest,验证 consumed + 利息 = 基线。
/// 如果扣多了(守恒不成立→合计<基线)或没扣(合计>基线),测试失败。
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void CI_003_守恒consumed加平仓等于基线()
{
var unwindDate = StartDate.AddDays(10);
decimal baseline = CalcCompoundUnwind(CreateService(0m), unwindDate).InterestAmount;
// 注入不同的consumedInterest验证守恒
decimal[] testConsumed = { baseline * 0.3m, baseline * 0.5m, baseline * 0.8m };
foreach (var consumed in testConsumed)
{
var result = CalcCompoundUnwind(CreateService(consumed), unwindDate);
decimal actual = consumed + result.InterestAmount;
AssertDecimal(baseline, actual,
$"守恒: consumed({consumed:F6}) + 利息({result.InterestAmount:F6}) = {actual:F6} 应=基线({baseline:F6})");
Console.WriteLine($"consumed={consumed:F6} + 利息={result.InterestAmount:F6} = {actual:F6} = 基线{baseline:F6} ✅");
}
}
// ================================================================
// 场景4consumedInterest=全部基线 → 平仓利息≈0,不为负
// ================================================================
/// <summary>
/// [CI_004] 全部利息已结清(consumedInterest=基线),再平仓利息应≈0
/// ---------------------------------------------------------------
/// 验证不会扣过头变成负数(之前单利双重扣减的错误)。
/// 复利从头算全程 - 全程consumed = 0,应精确归零或微小正值。
/// ---------------------------------------------------------------
/// </summary>
[TestMethod]
public void CI_004_全部已结再平仓利息不为负()
{
var unwindDate = StartDate.AddDays(10);
decimal baseline = CalcCompoundUnwind(CreateService(0m), unwindDate).InterestAmount;
// consumedInterest=全部基线
var result = CalcCompoundUnwind(CreateService(baseline), unwindDate);
Console.WriteLine($"基线={baseline:F6}, consumed={baseline:F6}, 平仓利息={result.InterestAmount:F6}");
Assert.IsTrue(result.InterestAmount >= -0.01m,
$"全部已结再平仓利息应≈0(实际={result.InterestAmount:F6}),不应为负");
Console.WriteLine($"全部已结平仓≈0{result.InterestAmount:F6})✅");
}
}
}