- DealInterests 的 posiLongNotionalValue + posiShortNotionalValue 合并为 posiTotalNotional(调用点以 posiLongNotional+posiShortNotional 求和传入),净减一个参数 - SwapDealService / SwapEodPositionService / InterestCalcRequest 同步收敛多空死管道参数 - 19 个测试调用点适配新签名 - SwapEodPositionServiceIntegrationTest 参数计数断言由裸数字改为参数名集合断言(CollectionAssert.AreEquivalent,对增删/重排/改名敏感)
354 lines
17 KiB
C#
354 lines
17 KiB
C#
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-0007(T+0 加点 算头算尾 复利 当前营业日)
|
||
/// 逐日累计利息从 4/21 到 5/19(到期日),共 29 天
|
||
/// 最终累计 = 268428.73(Excel 场景3/4 全平 oracle)
|
||
/// - GLMS-20260421-0006(T+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-0007:T+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-0006:T+1 减点 算头不算尾 复利 当前营业日
|
||
|
||
// Excel 单笔交易0006:平仓日5/11(第三重置期内,不算尾)
|
||
// 场景3 全平 oracle = -117918.47(Excel 标记"通过")
|
||
[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
|
||
}
|
||
}
|