fix(swap): 解决互换交易全额平仓计算和判定问题
- 在前端计算函数中添加全额平仓时返回剩余数量的逻辑 - 添加针对 trade2308 问题的单元测试验证全额平仓场景 - 实现后端全额平仓请求标准化处理方法 NormalizeFullCloseRequest - 添加重新计算标准化平仓金额的方法 RecalculateNormalizedUnwindAmounts - 实现平仓后是否全额关闭的判定逻辑 IsFullCloseAfterDeduction - 在平仓流程中集成全额平仓标准化处理和金额重新计算 - 更新平仓后交易状态和剩余金额数量的处理逻辑 - 完善审批流程中的平仓数据标准化和流动事件处理 - 优化持仓扣减时全额平仓的处理逻辑 - 添加多个单元测试验证各种全额平仓场景的正确性
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,65 @@ namespace YLErp.Modules.SwapModule
|
||||
unwindData.CloseNotionalValue = Math.Round(unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
private static bool NormalizeFullCloseRequest(UnwindData unwindData)
|
||||
{
|
||||
if (unwindData.CloseMethod != (int)CloseMethodEnum.全部平仓
|
||||
&& unwindData.ClosePercent < 1
|
||||
&& !(unwindData.PositionQty > 0 && unwindData.CloseQty >= unwindData.PositionQty)
|
||||
&& !(unwindData.PosiNotionalValue > 0 && unwindData.CloseNotionalValue >= unwindData.PosiNotionalValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var closeQty = unwindData.CloseQty;
|
||||
var closeNotionalValue = unwindData.CloseNotionalValue;
|
||||
unwindData.ClosePercent = 1;
|
||||
if (unwindData.PositionQty > 0) unwindData.CloseQty = unwindData.PositionQty;
|
||||
if (unwindData.PosiNotionalValue > 0) unwindData.CloseNotionalValue = unwindData.PosiNotionalValue;
|
||||
return closeQty != unwindData.CloseQty || closeNotionalValue != unwindData.CloseNotionalValue;
|
||||
}
|
||||
|
||||
private static void RecalculateNormalizedUnwindAmounts(UnwindData unwindData)
|
||||
{
|
||||
var floatLeg = unwindData.FlowEvents.FirstOrDefault(x => !string.IsNullOrEmpty(x.UnderlyingCode));
|
||||
if (floatLeg == null || floatLeg.PosiGrossPrice == 0) return;
|
||||
|
||||
var input = new UnwindInput
|
||||
{
|
||||
Multiplier = ConsGlobal.InstrumentType.IsBond(floatLeg.UnderlyingInstrumentType) ? 100 : 1,
|
||||
PosiGrossPrice = floatLeg.PosiGrossPrice,
|
||||
TradingAmountAvg = floatLeg.TradingAmountAvg,
|
||||
CloseQty = unwindData.CloseQty,
|
||||
PositionQty = unwindData.PositionQty,
|
||||
ContractSize = floatLeg.ContractSize,
|
||||
CloseNotionalValue = unwindData.CloseNotionalValue,
|
||||
PayDirection = floatLeg.PayDirection,
|
||||
PositionType = floatLeg.PositionType,
|
||||
TradingFee = floatLeg.TradingFee.ToString(),
|
||||
TradingFeePending = floatLeg.TradingFeePending.ToString(),
|
||||
DividendIn = floatLeg.DividendIn.ToString()
|
||||
};
|
||||
foreach (var leg in unwindData.FlowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)))
|
||||
{
|
||||
var target = leg.InterestMode == (int)InterestModeEnum.初始预付金
|
||||
|| leg.InterestMode == (int)InterestModeEnum.追加预付金
|
||||
? input.MarginLegs
|
||||
: input.InterestLegs;
|
||||
target.Add(new LegInput { InterestClosePnL = leg.InterestClosePnL });
|
||||
}
|
||||
|
||||
var result = FrontendCalcReference.CalcUnwind(input);
|
||||
floatLeg.MarkClosePnl = result.MarkClosePnl;
|
||||
unwindData.SwapCloseAmount = result.SwapCloseAmount;
|
||||
unwindData.SwapRealizedPnL = result.SwapRealizedPnL;
|
||||
unwindData.SwapMarginRebatePnl = result.SwapMarginRebatePnl;
|
||||
}
|
||||
|
||||
private static bool IsFullCloseAfterDeduction(UnwindData unwindData, double remainingNotional, double remainingQuantity)
|
||||
{
|
||||
return unwindData.ClosePercent == 1 || (remainingNotional == 0 && remainingQuantity == 0);
|
||||
}
|
||||
|
||||
// 待实现利息会进入 decimal(30,12) 日终快照
|
||||
private const int InterestCalculationPrecision = 12;
|
||||
|
||||
@@ -1335,10 +1394,14 @@ namespace YLErp.Modules.SwapModule
|
||||
NormalizeNotionalValues(unwindData);
|
||||
NormalizeManualSettlementAmounts(unwindData, (int)SwapEventTypeEnum.平仓, "系统操作_平仓");
|
||||
//CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制
|
||||
ValidateFrontendPnL(unwindData, isIncome: false); // 只读校验告警,不阻断交易
|
||||
// 前端按"占期初(original)"语义传 ClosePercent(A);后端全链路按"占剩余(remaining)"语义(B)消费。
|
||||
// 入口统一转换为 B,落库展示用的 A 由 SaveSwapDealInternal 还原。
|
||||
unwindData.ClosePercent = ToRemainingClosePercent(unwindData.ClosePercent, unwindData.NotionalValue, unwindData.PosiNotionalValue);
|
||||
if (NormalizeFullCloseRequest(unwindData))
|
||||
{
|
||||
RecalculateNormalizedUnwindAmounts(unwindData);
|
||||
}
|
||||
ValidateFrontendPnL(unwindData, isIncome: false); // 只读校验告警,不阻断交易
|
||||
bool cofirm = false;
|
||||
ExecuteInTransaction(() =>
|
||||
{
|
||||
@@ -1354,18 +1417,24 @@ namespace YLErp.Modules.SwapModule
|
||||
DealFloatPosition(unwindData);
|
||||
var flowList = new List<swap_flow_event>(unwindData.FlowEvents);
|
||||
var eventId = SaveSwapDeal(unwindData, (int)SwapEventTypeEnum.平仓, clientCashId, "系统操作_平仓");
|
||||
if (unwindData.CloseMethod == (int)CloseMethodEnum.全部平仓 || unwindData.ClosePercent == 1)
|
||||
var remainingStockEqvNotional = Math.Round(td.StockEqvNotional - Convert.ToDouble(unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
var remainingTradeAmount = td.TradeAmount - Convert.ToDouble(unwindData.CloseQty);
|
||||
var isFullClose = IsFullCloseAfterDeduction(unwindData, remainingStockEqvNotional, remainingTradeAmount);
|
||||
if (isFullClose)
|
||||
{
|
||||
td.TradeStatus = "已平仓";
|
||||
td.StockEqvNotional = 0;
|
||||
td.TradeAmount = 0;
|
||||
CallSaveSwapTradeClientCash(td, unwindData.ValueDate);
|
||||
}
|
||||
else
|
||||
{
|
||||
td.HasPartialUnWind = 1;
|
||||
td.StockEqvNotional = remainingStockEqvNotional;
|
||||
td.TradeAmount = remainingTradeAmount;
|
||||
}
|
||||
td.Notional = td.TradeAmount;
|
||||
td.UnWindDate = unwindData.UnwindDate;
|
||||
td.StockEqvNotional = Math.Round(td.StockEqvNotional - Convert.ToDouble(unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
td.TradeAmount -= Convert.ToDouble(unwindData.CloseQty);
|
||||
SaveAllChanges();
|
||||
cofirm = true;
|
||||
});
|
||||
@@ -1860,12 +1929,36 @@ namespace YLErp.Modules.SwapModule
|
||||
throw new Exception("该笔交易状态为平仓待复核,未找到相关记录,请检查该笔交易是否有效");
|
||||
}
|
||||
swapEvent.unwindData = JsonConvert.DeserializeObject<UnwindData>(swapEvent.EventData);
|
||||
NormalizeNotionalValues(swapEvent.unwindData);
|
||||
// Stored events keep display ratio A; approval calculations consume remaining ratio B.
|
||||
swapEvent.unwindData.ClosePercent = ToRemainingClosePercent(
|
||||
swapEvent.unwindData.ClosePercent,
|
||||
swapEvent.unwindData.NotionalValue,
|
||||
swapEvent.unwindData.PosiNotionalValue);
|
||||
var flowList = FindFlowEventsByEventId(swapEvent.id);
|
||||
swapEvent.unwindData.FlowEvents = flowList;
|
||||
if (eventType == (int)SwapEventTypeEnum.平仓)
|
||||
{
|
||||
if (NormalizeFullCloseRequest(swapEvent.unwindData))
|
||||
{
|
||||
RecalculateNormalizedUnwindAmounts(swapEvent.unwindData);
|
||||
}
|
||||
}
|
||||
if (eventType == (int)SwapEventTypeEnum.互换)
|
||||
{
|
||||
NormalizeIncomeUnwindDate(swapEvent.unwindData);
|
||||
ValidateIncomeValueDate(swapEvent.unwindData, td);
|
||||
}
|
||||
var flowList = FindFlowEventsByEventId(swapEvent.id);
|
||||
if (eventType == (int)SwapEventTypeEnum.平仓)
|
||||
{
|
||||
foreach (var item in flowList.Where(x => x.PositionType > 0))
|
||||
{
|
||||
item.Quantity = swapEvent.unwindData.CloseQty;
|
||||
item.PositionQty = swapEvent.unwindData.ClosePercent == 1
|
||||
? 0
|
||||
: swapEvent.unwindData.PositionQty - swapEvent.unwindData.CloseQty;
|
||||
}
|
||||
}
|
||||
string action = eventType == (int)SwapEventTypeEnum.互换 ? ClientCashInCashOut.系统操作_互换 : ClientCashInCashOut.系统操作_平仓费;
|
||||
int clientCashId = AddClientCash(td, Convert.ToDouble(-swapEvent.unwindData.SwapRealizedPnL), action, swapEvent.unwindData.ValueDate);
|
||||
if (swapEvent.unwindData.SwapMarginAmount != 0)
|
||||
@@ -1873,7 +1966,28 @@ namespace YLErp.Modules.SwapModule
|
||||
AddClientCash(td, Convert.ToDouble(swapEvent.unwindData.SwapMarginAmount), ClientCashInCashOut.系统操作_应付预付金, swapEvent.unwindData.ValueDate);
|
||||
}
|
||||
swapEvent.ClientCashId = clientCashId;
|
||||
if (swapEvent.unwindData.CloseMethod == (int)CloseMethodEnum.全部平仓)
|
||||
td.UnWindDate = swapEvent.unwindData.UnwindDate;
|
||||
if (eventType != (int)SwapEventTypeEnum.互换)
|
||||
{
|
||||
var remainingStockEqvNotional = Math.Round(td.StockEqvNotional - Convert.ToDouble(swapEvent.unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
var remainingTradeAmount = td.TradeAmount - Convert.ToDouble(swapEvent.unwindData.CloseQty);
|
||||
var isFullClose = IsFullCloseAfterDeduction(swapEvent.unwindData, remainingStockEqvNotional, remainingTradeAmount);
|
||||
if (isFullClose)
|
||||
{
|
||||
td.TradeStatus = "已平仓";
|
||||
td.StockEqvNotional = 0;
|
||||
td.TradeAmount = 0;
|
||||
CallSaveSwapTradeClientCash(td, swapEvent.unwindData.ValueDate);
|
||||
}
|
||||
else
|
||||
{
|
||||
td.TradeStatus = ConsTrade.确认成交;
|
||||
td.HasPartialUnWind = 1;
|
||||
td.StockEqvNotional = remainingStockEqvNotional;
|
||||
td.TradeAmount = remainingTradeAmount;
|
||||
}
|
||||
}
|
||||
else if (swapEvent.unwindData.CloseMethod == (int)CloseMethodEnum.全部平仓)
|
||||
{
|
||||
td.TradeStatus = "已平仓";
|
||||
CallSaveSwapTradeClientCash(td, swapEvent.unwindData.ValueDate);
|
||||
@@ -1883,12 +1997,6 @@ namespace YLErp.Modules.SwapModule
|
||||
td.TradeStatus = ConsTrade.确认成交;
|
||||
td.HasPartialUnWind = 1;
|
||||
}
|
||||
td.UnWindDate = swapEvent.unwindData.UnwindDate;
|
||||
if (eventType != (int)SwapEventTypeEnum.互换)
|
||||
{
|
||||
td.StockEqvNotional = Math.Round(td.StockEqvNotional - Convert.ToDouble(swapEvent.unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
td.TradeAmount -= Convert.ToDouble(swapEvent.unwindData.CloseQty);
|
||||
}
|
||||
|
||||
td.Notional = td.TradeAmount;
|
||||
UpdateInitalPosition(flowList, swapEvent.unwindData, eventType);
|
||||
@@ -1920,6 +2028,13 @@ namespace YLErp.Modules.SwapModule
|
||||
// 与 SwapUnwind(L1270) 保持一致——缺少此转换会导致 SaveSwapDealInternal 的 B→A 还原出错
|
||||
// (例如第二次部分平仓 50%(A) → 错误还原为 0.325 而非 0.50)。
|
||||
unwindData.ClosePercent = ToRemainingClosePercent(unwindData.ClosePercent, unwindData.NotionalValue, unwindData.PosiNotionalValue);
|
||||
if (eventType == (int)SwapEventTypeEnum.平仓)
|
||||
{
|
||||
if (NormalizeFullCloseRequest(unwindData))
|
||||
{
|
||||
RecalculateNormalizedUnwindAmounts(unwindData);
|
||||
}
|
||||
}
|
||||
string action = eventType == (int)SwapEventTypeEnum.互换 ? ClientCashInCashOut.系统操作_互换 : ClientCashInCashOut.系统操作_平仓费;
|
||||
ExecuteInTransaction(() =>
|
||||
{
|
||||
@@ -2053,8 +2168,17 @@ namespace YLErp.Modules.SwapModule
|
||||
else
|
||||
{
|
||||
// 平仓时才扣减持仓
|
||||
position.PosiQuantity -= unwindData.CloseQty;
|
||||
position.PosiNotionalValue = Math.Round(position.PosiNotionalValue - unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
var remainingPositionQty = position.PosiQuantity - unwindData.CloseQty;
|
||||
var remainingPositionNotional = Math.Round(
|
||||
position.PosiNotionalValue - unwindData.CloseNotionalValue,
|
||||
ConsGlobal.MoneyRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
position.PosiQuantity = unwindData.ClosePercent == 1
|
||||
? 0
|
||||
: remainingPositionQty;
|
||||
position.PosiNotionalValue = unwindData.ClosePercent == 1
|
||||
? 0
|
||||
: remainingPositionNotional;
|
||||
position.PosiTradingFee -= position.PosiTradingFee * unwindData.ClosePercent;
|
||||
position.PosiTradingFeePending -= position.PosiTradingFeePending * unwindData.ClosePercent;
|
||||
}
|
||||
@@ -2068,10 +2192,13 @@ namespace YLErp.Modules.SwapModule
|
||||
position.InterestFeePending += interest.InterestFee;
|
||||
if ((interest.InterestMode == (int)InterestModeEnum.追加预付金 || interest.InterestMode == (int)InterestModeEnum.初始预付金) && eventType == (int)SwapEventTypeEnum.平仓)
|
||||
{
|
||||
position.InterestPrincipalFix = Math.Round(
|
||||
var remainingInterestPrincipal = Math.Round(
|
||||
position.InterestPrincipalFix - interest.InterestPrincipal,
|
||||
ConsGlobal.MoneyRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
position.InterestPrincipalFix = unwindData.ClosePercent == 1
|
||||
? 0
|
||||
: remainingInterestPrincipal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +182,19 @@ describe('多次部分平仓:全部↔部分切换 CloseQty 不跳变(占期
|
||||
expectClose(closeQty, 25000000, '应=25000000 不受 JS 浮点偏差影响');
|
||||
expect(closeQty).not.toBe(24999999.999999996);
|
||||
});
|
||||
|
||||
test('trade2308: full close uses the remaining quantity after fixed6 percent formatting', () => {
|
||||
const notionalValue = 9812312.31;
|
||||
const posiNotionalValue = 4906156.15;
|
||||
const oriClosePercent = posiNotionalValue / notionalValue;
|
||||
const closePercent = 0.5;
|
||||
const positionQty = 5000000;
|
||||
|
||||
const closeQty = SwapCalc.calcCloseQtyByOriginalPercent(
|
||||
closePercent, oriClosePercent, positionQty);
|
||||
|
||||
expect(closeQty).toBe(positionQty);
|
||||
});
|
||||
});
|
||||
|
||||
describe('交叉校验:对齐 C# FrontendCalcCharacterizationTest 金标准', () => {
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
function calcCloseQtyByOriginalPercent(closePercent, oriClosePercent, positionQty) {
|
||||
var ori = Number(oriClosePercent);
|
||||
if (ori === 0) return 0;
|
||||
if (Number(closePercent) >= ori) return Number(positionQty);
|
||||
return roundHalfAwayFromZero(Number(positionQty) * (Number(closePercent) / ori), 2);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user