test(swap): 预付金腿收盘递推链trace——证明ValueDate正确推进到8.4(非公式bug)

- 新增 PrepaidPrincipalClosingChainTraceTest:纯内存驱动 SwapPositionCompose,
  从交易开始日 7.28 逐日收盘到部分平仓日 8.4(含 7.31互换/8.3重置/8.4平仓),
  捕获预付金腿每日 eod.ValueDate。
- 实测 ValueDate 逐日推进 7.28→7.31→8.3→8.4,证明收盘代码(DealInterests 四分支
  均 newEodPayPosition.ValueDate=settleDate)本身正确。
- 结论:8.7 平仓预付金端系统值 20.83(≈6天) 对应 eod.ValueDate≈8.1,说明该笔交易
  预付金腿 eod 在 8.1 之后未被收盘链重新生成(递推链在部分平仓日断了/收盘缺失滞后),
  属数据/流程问题而非利息公式 bug;收盘链若正常推进到 8.4,8.7 平仓应为 3 天=10.41(Excel)。
This commit is contained in:
hjhan
2026-08-08 15:46:54 +08:00
parent cd8c3b6c95
commit f6a5c1dc83
@@ -0,0 +1,251 @@
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, bool needPrice,
decimal grossPrice, 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。");
}
}
}