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;
|