Files
zszq-trs/UnitTestProject/Modules/SwapModule/ComposePageScenarioTest.cs
T
hjhan 7b007bddfa refactor(swap-test): 抽取 TestableSwapEodPositionService 公共基类收敛重复 Stub
- 新建 TestableSwapEodPositionService 收敛 8/8 Stub 重复的高频 override
  (PersistEodSwapPosition/SaveAllChanges/GetCurrencyRate/AddClientCash)
  + 统一 OptUserInfo 构造 + PersistedPositions/SaveChangesCount 输出捕获
- SwapEodPositionService.DealInterests 改 protected virtual(行为零变化)
- 8 个 ScenarioTest 改为继承基类,删除重复 override
- 消灭 DealInterestsScenarioTest/DealInterestsGoldenReplayTest 的反射调用
  (typeof().GetMethod().Invoke → 直接调用 DealInterests)

验证:dotnet build 0 错误;dotnet test SwapModule 284通过/6跳过/0失败
2026-07-23 11:16:11 +08:00

254 lines
12 KiB
C#

using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 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 : TestableSwapEodPositionService
{
// 输出别名(转发到基类捕获属性,保持测试断言不变)
public List<eod_swap_position> CreatedEodPositions => PersistedPositions;
public int ClientCashCallCount => ClientCashCalls.Count;
public StubEodService() : base(nameof(ComposePageScenarioTest))
{
}
// 内存数据
public Dictionary<int, trade> Trades { get; set; } = new();
public Dictionary<int, trade_extend> Extends { get; set; } = new();
public List<swap_position> Positions { get; set; } = new();
public List<eod_swap_position> 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<eod_swap_position> FindEodSwapPositions(int swapTradeId, DateTime preSettleDate)
=> EodPositions.Where(x => x.SwapTradeId == swapTradeId && !x.Invalid && x.ValueDate >= preSettleDate).ToList();
protected override List<swap_position> 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 = 1, SwapTradeId = swapTradeId, EventType = eventType, ValueDate = tradeDate, EventData = data };
}
protected override void SaveEodSwapRecord(trade td, DateTime settleDate, DateTime preSettleDate)
{
// 不做任何事(测试不验证框架合约汇总)
}
protected override void ClearSwapPositionsForCompose(trade td, DateTime tradeDate, List<int> eventTypes)
{
// 不做任何事(测试无历史事件需清理)
}
// 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<swap_flow_event> 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<swap_flow_event>(), TradeDate);
Assert.AreEqual(0, service.CreatedEodPositions.Count, "无事件不应创建持仓");
}
// ================================================================
// 场景2:单条开仓 → 创建1条持仓,均价=开仓价
// ================================================================
[TestMethod]
public void CP_002_单条开仓创建一条持仓()
{
var service = CreateService();
var events = new List<swap_flow_event>
{
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<swap_flow_event>
{
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<swap_flow_event>
{
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, "平仓应产生资金记录");
}
}
}