Files
zszq-trs/UnitTestProject/Modules/SwapModule/SwapUnwindScenarioTest.cs
T
hjhan 4fede8339a 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测试全绿,无回归。
2026-07-03 18:12:52 +08:00

192 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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} ✅");
}
}
}