Files
zszq-trs/YLErpWeb/wwwroot/Scripts/app/swaptrade/swapCalc.js
T
hjhan b3c2fdfe38 test(fe): 补充守卫注释与架构闸门,防新增内联金额计算
- 生产接线处补注释,追溯到具体历史 bug (dcf649f2/20ea93d8/3c5f25a5/f873239a)

- swapCalc.js 注明哪些函数已接入生产、哪些是纯交叉校验规格

- 新增 guard_arch.js 架构闸门:扫描提交新增行,拦截 Vue 组件内联金额计算

- pre-commit hook 串行运行 单元测试 + 架构闸门
2026-07-07 13:26:32 +08:00

172 lines
8.3 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 依赖,全部为纯函数,便于 jest 直接 import。
* - 浏览器:挂到 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();
} 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;
}
// 金额四舍五入到指定小数位(避免 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);
}
// 盯市平仓盈亏(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 markClosePnl = roundHalfAwayFromZero(
input.closeNotionalValue * (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
};
}
return {
roundHalfAwayFromZero: roundHalfAwayFromZero,
getPriceScale: getPriceScale,
deriveTradingAmountAvg: deriveTradingAmountAvg,
roundMoney: roundMoney,
calcFloatPnlSum: calcFloatPnlSum,
calcStockEqvNotional: calcStockEqvNotional,
calcMarkClosePnl: calcMarkClosePnl,
calcUnwind: calcUnwind,
calcIncome: calcIncome
};
});