diff --git a/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml b/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml index 04dd36f2..c1fdaf8d 100644 --- a/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml +++ b/YLErpWeb/Views/SwapTrade2/SwapUnwind.cshtml @@ -26,6 +26,7 @@ + }
@@ -197,6 +198,8 @@ 期初标的价格 期末标的交割全价% 期末标的价格 + @* EQD-6953 期末标的结算收益率:仅普通债券类收益互换显示,期末交割全价回车反算,可手填覆盖 *@ + 期末标的结算收益率% 数量 交易费用(平仓) 交易费用(待结算) @@ -223,11 +226,17 @@ {{priceFormat(floatPosition.PosiGrossPrice)}} - + + AUTOREV + @* EQD-6953 期末标的结算收益率:回车以它为源反算交割全价(联动盈亏);手填未回车=REV人工输入 *@ + + + AUTOREV + {{formatQuantity(deal.CloseQty)}} diff --git a/YLErpWeb/fe-tests/closePercentInterestRefresh.test.js b/YLErpWeb/fe-tests/closePercentInterestRefresh.test.js index eeeb4310..2fb4a7eb 100644 --- a/YLErpWeb/fe-tests/closePercentInterestRefresh.test.js +++ b/YLErpWeb/fe-tests/closePercentInterestRefresh.test.js @@ -148,6 +148,13 @@ function loadVueApp(model, mockPost) { return Number(Number(positionQty) * (Number(closePercent) / ori).toFixed(6)); } }, + // EQD-6953:平仓页现于 unwindSwapTrade.js 之前加载 unwindBondCalc.js(见 SwapUnwind.cshtml), + // dataFormat 提交取整会引用其守卫;此处给同款行为的最小 mock(真实实现由 unwindBondCalc.test.js 覆盖) + UnwindBondCalc: { + hasValue(v) { return v !== null && v !== undefined && v !== '' && !isNaN(v); }, + roundExitYtm(v) { return v; }, + applyManualEdit(s) { return s; } + }, _: { round(value, precision) { return Number(Number(value || 0).toFixed(precision || 0)); diff --git a/YLErpWeb/fe-tests/unwindBondCalc.test.js b/YLErpWeb/fe-tests/unwindBondCalc.test.js index 792617bd..2937f9a2 100644 --- a/YLErpWeb/fe-tests/unwindBondCalc.test.js +++ b/YLErpWeb/fe-tests/unwindBondCalc.test.js @@ -95,6 +95,15 @@ describe('applyCalcFailure:保留源字段、只清对方字段、标识全清 expect(state.ExitYtm).toBe(6.3); expect(state.TradingAmountAvg).toBeNull(); }); + + test('driver 非法(null):仅清标识、不动任何数值(setValueDate 清陈旧标识场景的防误清守卫)', () => { + const state = { TradingAmountAvg: 100.5, ExitYtm: 6.3, bondDriverType: 'DP', bondAuto: { DP: false, YD: true } }; + UnwindBondCalc.applyCalcFailure(state, null); + expect(state.TradingAmountAvg).toBe(100.5); // 绝不能被清 + expect(state.ExitYtm).toBe(6.3); + expect(state.bondDriverType).toBeNull(); + expect(state.bondAuto).toEqual({ DP: false, YD: false }); + }); }); describe('标识状态机(交互约定2/3,对齐录入页)', () => { diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindBondCalc.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindBondCalc.js index dc533376..4c52f009 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindBondCalc.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindBondCalc.js @@ -83,10 +83,14 @@ /** * 计算失败落地(交互约定2):保留源字段值(用户可就地改),清对方字段、清全部标识。 * 与录入页差异:只清"对方"一个字段(录入页清另外两个)。 + * driver 非法(null/未知)时仅清标识、不动任何数值——防调用方误清源字段 + * (如 setValueDate 只想清陈旧标识时误传 null,会把 TradingAmountAvg 清掉)。 */ function applyCalcFailure(state, driver) { - var other = driver === 'DP' ? 'YD' : 'DP'; - state[FIELDS[other]] = null; + if (driver === 'DP' || driver === 'YD') { + var other = driver === 'DP' ? 'YD' : 'DP'; + state[FIELDS[other]] = null; + } state.bondDriverType = null; state.bondAuto = { DP: false, YD: false }; state.bondRev = { DP: false, YD: false }; diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js index 1f70d833..764e9623 100644 --- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js +++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js @@ -11,6 +11,9 @@ const formatSwapQuantity = value => swapPricePrecision.normalizeCommon('quantity 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 }); +// EQD-6953 期末标的结算收益率(ExitYtm):展示态百分数(如 6.3721),trimTailZeros:false 不去零; +// 精度由 getInputFormat('yield') 规则给 4 位——不得低于结算确认书导出的固定 4 位(0.0000)。 +const inputFormatUnwindExitYtm = Object.freeze({ append: '', negative: true, trimTailZeros: false }); const consPosiFeeType = Object.freeze({ Percent: 0, Unit: 1 }); const swapPosiFeeCalc = { normalizeFeeType(feeType) { @@ -61,6 +64,8 @@ const vue = new Vue({ marginList: [], initPosiNetPrice: 0, multiplier: 1, + // EQD-6953 簿记模板=普通债券类收益互换 时启用 期末交割全价↔结算收益率(ExitYtm) 互算 + isBondTRS: false, // 平仓比例展示/输入均为"占期初(original)"语义(A):默认与每次重开都基于原始名义本金。 // oriClosePercent = 剩余名义本金/期初名义本金 = 最多可平比例(不能平超过剩余持仓)。 oriClosePercent: 1, @@ -73,10 +78,15 @@ const vue = new Vue({ }, minStartDate() { return this.deal.StartDate; + }, + // EQD-6953:簿记模板为普通债券类收益互换 且 浮动腿标的为债券 时,才展示 源/AUTO/REV 标识并允许互算 + isBondUnwindLeg() { + return this.isBondTRS && !!this.floatPosition && this.IsBond(this.floatPosition.UnderlyingInstrumentType); } }, created() { - this.multiplier = this.deal.StructureType == '普通债券类收益互换' ? 100 : 1; + this.isBondTRS = this.deal.StructureType == '普通债券类收益互换'; + this.multiplier = this.isBondTRS ? 100 : 1; this.initDeal(); this.setValueDate(this.deal.ValueDate); }, @@ -151,6 +161,10 @@ const vue = new Vue({ this.floatPosition.TradingAmountAvg, this.floatPosition.UnderlyingInstrumentType, 'grossPrice'); + // EQD-6953 期末结算收益率:提交前统一 4 位四舍五入(幂等;空/非数保持原样不覆盖) + if (UnwindBondCalc.hasValue(this.floatPosition.ExitYtm)) { + this.floatPosition.ExitYtm = UnwindBondCalc.roundExitYtm(this.floatPosition.ExitYtm); + } this.floatPosition.TradingFee = formatSwapAmount(this.floatPosition.TradingFee); this.floatPosition.TradingFeePending = formatSwapAmount(this.floatPosition.TradingFeePending); this.floatPosition.DividendIn = formatSwapAmount(this.floatPosition.DividendIn); @@ -174,6 +188,14 @@ const vue = new Vue({ this.deal.UnwindDate = e; this.floatPosition.UnwindDate = e; } + // EQD-6953 估值日(平仓日)变了,已算出的期末结算收益率随之失效:清值+清标识,待用户重新回车计算 + // (applyManualEdit(null) 只清 源/AUTO/REV 不动任何数值;勿用 applyCalcFailure(null)—— + // 它对 null driver 会误清 TradingAmountAvg) + if (this.isBondUnwindLeg && this.floatPosition.ExitYtm != null) { + this.floatPosition.ExitYtm = null; + UnwindBondCalc.applyManualEdit(this.floatPosition, null); + this.syncUnwindBondFlags(); + } if (!isUseApproval) { this.getInterestList(); //this.refreshUnderlyingPrice(); @@ -299,6 +321,107 @@ const vue = new Vue({ thisObj.calcFloatClosePnl(); }); }, + //================================================================================== + // EQD-6953 期末标的交割全价 ↔ 期末标的结算收益率(ExitYtm) 互算 + // 交互对齐录入页(swapTradeEdit.js):回车=以该字段为源调 /Bond/CalcBond 反算对方; + // 失败=保留源字段、清对方、标识全清;手填未回车=REV 不联动(允许计算有问题时手工覆盖)。 + // 关键差异见 unwindBondCalc.js 头注:平仓页两字段均为展示态(不 ×100/÷100),估值日=平仓日 ValueDate。 + //================================================================================== + getYieldInputFormat() { + return swapPricePrecision.getInputFormat( + this.floatPosition && this.floatPosition.UnderlyingInstrumentType, + 'yield', + inputFormatUnwindExitYtm); + }, + //期末交割全价的 input(失焦/回车都会触发):保持既有盈亏联动;债券腿按交互约定3标 REV。 + onEndDeliveryPriceInput() { + this.changeUnderlyingPrice(); + if (this.isBondUnwindLeg) { + UnwindBondCalc.applyManualEdit(this.floatPosition, 'DP'); + this.syncUnwindBondFlags(); + } + }, + //收益率字段编辑(失焦/回车触发,组件内部回车时 enter 随后修正标识):约定3 标 REV 不联动。 + onEndBondPriceEdit(type) { + if (!this.isBondUnwindLeg) return; + UnwindBondCalc.applyManualEdit(this.floatPosition, type); + this.syncUnwindBondFlags(); + }, + //按键实时标 REV(组件 input 事件只在失焦/回车触发,逐键输入期间靠 keydown 清 源/AUTO)。 + //回车/修饰键/导航键/功能键不改标识(SwapCalc.isBondPriceValueKey 白名单)。 + onEndBondPriceKeydown(type, event) { + if (!this.isBondUnwindLeg) return; + if (!SwapCalc.isBondPriceValueKey(event)) return; + UnwindBondCalc.applyManualEdit(this.floatPosition, type); + this.syncUnwindBondFlags(); + }, + //回车=以该字段为源调计算器反算对方字段。 + onEndBondPriceEnter(type) { // type: 'DP'期末交割全价 / 'YD'期末结算收益率 + if (!this.isBondUnwindLeg) return; + if (!this.floatPosition.UnderlyingCode) { main.message("请先选择债券标的"); return; } + var price = type === 'DP' ? this.floatPosition.TradingAmountAvg : this.floatPosition.ExitYtm; + if (!UnwindBondCalc.hasValue(price)) return; // 空/非数:不触发计算器 + this.calcUnwindBond(type); + }, + //以 driver(回车字段)为源调 /Bond/CalcBond。成败路由见 base/main.js __post:业务错误走 reject, + //失败落地必须在 .fail(录入页曾把失败处理只写 .done 导致静默失效,勿重蹈)。 + calcUnwindBond(driver) { + if (!this.isBondUnwindLeg) return; + var fp = this.floatPosition; + fp.bondDriverType = driver; // 先定源,回写时 applyCalcResult 会跳过该字段 + var price = driver === 'DP' ? fp.TradingAmountAvg : fp.ExitYtm; + if (!UnwindBondCalc.hasValue(price)) { + UnwindBondCalc.applyCalcFailure(fp, driver); + this.syncUnwindBondFlags(); + return; + } + if (!this.deal.ValueDate) { + main.message("请先填写平仓日期(作为债券计算器估值日)"); + UnwindBondCalc.applyCalcFailure(fp, driver); + this.syncUnwindBondFlags(); + return; + } + var req = UnwindBondCalc.getCalcRequest(fp.UnderlyingCode, price, driver, this.deal.ValueDate); + otcDebug.log('[unwindBondCalc] enter driver=' + driver + ' req=' + JSON.stringify(req)); + var self = this; + main.post('/Bond/CalcBond', req, { alertFn: main.message }).done(function (resp) { + if (!resp || !resp.obj) { // 网络错误/响应异常:框架已提示,按约定2清对方字段+标识 + UnwindBondCalc.applyCalcFailure(fp, driver); + self.syncUnwindBondFlags(); + return; + } + var err = SwapCalc.getBondCalcErrorMessage(resp.obj); + if (err) { // 业务错误(债券不存在/信息不全/参数非法/值域离谱):toast 去重提示,不回写 + if (SwapCalc.shouldShowBondErr(fp, err)) main.message(err); + UnwindBondCalc.applyCalcFailure(fp, driver); + self.syncUnwindBondFlags(); + otcDebug.log('[unwindBondCalc] calc-error driver=' + driver + ' msg=' + err); + return; + } + fp._lastBondErr = null; + UnwindBondCalc.applyCalcResult(fp, resp.obj, driver); + UnwindBondCalc.applyCalcSuccess(fp, driver); + self.syncUnwindBondFlags(); + // YD 为源时 TradingAmountAvg 被反算回填,直接赋值不触发 input 事件,须显式重算盈亏 + if (driver === 'YD') self.calcFloatClosePnl(); + otcDebug.log('[unwindBondCalc] success driver=' + driver + + ' ExitYtm=' + fp.ExitYtm + ' TradingAmountAvg=' + fp.TradingAmountAvg + + ' auto=' + JSON.stringify(fp.bondAuto)); + }).fail(function () { + UnwindBondCalc.applyCalcFailure(fp, driver); + self.syncUnwindBondFlags(); + otcDebug.log('[unwindBondCalc] post-fail/reject driver=' + driver); + }); + }, + //确保 bondDriverType/bondAuto/bondRev 的变更触发 Vue 2 响应式更新(同录入页 syncBondFlags: + //这些属性不在后端模型里,$set + 全新对象引用 + $forceUpdate 兜底,缺一视图不刷新)。 + syncUnwindBondFlags() { + var fp = this.floatPosition; + this.$set(fp, 'bondDriverType', fp.bondDriverType === undefined ? null : fp.bondDriverType); + this.$set(fp, 'bondAuto', { DP: !!(fp.bondAuto && fp.bondAuto.DP), YD: !!(fp.bondAuto && fp.bondAuto.YD) }); + this.$set(fp, 'bondRev', { DP: !!(fp.bondRev && fp.bondRev.DP), YD: !!(fp.bondRev && fp.bondRev.YD) }); + this.$forceUpdate(); + }, calcFloatClosePnl() {//计算浮动端平仓盈亏 var thisObj = this; let floatRatio = thisObj.floatPosition.PayDirection == 1 ? 1 : -1;