搬迁(算法体逐字未动,仅换命名空间与归属): - SwapInterest.Round/AccrualDays/FundingLegPrecision + AccrualBoundary/InterestResult → YLErpDAL/Modules/SwapModule/Accrual/InterestMath.cs - AccrualTrace → Accrual/AccrualTrace.cs(被迫同迁:其 MarkStart 引用 AccrualBoundary, Core 不能反向依赖 DAL) - 引用切换:Simple/CompoundInterestAccrual、AccrualPolicy、SwapCalcTrace、SwapDealService (保留 using YLErp.Derivatives.Interest——IIndexFixer/IndexFixerBase 留 Core) 删除(零生产引用,孤儿清零): - Core:SwapInterest.cs 算法方法(AccrueSimple/AccrueCompoundInArrears/ApplyUnwind/ AccrueUnrealized/ToInterestRate,未接线且与 DAL 生产实现舍入/rollover 口径已分叉)、 AccrualContext.cs、InterestRate.cs - DAL:AccrualState.cs(零引用死类) - 测试:SwapInterest_CompoundInArrears_RolloverTimingTests.cs(仅测已删原语) 验证:两解决方案 Rebuild 0 错误;磁盘 SwapInterest. 残留 0;影子/分红/场景 86/86 通过 (含 Accrual 3 影子对账、Margin 影子、divPower 新增 AutoUnwindMultiPartial)。 注:AccrualContext 默认精度 11 与生产 12 的分叉隐患随删除一并消除; 已删原语若将来重建须先补对账测试,勿凭记忆复原(ARCHITECTURE.md 已留警告)。
149 lines
6.5 KiB
C#
149 lines
6.5 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;
|
|
|
|
namespace UnitTestProject.Modules.SwapModule.Accrual
|
|
{
|
|
/// <summary>
|
|
/// 影子测试:CalcDailyCompoundInterestByEod(旧逐日循环)vs CompoundInterestAccrual.AccrueEod(新纯函数)。
|
|
/// 构造同一组参数,两套实现并行跑,断言结果一致(到分)。
|
|
/// </summary>
|
|
[TestClass]
|
|
public class CompoundEodShadowTest
|
|
{
|
|
private const decimal Notional = 100_000_000m;
|
|
private const decimal FixedRate = 0.03m;
|
|
private const int AnnualDays = 365;
|
|
private static readonly DateTime TradeDate = new(2026, 4, 21);
|
|
private static readonly DateTime EodDate = new(2026, 4, 28); // 第7天=重置日
|
|
|
|
private static trade CreateTrade()
|
|
{
|
|
return new trade
|
|
{
|
|
id = 1, TradeNumber = "UT-SHADOW", ClientId = 999998,
|
|
TradeType = "收益互换", TradeDate = TradeDate, StartDate = TradeDate,
|
|
ExerciseDate = TradeDate.AddYears(1), TradeStatus = "确认成交", ValidState = "Valid",
|
|
trade_extend = new trade_extend
|
|
{
|
|
TradeId = 1,
|
|
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
|
{
|
|
AnnualDays = AnnualDays, InterestCalcMode = "11", SettlementRules = 0
|
|
})
|
|
}
|
|
};
|
|
}
|
|
|
|
private static swap_position CreatePosition(int interestMode, int interestType, int resetDays)
|
|
{
|
|
return new swap_position
|
|
{
|
|
id = 1001, SwapTradeId = 1,
|
|
PosiDirection = 0,
|
|
InterestDirection = (int)SwapDirectionEnum.收取,
|
|
InterestMode = interestMode,
|
|
InterestRateDefault = FixedRate,
|
|
InterestPrincipalFix = Notional,
|
|
PosiStartDate = TradeDate,
|
|
PosiMatuirityDate = TradeDate.AddYears(1),
|
|
IsInitial = true, Invalid = false,
|
|
InterestType = interestType,
|
|
IsAnnualized = true,
|
|
interest_rest_days = resetDays,
|
|
interest_rule = 0,
|
|
FloatRateUnderlyingCode = null,
|
|
InterestSwapInterval = "[]"
|
|
};
|
|
}
|
|
|
|
private static eod_swap_position CreatePreEod(decimal tdPrincipal, decimal unrealized)
|
|
{
|
|
return new eod_swap_position
|
|
{
|
|
id = 1, SwapTradeId = 1, PositionId = 1001,
|
|
ValueDate = EodDate.AddDays(-1),
|
|
TdInterestPrincipal = tdPrincipal,
|
|
InterestProfitSum = unrealized,
|
|
PosiNotionalValue = Notional,
|
|
FloatRate = 0m
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 重置日场景:EOD 恰为重置日(7天周期,第7天)。
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void 影子_重置日_旧新一致()
|
|
{
|
|
var position = CreatePosition((int)InterestModeEnum.合约名义本金规模, (int)InterestTypeEnum.复利, 7);
|
|
var preEod = CreatePreEod(Notional, 50_000m);
|
|
var flowEvent = new swap_flow_event { InterestRate = FixedRate };
|
|
|
|
// 旧方法
|
|
decimal oldInterest = 0, oldTd = 0;
|
|
var svc = new StubSvc();
|
|
svc.CalcDailyCompoundInterestByEod(preEod, EodDate, TradeDate, position,
|
|
Notional, Notional, flowEvent, AnnualDays, 0m, 1m,
|
|
ref oldInterest, ref oldTd);
|
|
|
|
// 新方法
|
|
var rate = FundingLegRate.Fixed(FixedRate);
|
|
var policy = new AccrualPolicy(AccrualBoundary.Both, true, 7, AnnualDays, true);
|
|
var remainingPercent = Math.Max(0m, Math.Min(1m, Notional / Notional));
|
|
var result = CompoundInterestAccrual.AccrueEod(
|
|
50_000m, Notional, Notional, 1m, rate, policy,
|
|
isResetDay: true, remainingPercent, EodDate);
|
|
|
|
Console.WriteLine($"重置日: 旧 InterestAmount={oldInterest} Td={oldTd}");
|
|
Console.WriteLine($"重置日: 新 Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
|
|
Assert.AreEqual((double)oldInterest, (double)result.Accrued, 0.01, "InterestAmount 一致");
|
|
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 非重置日场景:第3天(非7的倍数)。
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void 影子_非重置日_旧新一致()
|
|
{
|
|
var nonResetDate = new DateTime(2026, 4, 24); // 第3天
|
|
var position = CreatePosition((int)InterestModeEnum.合约名义本金规模, (int)InterestTypeEnum.复利, 7);
|
|
var preEod = CreatePreEod(Notional, 30_000m);
|
|
preEod.ValueDate = nonResetDate.AddDays(-1);
|
|
var flowEvent = new swap_flow_event { InterestRate = FixedRate };
|
|
|
|
// 旧方法
|
|
decimal oldInterest = 0, oldTd = 0;
|
|
var svc = new StubSvc();
|
|
svc.CalcDailyCompoundInterestByEod(preEod, nonResetDate, TradeDate, position,
|
|
Notional, Notional, flowEvent, AnnualDays, 0m, 1m,
|
|
ref oldInterest, ref oldTd);
|
|
|
|
// 新方法
|
|
var rate = FundingLegRate.Fixed(FixedRate);
|
|
var policy = new AccrualPolicy(AccrualBoundary.Both, true, 7, AnnualDays, true);
|
|
var result = CompoundInterestAccrual.AccrueEod(
|
|
30_000m, Notional, Notional, 1m, rate, policy,
|
|
isResetDay: false, 0m, nonResetDate);
|
|
|
|
Console.WriteLine($"非重置日: 旧 InterestAmount={oldInterest} Td={oldTd}");
|
|
Console.WriteLine($"非重置日: 新 Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
|
|
Assert.AreEqual((double)oldInterest, (double)result.Accrued, 0.01, "InterestAmount 一致");
|
|
Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致");
|
|
}
|
|
|
|
private sealed class StubSvc : SwapDealService
|
|
{
|
|
public StubSvc() : base(new OptUserInfo(0, nameof(CompoundEodShadowTest), OptUserFrom.UnitTest)) { }
|
|
public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate) => 0m;
|
|
}
|
|
}
|
|
}
|