fix(swap): 修复互换价格变动时盈亏计算错误
- 在后端计算逻辑中添加 longRatio 参数用于区分多空头寸类型 - 修改前端 JavaScript 代码中的盈亏计算公式,加入 longRatio 参与计算 - 更新单元测试,增加空头头寸价格上涨时亏损的测试用例 - 补充前端测试用例验证空头头寸计算逻辑的准确性 - 修复了空头头寸在价格上涨时显示盈利而非亏损的计算错误
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user