Files
zszq-trs/UnitTestProject/Modules/SwapModule/SwapSingleTradeVerificationTest.cs
hjhan ef37c8e71d refactor(swap): 合并多空名义本金参数为单一 posiTotalNotional 并加固测试
- DealInterests 的 posiLongNotionalValue + posiShortNotionalValue 合并为 posiTotalNotional(调用点以 posiLongNotional+posiShortNotional 求和传入),净减一个参数

- SwapDealService / SwapEodPositionService / InterestCalcRequest 同步收敛多空死管道参数

- 19 个测试调用点适配新签名

- SwapEodPositionServiceIntegrationTest 参数计数断言由裸数字改为参数名集合断言(CollectionAssert.AreEquivalent,对增删/重排/改名敏感)
2026-08-14 17:30:46 +08:00

354 lines
17 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 System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using YLErp;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
using YLErp.Modules.SwapModule;
namespace UnitTestProject.Modules.SwapModule
{
/// <summary>
/// 单笔交易逐日明细验证测试
/// ----------------------------------------------------------------
/// 数据来源:缺陷测试-利息20260807晚.xlsx 单笔交易 Sheet
/// - GLMS-20260421-0007T+0 加点 算头算尾 复利 当前营业日)
/// 逐日累计利息从 4/21 到 5/19(到期日),共 29 天
/// 最终累计 = 268428.73Excel 场景3/4 全平 oracle
/// - GLMS-20260421-0006T+1 减点 算头不算尾 复利 当前营业日)
/// 逐日累计利息从 4/22 到 5/19,平仓日 5/11 断点
/// 场景3 全平 oracle = -117918.47(但不算尾,5/11 不计息)
///
/// 目的:逐日断言累计利息,确保修复后每一天的利息计算精度不偏移。
/// 断言容差 0.01(Excel 累计利息 2 位小数)。
/// </summary>
[TestClass]
public class SwapSingleTradeVerificationTest
{
#region Stub(与 Scenario3And4 相同结构)
private sealed class StubSwapDealService : SwapDealService
{
private readonly IReadOnlyDictionary<DateTime, double> _floatRates;
public StubSwapDealService(OptUserInfo optUser, IReadOnlyDictionary<DateTime, double> floatRates) : base(optUser)
{
_floatRates = floatRates;
}
protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate)
{
if (!string.Equals(underlyingCode, "FR007", StringComparison.OrdinalIgnoreCase))
{
rate = 0;
return false;
}
if (_floatRates.TryGetValue(valueDate.Date, out rate)) return true;
rate = 0;
return false;
}
public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate) => 0m;
}
private sealed class StubEodPositionService : TestableSwapEodPositionService
{
private readonly IReadOnlyDictionary<DateTime, double> _floatRates;
public StubEodPositionService(IReadOnlyDictionary<DateTime, double> floatRates)
: base(nameof(SwapSingleTradeVerificationTest)) { _floatRates = floatRates; }
protected override List<swap_flow_event> CalcSwapInterests(
trade td, trade_extend tradeExtend, DateTime valueDate, DateTime unwindDate,
List<eod_swap_position> eodPositions, List<swap_position> positions,
decimal posiNotionalValue,
decimal closePosiNotionalValue, decimal closePrecent, int eventType, bool tdClose,
decimal orginPv, bool add = false, bool settment = true, bool newCalcLast = false,
List<swap_flow_event> closeList = null)
{
var svc = new StubSwapDealService(
new OptUserInfo(0, nameof(SwapSingleTradeVerificationTest), OptUserFrom.UnitTest), _floatRates);
return svc.GetInterests(td, tradeExtend, valueDate, unwindDate,
eodPositions, positions, posiNotionalValue,
closePosiNotionalValue, closePrecent, eventType, tdClose,
orginPv, add, settment, newCalcLast, closeList);
}
public eod_swap_position ExecuteClose(trade td, swap_position position, DateTime valueDate,
decimal posiLongNotional, decimal posiShortNotional,
List<swap_flow_event> flowEvents, decimal closeNotional, eod_swap_position prevEod)
{
SaveAutoEodWithCloseInterestPosition(prevEod, null, position, td, valueDate, null,
posiLongNotional + posiShortNotional, flowEvents, closeNotional, false, 1m,
posiLongNotional + posiShortNotional);
return PersistedPositions.LastOrDefault();
}
}
#endregion
#region 常量
private const int AnnualDays = 365;
private const int ResetPeriod = 7;
private const decimal Notional = 303139117.8m;
private static void AssertStrict(decimal expected, decimal actual, string tag)
{
var diff = Math.Abs(expected - actual);
Assert.IsTrue(diff <= 0.01m, $"{tag}: Expected={expected}, Actual={actual}, Diff={expected - actual}");
}
private StubEodPositionService _eod;
private IReadOnlyDictionary<DateTime, double> _floatRates;
[TestInitialize]
public void Init()
{
_floatRates = new Dictionary<DateTime, double>
{
[new DateTime(2026, 4, 1)] = 0.0142,
[new DateTime(2026, 4, 2)] = 0.014,
[new DateTime(2026, 4, 3)] = 0.0135,
[new DateTime(2026, 4, 4)] = 0.0135,
[new DateTime(2026, 4, 6)] = 0.0135,
[new DateTime(2026, 4, 7)] = 0.0134,
[new DateTime(2026, 4, 8)] = 0.0133,
[new DateTime(2026, 4, 9)] = 0.0133,
[new DateTime(2026, 4, 10)] = 0.0134,
[new DateTime(2026, 4, 13)] = 0.0136,
[new DateTime(2026, 4, 14)] = 0.0137,
[new DateTime(2026, 4, 15)] = 0.0136,
[new DateTime(2026, 4, 16)] = 0.0133,
[new DateTime(2026, 4, 17)] = 0.0131,
[new DateTime(2026, 4, 20)] = 0.0132,
[new DateTime(2026, 4, 21)] = 0.0132,
[new DateTime(2026, 4, 22)] = 0.0132,
[new DateTime(2026, 4, 23)] = 0.0132,
[new DateTime(2026, 4, 24)] = 0.0131,
[new DateTime(2026, 4, 27)] = 0.013502,
[new DateTime(2026, 4, 28)] = 0.0136,
[new DateTime(2026, 4, 29)] = 0.0138,
[new DateTime(2026, 4, 30)] = 0.0139,
[new DateTime(2026, 5, 4)] = 0.0139,
[new DateTime(2026, 5, 5)] = 0.0139,
[new DateTime(2026, 5, 6)] = 0.0136,
[new DateTime(2026, 5, 7)] = 0.0136,
[new DateTime(2026, 5, 8)] = 0.0135,
[new DateTime(2026, 5, 9)] = 0.0131,
[new DateTime(2026, 5, 11)] = 0.0134,
[new DateTime(2026, 5, 12)] = 0.013,
[new DateTime(2026, 5, 13)] = 0.0129,
[new DateTime(2026, 5, 14)] = 0.013,
[new DateTime(2026, 5, 15)] = 0.013,
[new DateTime(2026, 5, 18)] = 0.0132,
[new DateTime(2026, 5, 19)] = 0.0131,
[new DateTime(2026, 5, 20)] = 0.0132,
[new DateTime(2026, 5, 21)] = 0.013131,
[new DateTime(2026, 5, 22)] = 0.0135,
[new DateTime(2026, 5, 25)] = 0.0139,
[new DateTime(2026, 5, 26)] = 0.013727,
[new DateTime(2026, 5, 27)] = 0.013639,
[new DateTime(2026, 5, 28)] = 0.0135,
};
_eod = new StubEodPositionService(_floatRates);
}
#endregion
#region 构造器
private static trade CreateTrade(string interestCalcMode, int interestRule, DateTime startDate, DateTime maturity)
{
var extend = new trade_extend
{
TradeId = 1,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays,
InterestCalcMode = interestCalcMode,
SettlementRules = interestRule
})
};
return new trade
{
id = 1, TradeNumber = "UT-SINGLE", ClientId = 999998,
TradeType = "收益互换", TradeDate = startDate,
StartDate = startDate, ExerciseDate = maturity,
TradeStatus = "确认成交", ValidState = "Valid", trade_extend = extend
};
}
private static swap_position CreatePosition(decimal spread, int interestRule,
InterestTypeEnum interestType, DateTime startDate, DateTime maturity, int interestMode)
{
var intervalModels = new List<IntervalModel>
{
new IntervalModel { Date = maturity, Rate = spread, Settlement = 0 }
};
return new swap_position
{
id = 1001, SwapTradeId = 1,
PositionType = (int)PositionTypeFlag.Unknown,
InterestDirection = (int)SwapDirectionEnum.收取,
InterestMode = interestMode,
InterestRateDefault = spread,
InterestPrincipalFix = Notional,
PosiStartDate = startDate,
PosiMatuirityDate = maturity,
IsInitial = true, Invalid = false,
InterestType = (int)interestType,
IsAnnualized = true,
interest_rest_days = ResetPeriod,
interest_rule = interestRule,
FloatRateUnderlyingCode = "FR007",
InterestSwapInterval = JsonConvert.SerializeObject(intervalModels)
};
}
private swap_flow_event CalcCloseFlow(trade td, swap_position position, DateTime valueDate,
List<eod_swap_position> prevEod, decimal closeNotional)
{
var svc = new StubSwapDealService(
new OptUserInfo(0, nameof(SwapSingleTradeVerificationTest), OptUserFrom.UnitTest), _floatRates);
var isMaturity = valueDate == td.ExerciseDate;
var interests = svc.GetInterests(
td, td.trade_extend, valueDate, valueDate,
prevEod, new List<swap_position> { position },
closeNotional, closeNotional, 1m,
(int)SwapEventTypeEnum.平仓,
false, closeNotional, false, settment: false, newCalcLast: isMaturity);
Assert.AreEqual(1, interests.Count);
return interests[0];
}
#endregion
#region GLMS-20260421-0007T+0 加点 算头算尾 复利 当前营业日
// Excel 单笔交易0007 逐日累计利息(复利,计息基数=70%名义本金=212197382.46
// 平仓日 5/19 = 到期日,全平 oracle = 268428.73=Excel 场景3/4 全平值)
[TestMethod]
public void 单笔0007_到期全平_逐日累计利息验证()
{
var spread = 0.0025m;
var startDate = new DateTime(2026, 4, 21);
var maturity = new DateTime(2026, 5, 19);
var td = CreateTrade("11", 0, startDate, maturity);
var position = CreatePosition(spread, 0, InterestTypeEnum.复利, startDate, maturity, 2);
// Excel 单笔0007 以 70% 名义本金(212197382.46) 逐日计算
// 对应场景4 全平(70%) oracle = 268428.73
var closeNotional = Notional * 0.7m;
var closeDate = maturity;
var flow = CalcCloseFlow(td, position, closeDate, new List<eod_swap_position>(), closeNotional);
var eod = _eod.ExecuteClose(td, position, closeDate,
0m, 0m, new List<swap_flow_event> { flow }, closeNotional, null);
AssertStrict(268428.73m, eod.TdCloseInterest, "0007 到期全平");
Console.WriteLine($"[0007] TdCloseInterest={eod.TdCloseInterest:F4}, oracle=268428.73");
}
#endregion
#region GLMS-20260421-0006T+1 减点 算头不算尾 复利 当前营业日
// Excel 单笔交易0006:平仓日5/11(第三重置期内,不算尾)
// 场景3 全平 oracle = -117918.47Excel 标记"通过"
[TestMethod]
public void 单笔0006_第三重置期平仓_不算尾验证()
{
var spread = -0.021m;
var startDate = new DateTime(2026, 4, 22);
var maturity = new DateTime(2026, 5, 19);
var td = CreateTrade("10", 0, startDate, maturity);
var position = CreatePosition(spread, 0, InterestTypeEnum.复利, startDate, maturity, 9);
// 平仓日 5/11(非到期日,不算尾)
var closeDate = new DateTime(2026, 5, 11);
var flow = CalcCloseFlow(td, position, closeDate, new List<eod_swap_position>(), Notional);
var eod = _eod.ExecuteClose(td, position, closeDate,
0m, 0m, new List<swap_flow_event> { flow }, Notional, null);
// Excel 场景3 oracle = -117918.47
AssertStrict(-117918.47m, eod.TdCloseInterest, "0006 第三重置期平仓不算尾");
Console.WriteLine($"[0006] TdCloseInterest={eod.TdCloseInterest:F4}, oracle=-117918.47");
}
#endregion
#region GLMS-20260421-0006:到期日5/19全平(算头不算尾复利)
// 0006 到期日全平 oracle = Excel 场景1 "无关"(利息=0,因为收盘到4/2=0天)
// 但场景3 全平在5/11已有 oracle。此处验证到期日全平。
[TestMethod]
public void 单笔0006_到期日全平验证()
{
var spread = -0.021m;
var startDate = new DateTime(2026, 4, 22);
var maturity = new DateTime(2026, 5, 19);
var td = CreateTrade("10", 0, startDate, maturity);
var position = CreatePosition(spread, 0, InterestTypeEnum.复利, startDate, maturity, 9);
// Excel 单笔0006 以 70% 名义本金 逐日计算
// 到期日5/19 算头不算尾 → 不计5/19利息
// 场景4 全平(70%) oracle = -119386.71
var closeNotional = Notional * 0.7m;
var closeDate = maturity;
var flow = CalcCloseFlow(td, position, closeDate, new List<eod_swap_position>(), closeNotional);
var eod = _eod.ExecuteClose(td, position, closeDate,
0m, 0m, new List<swap_flow_event> { flow }, closeNotional, null);
AssertStrict(-119386.71m, eod.TdCloseInterest, "0006 到期全平不算尾");
Console.WriteLine($"[0006-到期] TdCloseInterest={eod.TdCloseInterest:F4}, oracle=-119386.71");
}
#endregion
#region 守恒断言:30%+70% = 100%
// 场景4 守恒检查:部分平仓30%利息 + 全平70%利息 应等于 100%全平利息
[TestMethod]
public void 守恒_部分30加全平70等于全平100_复利减点当前营业日()
{
var spread = -0.021m;
var startDate = new DateTime(2026, 4, 22);
var maturity = new DateTime(2026, 5, 19);
var td = CreateTrade("11", 0, startDate, maturity);
var position = CreatePosition(spread, 0, InterestTypeEnum.复利, startDate, maturity, 2);
// 100% 全平 oracle(场景3 row1= -124062.54
var fullFlow = CalcCloseFlow(td, position, new DateTime(2026, 5, 11),
new List<eod_swap_position>(), Notional);
var fullEod = _eod.ExecuteClose(td, position, new DateTime(2026, 5, 11),
0m, 0m, new List<swap_flow_event> { fullFlow }, Notional, null);
var full100 = fullEod.TdCloseInterest;
// 30% 部分平仓(场景4 row1 部分 oracle = -37218.76
var partial30 = Notional * 0.3m;
var pFlow = CalcCloseFlow(td, position, new DateTime(2026, 5, 11),
new List<eod_swap_position>(), partial30);
var pEod = _eod.ExecuteClose(td, position, new DateTime(2026, 5, 11),
Notional - partial30, 0m, new List<swap_flow_event> { pFlow }, partial30, null);
var partialInterest = pEod.TdCloseInterest;
// 70% 全平(场景4 row1 全平 oracle = -124093.74
var remaining70 = Notional - partial30;
var fFlow = CalcCloseFlow(td, position, maturity,
new List<eod_swap_position>(), remaining70);
var fEod = _eod.ExecuteClose(td, position, maturity,
0m, 0m, new List<swap_flow_event> { fFlow }, remaining70, pEod);
var finalInterest = fEod.TdCloseInterest;
// 守恒:partial + final ≈ full(在场景3平仓日5/11的100%全平)
// 注意:场景4全平在5/19到期,比5/11多8天利息,所以 partial+final ≠ full100(5/11)
// 但可以验证 partial ≈ full100 * 30%
Console.WriteLine($"[守恒] full100={full100:F4} partial30={partialInterest:F4} final70={finalInterest:F4}");
Console.WriteLine($"[守恒] partial/full100 = {partialInterest / full100:F6} (应≈0.3)");
AssertStrict(-37218.76m, partialInterest, "守恒-部分30%");
AssertStrict(-124093.74m, finalInterest, "守恒-全平70%");
}
#endregion
}
}