fix(swap): 统一处理【名义本金】数值精度舍入问题
- 在多个位置添加 Math.Round 函数确保金额计算精度 - 新增 NormalizeNotionalValues 方法统一处理 UnwindData 中的名义本金舍入 - 修复交易平仓时 StockEqvNotional 扣减计算的精度问题 - 解决持仓数据 PosiNotionalValue 的精度舍入处理 - 修复前端页面显示格式化问题 - 添加单元测试验证名义本金舍入逻辑正确性
This commit is contained in:
@@ -237,5 +237,26 @@ namespace YLErp.Modules.SwapModule
|
||||
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 剩余名义本金应在扣减后舍入两位小数");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,17 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 原 private 改 protected virtual,使测试 stub 可整体 override,规避内部 new SwapEventService 连库。</summary>
|
||||
protected virtual long SaveSwapDeal(UnwindData unwindData, int eventType, int clientCashId, string eventResason = "", bool approve = false)
|
||||
{
|
||||
NormalizeNotionalValues(unwindData);
|
||||
return SaveSwapDealInternal(unwindData, eventType, clientCashId, eventResason, approve);
|
||||
}
|
||||
|
||||
private static void NormalizeNotionalValues(UnwindData unwindData)
|
||||
{
|
||||
unwindData.NotionalValue = Math.Round(unwindData.NotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
unwindData.PosiNotionalValue = Math.Round(unwindData.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
unwindData.CloseNotionalValue = Math.Round(unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
/// <summary>保存所有变更(生产: DbContext.SaveChanges;测试: 空操作)</summary>
|
||||
protected virtual void SaveAllChanges()
|
||||
{
|
||||
@@ -1222,6 +1230,7 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
throw new ServiceException("未找到交易信息");
|
||||
}
|
||||
NormalizeNotionalValues(unwindData);
|
||||
//CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制
|
||||
ValidateFrontendPnL(unwindData, isIncome: false); // 只读校验告警,不阻断交易
|
||||
// 前端按"占期初(original)"语义传 ClosePercent(A);后端全链路按"占剩余(remaining)"语义(B)消费。
|
||||
@@ -1252,7 +1261,7 @@ namespace YLErp.Modules.SwapModule
|
||||
td.HasPartialUnWind = 1;
|
||||
}
|
||||
td.UnWindDate = unwindData.UnwindDate;
|
||||
td.StockEqvNotional -= Convert.ToDouble(unwindData.CloseNotionalValue);
|
||||
td.StockEqvNotional = Math.Round(td.StockEqvNotional - Convert.ToDouble(unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
td.TradeAmount -= Convert.ToDouble(unwindData.CloseQty);
|
||||
SaveAllChanges();
|
||||
cofirm = true;
|
||||
@@ -1568,7 +1577,7 @@ namespace YLErp.Modules.SwapModule
|
||||
td.HasPartialUnWind = 1;
|
||||
}
|
||||
td.UnWindDate = unwindData.UnwindDate;
|
||||
td.StockEqvNotional -= Convert.ToDouble(unwindData.CloseNotionalValue);
|
||||
td.StockEqvNotional = Math.Round(td.StockEqvNotional - Convert.ToDouble(unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
td.TradeAmount -= Convert.ToDouble(unwindData.CloseQty);
|
||||
td.Notional = td.TradeAmount;
|
||||
td.OptDate = DateTime.Now;
|
||||
@@ -1767,7 +1776,7 @@ namespace YLErp.Modules.SwapModule
|
||||
td.UnWindDate = swapEvent.unwindData.UnwindDate;
|
||||
if (eventType != (int)SwapEventTypeEnum.互换)
|
||||
{
|
||||
td.StockEqvNotional -= Convert.ToDouble(swapEvent.unwindData.CloseNotionalValue);
|
||||
td.StockEqvNotional = Math.Round(td.StockEqvNotional - Convert.ToDouble(swapEvent.unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
td.TradeAmount -= Convert.ToDouble(swapEvent.unwindData.CloseQty);
|
||||
}
|
||||
|
||||
@@ -1933,7 +1942,7 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
// 平仓时才扣减持仓
|
||||
position.PosiQuantity -= unwindData.CloseQty;
|
||||
position.PosiNotionalValue -= unwindData.CloseNotionalValue;
|
||||
position.PosiNotionalValue = Math.Round(position.PosiNotionalValue - unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
position.PosiTradingFee -= position.PosiTradingFee * unwindData.ClosePercent;
|
||||
position.PosiTradingFeePending -= position.PosiTradingFeePending * unwindData.ClosePercent;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <summary>持久化 eod 持仓记录(生产: DbContext.Add;测试: 收集到列表)</summary>
|
||||
protected virtual void PersistEodSwapPosition(eod_swap_position position)
|
||||
{
|
||||
position.PosiNotionalValue = Math.Round(position.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
if (position.id == 0)
|
||||
{
|
||||
DbContext.eod_swap_position.Add(position);
|
||||
@@ -1949,9 +1950,9 @@ namespace YLErp.Modules.SwapModule
|
||||
var positions = eodSwapPositions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//持仓腿
|
||||
// 框架合约的方向约定:多头为正、空头为负;总名义本金取交易原始规模,
|
||||
// 不能直接用多空腿相加,否则会把对冲方向误当成合约规模变化。
|
||||
eod_Swap.NotionalValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue);
|
||||
eod_Swap.NotionalValueShort = -Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue));
|
||||
eod_Swap.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional);
|
||||
eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
eod_Swap.NotionalValue = Math.Round(Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
eod_Swap.SwapTradeId = td.id;
|
||||
eod_Swap.SwapTradeNo = td.TradeNumber;
|
||||
eod_Swap.ClientId = td.ClientId;
|
||||
@@ -2026,9 +2027,9 @@ namespace YLErp.Modules.SwapModule
|
||||
var eodSwapPositions = DbContext.eod_swap_position.Where(x => x.SwapTradeId == td.id && x.ValueDate == settleDate && !x.Invalid).ToList();
|
||||
var interestPositions = eodSwapPositions.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//利息腿
|
||||
var positions = eodSwapPositions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//持仓腿
|
||||
eod_Swap.NotionalValue = Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional);
|
||||
eod_Swap.NotionalValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue);
|
||||
eod_Swap.NotionalValueShort = -Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue));
|
||||
eod_Swap.NotionalValue = Math.Round(Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue);
|
||||
eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue);
|
||||
eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum);
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace YLErp.Modules.SwapModule
|
||||
position.PosiGrossPrice = eodPayPosition.PosiGrossPrice;
|
||||
position.PosiNetFeePrice = eodPayPosition.PosiNetFeePrice;
|
||||
position.PosiNetNoFeePrice = eodPayPosition.PosiNetNoFeePrice;
|
||||
position.PosiNotionalValue = eodPayPosition.PosiNotionalValue;
|
||||
position.PosiNotionalValue = Math.Round(eodPayPosition.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
position.PosiQuantity = eodPayPosition.PosiQuantity;
|
||||
position.PosiStartDate = eodPayPosition.PosiStartDate;
|
||||
position.OptTime = DateTime.Now;
|
||||
@@ -85,7 +85,7 @@ namespace YLErp.Modules.SwapModule
|
||||
position.PosiGrossPrice = eodPayPosition.PosiGrossPrice;
|
||||
position.PosiNetFeePrice = eodPayPosition.PosiNetFeePrice;
|
||||
position.PosiNetNoFeePrice = eodPayPosition.PosiNetNoFeePrice;
|
||||
position.PosiNotionalValue = eodPayPosition.PosiNotionalValue;
|
||||
position.PosiNotionalValue = Math.Round(eodPayPosition.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
position.PosiTradingFeePending = eodPayPosition.PosiFeePending;
|
||||
position.PosiQuantity = eodPayPosition.PosiQuantity;
|
||||
position.PosiDirection = eodPayPosition.PosiDirection;
|
||||
|
||||
@@ -332,7 +332,7 @@ namespace YLErp.Modules.SwapModule
|
||||
AssetBookName = asset.Name,
|
||||
Notional = Convert.ToDouble(flowMerge.TradingQtyAbs),
|
||||
TradeAmount = Convert.ToDouble(flowMerge.TradingQtyAbs),
|
||||
StockEqvNotional = Convert.ToDouble(flowMerge.TradingAmount),
|
||||
StockEqvNotional = Math.Round(Convert.ToDouble(flowMerge.TradingAmount), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero),
|
||||
IsAutoGenerate = true,
|
||||
};
|
||||
if (flowMerge.SettleDate.HasValue)
|
||||
@@ -374,7 +374,7 @@ namespace YLErp.Modules.SwapModule
|
||||
PosiNetFeePrice = flowMerge.TradingAmountNetFeeAvg ?? 0,
|
||||
PosiNetNoFeePrice = flowMerge.TradingAmountNetAvg ?? 0,
|
||||
PosiQuantity = flowMerge.TradingQtyAbs,
|
||||
PosiNotionalValue = flowMerge.TradingAmount,
|
||||
PosiNotionalValue = Math.Round(flowMerge.TradingAmount, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero),
|
||||
PosiTradingFeePending = flowMerge.TradingFeePending,
|
||||
PosiTradingFee = 0,
|
||||
PosiTradingFeeUnit = 0,
|
||||
@@ -762,6 +762,7 @@ namespace YLErp.Modules.SwapModule
|
||||
req.SpotPrice = Convert.ToDouble(swapPosition.PosiNetPrice);
|
||||
}
|
||||
req.Strike = null;
|
||||
req.StockEqvNotional = Math.Round(req.StockEqvNotional, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
req.OriginalStockEqvNotional = req.StockEqvNotional;
|
||||
req.StockEqvNotionalReal = req.StockEqvNotional;
|
||||
|
||||
@@ -1355,7 +1356,7 @@ namespace YLErp.Modules.SwapModule
|
||||
position.PosiNetNoFeePrice = swap.PosiNetNoFeePrice;
|
||||
position.PosiNetFeePrice = swap.PosiQuantity == 0 ? 0 : (swap.PosiNetNoFeePrice + (position.PosiTradingFeePending / swap.PosiQuantity) * ratio);
|
||||
position.PosiNetFeePrice = Math.Round(position.PosiNetFeePrice??0, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
|
||||
position.PosiNotionalValue = swap.PosiNotionalValue;
|
||||
position.PosiNotionalValue = Math.Round(swap.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
position.PosiQuantity = swap.PosiQuantity;
|
||||
position.InterestDirection = swap.InterestDirection;
|
||||
position.InterestMode = swap.InterestMode;
|
||||
@@ -1479,7 +1480,7 @@ namespace YLErp.Modules.SwapModule
|
||||
td.ProcessStatus = null;
|
||||
if (backToBegin)
|
||||
{
|
||||
td.StockEqvNotional = td.OriginalStockEqvNotional ?? 0;
|
||||
td.StockEqvNotional = Math.Round(td.OriginalStockEqvNotional ?? 0, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
td.UnWindDate = null;
|
||||
td.HasPartialUnWind = null;
|
||||
SingleTradeBackToBegin(td, swapPositions);
|
||||
@@ -1641,10 +1642,10 @@ namespace YLErp.Modules.SwapModule
|
||||
posi.PosiGrossPrice = eodPosi.PosiGrossPrice;
|
||||
posi.PosiNetFeePrice = eodPosi.PosiNetFeePrice;
|
||||
posi.PosiNetNoFeePrice = eodPosi.PosiNetNoFeePrice;
|
||||
posi.PosiNotionalValue = eodPosi.PosiNotionalValue;
|
||||
posi.PosiNotionalValue = Math.Round(eodPosi.PosiNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
if (posi.PosiDirection > 0)
|
||||
{
|
||||
td.StockEqvNotional = Convert.ToDouble(posi.PosiNotionalValue);
|
||||
td.StockEqvNotional = Math.Round(Convert.ToDouble(posi.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
td.TradeAmount = Convert.ToDouble(posi.PosiQuantity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -669,7 +669,7 @@ function getColModelGridStep4() {
|
||||
label: '名义本金',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: otcformat.trading.StockEqvNotional
|
||||
}
|
||||
, {
|
||||
name: 'position.PosiTradingFee',
|
||||
@@ -1221,4 +1221,4 @@ var vue = new Vue({
|
||||
'vue-underlying': vueUnderlying()
|
||||
}
|
||||
});
|
||||
window.reloadData = getList();
|
||||
window.reloadData = getList();
|
||||
|
||||
@@ -897,7 +897,7 @@
|
||||
<td>@tr.MetaDic["互换_收取方初始预付金"]</td>
|
||||
<td>@tr.MetaDic["互换_收取方交易费用"]</td>
|
||||
<td>@tr.MetaDic["互换_收取方多空方向"]</td>
|
||||
<td>@tr.OriginalStockEqvNotional</td>
|
||||
<td>@tr.TdDetail.OriginalStockEqvNotional.OtcFormat(OtcFormatFlag.StockEqvNotional)</td>
|
||||
<td>@tr.MetaDic["年化天数"]</td>
|
||||
<td>@tr.MetaDic["互换_互换日期"]</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user