死参数收口(另一半): - SwapDealService.GetInterests 删 needPrice/grossPrice(体内零消费,2026-08 验证); InitSwapDealInterest.needPrice 同为死参数一并删 - SwapEodPositionService.CalcSwapInterests 签名+转发同步;两个 EOD 生产调用点 (SaveAutoEodInterestPosition/SaveEodInterestPositionCopy) 重排实参; CalcEodPostCloseSettleInterests/GetIntradayUnwindInterests 委托同步 - 14 个测试文件 ~44 处直调点机械更新(8 处 override 签名 + 36 处调用实参) - 注意:EOD 编排链(DealInterests→Save*家族)的 grossPrice(期初不含费价)有真实用途,保留未动 新增钉子:CalcEodPostCloseSettleInterests 工厂→接缝参数映射测试—— CalcSwapInterestsCapture 捕获 stub 断言 EodPostCloseSettle 的完整转发契约 (posi=平仓后剩余/closePosi=平掉额/恒1/settment:false/orginPv 等 11 项)。 该段位置转发含三个相邻同型 decimal,编译器不查错位,此测试兜底。 验证:定向 241 测试通过(含 T0/T1 Excel 验证期望值、EntrySemantics 精确值钉子—— 任何 decimal 错位即红);全量 903=145失败/746通过/12跳过,与基线逐位一致。
342 lines
18 KiB
C#
342 lines
18 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>
|
||
/// 回归保护测试:业务场景1(固定利率收盘平仓)+ 业务场景2(浮动利率第一重置期内平仓)
|
||
/// --------------------------------------------------------------------------
|
||
/// 数据来源:缺陷测试-利息20260807晚.xlsx
|
||
/// - 业务场景1-浮动(8 用例):收盘到 4/2 利息=0,Excel 标记"无关",此处验证返回 0
|
||
/// - 业务场景1-固定(12 用例):固定利率单利,平仓日 5/28 或 4/6,Excel 全标"通过"
|
||
/// - 业务场景2(12 用例):浮动利率第一重置期内平仓(4/27),Excel 全标"通过"
|
||
///
|
||
/// 目的:确保场景3/4 的 Bug 修复不回归破坏已通过的用例。
|
||
/// 断言容差 0.01(匹配 Excel 2 位小数精度)。
|
||
/// </summary>
|
||
[TestClass]
|
||
public class SwapInterestScenario1And2Test
|
||
{
|
||
#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(SwapInterestScenario1And2Test)) { _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 posiLongNotionalValue, decimal posiShortNotionalValue,
|
||
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(SwapInterestScenario1And2Test), OptUserFrom.UnitTest), _floatRates);
|
||
return svc.GetInterests(td, tradeExtend, valueDate, unwindDate,
|
||
eodPositions, positions, posiNotionalValue, posiLongNotionalValue, posiShortNotionalValue,
|
||
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 const decimal FixedNotional = 10012350m;
|
||
|
||
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, string tradeNo = "UT-SCEN-1-2")
|
||
{
|
||
var extend = new trade_extend
|
||
{
|
||
TradeId = 1,
|
||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||
{
|
||
AnnualDays = AnnualDays,
|
||
InterestCalcMode = interestCalcMode,
|
||
SettlementRules = interestRule
|
||
})
|
||
};
|
||
return new trade
|
||
{
|
||
id = 1, TradeNumber = tradeNo, 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, decimal notional, bool isFixed = false)
|
||
{
|
||
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 = isFixed ? 1 : ResetPeriod,
|
||
interest_rule = interestRule,
|
||
FloatRateUnderlyingCode = isFixed ? null : "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(SwapInterestScenario1And2Test), 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, 0m, closeNotional, 1m,
|
||
(int)SwapEventTypeEnum.平仓,
|
||
false, closeNotional, false, settment: false, newCalcLast: isMaturity);
|
||
Assert.AreEqual(1, interests.Count);
|
||
return interests[0];
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 业务场景1-浮动:收盘到4/2,利息=0(8用例,Excel标"无关")
|
||
|
||
// 浮动利率交易,收盘到 4/2 经过 0 天,全部平仓返还利息 = 0
|
||
[DataTestMethod]
|
||
[DataRow("T+1浮动减点算头算尾(当前营业日)", 0, 2, "-0.021")]
|
||
[DataRow("T+0浮动加点算头算尾(当前营业日)", 0, 2, "0.0025")]
|
||
[DataRow("T+1浮动减点算头不算尾(当前营业日)", 0, 9, "-0.021")]
|
||
[DataRow("T+0浮动加点算头不算尾(当前营业日)", 0, 2, "0.0025")]
|
||
[DataRow("T+1浮动减点算头算尾", -1, 9, "-0.021")]
|
||
[DataRow("T+0浮动加点算头算尾", -1, 2, "0.0025")]
|
||
[DataRow("T+1浮动减点算头不算尾", -1, 9, "-0.021")]
|
||
[DataRow("T+0浮动加点算头不算尾", -1, 9, "0.0025")]
|
||
public void 场景1_浮动_收盘到4月2日_利息应为0(string note, int rule, int interestMode, string spreadStr)
|
||
{
|
||
var spread = decimal.Parse(spreadStr, System.Globalization.CultureInfo.InvariantCulture);
|
||
var startDate = spread >= 0 ? new DateTime(2026, 4, 21) : new DateTime(2026, 4, 22);
|
||
var td = CreateTrade("11", rule, startDate, new DateTime(2026, 5, 19));
|
||
var position = CreatePosition(spread, rule, InterestTypeEnum.复利, startDate, new DateTime(2026, 5, 19), interestMode, Notional);
|
||
|
||
var flow = CalcCloseFlow(td, position, new DateTime(2026, 4, 2), new List<eod_swap_position>(), Notional);
|
||
var eod = _eod.ExecuteClose(td, position, new DateTime(2026, 4, 2),
|
||
0m, 0m, new List<swap_flow_event> { flow }, Notional, null);
|
||
// Excel 标记"无关",利息应为 0(T+1 起算日 4/22 > 4/2,0 天计息)
|
||
AssertStrict(0m, eod.TdCloseInterest, "场景1-浮动 " + note);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 业务场景1-固定:固定利率单利,平仓日5/28(12用例,Excel全标"通过")
|
||
|
||
// 固定利率单利,Notional=10012350,到期日6/12
|
||
// 利率准则列(利率准则)为空时固定利率不使用FR007, interest_rule不影响固定利率
|
||
// interestMode: 2=合约名义本金规模, 9=标的期初全价
|
||
// 平仓日 = 5/28 (53天 for T+1, 53天 for T+0), 4/6 平仓日变体 = 1天/2天
|
||
[DataTestMethod]
|
||
// 平仓日 4/6(T+1=closeDate4/6=1天, T+0=closeDate4/7=2天)
|
||
// T+1: tradeDate=4/3, startDate=4/6, closeDate=4/6
|
||
// T+0: tradeDate=4/6, startDate=4/6, closeDate=4/7
|
||
[DataRow("T+1固定正利率算头算尾_4月6日", true, true, 2, "0.0075", "205.73", "0406")]
|
||
[DataRow("T+0固定正利率算头算尾_4月6日", true, true, 2, "0.0075", "411.47", "0406")]
|
||
[DataRow("T+1固定负利率算头不算尾_4月6日", true, false, 9, "-0.0075", "0", "0406")]
|
||
[DataRow("T+1固定正利率算头不算尾_4月6日", true, false, 9, "0.0075", "0", "0406")]
|
||
[DataRow("T+0固定负利率算头不算尾_4月6日", true, false, 2, "-0.0075", "-205.73", "0406")]
|
||
[DataRow("T+0固定正利率算头不算尾_4月6日", true, false, 2, "0.0075", "205.73", "0406")]
|
||
// 平仓日 5/28(T+1=53天, T+0=53天)
|
||
[DataRow("T+1固定正利率算头算尾_5月28日", true, true, 2, "0.0075", "10903.86", "0528")]
|
||
[DataRow("T+0固定正利率算头算尾_5月28日", true, true, 2, "0.0075", "10903.86", "0528")]
|
||
[DataRow("T+1固定负利率算头不算尾_5月28日", true, false, 9, "-0.0075", "-10698.13", "0528")]
|
||
[DataRow("T+1固定正利率算头不算尾_5月28日", true, false, 9, "0.0075", "10698.13", "0528")]
|
||
[DataRow("T+0固定负利率算头不算尾_5月28日", true, false, 2, "-0.0075", "-10698.13", "0528")]
|
||
[DataRow("T+0固定正利率算头不算尾_5月28日", true, false, 2, "0.0075", "10698.13", "0528")]
|
||
public void 场景1_固定利率单利平仓(string note, bool calcFirst, bool calcLast,
|
||
int interestMode, string spreadStr, string oracleStr, string dateGroup)
|
||
{
|
||
var spread = decimal.Parse(spreadStr, System.Globalization.CultureInfo.InvariantCulture);
|
||
var oracle = decimal.Parse(oracleStr, System.Globalization.CultureInfo.InvariantCulture);
|
||
var mode = (calcFirst && calcLast) ? "11" : "10";
|
||
|
||
// 固定利率交易:起算日4/6, 到期日6/12
|
||
var startDate = new DateTime(2026, 4, 6);
|
||
var maturity = new DateTime(2026, 6, 12);
|
||
var td = CreateTrade(mode, 0, startDate, maturity, "UT-SCEN-1-FIX");
|
||
var position = CreatePosition(spread, 0, InterestTypeEnum.单利, startDate, maturity, interestMode, FixedNotional, isFixed: true);
|
||
|
||
// T+1: closeDate=4/6 (同起算日); T+0: closeDate=4/7 (起算日+1)
|
||
// 5/28变体: closeDate=5/28
|
||
DateTime closeDate;
|
||
if (dateGroup == "0528")
|
||
closeDate = new DateTime(2026, 5, 28);
|
||
else // 0406
|
||
closeDate = note.StartsWith("T+1") ? new DateTime(2026, 4, 6) : new DateTime(2026, 4, 7);
|
||
|
||
var flow = CalcCloseFlow(td, position, closeDate, new List<eod_swap_position>(), FixedNotional);
|
||
var eod = _eod.ExecuteClose(td, position, closeDate,
|
||
0m, 0m, new List<swap_flow_event> { flow }, FixedNotional, null);
|
||
AssertStrict(oracle, eod.TdCloseInterest, "场景1-固定 " + note);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 业务场景2:浮动利率第一重置期内平仓(4/27)(12用例,Excel全标"通过")
|
||
|
||
// 浮动利率,平仓日 4/27(第一重置期内),Notional=303139117.8
|
||
[DataTestMethod]
|
||
[DataRow("T+1浮动减点算头算尾(当前营业日)", true, true, true, 0, 2, "-0.021", "-38868.25")]
|
||
[DataRow("T+0浮动加点算头算尾(当前营业日)", true, true, true, 0, 2, "0.0025", "91273.94")]
|
||
[DataRow("T+1浮动减点算头不算尾(当前营业日)", true, true, false, 0, 9, "-0.021", "-32390.21")]
|
||
[DataRow("T+0浮动加点算头不算尾(当前营业日)", true, true, false, 0, 2, "0.0025", "78234.81")]
|
||
[DataRow("T+1浮动减点算头算尾", true, true, true, -1, 9, "-0.021", "-38868.25")]
|
||
[DataRow("T+0浮动加点算头算尾", true, true, true, -1, 2, "0.0025", "91273.94")]
|
||
[DataRow("T+1浮动减点算头不算尾", true, true, false, -1, 9, "-0.021", "-32390.21")]
|
||
[DataRow("T+0浮动加点算头不算尾", true, true, false, -1, 9, "0.0025", "78234.81")]
|
||
[DataRow("T+1浮动减点算头算尾(单利)", false, true, true, -1, 9, "-0.021", "-38868.25")]
|
||
[DataRow("T+0浮动加点算头算尾(单利)", false, true, true, -1, 2, "0.0025", "91273.94")]
|
||
[DataRow("T+1浮动减点算头不算尾(单利)", false, true, false, 0, 9, "-0.021", "-32390.21")]
|
||
[DataRow("T+0浮动加点算头不算尾(单利)", false, true, false, -1, 9, "0.0025", "78234.81")]
|
||
public void 场景2_第一重置期内平仓(string note, bool compound, bool calcFirst, bool calcLast,
|
||
int rule, int interestMode, string spreadStr, string oracleStr)
|
||
{
|
||
var spread = decimal.Parse(spreadStr, System.Globalization.CultureInfo.InvariantCulture);
|
||
var oracle = decimal.Parse(oracleStr, System.Globalization.CultureInfo.InvariantCulture);
|
||
var mode = (calcFirst && calcLast) ? "11" : "10";
|
||
var type = compound ? InterestTypeEnum.复利 : InterestTypeEnum.单利;
|
||
|
||
var startDate = spread >= 0 ? new DateTime(2026, 4, 21) : new DateTime(2026, 4, 22);
|
||
var td = CreateTrade(mode, rule, startDate, new DateTime(2026, 5, 19));
|
||
var position = CreatePosition(spread, rule, type, startDate, new DateTime(2026, 5, 19), interestMode, Notional);
|
||
|
||
// 平仓日 4/27(第一重置期内,非到期日)
|
||
var closeDate = new DateTime(2026, 4, 27);
|
||
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);
|
||
AssertStrict(oracle, eod.TdCloseInterest, "场景2 " + note);
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
}
|