Files
zszq-trs/UnitTestProject/Modules/SwapModule/GLMS20260817Fr007UnwindMorningTest.cs
T

282 lines
12 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 Newtonsoft.Json;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// EQD-6968 FR007 不算尾平仓"上午未发布"误拦截 —— 修复后回归套件(内存,不连库)。
/// 任务编号 EQD-6968;现象报告日 2026-08-17。参照 GLMS20260703CloseInterestTest 内存 FR007 写法。
///
/// 设计:所有场景经单一 Run 运行器驱动真实 GetInterests 平仓利息路径;
/// 内存 StubSwapDealService 重写 TryGetFloatRate 按日期返回 FR007(缺失即返回 false → 触发取价失败)。
/// 覆盖两条计息路径(复利 CalcDailyCompoundInterest / 单利 CalcDailySimpleInterest)共用的修复点 BuildSegmentRates
/// 以及全平重放分支、非整倍数边界、数值一致性("跳过取价=沿用上一重置日利率")。
/// </summary>
[TestClass]
public class GLMS20260817Fr007UnwindMorningTest
{
private static readonly Dictionary<DateTime, double> Fr007Market = new()
{
[new DateTime(2026, 7, 6)] = 0.0142,
[new DateTime(2026, 7, 13)] = 0.01425,
[new DateTime(2026, 7, 20)] = 0.0143,
};
private const double PreviousResetRate = 0.01425;
private const decimal Notional = 279486108.21m;
private const int AnnualDays = 365;
private const decimal Spread = -0.0155m;
private static readonly DateTime StartDate = new(2026, 7, 6);
private static readonly DateTime TradeDate = new(2026, 7, 3);
private static readonly DateTime CloseDate = new(2026, 7, 20);
private static readonly DateTime NonIntCloseDate = new(2026, 7, 22);
private sealed class StubSwapDealService : SwapDealService
{
private readonly HashSet<DateTime> _omit;
private readonly double _closeRate;
public readonly List<DateTime> PricedDates = new();
public StubSwapDealService(OptUserInfo optUser, IEnumerable<DateTime> omit, double closeRate = 0.0143)
: base(optUser)
{
_omit = new HashSet<DateTime>(omit.Select(d => d.Date));
_closeRate = closeRate;
}
protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate)
{
rate = 0d;
if (underlyingCode != "FR007") return false;
var map = new Dictionary<DateTime, double>(Fr007Market) { [CloseDate] = _closeRate };
if (_omit.Contains(valueDate.Date)) return false;
if (map.TryGetValue(valueDate.Date, out rate))
{
PricedDates.Add(valueDate.Date);
return true;
}
return false;
}
}
private sealed class Outcome
{
public swap_flow_event Fe;
public Exception Ex;
public bool Threw => Ex != null;
}
private static OptUserInfo MakeOptUser() =>
new(0, nameof(GLMS20260817Fr007UnwindMorningTest), OptUserFrom.UnitTest);
private static trade BuildTrade(DateTime closeDate)
{
var extend = new trade_extend
{
TradeId = 1,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays,
InterestCalcMode = "10",
SettlementRules = 0
})
};
return new trade
{
id = 1,
TradeNumber = "GLMS-20260817-FR007-MORNING",
ClientId = 999998,
TradeType = "债券TRS",
TradeDate = TradeDate,
StartDate = StartDate,
ExerciseDate = closeDate.AddDays(1),
TradeStatus = "已平仓",
ValidState = "Valid",
trade_extend = extend
};
}
private static swap_position BuildPosition(InterestTypeEnum interestType, DateTime closeDate, int restDays = 7)
{
var intervalModels = new List<IntervalModel>
{
new IntervalModel { Date = closeDate, Rate = Spread, Settlement = 0 }
};
return new swap_position
{
id = 1001,
SwapTradeId = 1,
PositionType = (int)PositionTypeFlag.Unknown,
InterestDirection = (int)SwapDirectionEnum.支付,
InterestMode = (int)InterestModeEnum.标的期初全价,
InterestRateDefault = Spread,
InterestPrincipalFix = Notional,
PosiStartDate = StartDate,
PosiMatuirityDate = closeDate,
IsInitial = true,
Invalid = false,
InterestType = (int)interestType,
IsAnnualized = true,
interest_rest_days = restDays,
interest_rule = 0,
FloatRateUnderlyingCode = "FR007",
FloatRate = 0m,
PosiNotionalValue = Notional,
UnderlyingCode = "2500002.IB",
InterestSwapInterval = JsonConvert.SerializeObject(intervalModels)
};
}
private static eod_swap_position BuildPreEod(DateTime valueDate)
{
return new eod_swap_position
{
id = 5001,
PositionId = 1001,
ValueDate = valueDate,
FloatRate = 0.01425m,
InterestProfitSum = -100000m,
TdInterestPrincipal = Notional,
InterestIncomeSum = -150000m
};
}
private static Outcome Run(
InterestTypeEnum interestType,
bool includeCloseDate,
DateTime? closeDate = null,
int restDays = 7,
eod_swap_position preEod = null,
decimal closePrecent = 1m,
DateTime? omitDate = null,
double closeRate = 0.0143)
{
var cd = closeDate ?? CloseDate;
var omit = new HashSet<DateTime>();
if (omitDate.HasValue) omit.Add(omitDate.Value.Date);
else if (!includeCloseDate) omit.Add(cd.Date);
var svc = new StubSwapDealService(MakeOptUser(), omit, closeRate);
var td = BuildTrade(cd);
var position = BuildPosition(interestType, cd, restDays);
var eodList = preEod == null
? new List<eod_swap_position>()
: new List<eod_swap_position> { preEod };
try
{
var interests = svc.GetInterests(
td, td.trade_extend, cd, cd, eodList,
new List<swap_position> { position },
Notional, Notional, closePrecent,
(int)SwapEventTypeEnum.平仓, false, Notional,
false, settment: false, newCalcLast: false, closeList: null);
Assert.AreEqual(1, interests.Count, "应返回恰好 1 条利息事件");
return new Outcome { Fe = interests[0] };
}
catch (Exception ex)
{
return new Outcome { Ex = ex };
}
}
private static void AssertNoThrow(Outcome o, string scenario)
{
Assert.IsFalse(o.Threw, scenario + " 不应因平仓日 FR007 未发布而抛异常:" + o.Ex?.Message);
Assert.IsNotNull(o.Fe, scenario + " 应返回利息事件");
Assert.IsFalse(o.Fe.InterestAmount == 0 && o.Fe.FloatRate == 0, scenario + " 利息不应全为零");
}
[TestMethod]
public void Red_Compound_WithoutCloseDateFr007_Succeeds()
{
AssertNoThrow(Run(InterestTypeEnum.复利, includeCloseDate: false), "复利-无preEod-缺平仓日");
}
[TestMethod]
public void Baseline_Compound_WithCloseDateFr007_Succeeds()
{
AssertNoThrow(Run(InterestTypeEnum.复利, includeCloseDate: true), "复利-无preEod-有平仓日");
}
[TestMethod]
public void Red_Compound_FullClose_WithPreEod_WithoutCloseDateFr007_Succeeds()
{
AssertNoThrow(Run(InterestTypeEnum.复利, includeCloseDate: false, preEod: BuildPreEod(new DateTime(2026, 7, 13))),
"复利-全平重放-缺平仓日");
}
[TestMethod]
public void Baseline_Compound_FullClose_WithPreEod_WithCloseDateFr007_Succeeds()
{
AssertNoThrow(Run(InterestTypeEnum.复利, includeCloseDate: true, preEod: BuildPreEod(new DateTime(2026, 7, 13))),
"复利-全平重放-有平仓日");
}
[TestMethod]
public void Red_Simple_WithoutPreEod_WithoutCloseDateFr007_Succeeds()
{
AssertNoThrow(Run(InterestTypeEnum.单利, includeCloseDate: false), "单利-无preEod-缺平仓日");
}
[TestMethod]
public void Red_Simple_WithPreEod_WithoutCloseDateFr007_Succeeds()
{
AssertNoThrow(Run(InterestTypeEnum.单利, includeCloseDate: false, preEod: BuildPreEod(StartDate)),
"单利-带preEod-缺平仓日");
}
[TestMethod]
public void Consistency_Compound_SkipEqualsPreviousRate()
{
var worldA = Run(InterestTypeEnum.复利, includeCloseDate: false);
var worldB = Run(InterestTypeEnum.复利, includeCloseDate: true, closeRate: PreviousResetRate);
Assert.IsFalse(worldA.Threw, "世界A 不应抛:" + worldA.Ex?.Message);
Assert.IsFalse(worldB.Threw, "世界B 不应抛:" + worldB.Ex?.Message);
Assert.AreEqual(worldA.Fe.InterestAmount, worldB.Fe.InterestAmount,
"缺价回退世界 应与 显式置上一期利率世界 利息完全一致(钉死=沿用上期)");
}
[TestMethod]
public void Consistency_Simple_SkipEqualsPreviousRate()
{
var worldA = Run(InterestTypeEnum.单利, includeCloseDate: false, preEod: BuildPreEod(StartDate));
var worldB = Run(InterestTypeEnum.单利, includeCloseDate: true, preEod: BuildPreEod(StartDate), closeRate: PreviousResetRate);
Assert.IsFalse(worldA.Threw, "世界A 不应抛:" + worldA.Ex?.Message);
Assert.IsFalse(worldB.Threw, "世界B 不应抛:" + worldB.Ex?.Message);
Assert.AreEqual(worldA.Fe.InterestAmount, worldB.Fe.InterestAmount,
"单利:缺价回退世界 应与 显式置上一期利率世界 利息完全一致");
}
[TestMethod]
public void Boundary_Compound_NonIntegerMultiple_LastResetStillPrices()
{
var o = Run(InterestTypeEnum.复利, includeCloseDate: false, closeDate: NonIntCloseDate, omitDate: new DateTime(2026, 7, 20));
Assert.IsTrue(o.Threw, "非整倍数时末段重置日 7/20 缺价应抛异常(该日利率被消费)");
StringAssert.Contains(o.Ex.Message, "FR007");
}
[TestMethod]
public void Boundary_Compound_NonIntegerMultiple_WithCloseDateSucceeds()
{
AssertNoThrow(Run(InterestTypeEnum.复利, includeCloseDate: true, closeDate: NonIntCloseDate),
"非整倍数-有7/20价-应成功");
}
[TestMethod]
public void Boundary_Simple_NonIntegerMultiple_LastResetStillPrices()
{
var o = Run(InterestTypeEnum.单利, includeCloseDate: false, closeDate: NonIntCloseDate,
preEod: BuildPreEod(StartDate), omitDate: new DateTime(2026, 7, 20));
Assert.IsTrue(o.Threw, "单利 非整倍数时末段重置日 7/20 缺价应抛异常");
StringAssert.Contains(o.Ex.Message, "FR007");
}
[TestMethod]
public void Boundary_Simple_NonIntegerMultiple_WithCloseDateSucceeds()
{
AssertNoThrow(Run(InterestTypeEnum.单利, includeCloseDate: true, closeDate: NonIntCloseDate, preEod: BuildPreEod(StartDate)),
"单利-非整倍数-有7/20价-应成功");
}
}
}