fix(swap): 解决互换交易全额平仓计算和判定问题

- 在前端计算函数中添加全额平仓时返回剩余数量的逻辑
- 添加针对 trade2308 问题的单元测试验证全额平仓场景
- 实现后端全额平仓请求标准化处理方法 NormalizeFullCloseRequest
- 添加重新计算标准化平仓金额的方法 RecalculateNormalizedUnwindAmounts
- 实现平仓后是否全额关闭的判定逻辑 IsFullCloseAfterDeduction
- 在平仓流程中集成全额平仓标准化处理和金额重新计算
- 更新平仓后交易状态和剩余金额数量的处理逻辑
- 完善审批流程中的平仓数据标准化和流动事件处理
- 优化持仓扣减时全额平仓的处理逻辑
- 添加多个单元测试验证各种全额平仓场景的正确性
This commit is contained in:
张名锐
2026-08-06 13:24:41 +08:00
parent c7900adca1
commit 6bbe9b1c13
4 changed files with 363 additions and 15 deletions
@@ -197,6 +197,8 @@ namespace YLErp.Modules.SwapModule
public void UW_007_SwapUnwind_占期初A转占剩余B_全平判定正确()
{
var td = SwapDealTestFactory.CreateTrade();
td.StockEqvNotional = 600000;
td.TradeAmount = 600000;
var service = new TestableSwapDealService(td);
var unwindData = SwapDealTestFactory.CreateUnwindData(
swapRealizedPnL: 0m, closeMethod: (int)CloseMethodEnum., closePercent: 0.6m,
@@ -259,6 +261,33 @@ namespace YLErp.Modules.SwapModule
Assert.AreEqual(500000.01, td.StockEqvNotional, 0.000001, "trade 剩余名义本金应在扣减后舍入两位小数");
}
[TestMethod]
public void UW_013_SwapUnwind_合法零点零一剩余不应判定全平()
{
var td = SwapDealTestFactory.CreateTrade();
td.StockEqvNotional = 1000000.01;
td.TradeAmount = 10000.01;
var service = new TestableSwapDealService(td);
var unwindData = SwapDealTestFactory.CreateUnwindData(
swapRealizedPnL: 0m,
closeMethod: (int)CloseMethodEnum.,
closePercent: 1000000m / 1000000.01m,
closeQty: 10000m,
closeNotionalValue: 1000000m,
positionQty: 10000.01m);
unwindData.NotionalValue = 1000000.01m;
unwindData.PosiNotionalValue = 1000000.01m;
service.SwapUnwind(unwindData);
Assert.AreEqual("确认成交", td.TradeStatus,
"剩余名义本金和数量均为0.01时仍应保持部分平仓状态");
Assert.AreEqual(1, td.HasPartialUnWind,
"合法的0.01尾差不应被清零");
Assert.AreEqual(0.01, td.StockEqvNotional, 0.000001);
Assert.AreEqual(0.01, td.TradeAmount, 0.000001);
}
[TestMethod]
public void UW_010_SwapUnwind_现金与两位利息事件保持一致()
{
@@ -289,5 +318,183 @@ namespace YLErp.Modules.SwapModule
Assert.AreEqual(10m, unwindData.SwapRealizedPnL);
Assert.AreEqual(-10d, service.ClientCashCalls[0].amount, 0.001d);
}
[TestMethod]
public void UW_011_trade2308_full_close_uses_remaining_values_and_zeroes_tail()
{
var td = SwapDealTestFactory.CreateTrade();
td.StockEqvNotional = 4906156.15;
td.TradeAmount = 5000000;
td.Notional = 5000000;
var service = new TestableSwapDealService(td);
var unwindData = SwapDealTestFactory.CreateUnwindData(
swapRealizedPnL: 0m,
closeMethod: (int)CloseMethodEnum.,
closePercent: 0.5m,
closeQty: 5000000.01m,
closeNotionalValue: 4906156.15m,
positionQty: 5000000m);
unwindData.NotionalValue = 9812312.31m;
unwindData.PosiNotionalValue = 4906156.15m;
service.SwapUnwind(unwindData);
var saved = service.SaveSwapDealCalls[0].data;
Assert.AreEqual((int)CloseMethodEnum., saved.CloseMethod,
"CloseMethod 保留本次部分平仓意图,终态由扣减后的持仓事实决定");
Assert.AreEqual(5000000m, saved.CloseQty);
Assert.AreEqual(4906156.15m, saved.CloseNotionalValue);
Assert.AreEqual(0d, td.StockEqvNotional, 0.000001);
Assert.AreEqual(0d, td.TradeAmount, 0.000001);
Assert.AreEqual(0d, td.Notional, 0.000001);
Assert.AreEqual("已平仓", td.TradeStatus);
}
[TestMethod]
public void UW_014_full_close_quantity_normalization_recalculates_pnl_and_cash()
{
var td = SwapDealTestFactory.CreateTrade();
td.StockEqvNotional = 4906156.15;
td.TradeAmount = 5000000;
td.Notional = 5000000;
var service = new TestableSwapDealService(td);
var unwindData = SwapDealTestFactory.CreateUnwindData(
swapRealizedPnL: 50000000.10m,
closeMethod: (int)CloseMethodEnum.,
closePercent: 0.5m,
closeQty: 5000000.01m,
closeNotionalValue: 4906156.15m,
positionQty: 5000000m);
unwindData.NotionalValue = 9812312.31m;
unwindData.PosiNotionalValue = 4906156.15m;
unwindData.SwapCloseAmount = 50000000.10m;
var floatEvent = new swap_flow_event
{
UnderlyingCode = "UT-FLOAT",
PositionType = (int)PositionTypeFlag.Long,
EventType = (int)SwapEventTypeEnum.,
PayDirection = 1,
PosiGrossPrice = 1m,
TradingAmountAvg = 11m,
MarkClosePnl = 50000000.10m
};
unwindData.FlowEvents.Add(floatEvent);
service.SwapUnwind(unwindData);
Assert.AreEqual(5000000m, unwindData.CloseQty);
Assert.AreEqual(50000000m, floatEvent.MarkClosePnl);
Assert.AreEqual(50000000m, unwindData.SwapRealizedPnL);
Assert.AreEqual(50000000m, unwindData.SwapCloseAmount);
Assert.AreEqual(-50000000d, service.ClientCashCalls.Single().amount, 0.001d);
}
[TestMethod]
public void UW_012_approve_restores_A_to_B_and_normalizes_flow_for_full_close()
{
var td = SwapDealTestFactory.CreateTrade();
td.StockEqvNotional = 4906156.15;
td.TradeAmount = 5000000;
var unwindData = SwapDealTestFactory.CreateUnwindData(
swapRealizedPnL: 0m,
closeMethod: (int)CloseMethodEnum.,
closePercent: 4906156.15m / 9812312.31m,
closeQty: 5000000.01m,
closeNotionalValue: 4906156.15m,
positionQty: 5000000m);
unwindData.NotionalValue = 9812312.31m;
unwindData.PosiNotionalValue = 4906156.15m;
var floatEvent = new swap_flow_event
{
EventId = 1,
UnderlyingCode = "261031.IB",
PositionType = (int)PositionTypeFlag.Long,
Quantity = 5000000.01m,
PositionQty = -0.01m
};
var swapEvent = new swap_event
{
id = 1,
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
},
flowEventsByEventId: new Dictionary<long, List<swap_flow_event>>
{
[1] = new List<swap_flow_event> { floatEvent }
});
service.ApproveSwapTrade(td, (int)SwapEventTypeEnum.);
Assert.AreEqual(1m, swapEvent.unwindData.ClosePercent);
Assert.AreEqual((int)CloseMethodEnum., swapEvent.unwindData.CloseMethod,
"审批不应把部分平仓事件改写为全平意图");
Assert.AreEqual(5000000m, swapEvent.unwindData.CloseQty);
Assert.AreEqual(4906156.15m, swapEvent.unwindData.CloseNotionalValue);
Assert.AreEqual(5000000m, floatEvent.Quantity);
Assert.AreEqual(0m, floatEvent.PositionQty);
Assert.AreEqual("已平仓", td.TradeStatus);
Assert.AreEqual(0d, td.StockEqvNotional, 0.000001);
Assert.AreEqual(0d, td.TradeAmount, 0.000001);
}
[TestMethod]
public void UW_015_approve_full_close_recalculates_normalized_pnl_before_cash()
{
var td = SwapDealTestFactory.CreateTrade();
td.StockEqvNotional = 4906156.15;
td.TradeAmount = 5000000;
var unwindData = SwapDealTestFactory.CreateUnwindData(
swapRealizedPnL: 50000000.10m,
closeMethod: (int)CloseMethodEnum.,
closePercent: 4906156.15m / 9812312.31m,
closeQty: 5000000.01m,
closeNotionalValue: 4906156.15m,
positionQty: 5000000m);
unwindData.NotionalValue = 9812312.31m;
unwindData.PosiNotionalValue = 4906156.15m;
unwindData.SwapCloseAmount = 50000000.10m;
var floatEvent = new swap_flow_event
{
EventId = 1,
UnderlyingCode = "261031.IB",
PositionType = (int)PositionTypeFlag.Long,
PayDirection = 1,
PosiGrossPrice = 1m,
TradingAmountAvg = 11m,
MarkClosePnl = 50000000.10m,
Quantity = 5000000.01m,
PositionQty = -0.01m
};
var swapEvent = new swap_event
{
id = 1,
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
},
flowEventsByEventId: new Dictionary<long, List<swap_flow_event>>
{
[1] = new List<swap_flow_event> { floatEvent }
});
service.ApproveSwapTrade(td, (int)SwapEventTypeEnum.);
Assert.AreEqual(5000000m, swapEvent.unwindData.CloseQty);
Assert.AreEqual(50000000m, swapEvent.unwindData.SwapRealizedPnL);
Assert.AreEqual(-50000000d, service.ClientCashCalls.Single().amount, 0.001d);
}
}
}