using YLErp.DBModels; using YLErp.DBModels.Enums; namespace YLErp.Modules.SwapModule { /// /// SwapPositionCompose 日终归档端到端测试 /// ============================================================================ /// 借鉴 testable 分支 SwapPositionComposeScenarioTest,基于当前分支 seam 重写。 /// 覆盖 DealFloatPositions 的首次归档/Copy/Update/异常路径。 /// 利息腿场景(自动互换)因 CalcSwapInterests 参数适配复杂留后续。 /// ============================================================================ [TestClass] public class SwapPositionComposeScenarioTest { private const int SwapTradeId = 100; private static readonly DateTime SettleDate = new(2025, 4, 24); private static readonly DateTime PreSettleDate = new(2025, 4, 23); #region 可测试化子类 /// /// 继承 SwapEodPositionService,override SwapPositionCompose 路径上的 seam。 /// 适配当前分支 seam 签名(GetUnderlyingPrice 带 out、GetCurrencyRate 返回 double 等)。 /// private sealed class TestableSwapEodService : TestableSwapEodPositionService { private readonly List _trades; private readonly List _positions; private readonly List _eodPositions; private readonly List _eodSwaps; private readonly List _extends; private readonly List _flowEvents; private readonly decimal _price; private readonly decimal _vobp; // 输出别名(转发到基类捕获属性) public List CreatedEodPositions => PersistedPositions; public TestableSwapEodService( List trades, List positions, List eodPositions, List eodSwaps, List extends, List flowEvents, decimal price = 100m, decimal vobp = 0m) : base(nameof(SwapPositionComposeScenarioTest)) { _trades = trades; _positions = positions; _eodPositions = eodPositions; _eodSwaps = eodSwaps; _extends = extends; _flowEvents = flowEvents; _price = price; _vobp = vobp; } // SwapPositionCompose 路径 seam override protected override List FindActiveSwapTrades(DateTime settleDate, IEnumerable clientIds) => _trades; protected override List FindAllSwapPositions(List tradeIds) => _positions; protected override List FindTradeExtends(List tradeIds) => _extends; protected override List FindEodSwapsByDate(DateTime valueDate) => _eodSwaps; protected override List FindFlowEvents(int swapTradeId, DateTime settleDate) => _flowEvents; protected override List FindEodSwapPositions(int swapTradeId, DateTime preSettleDate) => _eodPositions.Where(x => x.SwapTradeId == swapTradeId && x.ValueDate >= preSettleDate).ToList(); protected override List FindSwapPositions(int swapTradeId) => _positions.Where(x => x.SwapTradeId == swapTradeId && !x.IsInitial).ToList(); // DealFloatPositions 路径 seam override protected override underlying_manager GetUnderlyingData(string underlyingCode) => new underlying_manager { ValueAddedTax = 0m, UnderlyingInstrumentType = "TBonds" }; protected override decimal GetUnderlyingPrice(string code, DateTime settleDate, out decimal vobp) { vobp = _vobp; return _price; } protected override decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio) => 0m; // 持久化/事务 seam override(PersistEodSwapPosition/SaveAllChanges/GetCurrencyRate/AddClientCash 由基类提供) protected override void SaveEodSwapRecord(trade td, DateTime settleDate, DateTime preSettleDate) { } protected override void ExecuteInTransaction(Action action) => action(); protected override void ClearSwapPositionsForCompose(trade td, DateTime tradeDate, List eventTypes) { } public override void ClearSwapPositions(trade td, DateTime valueDate, List eventTypes, bool delAfter) { } protected override swap_event AddSwapEvent(DateTime tradeDate, int swapTradeId, int eventType, string data, int clientCashId, bool save, string reason) { return new swap_event { id = 1 }; } protected override List CalcSwapInterests( trade td, trade_extend tradeExtend, DateTime valueDate, DateTime unwindDate, List eodPositions, List positions, decimal posiNotionalValue, decimal posiLongNotionalValue, decimal posiShortNotionalValue, decimal closePosiNotionalValue, decimal closePrecent, int eventType, bool tdClose, bool needPrice, decimal grossPrice, decimal orginPv, bool add = false, bool settment = true, bool newCalcLast = false, List closeList = null) => new List(); public void ExecuteSwapPositionCompose(DateTime settleDate, DateTime preSettleDate) => SwapPositionCompose(settleDate, preSettleDate, null); } #endregion #region 工厂方法 private static trade CreateTrade(DateTime? startDate = null) { var date = startDate ?? SettleDate; return new trade { id = SwapTradeId, TradeNumber = "TEST-COMPOSE-001", ClientId = 10, TradeType = "收益互换", TradeDate = date, StartDate = date, ExerciseDate = SettleDate.AddYears(1), TradeStatus = "确认成交", ValidState = "Valid", QuoteCurrency = "CNY", SettlementCurrency = "CNY", StructureType = "普通债券类收益互换", OriginalStockEqvNotional = 100000, TradePrice = 0 }; } private static trade_extend CreateExtend() { return new trade_extend { TradeId = SwapTradeId, ExtendJson = @"{""NeedOpenFee"":false,""AnnualDays"":365,""SettlementRules"":0,""Direction"":1,""FlowBookMode"":0}" }; } private static swap_position CreateFloatPosition(long positionId, decimal qty) { return new swap_position { id = positionId, SwapTradeId = SwapTradeId, PositionId = positionId, PosiDirection = 1, PositionType = (int)PositionTypeFlag.Long, UnderlyingCode = "220205.IB", UnderlyingInstrumentType = "TBonds", ContractSize = 1m, CountRatio = 1m, IsInitial = true, Invalid = false, PosiQuantity = qty, PosiNotionalValue = qty, PosiNetPrice = 1.0050m, PosiGrossPrice = 1.0020m, PosiNetFeePrice = 1.0000m, PosiNetNoFeePrice = 0.9970m, InterestDirection = 0 }; } private static eod_swap_position CreateFloatEodPosition(long positionId, decimal qty, decimal grossPrice) { return new eod_swap_position { SwapTradeId = SwapTradeId, PositionId = positionId, ValueDate = PreSettleDate, PosiDirection = 1, PositionType = (int)PositionTypeFlag.Long, Invalid = false, PosiQuantity = qty, PosiGrossPrice = grossPrice, PosiNetPrice = 1.0050m, PosiNetFeePrice = 1.0030m, PosiNetNoFeePrice = 1.0000m, UnderlyingCode = "220205.IB", ContractSize = 1m, InterestIncomeSum = 0m, InterestProfitSum = 0m, PosiNotionalValue = qty }; } private static swap_flow_event CreateCloseFlowEvent(long positionId, decimal qty) { return new swap_flow_event { SwapTradeId = SwapTradeId, PositionId = positionId, EventType = (int)SwapFlowEventTypeEnum.平仓, Quantity = qty, EventDate = SettleDate, UnwindDate = SettleDate, MarkClosePnl = 500m, CloseFee = 10m, DividendIn = 5m, TradingAmountAvg = 1.0030m, DataState = (int)SwapFlowDateStateEnum.完成 }; } #endregion // ================================================================ // 场景1:首次归档(无前日eod,交易首日) // ================================================================ [TestMethod] public void SPC_001_首次归档_无前日Eod_直接取初始持仓() { var td = CreateTrade(); var extend = CreateExtend(); var positions = new List { CreateFloatPosition(1, 1000) }; var service = new TestableSwapEodService( new List { td }, positions, new List(), new List(), new List { extend }, new List()); service.ExecuteSwapPositionCompose(SettleDate, PreSettleDate); Assert.IsTrue(service.CreatedEodPositions.Count >= 1, "应创建至少1条eod"); var floatEod = service.CreatedEodPositions.FirstOrDefault(x => x.PositionId == 1); Assert.IsNotNull(floatEod, "应创建浮动腿持仓"); Assert.AreEqual(1000m, floatEod.PosiQuantity, "首次归档 PosiQuantity=初始持仓数量"); Console.WriteLine($"SPC_001: PosiQuantity={floatEod.PosiQuantity} ✅"); } // ================================================================ // 场景2:有前日eod无事件 → Copy // ================================================================ [TestMethod] public void SPC_002_Copy分支_有前日Eod无事件_价格原样复制() { var td = CreateTrade(); var extend = CreateExtend(); var positions = new List { CreateFloatPosition(1, 1000) }; var prevEod = new List { CreateFloatEodPosition(1, 1000, 1.0020m) }; var service = new TestableSwapEodService( new List { td }, positions, prevEod, new List(), new List { extend }, new List()); service.ExecuteSwapPositionCompose(SettleDate, PreSettleDate); var floatEod = service.CreatedEodPositions.FirstOrDefault(x => x.PositionId == 1); Assert.IsNotNull(floatEod); Assert.AreEqual(1000m, floatEod.PosiQuantity, "Copy分支 PosiQuantity不变"); Assert.AreEqual(1.0020m, floatEod.PosiGrossPrice, "Copy分支 PosiGrossPrice从前日eod复制"); Console.WriteLine($"SPC_002: PosiQuantity={floatEod.PosiQuantity}, PosiGrossPrice={floatEod.PosiGrossPrice} ✅"); } // ================================================================ // 场景3:有平仓事件 → Update(持仓扣减) // ================================================================ [TestMethod] public void SPC_003_Update分支_有平仓事件_持仓扣减() { var td = CreateTrade(); var extend = CreateExtend(); var positions = new List { CreateFloatPosition(1, 1000) }; var prevEod = new List { CreateFloatEodPosition(1, 1000, 1.0020m) }; var flowEvents = new List { CreateCloseFlowEvent(1, 400) }; var service = new TestableSwapEodService( new List { td }, positions, prevEod, new List(), new List { extend }, flowEvents); service.ExecuteSwapPositionCompose(SettleDate, PreSettleDate); var floatEod = service.CreatedEodPositions.FirstOrDefault(x => x.PositionId == 1); Assert.IsNotNull(floatEod); Assert.AreEqual(600m, floatEod.PosiQuantity, "Update分支 PosiQuantity=1000-400=600"); Assert.AreEqual(400m, floatEod.TdCloseQty, "TdCloseQty=平仓数量400"); Console.WriteLine($"SPC_003: PosiQuantity={floatEod.PosiQuantity}, TdCloseQty={floatEod.TdCloseQty} ✅"); } // ================================================================ // 场景4:未收盘抛异常 // ================================================================ [TestMethod] public void SPC_004_未收盘_非交易首日无前日Eod_抛异常() { // 交易起始日早于收盘日(非交易首日),且无前日eod var td = CreateTrade(startDate: SettleDate.AddDays(-10)); var extend = CreateExtend(); var positions = new List { CreateFloatPosition(1, 1000) }; var service = new TestableSwapEodService( new List { td }, positions, new List(), new List(), new List { extend }, new List()); var ex = Assert.ThrowsException(() => service.ExecuteSwapPositionCompose(SettleDate, PreSettleDate)); Assert.IsTrue(ex.Message.Contains("未收盘"), $"异常消息应含'未收盘',实际:{ex.Message}"); Console.WriteLine($"SPC_004: 抛异常'{ex.Message}' ✅"); } } }