refactor(test): SwapDealSettlementTest拆分为SwapUnwind/SwapIncome+提取共享stub

按业务拆分原 SwapDealSettlementTest(8场景)为3个文件,提升可读性:
- TestableSwapDealService.cs: 共享stub(7个seam override)+工厂方法(CreateTrade/CreateUnwindData)
- SwapUnwindScenarioTest.cs: 平仓全流程6场景(UW_001~006: 全平/部分平/含预付金/含费价重算/审核/提交)
- SwapIncomeScenarioTest.cs: 结息2场景(SI_001~002: 正常结息/含预付金返息)

命名对齐 testable 分支风格(SwapUnwindScenarioTest),场景前缀改 UW_/SI_ 更直观。
删除 SwapDealSettlementTest.cs(内容已拆分迁移)。
SwapModule 173测试全绿,无回归。
This commit is contained in:
hjhan
2026-07-03 18:12:52 +08:00
parent 99958f9b43
commit 4fede8339a
4 changed files with 385 additions and 426 deletions
@@ -0,0 +1,191 @@
using Newtonsoft.Json;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 互换平仓全流程测试(SwapUnwind/ApproveSwapTrade/ApplySwapTrade/DealFloatPosition
/// ============================================================================
/// 借鉴 testable 分支 SwapUnwindScenarioTest,基于当前分支 TestableSwapDealService 共享 stub。
/// 命名规范说明(见《互换价格字段命名规范决策文档》):
/// PosiGrossPrice 现状名,实为"期初全价不含费",规范名 EntryDirtyPrice
/// TradingAmountAvg 现状名,实为"期末全价不含费",规范名 ExitDirtyPrice
/// ============================================================================
[TestClass]
public class SwapUnwindScenarioTest
{
// ================================================================
// 场景1SwapUnwind 全平仓 —— 持仓归零、TradeStatus=已平仓
// ================================================================
[TestMethod]
public void UW_001_SwapUnwind_全平仓_持仓归零且资金流水正确()
{
var td = SwapDealTestFactory.CreateTrade();
var service = new TestableSwapDealService(td);
var unwindData = SwapDealTestFactory.CreateUnwindData(
swapRealizedPnL: 5000m, swapMarginAmount: 0m,
closeMethod: (int)CloseMethodEnum., closePercent: 1m,
closeQty: 10000m, closeNotionalValue: 1000000m, positionQty: 10000m);
service.SwapUnwind(unwindData);
Assert.AreEqual(1, service.ClientCashCalls.Count, "全平无预付金时应1条资金流水");
Assert.AreEqual(-5000.0, service.ClientCashCalls[0].amount, 0.001, "资金流水=-SwapRealizedPnL");
Assert.AreEqual(ClientCashInCashOut._平仓费, service.ClientCashCalls[0].action);
Assert.AreEqual("已平仓", td.TradeStatus, "全平仓 TradeStatus=已平仓");
Assert.AreNotEqual(1, td.HasPartialUnWind, "全平仓不应设 HasPartialUnWind");
Assert.AreEqual(0.0, td.StockEqvNotional, 0.001, "StockEqvNotional 扣减后=0");
Assert.AreEqual(0.0, td.TradeAmount, 0.001, "TradeAmount 扣减后=0");
Assert.AreEqual((int)SwapEventTypeEnum., service.SaveSwapDealCalls[0].eventType, "事件类型=平仓(2)");
Console.WriteLine($"UW_001: TradeStatus={td.TradeStatus}, StockEqvNotional={td.StockEqvNotional} ✅");
}
// ================================================================
// 场景2SwapUnwind 部分平仓 —— HasPartialUnWind=1TradeStatus 不变
// ================================================================
[TestMethod]
public void UW_002_SwapUnwind_部分平仓_设HasPartialUnWind且TradeStatus不变()
{
var td = SwapDealTestFactory.CreateTrade();
var service = new TestableSwapDealService(td);
var unwindData = SwapDealTestFactory.CreateUnwindData(
swapRealizedPnL: 3000m, swapMarginAmount: 0m,
closeMethod: (int)CloseMethodEnum., closePercent: 0.5m,
closeQty: 5000m, closeNotionalValue: 500000m, positionQty: 10000m);
service.SwapUnwind(unwindData);
Assert.AreEqual(1, td.HasPartialUnWind, "部分平仓应设 HasPartialUnWind=1");
Assert.AreEqual("确认成交", td.TradeStatus, "部分平仓 TradeStatus 保持不变");
Assert.AreEqual(500000.0, td.StockEqvNotional, 0.001, "StockEqvNotional 扣减后=500000");
Assert.AreEqual(5000.0, td.TradeAmount, 0.001, "TradeAmount 扣减后=5000");
Assert.AreEqual(1, service.ClientCashCalls.Count, "部分平仓应1条资金流水");
Assert.AreEqual(-3000.0, service.ClientCashCalls[0].amount, 0.001, "资金流水=-SwapRealizedPnL");
Console.WriteLine($"UW_002: HasPartialUnWind={td.HasPartialUnWind}, TradeStatus={td.TradeStatus} ✅");
}
// ================================================================
// 场景3SwapUnwind 含预付金 —— 两条资金流水
// ================================================================
[TestMethod]
public void UW_003_SwapUnwind_含预付金_两条资金流水()
{
var td = SwapDealTestFactory.CreateTrade();
var service = new TestableSwapDealService(td);
var unwindData = SwapDealTestFactory.CreateUnwindData(
swapRealizedPnL: 5000m, swapMarginAmount: 2000m,
closeMethod: (int)CloseMethodEnum., closePercent: 1m,
closeQty: 10000m, closeNotionalValue: 1000000m, positionQty: 10000m);
service.SwapUnwind(unwindData);
Assert.AreEqual(2, service.ClientCashCalls.Count, "含预付金时应2条资金流水");
Assert.AreEqual(-5000.0, service.ClientCashCalls[0].amount, 0.001, "第1条=平仓费");
Assert.AreEqual(ClientCashInCashOut._平仓费, service.ClientCashCalls[0].action);
Assert.AreEqual(2000.0, service.ClientCashCalls[1].amount, 0.001, "第2条=应付预付金");
Assert.AreEqual(ClientCashInCashOut._应付预付金, service.ClientCashCalls[1].action);
Console.WriteLine($"UW_003: 平仓费={service.ClientCashCalls[0].amount}, 应付预付金={service.ClientCashCalls[1].amount} ✅");
}
// ================================================================
// 场景4DealFloatPosition 含费价重算(后端唯二真做计算的地方)
// ================================================================
/// <summary>
/// 平仓事件重算三字段(SwapDealService DealFloatPosition):
/// TradingAmountFeeAvg(ExitDirtyFeePrice) = TradingAmountAvg(ExitDirtyPrice) + Fee/CloseQty × shortRatio
/// TradingAmountNetFeeAvg(ExitCleanFeePrice) = TradingAmountNetAvg(ExitCleanPrice) + Fee/CloseQty × shortRatio
/// TradingAmount = TradingAmountAvg × CloseQty
/// 手算:ExitDirtyPrice=1.02, Fee=50, CloseQty=1000, Long(shortRatio=-1)
/// ExitDirtyFeePrice = 1.02 + 50/1000×(-1) = 0.97
/// ExitCleanFeePrice = 1.00 + 50/1000×(-1) = 0.95
/// TradingAmount = 1.02 × 1000 = 1020
/// </summary>
[TestMethod]
public void UW_004_DealFloatPosition_含费价重算正确()
{
var td = SwapDealTestFactory.CreateTrade();
var service = new TestableSwapDealService(td);
var closeEvent = new swap_flow_event
{
EventType = (int)SwapEventTypeEnum.,
PositionType = (int)PositionTypeFlag.Long,
TradingAmountAvg = 1.02m, // ExitDirtyPrice
TradingAmountNetAvg = 1.00m, // ExitCleanPrice
TradingFeePending = 50m,
};
var unwindData = SwapDealTestFactory.CreateUnwindData(swapRealizedPnL: 0m, closeQty: 1000m);
unwindData.FlowEvents.Add(closeEvent);
service.SwapUnwind(unwindData);
Assert.AreEqual(0.97m, closeEvent.TradingAmountFeeAvg, 0.0001m,
$"TradingAmountFeeAvg(ExitDirtyFeePrice)=ExitDirtyPrice+Fee/Qty×(-1)=0.97");
Assert.AreEqual(0.95m, closeEvent.TradingAmountNetFeeAvg ?? 0m, 0.0001m,
$"TradingAmountNetFeeAvg(ExitCleanFeePrice)=ExitCleanPrice+Fee/Qty×(-1)=0.95");
Assert.AreEqual(1020m, closeEvent.TradingAmount, 0.0001m,
$"TradingAmount=ExitDirtyPrice×CloseQty=1020");
Console.WriteLine($"UW_004: ExitDirtyFeePrice={closeEvent.TradingAmountFeeAvg}, TradingAmount={closeEvent.TradingAmount} ✅");
}
// ================================================================
// 场景5ApproveSwapTrade 审核通过全平仓 —— 反序列化事件并记账
// ================================================================
[TestMethod]
public void UW_005_ApproveSwapTrade_全平仓审核_反序列化事件并记账()
{
var td = SwapDealTestFactory.CreateTrade();
td.ExerciseDate = new DateTime(2026, 12, 31);
var unwindData = SwapDealTestFactory.CreateUnwindData(swapRealizedPnL: 8000m,
closeMethod: (int)CloseMethodEnum., closePercent: 1m,
closeQty: 10000m, closeNotionalValue: 1000000m);
var swapEvent = new swap_event
{
id = 1, SwapTradeId = SwapDealTestFactory.SwapTradeId,
EventType = (int)SwapEventTypeEnum., Invalid = false,
EventData = JsonConvert.SerializeObject(unwindData)
};
var flowEvents = new Dictionary<long, List<swap_flow_event>>
{
[1] = new List<swap_flow_event> { new swap_flow_event { id = 1, EventId = 1, PositionId = 1 } }
};
var service = new TestableSwapDealService(td,
swapEvents: new Dictionary<int, swap_event> { [(int)SwapEventTypeEnum.] = swapEvent },
flowEventsByEventId: flowEvents);
service.ApproveSwapTrade(td, (int)SwapEventTypeEnum.);
Assert.AreEqual(1, service.ClientCashCalls.Count, "全平仓无预付金时应1条资金流水");
Assert.AreEqual(-8000.0, service.ClientCashCalls[0].amount, 0.001, "资金流水=-反序列化的SwapRealizedPnL");
Assert.AreEqual("已平仓", td.TradeStatus, "审核全平仓 TradeStatus=已平仓");
Console.WriteLine($"UW_005: 反序列化SwapRealizedPnL=8000, 资金流水={service.ClientCashCalls[0].amount}, TradeStatus={td.TradeStatus} ✅");
}
// ================================================================
// 场景6ApplySwapTrade 提交审核 —— 前置校验与保存事件
// ================================================================
[TestMethod]
public void UW_006_ApplySwapTrade_提交审核_前置校验与保存事件()
{
var td = SwapDealTestFactory.CreateTrade();
var service = new TestableSwapDealService(td);
var unwindData = SwapDealTestFactory.CreateUnwindData(swapRealizedPnL: 0m);
unwindData.SwapCloseAmount = 6000m;
service.ApplySwapTrade(unwindData, (int)SwapEventTypeEnum.);
Assert.AreEqual(1, service.CloseReCheckCallCount, "应调用 CloseReCheckSetTrade 1次");
Assert.AreEqual(1, service.SaveSwapDealCalls.Count, "应调用 SaveSwapDeal 1次");
Assert.AreEqual((int)SwapEventTypeEnum., service.SaveSwapDealCalls[0].eventType, "事件类型=平仓");
Assert.AreEqual(6000m, service.SaveSwapDealCalls[0].data.SwapRealizedPnL, 0.001m,
"SwapRealizedPnL 应=SwapCloseAmount(6000)");
Console.WriteLine($"UW_006: CloseReCheck={service.CloseReCheckCallCount}次, SwapRealizedPnL={service.SaveSwapDealCalls[0].data.SwapRealizedPnL} ✅");
}
}
}