715 lines
41 KiB
JavaScript
715 lines
41 KiB
JavaScript
//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(),
|
||
}
|
||
});
|