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

404 lines
21 KiB
JavaScript

//otcformat禁止千分位分组
window.otcformat.options.disableGrouping = true;
const inputFormatSwapRate = swapPricePrecision.getCommonInputFormat('rate', { append: '%' });
const inputFormatTradePrice = Object.freeze({ precision: otcformat.trading.tradePrice.precision, append: '' });
const inputFormatEqvNotional = swapPricePrecision.getCommonInputFormat('amount', { append: '', negative: true });
const inputFormatInterestAmount = swapPricePrecision.getCommonInputFormat('amount', { append: '', negative: true });
const inputFormatDividend = 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 });
let ValueDate = model.ValueDate;
let MaxIncomeValueDate = model.MaxIncomeValueDate ? model.MaxIncomeValueDate.substr(0, 10) : ValueDate;
function parseLocalDate(value) {
if (!value) {
return null;
}
var parts = value.substr(0, 10).split('-');
if (parts.length !== 3) {
return null;
}
return new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
}
const vue = new Vue({
el: '#vueDiv',
data: {
deal: model,
floatPosition: null,
interestList: [],
marginList: [],
multiplier: 1,
initPosiNetPrice: 0,
initPosiGrossPrice:0
},
computed: {
maxUnwindDate() {
return MaxIncomeValueDate;
},
minStartDate() {
return this.deal.StartDate;
}
},
created() {
this.multiplier = this.deal.StructureType == '普通债券类收益互换' ? 100 : 1;
this.initDeal();
this.setValueDate();
},
mounted() {
this.$nextTick(() => {
var $incomeValueDatePicker = $(this.$refs.incomeValueDatePicker.$el);
$incomeValueDatePicker.datepicker("option", "maxDate", parseLocalDate(MaxIncomeValueDate));
if (this.isAfterMaxIncomeValueDate(this.deal.ValueDate)) {
this.validateIncomeValueDate(this.deal.ValueDate);
} else {
$incomeValueDatePicker.val(this.deal.ValueDate);
}
});
},
methods: {
formatAmount(value) {
return swapPricePrecision.formatCommon('amount', value);
},
formatQuantity(value) {
return swapPricePrecision.formatCommon('quantity', value, swapInstrumentType);
},
isAfterMaxIncomeValueDate(valueDate) {
return valueDate && MaxIncomeValueDate && valueDate > MaxIncomeValueDate;
},
validateIncomeValueDate(valueDate) {
if (!this.isAfterMaxIncomeValueDate(valueDate)) {
return true;
}
main.message("手动互换结算日期不能晚于当前交易结束日期T-1:" + MaxIncomeValueDate);
this.deal.ValueDate = MaxIncomeValueDate;
this.deal.UnwindDate = MaxIncomeValueDate;
this.floatPosition.UnwindDate = MaxIncomeValueDate;
$(this.$refs.incomeValueDatePicker.$el).val(MaxIncomeValueDate);
return false;
},
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.PosiNetPrice;
this.initPosiGrossPrice = this.floatPosition.PosiGrossPrice;
// 互换标的价格固定为期初净价,与平仓不同不需要用户填写
// 期初净价入库为相对价(如1.02),需转换为界面百分比形态(102),与平仓页保持一致
// 普通打开时用期初全价作为默认值;审批打开时必须保留已提交的期末全价,不能再被期初全价覆盖。
// 两种来源入库时都是相对价,债券统一 ×100 还原为界面百分比态。
this.floatPosition.TradingAmountAvg = SwapCalc.resolveIncomeTradingAmountAvg(
this.initPosiGrossPrice,
this.floatPosition.TradingAmountAvg,
this.multiplier,
isUseApproval);
this.interestList = model.FlowEvents.filter((item) => {
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 7 || item.InterestMode == 8 || item.InterestMode == 9;
});
this.marginList = model.FlowEvents.filter((item) => {
return item.InterestMode == 5 || item.InterestMode == 6;
});
this.TradeStartDate = model.TradeStartDate;
},
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.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.SwapCloseAmount = formatSwapAmount(this.deal.SwapCloseAmount);
//this.floatPosition.PosiNetPrice = otcformat.trading.umprice(this.floatPosition.PosiNetPrice);
//this.floatPosition.PosiGrossPrice = otcformat.trading.umprice(this.floatPosition.PosiGrossPrice);
this.floatPosition.TradingFee = formatSwapAmount(this.floatPosition.TradingFee);
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.InterestFee = formatSwapAmount(x.InterestFee);
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.InterestFee = formatSwapAmount(x.InterestFee);
x.InterestAmount = formatSwapAmount(x.InterestAmount);
x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
});
},
setValueDate(e) {//修改平仓日期
if (e) {
if (!this.validateIncomeValueDate(e)) {
e = MaxIncomeValueDate;
}
this.deal.ValueDate = e;
this.deal.UnwindDate = e;
this.floatPosition.UnwindDate = e;
}
if (!isUseApproval) {
this.getInterestList();
} else {
this.dataFormat();
}
},
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();
});
},
changeTradingFee() {//修改交易费用
this.calcFloatClosePnl();
},
changeInterestAmount(item) {//修改利息金额
let InterestFee = item.InterestFee == "" ? 0 : parseFloat(item.InterestFee);
let interestRatio = item.InterestDirection == 1 ? 1 : -1;
item.InterestClosePnL = formatSwapAmount(parseFloat(item.InterestAmount) * interestRatio + InterestFee * interestRatio);
this.calcCloseAmount();
},
calcFloatClosePnl() {//计算浮动端平仓盈亏
var thisObj = this;
let floatRatio = thisObj.floatPosition.PayDirection == 1 ? 1 : -1;
let longRatio = thisObj.floatPosition.PositionType == 1 ? 1 : -1;
let TradingFee = thisObj.floatPosition.TradingFee == "" ? 0 : parseFloat(thisObj.floatPosition.TradingFee);
let TradingFeePending = thisObj.floatPosition.TradingFeePending == "" ? 0 : parseFloat(thisObj.floatPosition.TradingFeePending);
let DividendIn = thisObj.floatPosition.DividendIn == "" ? 0 : parseFloat(thisObj.floatPosition.DividendIn ?? 0);
let deliveryPrice = thisObj.getStorageDeliveryPrice();
// 债券全价是单位价格,价差盈亏应按持仓数量×合约乘数计算;
// CloseNotionalValue 是期初全价折算后的名义本金,直接乘价差会重复包含期初价格。
let positionAmount = parseFloat(thisObj.floatPosition.Quantity) * parseFloat(thisObj.floatPosition.ContractSize || 1);
thisObj.floatPosition.MarkClosePnl = positionAmount * (deliveryPrice - thisObj.initPosiGrossPrice) * floatRatio * longRatio;
thisObj.floatPosition.MarkClosePnl = formatSwapAmount(thisObj.floatPosition.MarkClosePnl);//MarkClosePnl 纯盯市不要计算交易费用和分红
// 守卫: 浮动盈亏合计必须保留 2 位小数 → 对应历史 bug 3c5f25a5(原代码缺精度保留)
// 数值由 swapCalc.calcFloatPnlSum 计算, 此处 .toFixed(2) 仅保留字符串类型以兼容下游
thisObj.floatPosition.FloatPnlSum = SwapCalc.calcFloatPnlSum(thisObj.floatPosition.MarkClosePnl, TradingFee, TradingFeePending, DividendIn).toFixed(2);
thisObj.calcCloseAmount();
},
//calcClosePnL() {//计算浮动端平仓盈亏
// let pnl = parseFloat(this.floatPosition.ClosePnL) - parseFloat(this.floatPosition.TradingFee);
// this.floatPosition.ClosePnL = formatSwapAmount(pnl);
// this.calcCloseAmount();
//},
calcCloseAmount() {//计算平仓总额=浮动收取+利息收取-浮动支付-利息支付
var floatRatio = this.floatPosition.PayDirection == 1 ? 1 : -1;
var 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.CloseNotionalValue);
thisObj.floatPosition.CloseFee = TradingFee;
if (thisObj.deal.CloseQty > 0) {
thisObj.floatPosition.TradingAmountFeeAvg = deliveryPrice + (TradingFee / thisObj.deal.CloseQty) * floatRatio;
} else {
thisObj.floatPosition.TradingAmountFeeAvg = deliveryPrice;
}
this.interestList.forEach(x => {
//let interestRatio = x.InterestDirection == 1 ? 1 : -1;
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) ;
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.SwapCloseAmount = formatSwapAmount(thisObj.deal.SwapCloseAmount);
thisObj.deal.SwapRealizedPnL = formatSwapAmount(thisObj.deal.SwapRealizedPnL);
thisObj.deal.SwapMarginRebatePnl = formatSwapAmount(thisObj.deal.SwapMarginRebatePnl);
},
getInterestList() {//根据平仓日期获取利息腿信息
var thisObj = this;
var postData = { ValueDate: thisObj.deal.ValueDate, unwindDate: thisObj.deal.UnwindDate, tradeId: thisObj.deal.SwapTradeId, closePercent: 1, eventType:3 }
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 == 7 || item.InterestMode == 8 || item.InterestMode == 9;
});
thisObj.marginList = resp.obj.filter((item) => {
return item.InterestMode == 5 || item.InterestMode == 6;
});
thisObj.calcCloseAmount();
thisObj.dataFormat();
thisObj.getDivindIn();
});
},
getDivindIn() {
var thisObj = this;
// 方案C:分红改由后端 InitIncome 读 EOD PosiDividendSum 填入 floatPosition.DividendIn(单一可信源)。
// 前端不再调用 GetBondPayMentInterest 自算——消除"期初持仓×totalInterest"对已平仓部分的重复计入。
// floatPosition.DividendIn 保持后端返回值不动(=本次互换要落袋的全量待实现)。
// DividendPending(待结算)互换页保持 0:互换=全量结清,结清后账上无待结算。
// ↳ 与平仓页不同:平仓只拿走一部分,剩余持仓仍有待结算 → 平仓页 DividendPending=PosiDividendSum(后端值)。
thisObj.floatPosition.DividendPending = 0;
thisObj.calcFloatClosePnl();
thisObj.dataFormat();
},
incomeTrade() {//互换
var thisObj = this;
if (main.isEmpty(thisObj.deal.ValueDate)) {
main.message("请输入平仓日期");
return;
}
if (!thisObj.validateIncomeValueDate(thisObj.deal.ValueDate)) {
return;
}
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 = 0;
floatPosition.TradingAmountAvg = thisObj.getStorageDeliveryPrice();
reqObj.FlowEvents.push(floatPosition);
var postData = { unwindData: reqObj };
var msg = "确认提交收益结算?";
var postUrl = "/swaptrade2/SwapIncomeJson";
if (g_isShowReCheckClose) {
msg = "确认提交收益结算审核?";
postUrl = "/swaptrade2/ApplyUnwind";
postData.eventType = 3;//互换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(),
}
});
$(function () {
$("#baseTable").width($("#floatTable").width());
});