/** * unwindBondCalc.js — 平仓页 期末标的交割全价 ↔ 期末标的结算收益率(ExitYtm) 两字段互算(EQD-6953) * ============================================================================ * 与录入页(swapTradeEdit.js + swapCalc.js 债券三字段互算)的关键差异,【勿"顺手统一"】: * 1. 单位状态:录入页三字段(CP/DP/YD)存【存储态小数】(0.995),发计算器前 ×100、回写 ÷100 * (bondPriceToCalc/bondCalcPriceToStorage);平仓页 TradingAmountAvg 在 initDeal 已转 * 【展示态百分数】(99.5),ExitYtm 亦定义为展示态(6.3721)——发计算器不换算、回写不换算。 * 提交时的存储态转换仍由既有 getStorageDeliveryPrice()/dataFormat() 负责。 * 2. 字段集:只有 DP(TradingAmountAvg)/YD(ExitYtm) 两个字段(平仓页无净价列), * 失败只清对方字段(录入页 applyBondCalcFailure 清另外两个,字段名也不同,故不复用)。 * 3. 估值日:用平仓日 deal.ValueDate(结算语义);录入页用互换起始日 StartDate。 * 4. 精度:ExitYtm 保留 4 位小数、四舍五入(AwayFromZero)——结算确认书导出要求 * 固定 4 位不去零(ToString("0.0000")),界面精度不得低于该要求。 * * 浏览器:挂 window.UnwindBondCalc(需在 swapCalc.js 之后、unwindSwapTrade.js 之前加载)。 * Node:module.exports(UMD),供 fe-tests/unwindBondCalc.test.js 使用。 * 命名遵《互换价格字段命名规范决策文档》:平仓/了结时点 = Exit(非 End/Close/Final)。 */ (function (root, factory) { if (typeof module === 'object' && module.exports) { module.exports = factory(require('./swapCalc.js')); } else { root.UnwindBondCalc = factory(root.SwapCalc); } })(typeof self !== 'undefined' ? self : this, function (SwapCalc) { 'use strict'; // driver(回车的"源"字段) → 平仓页浮动腿(floatPosition)字段名 var FIELDS = { DP: 'TradingAmountAvg', YD: 'ExitYtm' }; function hasValue(v) { return v !== null && v !== undefined && v !== '' && !isNaN(v); } /** * 构造 /Bond/CalcBond 请求体。 * ⚠️ price 必须传【展示态百分数】直传(99.5),不做录入页那种 ×100—— * 平仓页模型里两个字段本就是展示态。此约定由 fe-tests 冻结,防止后人误加换算。 * source='unwind':BondController 记进日志,便于区分录入页/平仓页来源定位问题。 */ function getCalcRequest(underlyingCode, price, driver, valueDate) { return { underlyingCode: underlyingCode, price: Number(price), priceType: driver, targetDate: valueDate || null, source: 'unwind' }; } /** * ExitYtm 统一取整:4 位、四舍五入(远离零),对齐后端 decimal ToString("0.0000")。 * 非数值(null/undefined/NaN)返回 null——供 write 跳过,绝不能把 null 取整成 0 回写 * (否则计算器未返回 ytm 时会清掉用户手工输入)。 */ function roundExitYtm(value) { if (value === null || value === undefined || isNaN(value)) return null; return SwapCalc.roundHalfAwayFromZero(value, 4); } /** * 计算成功回写(交互约定1+2):源字段保持用户手输值,仅回写对方字段(展示态直写)。 * - driver='DP'(全价回车)→ 回写 ExitYtm = ytm 取整 4 位;计算器未返回 ytm 则不覆盖。 * - driver='YD'(收益率回车)→ 回写 TradingAmountAvg = dirtyPrice;未返回则不覆盖。 * ⚠️ 调用方回写 TradingAmountAvg 后须显式重算平仓盈亏(calcFloatClosePnl)。 * 纯函数,jest 可直接测。 */ function applyCalcResult(state, calc, driver) { function write(field, value) { if (value === undefined || value === null) return; // 计算器未返回则不覆盖 var cur = state[field]; if (typeof cur === 'number' && Math.abs(cur - value) < 1e-9) return; // 无变化不写 state[field] = value; } if (driver === 'DP') { write('ExitYtm', roundExitYtm(calc && calc.ytm)); } else if (driver === 'YD') { write('TradingAmountAvg', calc && calc.dirtyPrice); } return state; } /** * 计算失败落地(交互约定2):保留源字段值(用户可就地改),清对方字段、清全部标识。 * 与录入页差异:只清"对方"一个字段(录入页清另外两个)。 * driver 非法(null/未知)时仅清标识、不动任何数值——防调用方误清源字段 * (如 setValueDate 只想清陈旧标识时误传 null,会把 TradingAmountAvg 清掉)。 */ function applyCalcFailure(state, driver) { 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 }; return state; } /** 计算成功后的标识(交互约定2):源字段标"源",对方标"AUTO"。 */ function applyCalcSuccess(state, driver) { state.bondDriverType = driver; state.bondAuto = { DP: driver !== 'DP', YD: driver !== 'YD' }; state.bondRev = { DP: false, YD: false }; return state; } /** * 编辑未回车(失焦/按键)的标识(交互约定3):清 源/AUTO,仅本字段累计标"REV", * 不联动对方字段(允许计算有问题时手工覆盖 ExitYtm)。同录入页:累计 REV 不互清。 */ function applyManualEdit(state, type) { state.bondDriverType = null; state.bondAuto = { DP: false, YD: false }; if (!state.bondRev || typeof state.bondRev !== 'object') { state.bondRev = { DP: false, YD: false }; } if (type) state.bondRev[type] = true; return state; } return { FIELDS: FIELDS, hasValue: hasValue, getCalcRequest: getCalcRequest, roundExitYtm: roundExitYtm, applyCalcResult: applyCalcResult, applyCalcFailure: applyCalcFailure, applyCalcSuccess: applyCalcSuccess, applyManualEdit: applyManualEdit }; });