EQD-6968: 不算尾缺价改沿用已有利率(删期初回退) + 取价决策FIX常驻日志
① GetFloatRate 不算尾缺价不再回退取期初定盘,改沿用已有利率 (preEod.FloatRate/position.FloatRate)——其返回值仅作 preEod.id==0 的种子 且会被分段首段强制取价覆盖,期初取价是无消费者的多余DB读且语义误导; ② BuildSegmentRates 等价重排为 if/else-if 结构,注释改"跳过取价/沿用"准确措辞 (行为不变:排除日有价仍取新利率,缺价沿用上一重置日利率); ③ 防历史bug"取错重置日利率"复发:GetFloatRate/ResolveFloatRate/BuildSegmentRates 三处取价决策经 SwapCalcTrace.Critical 常驻 Info 落盘——重置日判定、取价日 (interest_rule回拨)、命中/缺失、不算尾缺价沿用来源、排除日跳过全程可审计; ④ 测试真内存化:Stub 补 GetConsumedInterest=>0 override(复利路径原偷连96库, 与类注释"内存,不连库"矛盾);一致性断言措辞对齐新语义; ⑤ 修复 SwapDealService.cs 文件头双 BOM 损坏。 验证:GLMS20260817Fr007UnwindMorningTest 12/12 内存全绿; 邻近套件 67过/15败与改动前基线完全一致(15败均为沙箱无96库 Connect Timeout)。
This commit is contained in:
@@ -11,6 +11,9 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 内存 StubSwapDealService 重写 TryGetFloatRate 按日期返回 FR007(缺失即返回 false → 触发取价失败)。
|
||||
/// 覆盖两条计息路径(复利 CalcDailyCompoundInterest / 单利 CalcDailySimpleInterest)共用的修复点 BuildSegmentRates,
|
||||
/// 以及全平重放分支、非整倍数边界、数值一致性("跳过取价=沿用上一重置日利率")。
|
||||
///
|
||||
/// 核心语义:算头不算尾(calcLast=false)时 endDate 当天不计息,其 FR007 利率不参与计息。
|
||||
/// 缺价时跳过取价(currentFloat 保持不变),不回退取其他日期利率,不告警。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class GLMS20260817Fr007UnwindMorningTest
|
||||
@@ -57,6 +60,9 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 内存世界无历史结息流水,consumedInterest=0(与本类"内存,不连库"声明一致;否则复利路径偷连 96 库)
|
||||
public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate) => 0m;
|
||||
}
|
||||
|
||||
private sealed class Outcome
|
||||
@@ -233,7 +239,7 @@ namespace YLErp.Modules.SwapModule
|
||||
Assert.IsFalse(worldA.Threw, "世界A 不应抛:" + worldA.Ex?.Message);
|
||||
Assert.IsFalse(worldB.Threw, "世界B 不应抛:" + worldB.Ex?.Message);
|
||||
Assert.AreEqual(worldA.Fe.InterestAmount, worldB.Fe.InterestAmount,
|
||||
"缺价回退世界 应与 显式置上一期利率世界 利息完全一致(钉死=沿用上期)");
|
||||
"缺价跳过取价世界 应与 显式置上一期利率世界 利息完全一致(该日利率不参与计息,沿用上期)");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -244,7 +250,7 @@ namespace YLErp.Modules.SwapModule
|
||||
Assert.IsFalse(worldA.Threw, "世界A 不应抛:" + worldA.Ex?.Message);
|
||||
Assert.IsFalse(worldB.Threw, "世界B 不应抛:" + worldB.Ex?.Message);
|
||||
Assert.AreEqual(worldA.Fe.InterestAmount, worldB.Fe.InterestAmount,
|
||||
"单利:缺价回退世界 应与 显式置上一期利率世界 利息完全一致");
|
||||
"单利:缺价跳过取价世界 应与 显式置上一期利率世界 利息完全一致(该日利率不参与计息)");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
||||
@@ -853,31 +853,44 @@ namespace YLErp.Modules.SwapModule
|
||||
if (string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) return position.FloatRate;
|
||||
|
||||
int days = (endDate - startDate).Days;
|
||||
bool isResetDay = days % period == 0;
|
||||
// 重置日恰为到期日(endDate)时,取价日=endDate;否则=startDate(原逻辑)。
|
||||
DateTime rateDate = IndexFixerBase.GetFixingDate(
|
||||
days % period == 0 ? endDate : startDate, position.interest_rule);
|
||||
isResetDay ? endDate : startDate, position.interest_rule);
|
||||
// 历史上有"取错重置日利率"的线上 bug,取价决策必须常驻落盘(SwapCalcTrace.Critical 无条件 Info)。
|
||||
SwapCalcTrace.Critical(
|
||||
$"FIX GetFloatRate p{position.id} [{startDate:yyyy-MM-dd}→{endDate:yyyy-MM-dd}] days={days} period={period} " +
|
||||
$"重置日={isResetDay} rule={position.interest_rule} 取价日={rateDate:yyyy-MM-dd} calcLast={calcLast} " +
|
||||
$"preEod={(preEod.id != 0 ? $"{preEod.ValueDate:yyyy-MM-dd}:{preEod.FloatRate:P6}" : "无")}");
|
||||
|
||||
if (preEod.id != 0 && days % period != 0)
|
||||
if (preEod.id != 0 && !isResetDay)
|
||||
{
|
||||
SwapCalcTrace.Critical($"FIX GetFloatRate p{position.id} 非重置日→沿用昨日终FloatRate={preEod.FloatRate:P6}");
|
||||
position.FloatRate = positionClone.FloatRate = preEod.FloatRate;
|
||||
return preEod.FloatRate;
|
||||
}
|
||||
|
||||
if (IndexFixer.TryGetFixing(rateDate, position.FloatRateUnderlyingCode, out decimal rate))
|
||||
{
|
||||
SwapCalcTrace.Critical($"FIX GetFloatRate p{position.id} 重置日→取{rateDate:yyyy-MM-dd}定盘={rate:P6}");
|
||||
position.FloatRate = positionClone.FloatRate = rate;
|
||||
return position.FloatRate;
|
||||
}
|
||||
if (!swap)
|
||||
{
|
||||
// EQD-6968:算尾(calcLast=true)时结算日 FR007 必须落表,缺失即依赖报错(既有正确行为);
|
||||
// 不算尾(calcLast=false)时结算日 FR007 不强制,缺失则回退期初定盘,避免上午未发布误拦平仓。
|
||||
if (calcLast)
|
||||
{
|
||||
SwapCalcTrace.Critical($"FIX GetFloatRate p{position.id} {rateDate:yyyy-MM-dd}缺价且算尾→抛异常拦截");
|
||||
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{rateDate:yyyy年MM月dd日}的价格");
|
||||
if (IndexFixer.TryGetFixing(IndexFixerBase.GetFixingDate(startDate, position.interest_rule), position.FloatRateUnderlyingCode, out decimal fb))
|
||||
return fb;
|
||||
return position.FloatRate;
|
||||
}
|
||||
// 算头不算尾(calcLast=false):endDate 当天不计息,其 FR007 利率不参与计息,
|
||||
// 缺价时直接沿用已有利率,不回退取其他日期利率,不告警。
|
||||
var kept = preEod.id != 0 ? preEod.FloatRate : position.FloatRate;
|
||||
SwapCalcTrace.Critical(
|
||||
$"FIX GetFloatRate p{position.id} {rateDate:yyyy-MM-dd}缺价且不算尾→沿用已有利率={kept:P6}(来源={(preEod.id != 0 ? "preEod.FloatRate" : "position.FloatRate")})");
|
||||
return kept;
|
||||
}
|
||||
SwapCalcTrace.Critical($"FIX GetFloatRate p{position.id} 互换事件(swap=true)→利率不参与,返回0");
|
||||
return 0m;
|
||||
}
|
||||
|
||||
@@ -1301,7 +1314,19 @@ namespace YLErp.Modules.SwapModule
|
||||
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;
|
||||
{
|
||||
if (fixing != 0m)
|
||||
{
|
||||
SwapCalcTrace.Critical(
|
||||
$"FIX Resolve p{position.id} {date:yyyy-MM-dd}(取价日={fixingDate:yyyy-MM-dd} rule={position.interest_rule})→定盘={fixing:P6}");
|
||||
return fixing;
|
||||
}
|
||||
SwapCalcTrace.Critical(
|
||||
$"FIX Resolve p{position.id} {date:yyyy-MM-dd}(取价日={fixingDate:yyyy-MM-dd})→定盘=0视为缺价,沿用fallback={fallback:P6}");
|
||||
return fallback;
|
||||
}
|
||||
SwapCalcTrace.Critical(
|
||||
$"FIX Resolve p{position.id} {date:yyyy-MM-dd}(取价日={fixingDate:yyyy-MM-dd} rule={position.interest_rule})→缺价,抛异常");
|
||||
throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格");
|
||||
}
|
||||
|
||||
@@ -1317,26 +1342,37 @@ namespace YLErp.Modules.SwapModule
|
||||
var rates = new List<(DateTime, decimal)>();
|
||||
var calcDays = (endDate - startDate).Days;
|
||||
decimal currentFloat = initialFloat;
|
||||
SwapCalcTrace.Critical(
|
||||
$"FIX Segments p{position.id} [{startDate:yyyy-MM-dd}→{endDate:yyyy-MM-dd}] period={interestPeriod} " +
|
||||
$"fetchAfter={(fetchAfterDate?.ToString("yyyy-MM-dd") ?? "全程")} calcLast={calcLast} " +
|
||||
$"排除起点={(relaxedFixingFromDate?.ToString("yyyy-MM-dd") ?? (calcLast ? "无" : endDate.ToString("yyyy-MM-dd")))} seed={initialFloat:P6} spread={spread:P6}");
|
||||
for (int i = 0; i <= calcDays; i += interestPeriod)
|
||||
{
|
||||
var resetDate = startDate.AddDays(i);
|
||||
bool needFetch = (fetchAfterDate == null || resetDate > fetchAfterDate.Value);
|
||||
// EQD-6968:不算尾(calcLast=false)时,从真实平仓日(relaxedFixingFromDate,缺省取 endDate)起的重置
|
||||
// 优先取新利率(满足 GLMS-JIATT-20260805 重置日=平仓日用新利率);缺失则沿用上一重置利率,
|
||||
// 不抛异常,以免上午 FR007 未发布误拦平仓。算尾(calcLast=true)时尾日定盘照常强制。
|
||||
bool relaxEndFixing = !calcLast && resetDate >= (relaxedFixingFromDate ?? endDate);
|
||||
if (needFetch)
|
||||
// 算头不算尾(calcLast=false)时,endDate 当天不计息,其重置日利率不参与计息。
|
||||
// 从 relaxedFixingFromDate(缺省取 endDate)起的重置日:有价则取(满足重置日=平仓日用新利率),
|
||||
// 缺价则跳过取价——currentFloat 保持不变(沿用上一重置日利率),不回退、不告警、不抛异常。
|
||||
// 算尾(calcLast=true)时所有重置日定盘照常强制取价。
|
||||
bool isExcludedEnd = !calcLast && resetDate >= (relaxedFixingFromDate ?? endDate);
|
||||
if (needFetch && !isExcludedEnd)
|
||||
{
|
||||
if (!relaxEndFixing)
|
||||
currentFloat = ResolveFloatRate(position, resetDate, currentFloat);
|
||||
}
|
||||
else if (needFetch && isExcludedEnd)
|
||||
{
|
||||
// 有价则取新利率(重置日=平仓日时仍优先使用新利率);无价则跳过——该日利率不参与计息
|
||||
var fixingDate = IndexFixerBase.GetFixingDate(resetDate, position.interest_rule);
|
||||
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal r))
|
||||
{
|
||||
currentFloat = ResolveFloatRate(position, resetDate, currentFloat);
|
||||
currentFloat = r;
|
||||
SwapCalcTrace.Critical(
|
||||
$"FIX Segment p{position.id} {resetDate:yyyy-MM-dd} 排除日有价→取新定盘(取价日={fixingDate:yyyy-MM-dd})={r:P6}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var fixingDate = IndexFixerBase.GetFixingDate(resetDate, position.interest_rule);
|
||||
if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal r))
|
||||
currentFloat = r;
|
||||
// 否则沿用 currentFloat(上一重置日利率)
|
||||
SwapCalcTrace.Critical(
|
||||
$"FIX Segment p{position.id} {resetDate:yyyy-MM-dd} 排除日缺价→跳过取价,沿用={currentFloat:P6}(该日不计息)");
|
||||
}
|
||||
}
|
||||
rates.Add((resetDate, spread + currentFloat));
|
||||
@@ -1363,8 +1399,8 @@ namespace YLErp.Modules.SwapModule
|
||||
int interestPeriod = position.interest_rest_days ?? 1;
|
||||
|
||||
// 分段取率:复利全程重放,每个重置日(含 startDate)取 FR007(fetchAfterDate=null)。
|
||||
// EQD-6968:calcLast 透传,与下方 AccruePeriod 的边界同源,避免两处漂移;
|
||||
// relaxedFixingFromDate 仅重放补计不算尾漏计利息时非 null(=真实平仓日),锚定尾日定盘放松点。
|
||||
// calcLast 透传,与下方 AccruePeriod 的边界同源,避免两处漂移;
|
||||
// relaxedFixingFromDate 仅重放补计不算尾漏计利息时非 null(=真实平仓日),锚定尾日跳过取价起点。
|
||||
var (segmentRates, currentFloat) = BuildSegmentRates(
|
||||
startDate, endDate, interestPeriod, position, flowEvent.InterestRate, floateRate,
|
||||
fetchAfterDate: null, calcLast: calcLast, relaxedFixingFromDate: relaxedFixingFromDate);
|
||||
@@ -1407,7 +1443,7 @@ namespace YLErp.Modules.SwapModule
|
||||
var accrualBasis = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv;
|
||||
|
||||
// 分段取率:仅 ValueDate 之后的重置日才取 FR007(fetchAfterDate=ValueDate)。
|
||||
// EQD-6968:calcLast 透传,与下方 AccruePeriod 的边界同源,避免两处漂移。
|
||||
// calcLast 透传,与下方 AccruePeriod 的边界同源,避免两处漂移。
|
||||
var (segmentRates, currentFloat) = BuildSegmentRates(
|
||||
startDate, endDate, interestPeriod, position, flowEvent.InterestRate, floateRate,
|
||||
fetchAfterDate: preEodPosition.ValueDate, calcLast: calcLast, relaxedFixingFromDate: relaxedFixingFromDate);
|
||||
|
||||
Reference in New Issue
Block a user