feat(accrual): 新增 AccrueSimplePeriod + 影子测试

第2步迁移: CalcDailySimpleInterest(盘中单利多日)。

新增纯函数 AccrueSimplePeriod:
- 单利特征: 计息本金恒定(差分 = priorAccrualPrincipal + positionPrincipal - originalPv)
- 按重置日分段, 每段用 AccrualDays 算天数×日利息(无逐日循环)
- 续接 priorValueDate 之后的日期

修复: SwapInterest.Round 是 private, 新方法改用 Math.Round。
修复: IReadOnlyList 无 IndexOf, 改用 for 循环索引。

影子测试(2个,全过):
- 固定利率无归档: 旧新一致(差分本金=0,利息=0,符合旧逻辑)
- 有归档续接+部分平仓50%: 旧新一致

验证: 编译0错误, 全量517测试7失败(基线一致)。
This commit is contained in:
hjhan
2026-08-12 09:27:20 +08:00
parent 217c2d5826
commit 963b5e66af
2 changed files with 221 additions and 0 deletions
@@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using YLErp;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
using YLErp.Modules.SwapModule;
using YLErp.Modules.SwapModule.Accrual;
using YLErp.Derivatives.Interest;
namespace UnitTestProject.Modules.SwapModule.Accrual
{
/// <summary>
/// 影子测试:CalcDailySimpleInterest(旧逐日循环)vs FundingLegAccrual.AccrueSimplePeriod(新分段纯函数)。
/// </summary>
[TestClass]
public class SimplePeriodShadowTest
{
private const decimal Notional = 100_000_000m;
private const decimal Spread = 0.0025m;
private const int AnnualDays = 365;
private static readonly DateTime StartDate = new(2026, 4, 21);
private static readonly DateTime EndDate = new(2026, 5, 11); // 21天, 7天周期→重置日 4/28, 5/5
private static trade CreateTrade()
{
return new trade
{
id = 1, TradeNumber = "UT-SIMPLE-SHADOW", ClientId = 999998,
TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate,
ExerciseDate = StartDate.AddYears(1), TradeStatus = "确认成交", ValidState = "Valid",
trade_extend = new trade_extend
{
TradeId = 1,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays, InterestCalcMode = "10", SettlementRules = 0
})
}
};
}
private static swap_position CreatePosition()
{
return new swap_position
{
id = 1001, SwapTradeId = 1, PosiDirection = 0,
InterestDirection = (int)SwapDirectionEnum.,
InterestMode = (int)InterestModeEnum.,
InterestRateDefault = Spread,
InterestPrincipalFix = Notional,
PosiStartDate = StartDate, PosiMatuirityDate = StartDate.AddYears(1),
IsInitial = true, Invalid = false,
InterestType = (int)InterestTypeEnum.,
IsAnnualized = true, interest_rest_days = 7, interest_rule = 0,
FloatRateUnderlyingCode = null,
InterestSwapInterval = "[]"
};
}
private sealed class StubSvc : SwapDealService
{
public StubSvc() : base(new OptUserInfo(0, nameof(SimplePeriodShadowTest), OptUserFrom.UnitTest)) { }
}
/// <summary>
/// 固定利率(无FR007)算头不算尾,全平,无历史归档。
/// </summary>
[TestMethod]
public void _固定利率_无归档_旧新一致()
{
var td = CreateTrade();
var position = CreatePosition();
var flowEvent = new swap_flow_event { InterestRate = Spread };
var preEod = new eod_swap_position { id = 0, TdInterestPrincipal = 0, InterestProfitSum = 0 };
// 旧方法
decimal oldI = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent,
AnnualDays, false, 0m, 1m, Notional, true, false, ref oldI, ref oldTd);
// 新方法:固定利率全段相同
// 旧代码差分: dynomicPrincipal = preEod.TdInterestPrincipal(=0) + posiPrincipal - orginPv = 0
var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) };
var accrualPrincipal = 0m + Notional - Notional; // 差分 = 0
var result = FundingLegAccrual.AccrueSimplePeriod(
priorUnrealized: 0m,
accrualPrincipal: 0m, // 差分=0(无归档时 preEod.TdInterestPrincipal=0)
closeRatio: 1m,
segmentRates: segRates,
startDate: StartDate,
endDate: EndDate,
priorValueDate: DateTime.MinValue,
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 一致");
}
/// <summary>
/// 有历史归档(preEod.id != 0),续接上一日终。
/// </summary>
[TestMethod]
public void _有归档_续接_旧新一致()
{
var position = CreatePosition();
var preEodDate = new DateTime(2026, 5, 4); // 上一日终 = 第14天
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 };
// 旧方法
decimal oldI = 0, oldTd = 0;
var svc = new StubSvc();
svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent,
AnnualDays, false, 0m, 0.5m, Notional, true, false, ref oldI, ref oldTd);
// 新方法
// 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv
var accrualPrincipal = Notional + Notional - Notional;
var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) };
var result = FundingLegAccrual.AccrueSimplePeriod(
priorUnrealized: 200_000m * 0.5m, // InterestProfitSum × closePercent
accrualPrincipal: accrualPrincipal,
closeRatio: 0.5m,
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 一致");
}
}
}