Files
zszq-trs/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js
T

715 lines
41 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.
//otcformat禁止千分位分组
window.otcformat.options.disableGrouping = true;
const inputFormatSwapRate = Object.freeze({ precision: 4, append: '%', trimTailZeros: false });
const inputFormatTradePrice = Object.freeze({ precision: otcformat.trading.tradePrice.precision, append: '' });
const inputFormatEqvNotional = swapPricePrecision.getCommonInputFormat('amount', { append: '', negative: true });
const inputFormatCloseAmount = swapPricePrecision.getCommonInputFormat('amount', { append: '', negative: true });
const swapInstrumentType = (model.FlowEvents || []).find(item => item && item.UnderlyingInstrumentType)?.UnderlyingInstrumentType || model.UnderlyingInstrumentType || '';
const formatSwapAmount = value => swapPricePrecision.normalizeCommon('amount', value);
const formatSwapQuantity = value => swapPricePrecision.normalizeCommon('quantity', value, swapInstrumentType);
const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' });
const inputFormatMarginRateNoPercent = Object.freeze({ precision: otcformat.trading.umpriceP.precision, append: '', percent: true });
const inputFormatSwapDeliveryPrice = Object.freeze({ precision: 9, append: '', negative: true });
// EQD-6953 期末标的结算收益率(ExitYtm):展示态百分数(如 6.3721)trimTailZeros:false 不去零;
// 精度由 getInputFormat('yield') 规则给 4 位——不得低于结算确认书导出的固定 4 位(0.0000)。
const inputFormatUnwindExitYtm = Object.freeze({ append: '', negative: true, trimTailZeros: false });
const consPosiFeeType = Object.freeze({ Percent: 0, Unit: 1 });
const swapPosiFeeCalc = {
normalizeFeeType(feeType) {
return Number(feeType) === consPosiFeeType.Unit ? consPosiFeeType.Unit : consPosiFeeType.Percent;
},
calcTradingFee(feeType, feeUnit, closeNotionalValue, closeQty) {
const normalizedFeeType = this.normalizeFeeType(feeType);
const normalizedFeeUnit = Number(feeUnit) || 0;
const normalizedCloseNotionalValue = Number(closeNotionalValue) || 0;
const normalizedCloseQty = Number(closeQty) || 0;
const tradingFee = normalizedFeeType === consPosiFeeType.Unit
? normalizedFeeUnit * normalizedCloseQty
: normalizedFeeUnit / 100 * normalizedCloseNotionalValue;
return formatSwapAmount(_.round(tradingFee, 2));
},
calcAllocatedTradingFee(totalFee, feeType, feeUnit, closeNotionalValue, closeQty, notionalValue, notionalQty) {
const normalizedFeeUnit = Number(feeUnit) || 0;
if (normalizedFeeUnit === 0) {
return null;
}
const normalizedFeeType = this.normalizeFeeType(feeType);
const closeBase = normalizedFeeType === consPosiFeeType.Unit ? Number(closeQty) || 0 : Number(closeNotionalValue) || 0;
const originalBase = normalizedFeeType === consPosiFeeType.Unit ? Number(notionalQty) || 0 : Number(notionalValue) || 0;
if (originalBase <= 0) {
return null;
}
return formatSwapAmount(_.round((Number(totalFee) || 0) * closeBase / originalBase, 2));
},
calcTradingFeePending(beforeCloseFee, feeType, feeUnit, closeNotionalValue, closeQty, notionalValue, notionalQty, closePercent) {
const allocatedFee = this.calcAllocatedTradingFee(
beforeCloseFee, feeType, feeUnit, closeNotionalValue, closeQty, notionalValue, notionalQty);
if (allocatedFee !== null) {
return allocatedFee;
}
return (Number(beforeCloseFee) || 0) * (Number(closePercent) || 0);
}
};
let ValueDate = model.ValueDate;
const vue = new Vue({
el: '#vueDiv',
data: {
deal: model,
floatPosition: null,
interestList: [],
marginList: [],
initPosiNetPrice: 0,
multiplier: 1,
// EQD-6953 簿记模板=普通债券类收益互换 时启用 期末交割全价↔结算收益率(ExitYtm) 互算
isBondTRS: false,
// 平仓比例展示/输入均为"占期初(original)"语义(A):默认与每次重开都基于原始名义本金。
// oriClosePercent = 剩余名义本金/期初名义本金 = 最多可平比例(不能平超过剩余持仓)。
oriClosePercent: 1,
},
computed: {
maxUnwindDate() {
return ValueDate;
},
minStartDate() {
return this.deal.StartDate;
},
// EQD-6953:簿记模板为普通债券类收益互换 且 浮动腿标的为债券 时,才展示 源/AUTO/REV 标识并允许互算
isBondUnwindLeg() {
return this.isBondTRS && !!this.floatPosition && this.IsBond(this.floatPosition.UnderlyingInstrumentType);
}
},
created() {
this.isBondTRS = this.deal.StructureType == '普通债券类收益互换';
this.multiplier = this.isBondTRS ? 100 : 1;
this.initDeal();
this.setValueDate(this.deal.ValueDate);
},
methods: {
formatAmount(value) {
return swapPricePrecision.formatCommon('amount', value);
},
formatQuantity(value) {
return swapPricePrecision.formatCommon('quantity', value, swapInstrumentType);
},
getQuantityInputFormat() {
return swapPricePrecision.getCommonInputFormat('quantity', { append: '' }, swapInstrumentType);
},
getDeliveryPriceInputFormat() {
return swapPricePrecision.getInputFormat(
this.floatPosition && this.floatPosition.UnderlyingInstrumentType,
'grossPrice',
inputFormatSwapDeliveryPrice);
},
getStorageDeliveryPrice() {
return swapPricePrecision.roundForSubmit(
swapPricePrecision.shiftDecimal(this.floatPosition.TradingAmountAvg, this.multiplier === 100 ? -2 : 0),
this.floatPosition && this.floatPosition.UnderlyingInstrumentType,
'grossPrice',
this.multiplier === 100 ? 2 : 0);
},
initDeal() {
var positions = model.FlowEvents.filter((item) => {
return item.UnderlyingCode;
});
this.floatPosition = positions[0];
this.initPosiNetPrice = this.floatPosition.PosiGrossPrice;
this.interestList = model.FlowEvents.filter((item) => {
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 9;
});
this.marginList = model.FlowEvents.filter((item) => {
return item.InterestMode == 5 || item.InterestMode == 6;
});
this.TradeStartDate = model.TradeStartDate;
// 最多可平比例(占期初口径) = 剩余名义本金 / 期初名义本金;分母为 0 时兜底为 1
this.oriClosePercent = (this.deal.NotionalValue && this.deal.PosiNotionalValue)
? this.deal.PosiNotionalValue / this.deal.NotionalValue : 1;
// 转换期末标的价格为百分比形式
if (this.floatPosition.TradingAmountAvg) {
this.floatPosition.TradingAmountAvg = swapPricePrecision.shiftDecimal(this.floatPosition.TradingAmountAvg, this.multiplier === 100 ? 2 : 0);
}
},
IsBond(instType) {
return tradeHelper.IsBond(instType);
},
priceFormat(price) {
price = swapPricePrecision.shiftDecimal(price, this.multiplier === 100 ? 2 : 0);
var pricef = swapPricePrecision.format(price, this.floatPosition && this.floatPosition.UnderlyingInstrumentType, 'grossPrice');
return pricef;
},
dataFormat() {
this.deal.NotionalValue = formatSwapAmount(this.deal.NotionalValue);
this.deal.PosiNotionalValue = formatSwapAmount(this.deal.PosiNotionalValue);
this.deal.CloseNotionalValue = formatSwapAmount(this.deal.CloseNotionalValue);
this.deal.NotionalQty = formatSwapQuantity(this.deal.NotionalQty);
this.deal.PositionQty2 = formatSwapQuantity(this.deal.PositionQty);
this.floatPosition.Quantity = formatSwapQuantity(this.floatPosition.Quantity);
this.floatPosition.PositionQty = formatSwapQuantity(this.floatPosition.PositionQty);
this.deal.CloseQty = formatSwapQuantity(this.deal.CloseQty);
this.deal.ClosePercent = otcformat.fixed6(this.deal.ClosePercent);
this.deal.SwapCloseAmount = formatSwapAmount(this.deal.SwapCloseAmount);
//this.floatPosition.PosiNetPrice = otcformat.trading.tradeSinglePrice(this.floatPosition.PosiNetPrice);
//this.floatPosition.PosiGrossPrice = otcformat.trading.tradeSinglePrice(this.floatPosition.PosiGrossPrice);
this.floatPosition.TradingAmountAvg = swapPricePrecision.roundForSubmit(
this.floatPosition.TradingAmountAvg,
this.floatPosition.UnderlyingInstrumentType,
'grossPrice');
// EQD-6953 期末结算收益率:提交前统一 4 位四舍五入(幂等;空/非数保持原样不覆盖)
if (UnwindBondCalc.hasValue(this.floatPosition.ExitYtm)) {
this.floatPosition.ExitYtm = UnwindBondCalc.roundExitYtm(this.floatPosition.ExitYtm);
}
this.floatPosition.TradingFee = formatSwapAmount(this.floatPosition.TradingFee);
this.floatPosition.TradingFeePending = formatSwapAmount(this.floatPosition.TradingFeePending);
this.floatPosition.DividendIn = formatSwapAmount(this.floatPosition.DividendIn);
this.floatPosition.MarkClosePnl = formatSwapAmount(this.floatPosition.MarkClosePnl);
this.interestList.forEach(x => {
//x.Principal = formatSwapAmount(x.Principal);
//x.Rate = otcformat.fixed6(x.Rate);
x.InterestAmount = formatSwapAmount(x.InterestAmount);
x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
//x.InterestStartDate = x.InterestStartDate ? x.InterestStartDate.substr(0, 10) : "";
//x.InterestEndDate = x.InterestEndDate ? x.InterestEndDate.substr(0, 10) : "";
});
this.marginList.forEach(x => {
x.InterestAmount = formatSwapAmount(x.InterestAmount);
x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
});
},
setValueDate(e) {//修改事件日期,并同步平仓日期
if (e) {
this.deal.ValueDate = e;
this.deal.UnwindDate = e;
this.floatPosition.UnwindDate = e;
}
// EQD-6953 估值日(平仓日)变了,已算出的期末结算收益率随之失效:清值+清标识,待用户重新回车计算
// applyManualEdit(null) 只清 源/AUTO/REV 不动任何数值;勿用 applyCalcFailure(null)——
// 它对 null driver 会误清 TradingAmountAvg
if (this.isBondUnwindLeg && this.floatPosition.ExitYtm != null) {
this.floatPosition.ExitYtm = null;
UnwindBondCalc.applyManualEdit(this.floatPosition, null);
this.syncUnwindBondFlags();
}
if (!isUseApproval) {
// 新增:刷新持仓基线(处理公司行为除权)
this.refreshUnwindBaseline();
this.getInterestList();
//this.refreshUnderlyingPrice();
} else {
this.dataFormat();
}
},
setUnwindDate(e) {//修改平仓日期,并同步事件日期
this.setValueDate(e);
},
changeCloseMethod() {//修改平仓类型
if (this.deal.CloseMethod == 1) {
this.deal.ClosePercent = this.oriClosePercent;
this.deal.CloseNotionalValue = formatSwapAmount(parseFloat(this.deal.PosiNotionalValue));
this.deal.CloseQty = this.deal.PositionQty;
} else {
// ClosePercent 是占期初口径(A),需除以 oriClosePercent 转占剩余(B) 再乘剩余数量
this.deal.CloseQty = this.calcCloseQtyByPercent(this.deal.ClosePercent);
}
this.calcTradingFeePending();
this.refreshTradingFeeByUnit();
this.getInterestList();
this.calcFloatClosePnl();
},
// 按"占期初口径(A)"的 ClosePercent 反算平仓数量:CloseQty = PositionQty × (ClosePercent / oriClosePercent)
// 多次部分平仓后必须这样转换,否则全部↔部分切换时 ClosePercent 没变但 CloseQty 会变(不自洽)
// 使用 swapCalc.calcCloseQtyByOriginalPercent 的 roundHalfAwayFromZero 避免 JS 浮点精度偏差
// (如 32500000*(0.5/0.65)=24999999.999999996 而非 25000000
calcCloseQtyByPercent(closePercent) {
return SwapCalc.calcCloseQtyByOriginalPercent(closePercent, this.oriClosePercent, this.deal.PositionQty);
},
calcTradingFeePending() {
this.floatPosition.TradingFeePending = swapPosiFeeCalc.calcTradingFeePending(
this.floatPosition.BeforeCloseFee,
this.floatPosition.PosiFeeType,
this.floatPosition.PosiTradingFeeUnit,
this.deal.CloseNotionalValue,
this.deal.CloseQty,
this.deal.NotionalValue,
this.deal.NotionalQty,
this.deal.ClosePercent);
},
refreshTradingFeeByUnit() {
this.floatPosition.TradingFee = swapPosiFeeCalc.calcTradingFee(
this.floatPosition.PosiFeeType,
this.floatPosition.PosiTradingFeeUnit,
this.deal.CloseNotionalValue,
this.deal.CloseQty);
},
changeCloseQty() {//修改平仓数量
if (parseFloat(this.deal.CloseQty) > parseFloat(this.deal.PositionQty)) {
main.message("平仓数量不能超过持仓数量");
return;
}
// CloseQty/PositionQty 得占剩余(B),× oriClosePercent 转回占期初(A)
var ori = parseFloat(this.oriClosePercent) || 0;
this.deal.ClosePercent = otcformat.fixed6((parseFloat(this.deal.CloseQty) / parseFloat(this.deal.PositionQty)) * ori);
if (parseFloat(this.deal.CloseQty) == parseFloat(this.deal.PositionQty)) {
this.deal.CloseMethod = 1;
} else {
this.deal.CloseMethod = 2;
}
// 占期初口径:平仓名义本金 = 平仓比例 × 期初名义本金(NotionalValue)
this.deal.CloseNotionalValue = formatSwapAmount(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.NotionalValue));
this.calcTradingFeePending();
this.refreshTradingFeeByUnit();
this.getInterestList();
this.calcFloatClosePnl();
},
changeClosePercent() {//修改平仓比例
if (parseFloat(this.deal.ClosePercent) > this.oriClosePercent) {
main.message("平仓比例不能超过" + this.oriClosePercent * 100 + "%");
this.deal.ClosePercent = this.oriClosePercent;
return;
}
// 调试埋点(?otcdebug=1):记录用户改后的平仓比例,便于定位"改了比例利息腿却不动"的前端入口
if (window.otcDebug) window.otcDebug.log('[unwind] changeClosePercent → 平仓比例=', this.deal.ClosePercent,
' oriClosePercent=', this.oriClosePercent, ' 占期初口径');
this.deal.CloseQty = this.calcCloseQtyByPercent(this.deal.ClosePercent);
// 占期初口径:平仓名义本金 = 平仓比例 × 期初名义本金(NotionalValue)
this.deal.CloseNotionalValue = formatSwapAmount(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.NotionalValue));
if (parseFloat(this.deal.ClosePercent) == parseFloat(this.oriClosePercent)) {
this.deal.CloseMethod = 1;
} else {
this.deal.CloseMethod = 2;
}
this.calcTradingFeePending();
this.refreshTradingFeeByUnit();
this.getInterestList();
this.calcFloatClosePnl();
},
changeCloseNotionalValue() {//修改平仓名义本金
if (parseFloat(this.deal.CloseNotionalValue) > parseFloat(this.deal.PosiNotionalValue)) {
main.message("平仓名义本金不能超过持仓名义本金");
this.deal.CloseNotionalValue = this.deal.PosiNotionalValue;
return;
}
// 占期初口径:平仓比例 = 平仓名义本金 / 期初名义本金(NotionalValue)
this.deal.ClosePercent = otcformat.fixed6(parseFloat(this.deal.CloseNotionalValue) / parseFloat(this.deal.NotionalValue));
this.deal.CloseQty = this.calcCloseQtyByPercent(this.deal.ClosePercent);
if (parseFloat(this.deal.ClosePercent) == parseFloat(this.oriClosePercent)) {
this.deal.CloseMethod = 1;
} else {
this.deal.CloseMethod = 2;
}
this.calcTradingFeePending();
this.refreshTradingFeeByUnit();
this.getInterestList();
this.calcFloatClosePnl();
},
changeUnderlyingPrice() {//修改标的价格
this.calcFloatClosePnl();
},
refreshUnderlyingPrice() {//刷新标的价格
var thisObj = this;
main.post("/underlying_manager/GetUnderlyingPriceByCode",
{ code: thisObj.floatPosition.UnderlyingCode, valuedate: thisObj.deal.ValueDate })
.done(function (res) {
thisObj.floatPosition.TradingAmountAvg = swapPricePrecision.roundForSubmit(
swapPricePrecision.shiftDecimal(res.obj, thisObj.multiplier === 100 ? 2 : 0),
thisObj.floatPosition.UnderlyingInstrumentType,
'grossPrice');
thisObj.calcFloatClosePnl();
});
},
//==================================================================================
// EQD-6953 期末标的交割全价 ↔ 期末标的结算收益率(ExitYtm) 互算
// 交互对齐录入页(swapTradeEdit.js):回车=以该字段为源调 /Bond/CalcBond 反算对方;
// 失败=保留源字段、清对方、标识全清;手填未回车=REV 不联动(允许计算有问题时手工覆盖)。
// 关键差异见 unwindBondCalc.js 头注:平仓页两字段均为展示态(不 ×100/÷100),估值日=平仓日 ValueDate。
//==================================================================================
getYieldInputFormat() {
return swapPricePrecision.getInputFormat(
this.floatPosition && this.floatPosition.UnderlyingInstrumentType,
'yield',
inputFormatUnwindExitYtm);
},
//期末交割全价的 input(失焦/回车都会触发):保持既有盈亏联动;债券腿按交互约定3标 REV。
onEndDeliveryPriceInput() {
this.changeUnderlyingPrice();
if (this.isBondUnwindLeg) {
UnwindBondCalc.applyManualEdit(this.floatPosition, 'DP');
this.syncUnwindBondFlags();
}
},
//收益率字段编辑(失焦/回车触发,组件内部回车时 enter 随后修正标识):约定3 标 REV 不联动。
onEndBondPriceEdit(type) {
if (!this.isBondUnwindLeg) return;
UnwindBondCalc.applyManualEdit(this.floatPosition, type);
this.syncUnwindBondFlags();
},
//按键实时标 REV(组件 input 事件只在失焦/回车触发,逐键输入期间靠 keydown 清 源/AUTO)。
//回车/修饰键/导航键/功能键不改标识(SwapCalc.isBondPriceValueKey 白名单)。
onEndBondPriceKeydown(type, event) {
if (!this.isBondUnwindLeg) return;
if (!SwapCalc.isBondPriceValueKey(event)) return;
UnwindBondCalc.applyManualEdit(this.floatPosition, type);
this.syncUnwindBondFlags();
},
//回车=以该字段为源调计算器反算对方字段。
onEndBondPriceEnter(type) { // type: 'DP'期末交割全价 / 'YD'期末结算收益率
if (!this.isBondUnwindLeg) return;
if (!this.floatPosition.UnderlyingCode) { main.message("请先选择债券标的"); return; }
var price = type === 'DP' ? this.floatPosition.TradingAmountAvg : this.floatPosition.ExitYtm;
if (!UnwindBondCalc.hasValue(price)) return; // 空/非数:不触发计算器
this.calcUnwindBond(type);
},
//以 driver(回车字段)为源调 /Bond/CalcBond。成败路由见 base/main.js __post:业务错误走 reject
//失败落地必须在 .fail(录入页曾把失败处理只写 .done 导致静默失效,勿重蹈)。
calcUnwindBond(driver) {
if (!this.isBondUnwindLeg) return;
var fp = this.floatPosition;
fp.bondDriverType = driver; // 先定源,回写时 applyCalcResult 会跳过该字段
var price = driver === 'DP' ? fp.TradingAmountAvg : fp.ExitYtm;
if (!UnwindBondCalc.hasValue(price)) {
UnwindBondCalc.applyCalcFailure(fp, driver);
this.syncUnwindBondFlags();
return;
}
if (!this.deal.ValueDate) {
main.message("请先填写平仓日期(作为债券计算器估值日)");
UnwindBondCalc.applyCalcFailure(fp, driver);
this.syncUnwindBondFlags();
return;
}
var req = UnwindBondCalc.getCalcRequest(fp.UnderlyingCode, price, driver, this.deal.ValueDate);
otcDebug.log('[unwindBondCalc] enter driver=' + driver + ' req=' + JSON.stringify(req));
var self = this;
main.post('/Bond/CalcBond', req, { alertFn: main.message }).done(function (resp) {
if (!resp || !resp.obj) { // 网络错误/响应异常:框架已提示,按约定2清对方字段+标识
UnwindBondCalc.applyCalcFailure(fp, driver);
self.syncUnwindBondFlags();
return;
}
var err = SwapCalc.getBondCalcErrorMessage(resp.obj);
if (err) { // 业务错误(债券不存在/信息不全/参数非法/值域离谱):toast 去重提示,不回写
if (SwapCalc.shouldShowBondErr(fp, err)) main.message(err);
UnwindBondCalc.applyCalcFailure(fp, driver);
self.syncUnwindBondFlags();
otcDebug.log('[unwindBondCalc] calc-error driver=' + driver + ' msg=' + err);
return;
}
fp._lastBondErr = null;
UnwindBondCalc.applyCalcResult(fp, resp.obj, driver);
UnwindBondCalc.applyCalcSuccess(fp, driver);
self.syncUnwindBondFlags();
// YD 为源时 TradingAmountAvg 被反算回填,直接赋值不触发 input 事件,须显式重算盈亏
if (driver === 'YD') self.calcFloatClosePnl();
otcDebug.log('[unwindBondCalc] success driver=' + driver +
' ExitYtm=' + fp.ExitYtm + ' TradingAmountAvg=' + fp.TradingAmountAvg +
' auto=' + JSON.stringify(fp.bondAuto));
}).fail(function () {
UnwindBondCalc.applyCalcFailure(fp, driver);
self.syncUnwindBondFlags();
otcDebug.log('[unwindBondCalc] post-fail/reject driver=' + driver);
});
},
//确保 bondDriverType/bondAuto/bondRev 的变更触发 Vue 2 响应式更新(同录入页 syncBondFlags
//这些属性不在后端模型里,$set + 全新对象引用 + $forceUpdate 兜底,缺一视图不刷新)。
syncUnwindBondFlags() {
var fp = this.floatPosition;
this.$set(fp, 'bondDriverType', fp.bondDriverType === undefined ? null : fp.bondDriverType);
this.$set(fp, 'bondAuto', { DP: !!(fp.bondAuto && fp.bondAuto.DP), YD: !!(fp.bondAuto && fp.bondAuto.YD) });
this.$set(fp, 'bondRev', { DP: !!(fp.bondRev && fp.bondRev.DP), YD: !!(fp.bondRev && fp.bondRev.YD) });
this.$forceUpdate();
},
calcFloatClosePnl() {//计算浮动端平仓盈亏
var thisObj = this;
let floatRatio = UnwindLegSign.payDirectionSign(thisObj.floatPosition.PayDirection);
let longRatio = UnwindLegSign.positionTypeSign(thisObj.floatPosition.PositionType);
let TradingFee = thisObj.floatPosition.TradingFee == "" ? 0 : parseFloat(thisObj.floatPosition.TradingFee);
let TradingFeePending = thisObj.floatPosition.TradingFeePending == "" ? 0 : parseFloat(thisObj.floatPosition.TradingFeePending);
let deliveryPrice = thisObj.getStorageDeliveryPrice();
thisObj.floatPosition.MarkClosePnl = Math.round(thisObj.deal.CloseQty * (deliveryPrice - thisObj.initPosiNetPrice) * floatRatio * longRatio * 10000) / 10000;
thisObj.floatPosition.MarkClosePnl = Number(thisObj.floatPosition.MarkClosePnl.toFixed(2));//MarkClosePnl 纯盯市不要计算交易费用和分红
thisObj.floatPosition.MarkClosePnl = formatSwapAmount(thisObj.floatPosition.MarkClosePnl);
thisObj.floatPosition.FloatPnlSum = (parseFloat(thisObj.floatPosition.MarkClosePnl) + TradingFee + TradingFeePending + parseFloat(thisObj.floatPosition.DividendIn)).toFixed(2);
thisObj.calcCloseAmount();
},
changeTradingFee() {//修改交易费用
this.calcFloatClosePnl();
},
changeInterestAmount(item) {//修改利息金额
let interestRatio = UnwindLegSign.interestPnlSign(item.InterestDirection);
item.InterestClosePnL = formatSwapAmount(parseFloat(item.InterestAmount) * interestRatio + parseFloat(item.InterestFee) * interestRatio);
this.calcCloseAmount();
},
//calcClosePnL() {//计算浮动端平仓盈亏
// let pnl = parseFloat(this.floatPosition.ClosePnL) - parseFloat(this.floatPosition.TradingFee);
// this.floatPosition.ClosePnL = formatSwapAmount(pnl);
// this.calcCloseAmount();
//},
calcCloseAmount() {//计算平仓总额=浮动收取+利息收取-浮动支付-利息支付
let floatRatio = UnwindLegSign.payDirectionSign(this.floatPosition.PayDirection);
let ratio = UnwindLegSign.positionTypeSign(this.floatPosition.PositionType);
let thisObj = this;
let pnl = parseFloat(this.floatPosition.FloatPnlSum);
let TradingFee = thisObj.floatPosition.TradingFee == "" ? 0 : parseFloat(thisObj.floatPosition.TradingFee);
thisObj.deal.SwapCloseAmount = pnl;
thisObj.deal.SwapRealizedPnL = pnl;
thisObj.deal.SwapMarginRebatePnl = 0;
thisObj.deal.SwapMarginAmount = 0;
let deliveryPrice = thisObj.getStorageDeliveryPrice();
thisObj.floatPosition.TradingAmount = deliveryPrice * parseFloat(thisObj.deal.CloseQty);
thisObj.floatPosition.CloseFee = TradingFee;
if (thisObj.deal.CloseQty == 0) {
thisObj.floatPosition.TradingAmountFeeAvg = 0;
} else {
thisObj.floatPosition.TradingAmountFeeAvg = deliveryPrice + (TradingFee / thisObj.deal.CloseQty) * ratio;
}
this.interestList.forEach(x => {
let interestAmount = parseFloat(x.InterestClosePnL);
thisObj.deal.SwapCloseAmount = parseFloat(thisObj.deal.SwapCloseAmount) + interestAmount;
thisObj.deal.SwapRealizedPnL = parseFloat(thisObj.deal.SwapRealizedPnL) + interestAmount;
});
this.marginList.forEach(x => {
let interestAmount = parseFloat(x.InterestClosePnL);
// 保证金返还本金符号与利息盈亏同枚举反号(见 unwindLegSign.js 头注——业务事实勿统一)
let interestRatio = UnwindLegSign.marginRebatePrincipalSign(x.InterestDirection);
thisObj.deal.SwapCloseAmount = parseFloat(thisObj.deal.SwapCloseAmount) + interestAmount;
thisObj.deal.SwapMarginRebatePnl = parseFloat(thisObj.deal.SwapMarginRebatePnl) + interestAmount;
thisObj.deal.SwapRealizedPnL = parseFloat(thisObj.deal.SwapRealizedPnL) + interestAmount;
thisObj.deal.SwapMarginAmount = parseFloat(thisObj.deal.SwapMarginAmount) + parseFloat(x.InterestPrincipal) * interestRatio;
});
thisObj.deal.SwapRealizedPnL = Number(thisObj.deal.SwapRealizedPnL.toFixed(2));
thisObj.deal.SwapCloseAmount = Number(thisObj.deal.SwapCloseAmount.toFixed(2));
thisObj.deal.SwapCloseAmount = formatSwapAmount(thisObj.deal.SwapCloseAmount);
thisObj.deal.SwapRealizedPnL = formatSwapAmount(thisObj.deal.SwapRealizedPnL);
thisObj.deal.SwapMarginRebatePnl = formatSwapAmount(thisObj.deal.SwapMarginRebatePnl);
thisObj.deal.SwapMarginAmount = formatSwapAmount(thisObj.deal.SwapMarginAmount);
},
refreshUnwindBaseline() {//刷新持仓基线(处理公司行为除权)
var thisObj = this;
var postData = {
tradeId: thisObj.deal.SwapTradeId,
valueDate: thisObj.deal.ValueDate
};
main.post("/swaptrade2/RefreshUnwindBaseline", postData, { async: true }).done(function (resp) {
if (resp.success) {
var data = resp.obj;
// 更新持仓数量和名义本金
thisObj.deal.PositionQty = data.PositionQty;
thisObj.deal.PositionQty2 = data.PositionQty; // 新增:同时更新 PositionQty2(界面显示用)
thisObj.deal.PosiNotionalValue = data.PosiNotionalValue;
// 如果是全平,更新平仓数量和名义本金
if (thisObj.deal.CloseMethod == 1) { // 全部平仓
thisObj.deal.CloseQty = data.CloseQty;
thisObj.deal.CloseNotionalValue = data.CloseNotionalValue;
}
// 更新浮动腿的期初价格和数量
if (thisObj.floatPosition) {
thisObj.floatPosition.PosiGrossPrice = data.PosiGrossPrice;
thisObj.floatPosition.PosiNetPrice = data.PosiNetPrice;
thisObj.initPosiNetPrice = data.PosiGrossPrice;
// 更新浮动腿的持仓数量(底部表格显示用)
thisObj.floatPosition.PositionQty = data.PositionQty;
}
// 重新计算最多可平比例
thisObj.oriClosePercent = (thisObj.deal.NotionalValue && thisObj.deal.PosiNotionalValue)
? thisObj.deal.PosiNotionalValue / thisObj.deal.NotionalValue : 1;
// 如果是全平,更新平仓比例
if (thisObj.deal.CloseMethod == 1) {
thisObj.deal.ClosePercent = thisObj.oriClosePercent;
}
// 强制 Vue 更新界面
thisObj.$forceUpdate();
// 可选:显示提示信息
if (data.IsRestored && window.otcDebug) {
console.log('[公司行为] 已根据除权后的EOD基线刷新持仓数据', {
PositionQty: data.PositionQty,
PosiGrossPrice: data.PosiGrossPrice
});
}
}
});
},
getInterestList() {//根据平仓日期获取利息腿信息
var thisObj = this;
// closePercent 按"占期初(original)"语义(A)传给后端,由 GetUnwindInterestList 转为"占剩余(B)"计算
var postData = { valueDate: thisObj.deal.ValueDate, unwindDate: thisObj.deal.ValueDate, tradeId: thisObj.deal.SwapTradeId, closePercent: thisObj.deal.ClosePercent, eventType: 2, notionalValue: thisObj.deal.NotionalValue, posiNotionalValue: thisObj.deal.PosiNotionalValue, isPenaltyInterest: thisObj.deal.IsPenaltyInterest == true } // EQD-6977 是否罚息:预览即含罚息(其他费用含罚息)
// 调试埋点(?otcdebug=1):记录实际发给后端的平仓比例——未来若"改比例利息腿不动",
// 对比此处请求比例 与 下方返回各腿 principal/amount 是否随比例变化,即可定位是前端没传对还是后端没缩放。
if (window.otcDebug) window.otcDebug.log('[unwind] getInterestList → POST closePercent=', thisObj.deal.ClosePercent,
' closeNotionalValue=', thisObj.deal.CloseNotionalValue, ' posiNotionalValue=', thisObj.deal.PosiNotionalValue);
main.post("/swaptrade2/GetUnwindInterestList", postData, { async: true }).done(function (resp) {
thisObj.interestList = resp.obj.filter((item) => {
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 9;
});
thisObj.marginList = resp.obj.filter((item) => {
return item.InterestMode == 5 || item.InterestMode == 6;
});
// 调试埋点(?otcdebug=1):逐腿打印 mode/principal/amount/rate,定位哪条腿不随平仓比例缩放
// (如 mode=1 固定值腿在 GLMS 缺陷中曾恒为全量、不随比例变化)。
if (window.otcDebug) window.otcDebug.log('[unwind] getInterestList ← 返回利息腿=', thisObj.interestList.map(function (i) {
return { mode: i.InterestMode, principal: i.InterestPrincipal, amount: i.InterestAmount, rate: i.InterestRate };
}));
thisObj.calcCloseAmount();
thisObj.dataFormat();
thisObj.getDivindIn();
});
},
getDivindIn() {
var thisObj = this;
// 方案C:分红改由后端 InitUnwind 读 EOD PosiDividendSum 填入 floatPosition.DividendIn 与 DividendPending
// (单一可信源)。前端不再调用 GetBondPayMentInterest 自算——消除"期初持仓×totalInterest"对已平仓
// 部分的重复计入(GLMS-20260105-0004 平仓前部分平仓40%后,再平仓时分红误显 -36,160,应为 0)。
// ⚠ floatPosition.DividendIn / DividendPending 均保持后端返回值不动,前端不得覆盖:
// - DividendIn(本次落袋)、DividendPending(待结算存量=PosiDividendSum 全量口径)。
// - 互换页 DividendPending 保持 0(互换=全量结清,结清后待结算归0),见 incomeSwapTrade.js。
// - 历史:曾硬编码 DividendPending=0,对 PosiDividendSum≠0 的部分平仓会落库错误的 0(回归)。
thisObj.calcFloatClosePnl();
thisObj.dataFormat();
},
closeTrade() {//平仓
var thisObj = this;
if (main.isEmpty(thisObj.deal.ValueDate)) {
main.message("请输入平仓日期");
return;
}
if (thisObj.deal.CloseType == 1) {//数量平仓方式
if (parseFloat(thisObj.deal.CloseQty) > parseFloat(thisObj.deal.PositionQty)) {
main.message("平仓数量不能持仓数量");
return;
}
if (parseFloat(this.deal.CloseQty) <= 0) {
main.message("平仓数量不能小于或等于0");
return;
}
} else {
if (parseFloat(this.deal.ClosePercent) > 1) {
main.message("平仓比例不能超过100%");
return;
}
if (parseFloat(this.deal.CloseNotionalValue) > parseFloat(this.deal.PosiNotionalValue)) {
main.message("平仓名义本金不能超过持仓名义本金");
return;
}
}
thisObj.deal.UnwindDate = thisObj.deal.ValueDate;
let reqObj = _.cloneDeep(thisObj.deal);
let marginCloneList = _.cloneDeep(thisObj.marginList);
reqObj.FlowEvents = _.cloneDeep(thisObj.interestList);
marginCloneList.forEach((item) => {
reqObj.FlowEvents.push(item);
})
thisObj.floatPosition.EventDate = thisObj.deal.ValueDate;
let floatPosition = _.cloneDeep(thisObj.floatPosition);
floatPosition.Quantity = reqObj.CloseQty;
floatPosition.TradingAmountAvg = thisObj.getStorageDeliveryPrice();
reqObj.FlowEvents.push(floatPosition);
var postData = { unwindData: reqObj };
var msg = "确认提交平仓?";
var postUrl = "/swaptrade2/SwapUnwindJson";
if (g_isShowReCheckClose) {
msg = "确认提交平仓审核?";
postUrl = "/swaptrade2/ApplyUnwind";
postData.eventType = 2;//互换3,平仓2
}
main.confirm(msg,
function () {
//重新计算百分比
var thisObj2 = thisObj;
main.post(postUrl, postData).done(function (res) {
if (res.success) {
thisObj2.closetrade_cashWindow();
}
else {
try {
thisObj2.closetrade_cashWindow();
} catch (e) {
}
}
});
});
},
getSumbitText: function () {
return g_isShowReCheckClose ? "审核提交" : "保存";
},
submitApproval(status) {
var pop = '';
if (status === 'pass') {
pop = "确认通过审批?";
}
if (status === 'reject') {
pop = "确认拒绝?";
}
let thisObj = this;
var confirmFunc = function (additionalProcessing) {
var pData = { tradeId: thisObj.deal.SwapTradeId, status: status, text: "" };
if (!main.isEmpty(additionalProcessing)) {
pData.additionalProcessing = additionalProcessing;
}
var thisObj2 = thisObj;
main.post("/processtradelog/UpdateTradeProcessLog", pData).done(
function (data) {
if (data.obj && data.obj.proccessType == "AdditionalProcessing") {
if (data.obj.type == "LackOfMoney") {
var htmlContent = `<div style="padding:10px">${data.obj.message}</div>`;
var lackMoneyConfirmLayer = main.open2("提示",
htmlContent,
{
area: ["430px", "175px"],
btn: ['交易特批', '取消'],
yes: function (index, layero) {
var layerIndex = lackMoneyConfirmLayer;
main.confirm("客户资金或授信不足,强制成交会导致本机构产生风险!要继续审批通过?", function () {
layer.close(layerIndex);
confirmFunc("LackOfMoney");
});
},
cancel: function (index, layero) {
if (window.parent && window.parent.reloadtrade) {
thisObj2.closetrade_cashWindow();
}
(parent || window).layer.closeAll();
}
});
}
return;
}
(parent || window).main.message(data.msg);
try { thisObj2.closetrade_cashWindow(); }
catch (e) { }
if (parent) {
parent.layer.closeAll();
}
});
}
main.confirm(pop, confirmFunc);
},
closetrade_cashWindow: function () {
layer.closeMe('reloadData');
},
closeCurrentWindow: function () {
try {
if (window.parent && window.parent.reload) window.parent.reload();
} catch (e) {
}
try {
var layer = window.parent.layer;
layer.close(layer.getFrameIndex(window.name));
} catch (e) {
}
}
},
components: {
'vue-datepicker': FastVue.vueDatePicker(),
'vue-number-input': FastVue.vueNumberInput(),
'vue-swap-price-input': swapPricePrecision.createVueInputComponent(),
}
});