refactor(swap-eod): L1 类内去重——CopyInterestLegFields/FinalizeInterestEodRoll 收口四 Save* 重复段(行为保真)
- 腿字段拷贝 3 处×14 行 → 入参化 helper(FloatRate 来源随场景传入,勿统一) - 滚存收尾 4 处 → FinalizeInterestEodRoll(方向源参数化:三处 position、Copy 一处 eodPayPosition,勿统一;Persist 与日志留调用点保持原序) - 差异保真:WithClose 不设 InterestFeePending;勿缩放注释保留升级 - 安全网:InterestEodTailSnapshotTest 三格尾部快照(AutoSettle/AutoSettleWithClose/CloseOnly,钉值特征化)+ golden 两格 + DI_EXCEL 家族 - 验证:快照 3/3 逐字段一致;全量 SwapModule 576/566/10/0 与去重前基线完全相同 - 注:本提交为工作区被还原后按原编辑重放,验证结果与首次执行一致
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// L1(类内去重)前置安全网:DealInterests 四分支中无 golden 语料的三格
|
||||
/// (AutoSettle / AutoSettleWithClose / CloseOnly)尾部滚存字段特征化快照。
|
||||
///
|
||||
/// - ManualSwap / RollForward 两格已由 DealInterestsGoldenReplayTest 语料钉住
|
||||
/// (字段集见 GoldenReplayFramework.EodPositionToJson)。
|
||||
/// - 本测试钉"现状行为":L1 抽共享助手(腿字段拷贝段 + 滚存收尾段)前后,
|
||||
/// 以下字段必须逐字段不变。变化=去重改了口径。
|
||||
/// - 同时断言接 seam 指纹(哪个计息接缝 + eventType)与 autoInterests 收集行为,
|
||||
/// 兼作 L2(按腿拆类)的路由验收。
|
||||
/// - 计息金额由受控 CalcResult 注入(不连库、不依赖真实计息引擎)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class InterestEodTailSnapshotTest
|
||||
{
|
||||
private const decimal Principal = 10000m;
|
||||
private const decimal Rate = 0.03m;
|
||||
private static readonly DateTime StartDate = new(2026, 4, 27);
|
||||
private static readonly DateTime SettleDate = StartDate.AddDays(10); // 第10天收盘
|
||||
private const decimal Accrued10d = 8.22m; // 受控:10天理论应结
|
||||
private const decimal DailyNew = 0.82m; // 受控:当日新增
|
||||
private const decimal ManualSettled = 3.5m; // 受控:盘中平仓已结
|
||||
private const decimal Remaining = 7000m; // 平仓后剩余本金
|
||||
private const decimal ClosedNotional = 3000m; // 本次平掉本金
|
||||
|
||||
private sealed class TailStubService : TestableSwapEodPositionService
|
||||
{
|
||||
public TailStubService() : base(nameof(InterestEodTailSnapshotTest)) { }
|
||||
|
||||
/// <summary>受控计息结果:两个计息 seam 均返回它</summary>
|
||||
public List<swap_flow_event> CalcResult { get; set; } = new();
|
||||
|
||||
public string LastCalcSeam { get; private set; } = "";
|
||||
public List<int> CalcEventTypes { get; } = new();
|
||||
|
||||
protected override List<swap_flow_event> CalcSwapInterests(
|
||||
trade td, trade_extend tradeExtend,
|
||||
DateTime valueDate, DateTime unwindDate,
|
||||
List<eod_swap_position> eodPositions, List<swap_position> positions,
|
||||
decimal posiNotionalValue,
|
||||
decimal closePosiNotionalValue, decimal closePrecent,
|
||||
int eventType, bool tdClose,
|
||||
decimal orginPv,
|
||||
bool add = false, bool settment = true, bool newCalcLast = false,
|
||||
List<swap_flow_event> closeList = null)
|
||||
{
|
||||
LastCalcSeam = nameof(CalcSwapInterests);
|
||||
CalcEventTypes.Add(eventType);
|
||||
return CalcResult;
|
||||
}
|
||||
|
||||
protected override List<swap_flow_event> CalcEodPostCloseSettleInterests(InterestCalcRequest req)
|
||||
{
|
||||
LastCalcSeam = nameof(CalcEodPostCloseSettleInterests);
|
||||
CalcEventTypes.Add(req.EventType);
|
||||
return CalcResult;
|
||||
}
|
||||
|
||||
public List<swap_flow_event> ExecuteDealInterests(
|
||||
List<swap_position> interestList, List<eod_swap_position> eodPositions,
|
||||
DateTime settleDate, trade td, List<swap_flow_event> flowEvents,
|
||||
decimal posiTotalNotional, decimal closeNational, decimal grossPrice, decimal orginPv)
|
||||
{
|
||||
var autoInterests = new List<swap_flow_event>();
|
||||
DealInterests(interestList, eodPositions, new List<eod_swap_position>(),
|
||||
settleDate, td, flowEvents, autoInterests, null,
|
||||
posiTotalNotional, closeNational, grossPrice, orginPv);
|
||||
return autoInterests;
|
||||
}
|
||||
}
|
||||
|
||||
private static trade CreateTrade() => new()
|
||||
{
|
||||
id = 1, TradeNumber = "TAIL-SNAP-001", ClientId = 999998,
|
||||
TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate,
|
||||
ExerciseDate = new DateTime(2027, 4, 27), TradeStatus = "确认成交",
|
||||
ValidState = "Valid", StructureType = "单标的",
|
||||
QuoteCurrency = "CNY", SettlementCurrency = "CNY",
|
||||
trade_extend = new trade_extend
|
||||
{
|
||||
TradeId = 1,
|
||||
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson { AnnualDays = 365, InterestCalcMode = "10", SettlementRules = 0 })
|
||||
}
|
||||
};
|
||||
|
||||
/// <param name="observationDay">true=当日观察日(Settlement=1);false=观察日在别日</param>
|
||||
private static swap_position CreateInterestPosition(bool observationDay)
|
||||
{
|
||||
var interval = observationDay
|
||||
? new IntervalModel { Date = SettleDate, Rate = Rate, Settlement = 1 }
|
||||
: new IntervalModel { Date = StartDate, Rate = Rate, Settlement = 1 };
|
||||
return new swap_position
|
||||
{
|
||||
id = 1001, SwapTradeId = 1, InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = (int)InterestModeEnum.标的期初全价, InterestRateDefault = Rate,
|
||||
InterestPrincipalFix = Principal, PosiStartDate = StartDate,
|
||||
PosiMatuirityDate = new DateTime(2027, 4, 27), IsInitial = true,
|
||||
InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true,
|
||||
interest_rest_days = 1, interest_rule = 0,
|
||||
InterestSwapInterval = JsonConvert.SerializeObject(new List<IntervalModel> { interval })
|
||||
};
|
||||
}
|
||||
|
||||
private static eod_swap_position CreatePreEod(decimal accumulated) => new()
|
||||
{
|
||||
id = 100, PositionId = 1001, ValueDate = SettleDate.AddDays(-1),
|
||||
InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价,
|
||||
InterestIncomeSum = accumulated, InterestProfitSum = accumulated,
|
||||
InterestRateDefault = Rate, TdInterestPrincipal = Principal,
|
||||
InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 1
|
||||
};
|
||||
|
||||
private static swap_flow_event CreateCalcResult() => new()
|
||||
{
|
||||
EventType = (int)SwapEventTypeEnum.自动互换, PositionId = 1001,
|
||||
InterestAmount = Accrued10d, TdInterestAmount = DailyNew,
|
||||
InterestClosePnL = Accrued10d,
|
||||
InterestPrincipal = Principal, InterestRate = Rate,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取
|
||||
};
|
||||
|
||||
private static swap_flow_event CreateCloseEvent() => new()
|
||||
{
|
||||
EventType = (int)SwapEventTypeEnum.平仓, PositionId = 1001,
|
||||
InterestAmount = ManualSettled, InterestClosePnL = ManualSettled,
|
||||
InterestRate = Rate, InterestFee = 0m,
|
||||
InterestPrincipal = ClosedNotional,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
DataState = (int)SwapFlowDateStateEnum.完成
|
||||
};
|
||||
|
||||
/// <summary>AutoSettle 格:观察日无平仓 → SaveAutoEodInterestPosition,返回值收集进 autoInterests</summary>
|
||||
[TestMethod]
|
||||
public void AutoSettle_观察日无平仓_尾部快照()
|
||||
{
|
||||
var service = new TailStubService { CalcResult = new List<swap_flow_event> { CreateCalcResult() } };
|
||||
var autoInterests = service.ExecuteDealInterests(
|
||||
new List<swap_position> { CreateInterestPosition(observationDay: true) },
|
||||
new List<eod_swap_position> { CreatePreEod(Accrued10d) },
|
||||
SettleDate, CreateTrade(), new List<swap_flow_event>(),
|
||||
Principal, 0m, 100m, Principal);
|
||||
|
||||
Assert.AreEqual("CalcSwapInterests", service.LastCalcSeam, "观察日无平仓应走 CalcSwapInterests seam");
|
||||
Assert.AreEqual((int)SwapEventTypeEnum.自动互换, service.CalcEventTypes.Single(), "eventType 应为自动互换");
|
||||
Assert.AreEqual(1, autoInterests.Count, "观察日分支应收集返回值进 autoInterests(→资金记录)");
|
||||
|
||||
var p = service.PersistedPositions.Single();
|
||||
// 钉值于 2026-08-17 现状行为(受控输入:应结8.22/新增0.82/本金10000)
|
||||
Assert.AreEqual(8.22m, p.TdCloseInterest);
|
||||
Assert.AreEqual(0.82m, p.TdInterestIncome);
|
||||
Assert.AreEqual(10000m, p.TdInterestPrincipal);
|
||||
Assert.AreEqual(0.03m, p.TdInterestRate);
|
||||
Assert.AreEqual(0.00m, p.InterestIncomeSum, "应结=结算,待实现清零");
|
||||
Assert.AreEqual(0m, p.InterestFeeSum);
|
||||
Assert.AreEqual(0m, p.InterestProfitSum);
|
||||
Assert.AreEqual(8.22m, p.RealizedInterest);
|
||||
Assert.AreEqual(0m, p.RealizedInterestFee);
|
||||
Assert.AreEqual(0m, p.SwapPositionValue);
|
||||
Assert.AreEqual(1.0m, p.TdCurrency);
|
||||
}
|
||||
|
||||
/// <summary>AutoSettleWithClose 格(TEST-MATRIX §6 最弱格):观察日+平仓 → SaveAutoEodWithCloseInterestPosition(autoSwap:true),补结差额=恒1全额−盘中已结</summary>
|
||||
[TestMethod]
|
||||
public void AutoSettleWithClose_观察日加平仓_尾部快照()
|
||||
{
|
||||
var service = new TailStubService { CalcResult = new List<swap_flow_event> { CreateCalcResult() } };
|
||||
var autoInterests = service.ExecuteDealInterests(
|
||||
new List<swap_position> { CreateInterestPosition(observationDay: true) },
|
||||
new List<eod_swap_position> { CreatePreEod(Accrued10d) },
|
||||
SettleDate, CreateTrade(), new List<swap_flow_event> { CreateCloseEvent() },
|
||||
Remaining, ClosedNotional, 100m, Remaining);
|
||||
|
||||
Assert.AreEqual("CalcEodPostCloseSettleInterests", service.LastCalcSeam, "观察日+平仓应走 EodPostCloseSettle seam");
|
||||
Assert.AreEqual((int)SwapEventTypeEnum.自动互换, service.CalcEventTypes.Single(), "autoSwap=true → eventType=自动互换");
|
||||
Assert.AreEqual(1, autoInterests.Count, "观察日分支应收集返回值进 autoInterests");
|
||||
Assert.AreEqual(Accrued10d - ManualSettled, autoInterests[0].InterestAmount, "补结差额=恒1全额8.22−盘中已结3.50");
|
||||
|
||||
var p = service.PersistedPositions.Single();
|
||||
// 钉值于 2026-08-17 现状行为(受控输入:恒1全额8.22/盘中已结3.5/剩余7000/平掉3000)
|
||||
Assert.AreEqual(8.22m, p.TdCloseInterest, "TdCloseInterest=盘中已结3.50+补结4.72");
|
||||
Assert.AreEqual(0.5753424657534246575342465753m, p.TdInterestIncome, "autoSwap 重算展示应计=剩余7000×3%/365");
|
||||
Assert.AreEqual(7000m, p.TdInterestPrincipal, "单利部分平仓:跨日本金=剩余");
|
||||
Assert.AreEqual(0.03m, p.TdInterestRate);
|
||||
Assert.AreEqual(0.00m, p.InterestIncomeSum, "恒1口径:理论应结8.22−结算8.22=0");
|
||||
Assert.AreEqual(0m, p.InterestFeeSum);
|
||||
Assert.AreEqual(0m, p.InterestProfitSum);
|
||||
Assert.AreEqual(8.22m, p.RealizedInterest);
|
||||
Assert.AreEqual(0m, p.RealizedInterestFee);
|
||||
Assert.AreEqual(0m, p.SwapPositionValue);
|
||||
Assert.AreEqual(1.0m, p.TdCurrency);
|
||||
}
|
||||
|
||||
/// <summary>CloseOnly 格:非观察日平仓 → SaveAutoEodWithCloseInterestPosition(autoSwap:false),返回值不收集,TdCloseInterest=盘中已结</summary>
|
||||
[TestMethod]
|
||||
public void CloseOnly_非观察日平仓_尾部快照()
|
||||
{
|
||||
var service = new TailStubService { CalcResult = new List<swap_flow_event> { CreateCalcResult() } };
|
||||
var autoInterests = service.ExecuteDealInterests(
|
||||
new List<swap_position> { CreateInterestPosition(observationDay: false) },
|
||||
new List<eod_swap_position> { CreatePreEod(Accrued10d) },
|
||||
SettleDate, CreateTrade(), new List<swap_flow_event> { CreateCloseEvent() },
|
||||
Remaining, ClosedNotional, 100m, Remaining);
|
||||
|
||||
Assert.AreEqual("CalcEodPostCloseSettleInterests", service.LastCalcSeam, "纯平仓应走 EodPostCloseSettle seam");
|
||||
Assert.AreEqual((int)SwapEventTypeEnum.平仓, service.CalcEventTypes.Single(), "autoSwap=false → eventType=平仓");
|
||||
Assert.AreEqual(0, autoInterests.Count, "纯平仓分支不收集返回值(结算已在盘中流水定格)");
|
||||
|
||||
var p = service.PersistedPositions.Single();
|
||||
// 钉值于 2026-08-17 现状行为(受控输入:恒1重算8.22/盘中已结3.5/剩余7000/平掉3000)
|
||||
Assert.AreEqual(ManualSettled, p.TdCloseInterest, "TdCloseInterest 应仅为盘中已结3.50,不叠加恒1重算值");
|
||||
Assert.AreEqual(0.5753424657534246575342465753m, p.TdInterestIncome, "不算尾路径:剩余7000×3%/365");
|
||||
Assert.AreEqual(7000m, p.TdInterestPrincipal, "单利部分平仓:跨日本金=剩余");
|
||||
Assert.AreEqual(0.03m, p.TdInterestRate, "非观察日:利率取平仓流水 InterestRate");
|
||||
Assert.AreEqual(5.295342465753m, p.InterestIncomeSum, "尾差递推:上日8.22+新增0.575342−已结3.50");
|
||||
Assert.AreEqual(0m, p.InterestFeeSum);
|
||||
Assert.AreEqual(5.295342465753m, p.InterestProfitSum);
|
||||
Assert.AreEqual(3.5m, p.RealizedInterest);
|
||||
Assert.AreEqual(0m, p.RealizedInterestFee);
|
||||
Assert.AreEqual(5.295342465753m, p.SwapPositionValue);
|
||||
Assert.AreEqual(1.0m, p.TdCurrency);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user