- 新增 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)。
252 lines
14 KiB
C#
252 lines
14 KiB
C#
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<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>取预付金腿截至某日的最新 eod(ValueDate 最大)。</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。");
|
||
}
|
||
}
|
||
}
|