using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; using System.Linq; using YLErp.DBModels; using YLErp.DBModels.Enums; using YLErp.Model; namespace YLErp.Modules.SwapModule { /// /// 用构造数据覆盖 MergePageEvent 全部 6 种场景 /// 不连数据库,纯内存,秒级运行 /// [TestClass] public class MergePageEventScenarioTest { private const int TradeId = 9001; private const string TradeNumber = "TEST-IS-202504240001"; private const string UnderlyingCode = "220205.IB"; private const long PositionId = 50001; #region 场景1:单条流水 + 无持仓 → 纯开仓 [TestMethod] public void Scenario1_SingleMerge_NoPosition_ShouldOpen() { var service = CreateService(positions: new List()); var merges = new List { CreateMerge(BsType: 1, Qty: 100000, AvgPrice: 1.0022m, Fee: 0, FeePending: 2000) }; var result = service.ExecuteMergePageEvent(TradeId, merges, new DateTime(2025, 4, 24)); Assert.AreEqual(1, result.Count); Assert.AreEqual((int)SwapFlowEventTypeEnum.开仓, result[0].EventType); Assert.AreEqual(1, result[0].PositionType); // 多头 Assert.AreEqual(100000, result[0].Quantity); Assert.AreEqual(1.0022m, result[0].TradingAmountAvg); Assert.AreEqual(0, result[0].MarkClosePnl); // 开仓无平仓盈亏 } #endregion #region 场景2:单条流水 + 同向持仓 → 追加开仓 [TestMethod] public void Scenario2_SingleMerge_SameDirectionPosition_ShouldOpen() { var positions = new List { CreatePosition(PositionType: 1, Qty: 50000, GrossPrice: 0.99m) }; var service = CreateService(positions); var merges = new List { CreateMerge(BsType: 1, Qty: 30000, AvgPrice: 1.005m) }; var result = service.ExecuteMergePageEvent(TradeId, merges, new DateTime(2025, 4, 24)); Assert.AreEqual(1, result.Count); Assert.AreEqual((int)SwapFlowEventTypeEnum.开仓, result[0].EventType); Assert.AreEqual(1, result[0].PositionType); // 同向多头 Assert.AreEqual(30000, result[0].Quantity); // 新开仓数量 } #endregion #region 场景3:单条流水 + 反向持仓(全平) → 纯平仓 [TestMethod] public void Scenario3_SingleMerge_OppositeFullClose_ShouldCloseOnly() { var positions = new List { CreatePosition(PositionType: 2, Qty: 100000, GrossPrice: 0.98m) }; var service = CreateService(positions); // 买入100000,但持仓是空头100000 → 全部平仓 var merges = new List { CreateMerge(BsType: 1, Qty: 100000, AvgPrice: 1.01m) }; var result = service.ExecuteMergePageEvent(TradeId, merges, new DateTime(2025, 4, 24)); Assert.AreEqual(1, result.Count); Assert.AreEqual((int)SwapFlowEventTypeEnum.平仓, result[0].EventType); Assert.AreEqual(2, result[0].PositionType); // 平空头 Assert.AreEqual(100000, result[0].Quantity); // 平仓盈亏 = (平仓均价 - 持仓期初价) * 平仓数量 * 合约乘数 var expectedPnl = (1.01m - 0.98m) * 100000 * 1; AssertDecimalEqual(expectedPnl, result[0].MarkClosePnl, 0.01m); } #endregion #region 场景4:单条流水 + 反向持仓(部分平) → 平仓+开仓 [TestMethod] public void Scenario4_SingleMerge_OppositePartialClose_ShouldCloseAndOpen() { var positions = new List { CreatePosition(PositionType: 2, Qty: 30000, GrossPrice: 0.98m) }; var service = CreateService(positions); // 买入100000,持仓空头30000 → 先平30000,再开70000 var merges = new List { CreateMerge(BsType: 1, Qty: 100000, AvgPrice: 1.01m) }; var result = service.ExecuteMergePageEvent(TradeId, merges, new DateTime(2025, 4, 24)); Assert.AreEqual(2, result.Count); // 第一个:平仓 Assert.AreEqual((int)SwapFlowEventTypeEnum.平仓, result[0].EventType); Assert.AreEqual(2, result[0].PositionType); Assert.AreEqual(30000, result[0].Quantity); var expectedClosePnl = (1.01m - 0.98m) * 30000 * 1; AssertDecimalEqual(expectedClosePnl, result[0].MarkClosePnl, 0.01m); // 第二个:开仓 Assert.AreEqual((int)SwapFlowEventTypeEnum.开仓, result[1].EventType); Assert.AreEqual(1, result[1].PositionType); // 剩余方向=买 Assert.AreEqual(70000, result[1].Quantity); // 100000 - 30000 } #endregion #region 场景5:两条流水 + 无持仓 → 一开一平 [TestMethod] public void Scenario5_TwoMerges_NoPosition_ShouldOpenThenClose() { var service = CreateService(positions: new List()); var merges = new List { CreateMerge(BsType: 1, Qty: 100000, AvgPrice: 1.00m), // 买 10万 CreateMerge(BsType: 2, Qty: 30000, AvgPrice: 1.01m) // 卖 3万 }; var result = service.ExecuteMergePageEvent(TradeId, merges, new DateTime(2025, 4, 24)); Assert.AreEqual(2, result.Count); // 第一个事件:开仓(大的那条) var openEvt = result.First(x => x.EventType == (int)SwapFlowEventTypeEnum.开仓); Assert.AreEqual(1, openEvt.PositionType); Assert.AreEqual(100000, openEvt.Quantity); // 第二个事件:平仓(小的那条平大的) var closeEvt = result.First(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓); Assert.AreEqual(1, closeEvt.PositionType); // 平的是多头的方向 Assert.AreEqual(30000, closeEvt.Quantity); // 平仓盈亏 = (卖均价 - 买均价) * 平仓数量 * 合约乘数 var expectedPnl = (1.01m - 1.00m) * 30000 * 1; AssertDecimalEqual(expectedPnl, closeEvt.MarkClosePnl, 0.01m); } #endregion #region 场景6:两条流水 + 有持仓 → 复杂组合 [TestMethod] public void Scenario6_TwoMerges_HasPosition_ShouldCloseThenOpen() { var positions = new List { CreatePosition(PositionType: 2, Qty: 30000, GrossPrice: 0.98m) // 空头持仓 }; var service = CreateService(positions); // 买10万 + 卖5万,持仓空头3万 var merges = new List { CreateMerge(BsType: 1, Qty: 100000, AvgPrice: 1.00m), // 买(反向) CreateMerge(BsType: 2, Qty: 50000, AvgPrice: 1.01m) // 卖(同向) }; var result = service.ExecuteMergePageEvent(TradeId, merges, new DateTime(2025, 4, 24)); // 至少有平仓事件(买的10万 vs 空头3万) Assert.IsTrue(result.Count >= 2, $"Expected at least 2 events, got {result.Count}"); // 第一个事件应该是平仓(反向流水平空头持仓) Assert.AreEqual((int)SwapFlowEventTypeEnum.平仓, result[0].EventType); Assert.AreEqual(2, result[0].PositionType); // 平空头 Assert.AreEqual(30000, result[0].Quantity); // 后续应有开仓事件(100000-30000=70000剩余,再和卖5万处理) var openEvents = result.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.开仓).ToList(); Assert.IsTrue(openEvents.Count >= 1, "Should have at least 1 open event"); } #endregion #region 辅助方法 private TestableSwapFlowEventService CreateService(List positions) { var user = new OptUserInfo(1, "Test", OptUserFrom.UnitTest); var trade = new trade { id = TradeId, TradeNumber = TradeNumber, TradeDate = new DateTime(2025, 4, 24), ExerciseDate = new DateTime(2025, 6, 20) }; var extend = new trade_extend { TradeId = TradeId }; extend.ExtendJson = Newtonsoft.Json.JsonConvert.SerializeObject(new TradeExtendJson { Direction = 2, // 支付 SettlementRules = 1 // T+1 }); var underlyings = new Dictionary { [UnderlyingCode] = new underlying_manager { UnderlyingCode = UnderlyingCode, UnderlyingInstrumentType = "TBonds" } }; return new TestableSwapFlowEventService( user, trade, extend, positions, underlyings, nextBusinessDay: d => d.AddDays(1), positionId: PositionId ); } private swap_flow_merge CreateMerge(int BsType, decimal Qty, decimal AvgPrice, decimal Fee = 0, decimal FeePending = 0) { return new swap_flow_merge { SwapTradeId = TradeId, SwapTradeNo = TradeNumber, UnderlyingCode = UnderlyingCode, OccurTime = new DateTime(2025, 4, 24), BsType = BsType, TradingQty = BsType == 1 ? Qty : -Qty, TradingAmount = Math.Abs(Qty) * AvgPrice, TradingAmountAvg = AvgPrice, TradingAmountFeeAvg = AvgPrice, TradingFee = Fee, TradingFeePending = FeePending, ContractSize = 1, ClientId = 10, DataState = 1 }; } private swap_position CreatePosition(int PositionType, decimal Qty, decimal GrossPrice) { return new swap_position { PositionId = PositionId, SwapTradeId = TradeId, UnderlyingCode = UnderlyingCode, PositionType = PositionType, PosiDirection = 2, // 支付 PosiQuantity = Qty, PosiGrossPrice = GrossPrice, PosiNetPrice = GrossPrice, ContractSize = 1, IsInitial = false, Invalid = false, PosiTradingFee = 0 }; } private static void AssertDecimalEqual(decimal expected, decimal actual, decimal tolerance) { var diff = Math.Abs(expected - actual); Assert.IsTrue(diff <= tolerance, $"Expected {expected}, got {actual}, diff {diff} (tol {tolerance})"); } #endregion } }