diff --git a/YLErpDAL/Modules/SwapModule/EodPnlCalculator.cs b/YLErpDAL/Modules/SwapModule/EodPnlCalculator.cs
new file mode 100644
index 00000000..3532be45
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/EodPnlCalculator.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Collections.Generic;
+using YLErp;
+using YLErp.DBModels;
+using YLErp.DBModels.Enums;
+using YLErp.Modules.SwapModule.ReturnLegs;
+
+namespace YLErp.Modules.SwapModule
+{
+ ///
+ /// 互换日终盈亏/精度计算纯函数集合。
+ /// 自 SwapEodPositionService 抽出,支持无库单测;同类内部调用无需前缀。
+ ///
+ public static class EodPnlCalculator
+ {
+ // 日终利息待实现需跨日累计,按表设计保留 12 位;已实现结算仍按金额两位处理。
+ private const int EodInterestStoragePrecision = 12;
+
+ internal static decimal RoundMoney(decimal value)
+ {
+ return Math.Round(value, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
+ }
+
+ internal static decimal RoundEodInterest(decimal value)
+ {
+ return Math.Round(value, EodInterestStoragePrecision, MidpointRounding.AwayFromZero);
+ }
+
+ ///
+ /// 仅在写入 eod_swap_position 前统一快照精度。
+ /// 浮动腿收益最终以金额两位展示和存储;利息腿的待实现、计息基数及利率保留 12 位,
+ /// 使部分结算后的尾差可继续参与后续计息。
+ ///
+ internal static void NormalizeEodPositionForStorage(eod_swap_position position)
+ {
+ if (string.IsNullOrEmpty(position.UnderlyingCode))
+ {
+ // 利息腿没有标的代码:待实现字段保留高精度,已实现结算字段收敛到金额两位。
+ position.InterestPrincipalFix = RoundEodInterest(position.InterestPrincipalFix);
+ position.InterestRateDefault = RoundEodInterest(position.InterestRateDefault);
+ position.InterestFeePending = RoundEodInterest(position.InterestFeePending);
+ position.TdInterestPrincipal = RoundEodInterest(position.TdInterestPrincipal);
+ position.TdInterestRate = RoundEodInterest(position.TdInterestRate);
+ position.TdInterestIncome = RoundEodInterest(position.TdInterestIncome);
+ position.TdInterestFee = RoundEodInterest(position.TdInterestFee);
+ position.InterestIncomeSum = RoundEodInterest(position.InterestIncomeSum);
+ position.InterestFeeSum = RoundEodInterest(position.InterestFeeSum);
+ position.InterestProfitSum = RoundEodInterest(position.InterestProfitSum);
+ position.FloatRate = RoundEodInterest(position.FloatRate);
+ position.SwapPositionValue = RoundEodInterest(position.SwapPositionValue);
+ position.TdCloseInterest = RoundMoney(position.TdCloseInterest);
+ position.TdCloseInterestFee = RoundMoney(position.TdCloseInterestFee);
+ position.RealizedInterest = RoundMoney(position.RealizedInterest);
+ position.RealizedInterestFee = RoundMoney(position.RealizedInterestFee);
+ }
+ else
+ {
+ // 浮动腿有标的代码:其损益作为金额结果落库,统一按两位四舍五入。
+ position.TdPosiDividend = RoundMoney(position.TdPosiDividend);
+ position.PosiMtmPnL = RoundMoney(position.PosiMtmPnL);
+ position.PosiDividendSum = RoundMoney(position.PosiDividendSum);
+ position.PosiFeePending = RoundMoney(position.PosiFeePending);
+ position.PosiProfitSum = RoundMoney(position.PosiProfitSum);
+ position.TdCloseMtmPnl = RoundMoney(position.TdCloseMtmPnl);
+ position.TdCloseDividend = RoundMoney(position.TdCloseDividend);
+ position.TdCloseFee = RoundMoney(position.TdCloseFee);
+ position.RealizedMtmPnL = RoundMoney(position.RealizedMtmPnL);
+ position.RealizedDividend = RoundMoney(position.RealizedDividend);
+ position.RealizedFee = RoundMoney(position.RealizedFee);
+ position.SwapPositionValue = RoundMoney(position.SwapPositionValue);
+ }
+ position.RealizedPnl = RoundMoney(position.RealizedPnl);
+ }
+
+ ///
+ /// 浮动腿累计已实现盈亏由盯市、分红和费用三个已实现组成项汇总。
+ /// 各组成项已经按本方视角落库,此处不再额外转换方向。
+ ///
+ internal static void SetFloatingRealizedPnl(eod_swap_position position)
+ {
+ position.RealizedPnl = position.RealizedMtmPnL
+ + position.RealizedDividend
+ + position.RealizedFee;
+ }
+
+ ///
+ /// 汇总单条日终腿的我方已实现收益。
+ /// 浮动腿及普通利息腿维持数据库记录的方向;初始/追加预付金腿的利息
+ /// 则与保证金本金方向相反。这样“收取对手方保证金”产生的利息会作为
+ /// 我方支付给对手方的成本计入,而不会错误增加框架合约已实现收益。
+ /// 抽为静态纯函数以支持无库单测(marginTypes 等价于 ConsTrade.InterestMarginModels)。
+ ///
+ public static decimal CalculateSwapRealizedPnl(eod_swap_position position)
+ {
+ var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
+
+ return position.RealizedMtmPnL
+ + position.RealizedDividend
+ + position.RealizedFee
+ + position.RealizedInterest * interestRatio
+ + position.RealizedInterestFee;
+ }
+
+ /// 填充框架合约的持仓腿汇总字段(多空名义本金/市值/浮动盈亏/dv01/平仓量)。
+ /// SaveEodSwap 与 UpdateEodSwap 共用,消除 ~10 行重复。
+ internal static void FillPositionLegSummary(eod_swap eod_Swap, List positions)
+ {
+ eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
+ eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
+ eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue);
+ eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue);
+ eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum);
+ eod_Swap.dv01 = positions.Sum(s => s.dv01 ?? 0);
+ eod_Swap.TdCloseQty = positions.Sum(s => s.TdCloseQty);
+ }
+
+ /// 利息腿 PnL 汇总(按方向比例 + 保证金翻转)。原 SaveEodSwap/UpdateEodSwap 各一段 ForEach。
+ internal static decimal SumInterestPnL(List interestPositions)
+ {
+ decimal interestPnL = 0;
+ foreach (var x in interestPositions)
+ interestPnL += x.InterestProfitSum * DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode);
+ return interestPnL;
+ }
+
+ ///
+ /// 风险报表符号归一化:把历史两种符号口径的 TdCloseInterest/RealizedInterest
+ /// 统一按"绝对金额 × 业务方向"重写。普通利息腿收取为正、支付为负;
+ /// 预付金腿利息方向与保证金本金方向相反。随后重算 RealizedPnl。
+ /// 抽为 public static 纯函数以支持无库单测(见 SwapReportInterestSignNormalizeTest)。
+ /// 仅当 InterestDirection > 0 时执行(与原内联逻辑等价)。
+ ///
+ public static void NormalizeInterestSignForReport(eod_swap_position position)
+ {
+ if (position.InterestDirection <= 0) return;
+
+ if (position.InterestMode == (int)InterestModeEnum.标的期初全价)
+ {
+ return;
+ }
+ var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
+ position.TdCloseInterest = Math.Abs(position.TdCloseInterest) * interestRatio;
+ position.RealizedInterest = Math.Abs(position.RealizedInterest) * interestRatio;
+ // 兼容修复前已落库的利息腿:当时只累计了明细字段,未同步写入 RealizedPnl。
+ position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee;
+ }
+
+ ///
+ /// 计算预付金利率。多条初始/追加预付金腿按本金绝对值加权,
+ /// 不按收付方向轧差,避免相反方向本金抵消后放大利率。
+ ///
+ internal static decimal CalculateWeightedMarginRate(IEnumerable margins)
+ {
+ var marginList = margins.ToList();
+ var totalWeight = marginList.Sum(x => Math.Abs(x.InterestPrincipalFix));
+ return totalWeight == 0
+ ? 0
+ : marginList.Sum(x => x.InterestRateDefault * Math.Abs(x.InterestPrincipalFix)) / totalWeight;
+ }
+
+ ///
+ /// 计算预付金利息金额。InterestIncomeSum 已是各腿利息金额,
+ /// 按收取为正、支付为负直接轧差求和,不做本金加权。
+ /// 抽为 public static 纯函数以支持无库单测(见 SwapWeightedMarginInterestTest)。
+ ///
+ public static decimal CalculateWeightedMarginInterest(IEnumerable margins)
+ {
+ return margins.Sum(x =>
+ x.InterestIncomeSum * DirectionRatio.ReceivePay(x.InterestDirection));
+ }
+
+ ///
+ /// 固定利息腿的累计已实现盈亏 = 累计已实现利息 + 累计已实现利息费用。
+ /// 4 处 SaveAutoEodInterestPosition/SaveEodInterestPosition 路径口径一致,
+ /// 抽为 public static 纯函数以支持无库单测(见 SwapFixedLegRealizedPnlTest),
+ /// 并消除复制粘贴带来的笔误风险(如 L1296 历史双分号)。
+ ///
+ public static void SetFixedLegRealizedPnl(eod_swap_position position)
+ {
+ position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee;
+ }
+ }
+}
diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs
index e445a357..d42fc903 100644
--- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs
+++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs
@@ -48,72 +48,13 @@ namespace YLErp.Modules.SwapModule
: ConsGlobal.SwapDeliveryPriceRound;
}
- // 日终利息待实现需跨日累计,按表设计保留 12 位;已实现结算仍按金额两位处理。
- private const int EodInterestStoragePrecision = 12;
-
- private static decimal RoundMoney(decimal value)
- {
- return Math.Round(value, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
- }
-
- private static decimal RoundEodInterest(decimal value)
- {
- return Math.Round(value, EodInterestStoragePrecision, MidpointRounding.AwayFromZero);
- }
-
- ///
- /// 仅在写入 eod_swap_position 前统一快照精度。
- /// 浮动腿收益最终以金额两位展示和存储;利息腿的待实现、计息基数及利率保留 12 位,
- /// 使部分结算后的尾差可继续参与后续计息。
- ///
- private static void NormalizeEodPositionForStorage(eod_swap_position position)
- {
- if (string.IsNullOrEmpty(position.UnderlyingCode))
- {
- // 利息腿没有标的代码:待实现字段保留高精度,已实现结算字段收敛到金额两位。
- position.InterestPrincipalFix = RoundEodInterest(position.InterestPrincipalFix);
- position.InterestRateDefault = RoundEodInterest(position.InterestRateDefault);
- position.InterestFeePending = RoundEodInterest(position.InterestFeePending);
- position.TdInterestPrincipal = RoundEodInterest(position.TdInterestPrincipal);
- position.TdInterestRate = RoundEodInterest(position.TdInterestRate);
- position.TdInterestIncome = RoundEodInterest(position.TdInterestIncome);
- position.TdInterestFee = RoundEodInterest(position.TdInterestFee);
- position.InterestIncomeSum = RoundEodInterest(position.InterestIncomeSum);
- position.InterestFeeSum = RoundEodInterest(position.InterestFeeSum);
- position.InterestProfitSum = RoundEodInterest(position.InterestProfitSum);
- position.FloatRate = RoundEodInterest(position.FloatRate);
- position.SwapPositionValue = RoundEodInterest(position.SwapPositionValue);
- position.TdCloseInterest = RoundMoney(position.TdCloseInterest);
- position.TdCloseInterestFee = RoundMoney(position.TdCloseInterestFee);
- position.RealizedInterest = RoundMoney(position.RealizedInterest);
- position.RealizedInterestFee = RoundMoney(position.RealizedInterestFee);
- }
- else
- {
- // 浮动腿有标的代码:其损益作为金额结果落库,统一按两位四舍五入。
- position.TdPosiDividend = RoundMoney(position.TdPosiDividend);
- position.PosiMtmPnL = RoundMoney(position.PosiMtmPnL);
- position.PosiDividendSum = RoundMoney(position.PosiDividendSum);
- position.PosiFeePending = RoundMoney(position.PosiFeePending);
- position.PosiProfitSum = RoundMoney(position.PosiProfitSum);
- position.TdCloseMtmPnl = RoundMoney(position.TdCloseMtmPnl);
- position.TdCloseDividend = RoundMoney(position.TdCloseDividend);
- position.TdCloseFee = RoundMoney(position.TdCloseFee);
- position.RealizedMtmPnL = RoundMoney(position.RealizedMtmPnL);
- position.RealizedDividend = RoundMoney(position.RealizedDividend);
- position.RealizedFee = RoundMoney(position.RealizedFee);
- position.SwapPositionValue = RoundMoney(position.SwapPositionValue);
- }
- position.RealizedPnl = RoundMoney(position.RealizedPnl);
- }
-
#region 可测试化接缝(Seams)——override 这些虚方法可在测试中替换 DB/外部调用,生产代码行为不变
/// 持久化 eod 持仓记录(生产: DbContext.Add;测试: 收集到列表)
protected virtual void PersistEodSwapPosition(eod_swap_position position)
{
// 所有新增或更新的日终持仓都经过此入口,避免不同日终分支出现精度差异。
- NormalizeEodPositionForStorage(position);
+ EodPnlCalculator.NormalizeEodPositionForStorage(position);
var storagePriceRound = GetStorageDeliveryPriceRound(position.UnderlyingInstrumentType, position.UnderlyingCode);
position.PosiGrossPrice = Math.Round(position.PosiGrossPrice, storagePriceRound, MidpointRounding.AwayFromZero);
position.UnderlyingPrice = Math.Round(position.UnderlyingPrice, storagePriceRound, MidpointRounding.AwayFromZero);
@@ -1089,8 +1030,8 @@ namespace YLErp.Modules.SwapModule
var interestFeeBeforeSettlement = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee;
var isMaturityFinalSettlement = valueDate.Date >= td.ExerciseDate.Value.Date
&& flowEvents.Any()
- && RoundMoney(interestIncomeBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterest)
- && RoundMoney(interestFeeBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterestFee);
+ && EodPnlCalculator.RoundMoney(interestIncomeBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterest)
+ && EodPnlCalculator.RoundMoney(interestFeeBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterestFee);
if (isMaturityFinalSettlement)
{
@@ -1101,8 +1042,8 @@ namespace YLErp.Modules.SwapModule
}
else
{
- newEodPayPosition.InterestIncomeSum = RoundEodInterest(interestIncomeBeforeSettlement - newEodPayPosition.TdCloseInterest);
- newEodPayPosition.InterestFeeSum = RoundEodInterest(interestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee);
+ newEodPayPosition.InterestIncomeSum = EodPnlCalculator.RoundEodInterest(interestIncomeBeforeSettlement - newEodPayPosition.TdCloseInterest);
+ newEodPayPosition.InterestFeeSum = EodPnlCalculator.RoundEodInterest(interestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee);
}
newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum;
//持仓价值
@@ -1202,8 +1143,8 @@ namespace YLErp.Modules.SwapModule
// 日终快照仍使用上面的高精度应结金额计算待实现尾差,避免把舍入差提前丢掉。
interests.ForEach(x =>
{
- x.InterestAmount = RoundMoney(x.InterestAmount);
- x.InterestClosePnL = RoundMoney(x.InterestClosePnL);
+ x.InterestAmount = EodPnlCalculator.RoundMoney(x.InterestAmount);
+ x.InterestClosePnL = EodPnlCalculator.RoundMoney(x.InterestClosePnL);
});
decimal settledInterestAmount = interests.Sum(x => x.InterestAmount);
@@ -1240,10 +1181,10 @@ namespace YLErp.Modules.SwapModule
// 到期自动互换是最后一次自动结算:两位实际金额已落流水/资金,待实现不再滚入下一日。
newEodPayPosition.InterestIncomeSum = isMaturityFinalAutoSettlement
? 0
- : RoundEodInterest(interestAmountBeforeSettlement - settledInterestAmount);
+ : EodPnlCalculator.RoundEodInterest(interestAmountBeforeSettlement - settledInterestAmount);
newEodPayPosition.InterestFeeSum = isMaturityFinalAutoSettlement
? 0
- : RoundEodInterest(eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee);
+ : EodPnlCalculator.RoundEodInterest(eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee);
newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum;
//持仓价值
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio);
@@ -1365,7 +1306,7 @@ namespace YLErp.Modules.SwapModule
decimal autoSettledInterestAmount = 0m;
if (autoSwap && interests.Count > 0)
{
- autoSettledInterestAmount = RoundMoney(interestAmountBeforeSettlement - manualSettledInterestAmount);
+ autoSettledInterestAmount = EodPnlCalculator.RoundMoney(interestAmountBeforeSettlement - manualSettledInterestAmount);
var autoInterest = interests[0];
autoInterest.InterestAmount = autoSettledInterestAmount;
autoInterest.InterestClosePnL = autoSettledInterestAmount
@@ -1495,13 +1436,13 @@ namespace YLErp.Modules.SwapModule
// InterestIncomeSum 是收盘后仍未结算的尾差/剩余利息。
// 部分平仓:扣款前待实现 - TdCloseInterest;最终全平且两位金额已覆盖时直接清零。
newEodPayPosition.InterestIncomeSum = closePercent == 1
- && RoundMoney(pendingInterestBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterest)
+ && EodPnlCalculator.RoundMoney(pendingInterestBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterest)
? 0m
- : RoundEodInterest(pendingInterestBeforeSettlement - newEodPayPosition.TdCloseInterest);
+ : EodPnlCalculator.RoundEodInterest(pendingInterestBeforeSettlement - newEodPayPosition.TdCloseInterest);
newEodPayPosition.InterestFeeSum = closePercent == 1
- && RoundMoney(pendingInterestFeeBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterestFee)
+ && EodPnlCalculator.RoundMoney(pendingInterestFeeBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterestFee)
? 0m
- : RoundEodInterest(pendingInterestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee);
+ : EodPnlCalculator.RoundEodInterest(pendingInterestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee);
//持仓内容-利息腿-损益统计(本方视角)
// InterestProfitSum 是利息腿待实现总额,包含利息和费用;无费用时等于 InterestIncomeSum。
newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum;
@@ -1736,7 +1677,7 @@ namespace YLErp.Modules.SwapModule
newEodPayPosition.RealizedFee = closeFee;
newEodPayPosition.RealizedMtmPnL = newEodPayPosition.TdCloseMtmPnl;
newEodPayPosition.RealizedDividend = newEodPayPosition.TdCloseDividend;
- SetFloatingRealizedPnl(newEodPayPosition);
+ EodPnlCalculator.SetFloatingRealizedPnl(newEodPayPosition);
newEodPayPosition.PosiStatus = payQty == 0 ? 1 : 0;
UpdateDbOption(newEodPayPosition);
@@ -1810,7 +1751,7 @@ namespace YLErp.Modules.SwapModule
curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl;
curretEod.RealizedDividend = eod.RealizedDividend + curretEod.TdCloseDividend;
curretEod.RealizedFee = eod.RealizedFee + curretEod.TdCloseFee;
- SetFloatingRealizedPnl(curretEod);
+ EodPnlCalculator.SetFloatingRealizedPnl(curretEod);
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value
, seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection));
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
@@ -1825,17 +1766,6 @@ namespace YLErp.Modules.SwapModule
return curretEod;
}
- ///
- /// 浮动腿累计已实现盈亏由盯市、分红和费用三个已实现组成项汇总。
- /// 各组成项已经按本方视角落库,此处不再额外转换方向。
- ///
- private static void SetFloatingRealizedPnl(eod_swap_position position)
- {
- position.RealizedPnl = position.RealizedMtmPnL
- + position.RealizedDividend
- + position.RealizedFee;
- }
-
///
/// 更新虚拟交易费用
///
@@ -1911,7 +1841,7 @@ namespace YLErp.Modules.SwapModule
{
curretEod.PosiDividendSum = 0;
}
- SetFloatingRealizedPnl(curretEod);
+ EodPnlCalculator.SetFloatingRealizedPnl(curretEod);
curretEod.SwapPositionValue -= curretEod.TdCloseDividend;
curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending);
@@ -2091,7 +2021,7 @@ namespace YLErp.Modules.SwapModule
curretEod.RealizedMtmPnL = curretEod.TdCloseMtmPnl;
curretEod.RealizedDividend = curretEod.TdCloseDividend;
curretEod.RealizedFee = curretEod.TdCloseFee;
- SetFloatingRealizedPnl(curretEod);
+ EodPnlCalculator.SetFloatingRealizedPnl(curretEod);
curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0;
if (curretEod.PosiStatus == 1)
{
@@ -2182,8 +2112,8 @@ namespace YLErp.Modules.SwapModule
eod_Swap.BookId = td.AssetId;
eod_Swap.ValueDate = settleDate;
eod_Swap.StructureType = td.StructureType;
- FillPositionLegSummary(eod_Swap, positions);
- eod_Swap.InterestPnL = SumInterestPnL(interestPositions);
+ EodPnlCalculator.FillPositionLegSummary(eod_Swap, positions);
+ eod_Swap.InterestPnL = EodPnlCalculator.SumInterestPnL(interestPositions);
eod_Swap.PostionValue = eodSwapPositions.Sum(s => s.SwapPositionValue);
// 保证金腿的利息现金流方向与保证金本金方向相反。
// 不能直接汇总 RealizedPnl,否则“收取客户保证金”的腿会把应支付给客户的
@@ -2236,8 +2166,8 @@ namespace YLErp.Modules.SwapModule
var interestPositions = eodSwapPositions.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//利息腿
var positions = eodSwapPositions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//持仓腿
eod_Swap.NotionalValue = Math.Round(Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
- FillPositionLegSummary(eod_Swap, positions);
- eod_Swap.InterestPnL += SumInterestPnL(interestPositions);
+ EodPnlCalculator.FillPositionLegSummary(eod_Swap, positions);
+ eod_Swap.InterestPnL += EodPnlCalculator.SumInterestPnL(interestPositions);
eodSwapPositions.ForEach(x =>
{
var ratio = DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode);
@@ -2263,38 +2193,7 @@ namespace YLErp.Modules.SwapModule
/// 我方支付给对手方的成本计入,而不会错误增加框架合约已实现收益。
/// 抽为静态纯函数以支持无库单测(marginTypes 等价于 ConsTrade.InterestMarginModels)。
///
- public static decimal CalculateSwapRealizedPnl(eod_swap_position position)
- {
- var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
-
- return position.RealizedMtmPnL
- + position.RealizedDividend
- + position.RealizedFee
- + position.RealizedInterest * interestRatio
- + position.RealizedInterestFee;
- }
-
- /// 填充框架合约的持仓腿汇总字段(多空名义本金/市值/浮动盈亏/dv01/平仓量)。
- /// SaveEodSwap 与 UpdateEodSwap 共用,消除 ~10 行重复。
- private static void FillPositionLegSummary(eod_swap eod_Swap, List positions)
- {
- eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
- eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
- eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue);
- eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue);
- eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum);
- eod_Swap.dv01 = positions.Sum(s => s.dv01 ?? 0);
- eod_Swap.TdCloseQty = positions.Sum(s => s.TdCloseQty);
- }
-
- /// 利息腿 PnL 汇总(按方向比例 + 保证金翻转)。原 SaveEodSwap/UpdateEodSwap 各一段 ForEach。
- private static decimal SumInterestPnL(List interestPositions)
- {
- decimal interestPnL = 0;
- foreach (var x in interestPositions)
- interestPnL += x.InterestProfitSum * DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode);
- return interestPnL;
- }
+ public static decimal CalculateSwapRealizedPnl(eod_swap_position position) => EodPnlCalculator.CalculateSwapRealizedPnl(position);
///
/// 风险报表符号归一化:把历史两种符号口径的 TdCloseInterest/RealizedInterest
@@ -2303,20 +2202,7 @@ namespace YLErp.Modules.SwapModule
/// 抽为 public static 纯函数以支持无库单测(见 SwapReportInterestSignNormalizeTest)。
/// 仅当 InterestDirection > 0 时执行(与原内联逻辑等价)。
///
- public static void NormalizeInterestSignForReport(eod_swap_position position)
- {
- if (position.InterestDirection <= 0) return;
-
- if (position.InterestMode == (int)InterestModeEnum.标的期初全价)
- {
- return;
- }
- var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
- position.TdCloseInterest = Math.Abs(position.TdCloseInterest) * interestRatio;
- position.RealizedInterest = Math.Abs(position.RealizedInterest) * interestRatio;
- // 兼容修复前已落库的利息腿:当时只累计了明细字段,未同步写入 RealizedPnl。
- position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee;
- }
+ public static void NormalizeInterestSignForReport(eod_swap_position position) => EodPnlCalculator.NormalizeInterestSignForReport(position);
///
/// 获取多空组合 平仓详细
@@ -2806,7 +2692,7 @@ namespace YLErp.Modules.SwapModule
item.position.FloatRateUnderlyingCode = floatRateInterest?.FloatRateUnderlyingCode;
item.position.FloatRate = floatRateInterest?.FloatRate ?? 0;
item.OpenMarginAmount = initialMargins.Sum(s => s.InterestPrincipalFix * DirectionRatio.ReceivePay(s.InterestDirection));
- item.OpenMarginRate = CalculateWeightedMarginRate(tradeMargins);
+ item.OpenMarginRate = EodPnlCalculator.CalculateWeightedMarginRate(tradeMargins);
item.AdditionalMarginAmount = additionalMargins.Sum(s => s.InterestPrincipalFix * DirectionRatio.ReceivePay(s.InterestDirection));
item.MarginInterestAmount = CalculateWeightedMarginInterest(eodMargins);
item.InterestAmount = eodInterests.Sum(s => s.InterestIncomeSum * (-DirectionRatio.ReceivePay(s.InterestDirection)));
@@ -2831,29 +2717,12 @@ namespace YLErp.Modules.SwapModule
return retListResult;
}
- ///
- /// 计算预付金利率。多条初始/追加预付金腿按本金绝对值加权,
- /// 不按收付方向轧差,避免相反方向本金抵消后放大利率。
- ///
- private static decimal CalculateWeightedMarginRate(IEnumerable margins)
- {
- var marginList = margins.ToList();
- var totalWeight = marginList.Sum(x => Math.Abs(x.InterestPrincipalFix));
- return totalWeight == 0
- ? 0
- : marginList.Sum(x => x.InterestRateDefault * Math.Abs(x.InterestPrincipalFix)) / totalWeight;
- }
-
///
/// 计算预付金利息金额。InterestIncomeSum 已是各腿利息金额,
/// 按收取为正、支付为负直接轧差求和,不做本金加权。
/// 抽为 public static 纯函数以支持无库单测(见 SwapWeightedMarginInterestTest)。
///
- public static decimal CalculateWeightedMarginInterest(IEnumerable margins)
- {
- return margins.Sum(x =>
- x.InterestIncomeSum * DirectionRatio.ReceivePay(x.InterestDirection));
- }
+ public static decimal CalculateWeightedMarginInterest(IEnumerable margins) => EodPnlCalculator.CalculateWeightedMarginInterest(margins);
///
/// 固定利息腿的累计已实现盈亏 = 累计已实现利息 + 累计已实现利息费用。
@@ -2861,10 +2730,7 @@ namespace YLErp.Modules.SwapModule
/// 抽为 public static 纯函数以支持无库单测(见 SwapFixedLegRealizedPnlTest),
/// 并消除复制粘贴带来的笔误风险(如 L1296 历史双分号)。
///
- public static void SetFixedLegRealizedPnl(eod_swap_position position)
- {
- position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee;
- }
+ public static void SetFixedLegRealizedPnl(eod_swap_position position) => EodPnlCalculator.SetFixedLegRealizedPnl(position);
///
/// 将数据库中以公司/交易簿记方向保存的日终字段转换为客户视角。
/// 该转换必须在拆分浮动收益、费用和期间付息/分红之前完成,