(se.EventData);
+ Console.WriteLine($" EventData.ClosePercent: {ud["ClosePercent"]}");
+ Console.WriteLine($" EventData.CloseNotionalValue: {ud["CloseNotionalValue"]}");
+ Console.WriteLine($" EventData.CloseQty: {ud["CloseQty"]}");
+ Console.WriteLine($" EventData.NotionalValue: {ud["NotionalValue"]}");
+ Console.WriteLine($" EventData.PosiNotionalValue: {ud["PosiNotionalValue"]}");
+ Console.WriteLine($" EventData.PositionQty: {ud["PositionQty"]}");
+ Console.WriteLine($" EventData.CloseMethod: {ud["CloseMethod"]}");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($" EventData parse error: {ex.Message}");
+ }
+ }
+ Console.WriteLine();
+ }
+
+ // 查询 swap_flow_event 记录
+ Console.WriteLine($"===== swap_flow_event 记录 =====");
+ var flowEvents = db.swap_flow_event
+ .Where(f => f.SwapTradeId == td.id)
+ .OrderBy(f => f.EventDate).ThenBy(f => f.id)
+ .ToList();
+
+ foreach (var fe in flowEvents)
+ {
+ Console.WriteLine($" [id={fe.id}] EventDate={fe.EventDate:yyyy-MM-dd} EventType={fe.EventType}");
+ Console.WriteLine($" PositionId: {fe.PositionId}");
+ Console.WriteLine($" Quantity: {fe.Quantity}");
+ Console.WriteLine($" PositionQty: {fe.PositionQty}");
+ Console.WriteLine();
+ }
+ }
+
+ #endregion
}
}
diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs
index bff18695..eb960cc4 100644
--- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs
+++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs
@@ -1838,6 +1838,11 @@ namespace YLErp.Modules.SwapModule
ValidateIncomeValueDate(unwindData, td);
}
unwindData.SwapRealizedPnL = unwindData.SwapCloseAmount;
+ // 前端按"占期初(original)"语义传 ClosePercent(A);后端全链路按"占剩余(remaining)"语义(B)消费。
+ // 入口统一转换为 B,落库展示用的 A 由 SaveSwapDealInternal 还原。
+ // 与 SwapUnwind(L1270) 保持一致——缺少此转换会导致 SaveSwapDealInternal 的 B→A 还原出错
+ // (例如第二次部分平仓 50%(A) → 错误还原为 0.325 而非 0.50)。
+ unwindData.ClosePercent = ToRemainingClosePercent(unwindData.ClosePercent, unwindData.NotionalValue, unwindData.PosiNotionalValue);
string action = eventType == (int)SwapEventTypeEnum.互换 ? ClientCashInCashOut.系统操作_互换 : ClientCashInCashOut.系统操作_平仓费;
ExecuteInTransaction(() =>
{
diff --git a/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml b/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml
index 9eb4bd3b..1011c3c8 100644
--- a/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml
+++ b/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml
@@ -22,6 +22,7 @@
+
}
diff --git a/YLErpWeb/fe-tests/swapCalc.test.js b/YLErpWeb/fe-tests/swapCalc.test.js
index 25ff3deb..e8c1c6f6 100644
--- a/YLErpWeb/fe-tests/swapCalc.test.js
+++ b/YLErpWeb/fe-tests/swapCalc.test.js
@@ -159,6 +159,18 @@ describe('多次部分平仓:全部↔部分切换 CloseQty 不跳变(占期
test('除零保护:PositionQty=0 → ClosePercent=0', () => {
expect(SwapCalc.calcOriginalClosePercentByQty(100, 0, oriClosePercent)).toBe(0);
});
+
+ // GLMS-20260701-0006 生产 bug:第二次部分平仓 50%(占期初) 时
+ // 32500000 * (0.5 / 0.65) = 24999999.999999996(JS 浮点精度偏差)
+ // roundHalfAwayFromZero 应正确舍入为 25000000
+ test('GLMS-20260701-0006:32500000×(0.5/0.65) 应=25000000 而非 24999999.999999996', () => {
+ const closePercent = 0.5; // 占期初 50%
+ const oriClosePercent = 0.65; // 剩余/期初 = 32500000/50000000
+ const positionQty = 32500000; // 剩余持仓
+ const closeQty = SwapCalc.calcCloseQtyByOriginalPercent(closePercent, oriClosePercent, positionQty);
+ expectClose(closeQty, 25000000, '应=25000000 不受 JS 浮点偏差影响');
+ expect(closeQty).not.toBe(24999999.999999996);
+ });
});
describe('交叉校验:对齐 C# FrontendCalcCharacterizationTest 金标准', () => {
diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js
index 3faf8edb..c7fd23ff 100644
--- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js
+++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js
@@ -140,10 +140,10 @@ const vue = new Vue({
},
// 按"占期初口径(A)"的 ClosePercent 反算平仓数量:CloseQty = PositionQty × (ClosePercent / oriClosePercent)
// 多次部分平仓后必须这样转换,否则全部↔部分切换时 ClosePercent 没变但 CloseQty 会变(不自洽)
+ // 使用 swapCalc.calcCloseQtyByOriginalPercent 的 roundHalfAwayFromZero 避免 JS 浮点精度偏差
+ // (如 32500000*(0.5/0.65)=24999999.999999996 而非 25000000)
calcCloseQtyByPercent(closePercent) {
- var ori = parseFloat(this.oriClosePercent) || 0;
- var ratio = ori > 0 ? parseFloat(closePercent) / ori : 0;
- return otcformat.trading.notional(parseFloat(this.deal.PositionQty) * ratio);
+ return SwapCalc.calcCloseQtyByOriginalPercent(closePercent, this.oriClosePercent, this.deal.PositionQty);
},
calcTradingFeePending() {
this.floatPosition.TradingFeePending = this.floatPosition.BeforeCloseFee * parseFloat(this.deal.ClosePercent);