using Newtonsoft.Json; using YLErp.DBModels.Enums; namespace YLErp.Modules.SwapModule { /// /// EQD-6968 FR007 不算尾平仓"上午未发布"误拦截 - RED 复现,修复后转绿(内存,不连库) /// 任务编号 EQD-6968;现象报告日 2026-08-17。参照 GLMS20260703CloseInterestTest 内存 FR007 写法。 /// [TestClass] public class GLMS20260817Fr007UnwindMorningTest { private sealed class StubSwapDealService : SwapDealService { private readonly bool _includeCloseDate; public StubSwapDealService(OptUserInfo optUser, bool includeCloseDate) : base(optUser) { _includeCloseDate = includeCloseDate; } public readonly List<(DateTime RequestDate, double Rate)> FloatRateCalls = new(); protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate) { rate = 0d; if (underlyingCode != "FR007") return false; var fr007 = new Dictionary { [new DateTime(2026, 7, 6)] = 0.0142, [new DateTime(2026, 7, 13)] = 0.01425, }; if (_includeCloseDate) fr007[new DateTime(2026, 7, 20)] = 0.0143; if (fr007.TryGetValue(valueDate.Date, out rate)) { FloatRateCalls.Add((valueDate.Date, rate)); return true; } return false; } } 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 SwapDealService MakeService(bool includeCloseDate) { return new StubSwapDealService( new OptUserInfo(0, nameof(GLMS20260817Fr007UnwindMorningTest), OptUserFrom.UnitTest), includeCloseDate); } private static trade CreateTrade() { 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 CreateBondPosition(InterestTypeEnum interestType, int interestRule = 0) { var intervalModels = new List { 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 = 7, interest_rule = interestRule, FloatRateUnderlyingCode = "FR007", FloatRate = 0m, PosiNotionalValue = Notional, UnderlyingCode = "2500002.IB", InterestSwapInterval = JsonConvert.SerializeObject(intervalModels) }; } private swap_flow_event CalcCloseInterest(SwapDealService svc, InterestTypeEnum interestType, int interestRule = 0) { var td = CreateTrade(); var position = CreateBondPosition(interestType, interestRule); var interests = svc.GetInterests( td, td.trade_extend, CloseDate, CloseDate, new List(), new List { position }, Notional, Notional, 1m, (int)SwapEventTypeEnum.平仓, false, Notional, false, settment: false, newCalcLast: false, closeList: null); Assert.AreEqual(1, interests.Count); return interests[0]; } private static eod_swap_position CreatePreEod() { return new eod_swap_position { id = 5001, PositionId = 1001, ValueDate = new DateTime(2026, 7, 13), FloatRate = 0.01425m, InterestProfitSum = -100000m, TdInterestPrincipal = Notional, InterestIncomeSum = -150000m }; } private swap_flow_event CalcCloseInterestEod(SwapDealService svc, InterestTypeEnum interestType, List eodPositions) { var td = CreateTrade(); var position = CreateBondPosition(interestType); var interests = svc.GetInterests( td, td.trade_extend, CloseDate, CloseDate, eodPositions, new List { position }, Notional, Notional, 1m, (int)SwapEventTypeEnum.平仓, false, Notional, false, settment: false, newCalcLast: false, closeList: null); Assert.AreEqual(1, interests.Count); return interests[0]; } /// /// [RED] 不算尾平仓,平仓日 FR007 未发布(内存缺失)→ 当前抛"获取不到FR007...价格"。 /// 期望:修复后应成功返回(不抛)。当前为 RED(测试失败)。 /// [TestMethod] public void Red_UnwindMorning_WithoutCloseDateFr007_ShouldSucceed() { var svc = MakeService(includeCloseDate: false); try { var fe = CalcCloseInterest(svc, InterestTypeEnum.复利); Assert.IsNotNull(fe); Assert.IsFalse(fe.InterestAmount == 0 && fe.FloatRate == 0, "返回的利息不应全为零"); } catch (Exception ex) { StringAssert.Contains(ex.Message, "FR007"); Assert.Fail($"RED 复现成功:不算尾平仓因平仓日 FR007 未发布被误拦截 —— {ex.Message}"); } } /// /// [Baseline] 同场景但提供平仓日 07-20 的 FR007 → 应成功(绿),隔离 stub/路径问题。 /// [TestMethod] public void Baseline_WithCloseDateFr007_Succeeds() { var svc = MakeService(includeCloseDate: true); var fe = CalcCloseInterest(svc, InterestTypeEnum.复利); Assert.IsNotNull(fe); Assert.IsTrue(fe.InterestAmount != 0, "提供末日 FR007 时应正常算出利息"); } /// /// [RED-重放] 不算尾全平(带前日日终持仓 → 走重放分支 replayEndDate=平仓日+1),平仓日 FR007 未发布 → 当前抛。 /// 修复后(BuildSegmentRates 用 exclusionEndDate 排除真实平仓日)应成功,平仓日用上一重置日利率。 /// [TestMethod] public void Red_FullClose_WithPreEod_WithoutCloseDateFr007_Succeeds() { var svc = MakeService(includeCloseDate: false); var preEod = CreatePreEod(); try { var fe = CalcCloseInterestEod(svc, InterestTypeEnum.复利, new List { preEod }); Assert.IsNotNull(fe); Assert.IsFalse(fe.InterestAmount == 0 && fe.FloatRate == 0, "返回的利息不应全为零"); } catch (Exception ex) { StringAssert.Contains(ex.Message, "FR007"); Assert.Fail($"RED-重放 复现成功:不算尾全平因平仓日 FR007 未发布被误拦截 —— {ex.Message}"); } } /// /// [Baseline-重放] 同场景但提供平仓日 07-20 FR007 → 应成功(绿),隔离 stub/路径问题。 /// [TestMethod] public void Baseline_FullClose_WithPreEod_WithCloseDateFr007_Succeeds() { var svc = MakeService(includeCloseDate: true); var preEod = CreatePreEod(); var fe = CalcCloseInterestEod(svc, InterestTypeEnum.复利, new List { preEod }); Assert.IsNotNull(fe); Assert.IsTrue(fe.InterestAmount != 0, "提供末日 FR007 时应正常算出利息"); } } }