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 一致");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user