();
- var posDict = swapPositions.GroupBy(sp => sp.SwapTradeId).ToDictionary(g => g.Key, g => g.First().PosiNetPrice);
+ var posDict = swapPositions.GroupBy(sp => sp.SwapTradeId).ToDictionary(g => g.Key, g => g.First().PosiGrossPrice);
var umProvider = DataCacheProvider.GetUnderlyingDataSource();
// 需求②:平仓/行权/互换交易,审批角色应取 CloseProcess 流程的节点角色,而非 TradeProcess
var closeProcessRoles = db.approvalprocess
@@ -312,14 +312,14 @@ namespace YLErp.BLL
tradeLinq.TradeSinglePrice = option.OpenCommission;
}
- // --- 新增逻辑:针对收益互换类型,用 swap_position.PosiNetPrice 覆盖展示用的期初标的价格 ---
+ // --- 针对收益互换类型,用 swap_position.PosiGrossPrice 覆盖展示用的期初标的价格 ---
try
{
- if (tradeLinq.TradeType == "收益互换" && posDict.TryGetValue(tradeLinq.id, out var netPrice) && netPrice > 0)
+ if (tradeLinq.TradeType == "收益互换" && posDict.TryGetValue(tradeLinq.id, out var grossPrice) && grossPrice > 0)
{
- // 将期初价格覆盖为互换持仓的 PosiNetPrice(仅使用 PosiDirection != 0 的期初持仓)
- logger.Info($"tradeOpeningProcessQuery.DAL override: tradeId={tradeLinq.id} beforeInitialSpot={tradeLinq.InitialSpotPrice} dbPosi={netPrice}");
- tradeLinq.InitialSpotPrice = Convert.ToDouble(netPrice);
+ // 将期初价格覆盖为互换持仓的 PosiGrossPrice(仅使用 PosiDirection != 0 的期初持仓)
+ logger.Info($"tradeOpeningProcessQuery.DAL override: tradeId={tradeLinq.id} beforeInitialSpot={tradeLinq.InitialSpotPrice} dbPosi={grossPrice}");
+ tradeLinq.InitialSpotPrice = Convert.ToDouble(grossPrice);
}
}
catch (Exception ex)
diff --git a/YLErpWeb/App_Data/Config/swappriceprecision.js b/YLErpWeb/App_Data/Config/swappriceprecision.js
index ca3cca8a..70ddad58 100644
--- a/YLErpWeb/App_Data/Config/swappriceprecision.js
+++ b/YLErpWeb/App_Data/Config/swappriceprecision.js
@@ -1,48 +1,56 @@
// 通过 /front/swappriceprecision 加载。可按 UnderlyingInstrumentType 修改;缺项或非法值由页面精度组件回退内置默认规则。
window.main = window.main || {};
window.main.swapPricePrecision = {
- Stock: { integerDigits: 7, precision: 2 },
- StockIndex: { integerDigits: 7, precision: 2 },
- StockIF: { integerDigits: 7, precision: 4 },
- CommodityFutures: { integerDigits: 7, precision: 4 },
- CommoditySpot: { integerDigits: 7, precision: 4 },
- NewOtcStock: { integerDigits: 7, precision: 4 },
- HKStock: { integerDigits: 7, precision: 4 },
- HKStockIndex: { integerDigits: 7, precision: 4 },
- Fund: { integerDigits: 7, precision: 4 },
+ common: {
+ amount: { precision: 2, grouping: true },
+ quantity: { integerDigits: 16, precision: 2, grouping: true },
+ rate: { precision: 4 }
+ },
+ Stock: { integerDigits: 7, precision: 2, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ StockIndex: { integerDigits: 7, precision: 2, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ StockIF: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ CommodityFutures: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ CommoditySpot: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ NewOtcStock: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ HKStock: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ HKStockIndex: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ Fund: { integerDigits: 7, precision: 4, quantityPrecision: 4, quantityIntegerDigits: 12 },
Bond: {
+ quantityPrecision: 0, quantityIntegerDigits: 16,
grossPrice: { integerDigits: 6, precision: 9 },
netPrice: { integerDigits: 6, precision: 9 },
yield: { integerDigits: 2, precision: 4 }
},
TBonds: {
+ quantityPrecision: 0, quantityIntegerDigits: 16,
grossPrice: { integerDigits: 6, precision: 9 },
netPrice: { integerDigits: 6, precision: 9 },
yield: { integerDigits: 2, precision: 4 }
},
CreditBonds: {
+ quantityPrecision: 0, quantityIntegerDigits: 16,
grossPrice: { integerDigits: 6, precision: 9 },
netPrice: { integerDigits: 6, precision: 9 },
yield: { integerDigits: 2, precision: 4 }
},
OtherBonds: {
+ quantityPrecision: 0, quantityIntegerDigits: 16,
grossPrice: { integerDigits: 6, precision: 9 },
netPrice: { integerDigits: 6, precision: 9 },
yield: { integerDigits: 2, precision: 4 }
},
- TBFutures: { integerDigits: 8, precision: 4 },
- OtherFutures: { integerDigits: 8, precision: 4 },
- GoldSpot: { integerDigits: 8, precision: 4 },
- OtherSpot: { integerDigits: 8, precision: 4 },
- AbroadFutures: { integerDigits: 8, precision: 4 },
- AbroadSpot: { integerDigits: 8, precision: 4 },
- AbroadStock: { integerDigits: 8, precision: 2 },
- AbroadStockIndex: { integerDigits: 8, precision: 4 },
- ExRate: { integerDigits: 2, precision: 8 },
- Shibor: { integerDigits: 2, precision: 4 },
- FixingRepoRate: { integerDigits: 2, precision: 4 },
- RateYield: {integerDigits: 6, precision: 8},
- BondIndex: {integerDigits: 6, precision: 4},
-
- // TODO: 利率收益率(6+8)、债券指数(6+4)、黄金期货(6+4)待对应的 UnderlyingInstrumentType 枚举确认后启用。
+ TBFutures: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ OtherFutures: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ GoldFutures: { quantityIntegerDigits: 12 },
+ GoldSpot: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ OtherSpot: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ AbroadFutures: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ AbroadSpot: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ AbroadStock: { integerDigits: 8, precision: 2, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ AbroadStockIndex: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ ExRate: { integerDigits: 2, precision: 8, quantityPrecision: 8, quantityIntegerDigits: 16 },
+ Shibor: { integerDigits: 2, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ FixingRepoRate: { integerDigits: 2, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ RateYield: {integerDigits: 6, precision: 8, quantityPrecision: 2, quantityIntegerDigits: 12},
+ BondIndex: {integerDigits: 6, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12},
};
diff --git a/YLErpWeb/Controllers/BondPaymentController.cs b/YLErpWeb/Controllers/BondPaymentController.cs
index fa4f2ef2..f7368838 100644
--- a/YLErpWeb/Controllers/BondPaymentController.cs
+++ b/YLErpWeb/Controllers/BondPaymentController.cs
@@ -79,6 +79,10 @@ namespace YLErp.Web.Controllers
}
///
/// 获取某债券期间付息,扣除当天已消费部分
+ /// 【已废弃·不再用于平仓/互换预览】分红展示改由后端 SwapDealService.GetPreEodDividendSum
+ /// 读 EOD PosiDividendSum 提供(方案C,单一可信源)。前端 unwindSwapTrade.js / incomeSwapTrade.js
+ /// 的 getDivindIn 不再调用本接口。本接口仅保留供历史调用方,consumedDividend 查询无日期过滤的
+ /// 隐患随废弃自然消解,不再单独修复。
///
///
///
diff --git a/YLErpWeb/Views/SwapTrade2/ExtensionTime.cshtml b/YLErpWeb/Views/SwapTrade2/ExtensionTime.cshtml
index 0c7748b9..3b541234 100644
--- a/YLErpWeb/Views/SwapTrade2/ExtensionTime.cshtml
+++ b/YLErpWeb/Views/SwapTrade2/ExtensionTime.cshtml
@@ -55,7 +55,7 @@
{{dateFormat(extenstion.extenstionData.OldMaturityDate)}}
|
-
+
|
{{extenstion.OptName}} |
{{dateFormat(extenstion.OptTime,'YYYY-MM-DD HH:mm:ss')}} |
diff --git a/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml b/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml
index ea00430b..91d9d26f 100644
--- a/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml
+++ b/YLErpWeb/Views/SwapTrade2/SwapIncome.cshtml
@@ -42,15 +42,15 @@
| 成交名义本金 |
- {{deal.NotionalValue}} |
+ {{formatAmount(deal.NotionalValue)}} |
持仓名义本金 |
- {{deal.PosiNotionalValue}} |
+ {{formatAmount(deal.PosiNotionalValue)}} |
| 成交数量 |
- {{deal.NotionalQty}} |
+ {{formatQuantity(deal.NotionalQty)}} |
持仓数量 |
- {{deal.PositionQty2}} |
+ {{formatQuantity(deal.PositionQty2)}} |
| 起始日期 |
@@ -66,7 +66,7 @@
| 平仓总额 |
- {{deal.SwapCloseAmount}} |
+ {{formatAmount(deal.SwapCloseAmount)}} |
|
|
@@ -98,7 +98,7 @@
|
- {{item.InterestClosePnL}} |
+ {{formatAmount(item.InterestClosePnL)}} |
@@ -126,7 +126,7 @@
|
- {{item.InterestClosePnL}} |
+ {{formatAmount(item.InterestClosePnL)}} |
@@ -177,13 +177,13 @@
- {{floatPosition.Quantity}} |
+ {{formatQuantity(floatPosition.Quantity)}} |
我方{{floatPosition.PayDirection==1?"支付":"收取"}}交易费用
|
|
- {{floatPosition.FloatPnlSum}} |
+ {{formatAmount(floatPosition.FloatPnlSum)}} |
diff --git a/YLErpWeb/Views/SwapTrade2/SwapLongShortSwap.cshtml b/YLErpWeb/Views/SwapTrade2/SwapLongShortSwap.cshtml
index f004920a..4df23cf7 100644
--- a/YLErpWeb/Views/SwapTrade2/SwapLongShortSwap.cshtml
+++ b/YLErpWeb/Views/SwapTrade2/SwapLongShortSwap.cshtml
@@ -19,6 +19,8 @@
+
+
}
@@ -73,7 +75,7 @@
|
- {{item.InterestClosePnL}} |
+ {{formatAmount(item.InterestClosePnL)}} |
@@ -101,7 +103,7 @@
|
- {{item.InterestClosePnL}} |
+ {{formatAmount(item.InterestClosePnL)}} |
diff --git a/YLErpWeb/Views/SwapTrade2/SwapLongShortUnwind.cshtml b/YLErpWeb/Views/SwapTrade2/SwapLongShortUnwind.cshtml
index 5257e478..bdbc1601 100644
--- a/YLErpWeb/Views/SwapTrade2/SwapLongShortUnwind.cshtml
+++ b/YLErpWeb/Views/SwapTrade2/SwapLongShortUnwind.cshtml
@@ -20,6 +20,8 @@
+
+
}
@@ -89,7 +91,7 @@
|
- {{item.InterestClosePnL}} |
+ {{formatAmount(item.InterestClosePnL)}} |
@@ -113,7 +115,7 @@
|
- {{item.InterestClosePnL}} |
+ {{formatAmount(item.InterestClosePnL)}} |
diff --git a/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml b/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml
index 7f9e2ea1..4d7c979a 100644
--- a/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml
+++ b/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml
@@ -33,20 +33,20 @@
@@ -125,7 +125,7 @@
|
- {{item.InterestClosePnL}} |
+ {{formatAmount(item.InterestClosePnL)}} |
@@ -165,7 +165,7 @@
|
- {{item.InterestClosePnL}} |
+ {{formatAmount(item.InterestClosePnL)}} |
@@ -214,7 +214,7 @@
- {{deal.CloseQty}} |
+ {{formatQuantity(deal.CloseQty)}} |
我方{{floatPosition.PayDirection==1?"支付":"收取"}}交易费用
@@ -222,8 +222,8 @@
|
|
- {{floatPosition.DividendIn}} |
- {{floatPosition.FloatPnlSum}} |
+ {{formatAmount(floatPosition.DividendIn)}} |
+ {{formatAmount(floatPosition.FloatPnlSum)}} |
diff --git a/YLErpWeb/Views/SwapTrade2/SwapflowList.cshtml b/YLErpWeb/Views/SwapTrade2/SwapflowList.cshtml
index eb805b1c..863283eb 100644
--- a/YLErpWeb/Views/SwapTrade2/SwapflowList.cshtml
+++ b/YLErpWeb/Views/SwapTrade2/SwapflowList.cshtml
@@ -102,6 +102,8 @@
+
+
@@ -231,15 +233,15 @@
-
+
-
+
-
+
diff --git a/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml b/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml
index eead5b88..6561cbfe 100644
--- a/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml
+++ b/YLErpWeb/Views/SwapTrade2/TradeEdit.cshtml
@@ -493,7 +493,7 @@
- {{item.underlying!=null?item.underlying.QuoteUnitString:''}}
+ {{item.underlying!=null?item.underlying.QuoteUnitString:''}}
|
diff --git a/YLErpWeb/Views/SwapTrade2/TradeView.cshtml b/YLErpWeb/Views/SwapTrade2/TradeView.cshtml
index e82b88bc..24f94de3 100644
--- a/YLErpWeb/Views/SwapTrade2/TradeView.cshtml
+++ b/YLErpWeb/Views/SwapTrade2/TradeView.cshtml
@@ -32,6 +32,7 @@
var sr = trade.trade_extend.ExtendObj.SettlementRules;
bool hideFloatingIncomeDirection = PS.Config.ErpElement.SwapFloatingIncomeReceiveOnlyMode;
string SwapPriceData(decimal? value) => value?.ToString(CultureInfo.InvariantCulture) ?? string.Empty;
+ string SwapCommonData(object value) => value == null ? string.Empty : Convert.ToString(value, CultureInfo.InvariantCulture);
}
@section CSS{
@@ -134,7 +135,7 @@
| 名义本金 |
- @trade.OriginalStockEqvNotional.OtcFormat(OtcFormatFlag.StockEqvNotional) |
+ |
@*
| 初始预付金 |
@@ -277,9 +278,9 @@
@((SwapDirectionEnum)item.InterestDirection) |
@((InterestModeEnum)item.InterestMode) |
@item.HappenDate.OtcFormatDate() |
- @item.InterestPrincipalFix.OtcFormat(OtcFormatFlag.StockEqvNotional) |
+ |
@item.Currency |
- @item.InterestRateDefault.OtcFormatPercent(4) |
+ |
@(item.IsAnnualized ? "是" : "否") |
@@ -342,11 +343,11 @@
|
| @((SwapDirectionEnum)item.InterestDirection) |
@((InterestModeEnum)item.InterestMode) |
- @interestPrice?.OtcFormat(OtcFormatFlag.StockEqvNotional) |
+ |
@(string.IsNullOrEmpty(item.FloatRateUnderlyingCode) ? "无" : item.FloatRateUnderlyingCode)
@sign
- @item.InterestRateDefault.OtcFormatPercent(4)
+
|
@(item.IsAnnualized ? "是" : "否") |
@@ -422,7 +423,7 @@
|
}
- @item.PosiQuantity.OtcFormat(OtcFormatFlag.StockEqvNotional)
+
|
@if (item.PosiFeeType == 0)
@@ -435,7 +436,7 @@
}
|
- @item.PosiTradingFeePending.OtcFormat(OtcFormatFlag.StockEqvNotional)
+
|
}
@@ -507,9 +508,9 @@
@((SwapDirectionEnum)item.InterestDirection) |
@((InterestModeEnum)item.InterestMode) |
@item.HappenDate.OtcFormatDate() |
- @item.InterestPrincipalFix.OtcFormat(OtcFormatFlag.StockEqvNotional) |
+ |
@item.Currency |
- @item.InterestRateDefault.OtcFormatPercent(4) |
+ |
@(item.IsAnnualized ? "是" : "否") |
|
@@ -567,11 +568,11 @@
| @((SwapDirectionEnum)item.InterestDirection) |
@((InterestModeEnum)item.InterestMode) |
- @interestPrice?.OtcFormat(OtcFormatFlag.StockEqvNotional) |
+ |
@(string.IsNullOrEmpty(item.FloatRateUnderlyingCode) ? "无" : item.FloatRateUnderlyingCode)
@sign
- @item.InterestRateDefault.OtcFormatPercent(4)
+
|
@(item.IsAnnualized ? "是" : "否") |
@(item.InterestType == 0 ? "单利" : "复利") |
@@ -645,10 +646,10 @@
}
- @item.PosiQuantity.OtcFormat(OtcFormatFlag.StockEqvNotional)
+
|
- @item.PosiNotionalValue.OtcFormat(OtcFormatFlag.StockEqvNotional) |
- @item.PosiTradingFeePending.OtcFormat(OtcFormatFlag.StockEqvNotional) |
+ |
+ |
@item.PosiStartDate.OtcFormatDate() |
@item.PosiMatuirityDate.OtcFormatDate() |
@@ -709,14 +710,14 @@
@if (tc.CloseType == 1)
{
平仓数量 |
- @((PS.Config.IsUseDisplayNotional ? tc.CloseQty * (trade.CountRatio ?? 1) : tc.CloseQty).OtcFormatNotional()) |
+ |
}
else
{
平仓比例 |
@(tc.ClosePercent.OtcFormatPercent(4)) |
平仓名义本金 |
- @(tc.CloseNotionalValue.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
+ |
}
@@ -734,9 +735,9 @@
| 实现盈亏 |
- @(tc.SwapRealizedPnL.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
+ |
平仓总额 |
- @(tc.SwapCloseAmount.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
+ |
@@ -762,9 +763,9 @@
| @((SwapDirectionEnum)item.InterestDirection) |
@(item.InterestModeStr) |
- @(item.InterestPrincipal.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
- @(item.InterestAmount.OtcFormatMoney()) |
- @(item.InterestClosePnL.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
+ |
+ |
+ |
}
@@ -798,9 +799,9 @@
@(item.InterestStartDate.OtcFormatDate()) |
@(item.InterestEndDate.OtcFormatDate()) |
@(item.Rate.OtcFormatPercent()) | *@
- @(item.InterestFee.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
- @(item.InterestAmount.OtcFormatMoney()) |
- @(item.InterestClosePnL.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
+ |
+ |
+ |
}
@@ -855,11 +856,11 @@
|
|
}
- @(closeFloat.Quantity.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
- @(closeFloat.TradingFee.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
- @(closeFloat.TradingFeePending.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
- @(closeFloat.DividendIn.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
- @(closeFloat.FloatPnlSum.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
+ |
+ |
+ |
+ |
+ |
@@ -893,9 +894,9 @@
| 平仓总额 |
- @(eod.NotionalValue.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
+ |
实现盈亏 |
- @(eod.TdRealizedPnL.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
+ |
}
@@ -918,7 +919,7 @@
互换序号 |
@(index++) |
互换名义本金 |
- @(tc.PosiNotionalValue.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
+ |
| 起始日期 |
@@ -934,9 +935,9 @@
| 平仓总额 |
- @(tc.SwapCloseAmount.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
+ |
实现盈亏 |
- @(tc.SwapRealizedPnL.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
+ |
@@ -962,9 +963,9 @@
var bgclass = item.InterestDirection == (int)SwapDirectionEnum.收取 ? "swapget" : "swappay";
| @((SwapDirectionEnum)item.InterestDirection) |
- @(item.InterestFee.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
- @(item.InterestAmount.OtcFormatMoney()) |
- @(item.InterestClosePnL.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
+ |
+ |
+ |
}
@@ -994,9 +995,9 @@
var bgclass = item.InterestDirection == (int)SwapDirectionEnum.收取 ? "swapget" : "swappay";
| @((SwapDirectionEnum)item.InterestDirection) |
- @(item.InterestFee.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
- @(item.InterestAmount.OtcFormatMoney()) |
- @(item.InterestClosePnL.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
+ |
+ |
+ |
}
@@ -1057,10 +1058,10 @@
|
}
|
- @((closeFloat.PositionQty??0).OtcFormat(OtcFormatFlag.StockEqvNotional)) |
- @(closeFloat.TradingFee.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
- @(closeFloat.DividendIn.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
- @(closeFloat.FloatPnlSum.OtcFormat(OtcFormatFlag.StockEqvNotional)) |
+ |
+ |
+ |
+ |
diff --git a/YLErpWeb/fe-tests/_proof_short_income.js b/YLErpWeb/fe-tests/_proof_short_income.js
new file mode 100644
index 00000000..eb1196f3
--- /dev/null
+++ b/YLErpWeb/fe-tests/_proof_short_income.js
@@ -0,0 +1,75 @@
+/**
+ * _proof_short_income.js — 红测试原型(纯 Node 可跑,无需 jest 依赖)
+ * ============================================================================
+ * 目的:证明"income 页 MarkClosePnl 缺 longRatio"是一个真实存在、但当前测试未覆盖的不一致。
+ *
+ * 公式逐字抄录自团队金标准 YLErpDAL/Helpers/FrontendCalcReference.cs:
+ * CalcUnwind (unwindSwapTrade.js) : 含 longRatio
+ * CalcIncome (incomeSwapTrade.js) : 无 longRatio ← 差异点
+ *
+ * 跑法:node YLErpWeb/fe-tests/_proof_short_income.js
+ */
+'use strict';
+
+// ---- unwind 页公式(对应 FrontendCalcReference.CalcUnwind:42-47,含 longRatio)----
+function calcUnwindMarkClosePnl(input) {
+ const scale = input.multiplier === 100 ? 0.01 : 1;
+ const floatRatio = input.payDirection === 1 ? 1 : -1;
+ const longRatio = input.positionType === 1 ? 1 : -1;
+ const v = input.closeQty * (input.tradingAmountAvg * scale - input.posiGrossPrice) * floatRatio * longRatio * 10000;
+ const rounded = Math.round(v) / 10000;
+ return Math.round(rounded * 100) / 100; // toFixed(2)
+}
+
+// ---- income 页公式(对应 FrontendCalcReference.CalcIncome:105-108,无 longRatio)----
+function calcIncomeMarkClosePnl(input) {
+ const scale = input.multiplier === 100 ? 0.01 : 1;
+ const floatRatio = input.payDirection === 1 ? 1 : -1;
+ // 注意:此处按照当前生产代码,没有乘以 longRatio
+ const v = input.positionQty * input.contractSize * (input.tradingAmountAvg * scale - input.posiGrossPrice) * floatRatio;
+ return Math.round(v * 100) / 100;
+}
+
+// ---- 修复后 income 公式(补上 longRatio,与 unwind / 后端一致)----
+function calcIncomeMarkClosePnlFixed(input) {
+ const scale = input.multiplier === 100 ? 0.01 : 1;
+ const floatRatio = input.payDirection === 1 ? 1 : -1;
+ const longRatio = input.positionType === 1 ? 1 : -1;
+ const v = input.positionQty * input.contractSize * (input.tradingAmountAvg * scale - input.posiGrossPrice) * floatRatio * longRatio;
+ return Math.round(v * 100) / 100;
+}
+
+// 同一笔债券 TRS:期初全价 1.02,期末(互换/平仓价)105(×100形态→1.05),价差 0.03
+const base = { multiplier: 100, posiGrossPrice: 1.02, tradingAmountAvg: 105, contractSize: 1 };
+const longCase = { ...base, closeQty: 10000, positionQty: 10000, payDirection: 1, positionType: 1 };
+const shortCase = { ...base, closeQty: 10000, positionQty: 10000, payDirection: 1, positionType: 2 };
+
+function check(name, cond) {
+ console.log(` [${cond ? 'PASS' : 'FAIL'}] ${name}`);
+ return cond;
+}
+
+console.log('=== 场景A:多头(PositionType=1)—— 两页理应一致 ===');
+const aU = calcUnwindMarkClosePnl(longCase);
+const aI = calcIncomeMarkClosePnl(longCase);
+console.log(` unwind=${aU} income=${aI}`);
+let allPass = true;
+allPass &= check('多头:unwind == income', aU === aI);
+
+console.log('=== 场景B:空头(PositionType=2)—— 当前代码两页符号相反(红)===');
+const bU = calcUnwindMarkClosePnl(shortCase);
+const bI = calcIncomeMarkClosePnl(shortCase);
+console.log(` unwind=${bU} income=${bI} (空头价格涨应亏损,unwind 正确为负,income 错为正)`);
+allPass &= check('空头:unwind == income (当前代码会 FAIL → 证明 bug 存在)', bU === bI);
+
+console.log('=== 场景C:空头 + 修复后 income(补 longRatio)—— 应一致(绿)===');
+const bIf = calcIncomeMarkClosePnlFixed(shortCase);
+console.log(` unwind=${bU} incomeFixed=${bIf}`);
+allPass &= check('空头:unwind == incomeFixed (修复后 PASS → 证明改动可修复)', bU === bIf);
+
+console.log('');
+if (allPass) {
+ console.log('✅ 全部通过(若场景B也PASS,说明已修复或无空头场景)');
+} else {
+ console.log('❌ 场景B 失败 = 当前代码在「空头+income」下两页算出相反符号 → 真实不一致,且现有 FC_001~009 全为多头未覆盖。');
+}
diff --git a/YLErpWeb/fe-tests/markClosePnlShortConsistency.test.js b/YLErpWeb/fe-tests/markClosePnlShortConsistency.test.js
new file mode 100644
index 00000000..f829a566
--- /dev/null
+++ b/YLErpWeb/fe-tests/markClosePnlShortConsistency.test.js
@@ -0,0 +1,73 @@
+/**
+ * markClosePnlShortConsistency.test.js — 空头场景下 unwind/income 两页 MarkClosePnl 一致性(红测试)
+ * ============================================================================
+ * 状态:当前为 RED(证明 income 页 MarkClosePnl 缺 longRatio 的真实不一致)。
+ * 修复 incomeSwapTrade.js:207 与 YLErpDAL/Helpers/FrontendCalcReference.CalcIncome:107
+ * 补上 longRatio 后,本文件应全部转 GREEN。
+ *
+ * 背景:
+ * - 团队金标准 FrontendCalcReference 明确记录两页差异:unwind 含 longRatio,
+ * income 无 longRatio(CalcIncome:107,注释"无 longRatio")。
+ * - 现有特征化测试 FrontendCalcCharacterizationTest FC_001~009 的 income 场景
+ * (FC_006~009)全部 PositionType=1(多头),唯一空头场景 FC_005 是 unwind,
+ * 因此 income 的空头分支从未被覆盖 → bug 长期未被发现。
+ * - 后端 ValidateFrontendPnL 用同一 CalcIncome 重算比对,公式同源故永远自洽,抓不到。
+ *
+ * 公式逐字抄录(来源见注释行号),与生产代码一致;不改动任何生产文件。
+ */
+const SwapCalc = require('../wwwroot/Scripts/app/swaptrade/swapCalc.js');
+
+// unwind 页 MarkClosePnl(unwindSwapTrade.js:313,含 longRatio)
+function PROD_unwindMarkClosePnl({ closeQty, deliveryPrice, initPosiNetPrice, payDirection, positionType }) {
+ const floatRatio = payDirection === 1 ? 1 : -1;
+ const longRatio = positionType === 1 ? 1 : -1;
+ let v = Math.round(closeQty * (deliveryPrice - initPosiNetPrice) * floatRatio * longRatio * 10000) / 10000;
+ return Number(v.toFixed(2));
+}
+
+// income 页 MarkClosePnl(incomeSwapTrade.js:207,无 longRatio)
+function PROD_incomeMarkClosePnl({ positionAmount, deliveryPrice, initPosiGrossPrice, payDirection }) {
+ const floatRatio = payDirection === 1 ? 1 : -1;
+ let v = positionAmount * (deliveryPrice - initPosiGrossPrice) * floatRatio;
+ return Number(v.toFixed(2));
+}
+
+// 金标准(swapCalc.calcMarkClosePnl,对齐 C# FrontendCalcReference.CalcUnwind,含 longRatio)
+function GOLD({ closeQty, tradingAmountAvg, scale, entryPrice, payDirection, positionType }) {
+ const floatRatio = payDirection === 1 ? 1 : -1;
+ const longRatio = positionType === 1 ? 1 : -1;
+ return SwapCalc.calcMarkClosePnl(closeQty, tradingAmountAvg, scale, entryPrice, floatRatio, longRatio);
+}
+
+// 同一笔债券 TRS:期初全价 1.02,期末 105(×100形态→1.05),价差 0.03;数量 10000
+const CASE = {
+ closeQty: 10000, positionAmount: 10000,
+ deliveryPrice: 105, initPosiNetPrice: 1.02, initPosiGrossPrice: 1.02,
+ tradingAmountAvg: 105, scale: 0.01, entryPrice: 1.02,
+ payDirection: 1,
+};
+
+describe('MarkClosePnl 两页一致性(多头,应一致)', () => {
+ test('多头:unwind == income == gold', () => {
+ const u = PROD_unwindMarkClosePnl({ ...CASE, positionType: 1 });
+ const i = PROD_incomeMarkClosePnl({ ...CASE, });
+ const g = GOLD({ ...CASE, positionType: 1 });
+ expect(u).toBe(i);
+ expect(i).toBe(g);
+ });
+});
+
+describe('MarkClosePnl 两页一致性(空头,当前 RED)', () => {
+ test('空头:unwind == income(当前 FAIL → 证明 income 缺 longRatio 的 bug)', () => {
+ const u = PROD_unwindMarkClosePnl({ ...CASE, positionType: 2 });
+ const i = PROD_incomeMarkClosePnl({ ...CASE, });
+ // 空头价格涨应亏损:unwind = -300,income 当前 = +300(符号反了)
+ expect(u).toBe(i);
+ });
+
+ test('空头:income 应等于 gold(补 longRatio 后才会 PASS)', () => {
+ const i = PROD_incomeMarkClosePnl({ ...CASE, });
+ const g = GOLD({ ...CASE, positionType: 2 });
+ expect(i).toBe(g);
+ });
+});
diff --git a/YLErpWeb/fe-tests/swapCalc.test.js b/YLErpWeb/fe-tests/swapCalc.test.js
index 87c6e17d..3e32a3fa 100644
--- a/YLErpWeb/fe-tests/swapCalc.test.js
+++ b/YLErpWeb/fe-tests/swapCalc.test.js
@@ -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 金标准', () => {
diff --git a/YLErpWeb/fe-tests/swapPrecisionConfig.test.js b/YLErpWeb/fe-tests/swapPrecisionConfig.test.js
new file mode 100644
index 00000000..27ee7b35
--- /dev/null
+++ b/YLErpWeb/fe-tests/swapPrecisionConfig.test.js
@@ -0,0 +1,202 @@
+const fs = require('fs');
+const path = require('path');
+const vm = require('vm');
+
+const ROOT = path.resolve(__dirname, '..');
+const read = (relativePath) => fs.readFileSync(path.join(ROOT, relativePath), 'utf8');
+
+const precisionConfigSrc = read('App_Data/Config/swappriceprecision.js');
+const precisionHelperSrc = read('wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js');
+
+function loadConfiguredPrecision() {
+ const context = { window: {} };
+ vm.runInNewContext(precisionConfigSrc, context);
+ return context.window.main.swapPricePrecision;
+}
+
+function loadHelper(config) {
+ const context = { window: { main: { swapPricePrecision: config } } };
+ vm.runInNewContext(precisionHelperSrc, context);
+ return context.swapPricePrecision;
+}
+
+function activeViewSource(source) {
+ return source.replace(/@\*[\s\S]*?\*@/g, '');
+}
+
+describe('swap price precision common wiring', () => {
+ test('configured and fallback common precision expose 2/2/4', () => {
+ const configured = loadConfiguredPrecision();
+ expect(configured.common).toEqual({
+ amount: { precision: 2, grouping: true },
+ quantity: { integerDigits: 16, precision: 2, grouping: true },
+ rate: { precision: 4 }
+ });
+
+ const helper = loadHelper(configured);
+ expect(helper.getCommonPrecision('amount')).toBe(2);
+ expect(helper.getCommonPrecision('quantity')).toBe(2);
+ expect(helper.getCommonPrecision('rate')).toBe(4);
+
+ const expectedQuantityPrecisions = {
+ Stock: 2,
+ StockIndex: 2,
+ StockIF: 2,
+ CommodityFutures: 2,
+ CommoditySpot: 2,
+ NewOtcStock: 2,
+ HKStock: 2,
+ HKStockIndex: 2,
+ Fund: 4,
+ Bond: 0,
+ TBonds: 0,
+ CreditBonds: 0,
+ OtherBonds: 0,
+ TBFutures: 2,
+ OtherFutures: 2,
+ GoldSpot: 2,
+ OtherSpot: 2,
+ AbroadFutures: 2,
+ AbroadSpot: 2,
+ AbroadStock: 2,
+ AbroadStockIndex: 2,
+ ExRate: 8,
+ Shibor: 2,
+ FixingRepoRate: 2,
+ RateYield: 2,
+ BondIndex: 2
+ };
+ Object.entries(expectedQuantityPrecisions).forEach(([instrumentType, precision]) => {
+ expect(configured[instrumentType].quantityPrecision).toBe(precision);
+ expect(helper.getCommonPrecision('quantity', instrumentType)).toBe(precision);
+ expect(helper.getCommonInputFormat('quantity', { append: '' }, instrumentType).precision).toBe(precision);
+ });
+ const expectedQuantityIntegerDigits = {
+ Stock: 12, StockIndex: 12, StockIF: 12, CommodityFutures: 12, CommoditySpot: 12,
+ NewOtcStock: 12, HKStock: 12, HKStockIndex: 12, Fund: 12, TBFutures: 12,
+ OtherFutures: 12, GoldFutures: 12, GoldSpot: 12, OtherSpot: 12, AbroadFutures: 12,
+ AbroadSpot: 12, AbroadStock: 12, AbroadStockIndex: 12, Shibor: 12,
+ FixingRepoRate: 12, RateYield: 12, BondIndex: 12,
+ Bond: 16, TBonds: 16, CreditBonds: 16, OtherBonds: 16, ExRate: 16
+ };
+ Object.entries(expectedQuantityIntegerDigits).forEach(([instrumentType, integerDigits]) => {
+ expect(configured[instrumentType].quantityIntegerDigits).toBe(integerDigits);
+ expect(helper.getCommonInputFormat('quantity', { append: '' }, instrumentType).integerDigits).toBe(integerDigits);
+ });
+ expect(helper.getCommonPrecision('quantity', 'OtherRate')).toBe(2);
+ expect(helper.getCommonInputFormat('quantity', { append: '' }, 'Fund').precision).toBe(4);
+ expect(helper.getCommonInputFormat('quantity', { append: '' }, 'Bond').precision).toBe(0);
+ expect(helper.getCommonInputFormat('quantity', { append: '' }, 'OtherRate').integerDigits).toBe(16);
+
+ const input = helper.getCommonInputFormat('amount', { append: '', trimTailZeros: true });
+ expect(input).toEqual(expect.objectContaining({ precision: 2, grouping: true, trimTailZeros: false }));
+ expect(helper.getCommonInputFormat('rate', { append: '%' })).toEqual(
+ expect.objectContaining({ precision: 4, grouping: false }));
+ });
+
+ test('invalid common precision falls back and formatted values keep trailing zeros', () => {
+ const helper = loadHelper({
+ common: {
+ amount: { precision: -1 },
+ quantity: { precision: 14 },
+ rate: { precision: 'invalid' }
+ }
+ });
+ expect(helper.getCommonPrecision('amount')).toBe(2);
+ expect(helper.getCommonPrecision('quantity')).toBe(2);
+ expect(helper.getCommonPrecision('rate')).toBe(4);
+
+ const configured = loadHelper({
+ common: {
+ amount: { precision: 3 },
+ quantity: { precision: 1 },
+ rate: { precision: 2 }
+ }
+ });
+ expect(configured.formatCommon('amount', '12.5')).toBe('12.500');
+ expect(configured.formatCommon('quantity', 1)).toBe('1.0');
+ expect(configured.formatCommon('rate', '0.0123')).toBe('1.23%');
+ expect(configured.formatCommon('amount', '1234.5')).toBe('1,234.500');
+ expect(configured.normalizeCommon('amount', '1234.5')).toBe('1234.500');
+ expect(configured.formatCommon('amount', NaN)).toBe('');
+ expect(configured.formatCommon('amount', Infinity)).toBe('');
+ });
+
+ test('common grouping can be disabled independently from precision', () => {
+ const helper = loadHelper({
+ common: {
+ amount: { precision: 2, grouping: false },
+ quantity: { precision: 2, grouping: false },
+ rate: { precision: 4, grouping: false }
+ }
+ });
+ expect(helper.formatCommon('amount', '1234.5')).toBe('1234.50');
+ expect(helper.getCommonInputFormat('quantity', { append: '' }).grouping).toBe(false);
+ });
+
+ test('missing or invalid asset quantity precision falls back to common quantity precision', () => {
+ const helper = loadHelper({
+ common: { quantity: { precision: 3, grouping: true } },
+ Stock: { quantityPrecision: 'invalid' },
+ Bond: {}
+ });
+ expect(helper.getCommonPrecision('quantity', 'Stock')).toBe(3);
+ expect(helper.getCommonPrecision('quantity', 'Bond')).toBe(3);
+ expect(helper.getCommonPrecision('quantity', 'OtherRate')).toBe(3);
+ expect(helper.getCommonInputFormat('quantity', { append: '' }, 'Stock').integerDigits).toBe(16);
+ expect(loadHelper({
+ common: { quantity: { integerDigits: 0, precision: 3 } },
+ Stock: { quantityIntegerDigits: 'invalid' }
+ }).getCommonInputFormat('quantity', { append: '' }, 'Stock').integerDigits).toBe(16);
+ });
+
+ test('SwapTrade2 common fields use the price helper without changing price responsibilities', () => {
+ const scripts = {
+ edit: read('wwwroot/Scripts/app/swaptrade/swapTradeEdit.js'),
+ income: read('wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js'),
+ unwind: read('wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js'),
+ longShort: read('wwwroot/Scripts/app/swaptrade/swapLongShort.js'),
+ unwindLongShort: read('wwwroot/Scripts/app/swaptrade/unwindLongShort.js'),
+ view: read('wwwroot/Scripts/app/swaptrade/swapTradeView.js'),
+ flow: read('wwwroot/Scripts/app/swaptrade/SwapflowList.js'),
+ helper: precisionHelperSrc
+ };
+ const view = read('Views/SwapTrade2/TradeView.cshtml');
+ const activeView = activeViewSource(view);
+
+ for (const script of Object.values(scripts)) {
+ expect(script).not.toMatch(/otcformat\.trading\.swap(?:Amount|Quantity|RateP)/);
+ }
+ expect(scripts.edit).toMatch(/getCommonInputFormat\('amount'/);
+ expect(scripts.edit).toMatch(/getCommonInputFormat\(\s*['"]quantity['"]/);
+ expect(scripts.edit).toMatch(/getCommonInputFormat\('rate'/);
+ expect(scripts.view).toContain("swapPricePrecision.formatCommon('rate', item.Rate)");
+ expect(scripts.flow).toMatch(/name:\s*'ytm'[\s\S]*formatter:\s*otcformat\.trading\.marginRateP/);
+ expect(scripts.flow).not.toMatch(/name:\s*'ytm'[\s\S]*formatCommon\('rate'/);
+
+ expect(scripts.unwind).toContain('ClosePercent = otcformat.fixed6');
+ expect(scripts.unwind).toContain('_.round(tradingFee, 2)');
+ expect(scripts.unwind).toContain('.toFixed(2)');
+ expect(scripts.edit).toContain('observationRatePrecision = 12');
+ expect(scripts.edit).toMatch(/roundObservationRate[\s\S]*swapPricePrecision\.roundDecimal/);
+ expect(scripts.helper).toContain('getRule: getRule');
+ expect(scripts.helper).toContain('getInputFormat: function');
+ expect(scripts.helper).toContain('roundForSubmit: function');
+ expect(scripts.helper).toContain('normalizeCommon: function');
+
+ expect(activeView).not.toContain('OtcFormatHelper');
+ expect(activeView).not.toMatch(/OtcFormatMoney|OtcFormatNotional/);
+ expect(activeView).toContain('SwapCommonData');
+ expect(activeView).toMatch(/js-swap-common[\s\S]*data-kind="amount"/);
+ expect(activeView).toMatch(/js-swap-common[\s\S]*data-kind="quantity"/);
+ expect(activeView).toMatch(/js-swap-common[\s\S]*data-kind="rate"/);
+ expect(scripts.view).toContain('formatSwapCommonElements');
+ expect(scripts.edit).toContain('getQuantityInputFormat');
+ expect(scripts.unwind).toContain('getQuantityInputFormat');
+ expect(scripts.flow).toContain('getQuantityInputFormat');
+ expect(scripts.flow).toMatch(/formatSwapQuantity = function \(value, options, rowObject\)/);
+ expect(scripts.flow).toMatch(/row\.UnderlyingInstrumentType[\s\S]*row\.InstrumentType[\s\S]*row\.position[\s\S]*UnderlyingInstrumentType/);
+ expect(scripts.flow).toContain("formatCommon('quantity', value, instrumentType)");
+ expect(activeView).toMatch(/data-kind="quantity"[^>]*data-instrument-type=/);
+ });
+});
diff --git a/YLErpWeb/fe-tests/unwindSwapTrade.test.js b/YLErpWeb/fe-tests/unwindSwapTrade.test.js
index 53c8197d..eb87bc0d 100644
--- a/YLErpWeb/fe-tests/unwindSwapTrade.test.js
+++ b/YLErpWeb/fe-tests/unwindSwapTrade.test.js
@@ -43,6 +43,9 @@ function loadUnwindHelpers() {
vueDatePicker() { return {}; },
vueNumberInput() { return {}; }
},
+ swapPricePrecision: {
+ createVueInputComponent() { return {}; }
+ },
tradeHelper: { IsBond() { return false; } },
main: {
post() {
@@ -112,14 +115,14 @@ describe('base-rate pending trading fee', () => {
expectClose(result, 450.00);
});
- test('partial close fee and pending fee both use the rounded opening fee allocation', () => {
- const tradingFee = swapPosiFeeCalc.calcAllocatedTradingFee(
- 113.46, consPosiFeeType.Percent, 1.1234, 4039.2, 4000, 10098, 10000);
+ test('a manually adjusted pending fee does not override the base-rate close fee', () => {
+ const tradingFee = swapPosiFeeCalc.calcTradingFee(
+ consPosiFeeType.Unit, 0.123456, 0, 10000);
const pendingFee = swapPosiFeeCalc.calcTradingFeePending(
- 113.46, consPosiFeeType.Percent, 1.1234, 4039.2, 4000, 10098, 10000, 0.4);
+ 1235.56, consPosiFeeType.Unit, 0.123456, 0, 10000, 0, 10000, 1);
- expectClose(tradingFee, 45.38);
- expectClose(pendingFee, 45.38);
+ expectClose(tradingFee, 1234.56);
+ expectClose(pendingFee, 1235.56);
});
test('without a configured base rate, the legacy close-percent calculation remains', () => {
diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/ExtenstionTime.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/ExtenstionTime.js
index ce15be8f..7749b49b 100644
--- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/ExtenstionTime.js
+++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/ExtenstionTime.js
@@ -31,6 +31,10 @@
main.message("新到期日不能为空");
return;
}
+ if (this.extenstion.extenstionData.NewMaturityDate <= this.extenstion.extenstionData.OldMaturityDate) {
+ main.message("新到期日应晚于原到期日");
+ return;
+ }
main.post("/swaptrade2/SaveExtension", { swap_Event: thisObj.extenstion })
.done(function (res) {
thisObj.extenstion.id = res.obj.id;
@@ -80,4 +84,4 @@
components: {
'vue-datepicker': FastVue.vueDatePicker(),
}
-});
\ No newline at end of file
+});
diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/SwapflowList.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/SwapflowList.js
index 3af1b627..bfab8ce6 100644
--- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/SwapflowList.js
+++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/SwapflowList.js
@@ -1,7 +1,16 @@
//window.otcformat.options.disableGrouping = true;
const inputFormatTradePrice = Object.freeze({ precision: otcformat.trading.tradeSinglePrice.precision, negative: true, append: '' });
const inputFormatSwapDeliveryPrice = Object.freeze({ precision: 9, negative: true, append: '' });
-const inputFormatTradeAmount = Object.freeze({ precision: otcformat.trading.notional.precision, append: '' });
+const inputFormatSwapAmount = swapPricePrecision.getCommonInputFormat('amount', { negative: true, append: '' });
+const formatSwapAmount = value => swapPricePrecision.formatCommon('amount', value);
+const formatSwapQuantity = function (value, options, rowObject) {
+ const row = rowObject || {};
+ const instrumentType = row.UnderlyingInstrumentType
+ || row.InstrumentType
+ || (row.position && row.position.UnderlyingInstrumentType)
+ || '';
+ return swapPricePrecision.formatCommon('quantity', value, instrumentType);
+};
const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' });
var clients = ylotc.clients;
const consUnderlyingFlag = (function () {
@@ -268,19 +277,19 @@ function getColModelGridStep1() {
label: '成交数量/张数',
width: 160,
align: 'center',
- formatter: otcformat.trading.notional
+ formatter: formatSwapQuantity
}, {
name: 'TradingAmount',
label: '成交金额(元)',
width: 210,
align: 'center',
- formatter: otcformat.trading.umprice
+ formatter: formatSwapAmount
}, {
name: 'TradingFee',
label: '交易费用',
width: 90,
align: 'center',
- formatter: otcformat.trading.umprice
+ formatter: formatSwapAmount
}, {
name: 'TradingAmountAvg',
label: '成交全价',
@@ -372,13 +381,13 @@ function getColModelGridStep2() {
label: '成交数量/张数',
width: 90,
align: 'center',
- formatter: otcformat.trading.notional
+ formatter: formatSwapQuantity
}, {
name: 'TradingAmount',
label: '成交金额(元)',
width: 90,
align: 'center',
- formatter: otcformat.trading.umprice
+ formatter: formatSwapAmount
}, {
name: 'TradingAmountAvg',
label: '成交均价',
@@ -391,7 +400,7 @@ function getColModelGridStep2() {
label: '交易费用',
width: 90,
align: 'center',
- formatter: otcformat.trading.umprice
+ formatter: formatSwapAmount
}, {
name: 'TradingAmountFeeAvg',
label: '含费均价',
@@ -521,13 +530,13 @@ function getColModelGridStep3() {
label: '成交数量/张数',
width: 160,
align: 'center',
- formatter: otcformat.trading.notional
+ formatter: formatSwapQuantity
}, {
name: 'TradingAmount',
label: '成交金额(元)',
width: 160,
align: 'center',
- formatter: otcformat.trading.umprice
+ formatter: formatSwapAmount
}, {
name: 'ContractSize',
label: '乘数',
@@ -540,31 +549,31 @@ function getColModelGridStep3() {
label: '交易费用佣金',
width: 160,
align: 'center',
- formatter: otcformat.trading.umprice
+ formatter: formatSwapAmount
}, {
name: 'TradingFeePending',
label: '待结算交易费用佣金',
width: 160,
align: 'center',
- formatter: otcformat.trading.umprice
+ formatter: formatSwapAmount
}, {
name: 'DividendPending',
label: '待结算分红收益',
width: 160,
align: 'center',
- formatter: otcformat.trading.umprice
+ formatter: formatSwapAmount
}, {
name: 'MarkClosePnl',
label: '浮动端平仓盈亏·浮动',
width: 160,
align: 'center',
- formatter: otcformat.trading.umprice
+ formatter: formatSwapAmount
}, {
name: 'DividendIn',
label: '浮动端平仓盈亏·分红',
width: 160,
align: 'center',
- formatter: otcformat.trading.umprice
+ formatter: formatSwapAmount
}
];
return col;
@@ -664,26 +673,26 @@ function getColModelGridStep4() {
label: '名义数量',
width: 160,
align: 'center',
- formatter: otcformat.trading.notional
+ formatter: formatSwapQuantity
}, {
name: 'position.PosiNotionalValue',
label: '名义本金',
width: 160,
align: 'center',
- formatter: otcformat.trading.StockEqvNotional
+ formatter: formatSwapAmount
}
, {
name: 'position.PosiTradingFee',
label: '交易费用佣金',
width: 160,
align: 'center',
- formatter: otcformat.trading.umprice
+ formatter: formatSwapAmount
}, {
name: 'position.PosiTradingFeePending',
label: '待实现交易费用佣金',
width: 160,
align: 'center',
- formatter: otcformat.trading.umprice
+ formatter: formatSwapAmount
}, {
name: 'position.PosiStartDate',
label: '起始日期',
@@ -861,6 +870,7 @@ var vue = new Vue({
SwapTradeNo: "",
BsType: 1,
UnderlyingCode: "",
+ UnderlyingInstrumentType: "",
TradingQty: 0,
TradingAmount: 0,
TradingFee: 0,
@@ -940,8 +950,8 @@ var vue = new Vue({
const thisObj = this;
main.post(`/swapTrade2/SearchTodayWhetherFRData?dateTime=${this.FRData.date}`).done(function (resp) {
if (resp.UnderlyingCode) { // 如果这天有fr007获取值和id,隐藏警告
- thisObj.FRData.value = (resp.ReferencePrice * 100).toFixed(4);
- thisObj.FRData.oldValue = (resp.ReferencePrice * 100).toFixed(4);
+ thisObj.FRData.value = (resp.ReferencePrice * 100).toFixed(swapPricePrecision.getCommonPrecision('rate'));
+ thisObj.FRData.oldValue = (resp.ReferencePrice * 100).toFixed(swapPricePrecision.getCommonPrecision('rate'));
thisObj.FRData.id = resp.id;
thisObj.isShowWarning = false
thisObj.isFromArtifical = resp.DataSource === "人工"
@@ -1014,6 +1024,12 @@ var vue = new Vue({
this.tradeDate = page.valueDate; //将1970/08/08转化成1970-08-08
return this.tradeDate;
},
+ getQuantityInputFormat() {
+ return swapPricePrecision.getCommonInputFormat(
+ 'quantity',
+ { append: '' },
+ this.swapflow.UnderlyingInstrumentType);
+ },
addNew() {
this.tradeDate = page.valueDate;
this.initSwapFlow();
@@ -1029,6 +1045,7 @@ var vue = new Vue({
SwapTradeNo: "",
BsType: 1,
UnderlyingCode: "",
+ UnderlyingInstrumentType: "",
TradingQty: 0,
TradingAmount: 0,
TradingFee: 0,
@@ -1126,6 +1143,7 @@ var vue = new Vue({
thisObj.swapflow.SwapTradeNo = item.SwapTradeNo;
thisObj.swapflow.BsType = item.BsType;
thisObj.swapflow.UnderlyingCode = item.UnderlyingCode;
+ thisObj.swapflow.UnderlyingInstrumentType = item.UnderlyingInstrumentType || item.InstrumentType || "";
thisObj.swapflow.TradingQty = item.TradingQty;
thisObj.swapflow.TradingAmount = item.TradingAmount;
thisObj.swapflow.TradingFee = item.TradingFee;
@@ -1155,6 +1173,7 @@ var vue = new Vue({
setUnderlyingCode(data) {
this.swapflow.ContractSize = data.ContractSize;
this.swapflow.UnderlyingName = data.Name;
+ this.swapflow.UnderlyingInstrumentType = data.InstrumentType || data.UnderlyingInstrumentType || "";
},
changeClient: function (client) {
this.swapflow.ClientId = client.id;
diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js
index 8e694d16..105f68c9 100644
--- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js
+++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js
@@ -1,12 +1,14 @@
//otcformat禁止千分位分组
window.otcformat.options.disableGrouping = true;
-const inputFormatSwapRate = Object.freeze({ precision: otcformat.trading.premiumRateP.precision, append: '%' });
+const inputFormatSwapRate = swapPricePrecision.getCommonInputFormat('rate', { append: '%' });
const inputFormatTradePrice = Object.freeze({ precision: otcformat.trading.tradePrice.precision, append: '' });
-const inputFormatTradeAmount = Object.freeze({ precision: otcformat.trading.notional.precision, append: '' });
-const inputFormatEqvNotional = Object.freeze({ precision: 2, append: '', negative: true, trimTailZeros: false });
-const inputFormatInterestAmount = Object.freeze({ precision: 2, append: '', negative: true, trimTailZeros: false });
-const inputFormatDividend = Object.freeze({ precision: 2, append: '', negative: true });
+const inputFormatEqvNotional = swapPricePrecision.getCommonInputFormat('amount', { append: '', negative: true });
+const inputFormatInterestAmount = swapPricePrecision.getCommonInputFormat('amount', { append: '', negative: true });
+const inputFormatDividend = swapPricePrecision.getCommonInputFormat('amount', { append: '', negative: true });
+const swapInstrumentType = (model.FlowEvents || []).find(item => item && item.UnderlyingInstrumentType)?.UnderlyingInstrumentType || model.UnderlyingInstrumentType || '';
+const formatSwapAmount = value => swapPricePrecision.normalizeCommon('amount', value);
+const formatSwapQuantity = value => swapPricePrecision.normalizeCommon('quantity', value, swapInstrumentType);
const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' });
const inputFormatMarginRateNoPercent = Object.freeze({ precision: otcformat.trading.umpriceP.precision, append: '', percent: true });
const inputFormatSwapDeliveryPrice = Object.freeze({ precision: 9, append: '', negative: true });
@@ -60,6 +62,12 @@ const vue = new Vue({
});
},
methods: {
+ formatAmount(value) {
+ return swapPricePrecision.formatCommon('amount', value);
+ },
+ formatQuantity(value) {
+ return swapPricePrecision.formatCommon('quantity', value, swapInstrumentType);
+ },
isAfterMaxIncomeValueDate(valueDate) {
return valueDate && MaxIncomeValueDate && valueDate > MaxIncomeValueDate;
},
@@ -120,31 +128,31 @@ const vue = new Vue({
return pricef;
},
dataFormat() {
- this.deal.NotionalValue = otcformat.fixed2(parseFloat(this.deal.NotionalValue));
- this.deal.PosiNotionalValue = otcformat.fixed2(parseFloat(this.deal.PosiNotionalValue));
- this.deal.NotionalQty = otcformat.trading.notional(this.deal.NotionalQty);
- this.deal.PositionQty2 = otcformat.trading.notional(this.deal.PositionQty);
- this.floatPosition.Quantity = otcformat.trading.notional(this.floatPosition.Quantity);
- this.floatPosition.PositionQty = otcformat.trading.notional(this.floatPosition.PositionQty);
- this.deal.SwapCloseAmount = otcformat.trading.StockEqvNotional(this.deal.SwapCloseAmount);
+ this.deal.NotionalValue = formatSwapAmount(this.deal.NotionalValue);
+ this.deal.PosiNotionalValue = formatSwapAmount(this.deal.PosiNotionalValue);
+ this.deal.NotionalQty = formatSwapQuantity(this.deal.NotionalQty);
+ this.deal.PositionQty2 = formatSwapQuantity(this.deal.PositionQty);
+ this.floatPosition.Quantity = formatSwapQuantity(this.floatPosition.Quantity);
+ this.floatPosition.PositionQty = formatSwapQuantity(this.floatPosition.PositionQty);
+ this.deal.SwapCloseAmount = formatSwapAmount(this.deal.SwapCloseAmount);
//this.floatPosition.PosiNetPrice = otcformat.trading.umprice(this.floatPosition.PosiNetPrice);
//this.floatPosition.PosiGrossPrice = otcformat.trading.umprice(this.floatPosition.PosiGrossPrice);
- this.floatPosition.TradingFee = otcformat.trading.StockEqvNotional(this.floatPosition.TradingFee);
- this.floatPosition.DividendIn = parseFloat(this.floatPosition.DividendIn).toFixed(2);
- this.floatPosition.MarkClosePnl = otcformat.trading.StockEqvNotional(this.floatPosition.MarkClosePnl);
+ this.floatPosition.TradingFee = formatSwapAmount(this.floatPosition.TradingFee);
+ this.floatPosition.DividendIn = formatSwapAmount(this.floatPosition.DividendIn);
+ this.floatPosition.MarkClosePnl = formatSwapAmount(this.floatPosition.MarkClosePnl);
this.interestList.forEach(x => {
- //x.Principal = otcformat.trading.StockEqvNotional(x.Principal);
+ //x.Principal = formatSwapAmount(x.Principal);
//x.Rate = otcformat.fixed6(x.Rate);
- x.InterestFee = otcformat.trading.StockEqvNotional(x.InterestFee);
- x.InterestAmount = otcformat.trading.StockEqvNotional(x.InterestAmount);
- x.InterestClosePnL = otcformat.trading.StockEqvNotional(x.InterestClosePnL);
+ x.InterestFee = formatSwapAmount(x.InterestFee);
+ x.InterestAmount = formatSwapAmount(x.InterestAmount);
+ x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
//x.InterestStartDate = x.InterestStartDate ? x.InterestStartDate.substr(0, 10) : "";
//x.InterestEndDate = x.InterestEndDate ? x.InterestEndDate.substr(0, 10) : "";
});
this.marginList.forEach(x => {
- x.InterestFee = otcformat.trading.StockEqvNotional(x.InterestFee);
- x.InterestAmount = otcformat.trading.StockEqvNotional(x.InterestAmount);
- x.InterestClosePnL = otcformat.trading.StockEqvNotional(x.InterestClosePnL);
+ x.InterestFee = formatSwapAmount(x.InterestFee);
+ x.InterestAmount = formatSwapAmount(x.InterestAmount);
+ x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
});
},
setValueDate(e) {//修改平仓日期
@@ -183,7 +191,7 @@ const vue = new Vue({
changeInterestAmount(item) {//修改利息金额
let InterestFee = item.InterestFee == "" ? 0 : parseFloat(item.InterestFee);
let interestRatio = item.InterestDirection == 1 ? 1 : -1;
- item.InterestClosePnL = otcformat.trading.StockEqvNotional(parseFloat(item.InterestAmount) * interestRatio + InterestFee * interestRatio);
+ item.InterestClosePnL = formatSwapAmount(parseFloat(item.InterestAmount) * interestRatio + InterestFee * interestRatio);
this.calcCloseAmount();
},
calcFloatClosePnl() {//计算浮动端平仓盈亏
@@ -197,7 +205,7 @@ const vue = new Vue({
// CloseNotionalValue 是期初全价折算后的名义本金,直接乘价差会重复包含期初价格。
let positionAmount = parseFloat(thisObj.floatPosition.Quantity) * parseFloat(thisObj.floatPosition.ContractSize || 1);
thisObj.floatPosition.MarkClosePnl = positionAmount * (deliveryPrice - thisObj.initPosiGrossPrice) * floatRatio;
- thisObj.floatPosition.MarkClosePnl = otcformat.trading.StockEqvNotional(thisObj.floatPosition.MarkClosePnl);//MarkClosePnl 纯盯市不要计算交易费用和分红
+ thisObj.floatPosition.MarkClosePnl = formatSwapAmount(thisObj.floatPosition.MarkClosePnl);//MarkClosePnl 纯盯市不要计算交易费用和分红
// 守卫: 浮动盈亏合计必须保留 2 位小数 → 对应历史 bug 3c5f25a5(原代码缺精度保留)
// 数值由 swapCalc.calcFloatPnlSum 计算, 此处 .toFixed(2) 仅保留字符串类型以兼容下游
thisObj.floatPosition.FloatPnlSum = SwapCalc.calcFloatPnlSum(thisObj.floatPosition.MarkClosePnl, TradingFee, TradingFeePending, DividendIn).toFixed(2);
@@ -205,7 +213,7 @@ const vue = new Vue({
},
//calcClosePnL() {//计算浮动端平仓盈亏
// let pnl = parseFloat(this.floatPosition.ClosePnL) - parseFloat(this.floatPosition.TradingFee);
- // this.floatPosition.ClosePnL = otcformat.trading.StockEqvNotional(pnl);
+ // this.floatPosition.ClosePnL = formatSwapAmount(pnl);
// this.calcCloseAmount();
//},
calcCloseAmount() {//计算平仓总额=浮动收取+利息收取-浮动支付-利息支付
@@ -237,9 +245,9 @@ const vue = new Vue({
thisObj.deal.SwapMarginRebatePnl = parseFloat(thisObj.deal.SwapMarginRebatePnl) + interestAmount;
thisObj.deal.SwapRealizedPnL = parseFloat(thisObj.deal.SwapRealizedPnL) + interestAmount;
});
- thisObj.deal.SwapCloseAmount = otcformat.trading.StockEqvNotional(thisObj.deal.SwapCloseAmount);
- thisObj.deal.SwapRealizedPnL = otcformat.trading.StockEqvNotional(thisObj.deal.SwapRealizedPnL);
- thisObj.deal.SwapMarginRebatePnl = otcformat.trading.StockEqvNotional(thisObj.deal.SwapMarginRebatePnl);
+ thisObj.deal.SwapCloseAmount = formatSwapAmount(thisObj.deal.SwapCloseAmount);
+ thisObj.deal.SwapRealizedPnL = formatSwapAmount(thisObj.deal.SwapRealizedPnL);
+ thisObj.deal.SwapMarginRebatePnl = formatSwapAmount(thisObj.deal.SwapMarginRebatePnl);
},
getInterestList() {//根据平仓日期获取利息腿信息
var thisObj = this;
@@ -258,18 +266,14 @@ const vue = new Vue({
},
getDivindIn() {
var thisObj = this;
- let ratio = this.floatPosition.PositionType == 1 ? 1 : -1;
- let floatRatio = this.floatPosition.PayDirection == 1 ? 1 : -1;
- var postData = { startDate: thisObj.TradeStartDate, endDate: thisObj.deal.UnwindDate, underlyingCode: thisObj.floatPosition.UnderlyingCode, tradeId: thisObj.deal.SwapTradeId, unwindDate: thisObj.deal.UnwindDate }
- main.post("/BondPayment/GetBondPayMentInterest", postData, { async: false }).done(function (resp) {
- let totalDividend = parseFloat(thisObj.deal.NotionalQty) * resp.obj.totalInterest * ratio * floatRatio;//总的
- let consumedDividend = Math.abs(parseFloat(resp.obj.consumedDividend ?? 0)) * ratio * floatRatio;//已实现的
- // 互换是全量消费,consumedDividend>0 表示分红已被当天互换消费,归0
- thisObj.floatPosition.DividendIn = Math.abs(consumedDividend) > 0 ? parseFloat((totalDividend - consumedDividend).toFixed(2)) : parseFloat(totalDividend.toFixed(2));
- thisObj.floatPosition.DividendPending = 0;
- thisObj.calcFloatClosePnl();
- thisObj.dataFormat();
- });
+ // 方案C:分红改由后端 InitIncome 读 EOD PosiDividendSum 填入 floatPosition.DividendIn(单一可信源)。
+ // 前端不再调用 GetBondPayMentInterest 自算——消除"期初持仓×totalInterest"对已平仓部分的重复计入。
+ // floatPosition.DividendIn 保持后端返回值不动(=本次互换要落袋的全量待实现)。
+ // DividendPending(待结算)互换页保持 0:互换=全量结清,结清后账上无待结算。
+ // ↳ 与平仓页不同:平仓只拿走一部分,剩余持仓仍有待结算 → 平仓页 DividendPending=PosiDividendSum(后端值)。
+ thisObj.floatPosition.DividendPending = 0;
+ thisObj.calcFloatClosePnl();
+ thisObj.dataFormat();
},
incomeTrade() {//互换
var thisObj = this;
diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapCalc.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapCalc.js
index dfcea9a7..670a0748 100644
--- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapCalc.js
+++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapCalc.js
@@ -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);
}
diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapLongShort.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapLongShort.js
index 4ca4ce3b..60bcb46b 100644
--- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapLongShort.js
+++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapLongShort.js
@@ -1,6 +1,9 @@
//otcformat禁止千分位分组
window.otcformat.options.disableGrouping = true;
-const inputFormatEqvNotional = Object.freeze({ precision: 2, append: '', trimTailZeros: false });
+const inputFormatEqvNotional = swapPricePrecision.getCommonInputFormat('amount', { append: '' });
+const swapInstrumentType = (model.FlowEvents || []).find(item => item && item.UnderlyingInstrumentType)?.UnderlyingInstrumentType || model.UnderlyingInstrumentType || '';
+const formatSwapAmount = value => swapPricePrecision.normalizeCommon('amount', value);
+const formatSwapQuantity = value => swapPricePrecision.normalizeCommon('quantity', value, swapInstrumentType);
let ValueDate = model.ValueDate;
const vue = new Vue({
el: '#vueDiv',
@@ -22,6 +25,12 @@ const vue = new Vue({
this.setValueDate();
},
methods: {
+ formatAmount(value) {
+ return swapPricePrecision.formatCommon('amount', value);
+ },
+ formatQuantity(value) {
+ return swapPricePrecision.formatCommon('quantity', value, swapInstrumentType);
+ },
initDeal() {
this.interestList = model.FlowEvents.filter((item) => {
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 7 || item.InterestMode == 8 || item.InterestMode == 9;
@@ -42,29 +51,29 @@ const vue = new Vue({
}
},
dataFormat() {
- this.deal.NotionalValue = otcformat.fixed2(parseFloat(this.deal.NotionalValue));
- this.deal.PosiNotionalValue = otcformat.fixed2(parseFloat(this.deal.PosiNotionalValue));
- this.deal.NotionalQty = otcformat.trading.notional(this.deal.NotionalQty);
- this.deal.PositionQty = otcformat.trading.notional(this.deal.PositionQty);
- this.deal.SwapCloseAmount = otcformat.trading.StockEqvNotional(this.deal.SwapCloseAmount);
+ this.deal.NotionalValue = formatSwapAmount(this.deal.NotionalValue);
+ this.deal.PosiNotionalValue = formatSwapAmount(this.deal.PosiNotionalValue);
+ this.deal.NotionalQty = formatSwapQuantity(this.deal.NotionalQty);
+ this.deal.PositionQty = formatSwapQuantity(this.deal.PositionQty);
+ this.deal.SwapCloseAmount = formatSwapAmount(this.deal.SwapCloseAmount);
this.interestList.forEach(x => {
- //x.Principal = otcformat.trading.StockEqvNotional(x.Principal);
+ //x.Principal = formatSwapAmount(x.Principal);
//x.Rate = otcformat.fixed6(x.Rate);
- x.InterestFee = otcformat.trading.StockEqvNotional(x.InterestFee);
- x.InterestAmount = otcformat.trading.StockEqvNotional(x.InterestAmount);
- x.InterestClosePnL = otcformat.trading.StockEqvNotional(x.InterestClosePnL);
+ x.InterestFee = formatSwapAmount(x.InterestFee);
+ x.InterestAmount = formatSwapAmount(x.InterestAmount);
+ x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
//x.InterestStartDate = x.InterestStartDate ? x.InterestStartDate.substr(0, 10) : "";
//x.InterestEndDate = x.InterestEndDate ? x.InterestEndDate.substr(0, 10) : "";
});
this.marginList.forEach(x => {
- x.InterestFee = otcformat.trading.StockEqvNotional(x.InterestFee);
- x.InterestAmount = otcformat.trading.StockEqvNotional(x.InterestAmount);
- x.InterestClosePnL = otcformat.trading.StockEqvNotional(x.InterestClosePnL);
+ x.InterestFee = formatSwapAmount(x.InterestFee);
+ x.InterestAmount = formatSwapAmount(x.InterestAmount);
+ x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
});
},
changeInterestAmount(item) {//修改利息金额
let interestRatio = item.InterestDirection == 1 ? 1 : -1;
- item.InterestClosePnL = otcformat.trading.StockEqvNotional(parseFloat(item.InterestAmount) * interestRatio + parseFloat(item.InterestFee));
+ item.InterestClosePnL = formatSwapAmount(parseFloat(item.InterestAmount) * interestRatio + parseFloat(item.InterestFee));
this.calcCloseAmount();
},
calcCloseAmount() {//计算平仓总额=浮动收取+利息收取-浮动支付-利息支付
@@ -86,10 +95,10 @@ const vue = new Vue({
thisObj.deal.SwapRealizedPnL = parseFloat(thisObj.deal.SwapRealizedPnL) + interestAmount;
thisObj.deal.SwapMarginAmount = parseFloat(thisObj.deal.SwapMarginAmount) + parseFloat(x.InterestPrincipal);
});
- thisObj.deal.SwapCloseAmount = otcformat.trading.StockEqvNotional(thisObj.deal.SwapCloseAmount);
- thisObj.deal.SwapRealizedPnL = otcformat.trading.StockEqvNotional(thisObj.deal.SwapRealizedPnL);
- thisObj.deal.SwapMarginRebatePnl = otcformat.trading.StockEqvNotional(thisObj.deal.SwapMarginRebatePnl);
- thisObj.deal.SwapMarginAmount = otcformat.trading.StockEqvNotional(thisObj.deal.SwapMarginAmount);
+ thisObj.deal.SwapCloseAmount = formatSwapAmount(thisObj.deal.SwapCloseAmount);
+ thisObj.deal.SwapRealizedPnL = formatSwapAmount(thisObj.deal.SwapRealizedPnL);
+ thisObj.deal.SwapMarginRebatePnl = formatSwapAmount(thisObj.deal.SwapMarginRebatePnl);
+ thisObj.deal.SwapMarginAmount = formatSwapAmount(thisObj.deal.SwapMarginAmount);
},
getInterestList() {//根据平仓日期获取利息腿信息
var thisObj = this;
diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js
index fa37a9da..5f4d5a4c 100644
--- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js
+++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js
@@ -1,47 +1,57 @@
var swapPricePrecision = (function (global) {
const defaults = Object.freeze({
- Stock: { integerDigits: 7, precision: 2 },
- StockIndex: { integerDigits: 7, precision: 2 },
- StockIF: { integerDigits: 7, precision: 4 },
- CommodityFutures: { integerDigits: 7, precision: 4 },
- CommoditySpot: { integerDigits: 7, precision: 4 },
- NewOtcStock: { integerDigits: 7, precision: 4 },
- HKStock: { integerDigits: 7, precision: 4 },
- HKStockIndex: { integerDigits: 7, precision: 4 },
- Fund: { integerDigits: 7, precision: 4 },
+ common: {
+ amount: { precision: 2, grouping: true },
+ quantity: { integerDigits: 16, precision: 2, grouping: true },
+ rate: { precision: 4 }
+ },
+ Stock: { integerDigits: 7, precision: 2, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ StockIndex: { integerDigits: 7, precision: 2, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ StockIF: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ CommodityFutures: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ CommoditySpot: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ NewOtcStock: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ HKStock: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ HKStockIndex: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ Fund: { integerDigits: 7, precision: 4, quantityPrecision: 4, quantityIntegerDigits: 12 },
Bond: {
+ quantityPrecision: 0, quantityIntegerDigits: 16,
grossPrice: { integerDigits: 6, precision: 9 },
netPrice: { integerDigits: 6, precision: 9 },
yield: { integerDigits: 2, precision: 4 }
},
TBonds: {
+ quantityPrecision: 0, quantityIntegerDigits: 16,
grossPrice: { integerDigits: 6, precision: 9 },
netPrice: { integerDigits: 6, precision: 9 },
yield: { integerDigits: 2, precision: 4 }
},
CreditBonds: {
+ quantityPrecision: 0, quantityIntegerDigits: 16,
grossPrice: { integerDigits: 6, precision: 9 },
netPrice: { integerDigits: 6, precision: 9 },
yield: { integerDigits: 2, precision: 4 }
},
OtherBonds: {
+ quantityPrecision: 0, quantityIntegerDigits: 16,
grossPrice: { integerDigits: 6, precision: 9 },
netPrice: { integerDigits: 6, precision: 9 },
yield: { integerDigits: 2, precision: 4 }
},
- TBFutures: { integerDigits: 8, precision: 4 },
- OtherFutures: { integerDigits: 8, precision: 4 },
- GoldSpot: { integerDigits: 8, precision: 4 },
- OtherSpot: { integerDigits: 8, precision: 4 },
- AbroadFutures: { integerDigits: 8, precision: 4 },
- AbroadSpot: { integerDigits: 8, precision: 4 },
- AbroadStock: { integerDigits: 8, precision: 2 },
- AbroadStockIndex: { integerDigits: 8, precision: 4 },
- ExRate: { integerDigits: 2, precision: 8 },
- Shibor: { integerDigits: 2, precision: 4 },
- FixingRepoRate: { integerDigits: 2, precision: 4 },
- RateYield: {integerDigits: 6, precision: 8},
- BondIndex: {integerDigits: 6, precision: 4},
+ TBFutures: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ OtherFutures: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ GoldFutures: { quantityIntegerDigits: 12 },
+ GoldSpot: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ OtherSpot: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ AbroadFutures: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ AbroadSpot: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ AbroadStock: { integerDigits: 8, precision: 2, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ AbroadStockIndex: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ ExRate: { integerDigits: 2, precision: 8, quantityPrecision: 8, quantityIntegerDigits: 16 },
+ Shibor: { integerDigits: 2, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ FixingRepoRate: { integerDigits: 2, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
+ RateYield: {integerDigits: 6, precision: 8, quantityPrecision: 2, quantityIntegerDigits: 12},
+ BondIndex: {integerDigits: 6, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12},
});
@@ -121,6 +131,18 @@ var swapPricePrecision = (function (global) {
return { integerDigits: integerDigits, precision: precision };
}
+ function normalizeCommonRule(rule) {
+ if (!rule || typeof rule !== 'object') return null;
+ const precision = Number(rule.precision);
+ if (!Number.isInteger(precision) || precision < 0 || precision > 13) return null;
+ const integerDigits = Number(rule.integerDigits);
+ return {
+ precision: precision,
+ integerDigits: Number.isInteger(integerDigits) && integerDigits >= 1 && integerDigits <= 18 ? integerDigits : undefined,
+ grouping: typeof rule.grouping === 'boolean' ? rule.grouping : undefined
+ };
+ }
+
function findRule(source, instrumentType, field) {
const typeRule = source && source[instrumentType];
return typeRule ? normalizeRule(typeRule[field] || typeRule) : null;
@@ -132,6 +154,73 @@ var swapPricePrecision = (function (global) {
return findRule(global.main && global.main.swapPricePrecision, instrumentType, field) || fallback;
}
+ function formatFixed(value, precision) {
+ const normalized = normalizeDecimal(roundDecimal(value, precision));
+ if (!normalized) return '';
+ const negative = normalized.charAt(0) === '-';
+ const parts = (negative ? normalized.substring(1) : normalized).split('.');
+ const integerPart = parts[0];
+ if (precision === 0) return (negative ? '-' : '') + integerPart;
+ return (negative ? '-' : '') + integerPart + '.' + (parts[1] || '').padEnd(precision, '0');
+ }
+
+ function groupDecimal(value) {
+ if (!value) return value;
+ const negative = value.charAt(0) === '-';
+ const source = negative ? value.substring(1) : value;
+ const parts = source.split('.');
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+ return (negative ? '-' : '') + parts.join('.');
+ }
+
+ function getQuantityPrecision(instrumentType, fallback) {
+ if (!instrumentType) return fallback;
+ const configured = global.main && global.main.swapPricePrecision;
+ const typeRule = (configured || defaults)[instrumentType];
+ if (!typeRule || !Object.prototype.hasOwnProperty.call(typeRule, 'quantityPrecision')) return fallback;
+ const precision = Number(typeRule.quantityPrecision);
+ return Number.isInteger(precision) && precision >= 0 && precision <= 13 ? precision : fallback;
+ }
+
+ function getQuantityIntegerDigits(instrumentType, fallback) {
+ if (!instrumentType) return fallback;
+ const configured = global.main && global.main.swapPricePrecision;
+ const typeRule = (configured || defaults)[instrumentType];
+ if (!typeRule || !Object.prototype.hasOwnProperty.call(typeRule, 'quantityIntegerDigits')) return fallback;
+ const integerDigits = Number(typeRule.quantityIntegerDigits);
+ return Number.isInteger(integerDigits) && integerDigits >= 1 && integerDigits <= 18 ? integerDigits : fallback;
+ }
+
+ function getCommonRule(kind, instrumentType) {
+ const fallback = normalizeCommonRule(defaults.common[kind]) || { precision: 2, grouping: false };
+ const configured = global.main && global.main.swapPricePrecision
+ && global.main.swapPricePrecision.common;
+ const configuredRule = normalizeCommonRule(configured && configured[kind]);
+ const rule = configuredRule || fallback;
+ if (rule.grouping === undefined) rule.grouping = fallback.grouping;
+ if (rule.integerDigits === undefined) rule.integerDigits = fallback.integerDigits;
+ if (kind === 'quantity') {
+ rule.precision = getQuantityPrecision(instrumentType, rule.precision);
+ rule.integerDigits = getQuantityIntegerDigits(instrumentType, rule.integerDigits);
+ }
+ return rule;
+ }
+
+ function formatCommon(kind, value, instrumentType, options) {
+ if (value === null || value === undefined || value === '') return '';
+ if (instrumentType && typeof instrumentType === 'object') {
+ options = instrumentType;
+ instrumentType = options.instrumentType;
+ }
+ const rule = getCommonRule(kind, instrumentType);
+ const displayValue = kind === 'rate' ? shiftDecimal(value, 2) : value;
+ const formatted = formatFixed(displayValue, rule.precision);
+ if (!formatted) return '';
+ const grouping = options && options.grouping !== undefined ? !!options.grouping : rule.grouping;
+ const text = grouping ? groupDecimal(formatted) : formatted;
+ return kind === 'rate' ? text + '%' : text;
+ }
+
function format(value, instrumentType, field) {
const rule = getRule(instrumentType, field);
if (value === null || value === undefined || value === '') return '';
@@ -260,6 +349,26 @@ var swapPricePrecision = (function (global) {
return Object.freeze({
getRule: getRule,
+ getCommonPrecision: function (kind, instrumentType) {
+ return getCommonRule(kind, instrumentType).precision;
+ },
+ getCommonInputFormat: function (kind, options, instrumentType) {
+ const inputOptions = Object.assign({}, options || {});
+ const type = instrumentType || inputOptions.instrumentType;
+ delete inputOptions.instrumentType;
+ const rule = getCommonRule(kind, type);
+ const result = Object.assign(inputOptions, {
+ precision: rule.precision,
+ grouping: inputOptions.grouping === undefined ? !!rule.grouping : !!inputOptions.grouping,
+ trimTailZeros: false
+ });
+ if (rule.integerDigits !== undefined) result.integerDigits = rule.integerDigits;
+ return result;
+ },
+ formatCommon: formatCommon,
+ normalizeCommon: function (kind, value, instrumentType) {
+ return formatCommon(kind, value, instrumentType, { grouping: false });
+ },
getInputFormat: function (instrumentType, field, options) {
const rule = getRule(instrumentType, field);
return rule ? Object.assign({}, options, rule) : Object.assign({}, options);
diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js
index 2ed290b4..d1fd613c 100644
--- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js
+++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeEdit.js
@@ -10,13 +10,11 @@ const consAssetUnits = page.canAddNewTrader ? ylotc.assetunits
: ylotc.assetunits.filter(x => x.TraderIds.includes(page.Trade.TraderId));
const inputFormatInteger = Object.freeze({ precision: 0, append: '' });
-const inputFormatEqvNotional = Object.freeze({ precision: 2, append: '', trimTailZeros: false });
-const inputFormatTradeAmount = Object.freeze({ precision: otcformat.trading.notional.precision, append: '' });
-const inputFormatPositionQuantityFixed2 = Object.freeze({ precision: 2, append: '', trimTailZeros: false });
-const inputFormatSwapRate = Object.freeze({ precision: 4, negative: true, append: '%', trimTailZeros: false });
+const inputFormatEqvNotional = swapPricePrecision.getCommonInputFormat('amount', { append: '' });
+const inputFormatSwapRate = swapPricePrecision.getCommonInputFormat('rate', { negative: true, append: '%' });
const inputFormatTradePrice = Object.freeze({ precision: otcformat.trading.tradePrice.precision, negative: true, append: '' });
-const inputFormatTradeSinglePrice = Object.freeze({ precision: otcformat.trading.tradeSinglePrice.precision, negative: true, append: '', percent: false });
-const inputFormatTradeSinglePriceFixed2 = Object.freeze({ precision: 2, negative: true, append: '', percent: false, trimTailZeros: false });
+const inputFormatTradeSinglePrice = swapPricePrecision.getCommonInputFormat('amount', { negative: true, append: '', percent: false });
+const inputFormatTradeSinglePriceFixed2 = swapPricePrecision.getCommonInputFormat('amount', { negative: true, append: '', percent: false });
const inputFormatPosiFeePercent = Object.freeze({ precision: 4, negative: true, append: '%' });
const inputFormatPosiFeeUnit = Object.freeze({ precision: 6, negative: true, append: '' });
const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' });
@@ -37,7 +35,7 @@ const observationRateTextFromPercent = function (value) {
const formatObservationRate = function (value) {
if (value === null || value === undefined || value === '') return '';
const number = Number(value);
- return Number.isFinite(number) ? number.toFixed(4) : value;
+ return Number.isFinite(number) ? number.toFixed(swapPricePrecision.getCommonPrecision('rate')) : value;
};
const swapPosiFeeCalc = Object.freeze({
normalizeFeeType(feeType) {
@@ -270,6 +268,12 @@ const vue = new Vue({
});
},
methods: {
+ getQuantityInputFormat(item) {
+ return swapPricePrecision.getCommonInputFormat(
+ 'quantity',
+ { append: '' },
+ item && item.UnderlyingInstrumentType);
+ },
getPosiPriceInputFormat(item, field) {
return swapPricePrecision.getInputFormat(
item && item.UnderlyingInstrumentType,
@@ -563,7 +567,7 @@ const vue = new Vue({
},
//变更名义本金(仅格式化,不反算数量)
changeStockEqvNotional() {
- this.trade.StockEqvNotional = otcformat.trading.StockEqvNotional(this.trade.StockEqvNotional);
+ this.trade.StockEqvNotional = swapPricePrecision.normalizeCommon('amount', this.trade.StockEqvNotional);
this.refreshPayTradingFeesByUnit();
//计算数量
// if (this.paySwapList.length > 0) {
@@ -651,7 +655,7 @@ const vue = new Vue({
// 守卫: 名义本金必须 round 到 2 位 → 对应历史 bug f873239a(缺 _.round); 外置到 swapCalc.calcStockEqvNotional
var deliveryPrice = this.roundStoragePrice(payItem, payItem.PosiGrossPrice, 'grossPrice');
var stockEqvNotional = SwapCalc.calcStockEqvNotional(deliveryPrice, national);//名义本金=期初价格*数量*乘数
- this.trade.StockEqvNotional = otcformat.trading.stockEqvNotional(stockEqvNotional);
+ this.trade.StockEqvNotional = swapPricePrecision.normalizeCommon('amount', stockEqvNotional);
payItem.PosiNotionalValue = this.trade.StockEqvNotional;
this.refreshPayTradingFeesByUnit();
}
diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeView.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeView.js
index 003ab189..c6ad4bac 100644
--- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeView.js
+++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapTradeView.js
@@ -9,6 +9,15 @@ function formatSwapPriceElements() {
});
}
+function formatSwapCommonElements() {
+ $('.js-swap-common').each(function () {
+ this.textContent = swapPricePrecision.formatCommon(
+ this.dataset.kind,
+ this.dataset.value,
+ this.dataset.instrumentType);
+ });
+}
+
function deletetrade(id) { //无效化
main.confirm(page.ConfirmInfo, function () {
$.ajax({
@@ -502,7 +511,7 @@ var vueDetails = new Vue({
if (swapIntervals && swapIntervals.length>0) {
that.SwapIntervalList = JSON.parse(swapIntervals);
that.SwapIntervalList.forEach((item, index) => {
- that.SwapIntervalList[index].Rate = otcformat.fixed4P(item.Rate);
+ that.SwapIntervalList[index].Rate = swapPricePrecision.formatCommon('rate', item.Rate);
// 如果结算日期为空,默认等于观察日期
if (!item.SettlementDate) {
that.SwapIntervalList[index].SettlementDate = item.Date;
@@ -553,6 +562,7 @@ function chk_onclick(obj) {
$(function () {
formatSwapPriceElements();
+ formatSwapCommonElements();
refreshEntryExit();
});
diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindLongShort.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindLongShort.js
index 509177bb..ed32fced 100644
--- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindLongShort.js
+++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindLongShort.js
@@ -1,6 +1,9 @@
//otcformat禁止千分位分组
window.otcformat.options.disableGrouping = true;
-const inputFormatEqvNotional = Object.freeze({ precision: 2, append: '', trimTailZeros: false });
+const inputFormatEqvNotional = swapPricePrecision.getCommonInputFormat('amount', { append: '' });
+const swapInstrumentType = (model.FlowEvents || []).find(item => item && item.UnderlyingInstrumentType)?.UnderlyingInstrumentType || model.UnderlyingInstrumentType || '';
+const formatSwapAmount = value => swapPricePrecision.normalizeCommon('amount', value);
+const formatSwapQuantity = value => swapPricePrecision.normalizeCommon('quantity', value, swapInstrumentType);
let dealDate = model.DealDate;
const vue = new Vue({
el: '#vueDiv',
@@ -21,6 +24,12 @@ const vue = new Vue({
this.dataFormat();
},
methods: {
+ formatAmount(value) {
+ return swapPricePrecision.formatCommon('amount', value);
+ },
+ formatQuantity(value) {
+ return swapPricePrecision.formatCommon('quantity', value, swapInstrumentType);
+ },
initDeal() {
this.interestList = model.FlowEvents.filter((item) => {
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 7 || item.InterestMode == 8 || item.InterestMode == 9;
@@ -30,27 +39,27 @@ const vue = new Vue({
});
},
dataFormat() {
- this.deal.NotionalValue = otcformat.fixed2(parseFloat(this.deal.NotionalValue));
- this.deal.PosiNotionalValue = otcformat.fixed2(parseFloat(this.deal.PosiNotionalValue));
- this.deal.NotionalQty = otcformat.trading.notional(this.deal.NotionalQty);
- this.deal.SwapCloseAmount = otcformat.trading.StockEqvNotional(this.deal.SwapCloseAmount);
- this.deal.PositionQty = otcformat.trading.notional(this.deal.PositionQty);
+ this.deal.NotionalValue = formatSwapAmount(this.deal.NotionalValue);
+ this.deal.PosiNotionalValue = formatSwapAmount(this.deal.PosiNotionalValue);
+ this.deal.NotionalQty = formatSwapQuantity(this.deal.NotionalQty);
+ this.deal.SwapCloseAmount = formatSwapAmount(this.deal.SwapCloseAmount);
+ this.deal.PositionQty = formatSwapQuantity(this.deal.PositionQty);
this.interestList.forEach(x => {
- //x.Principal = otcformat.trading.StockEqvNotional(x.Principal);
+ //x.Principal = formatSwapAmount(x.Principal);
//x.Rate = otcformat.fixed6(x.Rate);
- x.InterestAmount = otcformat.trading.StockEqvNotional(x.InterestAmount);
- x.InterestClosePnL = otcformat.trading.StockEqvNotional(x.InterestClosePnL);
+ x.InterestAmount = formatSwapAmount(x.InterestAmount);
+ x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
//x.InterestStartDate = x.InterestStartDate ? x.InterestStartDate.substr(0, 10) : "";
//x.InterestEndDate = x.InterestEndDate ? x.InterestEndDate.substr(0, 10) : "";
});
this.marginList.forEach(x => {
- x.InterestAmount = otcformat.trading.StockEqvNotional(x.InterestAmount);
- x.InterestClosePnL = otcformat.trading.StockEqvNotional(x.InterestClosePnL);
+ x.InterestAmount = formatSwapAmount(x.InterestAmount);
+ x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
});
},
changeInterestAmount(item) {//修改利息金额
let interestRatio = item.InterestDirection == 1 ? 1 : -1;
- item.InterestClosePnL = otcformat.trading.StockEqvNotional(parseFloat(item.InterestAmount) * interestRatio+ parseFloat(item.InterestFee));
+ item.InterestClosePnL = formatSwapAmount(parseFloat(item.InterestAmount) * interestRatio+ parseFloat(item.InterestFee));
this.calcCloseAmount();
},
calcCloseAmount() {//计算平仓总额=浮动收取+利息收取-浮动支付-利息支付
@@ -73,10 +82,10 @@ const vue = new Vue({
thisObj.deal.SwapRealizedPnL = parseFloat(thisObj.deal.SwapRealizedPnL) + interestAmount;
thisObj.deal.SwapMarginAmount = parseFloat(thisObj.deal.SwapMarginAmount) + parseFloat(x.InterestPrincipal) * interestRatio;
});
- thisObj.deal.SwapCloseAmount = otcformat.trading.StockEqvNotional(thisObj.deal.SwapCloseAmount);
- thisObj.deal.SwapRealizedPnL = otcformat.trading.StockEqvNotional(thisObj.deal.SwapRealizedPnL);
- thisObj.deal.SwapMarginRebatePnl = otcformat.trading.StockEqvNotional(thisObj.deal.SwapMarginRebatePnl);
- thisObj.deal.SwapMarginAmount = otcformat.trading.StockEqvNotional(thisObj.deal.SwapMarginAmount);
+ thisObj.deal.SwapCloseAmount = formatSwapAmount(thisObj.deal.SwapCloseAmount);
+ thisObj.deal.SwapRealizedPnL = formatSwapAmount(thisObj.deal.SwapRealizedPnL);
+ thisObj.deal.SwapMarginRebatePnl = formatSwapAmount(thisObj.deal.SwapMarginRebatePnl);
+ thisObj.deal.SwapMarginAmount = formatSwapAmount(thisObj.deal.SwapMarginAmount);
},
closeTrade() {//平仓
var thisObj = this;
diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js
index 27e031a7..1c038aef 100644
--- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js
+++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js
@@ -3,9 +3,11 @@ window.otcformat.options.disableGrouping = true;
const inputFormatSwapRate = Object.freeze({ precision: 4, append: '%', trimTailZeros: false });
const inputFormatTradePrice = Object.freeze({ precision: otcformat.trading.tradePrice.precision, append: '' });
-const inputFormatTradeAmount = Object.freeze({ precision: otcformat.trading.notional.precision, append: '' });
-const inputFormatEqvNotional = Object.freeze({ precision: 2, append: '', negative: true, trimTailZeros: false });
-const inputFormatCloseAmount = Object.freeze({ precision: 2, append: '', negative: true, trimTailZeros: false });
+const inputFormatEqvNotional = swapPricePrecision.getCommonInputFormat('amount', { append: '', negative: true });
+const inputFormatCloseAmount = swapPricePrecision.getCommonInputFormat('amount', { append: '', negative: true });
+const swapInstrumentType = (model.FlowEvents || []).find(item => item && item.UnderlyingInstrumentType)?.UnderlyingInstrumentType || model.UnderlyingInstrumentType || '';
+const formatSwapAmount = value => swapPricePrecision.normalizeCommon('amount', value);
+const formatSwapQuantity = value => swapPricePrecision.normalizeCommon('quantity', value, swapInstrumentType);
const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' });
const inputFormatMarginRateNoPercent = Object.freeze({ precision: otcformat.trading.umpriceP.precision, append: '', percent: true });
const inputFormatSwapDeliveryPrice = Object.freeze({ precision: 9, append: '', negative: true });
@@ -22,7 +24,7 @@ const swapPosiFeeCalc = {
const tradingFee = normalizedFeeType === consPosiFeeType.Unit
? normalizedFeeUnit * normalizedCloseQty
: normalizedFeeUnit / 100 * normalizedCloseNotionalValue;
- return otcformat.trading.StockEqvNotional(_.round(tradingFee, 2));
+ return formatSwapAmount(_.round(tradingFee, 2));
},
calcAllocatedTradingFee(totalFee, feeType, feeUnit, closeNotionalValue, closeQty, notionalValue, notionalQty) {
const normalizedFeeUnit = Number(feeUnit) || 0;
@@ -37,7 +39,7 @@ const swapPosiFeeCalc = {
return null;
}
- return otcformat.trading.StockEqvNotional(_.round((Number(totalFee) || 0) * closeBase / originalBase, 2));
+ return formatSwapAmount(_.round((Number(totalFee) || 0) * closeBase / originalBase, 2));
},
calcTradingFeePending(beforeCloseFee, feeType, feeUnit, closeNotionalValue, closeQty, notionalValue, notionalQty, closePercent) {
const allocatedFee = this.calcAllocatedTradingFee(
@@ -79,6 +81,15 @@ const vue = new Vue({
this.setUnwindDate();
},
methods: {
+ formatAmount(value) {
+ return swapPricePrecision.formatCommon('amount', value);
+ },
+ formatQuantity(value) {
+ return swapPricePrecision.formatCommon('quantity', value, swapInstrumentType);
+ },
+ getQuantityInputFormat() {
+ return swapPricePrecision.getCommonInputFormat('quantity', { append: '' }, swapInstrumentType);
+ },
getDeliveryPriceInputFormat() {
return swapPricePrecision.getInputFormat(
this.floatPosition && this.floatPosition.UnderlyingInstrumentType,
@@ -124,37 +135,37 @@ const vue = new Vue({
return pricef;
},
dataFormat() {
- this.deal.NotionalValue = otcformat.fixed2(parseFloat(this.deal.NotionalValue));
- this.deal.PosiNotionalValue = otcformat.fixed2(parseFloat(this.deal.PosiNotionalValue));
- this.deal.CloseNotionalValue = otcformat.fixed2(parseFloat(this.deal.CloseNotionalValue));
- this.deal.NotionalQty = otcformat.trading.notional(this.deal.NotionalQty);
- this.deal.PositionQty2 = otcformat.trading.notional(this.deal.PositionQty);
- this.floatPosition.Quantity = otcformat.trading.notional(this.floatPosition.Quantity);
- this.floatPosition.PositionQty = otcformat.trading.notional(this.floatPosition.PositionQty);
- this.deal.CloseQty = otcformat.trading.notional(this.deal.CloseQty);
+ this.deal.NotionalValue = formatSwapAmount(this.deal.NotionalValue);
+ this.deal.PosiNotionalValue = formatSwapAmount(this.deal.PosiNotionalValue);
+ this.deal.CloseNotionalValue = formatSwapAmount(this.deal.CloseNotionalValue);
+ this.deal.NotionalQty = formatSwapQuantity(this.deal.NotionalQty);
+ this.deal.PositionQty2 = formatSwapQuantity(this.deal.PositionQty);
+ this.floatPosition.Quantity = formatSwapQuantity(this.floatPosition.Quantity);
+ this.floatPosition.PositionQty = formatSwapQuantity(this.floatPosition.PositionQty);
+ this.deal.CloseQty = formatSwapQuantity(this.deal.CloseQty);
this.deal.ClosePercent = otcformat.fixed6(this.deal.ClosePercent);
- this.deal.SwapCloseAmount = otcformat.trading.StockEqvNotional(this.deal.SwapCloseAmount);
+ this.deal.SwapCloseAmount = formatSwapAmount(this.deal.SwapCloseAmount);
//this.floatPosition.PosiNetPrice = otcformat.trading.tradeSinglePrice(this.floatPosition.PosiNetPrice);
//this.floatPosition.PosiGrossPrice = otcformat.trading.tradeSinglePrice(this.floatPosition.PosiGrossPrice);
this.floatPosition.TradingAmountAvg = swapPricePrecision.roundForSubmit(
this.floatPosition.TradingAmountAvg,
this.floatPosition.UnderlyingInstrumentType,
'grossPrice');
- this.floatPosition.TradingFee = otcformat.trading.StockEqvNotional(this.floatPosition.TradingFee);
- this.floatPosition.TradingFeePending = otcformat.trading.StockEqvNotional(this.floatPosition.TradingFeePending);
- this.floatPosition.DividendIn = parseFloat(this.floatPosition.DividendIn).toFixed(2);
- this.floatPosition.MarkClosePnl = otcformat.trading.StockEqvNotional(this.floatPosition.MarkClosePnl);
+ this.floatPosition.TradingFee = formatSwapAmount(this.floatPosition.TradingFee);
+ this.floatPosition.TradingFeePending = formatSwapAmount(this.floatPosition.TradingFeePending);
+ this.floatPosition.DividendIn = formatSwapAmount(this.floatPosition.DividendIn);
+ this.floatPosition.MarkClosePnl = formatSwapAmount(this.floatPosition.MarkClosePnl);
this.interestList.forEach(x => {
- //x.Principal = otcformat.trading.StockEqvNotional(x.Principal);
+ //x.Principal = formatSwapAmount(x.Principal);
//x.Rate = otcformat.fixed6(x.Rate);
- x.InterestAmount = otcformat.trading.StockEqvNotional(x.InterestAmount);
- x.InterestClosePnL = otcformat.trading.StockEqvNotional(x.InterestClosePnL);
+ x.InterestAmount = formatSwapAmount(x.InterestAmount);
+ x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
//x.InterestStartDate = x.InterestStartDate ? x.InterestStartDate.substr(0, 10) : "";
//x.InterestEndDate = x.InterestEndDate ? x.InterestEndDate.substr(0, 10) : "";
});
this.marginList.forEach(x => {
- x.InterestAmount = otcformat.trading.StockEqvNotional(x.InterestAmount);
- x.InterestClosePnL = otcformat.trading.StockEqvNotional(x.InterestClosePnL);
+ x.InterestAmount = formatSwapAmount(x.InterestAmount);
+ x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
});
},
setValueDate(e) {//修改平仓日期
@@ -183,7 +194,7 @@ const vue = new Vue({
changeCloseMethod() {//修改平仓类型
if (this.deal.CloseMethod == 1) {
this.deal.ClosePercent = this.oriClosePercent;
- this.deal.CloseNotionalValue = otcformat.trading.StockEqvNotional(parseFloat(this.deal.PosiNotionalValue));
+ this.deal.CloseNotionalValue = formatSwapAmount(parseFloat(this.deal.PosiNotionalValue));
this.deal.CloseQty = this.deal.PositionQty;
} else {
// ClosePercent 是占期初口径(A),需除以 oriClosePercent 转占剩余(B) 再乘剩余数量
@@ -213,15 +224,7 @@ const vue = new Vue({
this.deal.ClosePercent);
},
refreshTradingFeeByUnit() {
- const allocatedFee = swapPosiFeeCalc.calcAllocatedTradingFee(
- this.floatPosition.BeforeCloseFee,
- this.floatPosition.PosiFeeType,
- this.floatPosition.PosiTradingFeeUnit,
- this.deal.CloseNotionalValue,
- this.deal.CloseQty,
- this.deal.NotionalValue,
- this.deal.NotionalQty);
- this.floatPosition.TradingFee = allocatedFee !== null ? allocatedFee : swapPosiFeeCalc.calcTradingFee(
+ this.floatPosition.TradingFee = swapPosiFeeCalc.calcTradingFee(
this.floatPosition.PosiFeeType,
this.floatPosition.PosiTradingFeeUnit,
this.deal.CloseNotionalValue,
@@ -241,7 +244,7 @@ const vue = new Vue({
this.deal.CloseMethod = 2;
}
// 占期初口径:平仓名义本金 = 平仓比例 × 期初名义本金(NotionalValue)
- this.deal.CloseNotionalValue = otcformat.trading.StockEqvNotional(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.NotionalValue));
+ this.deal.CloseNotionalValue = formatSwapAmount(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.NotionalValue));
this.calcTradingFeePending();
this.refreshTradingFeeByUnit();
this.getInterestList();
@@ -255,7 +258,7 @@ const vue = new Vue({
}
this.deal.CloseQty = this.calcCloseQtyByPercent(this.deal.ClosePercent);
// 占期初口径:平仓名义本金 = 平仓比例 × 期初名义本金(NotionalValue)
- this.deal.CloseNotionalValue = otcformat.trading.StockEqvNotional(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.NotionalValue));
+ this.deal.CloseNotionalValue = formatSwapAmount(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.NotionalValue));
if (parseFloat(this.deal.ClosePercent) == parseFloat(this.oriClosePercent)) {
this.deal.CloseMethod = 1;
} else {
@@ -309,7 +312,7 @@ const vue = new Vue({
let deliveryPrice = thisObj.getStorageDeliveryPrice();
thisObj.floatPosition.MarkClosePnl = Math.round(thisObj.deal.CloseQty * (deliveryPrice - thisObj.initPosiNetPrice) * floatRatio * longRatio * 10000) / 10000;
thisObj.floatPosition.MarkClosePnl = Number(thisObj.floatPosition.MarkClosePnl.toFixed(2));//MarkClosePnl 纯盯市不要计算交易费用和分红
- thisObj.floatPosition.MarkClosePnl = otcformat.trading.StockEqvNotional(thisObj.floatPosition.MarkClosePnl);
+ thisObj.floatPosition.MarkClosePnl = formatSwapAmount(thisObj.floatPosition.MarkClosePnl);
thisObj.floatPosition.FloatPnlSum = (parseFloat(thisObj.floatPosition.MarkClosePnl) + TradingFee + TradingFeePending + parseFloat(thisObj.floatPosition.DividendIn)).toFixed(2);
thisObj.calcCloseAmount();
@@ -319,12 +322,12 @@ const vue = new Vue({
},
changeInterestAmount(item) {//修改利息金额
let interestRatio = item.InterestDirection == 1 ? 1 : -1;
- item.InterestClosePnL = otcformat.trading.StockEqvNotional(parseFloat(item.InterestAmount) * interestRatio + parseFloat(item.InterestFee) * interestRatio);
+ item.InterestClosePnL = formatSwapAmount(parseFloat(item.InterestAmount) * interestRatio + parseFloat(item.InterestFee) * interestRatio);
this.calcCloseAmount();
},
//calcClosePnL() {//计算浮动端平仓盈亏
// let pnl = parseFloat(this.floatPosition.ClosePnL) - parseFloat(this.floatPosition.TradingFee);
- // this.floatPosition.ClosePnL = otcformat.trading.StockEqvNotional(pnl);
+ // this.floatPosition.ClosePnL = formatSwapAmount(pnl);
// this.calcCloseAmount();
//},
calcCloseAmount() {//计算平仓总额=浮动收取+利息收取-浮动支付-利息支付
@@ -361,10 +364,10 @@ const vue = new Vue({
});
thisObj.deal.SwapRealizedPnL = Number(thisObj.deal.SwapRealizedPnL.toFixed(2));
thisObj.deal.SwapCloseAmount = Number(thisObj.deal.SwapCloseAmount.toFixed(2));
- thisObj.deal.SwapCloseAmount = otcformat.trading.StockEqvNotional(thisObj.deal.SwapCloseAmount);
- thisObj.deal.SwapRealizedPnL = otcformat.trading.StockEqvNotional(thisObj.deal.SwapRealizedPnL);
- thisObj.deal.SwapMarginRebatePnl = otcformat.trading.StockEqvNotional(thisObj.deal.SwapMarginRebatePnl);
- thisObj.deal.SwapMarginAmount = otcformat.trading.StockEqvNotional(thisObj.deal.SwapMarginAmount);
+ thisObj.deal.SwapCloseAmount = formatSwapAmount(thisObj.deal.SwapCloseAmount);
+ thisObj.deal.SwapRealizedPnL = formatSwapAmount(thisObj.deal.SwapRealizedPnL);
+ thisObj.deal.SwapMarginRebatePnl = formatSwapAmount(thisObj.deal.SwapMarginRebatePnl);
+ thisObj.deal.SwapMarginAmount = formatSwapAmount(thisObj.deal.SwapMarginAmount);
},
getInterestList() {//根据平仓日期获取利息腿信息
var thisObj = this;
@@ -384,27 +387,15 @@ const vue = new Vue({
},
getDivindIn() {
var thisObj = this;
- let ratio = this.floatPosition.PositionType == 1 ? 1 : -1;
- let floatRatio = this.floatPosition.PayDirection == 1 ? 1 : -1;
- var postData = { startDate: thisObj.TradeStartDate, endDate: thisObj.deal.UnwindDate, underlyingCode: thisObj.floatPosition.UnderlyingCode, tradeId: thisObj.deal.SwapTradeId, unwindDate: thisObj.deal.UnwindDate }
- main.post("/BondPayment/GetBondPayMentInterest", postData, { async: false }).done(function (resp) {
- let consumedDividend = Math.abs(parseFloat(resp.obj.consumedDividend ?? 0)) * ratio * floatRatio;//已实现的
- let totalDividend = parseFloat(thisObj.deal.NotionalQty) * resp.obj.totalInterest * ratio * floatRatio;
- let remainDividend = totalDividend - consumedDividend;
- let dividendIn = 0;
- if (remainDividend != 0) {
- dividendIn = parseFloat(thisObj.deal.CloseQty) / parseFloat(thisObj.floatPosition.Quantity) * remainDividend
- }
- // 这里计算已经分红的利息,从totalInterest里扣除
-
- // 互换是全量消费,consumedDividend>0 表示分红已被当天互换消费,归0
- // 现在可能做了纯分红的互换结算,所以不能直接归0
- thisObj.floatPosition.DividendIn = parseFloat(dividendIn.toFixed(2));
- var posiQty = parseFloat(thisObj.floatPosition.Quantity) - parseFloat(thisObj.deal.CloseQty);
- thisObj.floatPosition.DividendPending = parseFloat((posiQty * resp.obj.totalInterest * ratio * floatRatio).toFixed(2));
- thisObj.calcFloatClosePnl();
- thisObj.dataFormat();
- });
+ // 方案C:分红改由后端 InitUnwind 读 EOD PosiDividendSum 填入 floatPosition.DividendIn 与 DividendPending
+ // (单一可信源)。前端不再调用 GetBondPayMentInterest 自算——消除"期初持仓×totalInterest"对已平仓
+ // 部分的重复计入(GLMS-20260105-0004 平仓前部分平仓40%后,再平仓时分红误显 -36,160,应为 0)。
+ // ⚠ floatPosition.DividendIn / DividendPending 均保持后端返回值不动,前端不得覆盖:
+ // - DividendIn(本次落袋)、DividendPending(待结算存量=PosiDividendSum 全量口径)。
+ // - 互换页 DividendPending 保持 0(互换=全量结清,结清后待结算归0),见 incomeSwapTrade.js。
+ // - 历史:曾硬编码 DividendPending=0,对 PosiDividendSum≠0 的部分平仓会落库错误的 0(回归)。
+ thisObj.calcFloatClosePnl();
+ thisObj.dataFormat();
},
closeTrade() {//平仓
var thisObj = this;
diff --git a/YLErpWeb/wwwroot/Scripts/fast/fastVue.base.js b/YLErpWeb/wwwroot/Scripts/fast/fastVue.base.js
index b804e5a1..1b9dcc49 100644
--- a/YLErpWeb/wwwroot/Scripts/fast/fastVue.base.js
+++ b/YLErpWeb/wwwroot/Scripts/fast/fastVue.base.js
@@ -297,6 +297,8 @@
!options.append && (options.append = '');
let precision = parseInt(options.precision) || 0;
options.precision = precision < 0 ? 0 : precision;
+ let integerDigits = parseInt(options.integerDigits) || 0;
+ options.integerDigits = integerDigits > 0 ? integerDigits : 0;
options.negative = !!options.negative;
_options = options;
setValue(getValue());
@@ -336,6 +338,35 @@
return this.value.substring(0, this.selectionStart).indexOf('.') < 0 && this.value.substring(this.selectionEnd, this.value.length).indexOf('.') < 0;
}
+ function checkIntegerDigitsInput() {
+ if (!_options.integerDigits) return true;
+ let valueEnd = this.value.length;
+ if (_options.append && this.value.endsWith(_options.append)) valueEnd -= _options.append.length;
+ const dotIndex = this.value.indexOf('.');
+ const integerEnd = dotIndex < 0 ? valueEnd : dotIndex;
+ if (this.selectionStart > integerEnd) return true;
+ const selectionEnd = Math.min(this.selectionEnd, integerEnd);
+ const integerText = this.value.substring(0, this.selectionStart) + this.value.substring(selectionEnd, integerEnd);
+ return integerText.replace(/\D/g, '').length < _options.integerDigits;
+ }
+
+ function limitIntegerDigits(value) {
+ if (!_options.integerDigits) return value;
+ let digits = 0;
+ let hasDot = false;
+ let result = '';
+ for (let index = 0; index < value.length; index++) {
+ const ch = value.charAt(index);
+ if (ch === '.' || ch === '。') hasDot = true;
+ if (ch >= '0' && ch <= '9' && !hasDot) {
+ if (digits >= _options.integerDigits) continue;
+ digits++;
+ }
+ result += ch;
+ }
+ return result;
+ }
+
function __keyHandle(event) {
if (!event) return false;
@@ -385,6 +416,7 @@
if (charCode < 48 || charCode > 57 && (charCode < 96 || charCode > 105)) {
return false;
}
+ if (!checkIntegerDigitsInput.call(this)) return false;
if (this.selectionStart === 0) {
return !this.value || this.value.charAt(this.selectionEnd) !== '-';
}
@@ -445,6 +477,7 @@
// 粘贴进来的是显示值(可能带 %/‱ 后缀),先按 __change 同款口径解析为模型值再回显,
// 避免 setValue 把显示值再乘以 100/10000(#EQD-5914 债券价格粘贴 ×100)
let f = '';
+ this.value = limitIntegerDigits(this.value);
if (this.value && this.value !== _options.append) {
f = parseFloat(this.value.replaceAll(",", "")) || 0;
if (_options.append === '%' || _options.percent == true) f /= 100;
@@ -455,6 +488,7 @@
if (_chnInput >= 0) {
__onChineseInput.call(this, _chnInput);
}
+ this.value = limitIntegerDigits(this.value);
if (!_options.append || !this.value) return;
let appended = true;
if (this.value !== _options.append) {
diff --git a/YLErpWeb/wwwroot/Statics/bundles/bundle.js b/YLErpWeb/wwwroot/Statics/bundles/bundle.js
index d3baa7d7..2fd4e72b 100644
--- a/YLErpWeb/wwwroot/Statics/bundles/bundle.js
+++ b/YLErpWeb/wwwroot/Statics/bundles/bundle.js
@@ -14354,6 +14354,8 @@ $.fn.selectpicker.Constructor.DEFAULTS = Object.assign($.fn.selectpicker.Constru
!options.append && (options.append = '');
let precision = parseInt(options.precision) || 0;
options.precision = precision < 0 ? 0 : precision;
+ let integerDigits = parseInt(options.integerDigits) || 0;
+ options.integerDigits = integerDigits > 0 ? integerDigits : 0;
options.negative = !!options.negative;
_options = options;
setValue(getValue());
@@ -14393,6 +14395,35 @@ $.fn.selectpicker.Constructor.DEFAULTS = Object.assign($.fn.selectpicker.Constru
return this.value.substring(0, this.selectionStart).indexOf('.') < 0 && this.value.substring(this.selectionEnd, this.value.length).indexOf('.') < 0;
}
+ function checkIntegerDigitsInput() {
+ if (!_options.integerDigits) return true;
+ let valueEnd = this.value.length;
+ if (_options.append && this.value.endsWith(_options.append)) valueEnd -= _options.append.length;
+ const dotIndex = this.value.indexOf('.');
+ const integerEnd = dotIndex < 0 ? valueEnd : dotIndex;
+ if (this.selectionStart > integerEnd) return true;
+ const selectionEnd = Math.min(this.selectionEnd, integerEnd);
+ const integerText = this.value.substring(0, this.selectionStart) + this.value.substring(selectionEnd, integerEnd);
+ return integerText.replace(/\D/g, '').length < _options.integerDigits;
+ }
+
+ function limitIntegerDigits(value) {
+ if (!_options.integerDigits) return value;
+ let digits = 0;
+ let hasDot = false;
+ let result = '';
+ for (let index = 0; index < value.length; index++) {
+ const ch = value.charAt(index);
+ if (ch === '.' || ch === '。') hasDot = true;
+ if (ch >= '0' && ch <= '9' && !hasDot) {
+ if (digits >= _options.integerDigits) continue;
+ digits++;
+ }
+ result += ch;
+ }
+ return result;
+ }
+
function __keyHandle(event) {
if (!event) return false;
@@ -14442,6 +14473,7 @@ $.fn.selectpicker.Constructor.DEFAULTS = Object.assign($.fn.selectpicker.Constru
if (charCode < 48 || charCode > 57 && (charCode < 96 || charCode > 105)) {
return false;
}
+ if (!checkIntegerDigitsInput.call(this)) return false;
if (this.selectionStart === 0) {
return !this.value || this.value.charAt(this.selectionEnd) !== '-';
}
@@ -14502,6 +14534,7 @@ $.fn.selectpicker.Constructor.DEFAULTS = Object.assign($.fn.selectpicker.Constru
// 粘贴进来的是显示值(可能带 %/‱ 后缀),先按 __change 同款口径解析为模型值再回显,
// 避免 setValue 把显示值再乘以 100/10000(#EQD-5914 债券价格粘贴 ×100)
let f = '';
+ this.value = limitIntegerDigits(this.value);
if (this.value && this.value !== _options.append) {
f = parseFloat(this.value.replaceAll(",", "")) || 0;
if (_options.append === '%' || _options.percent == true) f /= 100;
@@ -14512,6 +14545,7 @@ $.fn.selectpicker.Constructor.DEFAULTS = Object.assign($.fn.selectpicker.Constru
if (_chnInput >= 0) {
__onChineseInput.call(this, _chnInput);
}
+ this.value = limitIntegerDigits(this.value);
if (!_options.append || !this.value) return;
let appended = true;
if (this.value !== _options.append) {
diff --git a/项目文档/互换计算口径一致性_残留问题与治理路线.md b/项目文档/互换计算口径一致性_残留问题与治理路线.md
new file mode 100644
index 00000000..25529faf
--- /dev/null
+++ b/项目文档/互换计算口径一致性_残留问题与治理路线.md
@@ -0,0 +1,230 @@
+# 互换(TRS)计算口径一致性:残留问题与治理路线
+
+> 配套文档:`互换分红损益字段语义与重复计算分析.md`(2026-06,根因分析)
+> 本次分析目的:在最新提交 `e3c473ba`(分红收益改由 EOD 单一可信源)之后,
+> 复核"分散计算 / 前后端重复算 / 精度口径不统一"这类 BUG 是否仍在其它地方存在,
+> 并给出"如何避免 + 后续逐渐解决"的路线。
+> 分析日期:2026-08-06
+
+---
+
+## 一、这类 BUG 的本质(统一定义)
+
+最新修复的"分红收益误显 -36,160",根因不是某一个 if 写错,而是一种**结构性缺陷**:
+
+> **金融计算缺乏「单一可信源 / 统一口径」** —— 同一个业务量在
+> **前端 JS / 后端 C# / EOD 批处理** 三条路径里被**各自重算**,
+> 且价格基准、方向符号、精度位数都零散硬编码,导致:
+> 1. 前后端(或同一功能的两页)对同一量算出**不同值**;
+> 2. 分红等分量被**重复计入**(MarkClosePnl 含分红 → 汇总翻倍);
+> 3. 金额/价格**精度不统一**,相邻环节差 1~2 位小数。
+
+它表现为 3 个子模式:
+
+| 子模式 | 例子 | 状态 |
+|--------|------|------|
+| ① 分散计算、无单一可信源 | 分红前端自算(`getDivindIn`)vs EOD `PosiDividendSum` | **最新提交已根治(方案C)** |
+| ② 字段语义重叠→重复累加 | `MarkClosePnl` 含分红,汇总 `RealizedPnl` 翻倍 | 自动路径已按方向A改造;**前端两页仍有口径差** |
+| ③ 硬编码精度/魔法数字 | `Math.Round(...,4/10)`、`ToString("F10")` vs `ConsGlobal.*` | **大量残留** |
+
+---
+
+## 二、最新提交(e3c473ba)做了什么(已修复)
+
+- 把"浮动端平仓盈亏·分红 `DividendIn`"从**前端 `getDivindIn` 自算**(`期初持仓×totalInterest`)
+ 改为**后端 `SwapDealService.GetPreEodDividendSum` 读 EOD `PosiDividendSum`**(单一可信源)。
+- 前端 `unwindSwapTrade.js` / `incomeSwapTrade.js` 的 `getDivindIn` 删除自算逻辑,只保留后端值。
+- `GetBondPayMentInterest` 标记废弃(保留接口供历史调用)。
+- 配套绿灯验收测试 `GLMS20260105PartialCloseDividendBugTest.cs`。
+
+**结论**:子模式①(分红分散计算)在"预览页展示"这条链路已根治。但**②④③ 仍在**。
+
+---
+
+## 三、同类问题在别处是否还存在(带 file:line 证据)
+
+### A. 前端两页 `MarkClosePnl` 口径不一致 —— 最危险、最具体的残留 ⚠️
+
+> **2026-08-06 修正**:初版称"平仓页用净价、互换页用全价"——经核对代码**不准确**。
+> 实际两页都用**全价**:`unwindSwapTrade.js:111` 把 `initPosiNetPrice` 绑到 `PosiGrossPrice`(变量名有误导性),
+> `incomeSwapTrade.js:103/207` 用 `initPosiGrossPrice`(也是全价)。真正差异见下表与 A.1。
+
+同一字段 `MarkClosePnl`(盯市盈亏),实现对照(来源:团队金标准 `YLErpDAL/Helpers/FrontendCalcReference.cs`):
+
+| 实现 | 位置 | 公式 | 价格基准 | 方向 | 数量基准 |
+|------|------|------|----------|------|----------|
+| 平仓页 | `unwindSwapTrade.js:313` | `CloseQty × (价 − 期初全价) × floatRatio × longRatio` | 全价(变量名骗人) | 含 `longRatio` | `CloseQty` |
+| 互换页 | `incomeSwapTrade.js:207` | `positionAmount × (价 − 期初全价) × floatRatio` | 全价 | **无 `longRatio`** | `positionAmount=PositionQty×ContractSize` |
+| 后端 | `SwapDealService.cs:1613` | `(价 − PosiGrossPrice) × unwindQty × floatRatio × longRatio` | 全价 | 含 `longRatio` | `unwindQty` |
+| 金标准 | `FrontendCalcReference.CalcUnwind:44` / `CalcIncome:107` | 同上(`CalcIncome` 注释明示"无 longRatio") | 全价 | unwind 有 / income 无 | — |
+
+**真正的风险点(已用红测试证明,见 A.1):**
+1. **互换页缺 `longRatio`(多空方向)** —— 这是实打实的 bug。一旦 `PositionType=2`(空头),
+ unwind 页与后端会乘 `−1`,而 income 页不乘 → **同一笔空头两页算出相反符号**。
+ 债券 TRS(国联民生等)普遍支持空头,并非边缘场景。
+2. **数量基准不同**(设计使然,非 bug):平仓页用 `CloseQty`(本次平仓量),互换页用 `PositionQty×ContractSize`
+ (剩余持仓量,因互换是全额置换剩余持仓)。语义不同但各自自洽,需业务确认是否期望一致。
+3. **中间取整**:income 无 `×10000/10000` 步骤(`FrontendCalcReference` 注释明示),但末端都保留 2 位,
+ 干净输入下等价(parity 已证明),属低风险。
+
+**共享的 `swapCalc.calcMarkClosePnl`(`swapCalc.js:113`,全价+`longRatio`+`scale`)已存在,
+但两个生产页面都没调用**——仍是"单一可信源已有却不采纳"。
+
+**会进库吗?** 会。手动平仓/互换的 `MarkClosePnl` 由前端算好传入,后端**直接存库不重算**
+(`SwapDealService:1500/1991` 的 `ValidateFrontendPnL` 仅"只读告警、不阻断")。
+因此 income 页空头符号错误会直接落到 `swap_flow_event.MarkClosePnl`,并带偏 `FloatPnlSum`/`SwapRealizedPnL`。
+
+### A.1 为什么一直没暴露?—— 红测试证据(2026-08-06 补)
+
+三层叠加导致这个 bug 长期潜伏:
+1. **校验同源、永远自洽**:`ValidateFrontendPnL` 用 `FrontendCalcReference.CalcIncome` 重算比对,
+ 而 `CalcIncome` 本身就没 `longRatio`,所以"被校验的前端"和"校验用的公式"完全一致,永远不告警。
+2. **特征化测试全是多头**:`FrontendCalcCharacterizationTest` 的 income 场景 `FC_006~009`
+ **全部 `PositionType=1`(多头)**;唯一空头场景 `FC_005` 是 unwind。income 的空头分支从未被触发。
+3. **告警不阻断** + 生产数据里"空头做互换"相对少见,进一步降低暴露概率。
+
+**已落地红测试(证明 bug 真实存在 + 证明改动可修复):**
+- `YLErpWeb/fe-tests/markClosePnlShortConsistency.test.js`(jest,与 `parity.test.js` 同风格)
+- `YLErpWeb/fe-tests/_proof_short_income.js`(纯 Node 可跑,无需依赖)
+- 运行结果:多头场景两页一致(PASS,掩盖了问题);**空头场景 `unwind=−300` / `income=+300`(FAIL,符号相反)**;
+ 给 income 补 `longRatio` 后 `income=−300` 与 unwind 一致(PASS)。
+ → 既证明"当前代码对空头不一致(有问题)",也证明"给 income 补 `longRatio` 即可修复"。
+
+### B. 后端硬编码精度魔法数字 —— 与集中常量冲突/不绑定的残留 ⚠️
+
+集中常量(`Framework/YLErp.Core/ConsGlobal.cs`):`PriceRound=11`、`SwapDeliveryPriceRound=9`、
+`MoneyRound=2`;模块内 `InterestCalculationPrecision=12`、`EodInterestStoragePrecision=12`。
+
+残留的裸数字(不引用上述常量):
+
+| 位置 | 写法 | 应参照 | 问题 |
+|------|------|--------|------|
+| `SwapFlowService.cs:116-118` | `Math.Round(price, 4)`(ClosePrice/SettlePrice/ReferencePrice) | `PriceRound=11` / `SwapDeliveryPriceRound=9` | 价格存储 4 位 vs 全系统 9~11 位,**口径不一致** |
+| `SwapDealService.cs:1543` | `unwindPriceFee.ToString("F10")` | — | 硬编码 10 位 |
+| `SwapTradeAutoService.cs:458/460` | `Math.Round(..., 10)`(净均价) | `PriceRound=11` | 差 1 位 |
+| `SwapTradeAutoService.cs:788/1409` | `Math.Round(..., 4)`(费用/平仓费) | `MoneyRound=2` | **费用 4 位 vs 全系统金额 2 位,错配** |
+| 分红链路多处 `Math.Round(...,2)`:`SwapEodPositionService.cs:1647/1711/1713/2003`、`SwapDealService.cs:1658/1882/1883` | 裸 `2` | `ConsGlobal.MoneyRound` | 目前恰等于 2,但属硬编码,`MoneyRound` 一旦配置化即失真 |
+
+说明:第 4 行(费用 4 位 vs 金额 2 位)是**真实精度错配**,不是巧合一致;其余价格/净均价是"差 1 位"的隐患。
+
+### C. 交叉校验基础设施已建,但未"落地到生产" —— 治理杠杆闲置
+
+- `swapCalc.js` 的 `calcUnwind` / `calcIncome`(`:135`/`:170`)是**"参考规格,未接入生产代码"**——
+ 注释明确写着生产 Vue 只调用 4 个叶子函数,聚合逻辑仍是各页内联。
+- `fe-tests/parity.test.js` + `swapCalc.test.js` 已能冻结 8 个 FC 场景,但只覆盖已迁移的 4 个叶子函数。
+- 这正是"避免反复打补丁"的关键设施,**却没把生产聚合逻辑迁过去**。
+
+### D. 横向同类风险(建议扫描,本次未深入)
+
+互换之外的 期货 / 期权 / 定价引擎(greeks)/ 估值报告 等模块,
+很可能也存在"前端重算 + 后端重算""硬编码精度"的同构问题,需专项扫描。
+
+---
+
+## 四、如何避免(规范 / 治理层)
+
+1. **单一可信源原则(最高优先级)**
+ - 每个金融量只允许**一个权威计算点**:分红→EOD `PosiDividendSum`;盯市→`swapCalc.calcMarkClosePnl`;
+ 金额聚合→`swapCalc.calcFloatPnlSum` / `calcUnwind` / `calcIncome`。
+ - 前端**只展示、不重算**;手动平仓/互换落库时由后端**重算**而非信任前端传值。
+2. **精度集中化(红线)**
+ - 任何 `Math.Round` / `ToString("F")` 必须引用 `ConsGlobal.*` 或 `GetStorageDeliveryPriceRound(underlyingInstrumentType, code)`,
+ **禁止裸数字**。代码评审把"裸精度数字"列为 blocking 项。
+3. **字段语义单一职责(已定义,需固化)**
+ - `MarkClosePnl`=纯价差盯市、`DividendIn`=纯分红、`RealizedPnl`=不重叠分量之和。
+ - 写进评审清单 + 用集成测试守护。
+4. **自动守护测试(把已建设施用起来)**
+ - 扩展 `parity.test.js` 的"生产表达式逐字抄录 vs `SwapCalc`"模式到**所有关键公式**;
+ - 后端补"分红守恒 / 持仓守恒"集成测试:`ΔSwapPositionValue + ΔRealizedPnl == 0`
+ (参考分析文档附录 A.2 的 SQL 思路,转成 `SwapEodPositionServiceIntegrationTest` 断言)。
+5. **防回归**:将 `swapCalc.calcUnwind/calcIncome` 接入生产,删除两页内联实现。
+
+---
+
+## 五、后续逐渐解决的路线(分阶段、低风险)
+
+- **阶段 0(已具备)**:`parity.test.js` / `swapCalc.test.js` / `GLMS20260105PartialCloseDividendBugTest.cs` 框架。
+- **阶段 1(低风险、先消除最危险口径)**:
+ 让 `incomeSwapTrade.js:207` 与 `unwindSwapTrade.js:313` 统一调用 `swapCalc.calcMarkClosePnl`
+ (全价 + `longRatio` + `scale`),并确认与后端 `PosiGrossPrice` 口径一致;
+ 新增"净价≠全价""空头 longRatio"两个 parity 场景守护。**改动小、收益高。**
+- **阶段 2(消除魔法数字)**:
+ 把 `SwapFlowService` / `SwapTradeAutoService` / `SwapDealService` 里的裸 `Math.Round(...,4/10)`、
+ `ToString("F10")` 替换为 `ConsGlobal.*` / `GetStorageDeliveryPriceRound`;分红的裸 `2` 改为 `ConsGlobal.MoneyRound`。
+ 逐文件改,每改一处跑 parity + 现有单测。
+- **阶段 3(后端收口)**:
+ 手动平仓/互换落库时,后端对 `MarkClosePnl` / `FloatPnlSum` / `SwapRealizedPnL` **重算**(与 `swapCalc` 金标准一致),
+ 不再信任前端;同时跑"持仓守恒"SQL 校验历史数据是否已被两页口径差污染。
+- **阶段 4(横向扫描)**:
+ 用脚本/Explore 扫描 期货、期权、定价引擎、估值报告 模块,查找"前端重算+后端重算""硬编码精度"同类结构,建立清单逐个治理。
+- **阶段 5(文档固化)**:
+ 更新 `互换分红损益字段语义与重复计算分析.md`,把"已修复 / 待修复"状态机化,作为新人评审清单与回归基线。
+
+---
+
+## 六、一句话结论
+
+> 最新修复根治了"分红预览"这一条链路的分散计算;但**同类结构依然存在**——
+> 前端平仓页/互换页的 `MarkClosePnl` 都用全价,真正的差异是**互换页(income)漏乘 `longRatio`(多空方向)**,
+> 对空头会算出相反符号并直接落库(后端对 income 不重算、原样存前端值);共享的 `swapCalc.calcMarkClosePnl` 两个页面都没用;
+> 后端仍有**费用 4 位 vs 金额 2 位**等硬编码精度错配。
+> 治理的关键不是再打补丁,而是**把已建好的 `swapCalc` 单一可信源 + parity 守护真正接入生产**,
+> 并按上述 5 个阶段低风险推进。
+
+---
+
+## 七、如何确认"income 错 / unwind 对"(而非相反)+ 改动安全性
+
+> 这一章回答一个关键质疑:两页口径不同,凭什么断定是 income 漏了 `longRatio`、而不是 unwind 多算了?
+> 以及:给 income 补 `longRatio` 会不会把正确逻辑改坏、或造成"双重翻转"?
+
+### 7.1 两个方向乘子是**独立轴**(这是避免误判的前提)
+
+- `floatRatio = PayDirection==1(收取) ? +1 : -1` —— 跟随**收付方向**(`FrontendCalcReference.cs:35`)
+- `longRatio = PositionType==1(多头) ? +1 : -1` —— 跟随**多空方向**(`FrontendCalcReference.cs:36`)
+
+二者在本系统里**互不决定**:一个"空头"完全可以 `PayDirection=收取`。
+
+### 7.2 裁决性证据:FC_005(空头平仓)冻结基线
+
+`UnitTestProject/.../FrontendCalcCharacterizationTest.cs` 的 `FC_005`:
+
+```
+输入:PositionType=2(空头) + PayDirection=1(收取)
+注释:floatRatio=1(收取), longRatio=-1(空头)
+期望:MarkClosePnl = 1000×(105−100)×1×(−1) = −5000 (空头涨价=亏损,经济正确)
+```
+
+**它证明两件事**:
+1. 空头下 `floatRatio` 仍是 **+1**(方向不靠 `floatRatio` 编码)→ 所以 income 只乘 `floatRatio(+1)` 而漏 `longRatio(-1)`,
+ 对同一个空头会算出 **+5000**,与权威 −5000 相反 → **income 错、unwind 对**。
+2. 因为两轴独立,**给 income 补 `longRatio` 是纠正、不是双重翻转**("floatRatio 已编码方向"的担忧不成立)。
+
+### 7.3 三条互相独立的证据链(任一都足以定罪)
+
+| # | 证据 | 来源 | 结论 |
+|---|------|------|------|
+| 1 | 盯市盈亏空头必须翻转符号(空头跌价才盈利)——会计不变式 | 业务数学,独立于代码 | income 漏方向乘子→空头符号必错 |
+| 2 | 后端平仓结算 `:1620` 重算并**覆写** `MarkClosePnl=…*floatRatio*longRatio`(入库存后端口径);`SwapIncome:1980-2013` **不重算**、原样存前端值 | `SwapDealService.cs` | 系统自身的权威定义含 longRatio,income 偏离它 |
+| 3 | `SwapIncome:1994` 用前端 `SwapRealizedPnl`(由 `MarkClosePnl` 派生)做 `AddClientCash(-SwapRealizedPnl)` | `SwapDealService.cs` | 空头 income 不仅显示错,**实际现金流方向也错**(非纯展示) |
+
+### 7.4 下游是否会"双重翻转 / 补偿性 hack"?
+
+- `SwapFlowEventService.cs:589` 对所有流水事件的 `MarkClosePnl/DividendIn/CloseFee/...` **统一取反**(全局视角翻转,firm book→client view),
+ **不针对 income 或空头**。修复后 income 与 unwind 走同一套取反,对称性不变 → 安全。
+- 全仓检索仅此一处 `MarkClosePnl=-MarkClosePnl`,无针对 income/空头的补偿性符号翻转 → 不存在"加了 longRatio 反而翻错"的 hack。
+
+### 7.5 改动安全性论证(如何确保不影响现有正确逻辑)
+
+修复 = 给 income 的 `MarkClosePnl`(含 `FrontendCalcReference.CalcIncome:107`)补 `× longRatio`,使其与权威 unwind 公式口径一致。
+
+1. **静态保证(零回归)**:对所有**多头**(现有 `FC_006~009` 及全部生产多头 income)`longRatio=+1`,
+ 乘积不变 → 数值**逐位相同**,现有正确行为完全不动。改动是"对多头的恒等变换 + 对空头的纠错",是严格的超集。
+2. **回归护栏**:
+ - 跑现有 `FC_001~009` + `SwapFrontendPnlValidateTest` + `SwapIncomeScenarioTest` → 全绿(均为多头,不受影响)。
+ - 新增 `FC_010`(空头 income,`PayDirection=1, PositionType=2`)镜像 `FC_005`,锁定纠正后行为,并断言 `income(空头)==unwind(空头)`(parity)。
+ - jest 红测试 `markClosePnlShortConsistency.test.js` 的"空头"用例由 FAIL 转 PASS,多头用例保持 PASS。
+3. **剩余风险(非逻辑正确性,需业务决策)**:
+ - **历史脏数据**:过去"空头+互换"事件已用错符号落库;修复后新事件正确,跨时间对比会出现不连续。需决定:回溯校正(改 `MarkClosePnl`+重算 `SwapRealizedPnl`+对账 `AddClientCash` 历史)还是标注留痕。
+ - **改动范围**:严格限定在 income 页 `:207` 与 `CalcIncome:107` 补 `longRatio`,切忌顺手改 `floatRatio` 或其他页面。
+4. **前置确认**:建议先让业务/量化签字"income 的 `MarkClosePnl` 应含多空方向(与平仓一致)",再动手——因为结论虽由代码+基线铁证支撑,但涉及客户现金流,需业务背书。
|