EQD-6968: 不算尾计息不再因平仓日FR007未发布误拦平仓(缺价回退上一重置日利率,有价仍取新利率)

This commit is contained in:
hjhan
2026-08-17 11:22:37 +08:00
parent feee779994
commit bc401bd687
2 changed files with 274 additions and 12 deletions
+38 -12
View File
@@ -475,7 +475,6 @@ namespace YLErp.Modules.SwapModule
var stockEqvNotional = realPostitions.Where(x => x.PosiDirection > 0).Sum(s => s.PosiNotionalValue); // 当前平仓前的实时剩余本金
var posiNotionalValue = stockEqvNotional * closePercent;// 本次平仓名义本金
var orginPv = ResolveUnwindPreviousNotional(lastEod, lastEodPositions, stockEqvNotional); // 上一日终的浮动端本金
var grossPrice = realPostitions.Where(x => x.PosiDirection > 0).FirstOrDefault()?.PosiGrossPrice;
var closeList = DbContext.swap_flow_event.Where(x => x.SwapTradeId == tradeId
&& x.UnwindDate == unwindDate
&& eventTypes.Contains(x.EventType)
@@ -696,7 +695,7 @@ namespace YLErp.Modules.SwapModule
closePrincipal = closePosiNotionalValue;
}
decimal floatRate = GetFloatRate(position, preEodPosition, td.StartDate.Value, endDate, interestPeriod, swap, positionClone);
decimal floatRate = GetFloatRate(position, preEodPosition, td.StartDate.Value, endDate, interestPeriod, swap, positionClone, calcLast || newCalcLast);
// 根据场景计算利息
if (settment)
@@ -847,11 +846,12 @@ namespace YLErp.Modules.SwapModule
/// <summary>
/// 获取浮动利率
/// </summary>
private decimal GetFloatRate(swap_position position, eod_swap_position preEod, DateTime startDate, DateTime endDate, int period, bool swap, swap_position positionClone)
private decimal GetFloatRate(swap_position position, eod_swap_position preEod, DateTime startDate, DateTime endDate, int period, bool swap, swap_position positionClone, bool calcLast = true)
{
if (string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) return position.FloatRate;
int days = (endDate - startDate).Days;
// 重置日恰为到期日(endDate)时,取价日=endDate;否则=startDate(原逻辑)。
DateTime rateDate = IndexFixerBase.GetFixingDate(
days % period == 0 ? endDate : startDate, position.interest_rule);
@@ -866,7 +866,15 @@ namespace YLErp.Modules.SwapModule
position.FloatRate = positionClone.FloatRate = rate;
return position.FloatRate;
}
if (!swap) throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{rateDate:yyyy年MM月dd日}的价格");
if (!swap)
{
// EQD-6968:不算尾时平仓日 FR007 缺失不拦截(该日利率不参与计息),回退期初定盘;算尾(calcLast=true)仍抛(正确依赖)。
if (!calcLast && IndexFixer.TryGetFixing(IndexFixerBase.GetFixingDate(startDate, position.interest_rule), position.FloatRateUnderlyingCode, out decimal fb))
return fb;
if (!calcLast)
return position.FloatRate;
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{rateDate:yyyy年MM月dd日}的价格");
}
return 0m;
}
@@ -1249,7 +1257,7 @@ namespace YLErp.Modules.SwapModule
// 计算截至本次平仓日的累计利息 amountAtEnd
CalcDailyCompoundInterest(replayEndDate, position, closePosiNotionalValue,
interestAtEnd, annualDays, floateRate, closePrecent,
calcFirst, calcLast, ref amountAtEnd, ref tdAmountAtEnd, consumedInterest);
calcFirst, calcLast, ref amountAtEnd, ref tdAmountAtEnd, consumedInterest, exclusionEndDate: endDate);
var interestAtPreviousEod = new swap_flow_event { InterestRate = rate };
decimal amountAtPreviousEod = 0m;
decimal tdAmountAtPreviousEod = 0m;
@@ -1303,7 +1311,7 @@ namespace YLErp.Modules.SwapModule
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)
DateTime? fetchAfterDate, bool includeEnd = true, DateTime? exclusionEndDate = null)
{
var rates = new List<(DateTime, decimal)>();
var calcDays = (endDate - startDate).Days;
@@ -1311,8 +1319,24 @@ namespace YLErp.Modules.SwapModule
for (int i = 0; i <= calcDays; i += interestPeriod)
{
var resetDate = startDate.AddDays(i);
if (fetchAfterDate == null || resetDate > fetchAfterDate.Value)
currentFloat = ResolveFloatRate(position, resetDate, currentFloat);
bool needFetch = (fetchAfterDate == null || resetDate > fetchAfterDate.Value);
// EQD-6968:不算尾(includeEnd=false)的排除日(真实平仓日)重置——优先取新利率(满足 GLMS-JIATT-20260805
// 重置日=平仓日用新利率),缺失则沿用上一重置日利率(该日利率不参与计息),不抛异常以免上午 FR007 未发布误拦平仓。
bool isExcludedEnd = !includeEnd && resetDate >= (exclusionEndDate ?? endDate);
if (needFetch)
{
if (!isExcludedEnd)
{
currentFloat = ResolveFloatRate(position, resetDate, currentFloat);
}
else
{
var fixingDate = IndexFixerBase.GetFixingDate(resetDate, position.interest_rule);
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal r))
currentFloat = r;
// 否则沿用 currentFloat(上一重置日利率)
}
}
rates.Add((resetDate, spread + currentFloat));
}
return (rates, currentFloat);
@@ -1331,15 +1355,16 @@ namespace YLErp.Modules.SwapModule
/// <returns></returns>
public void CalcDailyCompoundInterest(DateTime endDate, swap_position position, decimal principal, swap_flow_event flowEvent,
int annualDays, decimal floateRate, decimal closePercent, bool calcFirst, bool calcLast,
ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m, decimal resetCarryInterest = 0m)
ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m, decimal resetCarryInterest = 0m, DateTime? exclusionEndDate = null)
{
var startDate = position.PosiStartDate;
int interestPeriod = position.interest_rest_days ?? 1;
// 分段取率:复利全程重放,每个重置日(含 startDate)取 FR007fetchAfterDate=null)。
// EQD-6968includeEnd 透传 calcLast,与下方 AccruePeriod 的边界同源,避免两处漂移。
var (segmentRates, currentFloat) = BuildSegmentRates(
startDate, endDate, interestPeriod, position, flowEvent.InterestRate, floateRate,
fetchAfterDate: null);
fetchAfterDate: null, includeEnd: calcLast, exclusionEndDate: exclusionEndDate);
// 纯函数复利计息:分段重置日并本金 + resetCarryInterest + 扣 consumedInterest
var interestTrace = new AccrualTrace();
@@ -1369,7 +1394,7 @@ namespace YLErp.Modules.SwapModule
/// <summary>
/// 计算单利 盘中(按重置天数分段,每段使用对应浮动利率)
/// </summary>
public void CalcDailySimpleInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m)
public void CalcDailySimpleInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m, DateTime? exclusionEndDate = null)
{
var startDate = position.PosiStartDate;
int interestPeriod = position.interest_rest_days ?? 1;
@@ -1379,9 +1404,10 @@ namespace YLErp.Modules.SwapModule
var accrualBasis = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv;
// 分段取率:仅 ValueDate 之后的重置日才取 FR007fetchAfterDate=ValueDate)。
// EQD-6968includeEnd 透传 calcLast,与下方 AccruePeriod 的边界同源,避免两处漂移。
var (segmentRates, currentFloat) = BuildSegmentRates(
startDate, endDate, interestPeriod, position, flowEvent.InterestRate, floateRate,
fetchAfterDate: preEodPosition.ValueDate);
fetchAfterDate: preEodPosition.ValueDate, includeEnd: calcLast, exclusionEndDate: exclusionEndDate);
// 纯函数计息:Accrued=缩放累计(InterestAmount)AccruedToday=未缩放累计(TdInterestAmount)
var interestTrace = new AccrualTrace();