refactor(accrual): 接线SimpleInterest/CompoundInterest内部 + 分段修复 + 去重
CalcDailySimpleInterest/CalcDailyCompoundInterest内部逐日循环→分段纯函数(签名不变): - AccrueSimplePeriod: AccruedToday改未缩放累计(TdInterestAmount口径) - AccrueCompoundPeriod: 新增out finalBasis供flowEvent.InterestPrincipal精确赋值 - 修复5处分段边界bug(includeStart/includeEnd/resetCarryInterest interestPeriod=1场景) - 提取BuildSegmentRates消除SimpleInterest/CompoundInterest取率重复 - 影子测试补AccruedToday断言关闭测试盲区 SwapModule零回归(7基线/510通过)
This commit is contained in:
@@ -93,11 +93,16 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
endDate: EndDate,
|
||||
boundary: AccrualBoundary.StartOnly,
|
||||
annualDays: AnnualDays,
|
||||
isAnnualized: true);
|
||||
isAnnualized: true,
|
||||
resetCarryInterest: 0m,
|
||||
realizedInterest: 0m,
|
||||
unwindFraction: 1m,
|
||||
finalBasis: out _);
|
||||
|
||||
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
|
||||
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
|
||||
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
|
||||
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -138,11 +143,13 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
isAnnualized: true,
|
||||
resetCarryInterest: carry,
|
||||
realizedInterest: consumed,
|
||||
unwindFraction: closePct);
|
||||
unwindFraction: closePct,
|
||||
finalBasis: out _);
|
||||
|
||||
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
|
||||
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
|
||||
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
|
||||
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -177,11 +184,16 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
endDate: EndDate,
|
||||
boundary: AccrualBoundary.Both,
|
||||
annualDays: AnnualDays,
|
||||
isAnnualized: true);
|
||||
isAnnualized: true,
|
||||
resetCarryInterest: 0m,
|
||||
realizedInterest: 0m,
|
||||
unwindFraction: 1m,
|
||||
finalBasis: out _);
|
||||
|
||||
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
|
||||
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
|
||||
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
|
||||
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,27 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
public StubSvc() : base(new OptUserInfo(0, nameof(SimplePeriodShadowTest), OptUserFrom.UnitTest)) { }
|
||||
}
|
||||
|
||||
/// <summary>带浮动率 stub 的 SwapDealService:override IndexFixer 注入预设 FR007 取价。</summary>
|
||||
private sealed class FloatStubSvc : SwapDealService
|
||||
{
|
||||
private readonly IIndexFixer _fixer;
|
||||
public FloatStubSvc(IIndexFixer fixer) : base(new OptUserInfo(0, nameof(SimplePeriodShadowTest), OptUserFrom.UnitTest))
|
||||
=> _fixer = fixer;
|
||||
protected override IIndexFixer IndexFixer => _fixer;
|
||||
}
|
||||
|
||||
/// <summary>Stub IIndexFixer:对所有查询返回固定 fixing(不依赖日期匹配,规避 QDP 日历差异)。</summary>
|
||||
private sealed class StubIndexFixer : IIndexFixer
|
||||
{
|
||||
private readonly decimal _rate;
|
||||
public StubIndexFixer(decimal rate) => _rate = rate;
|
||||
public bool TryGetFixing(DateTime fixingDate, string underlyingCode, out decimal rate)
|
||||
{
|
||||
rate = _rate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 固定利率(无FR007)算头不算尾,全平,无历史归档。
|
||||
/// </summary>
|
||||
@@ -100,6 +121,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
|
||||
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
|
||||
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
|
||||
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -145,6 +167,70 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
|
||||
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
|
||||
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
|
||||
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单利 + FR007 浮动利率 + 多重置日 + 部分平仓 + 有历史归档。
|
||||
/// 验证旧方法内部取价循环生成的 segmentRates 与手算一致——为抽取共享 SegmentRateBuilder 做安全网。
|
||||
/// 场景:preEod.ValueDate=4/30,重置日 4/21(跳过取价), 4/28(跳过取价), 5/5(取 FR007 fixing)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void 影子_单利浮动_FR007_部分平仓_旧新一致()
|
||||
{
|
||||
const decimal floatRateIn = 0.0150m; // 入参 floateRate(上一次取到的浮动率 1.50%)
|
||||
const decimal fixingAtReset = 0.0125m; // 5/5 重置日取到的 FR007 fixing(1.25%)
|
||||
const decimal closePct = 0.5m;
|
||||
var preEodDate = new DateTime(2026, 4, 30); // 上一日终=4/30,5/5 > 4/30 触发取价
|
||||
|
||||
var position = CreatePosition();
|
||||
position.FloatRateUnderlyingCode = "FR007";
|
||||
|
||||
var preEod = new eod_swap_position
|
||||
{
|
||||
id = 1, SwapTradeId = 1, PositionId = 1001,
|
||||
ValueDate = preEodDate,
|
||||
TdInterestPrincipal = Notional,
|
||||
InterestProfitSum = 200_000m,
|
||||
PosiNotionalValue = Notional, FloatRate = 0m
|
||||
};
|
||||
var flowEvent = new swap_flow_event { InterestRate = Spread };
|
||||
|
||||
// 旧方法(通过 stub IndexFixer 注入 FR007 取价)
|
||||
decimal oldI = 0, oldTd = 0;
|
||||
var svc = new FloatStubSvc(new StubIndexFixer(fixingAtReset));
|
||||
svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent,
|
||||
AnnualDays, false, floatRateIn, closePct, Notional, true, false, ref oldI, ref oldTd);
|
||||
|
||||
// 新方法:手算 segmentRates(对齐旧代码取价循环的逻辑)
|
||||
// 4/21 <= preEodDate(4/30) → 跳过取价,currentFloat 保持入参 floatRateIn
|
||||
// 4/28 <= preEodDate(4/30) → 跳过取价,currentFloat 保持入参 floatRateIn
|
||||
// 5/5 > preEodDate(4/30) → 取价,currentFloat 更新为 fixingAtReset
|
||||
var segRates = new List<(DateTime, decimal)>
|
||||
{
|
||||
(StartDate, Spread + floatRateIn), // (4/21, 0.0175)
|
||||
(StartDate.AddDays(7), Spread + floatRateIn), // (4/28, 0.0175)
|
||||
(StartDate.AddDays(14), Spread + fixingAtReset), // (5/5, 0.0150)
|
||||
};
|
||||
|
||||
// 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv
|
||||
var accrualPrincipal = Notional + Notional - Notional;
|
||||
var result = FundingLegAccrual.AccrueSimplePeriod(
|
||||
priorAccrued: 200_000m * closePct,
|
||||
notional: accrualPrincipal,
|
||||
unwindFraction: closePct,
|
||||
segmentRates: segRates,
|
||||
startDate: StartDate,
|
||||
endDate: EndDate,
|
||||
priorValueDate: preEodDate,
|
||||
boundary: AccrualBoundary.StartOnly,
|
||||
annualDays: AnnualDays,
|
||||
isAnnualized: true);
|
||||
|
||||
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
|
||||
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
|
||||
Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致");
|
||||
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ public static class FundingLegAccrual
|
||||
/// <summary>
|
||||
/// 单利多日计息(替换 CalcDailySimpleInterest 的纯数学部分)。
|
||||
/// 本金全程恒定,按重置日分段取利率。
|
||||
/// Accrued = 缩放累计(InterestAmount),AccruedToday = 未缩放累计(TdInterestAmount)。
|
||||
/// </summary>
|
||||
public static InterestResult AccrueSimplePeriod(
|
||||
decimal priorAccrued,
|
||||
@@ -119,12 +120,12 @@ public static class FundingLegAccrual
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
var displayBasis = notional * unwindFraction;
|
||||
decimal accrued = priorAccrued;
|
||||
decimal accrued = priorAccrued; // 缩放累计 → InterestAmount
|
||||
decimal accruedUnscaled = priorAccrued; // 未缩放累计 → TdInterestAmount
|
||||
|
||||
trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
|
||||
|
||||
var segStart = startDate;
|
||||
var segIncludeStart = boundary.IncludeStart;
|
||||
|
||||
for (int si = 0; si < segmentRates.Count; si++)
|
||||
{
|
||||
@@ -135,22 +136,29 @@ public static class FundingLegAccrual
|
||||
var effectiveStart = segStart > priorValueDate ? segStart : priorValueDate.AddDays(1);
|
||||
if (effectiveStart > segEnd) { segStart = segEnd; continue; }
|
||||
|
||||
var segBoundary = AccrualBoundary.Of(segIncludeStart, segEnd == endDate && boundary.IncludeEnd);
|
||||
// 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; segIncludeStart = false; continue; }
|
||||
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;
|
||||
segIncludeStart = false;
|
||||
}
|
||||
|
||||
var result = new InterestResult(
|
||||
SwapInterest.Round(accrued, Precision),
|
||||
SwapInterest.Round(accrued, Precision));
|
||||
SwapInterest.Round(accruedUnscaled, Precision));
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
@@ -167,9 +175,10 @@ public static class FundingLegAccrual
|
||||
AccrualBoundary boundary,
|
||||
int annualDays,
|
||||
bool isAnnualized,
|
||||
decimal resetCarryInterest = 0m,
|
||||
decimal realizedInterest = 0m,
|
||||
decimal unwindFraction = 1m,
|
||||
decimal resetCarryInterest,
|
||||
decimal realizedInterest,
|
||||
decimal unwindFraction,
|
||||
out decimal finalBasis,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
decimal accrualBasis = notional;
|
||||
@@ -179,16 +188,19 @@ public static class FundingLegAccrual
|
||||
|
||||
for (int si = 0; si < segmentRates.Count; si++)
|
||||
{
|
||||
var segEnd = si < segmentRates.Count - 1
|
||||
? segmentRates[si + 1].StartDate
|
||||
: endDate;
|
||||
var isLastSegment = si == segmentRates.Count - 1;
|
||||
var segEnd = isLastSegment
|
||||
? endDate
|
||||
: segmentRates[si + 1].StartDate;
|
||||
|
||||
// 重置日并本金
|
||||
accrualBasis = si == 0 ? notional : notional + accrued;
|
||||
|
||||
// 末日重置且 carry 非零:用存量替代
|
||||
// 末日恰好是重置日且 carry 非零:用存量替代(旧代码 i%interestPeriod==0 && accrueDate==endDate)。
|
||||
// 注意:必须同时判断 startDate==endDate——endDate 非重置日时最后一段起点 < endDate,不应触发。
|
||||
var usedCarry = false;
|
||||
if (segEnd == endDate && si > 0 && resetCarryInterest != 0m)
|
||||
if (isLastSegment && si > 0 && resetCarryInterest != 0m
|
||||
&& segmentRates[si].StartDate == endDate)
|
||||
{
|
||||
accrualBasis = notional + resetCarryInterest;
|
||||
usedCarry = true;
|
||||
@@ -200,7 +212,7 @@ public static class FundingLegAccrual
|
||||
|
||||
// 半开区间:重置日归下一段(旧代码逐日循环中重置日先更新本金再算息)
|
||||
var segIncludeStart = (si == 0) ? boundary.IncludeStart : true;
|
||||
var segIncludeEnd = (segEnd == endDate) ? boundary.IncludeEnd : false;
|
||||
var segIncludeEnd = isLastSegment ? boundary.IncludeEnd : false;
|
||||
var days = SwapInterest.AccrualDays(segmentRates[si].StartDate, segEnd,
|
||||
AccrualBoundary.Of(segIncludeStart, segIncludeEnd));
|
||||
if (days <= 0) continue;
|
||||
@@ -211,6 +223,8 @@ public static class FundingLegAccrual
|
||||
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);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using MoreLinq.Extensions;
|
||||
using MoreLinq.Extensions;
|
||||
using Newtonsoft.Json;
|
||||
using YLErp.BLL;
|
||||
using YLErp.BLL.Eod;
|
||||
@@ -1325,6 +1325,40 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
return interest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按重置周期切分利率段,每段记录 all-in 利率(spread+fixing)。返回 (分段列表, 末段浮动利率)。
|
||||
/// fetchAfterDate: 仅该日期之后的重置日才取 FR007(单利传 ValueDate,复利传 null 全程取)。
|
||||
/// </summary>
|
||||
private (List<(DateTime StartDate, decimal Rate)> Segments, decimal LastFloat) BuildSegmentRates(
|
||||
DateTime startDate, DateTime endDate, int interestPeriod,
|
||||
swap_position position, decimal spread, decimal initialFloat,
|
||||
DateTime? fetchAfterDate)
|
||||
{
|
||||
var rates = new List<(DateTime, decimal)>();
|
||||
var calcDays = (endDate - startDate).Days;
|
||||
decimal currentFloat = initialFloat;
|
||||
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日}的价格");
|
||||
}
|
||||
}
|
||||
rates.Add((resetDate, spread + currentFloat));
|
||||
}
|
||||
return (rates, currentFloat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算复利 盘中
|
||||
/// </summary>
|
||||
@@ -1341,85 +1375,36 @@ namespace YLErp.Modules.SwapModule
|
||||
ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m, decimal resetCarryInterest = 0m)
|
||||
{
|
||||
var startDate = position.PosiStartDate;
|
||||
decimal interestProfitSum = 0;
|
||||
decimal TdInterestPrincipal = 0;
|
||||
decimal interest = interestProfitSum ;
|
||||
decimal tdinterest = interestProfitSum ;
|
||||
int interestPeriod = position.interest_rest_days ?? 1;
|
||||
// 复利:只能用要平仓的名义本金从头开始算
|
||||
decimal dynomicPrincipal = principal;
|
||||
decimal tdDynomicPrincipal = dynomicPrincipal;
|
||||
var calcDays = (endDate - startDate).Days;
|
||||
double floatRate = Convert.ToDouble(floateRate);
|
||||
for (int i = 0; i <= calcDays; i++)
|
||||
{
|
||||
var accrueDate = startDate.AddDays(i);
|
||||
// 重置日取价必须在 calcFirst/calcLast 跳过之前完成:calcLast=false(不算尾) 只应跳过计息,
|
||||
// 不应跳过重置日的 FR007 取价。否则平仓日=重置日时会沿用旧周期利率,
|
||||
// 且 flowEvent.FloatRate 落库为旧值,传染后续 EOD(GLMS-JIATT-20260805 根因)。
|
||||
if (accrueDate >= startDate && i % interestPeriod == 0
|
||||
&& !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
|
||||
{
|
||||
var fixingDate = IndexFixerBase.GetFixingDate(accrueDate, position.interest_rule);
|
||||
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
|
||||
{
|
||||
if (fixing != 0m) floatRate = Convert.ToDouble(fixing);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
|
||||
}
|
||||
}
|
||||
if (accrueDate >= startDate)
|
||||
{
|
||||
if (i % interestPeriod == 0)
|
||||
{
|
||||
// 每个重置节点 计息基数 = 前日本金 + 本期利息
|
||||
// resetCarryInterest 是上一日终待实现按本次平仓比例分摊后的存量,
|
||||
// 只能在 endDate 恰好是当前复利重置日时并入本金。历史重置点必须使用
|
||||
// 重放到当时的 interest,否则会把上一日终存量反复注入历史本金,
|
||||
// 例如 0007 的 5/11 部分平仓会由 84,090.95 被多算为 84,114.88。
|
||||
var interestToReset = i > 0 && accrueDate == endDate && resetCarryInterest != 0m
|
||||
? resetCarryInterest
|
||||
: interest;
|
||||
dynomicPrincipal = principal + interestToReset;
|
||||
tdDynomicPrincipal = principal + interestToReset;
|
||||
flowEvent.InterestPrincipal = tdDynomicPrincipal;
|
||||
TdInterestPrincipal = tdDynomicPrincipal;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 复利非重置日:利息不并入本金,不用closePercent缩放(principal已反映平仓比例)
|
||||
flowEvent.InterestPrincipal = tdDynomicPrincipal;
|
||||
TdInterestPrincipal = tdDynomicPrincipal;
|
||||
}
|
||||
}
|
||||
if (!calcFirst && accrueDate == startDate) continue; // 首日不算头
|
||||
if (!calcLast && accrueDate == endDate) continue; // 到期日不算尾(只跳过计息,重置本金已在上方完成)
|
||||
if (accrueDate >= startDate)
|
||||
{
|
||||
flowEvent.FloatRate = Convert.ToDecimal(floatRate);
|
||||
var interest1 = flowEvent.InterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
|
||||
var tdinterest1 = TdInterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
|
||||
if (position.IsAnnualized)
|
||||
{
|
||||
interest1 /= annualDays;
|
||||
tdinterest1 /= annualDays;
|
||||
}
|
||||
interest += interest1;
|
||||
tdinterest += tdinterest1;
|
||||
}
|
||||
}
|
||||
// 兜底:若循环因 calcLast 跳过最后一天(重置日=平仓日),flowEvent.FloatRate 不会被循环内赋值,
|
||||
// 用最终 floatRate 兜底,确保落库的 FloatRate 反映最后一个重置日的利率(GLMS-JIATT-20260805)。
|
||||
flowEvent.FloatRate = Convert.ToDecimal(floatRate);
|
||||
// 复利从头重放得到的是"假设从未结出"的整段总利息,需扣除历史已通过互换结出的利息,
|
||||
// 否则已结部分会重复计息(类比分红 PosiDividendSum = totalToDate − RealizedDividend)。
|
||||
// consumedInterest is full-position absolute interest; scale it to this close portion.
|
||||
interest -= consumedInterest * closePercent;
|
||||
tdinterest -= consumedInterest * closePercent;
|
||||
InterestAmount = Math.Round(interest, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
|
||||
TdInterestAmount = Math.Round(tdinterest, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
|
||||
|
||||
// 分段取率:复利全程重放,每个重置日(含 startDate)取 FR007(fetchAfterDate=null)。
|
||||
var (segmentRates, currentFloat) = BuildSegmentRates(
|
||||
startDate, endDate, interestPeriod, position, flowEvent.InterestRate, floateRate,
|
||||
fetchAfterDate: null);
|
||||
|
||||
// 纯函数复利计息:分段重置日并本金 + resetCarryInterest + 扣 consumedInterest
|
||||
var interestTrace = new AccrualTrace();
|
||||
var result = FundingLegAccrual.AccrueCompoundPeriod(
|
||||
notional: principal,
|
||||
segmentRates: segmentRates,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
boundary: AccrualBoundary.Of(calcFirst, calcLast),
|
||||
annualDays: annualDays,
|
||||
isAnnualized: position.IsAnnualized,
|
||||
resetCarryInterest: resetCarryInterest,
|
||||
realizedInterest: consumedInterest,
|
||||
unwindFraction: closePercent,
|
||||
finalBasis: out var finalBasis,
|
||||
trace: interestTrace);
|
||||
SwapCalcTrace.Write(interestTrace);
|
||||
|
||||
// flowEvent 副作用:FloatRate=末段浮动利率;InterestPrincipal=复利终期本金(最后一次并本金后的基数)。
|
||||
flowEvent.FloatRate = currentFloat;
|
||||
flowEvent.InterestPrincipal = finalBasis;
|
||||
|
||||
InterestAmount = result.Accrued;
|
||||
TdInterestAmount = result.AccruedToday;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1428,59 +1413,39 @@ namespace YLErp.Modules.SwapModule
|
||||
public void CalcDailySimpleInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m)
|
||||
{
|
||||
var startDate = position.PosiStartDate;
|
||||
decimal interestProfitSum = preEodPosition.InterestProfitSum;
|
||||
var TdInterestPrincipal = preEodPosition.TdInterestPrincipal;
|
||||
decimal interest = interestProfitSum * closePercent;
|
||||
decimal tdinterest = interestProfitSum * closePercent;
|
||||
int interestPeriod = position.interest_rest_days ?? 1;
|
||||
// 单利:可用上一日计息基数
|
||||
decimal dynomicPrincipal = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv;
|
||||
decimal tdDynomicPrincipal = dynomicPrincipal;
|
||||
var calcDays = (endDate - startDate).Days;
|
||||
double floatRate = Convert.ToDouble(floateRate);
|
||||
for (int i = 0; i <= calcDays; i++)
|
||||
{
|
||||
var accrueDate = startDate.AddDays(i);
|
||||
if (!calcFirst && accrueDate == startDate) continue; // 首日不算头
|
||||
if (!calcLast && accrueDate == endDate) continue; // 到期日不算尾
|
||||
if (accrueDate > preEodPosition.ValueDate)
|
||||
{
|
||||
// 重置日重新获取该段浮动利率;非重置日沿用上一段利率。
|
||||
// 两分支唯一差异即"是否重取利率",本金口径(只缩放一次)完全一致,
|
||||
// 合并后消除复制粘贴导致的 closePercent^N 类 bug(原非重置日分支多了一行
|
||||
// tdDynomicPrincipal = flowEvent.InterestPrincipal 使本金累积乘 closePercent^N)。
|
||||
if (i % interestPeriod == 0 && !string.IsNullOrEmpty(position.FloatRateUnderlyingCode))
|
||||
{
|
||||
var fixingDate = IndexFixerBase.GetFixingDate(accrueDate, position.interest_rule);
|
||||
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing))
|
||||
{
|
||||
if (fixing != 0m) floatRate = Convert.ToDouble(fixing);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
|
||||
}
|
||||
}
|
||||
|
||||
// 显示本金 = 计息基数 × closePercent(只缩放一次,与日终 ByEod 口径一致);
|
||||
// 计息基数(tdDynomicPrincipal)逐日恒定、不缩放(单利特征)。
|
||||
flowEvent.InterestPrincipal = tdDynomicPrincipal * closePercent;
|
||||
TdInterestPrincipal = tdDynomicPrincipal;
|
||||
// orginPv 是路径相关参考本金(资金腿=上一日终浮动端名义本金;保证金腿=上一日终保证金余额)。
|
||||
// 单利差分:accrualBasis 全程恒定 = 昨日终滚动基数 + 当日名义本金 - 参考本金。
|
||||
var accrualBasis = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv;
|
||||
|
||||
flowEvent.FloatRate = Convert.ToDecimal(floatRate);
|
||||
var interest1 = flowEvent.InterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
|
||||
var tdinterest1 = TdInterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate));
|
||||
if (position.IsAnnualized)
|
||||
{
|
||||
interest1 /= annualDays;
|
||||
tdinterest1 /= annualDays;
|
||||
}
|
||||
interest += interest1;
|
||||
tdinterest += tdinterest1;
|
||||
}
|
||||
}
|
||||
InterestAmount = Math.Round(interest, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
|
||||
TdInterestAmount = Math.Round(tdinterest, InterestCalculationPrecision, MidpointRounding.AwayFromZero);
|
||||
// 分段取率:仅 ValueDate 之后的重置日才取 FR007(fetchAfterDate=ValueDate)。
|
||||
var (segmentRates, currentFloat) = BuildSegmentRates(
|
||||
startDate, endDate, interestPeriod, position, flowEvent.InterestRate, floateRate,
|
||||
fetchAfterDate: preEodPosition.ValueDate);
|
||||
|
||||
// 纯函数计息:Accrued=缩放累计(InterestAmount),AccruedToday=未缩放累计(TdInterestAmount)
|
||||
var interestTrace = new AccrualTrace();
|
||||
var result = FundingLegAccrual.AccrueSimplePeriod(
|
||||
priorAccrued: preEodPosition.InterestProfitSum * closePercent,
|
||||
notional: accrualBasis,
|
||||
unwindFraction: closePercent,
|
||||
segmentRates: segmentRates,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
priorValueDate: preEodPosition.ValueDate,
|
||||
boundary: AccrualBoundary.Of(calcFirst, calcLast),
|
||||
annualDays: annualDays,
|
||||
isAnnualized: position.IsAnnualized,
|
||||
trace: interestTrace);
|
||||
SwapCalcTrace.Write(interestTrace);
|
||||
|
||||
// flowEvent 副作用(下游 EOD 用 InterestPrincipal 播种次日 TdInterestPrincipal)
|
||||
flowEvent.InterestPrincipal = accrualBasis * closePercent;
|
||||
flowEvent.FloatRate = currentFloat;
|
||||
|
||||
InterestAmount = result.Accrued;
|
||||
TdInterestAmount = result.AccruedToday;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user