unwindSwapTrade.js: - oriClosePercent 恒为 1(Definition B 下最多平 100% 剩余,修正全部平仓/按比例少返预付金 + 进页面 getInterestList 少算) - CloseMethod==1(全部平仓)时修正 ClosePercent=1;CloseMethod==2(部分平仓/待复核)保留已提交比例,避免覆盖 - changeCloseQty/Percent/NotionalValue 三处换算改用 PosiNotionalValue(剩余本金),不再用原始 NotionalValue swapCalc.js: 新增 calcCloseNotionalByRemaining / calcClosePercentByRemaining 纯函数(含除零保护) swapCalc.test.js + _shim_run.js: 补充多次部分平仓换算单测(剩余vs原始对比、第3次全平、3次合计=原始、除零保护)
187 lines
9.2 KiB
JavaScript
187 lines
9.2 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;
|
||
}
|
||
|
||
// 金额四舍五入到指定小数位(避免 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);
|
||
}
|
||
|
||
// 盯市平仓盈亏(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,
|
||
calcCloseNotionalByRemaining: calcCloseNotionalByRemaining,
|
||
calcClosePercentByRemaining: calcClosePercentByRemaining,
|
||
calcMarkClosePnl: calcMarkClosePnl,
|
||
calcUnwind: calcUnwind,
|
||
calcIncome: calcIncome
|
||
};
|
||
});
|