/// 校正 eod_commodity_future_price 行的 UnderlyingId,使其与 UnderlyingCode(=FutureContractId) 一致。
/// 背景:网页日终价格列表按 UnderlyingId(int) JOIN underlying_manager,而 EOD 结算(EodPriceProvider.Initialize)
@@ -45,18 +75,14 @@ namespace YLErp.Modules.EodModule
}
var um = db.underlying_manager.FirstOrDefault(u => u.UnderlyingCode == row.UnderlyingCode);
- if (um == null)
- {
- // 标的代码在 underlying_manager 不存在:无法校正,交由既有"标的代码不存在"等校验处理。
- return;
- }
-
- if (row.UnderlyingId != um.id)
+ var resolvedId = um == null ? (int?)null : um.id;
+ var before = row.UnderlyingId;
+ row.UnderlyingId = ResolveUnderlyingIdForCode(row.UnderlyingCode, row.UnderlyingId ?? 0, resolvedId);
+ if (row.UnderlyingId != before)
{
LogFactory.GetLogger("EodPrice").Info(
$"eod_commodity_future_price.UnderlyingId 与 UnderlyingCode 不一致,已自动校正: " +
- $"FutureContractId={row.UnderlyingCode}, 原UnderlyingId={row.UnderlyingId}, 修正为={um.id}");
- row.UnderlyingId = um.id;
+ $"FutureContractId={row.UnderlyingCode}, 原UnderlyingId={before}, 修正为={row.UnderlyingId}");
}
}
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 90dfe9c5..71c4993e 100644
--- a/YLErpWeb/fe-tests/swapCalc.test.js
+++ b/YLErpWeb/fe-tests/swapCalc.test.js
@@ -170,6 +170,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);