Files
zszq-trs/UnitTestProject/Modules/SwapModule/SwapInterestScenario1And2Test.cs
T
hjhan 4a3fee9292 refactor(swap)+test: 删 GetInterests/CalcSwapInterests 死参数 needPrice/grossPrice;补工厂→接缝映射钉子
死参数收口(另一半):
- 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跳过,与基线逐位一致。
2026-08-14 16:57:56 +08:00

342 lines
18 KiB
C#
Raw 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>
/// 回归保护测试:业务场景1(固定利率收盘平仓)+ 业务场景2(浮动利率第一重置期内平仓)
/// --------------------------------------------------------------------------
/// 数据来源:缺陷测试-利息20260807晚.xlsx
/// - 业务场景1-浮动(8 用例):收盘到 4/2 利息=0Excel 标记"无关",此处验证返回 0
/// - 业务场景1-固定(12 用例):固定利率单利,平仓日 5/28 或 4/6Excel 全标"通过"
/// - 业务场景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,利息=08用例,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 标记"无关",利息应为 0T+1 起算日 4/22 > 4/20 天计息)
AssertStrict(0m, eod.TdCloseInterest, "场景1-浮动 " + note);
}
#endregion
#region 业务场景1-固定:固定利率单利,平仓日5/2812用例,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/6T+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/28T+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
}
}