- 约定1:仅在净价/全价/收益率输入框敲回车触发计算器,失焦不触发 - 约定2:计算成功->源字段标"源"、另两字段标"AUTO";失败->保留源值、清空另两字段与标识 - 约定3:编辑某字段未回车->不联动另两字段,清源/AUTO、仅本字段标"REV" - swapCalc.js 新增 applyBondCalcSuccess/Failure/ManualEdit 纯函数,applyBondCalcResult 第三参由 manualSet 改为 driver(跳过源字段写回) - fastVue.base.js 新增 enter 事件区分回车/失焦,向后兼容旧的 input 行为 - TradeEdit.cshtml 三字段 input/enter 分离绑定 + 源/AUTO/REV 三态标签(去除旧铅笔图标) - 重写 bondCalc.test.js / 更新 swapCalc.test.js 覆盖状态机;jest 68 项全过
411 lines
22 KiB
JavaScript
411 lines
22 KiB
JavaScript
/**
|
||
* swapCalc.js — 互换结算/平仓纯计算函数(与 C# FrontendCalcReference 对齐)
|
||
* ============================================================================
|
||
* 设计要点:
|
||
* - 无 Vue / otcformat / jQuery / lodash 依赖,全部为纯函数,便于 jest 直接 import。
|
||
* - 浏览器:挂到 window.SwapCalc(需在 incomeSwapTrade.js / swapTradeEdit.js 之前加载)。
|
||
* - Node: module.exports(UMD 包装),供 fe-tests/*.test.js 使用。
|
||
* - 公式与 YLErpDAL/Helpers/FrontendCalcReference.cs 保持一致,是前后端同一份金标准。
|
||
*
|
||
* 生产接线状态(tested == used):incomeSwapTrade.js / swapTradeEdit.js 已调用
|
||
* getPriceScale / deriveTradingAmountAvg / calcFloatPnlSum / calcStockEqvNotional
|
||
* 这 4 个叶子函数(对应真实出过的 4 个 bug:20ea93d8 / dcf649f2 / 3c5f25a5 / f873239a)。
|
||
* calcUnwind / calcIncome 仅用于 swapCalc.test.js 的前后端金标准交叉校验,未接入生产代码。
|
||
*
|
||
* 守卫的 bug(见 git 历史):
|
||
* - 20ea93d8 / dcf649f2:deriveTradingAmountAvg 必须用 PosiGrossPrice(全价) 且债券 ×100
|
||
* - 3c5f25a5:calcFloatPnlSum 必须 .toFixed(2)(保留 2 位小数)
|
||
* - f873239a:calcStockEqvNotional 必须 round 到 2 位
|
||
* ============================================================================
|
||
*/
|
||
(function (root, factory) {
|
||
if (typeof module === 'object' && module.exports) {
|
||
module.exports = factory();
|
||
} else {
|
||
root.SwapCalc = factory();
|
||
}
|
||
})(typeof self !== 'undefined' ? self : this, function () {
|
||
'use strict';
|
||
|
||
// 四舍五入(远离零),对齐 C# MidpointRounding.AwayFromZero
|
||
function roundHalfAwayFromZero(value, digits) {
|
||
var f = Math.pow(10, digits);
|
||
var n = Number(value) * f;
|
||
var sign = n < 0 ? -1 : 1;
|
||
var r = Math.round(Math.abs(n)) * sign;
|
||
var result = r / f;
|
||
return result === 0 ? 0 : result; // 消除 -0
|
||
}
|
||
|
||
// 价格缩放因子:债券(multiplier=100)界面为百分比态,计算用相对价需 ÷100
|
||
function getPriceScale(multiplier) {
|
||
return multiplier === 100 ? 0.01 : 1;
|
||
}
|
||
|
||
// 期末全价(界面态) = 期初全价(相对价) × multiplier
|
||
// 必须用 PosiGrossPrice(全价),非 PosiNetPrice(净价);债券 ×100 转界面百分比态
|
||
function deriveTradingAmountAvg(posiGrossPrice, multiplier) {
|
||
return posiGrossPrice * multiplier;
|
||
}
|
||
|
||
// 普通打开时以期初全价作为期末价默认值;审批打开时回显已提交的期末相对价。
|
||
function resolveIncomeTradingAmountAvg(posiGrossPrice, submittedTradingAmountAvg, multiplier, isUseApproval) {
|
||
var relativePrice = isUseApproval && submittedTradingAmountAvg !== undefined && submittedTradingAmountAvg !== null
|
||
? submittedTradingAmountAvg
|
||
: posiGrossPrice;
|
||
return relativePrice * multiplier;
|
||
}
|
||
|
||
// 金额四舍五入到指定小数位(避免 0.1+0.2 类浮点误差)
|
||
function roundMoney(value, digits) {
|
||
return roundHalfAwayFromZero(value, digits);
|
||
}
|
||
|
||
// 浮动盈亏合计 = (平仓盈亏 + 交易费用 + 待结算费用 + 分红).toFixed(2)
|
||
function calcFloatPnlSum(markClosePnl, tradingFee, tradingFeePending, dividendIn) {
|
||
var sum = (+markClosePnl) + (+tradingFee) + (+tradingFeePending) + (+dividendIn);
|
||
return roundHalfAwayFromZero(sum, 2);
|
||
}
|
||
|
||
// 名义本金 = 期初全价 × 因子,保留 2 位(EQD-6090)
|
||
// factor 在前端 = 数量 × 乘数(national)
|
||
function calcStockEqvNotional(posiGrossPrice, factor) {
|
||
return roundHalfAwayFromZero(posiGrossPrice * factor, 2);
|
||
}
|
||
|
||
// 平仓名义本金 = 平仓比例 × 剩余持仓名义本金(PosiNotionalValue)
|
||
// 多次部分平仓后必须用剩余本金 PosiNotionalValue,不能用原始 NotionalValue,否则偏大
|
||
function calcCloseNotionalByRemaining(closePercent, posiNotionalValue) {
|
||
return roundHalfAwayFromZero(Number(closePercent) * Number(posiNotionalValue), 2);
|
||
}
|
||
|
||
// 平仓比例 = 平仓名义本金 / 剩余持仓名义本金(PosiNotionalValue)
|
||
// 多次部分平仓后必须以剩余本金为分母,否则比例偏小,导致后端预付金返还本金计算错误
|
||
function calcClosePercentByRemaining(closeNotionalValue, posiNotionalValue) {
|
||
if (Number(posiNotionalValue) === 0) return 0;
|
||
return roundHalfAwayFromZero(Number(closeNotionalValue) / Number(posiNotionalValue), 6);
|
||
}
|
||
|
||
// 平仓数量(占期初口径 A):CloseQty = PositionQty × (closePercent / oriClosePercent)
|
||
// closePercent 是"占期初名义本金比例"(A),需先除以 oriClosePercent(=剩余/期初) 转成"占剩余比例"(B),
|
||
// 再乘以剩余持仓数量 PositionQty。
|
||
// 多次部分平仓后必须这样转换,否则全部↔部分切换时 ClosePercent 没变但 CloseQty 会变(不自洽)。
|
||
// 除零保护:oriClosePercent=0(剩余为0,已全部平完)时返回 0。
|
||
function calcCloseQtyByOriginalPercent(closePercent, oriClosePercent, positionQty) {
|
||
var ori = Number(oriClosePercent);
|
||
if (ori === 0) return 0;
|
||
return roundHalfAwayFromZero(Number(positionQty) * (Number(closePercent) / ori), 2);
|
||
}
|
||
|
||
// 平仓比例(占期初口径 A)= (CloseQty / PositionQty) × oriClosePercent
|
||
// CloseQty/PositionQty 得到"占剩余比例"(B),乘以 oriClosePercent(=剩余/期初) 转成"占期初比例"(A)。
|
||
// 除零保护:PositionQty=0 时返回 0。
|
||
function calcOriginalClosePercentByQty(closeQty, positionQty, oriClosePercent) {
|
||
var qty = Number(positionQty);
|
||
if (qty === 0) return 0;
|
||
return roundHalfAwayFromZero((Number(closeQty) / qty) * Number(oriClosePercent), 6);
|
||
}
|
||
|
||
// 盯市平仓盈亏(unwind):CloseQty × (期末全价×scale − 期初全价) × floatRatio × longRatio
|
||
// 对齐 FrontendCalcReference.CalcUnwind:先 ×10000 取整再 ÷10000,最后 toFixed(2)
|
||
// 干净输入下等价于直接 round(.., 2)
|
||
function calcMarkClosePnl(closeQty, tradingAmountAvg, scale, entryPrice, floatRatio, longRatio) {
|
||
var product = closeQty * (tradingAmountAvg * scale - entryPrice) * floatRatio * longRatio;
|
||
var step = Math.round(product * 10000) / 10000; // 对齐 C# Math.Round(.. * 10000) / 10000
|
||
return roundHalfAwayFromZero(step, 2);
|
||
}
|
||
|
||
// ---- 组合函数:对齐 C# FrontendCalcReference.CalcUnwind / CalcIncome ----
|
||
// 用途:作为「前端 JS 完整盈亏聚合公式」与「后端 C# 金标准」的交叉校验
|
||
// (见 swapCalc.test.js 的 FC_001~FC_008 八个冻结场景)。
|
||
// 注意:以下 calcUnwind / calcIncome **未接入生产代码**——生产 Vue 组件只调用上方
|
||
// 4 个叶子函数。它们是冻结完整聚合逻辑的参考规格;若要让生产聚合逻辑也被自动守卫,
|
||
// 需把 incomeSwapTrade.js / swapTradeEdit.js / unwindSwapTrade.js 的聚合计算也改调它们。
|
||
|
||
function parseOrZero(s) {
|
||
return (s === undefined || s === null || s === '') ? 0 : Number(s);
|
||
}
|
||
|
||
function sumLegs(legs) {
|
||
return (legs || []).reduce(function (acc, l) { return acc + parseOrZero(l.interestClosePnL); }, 0);
|
||
}
|
||
|
||
// 平仓页(unwind)盈亏汇总 — 对齐 FrontendCalcReference.CalcUnwind
|
||
function calcUnwind(input) {
|
||
var entryPrice = input.posiGrossPrice;
|
||
var scale = input.multiplier === 100 ? 0.01 : 1;
|
||
var floatRatio = input.payDirection === 1 ? 1 : -1;
|
||
var longRatio = input.positionType === 1 ? 1 : -1;
|
||
|
||
var tradingFee = parseOrZero(input.tradingFee);
|
||
var tradingFeePending = parseOrZero(input.tradingFeePending);
|
||
var dividendIn = parseOrZero(input.dividendIn);
|
||
|
||
var markClosePnl = calcMarkClosePnl(
|
||
input.closeQty, input.tradingAmountAvg, scale, entryPrice, floatRatio, longRatio);
|
||
markClosePnl = roundHalfAwayFromZero(markClosePnl, 2);
|
||
|
||
var floatPnlSum = roundHalfAwayFromZero(markClosePnl + tradingFee + tradingFeePending + dividendIn, 2);
|
||
|
||
var swapRealizedPnL = floatPnlSum + sumLegs(input.interestLegs) + sumLegs(input.marginLegs);
|
||
var swapCloseAmount = floatPnlSum + sumLegs(input.interestLegs) + sumLegs(input.marginLegs);
|
||
var swapMarginRebatePnl = sumLegs(input.marginLegs);
|
||
|
||
var ratio = input.positionType === 1 ? 1 : -1;
|
||
var tradingAmountFeeAvg = input.closeQty === 0 ? 0
|
||
: input.tradingAmountAvg * scale + (tradingFee / input.closeQty) * ratio;
|
||
|
||
return {
|
||
MarkClosePnl: roundHalfAwayFromZero(markClosePnl, 2),
|
||
FloatPnlSum: floatPnlSum,
|
||
SwapRealizedPnL: roundHalfAwayFromZero(swapRealizedPnL, 2),
|
||
SwapCloseAmount: roundHalfAwayFromZero(swapCloseAmount, 2),
|
||
SwapMarginRebatePnl: roundHalfAwayFromZero(swapMarginRebatePnl, 2),
|
||
TradingAmountFeeAvg: tradingAmountFeeAvg
|
||
};
|
||
}
|
||
|
||
// 结息页(income)盈亏汇总 — 对齐 FrontendCalcReference.CalcIncome
|
||
function calcIncome(input) {
|
||
var entryPrice = input.posiGrossPrice;
|
||
var scale = input.multiplier === 100 ? 0.01 : 1;
|
||
var floatRatio = input.payDirection === 1 ? 1 : -1;
|
||
|
||
var tradingFee = parseOrZero(input.tradingFee);
|
||
var tradingFeePending = parseOrZero(input.tradingFeePending);
|
||
var dividendIn = parseOrZero(input.dividendIn);
|
||
|
||
var contractSize = input.contractSize === undefined || input.contractSize === null
|
||
? 1 : Number(input.contractSize);
|
||
var markClosePnl = roundHalfAwayFromZero(
|
||
input.positionQty * contractSize * (input.tradingAmountAvg * scale - entryPrice) * floatRatio, 2);
|
||
|
||
var floatPnlSum = roundHalfAwayFromZero(markClosePnl + tradingFee + tradingFeePending + dividendIn, 2);
|
||
|
||
var swapRealizedPnL = floatPnlSum + sumLegs(input.interestLegs) + sumLegs(input.marginLegs);
|
||
var swapCloseAmount = floatPnlSum + sumLegs(input.interestLegs) + sumLegs(input.marginLegs);
|
||
var swapMarginRebatePnl = sumLegs(input.marginLegs);
|
||
|
||
var tradingAmountFeeAvg = input.closeQty > 0
|
||
? input.tradingAmountAvg * scale + (tradingFee / input.closeQty) * floatRatio
|
||
: input.tradingAmountAvg * scale;
|
||
|
||
return {
|
||
MarkClosePnl: markClosePnl,
|
||
FloatPnlSum: floatPnlSum,
|
||
SwapRealizedPnL: roundHalfAwayFromZero(swapRealizedPnL, 2),
|
||
SwapCloseAmount: roundHalfAwayFromZero(swapCloseAmount, 2),
|
||
SwapMarginRebatePnl: roundHalfAwayFromZero(swapMarginRebatePnl, 2),
|
||
TradingAmountFeeAvg: tradingAmountFeeAvg
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 债券净价/全价/收益率三字段互算:以 driver(CP/DP/YD) 为"源",回写时跳过源字段(保留用户手输值),
|
||
* 其余两字段用计算器结果覆盖。
|
||
* - driver: 'CP'净价 / 'DP'全价 / 'YD'收益率,即本次回车的输入依据
|
||
* - calc: { cleanPrice, dirtyPrice, ytm } 来自 /Bond/CalcBond 的 resp.obj
|
||
* - state: 持有三个字段的对象(直接原地写回)
|
||
* 设计(对应交互约定1+2):
|
||
* 回车某字段即以其为源调计算器,另两个字段被反算覆盖;源字段保持用户手输值不被回写。
|
||
* 故用户回车净价→全价/收益率被重算;再次回车全价→净价/收益率被重算(最后回车者恒为源)。
|
||
* 纯函数,jest 可直接测(见 fe-tests/bondCalc.test.js)。
|
||
*/
|
||
function applyBondCalcResult(state, calc, driver) {
|
||
var EPS = 1e-4;
|
||
function write(field, type, value) {
|
||
if (driver === type) return; // 源字段:保留用户手输值,不回写
|
||
if (value === undefined || value === null) return; // 计算器未返回该值则不覆盖
|
||
var cur = state[field];
|
||
if (typeof cur === 'number' && Math.abs(cur - value) < EPS) return; // 无变化不写,避免光标跳动
|
||
state[field] = value;
|
||
}
|
||
write('cleanPrice', 'CP', calc && calc.cleanPrice);
|
||
write('dirtyPrice', 'DP', calc && calc.dirtyPrice);
|
||
write('ytm', 'YD', calc && calc.ytm);
|
||
}
|
||
|
||
/**
|
||
* 债券价格【存储态小数 ↔ 展示态每百元百分比】边界换算。
|
||
* 约定(见 ConsGlobal.bondPriceMultiple=0.01 / bondShowPriceMultiple=100、BondPriceConverter):
|
||
* - 模型/DB 存【存储态小数】(如 0.995 = 99.5 元/百元面值);
|
||
* - 债券计算器(bond-calc,经 zszq-bond-oms 代理)要【展示态百分比】(如 99.5)。
|
||
* 历史坑:calcBondForItem 曾漏掉这正反两步,把存储态 0.995 当 99.5 发给计算器、
|
||
* 又把返回 97.82 原样落库;配合 vue-number-input 的 percent:true(显示再×100)
|
||
* 造成 -117.93 / 378543 离谱值。故发计算器前 bondPriceToCalc(×100)、回写前 bondCalcPriceToStorage(÷100)。
|
||
* 纯函数,jest 可直接测。
|
||
*/
|
||
function bondPriceToCalc(storagePrice) {
|
||
return Number(storagePrice) * 100; // 存储态小数 → 展示态每百元百分比
|
||
}
|
||
function bondCalcPriceToStorage(displayPrice) {
|
||
return Number(displayPrice) / 100; // 展示态百分比 → 存储态小数
|
||
}
|
||
/**
|
||
* 清空债券三字段互算的【源/AUTO/REV】标志(纯函数,jest 可测)。
|
||
* 用途:切换标的(setUnderlyingCode)时调用,避免旧债券的状态污染新债券(D1 修复)。
|
||
* 直接对传入对象赋值;在 Vue 组件里该 item 已是响应式对象,重赋值能正常触发响应式更新。
|
||
*/
|
||
function clearBondCalcFlags(state) {
|
||
state.bondDriverType = null;
|
||
state.bondAuto = { CP: false, DP: false, YD: false };
|
||
state.bondRev = { CP: false, DP: false, YD: false };
|
||
return state;
|
||
}
|
||
|
||
/**
|
||
* 交互约定2(纯函数,jest 可测):计算器调用【成功】后的标识落地。
|
||
* - 以 driver(CP/DP/YD) 为"源",标记 bondDriverType=driver;
|
||
* - 其余两字段标记 bondAuto=true("AUTO"=计算器算出的);
|
||
* - 清除 bondRev(人工输入标记)。
|
||
* 不写任何价格数值——数值由 applyBondCalcResult 回写。
|
||
*/
|
||
function applyBondCalcSuccess(state, driver) {
|
||
state.bondDriverType = driver;
|
||
state.bondAuto = { CP: driver !== 'CP', DP: driver !== 'DP', YD: driver !== 'YD' };
|
||
state.bondRev = { CP: false, DP: false, YD: false };
|
||
return state;
|
||
}
|
||
|
||
/**
|
||
* 交互约定2(纯函数,jest 可测):计算器调用【失败】后的标识与数值落地。
|
||
* - 保留用户刚回车的"源"字段值(driver),让用户可就地修改;
|
||
* - 另两个字段清空为空白("另外两个数值都更新为空白");
|
||
* - 三个数值旁边的标识全部清空("三个数值旁边都清空标识")。
|
||
*/
|
||
function applyBondCalcFailure(state, driver) {
|
||
var fields = { CP: 'PosiNetNoFeePrice', DP: 'PosiGrossPrice', YD: 'InitYtm' };
|
||
Object.keys(fields).forEach(function (t) {
|
||
if (t !== driver) state[fields[t]] = null; // 非源字段清空
|
||
});
|
||
state.bondDriverType = null;
|
||
state.bondAuto = { CP: false, DP: false, YD: false };
|
||
state.bondRev = { CP: false, DP: false, YD: false };
|
||
return state;
|
||
}
|
||
|
||
/**
|
||
* 交互约定3(纯函数,jest 可测):编辑某字段但未回车(失焦/输入)时的标识落地。
|
||
* - 清除 源 与 AUTO 标识(不再代表任何已算结果);
|
||
* - 仅本字段标 bondRev[type]=true("REV"=人工输入),不联动另外两字段。
|
||
*/
|
||
function applyBondManualEdit(state, type) {
|
||
state.bondDriverType = null;
|
||
state.bondAuto = { CP: false, DP: false, YD: false };
|
||
state.bondRev = { CP: false, DP: false, YD: false };
|
||
if (type) state.bondRev[type] = true;
|
||
return state;
|
||
}
|
||
|
||
/**
|
||
* 从 /Bond/CalcBond 响应(resp.obj,即 CalBondResult)中提取应展示给用户的错误文案;
|
||
* 无错误返回 null(调用方据此决定是否回写、是否提示)。
|
||
* 覆盖:
|
||
* - 空响应(计算器无响应)
|
||
* - 业务错误码 errCode!=0(债券不存在 / 债券信息不全 / 参数非法)
|
||
* - 防御性值域校验(errCode=0 但数值离谱):即便计算器返回 success,
|
||
* 仍可能因【前端↔计算器的单位换算不匹配】或
|
||
* 行权收益率哨兵(-999999) 而给出负净价 / 收益率量级爆炸(如 378543) 之类的垃圾值。
|
||
* 现有 errCode 守卫拦不住这类"成功但离谱"的响应,故在此加值域闸门,
|
||
* 宁可不回写并提示用户,也绝不用垃圾值覆盖手工输入。
|
||
* 与 C# BondCalcHepler 的 errCode 守卫一一对应,确保"坏结果"不会静默回写覆盖手工输入。
|
||
* 纯函数,jest 可直接测(见 fe-tests/bondCalc.test.js)。
|
||
*/
|
||
function getBondCalcErrorMessage(resp) {
|
||
if (!resp) return "债券计算器无响应,已保留手工输入";
|
||
if (resp.errCode && resp.errCode !== 0) {
|
||
return resp.errMsg || "债券计算失败,请检查标的或参数";
|
||
}
|
||
// 防御性值域校验:拦截 errCode=0 但数值离谱的响应(部署环境主数据量纲错误 / 哨兵值)
|
||
var SENTINEL = -999999; // bond-calc 行权收益率不可用哨兵
|
||
var cp = resp.cleanPrice, dp = resp.dirtyPrice, yd = resp.ytm;
|
||
var absurd = function (v) {
|
||
return typeof v === 'number' && (v === SENTINEL || v === SENTINEL * 100);
|
||
};
|
||
if (absurd(cp) || absurd(dp) || absurd(yd)) {
|
||
return "债券计算返回哨兵值(部分指标不可用),已保留手工输入,请检查估值日/价格输入或联系管理员核对债券计算服务";
|
||
}
|
||
// 净价/全价:占面值百分比,正常约 20~300,绝不为负、也不会破千
|
||
if ((typeof cp === 'number' && (cp <= 0 || cp > 1000)) ||
|
||
(typeof dp === 'number' && (dp <= 0 || dp > 1000))) {
|
||
return "债券计算净价/全价超出合理范围(应为面值百分比且为正),已保留手工输入;" +
|
||
"请检查估值日/价格输入或联系管理员核对债券计算服务";
|
||
}
|
||
// 到期收益率:百分数口径(如 6.37 表示 6.37%),正常约 -5~30,|收益率|>100 视为爆炸
|
||
if (typeof yd === 'number' && Math.abs(yd) > 100) {
|
||
return "债券计算收益率量级异常(" + yd + "),已保留手工输入;" +
|
||
"请检查估值日/价格输入或联系管理员核对债券计算服务";
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 加载路径(重开/刷新已保存债券单)调用(纯函数,jest 可测):
|
||
* 把三字段互算的状态标识全部清空,使已保存的净价/全价/收益率纯展示、无任何 源/AUTO/REV 标记。
|
||
* - 不锁定、不置源:交互约定3规定"编辑未回车不联动",故用户载入后即使改某格(未回车)也不会反算另两格;
|
||
* 只有用户主动回车某格时,才以该格为源重新推导(符合交互约定1)。
|
||
* 仅在加载路径对"债券且三值齐全"的标的调用,全新未填的债券无副作用。
|
||
*/
|
||
function clearBondCalcMarksOnLoad(state) {
|
||
state.bondDriverType = null;
|
||
state.bondAuto = { CP: false, DP: false, YD: false };
|
||
state.bondRev = { CP: false, DP: false, YD: false };
|
||
return state;
|
||
}
|
||
|
||
/**
|
||
* D3 修复(纯函数,jest 可测):债券计算器失败提示去重。
|
||
* 不可算的债券上用户逐键手填时,v-on:input 每次按键都触发一次失败计算并弹 toast,会连刷数条相同提示。
|
||
* 这里按"同一错误文案连续出现只提示一次"抑制噪声;错误文案变化(如 债券不存在→信息不全)则照常提示,
|
||
* 成功(传入 null/空)时清掉标记,便于下次真出不同错误时仍能提示。
|
||
* 纯做"是否该弹"的决策并维护 state._lastBondErr,不触碰任何计算逻辑,零风险。
|
||
*/
|
||
function shouldShowBondErr(state, err) {
|
||
if (!err) { state._lastBondErr = null; return false; } // 成功/无错误:清标记、不提示
|
||
if (state._lastBondErr === err) return false; // 连续相同错误 → 抑制重复 toast
|
||
state._lastBondErr = err;
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 估值日(开始日)缺失校验(纯函数,jest 可测)。
|
||
* 需求:债券净价/全价/收益率以【互换起始日 StartDate】估值;开始日现已默认即有,
|
||
* 故不再"悄悄回退到交易日",而是真的为空时返回错误文案,由上层提示用户
|
||
* (并允许其手动填写三项数值,见需求2)。
|
||
* 返回 非空字符串=缺失需提示;返回 null=已具备估值日。
|
||
*/
|
||
function getBondStartDateMissingMsg(startDate) {
|
||
if (startDate) return null;
|
||
return "请先填写开始日(互换起始日,作为估值日)后再计算债券净价/全价/收益率;" +
|
||
"若暂不需要计算,可手动填写净价/全价/收益率三项数值";
|
||
}
|
||
|
||
return {
|
||
applyBondCalcResult: applyBondCalcResult,
|
||
getBondCalcErrorMessage: getBondCalcErrorMessage,
|
||
bondPriceToCalc: bondPriceToCalc,
|
||
bondCalcPriceToStorage: bondCalcPriceToStorage,
|
||
clearBondCalcFlags: clearBondCalcFlags,
|
||
applyBondCalcSuccess: applyBondCalcSuccess,
|
||
applyBondCalcFailure: applyBondCalcFailure,
|
||
applyBondManualEdit: applyBondManualEdit,
|
||
clearBondCalcMarksOnLoad: clearBondCalcMarksOnLoad,
|
||
shouldShowBondErr: shouldShowBondErr,
|
||
getBondStartDateMissingMsg: getBondStartDateMissingMsg,
|
||
roundHalfAwayFromZero: roundHalfAwayFromZero,
|
||
getPriceScale: getPriceScale,
|
||
deriveTradingAmountAvg: deriveTradingAmountAvg,
|
||
resolveIncomeTradingAmountAvg: resolveIncomeTradingAmountAvg,
|
||
roundMoney: roundMoney,
|
||
calcFloatPnlSum: calcFloatPnlSum,
|
||
calcStockEqvNotional: calcStockEqvNotional,
|
||
calcCloseNotionalByRemaining: calcCloseNotionalByRemaining,
|
||
calcClosePercentByRemaining: calcClosePercentByRemaining,
|
||
calcCloseQtyByOriginalPercent: calcCloseQtyByOriginalPercent,
|
||
calcOriginalClosePercentByQty: calcOriginalClosePercentByQty,
|
||
calcMarkClosePnl: calcMarkClosePnl,
|
||
calcUnwind: calcUnwind,
|
||
calcIncome: calcIncome
|
||
};
|
||
});
|