fix(swap): 修复互换价格变动时盈亏计算错误

- 在后端计算逻辑中添加 longRatio 参数用于区分多空头寸类型
- 修改前端 JavaScript 代码中的盈亏计算公式,加入 longRatio 参与计算
- 更新单元测试,增加空头头寸价格上涨时亏损的测试用例
- 补充前端测试用例验证空头头寸计算逻辑的准确性
- 修复了空头头寸在价格上涨时显示盈利而非亏损的计算错误
This commit is contained in:
张名锐
2026-08-07 17:44:04 +08:00
parent 0672dbbcca
commit d78d1f48e7
5 changed files with 45 additions and 5 deletions
@@ -258,6 +258,31 @@ namespace YLErp.Modules.SwapModule
AssertDecimalEqual(5355000m, result.FloatPnlSum, 0.01m, "income FloatPnlSum包含分红");
}
[TestMethod]
public void _价格上涨_应为亏损()
{
var input = new UnwindInput
{
Multiplier = 100,
PosiGrossPrice = 1.01654321m,
TradingAmountAvg = 101.754321m,
PositionQty = 50000000m,
ContractSize = 1m,
CloseNotionalValue = 50827160.5m,
CloseQty = 0m,
PayDirection = 1,
PositionType = 2,
TradingFee = "0",
TradingFeePending = "0",
DividendIn = "-90400"
};
var result = FrontendCalcReference.CalcIncome(input);
AssertDecimalEqual(-50000m, result.MarkClosePnl, 0.01m, "收取空头价格上涨=盯市亏损");
AssertDecimalEqual(-140400m, result.FloatPnlSum, 0.01m, "盯市亏损加分红");
}
private static void AssertDecimalEqual(decimal expected, decimal actual, decimal tolerance, string message = "")
{
Assert.IsTrue(Math.Abs(expected - actual) <= tolerance,
+4 -3
View File
@@ -97,14 +97,15 @@ namespace YLErp.Helpers
decimal entryPrice = input.PosiGrossPrice;
decimal scale = input.Multiplier == 100 ? 0.01m : 1m;
decimal floatRatio = input.PayDirection == 1 ? 1 : -1;
decimal longRatio = input.PositionType == 1 ? 1 : -1;
decimal tradingFee = ParseOrZero(input.TradingFee);
decimal tradingFeePending = ParseOrZero(input.TradingFeePending);
decimal dividendIn = ParseOrZero(input.DividendIn);
// MarkClosePnl = PositionQty × ContractSize × (TradingAmountAvg × scale EntryPrice) × floatRatio
// (无 longRatio、无 Math.round/10000
decimal markClosePnl = input.PositionQty * input.ContractSize * (input.TradingAmountAvg * scale - entryPrice) * floatRatio;
// MarkClosePnl = PositionQty × ContractSize × (TradingAmountAvg × scale EntryPrice) × floatRatio × longRatio
// (无 Math.round/10000
decimal markClosePnl = input.PositionQty * input.ContractSize * (input.TradingAmountAvg * scale - entryPrice) * floatRatio * longRatio;
markClosePnl = StockEqvNotional(markClosePnl);
decimal floatPnlSum = decimal.Parse(
+12
View File
@@ -308,6 +308,18 @@ describe('交叉校验:对齐 C# FrontendCalcCharacterizationTest 金标准',
expectClose(r.MarkClosePnl, 5400000, 'income MarkClosePnl按数量计算');
expectClose(r.FloatPnlSum, 5355000, 'income FloatPnlSum包含分红');
});
test('收取空头价格上涨应为亏损', () => {
const r = SwapCalc.calcIncome({
multiplier: 100, posiGrossPrice: 1.01654321, tradingAmountAvg: 101.754321,
positionQty: 50000000, contractSize: 1,
closeNotionalValue: 50827160.5, closeQty: 0,
payDirection: 1, positionType: 2,
tradingFee: '0', tradingFeePending: '0', dividendIn: '-90400'
});
expectClose(r.MarkClosePnl, -50000, '收取空头价格上涨=盯市亏损');
expectClose(r.FloatPnlSum, -140400, '盯市亏损加分红');
});
});
// ============================================================================
@@ -197,6 +197,7 @@ const vue = new Vue({
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);
@@ -204,7 +205,7 @@ const vue = new Vue({
// 债券全价是单位价格,价差盈亏应按持仓数量×合约乘数计算;
// CloseNotionalValue 是期初全价折算后的名义本金,直接乘价差会重复包含期初价格。
let positionAmount = parseFloat(thisObj.floatPosition.Quantity) * parseFloat(thisObj.floatPosition.ContractSize || 1);
thisObj.floatPosition.MarkClosePnl = positionAmount * (deliveryPrice - thisObj.initPosiGrossPrice) * floatRatio;
thisObj.floatPosition.MarkClosePnl = positionAmount * (deliveryPrice - thisObj.initPosiGrossPrice) * floatRatio * longRatio;
thisObj.floatPosition.MarkClosePnl = formatSwapAmount(thisObj.floatPosition.MarkClosePnl);//MarkClosePnl 纯盯市不要计算交易费用和分红
// 守卫: 浮动盈亏合计必须保留 2 位小数 → 对应历史 bug 3c5f25a5(原代码缺精度保留)
// 数值由 swapCalc.calcFloatPnlSum 计算, 此处 .toFixed(2) 仅保留字符串类型以兼容下游
@@ -179,6 +179,7 @@
var entryPrice = input.posiGrossPrice;
var scale = input.multiplier === 100 ? 0.01 : 1;
var floatRatio = input.payDirection === 1 ? 1 : -1;
var longRatio = input.positionType === 1 ? 1 : -1;
var tradingFee = parseOrZero(input.tradingFee);
var tradingFeePending = parseOrZero(input.tradingFeePending);
@@ -187,7 +188,7 @@
var contractSize = input.contractSize === undefined || input.contractSize === null
? 1 : Number(input.contractSize);
var markClosePnl = roundHalfAwayFromZero(
input.positionQty * contractSize * (input.tradingAmountAvg * scale - entryPrice) * floatRatio, 2);
input.positionQty * contractSize * (input.tradingAmountAvg * scale - entryPrice) * floatRatio * longRatio, 2);
var floatPnlSum = roundHalfAwayFromZero(markClosePnl + tradingFee + tradingFeePending + dividendIn, 2);