fix: 统一互换部分平仓交易费用与待结算费用的分摊舍入

This commit is contained in:
tengyufan
2026-07-29 13:47:45 +08:00
parent e1d68393f8
commit 8c4f452482
4 changed files with 79 additions and 22 deletions
@@ -34,10 +34,12 @@ namespace YLErp.Modules.SwapModule
var position = new swap_position
{
PosiFeeType = 0,
PosiTradingFeeUnit = 0.1234m
PosiTradingFeeUnit = 0.1234m,
PosiTradingFeePending = 1234.00m
};
var unwindData = new UnwindData
{
NotionalValue = 1_000_000m,
CloseNotionalValue = 1_000_000m,
CloseQty = 8888m
};
@@ -53,10 +55,12 @@ namespace YLErp.Modules.SwapModule
var position = new swap_position
{
PosiFeeType = 1,
PosiTradingFeeUnit = 1.235m
PosiTradingFeeUnit = 1.235m,
PosiTradingFeePending = 12.35m
};
var unwindData = new UnwindData
{
NotionalQty = 10m,
CloseNotionalValue = 1_000_000m,
CloseQty = 10m
};
@@ -106,6 +110,30 @@ namespace YLErp.Modules.SwapModule
Assert.AreEqual(450m, fee);
}
[TestMethod]
public void PartialCloseTradingFeeAndPendingFeeUseTheSameRoundedOriginalFeeAllocation()
{
var oriPosition = new swap_position
{
PosiFeeType = 0,
PosiTradingFeeUnit = 1.1234m,
PosiTradingFeePending = 113.46m
};
var unwindData = new UnwindData
{
NotionalValue = 10098m,
CloseNotionalValue = 4039.2m,
NotionalQty = 10000m,
CloseQty = 4000m
};
var tradingFee = InvokeCalcInitTradingFee(oriPosition, unwindData);
var pendingFee = InvokeCalcInitTradingFeePending(oriPosition, new swap_position(), unwindData);
Assert.AreEqual(45.38m, tradingFee);
Assert.AreEqual(45.38m, pendingFee);
}
[TestMethod]
public void LegacyPendingFeeKeepsCurrentPositionValueWhenNoBaseRateIsConfigured()
{
+11 -11
View File
@@ -323,12 +323,19 @@ namespace YLErp.Modules.SwapModule
return 0;
}
if (oriPosition.PosiFeeType == 1)
if (oriPosition.PosiTradingFeeUnit == 0)
{
return Math.Round(oriPosition.PosiTradingFeeUnit * unwindData.CloseQty, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
return 0;
}
return Math.Round(oriPosition.PosiTradingFeeUnit / 100m * unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
var closeBase = oriPosition.PosiFeeType == 1 ? unwindData.CloseQty : unwindData.CloseNotionalValue;
var originalBase = oriPosition.PosiFeeType == 1 ? unwindData.NotionalQty : unwindData.NotionalValue;
if (originalBase <= 0)
{
return 0;
}
return Math.Round(oriPosition.PosiTradingFeePending * closeBase / originalBase, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
}
private static decimal CalcInitTradingFeePending(swap_position oriPosition, swap_position position, UnwindData unwindData)
@@ -338,14 +345,7 @@ namespace YLErp.Modules.SwapModule
return position?.PosiTradingFeePending ?? 0;
}
var closeBase = oriPosition.PosiFeeType == 1 ? unwindData.CloseQty : unwindData.CloseNotionalValue;
var originalBase = oriPosition.PosiFeeType == 1 ? unwindData.NotionalQty : unwindData.NotionalValue;
if (originalBase <= 0)
{
return position?.PosiTradingFeePending ?? 0;
}
return Math.Round(oriPosition.PosiTradingFeePending * closeBase / originalBase, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
return CalcInitTradingFee(oriPosition, unwindData);
}
/// <summary>
/// 校验上日是否收盘
+10
View File
@@ -112,6 +112,16 @@ describe('base-rate pending trading fee', () => {
expectClose(result, 450.00);
});
test('partial close fee and pending fee both use the rounded opening fee allocation', () => {
const tradingFee = swapPosiFeeCalc.calcAllocatedTradingFee(
113.46, consPosiFeeType.Percent, 1.1234, 4039.2, 4000, 10098, 10000);
const pendingFee = swapPosiFeeCalc.calcTradingFeePending(
113.46, consPosiFeeType.Percent, 1.1234, 4039.2, 4000, 10098, 10000, 0.4);
expectClose(tradingFee, 45.38);
expectClose(pendingFee, 45.38);
});
test('without a configured base rate, the legacy close-percent calculation remains', () => {
const result = swapPosiFeeCalc.calcTradingFeePending(
2000, consPosiFeeType.Percent, 0, 300000, 3000, 1000000, 10000, 0.42);
@@ -23,15 +23,26 @@ const swapPosiFeeCalc = {
: normalizedFeeUnit / 100 * normalizedCloseNotionalValue;
return otcformat.trading.StockEqvNotional(_.round(tradingFee, 2));
},
calcTradingFeePending(beforeCloseFee, feeType, feeUnit, closeNotionalValue, closeQty, notionalValue, notionalQty, closePercent) {
calcAllocatedTradingFee(totalFee, feeType, feeUnit, closeNotionalValue, closeQty, notionalValue, notionalQty) {
const normalizedFeeUnit = Number(feeUnit) || 0;
if (normalizedFeeUnit !== 0) {
const normalizedFeeType = this.normalizeFeeType(feeType);
const closeBase = normalizedFeeType === consPosiFeeType.Unit ? Number(closeQty) || 0 : Number(closeNotionalValue) || 0;
const originalBase = normalizedFeeType === consPosiFeeType.Unit ? Number(notionalQty) || 0 : Number(notionalValue) || 0;
if (originalBase > 0) {
return otcformat.trading.StockEqvNotional(_.round((Number(beforeCloseFee) || 0) * closeBase / originalBase, 2));
}
if (normalizedFeeUnit === 0) {
return null;
}
const normalizedFeeType = this.normalizeFeeType(feeType);
const closeBase = normalizedFeeType === consPosiFeeType.Unit ? Number(closeQty) || 0 : Number(closeNotionalValue) || 0;
const originalBase = normalizedFeeType === consPosiFeeType.Unit ? Number(notionalQty) || 0 : Number(notionalValue) || 0;
if (originalBase <= 0) {
return null;
}
return otcformat.trading.StockEqvNotional(_.round((Number(totalFee) || 0) * closeBase / originalBase, 2));
},
calcTradingFeePending(beforeCloseFee, feeType, feeUnit, closeNotionalValue, closeQty, notionalValue, notionalQty, closePercent) {
const allocatedFee = this.calcAllocatedTradingFee(
beforeCloseFee, feeType, feeUnit, closeNotionalValue, closeQty, notionalValue, notionalQty);
if (allocatedFee !== null) {
return allocatedFee;
}
return (Number(beforeCloseFee) || 0) * (Number(closePercent) || 0);
@@ -192,7 +203,15 @@ const vue = new Vue({
this.deal.ClosePercent);
},
refreshTradingFeeByUnit() {
this.floatPosition.TradingFee = swapPosiFeeCalc.calcTradingFee(
const allocatedFee = swapPosiFeeCalc.calcAllocatedTradingFee(
this.floatPosition.BeforeCloseFee,
this.floatPosition.PosiFeeType,
this.floatPosition.PosiTradingFeeUnit,
this.deal.CloseNotionalValue,
this.deal.CloseQty,
this.deal.NotionalValue,
this.deal.NotionalQty);
this.floatPosition.TradingFee = allocatedFee !== null ? allocatedFee : swapPosiFeeCalc.calcTradingFee(
this.floatPosition.PosiFeeType,
this.floatPosition.PosiTradingFeeUnit,
this.deal.CloseNotionalValue,