fix(swap): 解决互换交易全额平仓计算和判定问题
- 在前端计算函数中添加全额平仓时返回剩余数量的逻辑 - 添加针对 trade2308 问题的单元测试验证全额平仓场景 - 实现后端全额平仓请求标准化处理方法 NormalizeFullCloseRequest - 添加重新计算标准化平仓金额的方法 RecalculateNormalizedUnwindAmounts - 实现平仓后是否全额关闭的判定逻辑 IsFullCloseAfterDeduction - 在平仓流程中集成全额平仓标准化处理和金额重新计算 - 更新平仓后交易状态和剩余金额数量的处理逻辑 - 完善审批流程中的平仓数据标准化和流动事件处理 - 优化持仓扣减时全额平仓的处理逻辑 - 添加多个单元测试验证各种全额平仓场景的正确性
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user