@@ -197,6 +199,8 @@
期初标的价格 |
期末标的交割全价% |
期末标的价格 |
+ @* EQD-6953 期末标的结算收益率:仅普通债券类收益互换显示,期末交割全价回车反算,可手填覆盖 *@
+
期末标的结算收益率% |
数量 |
交易费用(平仓) |
交易费用(待结算) |
@@ -223,11 +227,17 @@
{{priceFormat(floatPosition.PosiGrossPrice)}} |
-
+
+ 源AUTOREV
|
+ @* EQD-6953 期末标的结算收益率:回车以它为源反算交割全价(联动盈亏);手填未回车=REV人工输入 *@
+
+
+ 源AUTOREV
+ |
{{formatQuantity(deal.CloseQty)}} |
diff --git a/YLErpWeb/fe-tests/closePercentInterestRefresh.test.js b/YLErpWeb/fe-tests/closePercentInterestRefresh.test.js
index eeeb4310..5d31102f 100644
--- a/YLErpWeb/fe-tests/closePercentInterestRefresh.test.js
+++ b/YLErpWeb/fe-tests/closePercentInterestRefresh.test.js
@@ -148,6 +148,15 @@ function loadVueApp(model, mockPost) {
return Number(Number(positionQty) * (Number(closePercent) / ori).toFixed(6));
}
},
+ // EQD-6953:平仓页现于 unwindSwapTrade.js 之前加载 unwindBondCalc.js(见 SwapUnwind.cshtml),
+ // dataFormat 提交取整会引用其守卫;此处给同款行为的最小 mock(真实实现由 unwindBondCalc.test.js 覆盖)
+ UnwindBondCalc: {
+ hasValue(v) { return v !== null && v !== undefined && v !== '' && !isNaN(v); },
+ roundExitYtm(v) { return v; },
+ applyManualEdit(s) { return s; }
+ },
+ // 收付方向符号:纯函数零依赖,直接喂真实模块(映射冻结由 unwindLegSign.test.js 覆盖)
+ UnwindLegSign: require('../wwwroot/Scripts/app/swaptrade/unwindLegSign.js'),
_: {
round(value, precision) {
return Number(Number(value || 0).toFixed(precision || 0));
diff --git a/YLErpWeb/fe-tests/jest.config.js b/YLErpWeb/fe-tests/jest.config.js
index d60ebdc6..f6877998 100644
--- a/YLErpWeb/fe-tests/jest.config.js
+++ b/YLErpWeb/fe-tests/jest.config.js
@@ -23,8 +23,13 @@ module.exports = {
moduleDirectories: ['node_modules', '../wwwroot/Scripts'],
// 覆盖率配置(--coverage 时生效)
+ // 已知问题:源码在 rootDir(fe-tests) 之外,babel 提供器因 transform:{} 无插桩器、
+ // v8 提供器对 rootDir 逃逸路径无法归因(jest 29.7 实测,含 roots/绝对路径变体),
+ // 覆盖率表恒为 0 且门槛不触发。清单仍保留以固化意图,真实修复需重构 rootDir。
collectCoverageFrom: [
'../wwwroot/Scripts/app/swaptrade/swapCalc.js',
+ '../wwwroot/Scripts/app/swaptrade/unwindBondCalc.js',
+ '../wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js',
'../wwwroot/Scripts/fast/fastVue.base.js',
// 逐步加入更多文件
],
diff --git a/YLErpWeb/fe-tests/swapPricePrecisionHelper.test.js b/YLErpWeb/fe-tests/swapPricePrecisionHelper.test.js
new file mode 100644
index 00000000..d30ae970
--- /dev/null
+++ b/YLErpWeb/fe-tests/swapPricePrecisionHelper.test.js
@@ -0,0 +1,178 @@
+/**
+ * swapPricePrecisionHelper.test.js — 字符串精确十进制核心的特征测试
+ * ============================================================================
+ * 目的:swapPricePrecisionHelper.js 的组件事件(swapPriceInput.component.test.js)与
+ * 配置接线(swapPrecisionConfig.test.js)已有测试,但【字符串精确十进制核心】
+ * (shiftDecimal / roundDecimal / multiplyDecimal / getRule / format /
+ * roundForSubmit) 一直只有间接覆盖。精度即资损,这里用 golden 值冻结现状:
+ * 1. 全部走字符串数位运算,绕开 IEEE754 浮点陷阱(0.1*0.2、1.005.toFixed);
+ * 2. roundDecimal 是【绝对值上四舍五入】= AwayFromZero,与 unwindBondCalc
+ * 的 ExitYtm 约定同向;
+ * 3. roundDecimal 不补零(1.2 → '1.2'),补零是 formatCommon/formatFixed 的职责;
+ * 4. roundDecimal 对非法输入/负精度【原样返回不归一】,调用方别拿它当校验器;
+ * 5. getRule 的覆盖优先级:先查 defaults 有无该品种(没有直接 null),再看
+ * main.swapPricePrecision 覆盖,覆盖非法则回落 defaults。
+ *
+ * 做法:直接 require 源文件(jest v8 覆盖率据此插桩,readFileSync+new Function
+ * 的沙箱加载会让覆盖率统计看不见);配置覆盖用 global.main.swapPricePrecision
+ * 注入——模块内 global 绑定到 globalThis,与本测试文件的 global 同源,
+ * beforeEach/afterEach 清理防串扰。
+ *
+ * 运行:cd YLErpWeb/fe-tests && npx jest swapPricePrecisionHelper
+ */
+const helper = require('../wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js');
+
+function withConfig(config, extras) {
+ if (config !== undefined) global.main = { swapPricePrecision: config };
+ if (extras && extras.otcformat) global.otcformat = extras.otcformat;
+}
+
+beforeEach(() => { delete global.main; delete global.otcformat; });
+afterEach(() => { delete global.main; delete global.otcformat; });
+
+describe('shiftDecimal:纯数位平移,不经浮点', () => {
+
+ test('右移补零 / 左移进小数', () => {
+ expect(helper.shiftDecimal('1.5', 1)).toBe('15');
+ expect(helper.shiftDecimal('1.5', -1)).toBe('0.15');
+ expect(helper.shiftDecimal('12.34', -3)).toBe('0.01234');
+ expect(helper.shiftDecimal('0.001', -2)).toBe('0.00001');
+ expect(helper.shiftDecimal('5', 2)).toBe('500');
+ });
+
+ test('保留符号;places=0 或非整数只做归一化', () => {
+ expect(helper.shiftDecimal('-1.5', 1)).toBe('-15');
+ expect(helper.shiftDecimal('1.50', 0)).toBe('1.50'); // 不动尾巴零
+ expect(helper.shiftDecimal('1.50', 1.5)).toBe('1.50');
+ });
+
+ test('归一化先行:首尾零/符号/科学计数法', () => {
+ expect(helper.shiftDecimal('007.50', 0)).toBe('7.50');
+ expect(helper.shiftDecimal('+7', 0)).toBe('7');
+ expect(helper.shiftDecimal('-0.000', 0)).toBe('0.000'); // 负零坍缩为无符号
+ expect(helper.shiftDecimal('1e-7', 0)).toBe('0.0000001');
+ expect(helper.shiftDecimal('1.23e5', 0)).toBe('123000');
+ });
+
+ test('非法输入返回 null,空白串返回空串', () => {
+ expect(helper.shiftDecimal('abc', 2)).toBeNull();
+ expect(helper.shiftDecimal('1.2.3', 2)).toBeNull();
+ expect(helper.shiftDecimal('', 2)).toBe('');
+ });
+});
+
+describe('roundDecimal:绝对值四舍五入(AwayFromZero),字符串路径无浮点陷阱', () => {
+
+ test('经典浮点陷阱对照:1.005 与 0.1×0.2 场景字符串路径给正确答案', () => {
+ expect(Number(1.005).toFixed(2)).toBe('1.00'); // 浮点路径错(冻结对照)
+ expect(helper.roundDecimal('1.005', 2)).toBe('1.01');
+ expect(helper.roundDecimal('2.345', 2)).toBe('2.35');
+ expect(helper.roundDecimal('2.344', 2)).toBe('2.34');
+ });
+
+ test('负数远离零(与 ExitYtm 约定同向)', () => {
+ expect(helper.roundDecimal('-2.345', 2)).toBe('-2.35');
+ expect(helper.roundDecimal('-2.5', 0)).toBe('-3');
+ expect(helper.roundDecimal('-0.00005', 4)).toBe('-0.0001');
+ });
+
+ test('进位链:跨数量级与跨整数位', () => {
+ expect(helper.roundDecimal('9.99', 1)).toBe('10.0');
+ expect(helper.roundDecimal('9.99', 0)).toBe('10');
+ expect(helper.roundDecimal('99.999', 2)).toBe('100.00');
+ expect(helper.roundDecimal('0.00005', 4)).toBe('0.0001');
+ });
+
+ test('不补零也不主动去零:位数不足原样返回,已有位数保留尾巴零', () => {
+ expect(helper.roundDecimal('1.2', 4)).toBe('1.2'); // 不足4位不补零(补零是 formatCommon 的职责)
+ expect(helper.roundDecimal('3.10000', 4)).toBe('3.1000'); // 已到4位:只取整不去零(去零是 format 的职责)
+ });
+
+ test('防御姿态:负精度/非法输入原样返回,不做校验', () => {
+ expect(helper.roundDecimal('2.345', -1)).toBe('2.345');
+ expect(helper.roundDecimal('2.345', 1.5)).toBe('2.345');
+ expect(helper.roundDecimal('abc', 2)).toBe('abc');
+ });
+});
+
+describe('multiplyDecimal:字符串精确乘法', () => {
+
+ test('0.1×0.2 精确为 0.02(浮点给 0.020000000000000004)', () => {
+ expect(helper.multiplyDecimal('0.1', '0.2')).toBe('0.02');
+ expect(helper.multiplyDecimal('0.1', '0.1')).toBe('0.01');
+ expect(helper.multiplyDecimal('1.1', '1.1')).toBe('1.21');
+ });
+
+ test('符号组合;保留小数标度(1.5×2=3.0 而非 3)', () => {
+ expect(helper.multiplyDecimal('1.5', '2')).toBe('3.0');
+ expect(helper.multiplyDecimal('-1.5', '2')).toBe('-3.0');
+ expect(helper.multiplyDecimal('-1.5', '-2')).toBe('3.0');
+ expect(helper.multiplyDecimal('-0.03', '-0.02')).toBe('0.0006');
+ });
+
+ test('大整数精确(超出 Number.MAX_SAFE_INTEGER)', () => {
+ expect(helper.multiplyDecimal('123456789', '987654321')).toBe('121932631112635269');
+ });
+
+ test('非法输入 null;空串按 0 处理', () => {
+ expect(helper.multiplyDecimal('abc', '1')).toBeNull();
+ expect(helper.multiplyDecimal('', '1')).toBe('0');
+ });
+});
+
+describe('getRule / getInputFormat / roundForSubmit:字段规则解析与覆盖优先级', () => {
+ test('defaults 字段级规则:债券三字段 vs 股票兜底', () => {
+ expect(helper.getRule('Bond', 'yield')).toEqual({ integerDigits: 2, precision: 4 });
+ expect(helper.getRule('Bond', 'grossPrice')).toEqual({ integerDigits: 6, precision: 9 });
+ // 债券顶层没有 integerDigits/precision:字段拼错 → null(后续走 umprice 兜底),
+ // 而 Stock 顶层有 → 字段拼错回落品种级规则。这个不对称冻结于此。
+ expect(helper.getRule('Bond', 'unknownField')).toBeNull();
+ expect(helper.getRule('Stock', 'anything')).toEqual({ integerDigits: 7, precision: 2 });
+ expect(helper.getRule('NoSuchType', 'yield')).toBeNull();
+ });
+
+ test('main.swapPricePrecision 覆盖优先于 defaults,非法覆盖回落 defaults', () => {
+ withConfig({ Bond: { yield: { integerDigits: 3, precision: 5 } } });
+ expect(helper.getRule('Bond', 'yield')).toEqual({ integerDigits: 3, precision: 5 });
+
+ withConfig({ Bond: { yield: { integerDigits: 0 } } });
+ const bad = helper;
+ expect(bad.getRule('Bond', 'yield')).toEqual({ integerDigits: 2, precision: 4 });
+
+ // defaults 里没有的品种,配置了也不认:覆盖只允许白名单内微调
+ withConfig({ BrandNewType: { yield: { integerDigits: 3, precision: 5 } } });
+ const unknown = helper;
+ expect(unknown.getRule('BrandNewType', 'yield')).toBeNull();
+ });
+
+ test('getInputFormat 合并 options', () => {
+ expect(helper.getInputFormat('Bond', 'yield', { negative: true }))
+ .toEqual({ negative: true, integerDigits: 2, precision: 4 });
+ expect(helper.getInputFormat('NoSuchType', 'yield', { negative: true }))
+ .toEqual({ negative: true });
+ });
+
+ test('roundForSubmit:按规则精度+offset 取整;空值/无规则原样返回', () => {
+ expect(helper.roundForSubmit('3.14159265', 'Bond', 'yield')).toBe('3.1416');
+ expect(helper.roundForSubmit('3.14159265', 'Bond', 'yield', 2)).toBe('3.141593');
+ expect(helper.roundForSubmit('', 'Bond', 'yield')).toBe('');
+ expect(helper.roundForSubmit('1.2', 'NoSuchType', 'yield')).toBe('1.2');
+ expect(helper.roundForSubmit('3.14159265', 'Bond', 'yield', 2.5)).toBe('3.1416'); // 非整数offset忽略
+ });
+});
+
+describe('format:字段级展示态(取整+去尾巴零)', () => {
+ test('按字段规则取整并去尾零', () => {
+ expect(helper.format('3.14159', 'Bond', 'yield')).toBe('3.1416');
+ expect(helper.format('3.10000', 'Bond', 'yield')).toBe('3.1');
+ expect(helper.format('99.5', 'Stock', 'whatever')).toBe('99.5');
+ expect(helper.format('', 'Bond', 'yield')).toBe('');
+ });
+
+ test('无规则时回落 otcformat.trading.umprice', () => {
+ withConfig(undefined, {
+ otcformat: { trading: { umprice: function (v) { return 'UM:' + v; } } }
+ });
+ expect(helper.format('1.23', 'NoSuchType', 'yield')).toBe('UM:1.23');
+ });
+});
diff --git a/YLErpWeb/fe-tests/unwindBondCalc.test.js b/YLErpWeb/fe-tests/unwindBondCalc.test.js
new file mode 100644
index 00000000..2937f9a2
--- /dev/null
+++ b/YLErpWeb/fe-tests/unwindBondCalc.test.js
@@ -0,0 +1,145 @@
+/**
+ * unwindBondCalc.test.js — 平仓页 DP↔YD 两字段互算纯函数(EQD-6953)
+ * ============================================================================
+ * 冻结三条与录入页不同、极易被"顺手统一"改坏的约定:
+ * 1. 发计算器的价格是【展示态直传】,不 ×100(录入页是存储态 ×100);
+ * 2. ExitYtm 保留 4 位、四舍五入(AwayFromZero)——结算确认书导出固定 4 位不去零,
+ * 界面精度不得低于导出要求;
+ * 3. 失败只清【对方】字段(平仓页无净价列,仅 DP/YD 两字段)。
+ *
+ * 运行:cd YLErpWeb/fe-tests && npx jest unwindBondCalc
+ */
+const UnwindBondCalc = require('../wwwroot/Scripts/app/swaptrade/unwindBondCalc.js');
+
+const CALC = { cleanPrice: 98.5, dirtyPrice: 99.5, ytm: 6.3721 };
+
+describe('getCalcRequest:展示态直传,不做录入页的 ×100 换算', () => {
+ test('DP 回车:price 原样 99.5(若误加 ×100 会变成 9950),priceType=DP,估值日=平仓日', () => {
+ const req = UnwindBondCalc.getCalcRequest('240004.IB', 99.5, 'DP', '2026-08-20');
+ expect(req.price).toBe(99.5);
+ expect(req.priceType).toBe('DP');
+ expect(req.targetDate).toBe('2026-08-20');
+ expect(req.underlyingCode).toBe('240004.IB');
+ expect(req.source).toBe('unwind');
+ });
+
+ test('YD 回车:收益率展示态直传(6.3721 而非 0.063721)', () => {
+ const req = UnwindBondCalc.getCalcRequest('240004.IB', 6.3721, 'YD', '2026-08-20');
+ expect(req.price).toBe(6.3721);
+ expect(req.priceType).toBe('YD');
+ });
+
+ test('估值日为空时 targetDate 为 null(兜底,调用方应先校验)', () => {
+ const req = UnwindBondCalc.getCalcRequest('240004.IB', 99.5, 'DP', null);
+ expect(req.targetDate).toBeNull();
+ });
+});
+
+describe('roundExitYtm:4 位小数、四舍五入(远离零)', () => {
+ test('第 5 位进位', () => {
+ expect(UnwindBondCalc.roundExitYtm(6.37215)).toBe(6.3722);
+ });
+ test('第 5 位舍去', () => {
+ expect(UnwindBondCalc.roundExitYtm(6.37214)).toBe(6.3721);
+ });
+ test('负收益率同样远离零(负利率债)', () => {
+ expect(UnwindBondCalc.roundExitYtm(-0.00005)).toBe(-0.0001);
+ expect(UnwindBondCalc.roundExitYtm(-1.23445)).toBe(-1.2345);
+ });
+});
+
+describe('applyCalcResult:源字段保持、仅回写对方字段', () => {
+ test('DP 为源:TradingAmountAvg 保持手输值,ExitYtm 被反算覆盖(4位)', () => {
+ const state = { TradingAmountAvg: 100.123456, ExitYtm: null };
+ UnwindBondCalc.applyCalcResult(state, { ytm: 6.37215 }, 'DP');
+ expect(state.TradingAmountAvg).toBe(100.123456); // 源:保持
+ expect(state.ExitYtm).toBe(6.3722); // 对方:反算+4位取整
+ });
+
+ test('YD 为源:ExitYtm 保持手输值,TradingAmountAvg 被反算覆盖(展示态直写)', () => {
+ const state = { TradingAmountAvg: null, ExitYtm: 6.3721 };
+ UnwindBondCalc.applyCalcResult(state, { dirtyPrice: 99.5023 }, 'YD');
+ expect(state.ExitYtm).toBe(6.3721); // 源:保持
+ expect(state.TradingAmountAvg).toBe(99.5023); // 对方:直写(不 ÷100)
+ });
+
+ test('计算器未返回对方值时不覆盖(保留手工输入)', () => {
+ const state = { TradingAmountAvg: 100.5, ExitYtm: 6.3 };
+ UnwindBondCalc.applyCalcResult(state, { ytm: null }, 'DP');
+ expect(state.ExitYtm).toBe(6.3);
+ UnwindBondCalc.applyCalcResult(state, { dirtyPrice: undefined }, 'YD');
+ expect(state.TradingAmountAvg).toBe(100.5);
+ });
+
+ test('回写值与现值相同(<1e-9)时不写,避免光标跳动', () => {
+ const state = { TradingAmountAvg: 100, ExitYtm: 6.3721 };
+ UnwindBondCalc.applyCalcResult(state, { ytm: 6.3721 }, 'DP');
+ expect(state.ExitYtm).toBe(6.3721);
+ });
+});
+
+describe('applyCalcFailure:保留源字段、只清对方字段、标识全清', () => {
+ test('DP 为源失败:ExitYtm 清空、全价保留', () => {
+ const state = { TradingAmountAvg: 100.5, ExitYtm: 6.3, bondDriverType: 'DP', bondAuto: { DP: true, YD: false } };
+ UnwindBondCalc.applyCalcFailure(state, 'DP');
+ expect(state.TradingAmountAvg).toBe(100.5);
+ expect(state.ExitYtm).toBeNull();
+ expect(state.bondDriverType).toBeNull();
+ expect(state.bondAuto).toEqual({ DP: false, YD: false });
+ expect(state.bondRev).toEqual({ DP: false, YD: false });
+ });
+
+ test('YD 为源失败:TradingAmountAvg 清空、收益率保留', () => {
+ const state = { TradingAmountAvg: 100.5, ExitYtm: 6.3, bondDriverType: 'YD' };
+ UnwindBondCalc.applyCalcFailure(state, 'YD');
+ expect(state.ExitYtm).toBe(6.3);
+ expect(state.TradingAmountAvg).toBeNull();
+ });
+
+ test('driver 非法(null):仅清标识、不动任何数值(setValueDate 清陈旧标识场景的防误清守卫)', () => {
+ const state = { TradingAmountAvg: 100.5, ExitYtm: 6.3, bondDriverType: 'DP', bondAuto: { DP: false, YD: true } };
+ UnwindBondCalc.applyCalcFailure(state, null);
+ expect(state.TradingAmountAvg).toBe(100.5); // 绝不能被清
+ expect(state.ExitYtm).toBe(6.3);
+ expect(state.bondDriverType).toBeNull();
+ expect(state.bondAuto).toEqual({ DP: false, YD: false });
+ });
+});
+
+describe('标识状态机(交互约定2/3,对齐录入页)', () => {
+ test('成功:源字段标源、对方标 AUTO、REV 清空', () => {
+ const state = { TradingAmountAvg: 100.5, ExitYtm: null };
+ UnwindBondCalc.applyCalcSuccess(state, 'DP');
+ expect(state.bondDriverType).toBe('DP');
+ expect(state.bondAuto).toEqual({ DP: false, YD: true });
+ expect(state.bondRev).toEqual({ DP: false, YD: false });
+ });
+
+ test('手动编辑未回车:清 源/AUTO、本字段累计 REV、不清对方 REV', () => {
+ const state = { TradingAmountAvg: 100.5, ExitYtm: 6.3, bondRev: { DP: true, YD: false } };
+ UnwindBondCalc.applyManualEdit(state, 'YD');
+ expect(state.bondDriverType).toBeNull();
+ expect(state.bondAuto).toEqual({ DP: false, YD: false });
+ expect(state.bondRev).toEqual({ DP: true, YD: true }); // 累计:DP 的 REV 保留
+ });
+
+ test('bondRev 缺失时自动初始化', () => {
+ const state = {};
+ UnwindBondCalc.applyManualEdit(state, 'DP');
+ expect(state.bondRev).toEqual({ DP: true, YD: false });
+ });
+});
+
+describe('FIELDS 映射与 hasValue 守卫', () => {
+ test('字段映射', () => {
+ expect(UnwindBondCalc.FIELDS).toEqual({ DP: 'TradingAmountAvg', YD: 'ExitYtm' });
+ });
+ test('hasValue:空串/null/undefined/NaN 为 false,0 为 true', () => {
+ expect(UnwindBondCalc.hasValue('')).toBe(false);
+ expect(UnwindBondCalc.hasValue(null)).toBe(false);
+ expect(UnwindBondCalc.hasValue(undefined)).toBe(false);
+ expect(UnwindBondCalc.hasValue(NaN)).toBe(false);
+ expect(UnwindBondCalc.hasValue(0)).toBe(true);
+ expect(UnwindBondCalc.hasValue('6.37')).toBe(true);
+ });
+});
diff --git a/YLErpWeb/fe-tests/unwindLegSign.test.js b/YLErpWeb/fe-tests/unwindLegSign.test.js
new file mode 100644
index 00000000..3258511c
--- /dev/null
+++ b/YLErpWeb/fe-tests/unwindLegSign.test.js
@@ -0,0 +1,47 @@
+/**
+ * unwindLegSign.test.js — 平仓页收付方向符号映射(隐式约定显式化)
+ * ============================================================================
+ * 冻结的领域事实(2026-08-21 业务确认):
+ * 利息腿(融资成本)与保证金腿(返息/返还本金)方向【必须相反】——
+ * 利息是买方持有标的向交易商融资的成本(买方付出去的钱);
+ * 保证金是客户自己交的抵押金,返息/返还是把客户自己的钱退回来。
+ * 两者对同一 InterestDirection 枚举符号互为镜像,这是业务事实而非笔误,
+ * 任何人"顺手统一"这两个符号都会翻转保证金返还方向(资损级 bug)。
+ *
+ * 另冻结:PayDirection==1 → +1、PositionType==1(多头) → +1 的浮动端/多空符号。
+ *
+ * 运行:cd YLErpWeb/fe-tests && npx jest unwindLegSign
+ */
+const UnwindLegSign = require('../wwwroot/Scripts/app/swaptrade/unwindLegSign.js');
+
+describe('收付方向符号:各枚举映射', () => {
+ test('利息盈亏(利息腿/保证金腿通用):收取(1)→+1,支付(其他)→-1', () => {
+ expect(UnwindLegSign.interestPnlSign(1)).toBe(1);
+ expect(UnwindLegSign.interestPnlSign(0)).toBe(-1);
+ expect(UnwindLegSign.interestPnlSign(null)).toBe(-1);
+ });
+
+ test('保证金腿返还本金:与利息腿同枚举反号(返的是客户自己的钱)', () => {
+ expect(UnwindLegSign.marginRebatePrincipalSign(1)).toBe(-1);
+ expect(UnwindLegSign.marginRebatePrincipalSign(0)).toBe(1);
+ expect(UnwindLegSign.marginRebatePrincipalSign(null)).toBe(1);
+ });
+
+ test('浮动端收付:PayDirection==1(收取)→+1;多空:PositionType==1(多头)→+1', () => {
+ expect(UnwindLegSign.payDirectionSign(1)).toBe(1);
+ expect(UnwindLegSign.payDirectionSign(0)).toBe(-1);
+ expect(UnwindLegSign.positionTypeSign(1)).toBe(1);
+ expect(UnwindLegSign.positionTypeSign(0)).toBe(-1);
+ });
+});
+
+describe('不变量:利息腿与保证金腿符号互为镜像(业务事实,禁止统一)', () => {
+ test('同一 InterestDirection 下两符号之和恒为 0', () => {
+ [1, 0, null, undefined, 2].forEach(dir => {
+ expect(
+ UnwindLegSign.interestPnlSign(dir)
+ + UnwindLegSign.marginRebatePrincipalSign(dir)
+ ).toBe(0);
+ });
+ });
+});
diff --git a/YLErpWeb/fe-tests/unwindSwapTrade.test.js b/YLErpWeb/fe-tests/unwindSwapTrade.test.js
index f0774f96..488a0c3b 100644
--- a/YLErpWeb/fe-tests/unwindSwapTrade.test.js
+++ b/YLErpWeb/fe-tests/unwindSwapTrade.test.js
@@ -66,6 +66,8 @@ function loadUnwindHelpers() {
roundHalfAwayFromZero(value) { return value; },
calcCloseQtyByOriginalPercent() { return 0; }
},
+ // 收付方向符号:纯函数零依赖,直接喂真实模块(映射冻结由 unwindLegSign.test.js 覆盖)
+ UnwindLegSign: require('../wwwroot/Scripts/app/swaptrade/unwindLegSign.js'),
_: {
round(value, precision) {
return Number(Number(value || 0).toFixed(precision || 0));
diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/tradeEndConfirmList.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/tradeEndConfirmList.js
index 6f82f664..f6ee26e4 100644
--- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/tradeEndConfirmList.js
+++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/tradeEndConfirmList.js
@@ -167,21 +167,32 @@ function SendEmailEitherReport() {
main.message("请选择普通交易");
return;
}
- var url = "/trs_hub_api/swap/email/settle/batchSend";
- var postData = { swapFlowEventIds: selIds }
+ // EQD-5320: 改走本系统后端代理(/swaptrade2/BatchSendSettleEmail → BondOmsInterface_BaseUrl → bond-oms),
+ // 与债券计算器同一条服务端出口;原直连 /trs_hub_api 反向代理在未配 nginx 的环境 404
+ var url = "/swaptrade2/BatchSendSettleEmail";
+ var postData = { swapFlowEventIds: selIds.join(',') }
$.ajax({
type: "post",
url: url,
- data: JSON.stringify(postData),
+ data: postData,
dataType: "json",
- contentType:"application/json",
success: function (res) {
- if (res.success) {
- main.message("发送成功");
+ // 兜底非标准返回结构,绝不让点击"无反应"
+ if (res && res.success) {
+ main.message(res.msg || ("发送成功(共" + selIds.length + "笔)"));
+ SearchClick(); // 刷新列表以反映发送状态
} else {
- main.message(res.message);
+ var msg = (res && (res.msg || res.message)) || "发送失败:服务返回异常结构";
+ main.message(msg);
+ console.error("[批量发送确认书] 业务失败:", res);
}
},
+ error: function (xhr, textStatus, errThrown) {
+ // 请求层失败:超时/返回非JSON(如被登录页重定向)
+ var detail = "HTTP " + xhr.status + " " + (textStatus || "") + (errThrown ? " " + errThrown : "");
+ main.message("发送请求失败(" + detail + "):请检查服务是否可用,详情见控制台与网络面板");
+ console.error("[批量发送确认书] 请求失败:", detail, xhr);
+ },
beforeSend(jqXHR) {
main.waitMe(true);
},
diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindBondCalc.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindBondCalc.js
new file mode 100644
index 00000000..4c52f009
--- /dev/null
+++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindBondCalc.js
@@ -0,0 +1,132 @@
+/**
+ * unwindBondCalc.js — 平仓页 期末标的交割全价 ↔ 期末标的结算收益率(ExitYtm) 两字段互算(EQD-6953)
+ * ============================================================================
+ * 与录入页(swapTradeEdit.js + swapCalc.js 债券三字段互算)的关键差异,【勿"顺手统一"】:
+ * 1. 单位状态:录入页三字段(CP/DP/YD)存【存储态小数】(0.995),发计算器前 ×100、回写 ÷100
+ * (bondPriceToCalc/bondCalcPriceToStorage);平仓页 TradingAmountAvg 在 initDeal 已转
+ * 【展示态百分数】(99.5),ExitYtm 亦定义为展示态(6.3721)——发计算器不换算、回写不换算。
+ * 提交时的存储态转换仍由既有 getStorageDeliveryPrice()/dataFormat() 负责。
+ * 2. 字段集:只有 DP(TradingAmountAvg)/YD(ExitYtm) 两个字段(平仓页无净价列),
+ * 失败只清对方字段(录入页 applyBondCalcFailure 清另外两个,字段名也不同,故不复用)。
+ * 3. 估值日:用平仓日 deal.ValueDate(结算语义);录入页用互换起始日 StartDate。
+ * 4. 精度:ExitYtm 保留 4 位小数、四舍五入(AwayFromZero)——结算确认书导出要求
+ * 固定 4 位不去零(ToString("0.0000")),界面精度不得低于该要求。
+ *
+ * 浏览器:挂 window.UnwindBondCalc(需在 swapCalc.js 之后、unwindSwapTrade.js 之前加载)。
+ * Node:module.exports(UMD),供 fe-tests/unwindBondCalc.test.js 使用。
+ * 命名遵《互换价格字段命名规范决策文档》:平仓/了结时点 = Exit(非 End/Close/Final)。
+ */
+(function (root, factory) {
+ if (typeof module === 'object' && module.exports) {
+ module.exports = factory(require('./swapCalc.js'));
+ } else {
+ root.UnwindBondCalc = factory(root.SwapCalc);
+ }
+})(typeof self !== 'undefined' ? self : this, function (SwapCalc) {
+ 'use strict';
+
+ // driver(回车的"源"字段) → 平仓页浮动腿(floatPosition)字段名
+ var FIELDS = { DP: 'TradingAmountAvg', YD: 'ExitYtm' };
+
+ function hasValue(v) {
+ return v !== null && v !== undefined && v !== '' && !isNaN(v);
+ }
+
+ /**
+ * 构造 /Bond/CalcBond 请求体。
+ * ⚠️ price 必须传【展示态百分数】直传(99.5),不做录入页那种 ×100——
+ * 平仓页模型里两个字段本就是展示态。此约定由 fe-tests 冻结,防止后人误加换算。
+ * source='unwind':BondController 记进日志,便于区分录入页/平仓页来源定位问题。
+ */
+ function getCalcRequest(underlyingCode, price, driver, valueDate) {
+ return {
+ underlyingCode: underlyingCode,
+ price: Number(price),
+ priceType: driver,
+ targetDate: valueDate || null,
+ source: 'unwind'
+ };
+ }
+
+ /**
+ * ExitYtm 统一取整:4 位、四舍五入(远离零),对齐后端 decimal ToString("0.0000")。
+ * 非数值(null/undefined/NaN)返回 null——供 write 跳过,绝不能把 null 取整成 0 回写
+ * (否则计算器未返回 ytm 时会清掉用户手工输入)。
+ */
+ function roundExitYtm(value) {
+ if (value === null || value === undefined || isNaN(value)) return null;
+ return SwapCalc.roundHalfAwayFromZero(value, 4);
+ }
+
+ /**
+ * 计算成功回写(交互约定1+2):源字段保持用户手输值,仅回写对方字段(展示态直写)。
+ * - driver='DP'(全价回车)→ 回写 ExitYtm = ytm 取整 4 位;计算器未返回 ytm 则不覆盖。
+ * - driver='YD'(收益率回车)→ 回写 TradingAmountAvg = dirtyPrice;未返回则不覆盖。
+ * ⚠️ 调用方回写 TradingAmountAvg 后须显式重算平仓盈亏(calcFloatClosePnl)。
+ * 纯函数,jest 可直接测。
+ */
+ function applyCalcResult(state, calc, driver) {
+ function write(field, value) {
+ if (value === undefined || value === null) return; // 计算器未返回则不覆盖
+ var cur = state[field];
+ if (typeof cur === 'number' && Math.abs(cur - value) < 1e-9) return; // 无变化不写
+ state[field] = value;
+ }
+ if (driver === 'DP') {
+ write('ExitYtm', roundExitYtm(calc && calc.ytm));
+ } else if (driver === 'YD') {
+ write('TradingAmountAvg', calc && calc.dirtyPrice);
+ }
+ return state;
+ }
+
+ /**
+ * 计算失败落地(交互约定2):保留源字段值(用户可就地改),清对方字段、清全部标识。
+ * 与录入页差异:只清"对方"一个字段(录入页清另外两个)。
+ * driver 非法(null/未知)时仅清标识、不动任何数值——防调用方误清源字段
+ * (如 setValueDate 只想清陈旧标识时误传 null,会把 TradingAmountAvg 清掉)。
+ */
+ function applyCalcFailure(state, driver) {
+ if (driver === 'DP' || driver === 'YD') {
+ var other = driver === 'DP' ? 'YD' : 'DP';
+ state[FIELDS[other]] = null;
+ }
+ state.bondDriverType = null;
+ state.bondAuto = { DP: false, YD: false };
+ state.bondRev = { DP: false, YD: false };
+ return state;
+ }
+
+ /** 计算成功后的标识(交互约定2):源字段标"源",对方标"AUTO"。 */
+ function applyCalcSuccess(state, driver) {
+ state.bondDriverType = driver;
+ state.bondAuto = { DP: driver !== 'DP', YD: driver !== 'YD' };
+ state.bondRev = { DP: false, YD: false };
+ return state;
+ }
+
+ /**
+ * 编辑未回车(失焦/按键)的标识(交互约定3):清 源/AUTO,仅本字段累计标"REV",
+ * 不联动对方字段(允许计算有问题时手工覆盖 ExitYtm)。同录入页:累计 REV 不互清。
+ */
+ function applyManualEdit(state, type) {
+ state.bondDriverType = null;
+ state.bondAuto = { DP: false, YD: false };
+ if (!state.bondRev || typeof state.bondRev !== 'object') {
+ state.bondRev = { DP: false, YD: false };
+ }
+ if (type) state.bondRev[type] = true;
+ return state;
+ }
+
+ return {
+ FIELDS: FIELDS,
+ hasValue: hasValue,
+ getCalcRequest: getCalcRequest,
+ roundExitYtm: roundExitYtm,
+ applyCalcResult: applyCalcResult,
+ applyCalcFailure: applyCalcFailure,
+ applyCalcSuccess: applyCalcSuccess,
+ applyManualEdit: applyManualEdit
+ };
+});
diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindLegSign.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindLegSign.js
new file mode 100644
index 00000000..a07d62cd
--- /dev/null
+++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindLegSign.js
@@ -0,0 +1,45 @@
+/**
+ * unwindLegSign.js — 平仓页收付方向符号(纯函数)
+ * ============================================================================
+ * 把散落在 unwindSwapTrade.js 各计算式里的裸三元(xxx==1 ? ±1 : ∓1)显式化为命名函数。
+ * 【为什么必须显式化】利息盈亏与保证金返还本金对同一 InterestDirection 枚举符号相反——
+ * 这是业务事实而非笔误,任何"顺手统一"都会翻转保证金返还方向(资损级 bug):
+ * 利息盈亏(利息腿/保证金腿通用,changeInterestAmount 对两类行都生效):
+ * 收益互换中买方为持有标的向交易商融资,利息=融资成本(买方付出去的钱),
+ * InterestDirection==1(收取) → 盈亏记 +1;
+ * 保证金返还本金:保证金是客户自己交的抵押金,返息/返还本金是把客户自己的钱退回来,
+ * 本金流方向与利息(成本)相反 → 同枚举符号取镜像 -1。
+ *
+ * 其余两个符号:PayDirection==1(浮动端收取) → +1;PositionType==1(多头) → +1。
+ *
+ * 加载:浏览器 script 标签(先于 unwindSwapTrade.js);Node:module.exports 供 jest。
+ */
+var UnwindLegSign = (function () {
+ 'use strict';
+
+ // Number()===1 与页面原 ==1 对 '1'/true/数字等实际入参等价,且对 null/undefined 同样落到 -1 分支
+ function interestPnlSign(interestDirection) {
+ return Number(interestDirection) === 1 ? 1 : -1;
+ }
+
+ function marginRebatePrincipalSign(interestDirection) {
+ return Number(interestDirection) === 1 ? -1 : 1;
+ }
+
+ function payDirectionSign(payDirection) {
+ return Number(payDirection) === 1 ? 1 : -1;
+ }
+
+ function positionTypeSign(positionType) {
+ return Number(positionType) === 1 ? 1 : -1;
+ }
+
+ return Object.freeze({
+ interestPnlSign: interestPnlSign,
+ marginRebatePrincipalSign: marginRebatePrincipalSign,
+ payDirectionSign: payDirectionSign,
+ positionTypeSign: positionTypeSign
+ });
+}());
+
+if (typeof module === 'object' && module.exports) module.exports = UnwindLegSign;
diff --git a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js
index ab2e3c05..346174c4 100644
--- a/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js
+++ b/YLErpWeb/wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js
@@ -11,6 +11,9 @@ const formatSwapQuantity = value => swapPricePrecision.normalizeCommon('quantity
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) {
@@ -61,11 +64,11 @@ const vue = new Vue({
marginList: [],
initPosiNetPrice: 0,
multiplier: 1,
+ // EQD-6953 簿记模板=普通债券类收益互换 时启用 期末交割全价↔结算收益率(ExitYtm) 互算
+ isBondTRS: false,
// 平仓比例展示/输入均为"占期初(original)"语义(A):默认与每次重开都基于原始名义本金。
// oriClosePercent = 剩余名义本金/期初名义本金 = 最多可平比例(不能平超过剩余持仓)。
oriClosePercent: 1,
- ratio: 1,
- shortRatio: 1,
},
computed: {
maxUnwindDate() {
@@ -73,10 +76,15 @@ const vue = new Vue({
},
minStartDate() {
return this.deal.StartDate;
+ },
+ // EQD-6953:簿记模板为普通债券类收益互换 且 浮动腿标的为债券 时,才展示 源/AUTO/REV 标识并允许互算
+ isBondUnwindLeg() {
+ return this.isBondTRS && !!this.floatPosition && this.IsBond(this.floatPosition.UnderlyingInstrumentType);
}
},
created() {
- this.multiplier = this.deal.StructureType == '普通债券类收益互换' ? 100 : 1;
+ this.isBondTRS = this.deal.StructureType == '普通债券类收益互换';
+ this.multiplier = this.isBondTRS ? 100 : 1;
this.initDeal();
this.setValueDate(this.deal.ValueDate);
},
@@ -115,8 +123,6 @@ const vue = new Vue({
this.marginList = model.FlowEvents.filter((item) => {
return item.InterestMode == 5 || item.InterestMode == 6;
});
- this.ratio = this.floatPosition.PayDirection == 1 ? -1 : 1;
- this.shortRatio = this.floatPosition.PositionType == 1 ? 1 : -1;
this.TradeStartDate = model.TradeStartDate;
// 最多可平比例(占期初口径) = 剩余名义本金 / 期初名义本金;分母为 0 时兜底为 1
this.oriClosePercent = (this.deal.NotionalValue && this.deal.PosiNotionalValue)
@@ -151,6 +157,10 @@ const vue = new Vue({
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);
@@ -174,6 +184,14 @@ const vue = new Vue({
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();
@@ -301,10 +319,111 @@ const vue = new Vue({
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 = thisObj.floatPosition.PayDirection == 1 ? 1 : -1;
- let longRatio = thisObj.floatPosition.PositionType == 1 ? 1 : -1;
+ 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();
@@ -319,7 +438,7 @@ const vue = new Vue({
this.calcFloatClosePnl();
},
changeInterestAmount(item) {//修改利息金额
- let interestRatio = item.InterestDirection == 1 ? 1 : -1;
+ let interestRatio = UnwindLegSign.interestPnlSign(item.InterestDirection);
item.InterestClosePnL = formatSwapAmount(parseFloat(item.InterestAmount) * interestRatio + parseFloat(item.InterestFee) * interestRatio);
this.calcCloseAmount();
},
@@ -329,8 +448,8 @@ const vue = new Vue({
// this.calcCloseAmount();
//},
calcCloseAmount() {//计算平仓总额=浮动收取+利息收取-浮动支付-利息支付
- let floatRatio = this.floatPosition.PayDirection == 1 ? 1 : -1;
- let ratio = this.floatPosition.PositionType == 1 ? 1 : -1;
+ 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);
@@ -347,14 +466,14 @@ const vue = new Vue({
thisObj.floatPosition.TradingAmountFeeAvg = deliveryPrice + (TradingFee / thisObj.deal.CloseQty) * ratio;
}
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);
- let interestRatio = x.InterestDirection == 1 ? -1 : 1;
+ // 保证金返还本金符号与利息盈亏同枚举反号(见 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;
|