fix(swap): 复利重置日=平仓日时 calcLast 不再跳过 FR007 取价

根因(GLMS-JIATT-20260805):InterestCalcMode='10'(算头不算尾,calcLast=false)时,
CalcDailyCompoundInterest 循环的 'if(!calcLast && accrueDate==endDate) continue'
会跳过平仓日。若平仓日恰好是重置日(i%period==0),取价代码块被一并跳过,
导致 flowEvent.FloatRate 落库为旧周期利率,传染后续 EOD 复利计算。

修复:把重置日的 FR007 取价提前到 calcFirst/calcLast 跳过判断之前——
calcLast 只应跳过'计息',不应跳过'重置日利率取价'。同时循环外用最终
floatRate 兜底赋值 flowEvent.FloatRate,确保落库值反映最后重置日的利率。

验证:
- CI_007 合成测试(平仓日=重置日):修复前 FloatRate=旧值(FAIL),修复后=新值(PASS)
- 连库验证(GLMS-JIATT-20260805 8/4平仓):FloatRate 0.0123→0.0213(8/3新值)
- 利息金额不变(calcLast 不计当天利息,Amount 不受影响,只修正 FloatRate 字段)
- 全套 swap 测试无新增回归(334通过,2失败均为pre-existing单利/EOD路径)

附:CI_007 复现测试 + GLMS20260805 FR007/EOD 诊断工具 + 文档 longRatio 状态更新
This commit is contained in:
hjhan
2026-08-07 20:43:16 +08:00
parent 3178cf3f0f
commit 66a97e03ef
4 changed files with 762 additions and 35 deletions
@@ -44,18 +44,29 @@ namespace YLErp.Modules.SwapModule
{
private readonly double _floatRate;
private readonly decimal _consumedInterest;
private readonly Func<DateTime, double> _floatRateByDate;
public StubSwapDealService(OptUserInfo optUser, double floatRate, decimal consumedInterest)
: base(optUser)
{
_floatRate = floatRate;
_consumedInterest = consumedInterest;
_floatRateByDate = null;
}
/// <summary>按查询日期返回不同浮动利率(用于复现重置日取价 bug</summary>
public StubSwapDealService(OptUserInfo optUser, Func<DateTime, double> floatRateByDate, decimal consumedInterest = 0m)
: base(optUser)
{
_floatRate = 0;
_consumedInterest = consumedInterest;
_floatRateByDate = floatRateByDate;
}
protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate)
{
rate = _floatRate;
return true; // 始终返回固定浮动利率
rate = _floatRateByDate != null ? _floatRateByDate(valueDate) : _floatRate;
return true;
}
public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate)
@@ -260,5 +271,104 @@ namespace YLErp.Modules.SwapModule
AssertDecimal(baseline - consumed * closePercent, result,
$"partial close should deduct consumed interest by closePercent ({closePercent})");
}
// ================================================================
// 场景7:复现"平仓日=重置日 + calcLast=false → 重置日跳过 FR007 取价"
// ================================================================
/// <summary>
/// [CI_007] 平仓日恰好是重置日时,calcLast=false 不应导致该重置日的 FR007 取价被跳过
/// ----------------------------------------------------------------
/// 背景(GLMS-JIATT-20260805 根因)InterestCalcMode='10'(算头不算尾,calcLast=false)
/// CalcDailyCompoundInterest 循环里 `if(!calcLast && accrueDate==endDate) continue` 会跳过平仓日当天。
/// 若平仓日恰好是重置日(i%period==0),这个跳过会让"重置日取新FR007"的代码块永远不执行,
/// 沿用上一个重置周期的旧利率。
///
/// 构造:PosiStartDate=4/27, ResetPeriod=3, InterestCalcMode='10'(calcLast=false)
/// - FR007 按日期分段:5/3之前返回 rateOld=0.0015/3及之后返回 rateNew=0.002
/// - 对照A:平仓日=5/5(非重置日,9? 不: (5/5-4/27)=8, 8%3=2 非重置) → 不该取新值
/// - 对照B:平仓日=5/6(重置日,(5/6-4/27)=9, 9%3=0) → 应取新值 rateNew
///
/// 修复前:5/6 重置日被 calcLast 跳过 → 取到旧 rateOld → 与 5/5 相同
/// 修复后:5/6 重置日正常取价 → 取到 rateNew → 与 5/5 不同
/// ----------------------------------------------------------------
/// </summary>
/// <summary>
/// [CI_007] 平仓日恰好是重置日时,calcLast=false 不应导致该重置日的 FR007 取价被跳过
/// ----------------------------------------------------------------
/// 根因(GLMS-JIATT-20260805)InterestCalcMode='10'(calcLast=false)
/// CalcDailyCompoundInterest 循环 `if(!calcLast && accrueDate==endDate) continue` 跳过平仓日。
/// 若平仓日=重置日,取价代码块被跳过 → flowEvent.FloatRate 停留旧值 → 落库后传染 EOD。
///
/// 构造(避开周末,period=7)
/// PosiStartDate=4/27(周一), period=7, interest_rule=0, InterestCalcMode='10'
/// 重置日:i=0→4/27(周一), i=7→5/4(周一,工作日)
/// 平仓日=5/4(=重置日=endDate)
/// FR007 分界:rateDate>=5/4 返回 rateNew,否则 rateOld
///
/// 修复前:i=7(5/4)被 calcLast 跳过 → FloatRate=rateOld(旧值)
/// 修复后:i=7(5/4)正常取价 → FloatRate=rateNew(新值)
/// ----------------------------------------------------------------
/// </summary>
[TestMethod]
public void CI_007_平仓日等于重置日_calcLast_false_仍应取新FR007()
{
const double rateOld = 0.001;
const double rateNew = 0.002;
// 用 6 月日期避开五一/周末:PosiStartDate=6/1(周一), period=7, 平仓日=6/8(周一,重置日)
DateTime posiStart = new DateTime(2026, 6, 1);
DateTime unwindDate = new DateTime(2026, 6, 8); // (6/8-6/1)=7, 7%7=0 重置日
DateTime newRateFrom = new DateTime(2026, 6, 8); // 6/8(查询日,周一工作日)起为新利率
StubSwapDealService ServiceByDate() => new StubSwapDealService(
new OptUserInfo(0, nameof(ConsumedInterestScenarioTest), OptUserFrom.UnitTest),
d => d >= newRateFrom ? rateNew : rateOld);
var td = new trade
{
id = 1, TradeNumber = "UT-CI007", ClientId = 999998,
TradeType = "收益互换", TradeDate = posiStart, StartDate = posiStart,
ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid",
StructureType = "单标的", QuoteCurrency = "CNY", SettlementCurrency = "CNY",
trade_extend = new trade_extend
{
TradeId = 1,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays, InterestCalcMode = "10", SettlementRules = 0
})
}
};
var position = new swap_position
{
id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown,
InterestDirection = (int)SwapDirectionEnum., InterestMode = (int)InterestModeEnum.,
InterestRateDefault = FixedRate, InterestPrincipalFix = Principal,
PosiStartDate = posiStart, PosiMatuirityDate = ExerciseDate,
IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.,
IsAnnualized = true, interest_rest_days = 7, interest_rule = 0,
FloatRateUnderlyingCode = "FR007",
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel>
{
new IntervalModel { Date = ExerciseDate, Rate = FixedRate, Settlement = 0 }
})
};
var interests = ServiceByDate().GetInterests(td, td.trade_extend, unwindDate, unwindDate,
new List<eod_swap_position>(), new List<swap_position> { position },
Principal, Principal, Principal, Principal, 1m,
(int)SwapEventTypeEnum., false, false, Principal, Principal,
add: false, settment: false, newCalcLast: false);
Assert.AreEqual(1, interests.Count);
var result = interests[0];
Console.WriteLine($"6/8(重置日,周一)平仓:FloatRate={result.FloatRate} Amount={result.InterestAmount:F6}");
Console.WriteLine($" 期望 FloatRate={rateNew}6/8 重置日查询日=6/8工作日,应取新利率)");
// 核心断言:6/8 是重置日,flowEvent.FloatRate 应反映新利率 rateNew
Assert.IsTrue(Math.Abs((result.FloatRate ?? 0) - (decimal)rateNew) < 0.0001m,
$"平仓日=重置日时 FloatRate 应={rateNew}(取到新利率)。" +
$"实际={result.FloatRate},若={rateOld} 说明 calcLast=false 跳过了重置日取价(GLMS-JIATT-20260805 根因)");
}
}
}