Files
zszq-trs/UnitTestProject/Modules/SwapModule/PrepaidPrincipalClosingChainTraceTest.cs
T
hjhan 4a3fee9292 refactor(swap)+test: 删 GetInterests/CalcSwapInterests 死参数 needPrice/grossPrice;补工厂→接缝映射钉子
死参数收口(另一半):
- SwapDealService.GetInterests 删 needPrice/grossPrice(体内零消费,2026-08 验证);
  InitSwapDealInterest.needPrice 同为死参数一并删
- SwapEodPositionService.CalcSwapInterests 签名+转发同步;两个 EOD 生产调用点
  (SaveAutoEodInterestPosition/SaveEodInterestPositionCopy) 重排实参;
  CalcEodPostCloseSettleInterests/GetIntradayUnwindInterests 委托同步
- 14 个测试文件 ~44 处直调点机械更新(8 处 override 签名 + 36 处调用实参)
- 注意:EOD 编排链(DealInterests→Save*家族)的 grossPrice(期初不含费价)有真实用途,保留未动

新增钉子:CalcEodPostCloseSettleInterests 工厂→接缝参数映射测试——
CalcSwapInterestsCapture 捕获 stub 断言 EodPostCloseSettle 的完整转发契约
(posi=平仓后剩余/closePosi=平掉额/恒1/settment:false/orginPv 等 11 项)。
该段位置转发含三个相邻同型 decimal,编译器不查错位,此测试兜底。

验证:定向 241 测试通过(含 T0/T1 Excel 验证期望值、EntrySemantics 精确值钉子——
任何 decimal 错位即红);全量 903=145失败/746通过/12跳过,与基线逐位一致。
2026-08-14 16:57:56 +08:00

252 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Linq;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
using YLErp.Model;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 预付金腿「收盘递推链」trace 测试
/// ============================================================================
/// 背景:8.7 全部平仓时预付金端系统值=20.83(≈6天)Excel 预期本次利息=10.41(3天)。
/// 已证明(GetInterests/CalcDailySimpleInterest) 8.7 平仓利息完全由「前一日收盘归档的
/// eod_swap_position.ValueDate」地板决定:ValueDate=8.4→3天→10.41(Excel)ValueDate≈8.1→6天→20.83(系统)。
///
/// 本测试回答用户的核心疑问:「平仓页面是否依赖前一日收盘递推?」
/// —— 是。SwapUnwind 取 lastEod(ValueDate&lt;unwindDate) 的 eod 归档作为 preEodPosition
/// 收盘链 SwapPositionCompose→DealInterests 四个分支全部把 newEodPayPosition.ValueDate=settleDate。
/// 因此:若收盘链每天正常运行,预付金腿 eod.ValueDate 应逐日推进到部分平仓日 8.4;
/// 系统读到 ValueDate≈8.1 说明该腿 eod 在 8.1 之后没被收盘链重新生成(递推链在 8.4 断了),
/// 属「收盘缺失/滞后」而非利息公式 bug。
///
/// 本测试用纯内存驱动 SwapPositionCompose,从交易开始日 7.28 逐日收盘到 8.4(部分平仓),
/// 捕获预付金腿每日 eod.ValueDate,断言其正确推进到 8.4。
/// ============================================================================
[TestClass]
public class PrepaidPrincipalClosingChainTraceTest
{
private const int SwapTradeId = 777;
private static readonly DateTime TradeStart = new(2026, 7, 28);
private static readonly DateTime SwapDate = new(2026, 7, 31); // 先互换
private static readonly DateTime ResetDate = new(2026, 8, 3); // FR007 重置
private static readonly DateTime PartialDate = new(2026, 8, 4); // 部分平仓 40%
private const long PrepayInitialId = 2;
private const long PrepayRealId = 3;
private const long FloatId = 1;
#region 可测试化子类(驱动收盘链,不连库)
private sealed class TraceEodService : TestableSwapEodPositionService
{
private readonly List<trade> _trades;
private readonly List<swap_position> _positions;
private List<eod_swap_position> _eodPositions;
private readonly List<trade_extend> _extends;
private List<swap_flow_event> _currentFlowEvents;
public List<eod_swap_position> EodPositions => _eodPositions;
public TraceEodService(
List<trade> trades, List<swap_position> positions,
List<eod_swap_position> eodPositions, List<eod_swap> eodSwaps,
List<trade_extend> extends)
: base(nameof(PrepaidPrincipalClosingChainTraceTest))
{
_trades = trades; _positions = positions;
_eodPositions = eodPositions; _extends = extends;
_currentFlowEvents = new List<swap_flow_event>();
}
// 收盘链 seam override(对齐 SwapPositionComposeScenarioTest
protected override List<trade> FindActiveSwapTrades(DateTime settleDate, IEnumerable<int> clientIds) => _trades;
protected override List<swap_position> FindAllSwapPositions(List<int> tradeIds) => _positions;
protected override List<trade_extend> FindTradeExtends(List<int> tradeIds) => _extends;
protected override List<eod_swap> FindEodSwapsByDate(DateTime valueDate)
// 从已归档 eod 持仓派生 eod_swap 父记录(稳健,避免依赖 SaveEodSwapRecord seam 回填)
=> _eodPositions.Where(x => x.SwapTradeId == SwapTradeId)
.Select(x => x.ValueDate).Distinct()
.Select(d => new eod_swap { SwapTradeId = SwapTradeId, ValueDate = d })
.ToList();
protected override List<swap_flow_event> FindFlowEvents(int swapTradeId, DateTime settleDate) => _currentFlowEvents;
protected override List<swap_flow_event> FindCompletedFlowEvents(List<int> tradeIds) => _currentFlowEvents;
protected override List<eod_swap_position> FindEodSwapPositions(int swapTradeId, DateTime preSettleDate)
=> _eodPositions.Where(x => x.SwapTradeId == swapTradeId && x.ValueDate >= preSettleDate).ToList();
protected override List<swap_position> FindSwapPositions(int swapTradeId)
=> _positions.Where(x => x.SwapTradeId == swapTradeId && !x.IsInitial).ToList();
protected override underlying_manager GetUnderlyingData(string underlyingCode)
=> new underlying_manager { ValueAddedTax = 0m, UnderlyingInstrumentType = "TBonds" };
protected override decimal GetUnderlyingPrice(string code, DateTime settleDate, out decimal vobp)
{ vobp = 0m; return 100m; }
protected override decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio) => 0m;
protected override void SaveEodSwapRecord(trade td, DateTime settleDate, DateTime preSettleDate)
{
// eod_swap 父记录由 FindEodSwapsByDate 从归档 eod 持仓派生,此处无需操作
}
protected override void ExecuteInTransaction(Action action) => action();
protected override void ClearSwapPositionsForCompose(trade td, DateTime tradeDate, List<int> eventTypes) { }
public override void ClearSwapPositions(trade td, DateTime valueDate, List<int> eventTypes, bool delAfter) { }
protected override swap_event AddSwapEvent(DateTime tradeDate, int swapTradeId, int eventType, string data, int clientCashId, bool save, string reason)
{ return new swap_event { id = 1 }; }
// GetPreDealDate 默认命中 DB,覆写为安全早期日期
public override DateTime? GetPreDealDate(int tradeId, DateTime settleDate, List<int> eventTypes)
=> TradeStart;
// 利息计算 stub(本测试只关心 eod.ValueDate 递推,不关心利息金额)
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 posiLongNotionalValue, decimal posiShortNotionalValue,
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)
{
return positions.Select(p => new swap_flow_event
{
PositionId = p.id,
InterestPrincipal = p.InterestPrincipalFix,
InterestRate = 0.021111m,
FloatRate = 0m
}).ToList();
}
/// <summary>对指定日期收盘,并把生成的 eod / eod_swap 回填,供下一日递推。</summary>
public void RunClose(DateTime settleDate, DateTime preSettleDate, List<swap_flow_event> flowEvents)
{
_currentFlowEvents = flowEvents ?? new List<swap_flow_event>();
SwapPositionCompose(settleDate, preSettleDate, null);
// 回填:把本次持久化的 eod 并入 _eodPositions
foreach (var eod in PersistedPositions.Where(x => x.SwapTradeId == SwapTradeId))
{
if (!_eodPositions.Any(x => x.id == eod.id))
_eodPositions.Add(eod);
}
}
/// <summary>取预付金腿截至某日的最新 eodValueDate 最大)。</summary>
public eod_swap_position LatestPrepayEod()
=> _eodPositions
.Where(x => x.SwapTradeId == SwapTradeId && x.PositionId == PrepayInitialId)
.OrderByDescending(x => x.ValueDate)
.FirstOrDefault();
}
#endregion
#region 工厂方法
private static trade CreateTrade()
=> new trade
{
id = SwapTradeId, TradeNumber = "TRACE-PREPAY-001", ClientId = 10,
TradeType = "收益互换", TradeDate = TradeStart, StartDate = TradeStart,
ExerciseDate = TradeStart.AddYears(1), TradeStatus = "确认成交", ValidState = "Valid",
QuoteCurrency = "CNY", SettlementCurrency = "CNY", StructureType = "普通债券类收益互换",
OriginalStockEqvNotional = 100000, TradePrice = 0
};
private static trade_extend CreateExtend()
=> new trade_extend
{
TradeId = SwapTradeId,
ExtendJson = @"{""NeedOpenFee"":false,""AnnualDays"":365,""SettlementRules"":0,""Direction"":1,""FlowBookMode"":0,""InterestCalcMode"":""01""}"
};
private static swap_position CreateFloatPosition()
=> new swap_position
{
id = FloatId, SwapTradeId = SwapTradeId, PosiDirection = 1, PositionType = (int)PositionTypeFlag.Long,
UnderlyingCode = "220205.IB", UnderlyingInstrumentType = "TBonds",
ContractSize = 1m, CountRatio = 1m, IsInitial = true, Invalid = false,
PosiQuantity = 1000, PosiNotionalValue = 1000,
PosiNetPrice = 1.0050m, PosiGrossPrice = 1.0020m,
InterestDirection = 0
};
private static swap_position CreatePrepayInitial()
=> new swap_position
{
id = PrepayInitialId, SwapTradeId = SwapTradeId, PosiDirection = 0,
InterestDirection = (int)SwapDirectionEnum.收取,
InterestMode = (int)InterestModeEnum.初始预付金,
InterestPrincipalFix = 100000.23m, IsInitial = true, Invalid = false,
PosiStartDate = TradeStart, PosiMatuirityDate = TradeStart.AddYears(1),
InterestSwapInterval = "[]", InterestRateDefault = 0.021111m
};
private static swap_position CreatePrepayReal()
=> new swap_position
{
id = PrepayRealId, PositionId = PrepayInitialId, SwapTradeId = SwapTradeId, PosiDirection = 0,
InterestDirection = (int)SwapDirectionEnum.收取,
InterestMode = (int)InterestModeEnum.初始预付金,
InterestPrincipalFix = 60000.138m, IsInitial = false, Invalid = false
};
private static swap_flow_event CreatePartialCloseFlow(DateTime date)
=> new swap_flow_event
{
SwapTradeId = SwapTradeId, PositionId = FloatId,
EventType = (int)SwapEventTypeEnum.平仓,
Quantity = 400, EventDate = date, UnwindDate = date,
MarkClosePnl = 500m, CloseFee = 10m, DataState = (int)SwapFlowDateStateEnum.完成
};
#endregion
[TestMethod]
public void 收盘递推链_预付金腿ValueDate应从交易开始日逐日推进到部分平仓日8_4()
{
var td = CreateTrade();
var extend = CreateExtend();
var positions = new List<swap_position>
{
CreateFloatPosition(), CreatePrepayInitial(), CreatePrepayReal()
};
var eodPositions = new List<eod_swap_position>();
var eodSwaps = new List<eod_swap>();
var svc = new TraceEodService(
new List<trade> { td }, positions, eodPositions, eodSwaps,
new List<trade_extend> { extend });
// 从交易开始日逐日收盘:7.28 → 7.31(互换) → 8.3(重置) → 8.4(部分平仓)
var closes = new List<(DateTime settle, DateTime pre, List<swap_flow_event> flows)>
{
(TradeStart, TradeStart.AddDays(-1), new List<swap_flow_event>()),
(SwapDate, TradeStart, new List<swap_flow_event>
{
new swap_flow_event { SwapTradeId=SwapTradeId, PositionId=FloatId,
EventType=(int)SwapEventTypeEnum.互换, EventDate=SwapDate, UnwindDate=SwapDate,
DataState=(int)SwapFlowDateStateEnum.完成 }
}),
(ResetDate, SwapDate, new List<swap_flow_event>()),
(PartialDate, ResetDate, new List<swap_flow_event> { CreatePartialCloseFlow(PartialDate) }),
};
Console.WriteLine("=== 收盘递推链:预付金腿 eod.ValueDate 逐日追踪 ===");
foreach (var c in closes)
{
svc.RunClose(c.settle, c.pre, c.flows);
var prepay = svc.LatestPrepayEod();
var vd = prepay?.ValueDate ?? default;
Console.WriteLine($" 收盘 {c.settle:yyyy-MM-dd} → 预付金腿 eod.ValueDate={vd:yyyy-MM-dd} (TdInterestPrincipal={prepay?.TdInterestPrincipal})");
}
var finalPrepay = svc.LatestPrepayEod();
Assert.IsNotNull(finalPrepay, "预付金腿应有 eod 归档");
// 收盘链每天把每条腿 eod.ValueDate 设为当日收盘日;部分平仓日 8.4 收盘后应为 8.4
Assert.AreEqual(PartialDate, finalPrepay.ValueDate.Date,
"收盘链应把预付金腿 eod.ValueDate 正确推进到部分平仓日 8.4(证明收盘代码本身正确,系统 8.1 是递推链在 8.4 断了)");
Console.WriteLine($">> 结论:收盘链正确推进到 {finalPrepay.ValueDate:yyyy-MM-dd}" +
$"系统读到 8.1 说明该笔交易的预付金腿 eod 在 8.4 部分平仓日未被收盘链重新生成(收盘缺失/滞后),非利息公式 bug。");
}
}
}