Files
zszq-trs/UnitTestProject/Modules/SwapModule/SwapUnwindScenarioTest.cs
T

323 lines
18 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} ✅");
}
[TestMethod]
public void UW_005A_ApproveSwapTrade_互换审核_不返还预付金本金()
{
var td = SwapDealTestFactory.CreateTrade();
td.ExerciseDate = new DateTime(2026, 12, 31);
var unwindData = SwapDealTestFactory.CreateUnwindData(
swapRealizedPnL: 100m, swapMarginAmount: -10000m);
var swapEvent = new swap_event
{
id = 2,
SwapTradeId = SwapDealTestFactory.SwapTradeId,
EventType = (int)SwapEventTypeEnum.互换,
Invalid = false,
EventData = JsonConvert.SerializeObject(unwindData)
};
var service = new TestableSwapDealService(td,
swapEvents: new Dictionary<int, swap_event>
{
[(int)SwapEventTypeEnum.互换] = swapEvent
});
service.ApproveSwapTrade(td, (int)SwapEventTypeEnum.互换);
Assert.AreEqual(1, service.ClientCashCalls.Count, "互换审批只应生成互换结算流水");
Assert.AreEqual(ClientCashInCashOut.系统操作_互换, service.ClientCashCalls[0].action);
Assert.IsFalse(service.ClientCashCalls.Any(x => x.action == ClientCashInCashOut.系统操作_应付预付金),
"互换审批不应生成应付预付金流水");
}
// ================================================================
// 场景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} ✅");
}
// ================================================================
// 场景7:前端传"占期初(A)"语义,后端入口转"占剩余(B)" —— 全平判定
// 原始名义本金 100M / 剩余 60M,前端传 A=0.6(平掉原始 60M = 剩余全部)
// B = A × Notional/Posi = 0.6 × 100/60 = 1.0 → 触发全平
// ================================================================
[TestMethod]
public void UW_007_SwapUnwind_占期初A转占剩余B_全平判定正确()
{
var td = SwapDealTestFactory.CreateTrade();
var service = new TestableSwapDealService(td);
var unwindData = SwapDealTestFactory.CreateUnwindData(
swapRealizedPnL: 0m, closeMethod: (int)CloseMethodEnum.全部平仓, closePercent: 0.6m,
closeQty: 600000m, closeNotionalValue: 600000m, positionQty: 600000m);
unwindData.NotionalValue = 1000000m; // 期初名义本金
unwindData.PosiNotionalValue = 600000m; // 剩余名义本金
service.SwapUnwind(unwindData);
// 桩 SaveSwapDeal 收集的是转换后的 B(落库 A 还原在生产 SaveSwapDealInternal 中,桩跳过)
Assert.AreEqual(1.0m, service.SaveSwapDealCalls[0].data.ClosePercent, 0.0001m,
"入口 A=0.6 应转为 B=1.0(占剩余全平)");
Assert.AreEqual("已平仓", td.TradeStatus, "B==1 触发全平 TradeStatus=已平仓");
Console.WriteLine($"UW_007: A=0.6→B={service.SaveSwapDealCalls[0].data.ClosePercent}, TradeStatus={td.TradeStatus} ✅");
}
// ================================================================
// 场景8:占期初(A)转占剩余(B) —— 部分平仓
// 原始 100M / 剩余 60M,前端传 A=0.3(平掉原始 30M = 剩余的 50%)
// B = A × Notional/Posi = 0.3 × 100/60 = 0.5 → 部分平仓
// ================================================================
[TestMethod]
public void UW_008_SwapUnwind_占期初A转占剩余B_部分平仓正确()
{
var td = SwapDealTestFactory.CreateTrade();
var service = new TestableSwapDealService(td);
var unwindData = SwapDealTestFactory.CreateUnwindData(
swapRealizedPnL: 0m, closeMethod: (int)CloseMethodEnum.部分平仓, closePercent: 0.3m,
closeQty: 300000m, closeNotionalValue: 300000m, positionQty: 600000m);
unwindData.NotionalValue = 1000000m; // 期初名义本金
unwindData.PosiNotionalValue = 600000m; // 剩余名义本金
service.SwapUnwind(unwindData);
Assert.AreEqual(0.5m, service.SaveSwapDealCalls[0].data.ClosePercent, 0.0001m,
"入口 A=0.3 应转为 B=0.5(占剩余 50%");
Assert.AreEqual(1, td.HasPartialUnWind, "B≠1 应为部分平仓,设 HasPartialUnWind=1");
Assert.AreEqual("确认成交", td.TradeStatus, "部分平仓 TradeStatus 保持不变");
Console.WriteLine($"UW_008: A=0.3→B={service.SaveSwapDealCalls[0].data.ClosePercent}, HasPartialUnWind={td.HasPartialUnWind} ✅");
}
[TestMethod]
public void UW_009_SwapUnwind_名义本金写入前舍入两位小数()
{
var td = SwapDealTestFactory.CreateTrade();
td.StockEqvNotional = 1000000.006;
var service = new TestableSwapDealService(td);
var unwindData = SwapDealTestFactory.CreateUnwindData(
swapRealizedPnL: 0m, closeMethod: (int)CloseMethodEnum.部分平仓, closePercent: 0.5m,
closeQty: 5000m, closeNotionalValue: 500000.004m, positionQty: 10000m);
unwindData.NotionalValue = 1000000.006m;
unwindData.PosiNotionalValue = 1000000.006m;
service.SwapUnwind(unwindData);
var savedData = service.SaveSwapDealCalls[0].data;
Assert.AreEqual(1000000.01m, savedData.NotionalValue, "期初名义本金应按两位小数写入事件");
Assert.AreEqual(1000000.01m, savedData.PosiNotionalValue, "剩余名义本金应按两位小数写入事件");
Assert.AreEqual(500000.00m, savedData.CloseNotionalValue, "平仓名义本金应按两位小数写入事件");
Assert.AreEqual(500000.01, td.StockEqvNotional, 0.000001, "trade 剩余名义本金应在扣减后舍入两位小数");
}
[TestMethod]
public void UW_010_SwapUnwind_现金与两位利息事件保持一致()
{
var td = SwapDealTestFactory.CreateTrade();
var service = new TestableSwapDealService(td);
var unwindData = SwapDealTestFactory.CreateUnwindData(
swapRealizedPnL: 10.0049m, closeMethod: (int)CloseMethodEnum.全部平仓,
closePercent: 1m, closeQty: 10000m, closeNotionalValue: 1000000m, positionQty: 10000m);
var floatEvent = new swap_flow_event
{
UnderlyingCode = "UT-FLOAT", PositionType = (int)PositionTypeFlag.Long,
EventType = (int)SwapEventTypeEnum.平仓, PayDirection = 1, MarkClosePnl = 10m
};
var interestEvent = new swap_flow_event
{
PositionType = 0, InterestAmount = 0.0049m, TdInterestAmount = 0.0049m,
InterestClosePnL = 0.0049m, InterestFee = 0.0049m
};
unwindData.FlowEvents.Add(floatEvent);
unwindData.FlowEvents.Add(interestEvent);
service.SwapUnwind(unwindData);
Assert.AreEqual(0m, interestEvent.InterestAmount);
Assert.AreEqual(0m, interestEvent.TdInterestAmount);
Assert.AreEqual(0m, interestEvent.InterestClosePnL);
Assert.AreEqual(0m, interestEvent.InterestFee);
Assert.AreEqual(10m, unwindData.SwapRealizedPnL);
Assert.AreEqual(-10d, service.ClientCashCalls[0].amount, 0.001d);
}
}
}