/** * closePercentContract.test.js — 平仓比例口径A(占期初名义本金)前端接线守卫 * ============================================================================ * 产品需求(不可违背):ClosePercent 永远是"占期初名义本金(NotionalValue)的比例",即口径A。 * * 本文件是"接线测试"(wiring test):读 unwindSwapTrade.js 源码文本,断言关键逻辑 * 仍然使用口径A的公式。如果有人把公式改成口径B(如 c9071a4e 曾犯的错误), * 对应断言会立即变红。 * * 与 swapCalc.test.js 的区别: * swapCalc.test.js 测 SwapCalc 纯函数本身正确性(零件级) * 本文件测 unwindSwapTrade.js 确实在调用这些函数/使用正确公式(装配级) * ============================================================================ */ const fs = require('fs'); const path = require('path'); const unwindSrc = fs.readFileSync( path.join(__dirname, '..', 'wwwroot', 'Scripts', 'app', 'swaptrade', 'unwindSwapTrade.js'), 'utf8' ); describe('口径A产品契约:unwindSwapTrade.js 接线守卫', () => { // ==================================================================== // 契约1:oriClosePercent 必须用 PosiNotionalValue / NotionalValue 计算 // 不能硬编码为 1(c9071a4e 的错误) // ==================================================================== describe('oriClosePercent 必须为剩余/期初', () => { test('源码中 oriClosePercent 必须包含 PosiNotionalValue / NotionalValue 公式', () => { // 正确代码:this.oriClosePercent = ... PosiNotionalValue / ... NotionalValue expect(unwindSrc).toMatch(/oriClosePercent.*PosiNotionalValue.*\/.*NotionalValue/s); }); test('源码中 oriClosePercent 不能被硬编码为 1', () => { // c9071a4e 的错误:this.deal.ClosePercent = 1 (直接覆盖) // 检查 initDeal 中不存在 oriClosePercent = 1 的硬编码 const initDealSection = unwindSrc.match(/initDeal\(\)[\s\S]*?\},/); expect(initDealSection).toBeTruthy(); // 不应出现 oriClosePercent = 1 或 ClosePercent = 1 的硬编码 //(CloseMethod===1 时设置 ClosePercent=1 是允许的,但 oriClosePercent 不应被设为1) expect(initDealSection[0]).not.toMatch(/oriClosePercent\s*=\s*1\b/); }); }); // ==================================================================== // 契约2:calcCloseQtyByPercent 必须调用 SwapCalc.calcCloseQtyByOriginalPercent // 不能直接 PositionQty × ClosePercent(c9071a4e 的错误) // ==================================================================== describe('calcCloseQtyByPercent 必须调用 SwapCalc', () => { test('源码中 calcCloseQtyByPercent 必须调用 SwapCalc.calcCloseQtyByOriginalPercent', () => { expect(unwindSrc).toContain('SwapCalc.calcCloseQtyByOriginalPercent'); }); test('changeCloseMethod 的部分平仓分支必须调用 calcCloseQtyByPercent', () => { // 正确代码:this.deal.CloseQty = this.calcCloseQtyByPercent(this.deal.ClosePercent); expect(unwindSrc).toMatch(/calcCloseQtyByPercent\s*\(this\.deal\.ClosePercent\)/); }); test('changeClosePercent 必须调用 calcCloseQtyByPercent', () => { expect(unwindSrc).toMatch(/this\.deal\.CloseQty\s*=\s*this\.calcCloseQtyByPercent/); }); test('changeCloseNotionalValue 必须调用 calcCloseQtyByPercent', () => { expect(unwindSrc).toMatch(/this\.deal\.CloseQty\s*=\s*this\.calcCloseQtyByPercent/); }); }); // ==================================================================== // 契约3:CloseNotionalValue 必须用 NotionalValue 做乘数/分母(口径A) // 不能用 PosiNotionalValue(c9071a4e 的错误) // ==================================================================== describe('CloseNotionalValue 必须基于 NotionalValue(口径A)', () => { test('changeCloseQty: CloseNotionalValue = ClosePercent × NotionalValue', () => { // 正确:ClosePercent × parseFloat(this.deal.NotionalValue) // 错误:ClosePercent × parseFloat(this.deal.PosiNotionalValue) const changeCloseQtySection = unwindSrc.match(/changeCloseQty\(\)[\s\S]*?\n\s*\},/); expect(changeCloseQtySection).toBeTruthy(); expect(changeCloseQtySection[0]).toMatch(/CloseNotionalValue.*NotionalValue/); expect(changeCloseQtySection[0]).not.toMatch(/CloseNotionalValue.*PosiNotionalValue/); }); test('changeClosePercent: CloseNotionalValue = ClosePercent × NotionalValue', () => { const changeClosePercentSection = unwindSrc.match(/changeClosePercent\(\)[\s\S]*?\n\s*\},/); expect(changeClosePercentSection).toBeTruthy(); expect(changeClosePercentSection[0]).toMatch(/CloseNotionalValue.*NotionalValue/); expect(changeClosePercentSection[0]).not.toMatch(/CloseNotionalValue.*PosiNotionalValue/); }); }); // ==================================================================== // 契约4:changeCloseQty 的 ClosePercent 必须乘以 oriClosePercent(口径A→B→A 转换) // 不能直接 CloseQty / PositionQty(c9071a4e 的错误) // ==================================================================== describe('changeCloseQty 的 ClosePercent 必须乘以 oriClosePercent', () => { test('ClosePercent = (CloseQty/PositionQty) × oriClosePercent', () => { // 正确:× ori(把占剩余比例转回占期初口径) // 错误:不乘 ori(直接用占剩余比例作为 ClosePercent) const changeCloseQtySection = unwindSrc.match(/changeCloseQty\(\)[\s\S]*?\n\s*\},/); expect(changeCloseQtySection).toBeTruthy(); expect(changeCloseQtySection[0]).toMatch(/oriClosePercent/); expect(changeCloseQtySection[0]).toMatch(/\*\s*ori/); }); test('changeCloseNotionalValue 的 ClosePercent 必须除以 NotionalValue', () => { // 正确:ClosePercent = CloseNotionalValue / NotionalValue // 错误:ClosePercent = CloseNotionalValue / PosiNotionalValue const changeCloseNotionalSection = unwindSrc.match(/changeCloseNotionalValue\(\)[\s\S]*?\n\s*\},/); expect(changeCloseNotionalSection).toBeTruthy(); expect(changeCloseNotionalSection[0]).toMatch(/ClosePercent.*NotionalValue/); expect(changeCloseNotionalSection[0]).not.toMatch(/ClosePercent.*PosiNotionalValue/); }); }); // ==================================================================== // 契约5:getInterestList 必须传 notionalValue 和 posiNotionalValue 给后端 // 后端 GetUnwindInterestList 需要这两个值做 A→B 转换 // 如果删掉(c9071a4e 的错误),后端不转换,利息用错口径计算 // ==================================================================== describe('getInterestList 必须传 notionalValue/posiNotionalValue', () => { test('postData 必须包含 notionalValue', () => { expect(unwindSrc).toMatch(/notionalValue:\s*thisObj\.deal\.NotionalValue/); }); test('postData 必须包含 posiNotionalValue', () => { expect(unwindSrc).toMatch(/posiNotionalValue:\s*thisObj\.deal\.PosiNotionalValue/); }); test('getInterestList 的 postData 不能只有 closePercent 而缺少 notionalValue', () => { // 精确匹配 getInterestList 方法定义(以 getInterestList() { 开头,到 main.post 结束) // 匹配模式:方法名+参数列表+花括号开始,一直到包含 main.post 的 postData 定义 const methodMatch = unwindSrc.match( /getInterestList\(\)\s*\{[\s\S]*?var\s+postData\s*=\s*\{[^}]*\}/ ); expect(methodMatch).toBeTruthy(); const postData = methodMatch[0]; expect(postData).toContain('notionalValue'); expect(postData).toContain('posiNotionalValue'); }); }); // ==================================================================== // 契约6:CloseMethod 赋值对象必须是 deal(不是 floatPosition) // c9071a4e 在 changeClosePercent 中误赋值到 floatPosition.CloseMethod // ==================================================================== describe('CloseMethod 必须赋值给 deal', () => { test('changeClosePercent 的 CloseMethod 必须赋值给 this.deal', () => { const changeClosePercentSection = unwindSrc.match(/changeClosePercent\(\)[\s\S]*?\n\s*\},/); expect(changeClosePercentSection).toBeTruthy(); expect(changeClosePercentSection[0]).toMatch(/this\.deal\.CloseMethod\s*=/); expect(changeClosePercentSection[0]).not.toMatch(/this\.floatPosition\.CloseMethod\s*=/); }); test('changeCloseNotionalValue 的 CloseMethod 必须赋值给 this.deal', () => { const changeCloseNotionalSection = unwindSrc.match(/changeCloseNotionalValue\(\)[\s\S]*?\n\s*\},/); expect(changeCloseNotionalSection).toBeTruthy(); expect(changeCloseNotionalSection[0]).toMatch(/this\.deal\.CloseMethod\s*=/); expect(changeCloseNotionalSection[0]).not.toMatch(/this\.floatPosition\.CloseMethod\s*=/); }); test('changeCloseNotionalValue 必须设置 CloseMethod(不能删除)', () => { // c9071a4e 完全删除了 changeCloseNotionalValue 中的 CloseMethod 判断 const changeCloseNotionalSection = unwindSrc.match(/changeCloseNotionalValue\(\)[\s\S]*?\n\s*\},/); expect(changeCloseNotionalSection).toBeTruthy(); expect(changeCloseNotionalSection[0]).toMatch(/CloseMethod/); }); }); });