Files
zszq-trs/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapCalc.js
T
张名锐 d78d1f48e7 fix(swap): 修复互换价格变动时盈亏计算错误
- 在后端计算逻辑中添加 longRatio 参数用于区分多空头寸类型
- 修改前端 JavaScript 代码中的盈亏计算公式,加入 longRatio 参与计算
- 更新单元测试,增加空头头寸价格上涨时亏损的测试用例
- 补充前端测试用例验证空头头寸计算逻辑的准确性
- 修复了空头头寸在价格上涨时显示盈利而非亏损的计算错误
2026-08-07 17:44:04 +08:00

460 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* swapCalc.js — 互换结算/平仓纯计算函数(与 C# FrontendCalcReference 对齐)
* ============================================================================
* 设计要点:
* - 无 Vue / otcformat / jQuery / lodash 依赖;复用 swapPricePrecision 的字符串十进制运算。
* - 浏览器:挂到 window.SwapCalc(需在 incomeSwapTrade.js / swapTradeEdit.js 之前加载)。
* - Node module.exportsUMD 包装),供 fe-tests/*.test.js 使用。
* - 公式与 YLErpDAL/Helpers/FrontendCalcReference.cs 保持一致,是前后端同一份金标准。
*
* 生产接线状态(tested == used):incomeSwapTrade.js / swapTradeEdit.js 已调用
* getPriceScale / deriveTradingAmountAvg / calcFloatPnlSum / calcStockEqvNotional
* 这 4 个叶子函数(对应真实出过的 4 个 bug20ea93d8 / dcf649f2 / 3c5f25a5 / f873239a)。
* calcUnwind / calcIncome 仅用于 swapCalc.test.js 的前后端金标准交叉校验,未接入生产代码。
*
* 守卫的 bug(见 git 历史):
* - 20ea93d8 / dcf649f2deriveTradingAmountAvg 必须用 PosiGrossPrice(全价) 且债券 ×100
* - 3c5f25a5calcFloatPnlSum 必须 .toFixed(2)(保留 2 位小数)
* - f873239acalcStockEqvNotional 必须 round 到 2 位
* ============================================================================
*/
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(require('./swapPricePrecisionHelper.js'));
} else {
root.SwapCalc = factory(root.swapPricePrecision);
}
})(typeof self !== 'undefined' ? self : this, function (swapPricePrecision) {
'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);传入乘数时避免数量先被 JS Number 相乘。
function calcStockEqvNotional(posiGrossPrice, quantity, contractSize) {
var factor = contractSize === undefined
? quantity
: swapPricePrecision.multiplyDecimal(quantity, contractSize);
var product = factor === null ? null : swapPricePrecision.multiplyDecimal(posiGrossPrice, factor);
if (product === null) return 0;
var rounded = swapPricePrecision.roundDecimal(product, 2);
return typeof posiGrossPrice === 'string' || typeof quantity === 'string' || typeof contractSize === 'string'
? rounded
: Number(rounded);
}
// 平仓名义本金 = 平仓比例 × 剩余持仓名义本金(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;
if (Number(closePercent) >= ori) return Number(positionQty);
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 longRatio = input.positionType === 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 * longRatio, 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"=人工输入),不联动另外两字段;
* - ★ 累计 REV(不清空其它已手工编辑字段的 REV):连续手动编辑多字段(不回车)时,
* 每个被改过的字段都保留 REV,使 UI 正确显示"全部为人工输入",且不会被后续计算器覆盖
* (见文档步骤3-b)。此前的实现每次重置全部 REV 再只标当前字段,导致"只有最后编辑的字段留 REV",
* 中间手工编辑的字段 REV 被静默抹掉(步骤3-b 不满足需求)。
*/
function applyBondManualEdit(state, type) {
state.bondDriverType = null;
state.bondAuto = { CP: false, DP: false, YD: false };
if (!state.bondRev || typeof state.bondRev !== 'object') {
state.bondRev = { CP: false, DP: false, YD: false };
}
if (type) state.bondRev[type] = true; // 累加:仅置当前字段,保留其它已手工编辑字段的 REV
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 "请先填写开始日(互换起始日,作为估值日)后再计算债券净价/全价/收益率;" +
"若暂不需要计算,可手动填写净价/全价/收益率三项数值";
}
/**
* 判断按键是否会【改变】债券三字段的数值(纯函数,jest 可测)。
* 用于 onBondPriceKeydown:仅"会改变数值"的按键才清 源/AUTO 标识并标本字段 REV;
* 其余(回车/修饰键/导航键/功能键/纯 Shift)一律不改标识,避免"没改值却出现 REV"。
* 正向白名单(最稳,不会漏掉未来新增的导航键):
* - 数字键:主键盘 48-57 / 小键盘 96-105
* - 小数点:主键盘 190 / 小键盘 110
* - 减号: 主键盘 189 / 小键盘 109
* - Backspace(8) / Delete(46)
* 明确排除:回车(13/108,由 v-on:enter 单独处理)、Ctrl/Alt/Meta 组合键、
* Tab(9)/Home(36)/End(35)/←(37)/→(39)/↑(38)/↓(40)/Insert(45)/PageUp(33)/PageDown(34)、
* F1-F12(112-123)、纯 Shift(16)。这些键不改数值,不应清标识/标 REV。
*/
function isBondPriceValueKey(event) {
if (!event) return false;
if (event.ctrlKey || event.altKey || event.metaKey) return false; // 组合键(如 Ctrl+C/V)不改数值
var kc = event.which || event.keyCode;
if (kc === 13 || kc === 108) return false; // 回车由 enter 事件处理
// 纯 Shift 单独按下不改数值(Shift+数字在数值框不产生数字,亦不改数值)
if (kc === 16) return false;
// 导航/功能键不改数值
if (kc >= 33 && kc <= 40) return false; // PageUp/PageDown/End/Home/←/↑/→/↓
if (kc === 45) return false; // Insert
if (kc >= 112 && kc <= 123) return false; // F1-F12
if (kc === 9) return false; // Tab
// 以下为会改变数值的键
var valueKeys = [8, 46, 48,49,50,51,52,53,54,55,56,57,
96,97,98,99,100,101,102,103,104,105,
109,110,189,190];
return valueKeys.indexOf(kc) !== -1;
}
return {
applyBondCalcResult: applyBondCalcResult,
isBondPriceValueKey: isBondPriceValueKey,
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
};
});