- 抽取 BuildSegmentRates:统一单利/复利分段取率循环,参数化 fetchAfterDate (单利传 ValueDate 仅取新段,复利传 null 全程取) - 修复 BuildSegmentRates static→instance:访问实例属性 IndexFixer - 清理 CalcDaily 层 needPrice 死参数:4个 CalcDaily* 方法签名移除 needPrice;上层 CalcSwapInterests/GetInterests 保留 (virtual seam/位置参数兼容) - 修复 6 处调用点 needPrice 参数传递 - 新增影子测试:单利+FR007浮动+部分平仓+历史归档,验证 segmentRates 一致 - 新增 TdCarryInCharacterizationTest:钉死部分平仓 TdInterestAmount carry-in 行为 测试: 511通过 / 7预存在失败(数据依赖) / 9跳过
237 lines
11 KiB
C#
237 lines
11 KiB
C#
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 SimpleInterestAccrual.AccruePeriod(新分段纯函数)。
|
||
/// </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>带浮动率 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>
|
||
[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, 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 = SimpleInterestAccrual.AccruePeriod(
|
||
priorAccrued: 0m,
|
||
notional: 0m,
|
||
unwindFraction: 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 一致");
|
||
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
|
||
}
|
||
|
||
/// <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, 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 = SimpleInterestAccrual.AccruePeriod(
|
||
priorAccrued: 200_000m * 0.5m,
|
||
notional: accrualPrincipal,
|
||
unwindFraction: 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 一致");
|
||
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, 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 = SimpleInterestAccrual.AccruePeriod(
|
||
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 一致");
|
||
}
|
||
}
|
||
}
|