CalcDailySimpleInterest/CalcDailyCompoundInterest内部逐日循环→分段纯函数(签名不变): - AccrueSimplePeriod: AccruedToday改未缩放累计(TdInterestAmount口径) - AccrueCompoundPeriod: 新增out finalBasis供flowEvent.InterestPrincipal精确赋值 - 修复5处分段边界bug(includeStart/includeEnd/resetCarryInterest interestPeriod=1场景) - 提取BuildSegmentRates消除SimpleInterest/CompoundInterest取率重复 - 影子测试补AccruedToday断言关闭测试盲区 SwapModule零回归(7基线/510通过)
200 lines
8.3 KiB
C#
200 lines
8.3 KiB
C#
using Newtonsoft.Json;
|
||
using YLErp;
|
||
using YLErp.Derivatives.Interest;
|
||
using YLErp.Modules.SwapModule;
|
||
using YLErp.Modules.SwapModule.Accrual;
|
||
|
||
namespace UnitTestProject.Modules.SwapModule.Accrual
|
||
{
|
||
/// <summary>
|
||
/// 影子测试:CalcDailyCompoundInterest(旧逐日循环)vs FundingLegAccrual.AccrueCompoundPeriod(新分段纯函数)。
|
||
/// </summary>
|
||
[TestClass]
|
||
public class CompoundPeriodShadowTest
|
||
{
|
||
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天 = 3×7, 末日是重置日
|
||
|
||
private static trade CreateTrade()
|
||
{
|
||
return new trade
|
||
{
|
||
id = 1, TradeNumber = "UT-COMPOUND-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(CompoundPeriodShadowTest), OptUserFrom.UnitTest)) { }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 固定利率(无FR007)算头不算尾,全平。
|
||
/// 重置日 4/28, 5/5, 末日 5/11 恰为重置日(21天=3×7)。
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void 影子_固定利率_全平_末日重置日_旧新一致()
|
||
{
|
||
var position = CreatePosition();
|
||
var flowEvent = new swap_flow_event { InterestRate = Spread };
|
||
var preEod = new eod_swap_position { id = 0 };
|
||
|
||
// 旧方法
|
||
decimal oldI = 0, oldTd = 0;
|
||
var svc = new StubSvc();
|
||
svc.CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent,
|
||
AnnualDays, false, 0m, 1m, Notional, true, false,
|
||
ref oldI, ref oldTd);
|
||
|
||
// 新方法:固定利率全段相同,分段点 = PosiStartDate + k×7
|
||
// 段: [4/21,4/28), [4/28,5/5), [5/5,5/11] → 注意旧代码 calcLast=false 不算末日
|
||
var allInRate = Spread; // 无浮动利率
|
||
var segRates = new List<(DateTime, decimal)>
|
||
{
|
||
(StartDate, allInRate),
|
||
(StartDate.AddDays(7), allInRate),
|
||
(StartDate.AddDays(14), allInRate),
|
||
};
|
||
var result = FundingLegAccrual.AccrueCompoundPeriod(
|
||
notional: Notional,
|
||
segmentRates: segRates,
|
||
startDate: StartDate,
|
||
endDate: EndDate,
|
||
boundary: AccrualBoundary.StartOnly,
|
||
annualDays: AnnualDays,
|
||
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>
|
||
/// 部分平仓 30% + consumedInterest 扣除。
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void 影子_部分平仓30pct_consumedInterest_旧新一致()
|
||
{
|
||
var position = CreatePosition();
|
||
var flowEvent = new swap_flow_event { InterestRate = Spread };
|
||
var preEod = new eod_swap_position { id = 0 };
|
||
const decimal consumed = 50_000m;
|
||
const decimal closePct = 0.3m;
|
||
const decimal carry = 0m; // 无历史归档
|
||
|
||
// 旧方法
|
||
decimal oldI = 0, oldTd = 0;
|
||
var svc = new StubSvc();
|
||
svc.CalcDailyCompoundInterest(EndDate, position, Notional * closePct, flowEvent,
|
||
AnnualDays, false, 0m, closePct, Notional, true, false,
|
||
ref oldI, ref oldTd, consumedInterest: consumed, resetCarryInterest: carry);
|
||
|
||
// 新方法
|
||
var allInRate = Spread;
|
||
var segRates = new List<(DateTime, decimal)>
|
||
{
|
||
(StartDate, allInRate),
|
||
(StartDate.AddDays(7), allInRate),
|
||
(StartDate.AddDays(14), allInRate),
|
||
};
|
||
var result = FundingLegAccrual.AccrueCompoundPeriod(
|
||
notional: Notional * closePct,
|
||
segmentRates: segRates,
|
||
startDate: StartDate,
|
||
endDate: EndDate,
|
||
boundary: AccrualBoundary.StartOnly,
|
||
annualDays: AnnualDays,
|
||
isAnnualized: true,
|
||
resetCarryInterest: carry,
|
||
realizedInterest: consumed,
|
||
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>
|
||
/// 算头算尾(calcMode="11")对比。
|
||
/// </summary>
|
||
[TestMethod]
|
||
public void 影子_算头算尾_旧新一致()
|
||
{
|
||
var position = CreatePosition();
|
||
var flowEvent = new swap_flow_event { InterestRate = Spread };
|
||
var preEod = new eod_swap_position { id = 0 };
|
||
|
||
// 旧方法
|
||
decimal oldI = 0, oldTd = 0;
|
||
var svc = new StubSvc();
|
||
svc.CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent,
|
||
AnnualDays, false, 0m, 1m, Notional, true, true,
|
||
ref oldI, ref oldTd);
|
||
|
||
// 新方法
|
||
var allInRate = Spread;
|
||
var segRates = new List<(DateTime, decimal)>
|
||
{
|
||
(StartDate, allInRate),
|
||
(StartDate.AddDays(7), allInRate),
|
||
(StartDate.AddDays(14), allInRate),
|
||
};
|
||
var result = FundingLegAccrual.AccrueCompoundPeriod(
|
||
notional: Notional,
|
||
segmentRates: segRates,
|
||
startDate: StartDate,
|
||
endDate: EndDate,
|
||
boundary: AccrualBoundary.Both,
|
||
annualDays: AnnualDays,
|
||
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 一致");
|
||
}
|
||
}
|
||
}
|