using YLErp.DBModels; using YLErp.DBModels.Enums; namespace YLErp.Modules.SwapModule { /// /// ComposePage 流水合成持仓 - 合成单元测试 /// ============================================================================ /// 验证 swap_flow_event(开仓/平仓事件)→ eod_swap_position(持仓)的转换。 /// ComposePage 是每笔开仓/平仓/互换都要经过的核心逻辑。 /// /// 场景参考 testable 分支 ComposePageScenarioTest,简化为最核心的 3 个: /// ① 空事件直接返回 /// ② 单条开仓 → 创建1条持仓,均价=开仓价 /// ③ 两条开仓(同标的) → 加权均价 /// ============================================================================ [TestClass] public class ComposePageScenarioTest { private const int SwapTradeId = 100; private static readonly DateTime TradeDate = new(2026, 4, 27); #region Stub private sealed class StubEodService : SwapEodPositionService { public List CreatedEodPositions { get; } = new(); public int ClientCashCallCount { get; private set; } private int _nextId = 1; public StubEodService() : base(new OptUserInfo(0, nameof(ComposePageScenarioTest), OptUserFrom.UnitTest)) { } // 内存数据 public Dictionary Trades { get; set; } = new(); public Dictionary Extends { get; set; } = new(); public List Positions { get; set; } = new(); public List EodPositions { get; set; } = new(); public eod_swap LastEodSwap { get; set; } protected override trade FindTrade(int swapTradeId) => Trades.TryGetValue(swapTradeId, out var t) ? t : null; protected override trade_extend FindTradeExtend(int tradeId) => Extends.TryGetValue(tradeId, out var e) ? e : null; protected override List FindEodSwapPositions(int swapTradeId, DateTime preSettleDate) => EodPositions.Where(x => x.SwapTradeId == swapTradeId && !x.Invalid && x.ValueDate >= preSettleDate).ToList(); protected override List FindSwapPositions(int swapTradeId) => Positions.Where(x => x.SwapTradeId == swapTradeId && !x.Invalid).ToList(); protected override eod_swap FindEodSwap(int swapTradeId, DateTime valueDate) => LastEodSwap?.SwapTradeId == swapTradeId ? LastEodSwap : null; protected override swap_event AddSwapEvent(DateTime tradeDate, int swapTradeId, int eventType, string data, int clientCashId, bool save, string reason) { return new swap_event { id = _nextId++, SwapTradeId = swapTradeId, EventType = eventType, ValueDate = tradeDate, EventData = data }; } protected override int AddClientCash(trade td, double amount, string action, DateTime valueDate) { ClientCashCallCount++; return _nextId++; } protected override void SaveEodSwapRecord(trade td, DateTime settleDate, DateTime preSettleDate) { // 不做任何事(测试不验证框架合约汇总) } protected override void ClearSwapPositionsForCompose(trade td, DateTime tradeDate, List eventTypes) { // 不做任何事(测试无历史事件需清理) } protected override void PersistEodSwapPosition(eod_swap_position position) { if (position.id == 0) position.id = _nextId++; CreatedEodPositions.Add(position); } protected override void SaveAllChanges() { } protected override double GetCurrencyRate(string quoteCurrency, string settlementCurrency, DateTime valueDate, bool seekPreday, CurrencyRateType currencyRateType) => 1.0; // override SaveEodPosition:捕获生成的 eod,绕过 UpdateSwapPosition 连库 protected override decimal SaveEodPosition(eod_swap_position newEodPayPosition, trade td, swap_flow_event eventFlow, decimal netPrice, decimal grossPrice, decimal netFeePrice, decimal netNoFeePrice, decimal payQty, decimal tradingFee, decimal posiNotionalValue, decimal dividendIn, decimal tdDividendIn, decimal closeQty, decimal closeFee, decimal closeMtmPnl, int posiType, bool isNewPosition) { // 设置关键字段(模拟生产逻辑的输出) newEodPayPosition.PosiNetPrice = netPrice; newEodPayPosition.PosiGrossPrice = grossPrice; newEodPayPosition.PosiQuantity = payQty; newEodPayPosition.PosiNotionalValue = posiNotionalValue; newEodPayPosition.SwapTradeId = td.id; newEodPayPosition.ClientId = td.ClientId; PersistEodSwapPosition(newEodPayPosition); return 0m; // 开仓费(测试不关心) } public void ExecuteComposePage(int swapTradeId, List flowEvents, DateTime tradeDate) { // needTrans=false 跳过事务 ComposePage(swapTradeId, flowEvents, tradeDate, needTrans: false); } } #endregion #region 数据构建 private static trade CreateTrade() { return new trade { id = SwapTradeId, TradeNumber = "UT-COMPOSE-001", ClientId = 999998, TradeType = "收益互换", TradeDate = TradeDate, StartDate = TradeDate, ExerciseDate = new DateTime(2027, 4, 27), TradeStatus = "确认成交", ValidState = "Valid", StructureType = "单标的", QuoteCurrency = "CNY", SettlementCurrency = "CNY", trade_extend = new trade_extend { TradeId = SwapTradeId } }; } private static swap_position CreateFloatPosition(int positionId = 1, int positionType = 1) { return new swap_position { id = positionId, SwapTradeId = SwapTradeId, PosiDirection = 2, PositionType = positionType, UnderlyingCode = "210210.IB", ContractSize = 1m, PosiQuantity = 0, PosiNotionalValue = 0, PosiNetPrice = 0, PosiGrossPrice = 0, IsInitial = true, Invalid = false }; } private static swap_flow_event CreateOpenEvent(int positionId, decimal qty, decimal feeAvg, decimal avg, int positionType = 1) { return new swap_flow_event { SwapTradeId = SwapTradeId, EventType = (int)SwapFlowEventTypeEnum.开仓, PositionId = positionId, Quantity = qty, TradingAmountFeeAvg = feeAvg, TradingAmountAvg = avg, TradingAmountNetFeeAvg = feeAvg, TradingAmountNetAvg = avg, ContractSize = 1m, PositionType = positionType, MarkClosePnl = 0, DividendIn = 0, CloseFee = 0, TradingFeePending = 0, UnwindDate = TradeDate, EventDate = TradeDate, PayDate = TradeDate, DataState = (int)SwapFlowDateStateEnum.等待完成 }; } private static void AssertDecimalEqual(decimal expected, decimal actual, decimal tolerance, string message = "") { Assert.IsTrue(Math.Abs(expected - actual) <= tolerance, $"{message} Expected: {expected}, Actual: {actual}, Diff: {expected - actual}"); } private static StubEodService CreateService() { var svc = new StubEodService(); svc.Trades[SwapTradeId] = CreateTrade(); svc.Extends[SwapTradeId] = CreateTrade().trade_extend; svc.Positions.Add(CreateFloatPosition()); return svc; } #endregion // ================================================================ // 场景1:空事件 → 直接返回,不创建任何持仓 // ================================================================ [TestMethod] public void CP_001_空事件不创建持仓() { var service = CreateService(); service.ExecuteComposePage(SwapTradeId, new List(), TradeDate); Assert.AreEqual(0, service.CreatedEodPositions.Count, "无事件不应创建持仓"); } // ================================================================ // 场景2:单条开仓 → 创建1条持仓,均价=开仓价 // ================================================================ [TestMethod] public void CP_002_单条开仓创建一条持仓() { var service = CreateService(); var events = new List { CreateOpenEvent(positionId: 1, qty: 1000, feeAvg: 1.0050m, avg: 1.0020m) }; service.ExecuteComposePage(SwapTradeId, events, TradeDate); Assert.AreEqual(1, service.CreatedEodPositions.Count, "应创建1条持仓"); var pos = service.CreatedEodPositions[0]; Assert.AreEqual(1000m, pos.PosiQuantity, "持仓数量=1000"); AssertDecimalEqual(1.0050m, pos.PosiNetPrice, 0.0001m, "含费均价"); AssertDecimalEqual(1.0020m, pos.PosiGrossPrice, 0.0001m, "不含费均价"); Assert.AreEqual((int)SwapFlowDateStateEnum.完成, events[0].DataState, "事件应标记完成"); } // ================================================================ // 场景3:两条开仓(同标的) → 加权均价 // ================================================================ [TestMethod] public void CP_003_两条开仓加权均价() { var service = CreateService(); var events = new List { CreateOpenEvent(positionId: 1, qty: 600, feeAvg: 1.0040m, avg: 1.0010m), CreateOpenEvent(positionId: 1, qty: 400, feeAvg: 1.0060m, avg: 1.0030m) }; service.ExecuteComposePage(SwapTradeId, events, TradeDate); Assert.AreEqual(1, service.CreatedEodPositions.Count); var pos = service.CreatedEodPositions[0]; // 加权均价: netPrice = (1.0040*600 + 1.0060*400) / 1000 = 1.0048 AssertDecimalEqual(1.0048m, pos.PosiNetPrice, 0.0001m, "加权含费均价"); // grossPrice = (1.0010*600 + 1.0030*400) / 1000 = 1.0018 AssertDecimalEqual(1.0018m, pos.PosiGrossPrice, 0.0001m, "加权不含费均价"); } // ================================================================ // 场景4:一条开仓+一条平仓 → 验证平仓扣减数量 // ================================================================ [TestMethod] public void CP_004_开仓后平仓扣减数量() { var service = CreateService(); var events = new List { CreateOpenEvent(positionId: 1, qty: 1000, feeAvg: 1.0050m, avg: 1.0020m), new swap_flow_event { SwapTradeId = SwapTradeId, EventType = (int)SwapFlowEventTypeEnum.平仓, PositionId = 1, Quantity = 400, TradingAmountFeeAvg = 1.0050m, TradingAmountAvg = 1.0020m, ContractSize = 1m, PositionType = 1, MarkClosePnl = 100m, DividendIn = 0, CloseFee = 5m, TradingFeePending = 0, UnwindDate = TradeDate, EventDate = TradeDate, PayDate = TradeDate, DataState = (int)SwapFlowDateStateEnum.等待完成 } }; service.ExecuteComposePage(SwapTradeId, events, TradeDate); Assert.AreEqual(1, service.CreatedEodPositions.Count); var pos = service.CreatedEodPositions[0]; // 开仓1000 - 平仓400 = 剩余600 Assert.AreEqual(600m, pos.PosiQuantity, "开仓1000-平仓400=剩余600"); Assert.IsTrue(service.ClientCashCallCount > 0, "平仓应产生资金记录"); } } }