新增前后端回归测试,锁定产品需求:ClosePercent 永远是占期初名义本金的比例。后端 CPC_001~010 装配测试验证 SwapDealService 真实执行 ToRemainingClosePercent/ToOriginalClosePercent 转换;前端 16 项接线测试验证 unwindSwapTrade.js 使用 NotionalValue 作为分母且 postData 携带 notionalValue 字段。测试背景:c9071a4e 曾将 ClosePercent 改为占剩余口径导致回归但现有测试全通过,本测试通过源码装配与契约验证弥补该缺口。
169 lines
9.8 KiB
JavaScript
169 lines
9.8 KiB
JavaScript
/**
|
||
* 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/);
|
||
});
|
||
});
|
||
});
|