diff --git a/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs
index 4c2af32c..a904af3a 100644
--- a/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs
+++ b/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs
@@ -13,7 +13,7 @@ using YLErp.Core.Interest;
namespace UnitTestProject.Modules.SwapModule.Accrual
{
///
- /// 影子测试:CalcDailyCompoundInterestByEod(旧逐日循环)vs FundingLegAccrual.AccrueCompoundEod(新纯函数)。
+ /// 影子测试:CalcDailyCompoundInterestByEod(旧逐日循环)vs CompoundInterestAccrual.AccrueEod(新纯函数)。
/// 构造同一组参数,两套实现并行跑,断言结果一致(到分)。
///
[TestClass]
@@ -92,14 +92,14 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
decimal oldInterest = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailyCompoundInterestByEod(preEod, EodDate, TradeDate, position,
- Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m, Notional,
+ Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m,
ref oldInterest, ref oldTd);
// 新方法
var rate = FundingLegRate.Fixed(FixedRate);
var policy = new AccrualPolicy(AccrualBoundary.Both, true, 7, AnnualDays, true);
var remainingPercent = Math.Max(0m, Math.Min(1m, Notional / Notional));
- var result = FundingLegAccrual.AccrueCompoundEod(
+ var result = CompoundInterestAccrual.AccrueEod(
50_000m, Notional, Notional, 1m, rate, policy,
isResetDay: true, remainingPercent, EodDate);
@@ -125,13 +125,13 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
decimal oldInterest = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailyCompoundInterestByEod(preEod, nonResetDate, TradeDate, position,
- Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m, Notional,
+ Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m,
ref oldInterest, ref oldTd);
// 新方法
var rate = FundingLegRate.Fixed(FixedRate);
var policy = new AccrualPolicy(AccrualBoundary.Both, true, 7, AnnualDays, true);
- var result = FundingLegAccrual.AccrueCompoundEod(
+ var result = CompoundInterestAccrual.AccrueEod(
30_000m, Notional, Notional, 1m, rate, policy,
isResetDay: false, 0m, nonResetDate);
diff --git a/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs
index ce55867b..855419c5 100644
--- a/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs
+++ b/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs
@@ -7,7 +7,7 @@ using YLErp.Modules.SwapModule.Accrual;
namespace UnitTestProject.Modules.SwapModule.Accrual
{
///
- /// 影子测试:CalcDailyCompoundInterest(旧逐日循环)vs FundingLegAccrual.AccrueCompoundPeriod(新分段纯函数)。
+ /// 影子测试:CalcDailyCompoundInterest(旧逐日循环)vs CompoundInterestAccrual.AccruePeriod(新分段纯函数)。
///
[TestClass]
public class CompoundPeriodShadowTest
@@ -74,7 +74,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
decimal oldI = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent,
- AnnualDays, false, 0m, 1m, Notional, true, false,
+ AnnualDays, false, 0m, 1m, true, false,
ref oldI, ref oldTd);
// 新方法:固定利率全段相同,分段点 = PosiStartDate + k×7
@@ -86,7 +86,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
(StartDate.AddDays(7), allInRate),
(StartDate.AddDays(14), allInRate),
};
- var result = FundingLegAccrual.AccrueCompoundPeriod(
+ var result = CompoundInterestAccrual.AccruePeriod(
notional: Notional,
segmentRates: segRates,
startDate: StartDate,
@@ -122,7 +122,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
decimal oldI = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailyCompoundInterest(EndDate, position, Notional * closePct, flowEvent,
- AnnualDays, false, 0m, closePct, Notional, true, false,
+ AnnualDays, false, 0m, closePct, true, false,
ref oldI, ref oldTd, consumedInterest: consumed, resetCarryInterest: carry);
// 新方法
@@ -133,7 +133,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
(StartDate.AddDays(7), allInRate),
(StartDate.AddDays(14), allInRate),
};
- var result = FundingLegAccrual.AccrueCompoundPeriod(
+ var result = CompoundInterestAccrual.AccruePeriod(
notional: Notional * closePct,
segmentRates: segRates,
startDate: StartDate,
@@ -166,7 +166,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
decimal oldI = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent,
- AnnualDays, false, 0m, 1m, Notional, true, true,
+ AnnualDays, false, 0m, 1m, true, true,
ref oldI, ref oldTd);
// 新方法
@@ -177,7 +177,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
(StartDate.AddDays(7), allInRate),
(StartDate.AddDays(14), allInRate),
};
- var result = FundingLegAccrual.AccrueCompoundPeriod(
+ var result = CompoundInterestAccrual.AccruePeriod(
notional: Notional,
segmentRates: segRates,
startDate: StartDate,
diff --git a/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs
index d932e035..283ca087 100644
--- a/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs
+++ b/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs
@@ -12,7 +12,7 @@ using YLErp.Derivatives.Interest;
namespace UnitTestProject.Modules.SwapModule.Accrual
{
///
- /// 影子测试:CalcDailySimpleInterest(旧逐日循环)vs FundingLegAccrual.AccrueSimplePeriod(新分段纯函数)。
+ /// 影子测试:CalcDailySimpleInterest(旧逐日循环)vs SimpleInterestAccrual.AccruePeriod(新分段纯函数)。
///
[TestClass]
public class SimplePeriodShadowTest
@@ -106,7 +106,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
// 旧代码差分: dynomicPrincipal = preEod.TdInterestPrincipal(=0) + posiPrincipal - orginPv = 0
var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) };
var accrualPrincipal = 0m + Notional - Notional; // 差分 = 0
- var result = FundingLegAccrual.AccrueSimplePeriod(
+ var result = SimpleInterestAccrual.AccruePeriod(
priorAccrued: 0m,
notional: 0m,
unwindFraction: 1m,
@@ -152,7 +152,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
// 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv
var accrualPrincipal = Notional + Notional - Notional;
var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) };
- var result = FundingLegAccrual.AccrueSimplePeriod(
+ var result = SimpleInterestAccrual.AccruePeriod(
priorAccrued: 200_000m * 0.5m,
notional: accrualPrincipal,
unwindFraction: 0.5m,
@@ -215,7 +215,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
// 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv
var accrualPrincipal = Notional + Notional - Notional;
- var result = FundingLegAccrual.AccrueSimplePeriod(
+ var result = SimpleInterestAccrual.AccruePeriod(
priorAccrued: 200_000m * closePct,
notional: accrualPrincipal,
unwindFraction: closePct,
diff --git a/UnitTestProject/Modules/SwapModule/ConsumedInterestScenarioTest.cs b/UnitTestProject/Modules/SwapModule/ConsumedInterestScenarioTest.cs
index 6ad2800d..9b70262f 100644
--- a/UnitTestProject/Modules/SwapModule/ConsumedInterestScenarioTest.cs
+++ b/UnitTestProject/Modules/SwapModule/ConsumedInterestScenarioTest.cs
@@ -509,7 +509,7 @@ namespace YLErp.Modules.SwapModule
decimal tdInterestAmount = 0m;
service.CalcDailyCompoundInterestByEod(preEod, resetDate, startDate, position,
- principal, principal, flowEvent, AnnualDays, false, 0.013502m, 1m, principal,
+ principal, principal, flowEvent, AnnualDays, false, 0.013502m, 1m,
ref interestAmount, ref tdInterestAmount);
AssertDecimal(principal + pendingInterest, flowEvent.InterestPrincipal,
diff --git a/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs b/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs
index 29c1b37a..ad142e51 100644
--- a/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs
+++ b/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs
@@ -1725,14 +1725,14 @@ namespace YLErp.Modules.SwapModule
decimal expectedTdAmountAtEnd = 0m;
dealService.CalcDailyCompoundInterest(
finalCloseDate, position, remainingNotional, expectedEndFlow, AnnualDays, false,
- intermediateEod.FloatRate, 1m, originalNotional, true, false,
+ intermediateEod.FloatRate, 1m, true, false,
ref expectedAmountAtEnd, ref expectedTdAmountAtEnd);
var expectedPreviousFlow = new swap_flow_event { InterestRate = spread };
decimal expectedAmountAtPreviousEod = 0m;
decimal expectedTdAmountAtPreviousEod = 0m;
dealService.CalcDailyCompoundInterest(
intermediateDate, position, remainingNotional, expectedPreviousFlow, AnnualDays, false,
- intermediateEod.FloatRate, 1m, originalNotional, true, true,
+ intermediateEod.FloatRate, 1m, true, true,
ref expectedAmountAtPreviousEod, ref expectedTdAmountAtPreviousEod);
var expectedFinalInterest = intermediateEod.InterestIncomeSum
+ expectedAmountAtEnd - expectedAmountAtPreviousEod;
diff --git a/UnitTestProject/Modules/SwapModule/SwapUnwindSameDayDoublePartialTest.cs b/UnitTestProject/Modules/SwapModule/SwapUnwindSameDayDoublePartialTest.cs
index e6442d6c..5e18c5d1 100644
--- a/UnitTestProject/Modules/SwapModule/SwapUnwindSameDayDoublePartialTest.cs
+++ b/UnitTestProject/Modules/SwapModule/SwapUnwindSameDayDoublePartialTest.cs
@@ -8,7 +8,7 @@ namespace YLErp.Modules.SwapModule
/// 【同日多次部分平仓 · unwind 基数滚动表征测试】
/// ============================================================================
/// 背景:unwind 计息基数公式 basis = priorNotional + notional - baseNotional
- /// (FundingLegAccrual / CalcDailyCompoundInterestByEod 同源),其中
+ /// (CompoundInterestAccrual / CalcDailyCompoundInterestByEod 同源),其中
/// - priorNotional = 上一日终归档 eod_swap_position.TdInterestPrincipal
/// - baseNotional = orginPv = ResolveUnwindPreviousNotional(lastEod)(上一日终浮动端名义本金)
/// - notional = 当前持仓名义本金(posiNotionalValue,来自实时持仓)
diff --git a/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs b/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs
new file mode 100644
index 00000000..9418aa46
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs
@@ -0,0 +1,132 @@
+using YLErp.Core.Interest;
+using YLErp.Derivatives.Interest;
+
+namespace YLErp.Modules.SwapModule.Accrual;
+
+///
+/// 复利计息纯函数——EOD 单日 + intraday 多日。
+/// 复利特征:每个重置日把累计利息并入本金(basis = notional + accrued)。
+///
+public static class CompoundInterestAccrual
+{
+ private const int Precision = SwapInterest.FundingLegPrecision;
+
+ /// 复利日终计息基数(单一真相源,纯函数与调用方共用):
+ /// 重置日 = notional + 累计利息×剩余比例(利息并入本金);非重置日 = priorNotional(昨日滚动基数)。
+ /// remainingFraction 对齐 legacy 钳制到 [0,1]。
+ public static decimal EodBasis(
+ bool isResetDay, decimal notional, decimal priorAccrued, decimal remainingFraction, decimal priorNotional)
+ => isResetDay
+ ? notional + priorAccrued * Math.Max(0m, Math.Min(1m, remainingFraction))
+ : priorNotional;
+
+ ///
+ /// 复利日终计息(替换 CalcDailyCompoundInterestByEod 的纯数学部分)。
+ /// 重置日:basis = notional + priorAccrued × remainingFraction(利息并入本金)。
+ /// 非重置日:basis = priorNotional(昨日终滚动计息基数)。
+ ///
+ public static InterestResult AccrueEod(
+ decimal priorAccrued,
+ decimal priorNotional,
+ decimal notional,
+ decimal unwindFraction,
+ FundingLegRate rate,
+ AccrualPolicy policy,
+ bool isResetDay,
+ decimal remainingFraction,
+ DateTime eodDate,
+ AccrualTrace? trace = null)
+ {
+ var basis = EodBasis(isResetDay, notional, priorAccrued, remainingFraction, priorNotional);
+ var displayBasis = basis * unwindFraction;
+
+ var allInRate = rate.AllInRate;
+ trace?.EodContext(eodDate, isResetDay, unwindFraction, priorAccrued, priorNotional, notional, remainingFraction);
+ var dayInterest = displayBasis * allInRate;
+ var tdInterest = basis * allInRate;
+ if (policy.IsAnnualized)
+ {
+ dayInterest /= policy.AnnualDays;
+ tdInterest /= policy.AnnualDays;
+ }
+
+ var totalAccrued = priorAccrued * unwindFraction + dayInterest;
+ var result = new InterestResult(
+ SwapInterest.Round(totalAccrued, Precision),
+ SwapInterest.Round(tdInterest, Precision));
+
+ trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
+ trace?.MarkEnd(result.Accrued, result.AccruedToday);
+ return result;
+ }
+
+ ///
+ /// 复利多日计息(替换 CalcDailyCompoundInterest 的纯数学部分)。
+ /// 从 startDate 到 endDate 全程重放,每个重置日把累计利息并入本金。
+ ///
+ public static InterestResult AccruePeriod(
+ decimal notional,
+ IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
+ DateTime startDate,
+ DateTime endDate,
+ AccrualBoundary boundary,
+ int annualDays,
+ bool isAnnualized,
+ decimal resetCarryInterest,
+ decimal realizedInterest,
+ decimal unwindFraction,
+ out decimal finalBasis,
+ AccrualTrace? trace = null)
+ {
+ decimal accrualBasis = notional;
+ decimal accrued = 0m;
+
+ trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
+
+ for (int si = 0; si < segmentRates.Count; si++)
+ {
+ var isLastSegment = si == segmentRates.Count - 1;
+ var segEnd = isLastSegment
+ ? endDate
+ : segmentRates[si + 1].StartDate;
+
+ // 重置日并本金
+ accrualBasis = si == 0 ? notional : notional + accrued;
+
+ // 末日恰好是重置日且 carry 非零:用存量替代(旧代码 i%interestPeriod==0 && accrueDate==endDate)。
+ var usedCarry = false;
+ if (isLastSegment && si > 0 && resetCarryInterest != 0m
+ && segmentRates[si].StartDate == endDate)
+ {
+ accrualBasis = notional + resetCarryInterest;
+ usedCarry = true;
+ }
+
+ if (si > 0)
+ trace?.Rollover(segmentRates[si].StartDate, usedCarry ? resetCarryInterest : accrued, accrualBasis);
+
+ var segIncludeStart = (si == 0) ? boundary.IncludeStart : true;
+ var segIncludeEnd = isLastSegment ? boundary.IncludeEnd : false;
+ var days = SwapInterest.AccrualDays(segmentRates[si].StartDate, segEnd,
+ AccrualBoundary.Of(segIncludeStart, segIncludeEnd));
+ if (days <= 0) continue;
+
+ var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
+ var segInterest = accrualBasis * dailyRate * days;
+ accrued += segInterest;
+ trace?.Segment(si, segmentRates[si].StartDate, segEnd, days, segmentRates[si].Rate, accrualBasis, segInterest, accrued);
+ }
+
+ finalBasis = accrualBasis;
+
+ if (realizedInterest != 0m)
+ trace?.Unwind(endDate, unwindFraction, realizedInterest * unwindFraction, accrued - realizedInterest * unwindFraction);
+ accrued -= realizedInterest * unwindFraction;
+
+ var result = new InterestResult(
+ SwapInterest.Round(accrued, Precision),
+ SwapInterest.Round(accrued, Precision));
+ trace?.MarkEnd(result.Accrued, result.AccruedToday);
+ return result;
+ }
+}
diff --git a/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs b/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs
deleted file mode 100644
index 90d3500d..00000000
--- a/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs
+++ /dev/null
@@ -1,239 +0,0 @@
-using YLErp.Core.Interest;
-using YLErp.Derivatives.Interest;
-
-namespace YLErp.Modules.SwapModule.Accrual;
-
-///
-/// 融资腿计息编排层——纯数学部分(替换 SwapDealService 内 CalcDaily* 家族的纯计算)。
-///
-/// 命名规范(对齐 QuantLib / Strata):
-/// - notional → 计息名义本金(不用 principal,swap leg 用 notional 是业界标准)
-/// - accrued → 累计应计利息
-/// - unwindFraction → 平仓比例(0~1)
-/// - realizedInterest → 历史已结利息(legacy: consumedInterest)
-/// - priorNotional → 昨日终滚动计息基数(legacy: TdInterestPrincipal / dynomicPrincipal)
-///
-public static class FundingLegAccrual
-{
- private const int Precision = SwapInterest.FundingLegPrecision;
-
- /// 复利日终计息基数(单一真相源,纯函数与调用方共用):
- /// 重置日 = notional + 累计利息×剩余比例(利息并入本金);非重置日 = priorNotional(昨日滚动基数)。
- /// remainingFraction 对齐 legacy 钳制到 [0,1]。
- public static decimal CompoundEodBasis(
- bool isResetDay, decimal notional, decimal priorAccrued, decimal remainingFraction, decimal priorNotional)
- => isResetDay
- ? notional + priorAccrued * Math.Max(0m, Math.Min(1m, remainingFraction))
- : priorNotional;
-
- ///
- /// 单利日终计息(替换 CalcDailySimpleInterestByEod 的纯数学部分)。
- /// EOD 无差分:basis = priorNotional(昨日终滚动计息基数)。
- ///
- public static InterestResult AccrueSimpleEod(
- decimal priorAccrued,
- decimal priorNotional,
- decimal unwindFraction,
- FundingLegRate rate,
- AccrualPolicy policy,
- DateTime eodDate,
- AccrualTrace? trace = null)
- {
- var basis = priorNotional;
- var displayBasis = basis * unwindFraction;
-
- var allInRate = rate.AllInRate;
- var dayInterest = displayBasis * allInRate;
- var tdInterest = basis * allInRate;
- if (policy.IsAnnualized)
- {
- dayInterest /= policy.AnnualDays;
- tdInterest /= policy.AnnualDays;
- }
-
- var totalAccrued = priorAccrued + dayInterest;
- var result = new InterestResult(
- SwapInterest.Round(totalAccrued, Precision),
- SwapInterest.Round(tdInterest, Precision));
-
- trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
- trace?.MarkEnd(result.Accrued, result.AccruedToday);
- return result;
- }
-
- ///
- /// 复利日终计息(替换 CalcDailyCompoundInterestByEod 的纯数学部分)。
- /// 重置日:basis = notional + priorAccrued × remainingFraction(利息并入本金)。
- /// 非重置日:basis = priorNotional(昨日终滚动计息基数)。
- ///
- public static InterestResult AccrueCompoundEod(
- decimal priorAccrued,
- decimal priorNotional,
- decimal notional,
- decimal unwindFraction,
- FundingLegRate rate,
- AccrualPolicy policy,
- bool isResetDay,
- decimal remainingFraction,
- DateTime eodDate,
- AccrualTrace? trace = null)
- {
- var basis = CompoundEodBasis(isResetDay, notional, priorAccrued, remainingFraction, priorNotional);
- var displayBasis = basis * unwindFraction;
-
- var allInRate = rate.AllInRate;
- trace?.EodContext(eodDate, isResetDay, unwindFraction, priorAccrued, priorNotional, notional, remainingFraction);
- var dayInterest = displayBasis * allInRate;
- var tdInterest = basis * allInRate;
- if (policy.IsAnnualized)
- {
- dayInterest /= policy.AnnualDays;
- tdInterest /= policy.AnnualDays;
- }
-
- var totalAccrued = priorAccrued * unwindFraction + dayInterest;
- var result = new InterestResult(
- SwapInterest.Round(totalAccrued, Precision),
- SwapInterest.Round(tdInterest, Precision));
-
- trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
- trace?.MarkEnd(result.Accrued, result.AccruedToday);
- return result;
- }
-
- ///
- /// 单利多日计息(替换 CalcDailySimpleInterest 的纯数学部分)。
- /// 本金全程恒定,按重置日分段取利率。
- /// Accrued = 缩放累计(InterestAmount),AccruedToday = 未缩放累计(TdInterestAmount)。
- ///
- public static InterestResult AccrueSimplePeriod(
- decimal priorAccrued,
- decimal notional,
- decimal unwindFraction,
- IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
- DateTime startDate,
- DateTime endDate,
- DateTime priorValueDate,
- AccrualBoundary boundary,
- int annualDays,
- bool isAnnualized,
- AccrualTrace? trace = null)
- {
- var displayBasis = notional * unwindFraction;
- decimal accrued = priorAccrued; // 缩放累计 → InterestAmount
- decimal accruedUnscaled = priorAccrued; // 未缩放累计 → TdInterestAmount
-
- trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
-
- var segStart = startDate;
-
- for (int si = 0; si < segmentRates.Count; si++)
- {
- var segEnd = si < segmentRates.Count - 1
- ? segmentRates[si + 1].StartDate
- : endDate;
-
- var effectiveStart = segStart > priorValueDate ? segStart : priorValueDate.AddDays(1);
- if (effectiveStart > segEnd) { segStart = segEnd; continue; }
-
- // calcFirst 只跳过 startDate 本身;其余天(含重置日、ValueDate+1)只要 > ValueDate 恒纳入。
- // 与旧逐日循环一致:if (!calcFirst && accrueDate == startDate) continue 是唯一的首日跳过。
- // 中间段的 segIncludeStart 被 days<=0 跳过后误置 false,此处按 startDate 判定而非继承标记。
- var includeStart = effectiveStart == startDate ? boundary.IncludeStart : true;
- // calcLast 只影响 endDate 本身——只有真正的末段(si==Count-1)才算尾,
- // 不能用 segEnd==endDate 判断(interestPeriod=1 时中间段 segEnd 也可能==endDate)。
- var isLastSegment = si == segmentRates.Count - 1;
- var segBoundary = AccrualBoundary.Of(includeStart, isLastSegment && boundary.IncludeEnd);
- var days = SwapInterest.AccrualDays(effectiveStart, segEnd, segBoundary);
- if (days <= 0) { segStart = segEnd; continue; }
-
- var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
- var segInterest = displayBasis * dailyRate * days;
- accrued += segInterest;
- accruedUnscaled += notional * dailyRate * days;
- trace?.Segment(si, effectiveStart, segEnd, days, segmentRates[si].Rate, displayBasis, segInterest, accrued);
-
- segStart = segEnd;
- }
-
- var result = new InterestResult(
- SwapInterest.Round(accrued, Precision),
- SwapInterest.Round(accruedUnscaled, Precision));
- trace?.MarkEnd(result.Accrued, result.AccruedToday);
- return result;
- }
-
- ///
- /// 复利多日计息(替换 CalcDailyCompoundInterest 的纯数学部分)。
- /// 从 startDate 到 endDate 全程重放,每个重置日把累计利息并入本金。
- ///
- public static InterestResult AccrueCompoundPeriod(
- decimal notional,
- IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
- DateTime startDate,
- DateTime endDate,
- AccrualBoundary boundary,
- int annualDays,
- bool isAnnualized,
- decimal resetCarryInterest,
- decimal realizedInterest,
- decimal unwindFraction,
- out decimal finalBasis,
- AccrualTrace? trace = null)
- {
- decimal accrualBasis = notional;
- decimal accrued = 0m;
-
- trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
-
- for (int si = 0; si < segmentRates.Count; si++)
- {
- var isLastSegment = si == segmentRates.Count - 1;
- var segEnd = isLastSegment
- ? endDate
- : segmentRates[si + 1].StartDate;
-
- // 重置日并本金
- accrualBasis = si == 0 ? notional : notional + accrued;
-
- // 末日恰好是重置日且 carry 非零:用存量替代(旧代码 i%interestPeriod==0 && accrueDate==endDate)。
- // 注意:必须同时判断 startDate==endDate——endDate 非重置日时最后一段起点 < endDate,不应触发。
- var usedCarry = false;
- if (isLastSegment && si > 0 && resetCarryInterest != 0m
- && segmentRates[si].StartDate == endDate)
- {
- accrualBasis = notional + resetCarryInterest;
- usedCarry = true;
- }
-
- // 复利每段起点:记录并本金瞬间(非首段 = 利息滚入计息基数)
- if (si > 0)
- trace?.Rollover(segmentRates[si].StartDate, usedCarry ? resetCarryInterest : accrued, accrualBasis);
-
- // 半开区间:重置日归下一段(旧代码逐日循环中重置日先更新本金再算息)
- var segIncludeStart = (si == 0) ? boundary.IncludeStart : true;
- var segIncludeEnd = isLastSegment ? boundary.IncludeEnd : false;
- var days = SwapInterest.AccrualDays(segmentRates[si].StartDate, segEnd,
- AccrualBoundary.Of(segIncludeStart, segIncludeEnd));
- if (days <= 0) continue;
-
- var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
- var segInterest = accrualBasis * dailyRate * days;
- accrued += segInterest;
- trace?.Segment(si, segmentRates[si].StartDate, segEnd, days, segmentRates[si].Rate, accrualBasis, segInterest, accrued);
- }
-
- finalBasis = accrualBasis;
-
- // 扣除历史已结利息
- if (realizedInterest != 0m)
- trace?.Unwind(endDate, unwindFraction, realizedInterest * unwindFraction, accrued - realizedInterest * unwindFraction);
- accrued -= realizedInterest * unwindFraction;
-
- var result = new InterestResult(
- SwapInterest.Round(accrued, Precision),
- SwapInterest.Round(accrued, Precision));
- trace?.MarkEnd(result.Accrued, result.AccruedToday);
- return result;
- }
-}
diff --git a/YLErpDAL/Modules/SwapModule/Accrual/SimpleInterestAccrual.cs b/YLErpDAL/Modules/SwapModule/Accrual/SimpleInterestAccrual.cs
new file mode 100644
index 00000000..fb1378ab
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/Accrual/SimpleInterestAccrual.cs
@@ -0,0 +1,106 @@
+using YLErp.Core.Interest;
+using YLErp.Derivatives.Interest;
+
+namespace YLErp.Modules.SwapModule.Accrual;
+
+///
+/// 单利计息纯函数——EOD 单日 + intraday 多日。
+/// 单利特征:本金全程恒定(无并本金),按重置日分段取利率。
+///
+public static class SimpleInterestAccrual
+{
+ private const int Precision = SwapInterest.FundingLegPrecision;
+
+ ///
+ /// 单利日终计息(替换 CalcDailySimpleInterestByEod 的纯数学部分)。
+ /// EOD 无差分:basis = priorNotional(昨日终滚动计息基数)。
+ ///
+ public static InterestResult AccrueEod(
+ decimal priorAccrued,
+ decimal priorNotional,
+ decimal unwindFraction,
+ FundingLegRate rate,
+ AccrualPolicy policy,
+ DateTime eodDate,
+ AccrualTrace? trace = null)
+ {
+ var basis = priorNotional;
+ var displayBasis = basis * unwindFraction;
+
+ var allInRate = rate.AllInRate;
+ var dayInterest = displayBasis * allInRate;
+ var tdInterest = basis * allInRate;
+ if (policy.IsAnnualized)
+ {
+ dayInterest /= policy.AnnualDays;
+ tdInterest /= policy.AnnualDays;
+ }
+
+ var totalAccrued = priorAccrued + dayInterest;
+ var result = new InterestResult(
+ SwapInterest.Round(totalAccrued, Precision),
+ SwapInterest.Round(tdInterest, Precision));
+
+ trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
+ trace?.MarkEnd(result.Accrued, result.AccruedToday);
+ return result;
+ }
+
+ ///
+ /// 单利多日计息(替换 CalcDailySimpleInterest 的纯数学部分)。
+ /// 本金全程恒定,按重置日分段取利率。
+ /// Accrued = 缩放累计(InterestAmount),AccruedToday = 未缩放累计(TdInterestAmount)。
+ ///
+ public static InterestResult AccruePeriod(
+ decimal priorAccrued,
+ decimal notional,
+ decimal unwindFraction,
+ IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
+ DateTime startDate,
+ DateTime endDate,
+ DateTime priorValueDate,
+ AccrualBoundary boundary,
+ int annualDays,
+ bool isAnnualized,
+ AccrualTrace? trace = null)
+ {
+ var displayBasis = notional * unwindFraction;
+ decimal accrued = priorAccrued; // 缩放累计 → InterestAmount
+ decimal accruedUnscaled = priorAccrued; // 未缩放累计 → TdInterestAmount
+
+ trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
+
+ var segStart = startDate;
+
+ for (int si = 0; si < segmentRates.Count; si++)
+ {
+ var segEnd = si < segmentRates.Count - 1
+ ? segmentRates[si + 1].StartDate
+ : endDate;
+
+ var effectiveStart = segStart > priorValueDate ? segStart : priorValueDate.AddDays(1);
+ if (effectiveStart > segEnd) { segStart = segEnd; continue; }
+
+ // calcFirst 只跳过 startDate 本身;其余天(含重置日、ValueDate+1)只要 > ValueDate 恒纳入。
+ var includeStart = effectiveStart == startDate ? boundary.IncludeStart : true;
+ var isLastSegment = si == segmentRates.Count - 1;
+ var segBoundary = AccrualBoundary.Of(includeStart, isLastSegment && boundary.IncludeEnd);
+ var days = SwapInterest.AccrualDays(effectiveStart, segEnd, segBoundary);
+ if (days <= 0) { segStart = segEnd; continue; }
+
+ var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
+ var segInterest = displayBasis * dailyRate * days;
+ accrued += segInterest;
+ accruedUnscaled += notional * dailyRate * days;
+ trace?.Segment(si, effectiveStart, segEnd, days, segmentRates[si].Rate, displayBasis, segInterest, accrued);
+
+ segStart = segEnd;
+ }
+
+ var result = new InterestResult(
+ SwapInterest.Round(accrued, Precision),
+ SwapInterest.Round(accruedUnscaled, Precision));
+ trace?.MarkEnd(result.Accrued, result.AccruedToday);
+ return result;
+ }
+}
diff --git a/YLErpDAL/Modules/SwapModule/Margin/MarginModes.cs b/YLErpDAL/Modules/SwapModule/Margin/MarginModes.cs
index 7232022f..359e3fc9 100644
--- a/YLErpDAL/Modules/SwapModule/Margin/MarginModes.cs
+++ b/YLErpDAL/Modules/SwapModule/Margin/MarginModes.cs
@@ -41,4 +41,17 @@ public static class MarginModes
/// 判断 mode 是否属于保证金(非 LINQ 场景用)。
public static bool Contains(int interestMode) => All.Contains(interestMode);
+
+ /// 固定值 + 保证金 mode 集合(固定值/初始预付金/追加预付金)。
+ /// 用于 EOD 场景判断"计息基数取 InterestPrincipalFix 而非持仓名义本金"的腿。
+ /// 替代 SwapEodPositionService 中 3 处内联 new List{固定值, 初始预付金, 追加预付金}。
+ public static readonly IReadOnlyCollection FixedAmountAndMargin = new HashSet
+ {
+ (int)InterestModeEnum.固定值,
+ (int)InterestModeEnum.初始预付金,
+ (int)InterestModeEnum.追加预付金,
+ };
+
+ /// 判断 mode 是否为固定值或保证金。
+ public static bool IsFixedAmountOrMargin(int interestMode) => FixedAmountAndMargin.Contains(interestMode);
}
diff --git a/YLErpDAL/Modules/SwapModule/ReturnLegs/DirectionRatio.cs b/YLErpDAL/Modules/SwapModule/ReturnLegs/DirectionRatio.cs
index 13808855..33d80a87 100644
--- a/YLErpDAL/Modules/SwapModule/ReturnLegs/DirectionRatio.cs
+++ b/YLErpDAL/Modules/SwapModule/ReturnLegs/DirectionRatio.cs
@@ -1,5 +1,6 @@
using YLErp.DBModels;
using YLErp.DBModels.Enums;
+using YLErp.Modules.SwapModule.Margin;
namespace YLErp.Modules.SwapModule.ReturnLegs;
@@ -19,4 +20,17 @@ public static class DirectionRatio
/// 收付方向因子。收取=+1, 支付=-1。
public static int ReceivePay(int direction)
=> direction == (int)SwapDirectionEnum.收取 ? 1 : -1;
+
+ /// 利息腿 PnL 方向因子。收取=+1/支付=-1;保证金腿翻转(利息现金流与本金方向相反)。
+ /// 原 7 处内联 `收取?1:-1; if(MarginModes) ratio=-ratio` 收口到此。
+ public static int InterestLegPnl(int interestDirection, int interestMode)
+ {
+ var ratio = ReceivePay(interestDirection);
+ return MarginModes.Contains(interestMode) ? -ratio : ratio;
+ }
+
+ /// 按收付方向选汇率类型。收取→Buy, 支付→Sell。
+ /// 原 7 处内联 `收取 ? Buy : Sell` 收口到此。
+ public static CurrencyRateType RateType(int direction)
+ => direction == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell;
}
diff --git a/YLErpDAL/Modules/SwapModule/ReturnLegs/InterestIncomeCalc.cs b/YLErpDAL/Modules/SwapModule/ReturnLegs/InterestIncomeCalc.cs
new file mode 100644
index 00000000..8f1b2aa4
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/ReturnLegs/InterestIncomeCalc.cs
@@ -0,0 +1,21 @@
+namespace YLErp.Modules.SwapModule.ReturnLegs;
+
+///
+/// EOD 利息腿展示值计算——轻量单日公式(非 SwapDealService 的分段计息引擎)。
+///
+public static class InterestIncomeCalc
+{
+ /// 日应计利息 = 本金 × (固定利率 + 浮动利差) ÷ 年化天数(若年化)。
+ /// 原 3 处内联 `principal*(rate+float); if(annualized) /=annualDays` 收口到此。
+ public static decimal DailyAccrual(decimal principal, decimal rate, decimal floatRate, bool isAnnualized, int annualDays)
+ {
+ var amount = principal * (rate + floatRate);
+ return isAnnualized ? amount / annualDays : amount;
+ }
+
+ /// 已实现利息滚存。(prev + today×ratio, prevFee + todayFee)。
+ /// 原 4 处内联 2 行赋值收口到此。
+ public static (decimal Interest, decimal Fee) RollRealized(
+ decimal prevInterest, decimal prevFee, decimal closeInterest, decimal closeInterestFee, int ratio)
+ => (prevInterest + closeInterest * ratio, prevFee + closeInterestFee);
+}
diff --git a/YLErpDAL/Modules/SwapModule/ReturnLegs/MtmCalc.cs b/YLErpDAL/Modules/SwapModule/ReturnLegs/MtmCalc.cs
index 4bbb5620..7dedab2c 100644
--- a/YLErpDAL/Modules/SwapModule/ReturnLegs/MtmCalc.cs
+++ b/YLErpDAL/Modules/SwapModule/ReturnLegs/MtmCalc.cs
@@ -19,4 +19,12 @@ public static class MtmCalc
/// 收取=1, 支付=-1。
public static decimal UnrealizedPnl(decimal price, decimal costGrossPrice, decimal qty, decimal contractSize, int shortRatio, int ratio)
=> (price - costGrossPrice) * qty * contractSize * shortRatio * ratio;
+
+ /// 浮动端总未实现盈亏 = 盯市盈亏 + 分红 + 待结费用。原 4 处内联收口到此。
+ public static decimal ReturnLegProfitSum(decimal mtmPnl, decimal dividendSum, decimal feePending)
+ => mtmPnl + dividendSum + feePending;
+
+ /// 加权均价混合:(昨日均价×昨日量 + 今日∑(量×均额)) / 总量。原 4 处内联收口到此。
+ public static decimal BlendPrice(decimal prevPrice, decimal prevQty, decimal sumQtyTimesPrice, decimal totalQty)
+ => (prevPrice * prevQty + sumQtyTimesPrice) / totalQty;
}
diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs
index 307f3b5f..00f3419d 100644
--- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs
+++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs
@@ -1096,12 +1096,12 @@ namespace YLErp.Modules.SwapModule
if (position.InterestType == (int)InterestTypeEnum.复利)
{
// 复利计算
- CalcDailyCompoundInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, posiPrincipal, ref interestAmount, ref tdInterestAmount);
+ CalcDailyCompoundInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, ref interestAmount, ref tdInterestAmount);
}
else
{
// 单利计算
- CalcDailySimpleInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, posiPrincipal, ref interestAmount, ref tdInterestAmount);
+ CalcDailySimpleInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, ref interestAmount, ref tdInterestAmount);
}
}
@@ -1272,7 +1272,7 @@ namespace YLErp.Modules.SwapModule
// 它只在当前 endDate 恰好为重置日时使用,避免把同一笔历史利息重复资本化。
var resetCarryInterest = preEodPosition.InterestIncomeSum * remainingPercent;
CalcDailyCompoundInterest(endDate, position, closePosiNotionalValue, interest, annualDays, needPrice,
- floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount,
+ floateRate, closePrecent, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount,
consumedInterest, resetCarryInterest);
if (preEodPosition.id != 0 && closePrecent == 1m)
{
@@ -1299,7 +1299,7 @@ namespace YLErp.Modules.SwapModule
}
// 计算截至本次平仓日的累计利息 amountAtEnd
CalcDailyCompoundInterest(replayEndDate, position, closePosiNotionalValue,
- interestAtEnd, annualDays, needPrice, floateRate, closePrecent, orginPv,
+ interestAtEnd, annualDays, needPrice, floateRate, closePrecent,
calcFirst, calcLast, ref amountAtEnd, ref tdAmountAtEnd, consumedInterest);
var interestAtPreviousEod = new swap_flow_event { InterestRate = rate };
decimal amountAtPreviousEod = 0m;
@@ -1308,7 +1308,7 @@ namespace YLErp.Modules.SwapModule
// 因此此处按闭区间包含上一日终当天,避免算头不算尾时重复加入该日利息。
// 计算截至上一日终累积的利息 amountAtPreviousEod
CalcDailyCompoundInterest(preEodPosition.ValueDate, position, closePosiNotionalValue,
- interestAtPreviousEod, annualDays, needPrice, floateRate, closePrecent, orginPv,
+ interestAtPreviousEod, annualDays, needPrice, floateRate, closePrecent,
calcFirst, true, ref amountAtPreviousEod, ref tdAmountAtPreviousEod, consumedInterest);
// 例如 0004:5/18 待实现 -118631.261797,加 5/19 新增约 -4648.912760,
// 得到最终应结 -123280.174557,按金额两位落为 Excel BN 的 -123280.17。
@@ -1334,6 +1334,29 @@ namespace YLErp.Modules.SwapModule
return interest;
}
+ ///
+ /// 按 interest_rule 取 FR007 定盘价。无浮动标的时返回 fallback;取不到抛异常。
+ /// EOD 单日取率 + BuildSegmentRates 多日取率共用此方法,FR007 定盘逻辑收口到一处。
+ ///
+ private decimal ResolveFloatRate(swap_position position, DateTime date, decimal fallback)
+ {
+ if (string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) return fallback;
+ var fixingDate = IndexFixerBase.GetFixingDate(date, position.interest_rule);
+ if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
+ return fixing != 0m ? fixing : fallback;
+ throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
+ }
+
+ /// 构造利率值对象:固定腿→Fixed(spread),浮动腿→Floating(spread+fixing)。
+ private static FundingLegRate BuildLegRate(swap_position position, decimal spread, decimal effectiveFloat)
+ => string.IsNullOrEmpty(position.FloatRateUnderlyingCode)
+ ? FundingLegRate.Fixed(spread)
+ : FundingLegRate.Floating(spread, effectiveFloat);
+
+ /// 构造 EOD 计息政策(算头算尾,重置周期取 position.interest_rest_days)。
+ private static AccrualPolicy BuildEodPolicy(swap_position position, int annualDays, bool isCompound)
+ => new AccrualPolicy(AccrualBoundary.Both, isCompound, position.interest_rest_days ?? 1, annualDays, position.IsAnnualized);
+
///
/// 按重置周期切分利率段,每段记录 all-in 利率(spread+fixing)。返回 (分段列表, 末段浮动利率)。
/// fetchAfterDate: 仅该日期之后的重置日才取 FR007(单利传 ValueDate,复利传 null 全程取)。
@@ -1349,19 +1372,8 @@ namespace YLErp.Modules.SwapModule
for (int i = 0; i <= calcDays; i += interestPeriod)
{
var resetDate = startDate.AddDays(i);
- if ((fetchAfterDate == null || resetDate > fetchAfterDate.Value)
- && !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
- {
- var fixingDate = IndexFixerBase.GetFixingDate(resetDate, position.interest_rule);
- if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
- {
- if (fixing != 0m) currentFloat = fixing;
- }
- else
- {
- throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
- }
- }
+ if (fetchAfterDate == null || resetDate > fetchAfterDate.Value)
+ currentFloat = ResolveFloatRate(position, resetDate, currentFloat);
rates.Add((resetDate, spread + currentFloat));
}
return (rates, currentFloat);
@@ -1379,7 +1391,7 @@ namespace YLErp.Modules.SwapModule
/// 年化天数
///
public void CalcDailyCompoundInterest(DateTime endDate, swap_position position, decimal principal, swap_flow_event flowEvent,
- int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast,
+ int annualDays, bool needPrice, decimal floateRate, decimal closePercent, bool calcFirst, bool calcLast,
ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m, decimal resetCarryInterest = 0m)
{
var startDate = position.PosiStartDate;
@@ -1392,7 +1404,7 @@ namespace YLErp.Modules.SwapModule
// 纯函数复利计息:分段重置日并本金 + resetCarryInterest + 扣 consumedInterest
var interestTrace = new AccrualTrace();
- var result = FundingLegAccrual.AccrueCompoundPeriod(
+ var result = CompoundInterestAccrual.AccruePeriod(
notional: principal,
segmentRates: segmentRates,
startDate: startDate,
@@ -1434,7 +1446,7 @@ namespace YLErp.Modules.SwapModule
// 纯函数计息:Accrued=缩放累计(InterestAmount),AccruedToday=未缩放累计(TdInterestAmount)
var interestTrace = new AccrualTrace();
- var result = FundingLegAccrual.AccrueSimplePeriod(
+ var result = SimpleInterestAccrual.AccruePeriod(
priorAccrued: preEodPosition.InterestProfitSum * closePercent,
notional: accrualBasis,
unwindFraction: closePercent,
@@ -1468,26 +1480,14 @@ namespace YLErp.Modules.SwapModule
/// 是否年化
/// 年化天数
///
- public void CalcDailyCompoundInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, ref decimal InterestAmount, ref decimal TdInterestAmount)
+ public void CalcDailyCompoundInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, ref decimal InterestAmount, ref decimal TdInterestAmount)
{
int interestPeriod = position.interest_rest_days ?? 1;
var isResetDay = (endDate - tradeDate).Days % interestPeriod == 0;
// 重置日按 interest_rule 重新定盘浮动利率(GLMS-JIATT-20260805 根因——
// 重置日=平仓日必须用新利率,否则沿用旧周期利率并污染后续 EOD)。非重置日沿用 floateRate。
- decimal effectiveFloat = floateRate;
- if (isResetDay && !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
- {
- var fixingDate = IndexFixerBase.GetFixingDate(endDate, position.interest_rule);
- if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
- {
- if (fixing != 0m) effectiveFloat = fixing;
- }
- else
- {
- throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
- }
- }
+ var effectiveFloat = isResetDay ? ResolveFloatRate(position, endDate, floateRate) : floateRate;
flowEvent.FloatRate = effectiveFloat;
// remainingFraction:重置日把上一日终待实现利息按本次平仓基数分摊(EOD 全量为 1)。
@@ -1495,21 +1495,13 @@ namespace YLErp.Modules.SwapModule
? Math.Max(0m, Math.Min(1m, principal / posiPrincipal))
: 1m;
- // 纯数学下沉至 FundingLegAccrual.AccrueCompoundEod(DDD 命名 + 末位生产精度 12 舍入)。
- var isFixedLeg = string.IsNullOrEmpty(position.FloatRateUnderlyingCode);
- var legRate = isFixedLeg
- ? FundingLegRate.Fixed(flowEvent.InterestRate)
- : FundingLegRate.Floating(flowEvent.InterestRate, effectiveFloat);
- var accrualPolicy = new AccrualPolicy(
- convention: AccrualBoundary.Both,
- isCompound: true,
- resetPeriodDays: interestPeriod,
- annualDays: annualDays,
- isAnnualized: position.IsAnnualized);
+ // 纯数学下沉至 CompoundInterestAccrual.AccrueEod(DDD 命名 + 末位生产精度 12 舍入)。
+ var legRate = BuildLegRate(position, flowEvent.InterestRate, effectiveFloat);
+ var accrualPolicy = BuildEodPolicy(position, annualDays, isCompound: true);
// 完整计息 trace:前后日期/基数/利率/重置标志全过程,经 SwapCalcTrace 常驻落盘(关键路径日志)。
var interestTrace = new AccrualTrace();
- var result = FundingLegAccrual.AccrueCompoundEod(
+ var result = CompoundInterestAccrual.AccrueEod(
priorAccrued: preEodPosition.InterestProfitSum,
priorNotional: preEodPosition.TdInterestPrincipal,
notional: posiPrincipal,
@@ -1524,7 +1516,7 @@ namespace YLErp.Modules.SwapModule
// flowEvent.InterestPrincipal:当日计息基数(已按平仓比例缩放)——下游 EOD 用它播种次日 TdInterestPrincipal。
// 复用 CompoundEodBasis 单一真相源(与 AccrueCompoundEod 内部同一公式)。
- flowEvent.InterestPrincipal = FundingLegAccrual.CompoundEodBasis(
+ flowEvent.InterestPrincipal = CompoundInterestAccrual.EodBasis(
isResetDay, posiPrincipal, preEodPosition.InterestProfitSum, remainingFraction,
preEodPosition.TdInterestPrincipal) * closePercent;
@@ -1535,7 +1527,7 @@ namespace YLErp.Modules.SwapModule
///
/// 计算单利 收盘(按重置天数分段,每段使用对应浮动利率)
///
- public void CalcDailySimpleInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, ref decimal InterestAmount, ref decimal TdInterestAmount)
+ public void CalcDailySimpleInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, ref decimal InterestAmount, ref decimal TdInterestAmount)
{
// 首次操作(preEod.id == 0):计息基数按存量本金初始化——保留旧行为(含对 preEod 的就地修正)。
if (preEodPosition.id == 0)
@@ -1545,39 +1537,18 @@ namespace YLErp.Modules.SwapModule
// 取率:重置日按 interest_rule 重新定盘浮动利率(GLMS-JIATT-20260805 根因——
// 重置日=平仓日必须用新利率,否则沿用旧周期利率并污染后续 EOD)。
- decimal effectiveFloat = floateRate;
int interestPeriod = position.interest_rest_days ?? 1;
- if ((endDate - tradeDate).Days % interestPeriod == 0
- && !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
- {
- var fixingDate = IndexFixerBase.GetFixingDate(endDate, position.interest_rule);
- if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
- {
- if (fixing != 0m) effectiveFloat = fixing;
- }
- else
- {
- throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
- }
- }
+ var isResetDay = (endDate - tradeDate).Days % interestPeriod == 0;
+ var effectiveFloat = isResetDay ? ResolveFloatRate(position, endDate, floateRate) : floateRate;
flowEvent.FloatRate = effectiveFloat;
- // 纯数学下沉至 FundingLegAccrual(DDD 命名 + 末位生产精度 12 舍入),行为与上版逐字对齐。
- // 利率构成按腿型封装:固定腿 → FixedRate;浮动腿 → Spread + IndexFixing(沿用旧实现 InterestRate+浮动利率 的口径)。
- var isFixedLeg = string.IsNullOrEmpty(position.FloatRateUnderlyingCode);
- var legRate = isFixedLeg
- ? FundingLegRate.Fixed(flowEvent.InterestRate)
- : FundingLegRate.Floating(flowEvent.InterestRate, effectiveFloat);
- var accrualPolicy = new AccrualPolicy(
- convention: AccrualBoundary.Both,
- isCompound: false,
- resetPeriodDays: position.interest_rest_days ?? 1,
- annualDays: annualDays,
- isAnnualized: position.IsAnnualized);
+ // 纯数学下沉至 SimpleInterestAccrual(末位生产精度 12 舍入)。
+ var legRate = BuildLegRate(position, flowEvent.InterestRate, effectiveFloat);
+ var accrualPolicy = BuildEodPolicy(position, annualDays, isCompound: false);
// 完整计息 trace:收集器由适配器创建,随后经 SwapCalcTrace 常驻落盘(关键路径日志,无条件)。
var interestTrace = new AccrualTrace();
- var result = FundingLegAccrual.AccrueSimpleEod(
+ var result = SimpleInterestAccrual.AccrueEod(
priorAccrued: preEodPosition.InterestProfitSum,
priorNotional: preEodPosition.TdInterestPrincipal,
unwindFraction: closePercent,
diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs
index f8d9dabb..e445a357 100644
--- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs
+++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs
@@ -662,7 +662,7 @@ namespace YLErp.Modules.SwapModule
decimal premiumTotal = 0;
premiumInterests.ForEach(x =>
{
- var ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? -1 : 1;
+ var ratio = -DirectionRatio.ReceivePay(x.InterestDirection);
premiumTotal += x.InterestClosePnL * ratio;
});
unwindData.SwapMarginRebatePnl = premiumTotal;
@@ -670,7 +670,7 @@ namespace YLErp.Modules.SwapModule
decimal interestTotal = 0;
interestLegs.ForEach(x =>
{
- var ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;
+ var ratio = DirectionRatio.ReceivePay(x.InterestDirection);
interestTotal += x.InterestClosePnL * ratio;
});
unwindData.SwapCloseAmount = interestTotal ;
@@ -1041,11 +1041,7 @@ namespace YLErp.Modules.SwapModule
eodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value;
}
var tradeExtend = td.trade_extend.ExtendObj;
- decimal ratio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负
- if (MarginModes.Contains(position.InterestMode))
- {
- ratio = -ratio;
- }
+ var ratio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
if (newEodPayPosition == null)
{
newEodPayPosition = new eod_swap_position();
@@ -1085,11 +1081,9 @@ namespace YLErp.Modules.SwapModule
newEodPayPosition.TdCloseInterest = flowEvents.Sum(x => x.InterestAmount);
newEodPayPosition.TdCloseInterestFee = newEodPayPosition.TdInterestFee;
//持仓内容-利息腿-损益统计(本方视角)
- var intersetAcmount = newEodPayPosition.TdInterestPrincipal * (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate);
- if (position.IsAnnualized)
- {
- intersetAcmount /= tradeExtend.AnnualDays;
- }
+ var intersetAcmount = InterestIncomeCalc.DailyAccrual(
+ newEodPayPosition.TdInterestPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate,
+ newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays);
newEodPayPosition.TdInterestIncome = intersetAcmount;// 要算一下当天产生的利息
var interestIncomeBeforeSettlement = eodPayPosition.InterestIncomeSum + newEodPayPosition.TdInterestIncome;
var interestFeeBeforeSettlement = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee;
@@ -1115,11 +1109,12 @@ namespace YLErp.Modules.SwapModule
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio);
//累计已实现
- newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio;
- newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee;
+ var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio);
+ newEodPayPosition.RealizedInterest = rolled.Interest;
+ newEodPayPosition.RealizedInterestFee = rolled.Fee;
SetFixedLegRealizedPnl(newEodPayPosition);
var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
- position.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell);
+ DirectionRatio.RateType(position.InterestDirection));
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
PersistEodSwapPosition(newEodPayPosition);
}
@@ -1176,11 +1171,7 @@ namespace YLErp.Modules.SwapModule
var tradeExtend = td.trade_extend.ExtendObj;
decimal posiNotionalValue = posiLongNotional + posiShortNational;
decimal closePercent = 1;
- decimal ratio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负
- if (MarginModes.Contains(position.InterestMode))
- {
- ratio = -ratio;
- }
+ var ratio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
if (eodPayPosition == null)
{
eodPayPosition = new eod_swap_position();
@@ -1198,7 +1189,7 @@ namespace YLErp.Modules.SwapModule
positions.Add(position);
List preEodPositions = new List();
preEodPositions.Add(eodPayPosition);
- var interestModes = new List() { (int)InterestModeEnum.固定值, (int)InterestModeEnum.初始预付金, (int)InterestModeEnum.追加预付金 };
+ var interestModes = MarginModes.FixedAmountAndMargin;
if (interestModes.Contains(position.InterestMode))
{
orginPv = eodPayPosition.InterestPrincipalFix;
@@ -1258,11 +1249,12 @@ namespace YLErp.Modules.SwapModule
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio);
//累计已实现
- newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio;
- newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee;
+ var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio);
+ newEodPayPosition.RealizedInterest = rolled.Interest;
+ newEodPayPosition.RealizedInterestFee = rolled.Fee;
SetFixedLegRealizedPnl(newEodPayPosition);
var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
- position.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell);
+ DirectionRatio.RateType(position.InterestDirection));
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
PersistEodSwapPosition(newEodPayPosition);
Log.Info($"the last newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
@@ -1301,11 +1293,7 @@ namespace YLErp.Modules.SwapModule
decimal oriPosiNotionalValue = posiLongNotional + posiShortNational + closeNational;
decimal posiNotionalValue = posiLongNotional + posiShortNational;
// ratio 只负责把腿内原始金额转换为本方盈亏方向,不参与计息金额本身的计算。
- decimal ratio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负
- if (MarginModes.Contains(position.InterestMode))
- {
- ratio = -ratio;
- }
+ var ratio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
// 首次日终结算可能包含当日收盘,因此尚无先前的日终利息持仓。
// 部分平仓仍要续接上一日日终:CalcUnwindInterest 会将 InterestProfitSum
// 加入本次待实现,已实现字段也必须按日累计,不能从新建的临时对象重新开始。
@@ -1339,7 +1327,7 @@ namespace YLErp.Modules.SwapModule
newEodPayPosition = eodPayPosition.Clone();
newEodPayPosition.id = 0;
}
- var interestModes = new List() { (int)InterestModeEnum.固定值, (int)InterestModeEnum.初始预付金, (int)InterestModeEnum.追加预付金 };
+ var interestModes = MarginModes.FixedAmountAndMargin;
if (interestModes.Contains(position.InterestMode))
{
orginPv = eodPayPosition.InterestPrincipalFix;
@@ -1381,7 +1369,7 @@ namespace YLErp.Modules.SwapModule
var autoInterest = interests[0];
autoInterest.InterestAmount = autoSettledInterestAmount;
autoInterest.InterestClosePnL = autoSettledInterestAmount
- * (autoInterest.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m);
+ * DirectionRatio.ReceivePay(autoInterest.InterestDirection);
}
newEodPayPosition.ValueDate = valueDate;
newEodPayPosition.PositionId = position.id;
@@ -1427,11 +1415,9 @@ namespace YLErp.Modules.SwapModule
newEodPayPosition.TdCloseInterest = manualSettledInterestAmount + autoSettledInterestAmount;
// intersetAcmount 是收盘后本金的一天应计展示值。算尾部分平仓时,下面的复利分支会改用
// 平仓前全额本金重算当天新增,但跨日携带的 TdInterestPrincipal 仍只能是剩余本金。
- var intersetAcmount = newEodPayPosition.TdInterestPrincipal * (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate);
- if (position.IsAnnualized)
- {
- intersetAcmount /= tradeExtend.AnnualDays;
- }
+ var intersetAcmount = InterestIncomeCalc.DailyAccrual(
+ newEodPayPosition.TdInterestPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate,
+ newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays);
newEodPayPosition.TdInterestIncome = autoSwap
? intersetAcmount
: !hasPreviousEod
@@ -1481,12 +1467,9 @@ namespace YLErp.Modules.SwapModule
var accrualPrincipal = calcLast
? fullPrincipal
: newEodPayPosition.TdInterestPrincipal;
- newEodPayPosition.TdInterestIncome = accrualPrincipal
- * (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate);
- if (position.IsAnnualized)
- {
- newEodPayPosition.TdInterestIncome /= tradeExtend.AnnualDays;
- }
+ newEodPayPosition.TdInterestIncome = InterestIncomeCalc.DailyAccrual(
+ accrualPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate,
+ newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays);
}
if (!autoSwap
&& closePercent > 0m && closePercent < 1m
@@ -1532,11 +1515,12 @@ namespace YLErp.Modules.SwapModule
//累计已实现
// RealizedInterest 只增不回滚:上日累计已实现 + 当日结息按方向后的金额。
// 收取腿的 -37119.14 会把累计已实现更新为 -37119.14;后续普通 EOD 保持该值。
- newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio;
- newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee;
+ var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio);
+ newEodPayPosition.RealizedInterest = rolled.Interest;
+ newEodPayPosition.RealizedInterestFee = rolled.Fee;
SetFixedLegRealizedPnl(newEodPayPosition);
var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
- position.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell);
+ DirectionRatio.RateType(position.InterestDirection));
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
Log.Info($"即将插入数据库的 newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
PersistEodSwapPosition(newEodPayPosition);
@@ -1556,7 +1540,7 @@ namespace YLErp.Modules.SwapModule
Log.Info($"eodPayPosition is {JsonHelper.Serialize(eodPayPosition, false)},newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
List intervals = position.SwapIntervalList;
var tradeExtend = td.trade_extend.ExtendObj;
- var interestModes = new List() { (int)InterestModeEnum.固定值, (int)InterestModeEnum.初始预付金, (int)InterestModeEnum.追加预付金 };
+ var interestModes = MarginModes.FixedAmountAndMargin;
if (eodPayPosition == null)
{
//if (position.PosiStartDate > valueDate)
@@ -1611,11 +1595,7 @@ namespace YLErp.Modules.SwapModule
{
closePercent = 1;
}
- decimal ratio = eodPayPosition.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负
- if (MarginModes.Contains(position.InterestMode))
- {
- ratio = -ratio;
- }
+ var ratio = DirectionRatio.InterestLegPnl(eodPayPosition.InterestDirection, position.InterestMode);
List positions = new List
{
position
@@ -1654,11 +1634,12 @@ namespace YLErp.Modules.SwapModule
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio);
//累计已实现
- newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio;
- newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee;
+ var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio);
+ newEodPayPosition.RealizedInterest = rolled.Interest;
+ newEodPayPosition.RealizedInterestFee = rolled.Fee;
SetFixedLegRealizedPnl(newEodPayPosition);
var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true,
- eodPayPosition.InterestDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell);
+ DirectionRatio.RateType(eodPayPosition.InterestDirection));
newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate);
Log.Info($"the last newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}");
PersistEodSwapPosition(newEodPayPosition);
@@ -1699,7 +1680,7 @@ namespace YLErp.Modules.SwapModule
bool open)
{
payQty = Math.Abs(payQty);
- int ratio = eventFlow.PayDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负
+ int ratio = DirectionRatio.ReceivePay(eventFlow.PayDirection);//收取为正,支付为负
int shortRatio = DirectionRatio.LongShort(newEodPayPosition.PositionType);
newEodPayPosition.ValueDate = eventFlow.PayDate.Value;
newEodPayPosition.PositionId = eventFlow.PositionId;
@@ -1747,7 +1728,7 @@ namespace YLErp.Modules.SwapModule
newEodPayPosition.TdPosiDividend = Math.Round(dividendIn * ratio, 2);
newEodPayPosition.PosiMtmPnL = MtmCalc.UnrealizedPnl(newEodPayPosition.UnderlyingPrice, newEodPayPosition.PosiGrossPrice, newEodPayPosition.PosiQuantity, newEodPayPosition.ContractSize, shortRatio, (int)ratio);
newEodPayPosition.PosiDividendSum = Math.Round(newEodPayPosition.TdPosiDividend - newEodPayPosition.TdCloseDividend, 2);
- newEodPayPosition.PosiProfitSum = newEodPayPosition.PosiMtmPnL + newEodPayPosition.PosiDividendSum + newEodPayPosition.PosiFeePending;
+ newEodPayPosition.PosiProfitSum = MtmCalc.ReturnLegProfitSum(newEodPayPosition.PosiMtmPnL, newEodPayPosition.PosiDividendSum, newEodPayPosition.PosiFeePending);
//持仓价值
newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum);
@@ -1799,7 +1780,7 @@ namespace YLErp.Modules.SwapModule
}
var dealDate = curretEod.ValueDate;
int shortRatio = DirectionRatio.LongShort(eod.PositionType);
- int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;
+ int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection);
curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0;
var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp);
curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp);
@@ -1820,7 +1801,7 @@ namespace YLErp.Modules.SwapModule
curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio);
//curretEod.TdPosiDividend = 0;
//curretEod.PosiDividendSum = eod.PosiDividendSum + curretEod.TdPosiDividend;
- curretEod.PosiProfitSum = curretEod.PosiMtmPnL + curretEod.PosiDividendSum + curretEod.PosiFeePending;
+ curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending);
curretEod.TdCloseFee = 0;
curretEod.TdCloseQty = 0;
curretEod.TdCloseMtmPnl = 0;
@@ -1831,7 +1812,7 @@ namespace YLErp.Modules.SwapModule
curretEod.RealizedFee = eod.RealizedFee + curretEod.TdCloseFee;
SetFloatingRealizedPnl(curretEod);
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value
- , seekPreday: true, currencyRateType: curretEod.PosiDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell);
+ , seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection));
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
//持仓价值
curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum);
@@ -1891,7 +1872,7 @@ namespace YLErp.Modules.SwapModule
}
var dealDate = curretEod.ValueDate;
int shortRatio = DirectionRatio.LongShort(eod.PositionType);
- int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;
+ int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection);
var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp);
var todayConsumedDividend = CalcConsumedDividend(curretEod, unwindEvents);
var originNotional = (decimal)td.OriginalStockEqvNotional / swapPosition.PosiNetPrice;
@@ -1933,13 +1914,13 @@ namespace YLErp.Modules.SwapModule
SetFloatingRealizedPnl(curretEod);
curretEod.SwapPositionValue -= curretEod.TdCloseDividend;
- curretEod.PosiProfitSum = curretEod.PosiMtmPnL + curretEod.PosiDividendSum + curretEod.PosiFeePending;
+ curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending);
if (curretEod.PosiStatus == 1)
{
curretEod.PosiNotionalValue = 0;
}
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value
- , seekPreday: true, currencyRateType: curretEod.PosiDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell);
+ , seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection));
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
//持仓价值
curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum);
@@ -1979,7 +1960,7 @@ namespace YLErp.Modules.SwapModule
return;
}
int shortRatio = DirectionRatio.LongShort(eod.PositionType);
- int directionRatio = eod.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;
+ int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection);
var unwindFlowEvents = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.平仓).ToList();
var openFlowEvents = unwindEvents.Where(x => x.EventType == (int)SwapFlowEventTypeEnum.开仓).ToList();
decimal unwindQty = unwindFlowEvents.Sum(s => s.Quantity);
@@ -2012,16 +1993,16 @@ namespace YLErp.Modules.SwapModule
{
posiQty = 0;
}
- curretEod.PosiGrossPrice = (eod.PosiGrossPrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountAvg)) / (eod.PosiQuantity + openQty);
+ curretEod.PosiGrossPrice = MtmCalc.BlendPrice(eod.PosiGrossPrice, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * a.TradingAmountAvg), eod.PosiQuantity + openQty);
curretEod.PosiGrossPrice = Math.Round(
curretEod.PosiGrossPrice,
GetStorageDeliveryPriceRound(curretEod.UnderlyingInstrumentType, curretEod.UnderlyingCode),
MidpointRounding.AwayFromZero);
- curretEod.PosiNetPrice = (eod.PosiNetPrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountFeeAvg)) / (eod.PosiQuantity + openQty);
+ curretEod.PosiNetPrice = MtmCalc.BlendPrice(eod.PosiNetPrice, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * a.TradingAmountFeeAvg), eod.PosiQuantity + openQty);
curretEod.PosiNetPrice = Math.Round(curretEod.PosiNetPrice, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
- curretEod.PosiNetNoFeePrice = (eod.PosiNetNoFeePrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountNetAvg)) / (eod.PosiQuantity + openQty);
+ curretEod.PosiNetNoFeePrice = MtmCalc.BlendPrice(eod.PosiNetNoFeePrice ?? 0m, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * (a.TradingAmountNetAvg ?? 0m)), eod.PosiQuantity + openQty);
curretEod.PosiNetNoFeePrice = Math.Round(curretEod.PosiNetNoFeePrice ?? 0, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
- curretEod.PosiNetFeePrice = (eod.PosiNetFeePrice * eod.PosiQuantity + openFlowEvents.Sum(a => a.Quantity * a.TradingAmountNetFeeAvg)) / (eod.PosiQuantity + openQty);
+ curretEod.PosiNetFeePrice = MtmCalc.BlendPrice(eod.PosiNetFeePrice ?? 0m, eod.PosiQuantity, openFlowEvents.Sum(a => a.Quantity * (a.TradingAmountNetFeeAvg ?? 0m)), eod.PosiQuantity + openQty);
curretEod.PosiNetFeePrice = Math.Round(curretEod.PosiNetFeePrice ?? 0, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
}
curretEod.PosiNotionalValue = curretEod.PosiGrossPrice * curretEod.PosiQuantity * curretEod.ContractSize;
@@ -2060,7 +2041,7 @@ namespace YLErp.Modules.SwapModule
curretEod.PositionId = position.id;
curretEod.ClientId = td.ClientId;
int shortRatio = DirectionRatio.LongShort(position.PositionType);
- int directionRatio = position.PosiDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;
+ int directionRatio = DirectionRatio.ReceivePay(position.PosiDirection);
curretEod.PositionType = position.PositionType;
var eod = new eod_swap_position()
{
@@ -2106,7 +2087,7 @@ namespace YLErp.Modules.SwapModule
curretEod.UnderlyingMarketValue = MtmCalc.MarketValue(curretEod.UnderlyingPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio);
curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio);
- curretEod.PosiProfitSum = curretEod.PosiMtmPnL + curretEod.PosiDividendSum + curretEod.PosiFeePending;
+ curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending);
curretEod.RealizedMtmPnL = curretEod.TdCloseMtmPnl;
curretEod.RealizedDividend = curretEod.TdCloseDividend;
curretEod.RealizedFee = curretEod.TdCloseFee;
@@ -2119,7 +2100,7 @@ namespace YLErp.Modules.SwapModule
//持仓价值
curretEod.SwapPositionValue = PositionValueCalc.Calc(curretEod.InterestProfitSum, curretEod.PosiProfitSum);
var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value
- , seekPreday: true, currencyRateType: curretEod.PosiDirection == (int)SwapDirectionEnum.收取 ? CurrencyRateType.Buy : CurrencyRateType.Sell);
+ , seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection));
curretEod.TdCurrency = Convert.ToDecimal(currencyRate);
UpdateDbOption(curretEod);
curretEod.Invalid = false;
@@ -2194,8 +2175,6 @@ namespace YLErp.Modules.SwapModule
var positions = eodSwapPositions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//持仓腿
// 框架合约的方向约定:多头为正、空头为负;总名义本金取交易原始规模,
// 不能直接用多空腿相加,否则会把对冲方向误当成合约规模变化。
- 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.NotionalValue = Math.Round(Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
eod_Swap.SwapTradeId = td.id;
eod_Swap.SwapTradeNo = td.TradeNumber;
@@ -2203,23 +2182,8 @@ namespace YLErp.Modules.SwapModule
eod_Swap.BookId = td.AssetId;
eod_Swap.ValueDate = settleDate;
eod_Swap.StructureType = td.StructureType;
- 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);
- decimal interestPnL = 0;
- // 利息腿按我方视角归集。保证金腿的利息现金流方向与普通利息腿相反,
- // 因此保证金腿需要额外反转符号,确保 InterestPnL 表示我方的合约利率端收益。
- interestPositions.ForEach(x =>
- {
- decimal ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负
- if (MarginModes.Contains(x.InterestMode))
- {
- ratio = -ratio;
- }
- interestPnL += x.InterestProfitSum * ratio;
- });
- eod_Swap.InterestPnL = interestPnL;
+ FillPositionLegSummary(eod_Swap, positions);
+ eod_Swap.InterestPnL = SumInterestPnL(interestPositions);
eod_Swap.PostionValue = eodSwapPositions.Sum(s => s.SwapPositionValue);
// 保证金腿的利息现金流方向与保证金本金方向相反。
// 不能直接汇总 RealizedPnl,否则“收取客户保证金”的腿会把应支付给客户的
@@ -2272,33 +2236,15 @@ 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);
- 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);
- interestPositions.ForEach(x =>
- {
- decimal ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负
- if (MarginModes.Contains(x.InterestMode))
- {
- ratio = -ratio;
- }
- eod_Swap.InterestPnL += x.InterestProfitSum * ratio;
- });
+ FillPositionLegSummary(eod_Swap, positions);
+ eod_Swap.InterestPnL += SumInterestPnL(interestPositions);
eodSwapPositions.ForEach(x =>
{
- decimal ratio = x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1;//收取为正,支付为负
- if (MarginModes.Contains(x.InterestMode))
- {
- ratio = -ratio;
- }
+ var ratio = DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode);
eod_Swap.TdRealizedPnL += x.TdCloseMtmPnl + x.TdCloseDividend + x.TdCloseFee + x.TdCloseInterest * ratio + x.TdCloseInterestFee;
});
eod_Swap.PostionValue = eodSwapPositions.Sum(s => s.SwapPositionValue);
eod_Swap.RealizedPnL = eodSwapPositions.Sum(CalculateSwapRealizedPnl);
- eod_Swap.TdCloseQty = positions.Sum(s => s.TdCloseQty);
var tradeInitMarginObj = DbContext.trade_initial_margin.FirstOrDefault(x => x.TradeId == td.id);
var initMarginList = interestPositions.Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金 && x.HappenDate == settleDate).ToList();
var addMarginList = interestPositions.Where(x => x.InterestMode == (int)InterestModeEnum.追加预付金 && x.HappenDate == settleDate).ToList();
@@ -2319,11 +2265,7 @@ namespace YLErp.Modules.SwapModule
///
public static decimal CalculateSwapRealizedPnl(eod_swap_position position)
{
- var interestRatio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;
- if (MarginModes.Contains(position.InterestMode))
- {
- interestRatio = -interestRatio;
- }
+ var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode);
return position.RealizedMtmPnL
+ position.RealizedDividend
@@ -2332,6 +2274,28 @@ namespace YLErp.Modules.SwapModule
+ 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;
+ }
+
///
/// 风险报表符号归一化:把历史两种符号口径的 TdCloseInterest/RealizedInterest
/// 统一按"绝对金额 × 业务方向"重写。普通利息腿收取为正、支付为负;
@@ -2343,15 +2307,11 @@ namespace YLErp.Modules.SwapModule
{
if (position.InterestDirection <= 0) return;
- var interestRatio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;
- if (MarginModes.Contains(position.InterestMode))
- {
- interestRatio = -interestRatio;
- }
- else if (position.InterestMode == (int)InterestModeEnum.标的期初全价)
+ 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。
@@ -2845,11 +2805,11 @@ namespace YLErp.Modules.SwapModule
var floatRateInterest = eodInterests.Where(x => !string.IsNullOrEmpty(x.FloatRateUnderlyingCode)).FirstOrDefault();
item.position.FloatRateUnderlyingCode = floatRateInterest?.FloatRateUnderlyingCode;
item.position.FloatRate = floatRateInterest?.FloatRate ?? 0;
- item.OpenMarginAmount = initialMargins.Sum(s => s.InterestPrincipalFix * (s.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1));
+ item.OpenMarginAmount = initialMargins.Sum(s => s.InterestPrincipalFix * DirectionRatio.ReceivePay(s.InterestDirection));
item.OpenMarginRate = CalculateWeightedMarginRate(tradeMargins);
- item.AdditionalMarginAmount = additionalMargins.Sum(s => s.InterestPrincipalFix * (s.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1));
+ item.AdditionalMarginAmount = additionalMargins.Sum(s => s.InterestPrincipalFix * DirectionRatio.ReceivePay(s.InterestDirection));
item.MarginInterestAmount = CalculateWeightedMarginInterest(eodMargins);
- item.InterestAmount = eodInterests.Sum(s => s.InterestIncomeSum * (s.InterestDirection == (int)SwapDirectionEnum.收取 ? -1 : 1));
+ item.InterestAmount = eodInterests.Sum(s => s.InterestIncomeSum * (-DirectionRatio.ReceivePay(s.InterestDirection)));
item.InterestRate = eodInterests.Sum(s => s.InterestRateDefault);
// 到期轧差才把期间付息/分红并入净额结算;派息日支付已在现金流层独立结算,不能重复计入估值。
var nettingDividend = (tradeExtend?.ExtendObj?.DividendPayDate ?? 1) == 0 ? pendingDividend : 0m;
@@ -2892,7 +2852,7 @@ namespace YLErp.Modules.SwapModule
public static decimal CalculateWeightedMarginInterest(IEnumerable margins)
{
return margins.Sum(x =>
- x.InterestIncomeSum * (x.InterestDirection == (int)SwapDirectionEnum.收取 ? 1 : -1));
+ x.InterestIncomeSum * DirectionRatio.ReceivePay(x.InterestDirection));
}
///