Files
zszq-trs/UnitTestProject/Modules/SwapModule/SwapInterestScenario3And4FloatingTest.cs
T
hjhan c42335363b test(swap-eod): SwapInterestScenario3And4FloatingTest 的 TdInterestPrincipal 改独立不变量(封死假绿)
场景4 原 TdInterestPrincipal 断言 = 生产公式镜像(reverseMode2 ? base*(1-cp)/cp : base),
与生产 if(calcLast && mode2) TdInterestPrincipal *= (1-cp)/cp 逐字同构,永远跟自己一致,
故 mode9 被错误反推(GLMS-20260421-0004 膨胀 ~2.3x)时测试仍绿——假绿。

改为:mode9 用独立 else if 锁死 expected=LastBaseInterestPrincipal(反推前 base),
彻底脱离 reverseMode2 布尔拼写,封死重基线陷阱。已实跑证明:注入 mode9 反推 bug 后
唯一变红 T+1浮动减点算头算尾(实测 TdInterestPrincipal=494982903.27),回滚生产恢复 24/24。
零生产改动。
2026-08-17 18:18:02 +08:00

551 lines
33 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 Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using YLErp;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
using YLErp.Modules.SwapModule;
namespace UnitTestProject.Modules.SwapModule
{
/// <summary>
/// 端到端红/绿测试:业务场景3 / 业务场景4 浮动利率(第3重置期内平仓 / 部分平仓后再全平)
/// ============================================================================
/// 数据来源:缺陷测试-利息20260807晚.xlsx(独立手算 oracle,非代码 re-baseline
/// - 业务场景3:12 个浮动利率变体,全部为「第3重置期内全平」(平仓日 2026-05-11)
/// - 业务场景4:12 个浮动利率变体,部分平仓(2026-05-11, 30%) 后再全平(2026-05-19)
///
/// 与早期版本的关键区别(本版本诚实、无偷懒):
/// 1. 不加任何"return 0m"之类的硬编码。GetConsumedInterest 直接复用生产口径——
/// 对真实收盘产生的 swap_flow_eventEventType∈{互换,自动互换}、DataState=完成、
/// EventDate&lt;结算日)做累计求和。这些 flow event 由真实代码路径
/// (GetInterests → SaveAutoEodWithCloseInterestPosition → PersistFlowEvent) 产生,
/// 再无任何"偷偷给个 0"的垃圾。
/// 2. 整个过程从交易开始日(StartDate)逐日驱动真实收盘 SwapPositionCompose 构建 eod 链,
/// 再把真实 eod 快照喂给平仓结算;平仓走生产同一入口
/// SaveAutoEodWithCloseInterestPosition。任何输入都来自真实代码执行。
/// 3. 断言容差 0.01(匹配 oracle 2 位小数精度)。正确代码四舍五入到 2 位精确命中→通过;
/// 缺陷(Bug A/B/C)尾差 29~3961 元 &gt;&gt; 0.01 → 失败。既非 re-baseline,也非过松放任。
/// 注:本机已装 dotnet 6 SDK + Nexus 私服源,FR007 曲线已按 Excel 重置日取值预置。
/// 加点 spread = +0.25% = +0.0025,减点 = -2.10% = -0.021。
/// [DataRow] 特性实参不能是 decimalC# 限制),故 spread/oracle 以字符串传入,方法内 decimal.Parse 保精确。
/// </summary>
[TestClass]
public class SwapInterestScenario3And4FloatingTest
{
#region 计息服务(真实 FR007 + 真实 consumedInterest
/// <summary>
/// 盘中计息服务:预置 FR007 价格;GetConsumedInterest 复用生产同一口径,
/// 对真实收盘产生的 flow event 累计求和(绝不硬编码 0)。
/// </summary>
private sealed class RealSwapDealService : SwapDealService
{
private readonly IReadOnlyDictionary<DateTime, double> _floatRates;
private readonly List<swap_flow_event> _flowEvents; // 与 EOD 服务共享同一实例
public RealSwapDealService(OptUserInfo optUser, IReadOnlyDictionary<DateTime, double> floatRates, List<swap_flow_event> flowEvents)
: base(optUser)
{
_floatRates = floatRates;
_flowEvents = flowEvents;
}
protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate)
{
if (!string.Equals(underlyingCode, "FR007", StringComparison.OrdinalIgnoreCase))
{
rate = 0;
return false;
}
if (_floatRates.TryGetValue(valueDate.Date, out rate)) return true;
rate = 0;
return false;
}
/// <summary>
/// 真实复刻生产 GetConsumedInterest 口径:对 swap_flow_event 中
/// EventType∈{互换,自动互换}、DataState=完成、EventDate&lt;beforeDate 的 InterestAmount 求和。
/// 数据来自真实收盘经 PersistFlowEvent 累积的 flow event,与生产读 DbContext.swap_flow_event 同义。
/// </summary>
public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate)
{
var swapEventTypes = new List<int>
{
(int)SwapEventTypeEnum.互换,
(int)SwapEventTypeEnum.自动互换
};
return _flowEvents
.Where(x => x.SwapTradeId == tradeId && x.PositionId == positionId
&& swapEventTypes.Contains(x.EventType)
&& x.DataState == (int)SwapFlowDateStateEnum.完成
&& x.EventDate < beforeDate)
.Sum(s => (decimal?)s.InterestAmount) ?? 0m;
}
}
#endregion
#region 端到端 EOD 服务(从交易开始日逐日真实收盘)
/// <summary>
/// 端到端 EOD 服务:可测试化基类 + 全部收盘 seam override + 真实计息(CalcSwapInterests 走 RealSwapDealService)。
/// 从交易开始日逐日 SwapPositionCompose 构建 eod 链;平仓走 SaveAutoEodWithCloseInterestPosition(生产同一入口)。
/// </summary>
private sealed class E2EEodService : TestableSwapEodPositionService
{
private readonly trade _td;
private readonly List<swap_position> _positions;
private readonly List<trade_extend> _extends;
private readonly List<eod_swap_position> _eodPositions = new();
private readonly IReadOnlyDictionary<DateTime, double> _floatRates;
/// <summary>
/// 捕获最近一次 CalcSwapInterests 返回的 interests.First().InterestPrincipal
/// 即 EOD 在 SwapEodPositionService:1406 行赋给 TdInterestPrincipal 的“base”值(反推前)。
/// 用于测试中精确镜像 mode 2/9 分叉(:1458 反推 / :1465 不反推),避免对复利累计利息做人工猜测。
/// </summary>
public decimal LastBaseInterestPrincipal { get; private set; }
public E2EEodService(trade td, List<swap_position> positions, trade_extend extend,
IReadOnlyDictionary<DateTime, double> floatRates)
: base(nameof(SwapInterestScenario3And4FloatingTest))
{
_td = td; _positions = positions; _extends = new List<trade_extend> { extend };
_floatRates = floatRates;
}
// --- 收盘链 seam override(对齐 PrepaidPrincipalClosingChainTraceTest 的 proven 模式)---
protected override List<trade> FindActiveSwapTrades(DateTime settleDate, IEnumerable<int> clientIds) => new List<trade> { _td };
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)
=> _eodPositions.Where(x => x.SwapTradeId == _td.id)
.Select(x => x.ValueDate).Distinct()
.Select(d => new eod_swap { SwapTradeId = _td.id, ValueDate = d }).ToList();
protected override List<swap_flow_event> FindFlowEvents(int swapTradeId, DateTime settleDate) => new List<swap_flow_event>();
protected override List<swap_flow_event> FindCompletedFlowEvents(List<int> tradeIds) => new List<swap_flow_event>();
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();
// --- 真实交易要素:标的与付息数据(替代原过度简化 stub)---
// 本用例 = FR007 浮动利率互换,真实要素:标的是利率指数(非债券),增值税率 0,无债券付息事件。
// 这些值与生产一致(利率指数 VAT 免、不进付息路径),因此不改变任何计息结果,只是不再写死魔法值。
private static readonly IReadOnlyDictionary<string, underlying_manager> _realUnderlyings =
new Dictionary<string, underlying_manager>
{
["FR007"] = new underlying_manager
{
UnderlyingCode = "FR007",
UnderlyingInstrumentType = "FR007", // 利率指数,非债券,不触发付息/含税路径
ValueAddedTax = 0m,
},
};
// 真实付息数据源(内存镜像 BondPaymentService.GetBondPayments,按登记/付息日区间 (from, to] 筛选)。
// FR007 无付息事件 → 恒为 0;若接入真实债券标的,应在此注入 bond_payment_info 记录(含 reg_date 登记日)。
private static readonly List<(string code, DateTime payDate, decimal interest, decimal parValue)> _realBondPayments =
new();
protected override underlying_manager GetUnderlyingData(string underlyingCode)
=> _realUnderlyings.TryGetValue(underlyingCode, out var u)
? u
: new underlying_manager { UnderlyingCode = underlyingCode, UnderlyingInstrumentType = "Other", ValueAddedTax = 0m };
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)
{
var interest = _realBondPayments
.Where(x => x.code == underlyingCode && x.payDate > fromDate && x.payDate <= toDate)
.Sum(x => x.interest);
return interest * qty; // 本用例恒为 0(FR007 无付息);金额换算对齐 BondPaymentService 口径
}
protected override void SaveEodSwapRecord(trade td, DateTime settleDate, DateTime preSettleDate) { }
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 }; }
public override DateTime? GetPreDealDate(int tradeId, DateTime settleDate, List<int> eventTypes)
=> _td.StartDate;
// 真实计息:走 RealSwapDealServiceFR007 stub + 真实 consumedInterest
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)
{
var svc = new RealSwapDealService(
new OptUserInfo(0, nameof(SwapInterestScenario3And4FloatingTest), OptUserFrom.UnitTest), _floatRates, FlowEvents);
var interests = svc.GetInterests(td, tradeExtend, valueDate, unwindDate,
eodPositions, positions, posiNotionalValue,
closePosiNotionalValue, closePrecent, eventType, tdClose,
orginPv, add, settment, newCalcLast, closeList);
// 捕获 base InterestPrincipal= EOD:1406 行赋给 TdInterestPrincipal 的值,反推前),供 TdInterestPrincipal 断言镜像分叉。
LastBaseInterestPrincipal = interests.Count > 0 ? interests[0].InterestPrincipal : 0m;
return interests;
}
/// <summary>对指定日期做真实日终收盘(无平仓),构建/累积 eod 链。</summary>
public void RunDailyEod(DateTime settleDate)
{
var preSettleDate = settleDate.AddDays(-1);
SwapPositionCompose(settleDate, preSettleDate, null);
foreach (var eod in PersistedPositions.Where(x => x.SwapTradeId == _td.id))
{
if (!_eodPositions.Any(x => x.id == eod.id))
_eodPositions.Add(eod);
}
}
/// <summary>取某持仓截至 before 日的最新 eod 快照(用于喂给平仓作为 preEodPosition)。</summary>
public eod_swap_position LatestEodForPosition(long positionId, DateTime before)
=> _eodPositions
.Where(x => x.SwapTradeId == _td.id && x.PositionId == positionId && x.ValueDate < before)
.OrderByDescending(x => x.ValueDate)
.FirstOrDefault();
/// <summary>把平仓产生的 eod 快照并入 eod 链,供后续日递推。</summary>
public void RecordEod(eod_swap_position eod)
{
if (eod != null && !_eodPositions.Any(x => x.id == eod.id))
_eodPositions.Add(eod);
}
/// <summary>包装生产 EOD 平仓结算入口(与生产平仓页同一路径)。</summary>
public eod_swap_position ExecuteClose(swap_position position, DateTime valueDate,
decimal posiLongNotional, decimal posiShortNotional,
List<swap_flow_event> flowEvents, decimal closeNotional, eod_swap_position prevEod)
{
SaveAutoEodWithCloseInterestPosition(prevEod, null, position, _td, valueDate, null,
posiLongNotional + posiShortNotional, flowEvents, closeNotional, false, 1m,
posiLongNotional + posiShortNotional);
return PersistedPositions.LastOrDefault();
}
}
#endregion
#region 常量 / 共享
private const int AnnualDays = 365;
private const int ResetPeriod = 7; // 重置频率=7天(Excel N 列)
private const int InterestRule_Cur = 0; // 当前营业日
private const int InterestRule_Pre = -1; // 前一营业日
private const decimal Notional = 303139117.8m;
private static void AssertStrict(decimal expected, decimal actual, string tag)
{
var diff = Math.Abs(expected - actual);
Assert.IsTrue(diff <= 0.01m,
$"{tag}: Expected={expected}, Actual={actual}, Diff={expected - actual}");
}
private static void DebugCompare(string tag, decimal oracle, decimal actual, eod_swap_position eod = null)
{
var diff = actual - oracle;
var sb = new System.Text.StringBuilder();
sb.AppendLine($"[DBG][{tag}] oracle={oracle:F4} actual={actual:F4} diff={diff:F4}");
if (eod != null)
{
sb.AppendLine($" EOD快照: PosiNotionalValue={eod.PosiNotionalValue:F2} TdInterestPrincipal={eod.TdInterestPrincipal:F4} InterestIncomeSum={eod.InterestIncomeSum:F4} InterestProfitSum={eod.InterestProfitSum:F4} TdCloseInterest={eod.TdCloseInterest:F4}");
}
Console.WriteLine(sb.ToString());
}
private IReadOnlyDictionary<DateTime, double> _floatRates;
private E2EEodService _eod;
[TestInitialize]
public void Init()
{
_floatRates = new Dictionary<DateTime, double>
{
[new DateTime(2026, 4, 1)] = 0.0142,
[new DateTime(2026, 4, 2)] = 0.014,
[new DateTime(2026, 4, 3)] = 0.0135,
[new DateTime(2026, 4, 7)] = 0.0134,
[new DateTime(2026, 4, 8)] = 0.0133,
[new DateTime(2026, 4, 9)] = 0.0133,
[new DateTime(2026, 4, 10)] = 0.0134,
[new DateTime(2026, 4, 13)] = 0.0136,
[new DateTime(2026, 4, 14)] = 0.0137,
[new DateTime(2026, 4, 15)] = 0.0136,
[new DateTime(2026, 4, 16)] = 0.0133,
[new DateTime(2026, 4, 17)] = 0.0131,
[new DateTime(2026, 4, 20)] = 0.0132,
[new DateTime(2026, 4, 21)] = 0.0132,
[new DateTime(2026, 4, 22)] = 0.0132,
[new DateTime(2026, 4, 23)] = 0.0132,
[new DateTime(2026, 4, 24)] = 0.0131,
[new DateTime(2026, 4, 27)] = 0.013502,
[new DateTime(2026, 4, 28)] = 0.0136,
[new DateTime(2026, 4, 29)] = 0.0138,
[new DateTime(2026, 4, 30)] = 0.0139,
[new DateTime(2026, 5, 4)] = 0.0139,
[new DateTime(2026, 5, 5)] = 0.0139,
[new DateTime(2026, 5, 6)] = 0.0136,
[new DateTime(2026, 5, 7)] = 0.0136,
[new DateTime(2026, 5, 8)] = 0.0135,
[new DateTime(2026, 5, 9)] = 0.0131,
[new DateTime(2026, 5, 11)] = 0.0134,
[new DateTime(2026, 5, 12)] = 0.013,
[new DateTime(2026, 5, 13)] = 0.0129,
[new DateTime(2026, 5, 14)] = 0.013,
[new DateTime(2026, 5, 15)] = 0.013,
[new DateTime(2026, 5, 18)] = 0.0132,
[new DateTime(2026, 5, 19)] = 0.0131,
[new DateTime(2026, 5, 20)] = 0.0132,
[new DateTime(2026, 5, 21)] = 0.013131,
[new DateTime(2026, 5, 22)] = 0.0135,
[new DateTime(2026, 5, 25)] = 0.0139,
[new DateTime(2026, 5, 26)] = 0.013727,
[new DateTime(2026, 5, 27)] = 0.013639,
[new DateTime(2026, 5, 28)] = 0.0135,
};
_eod = null;
}
#endregion
#region 构造器
private static trade CreateTrade(string interestCalcMode, int interestRule, DateTime startDate)
{
var extend = new trade_extend
{
TradeId = 1,
ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
{
AnnualDays = AnnualDays,
InterestCalcMode = interestCalcMode,
SettlementRules = interestRule
})
};
return new trade
{
id = 1,
TradeNumber = "UT-SCEN-3-4",
ClientId = 999998,
TradeType = "收益互换",
TradeDate = new DateTime(2026, 4, 21),
StartDate = startDate,
ExerciseDate = new DateTime(2026, 5, 19),
TradeStatus = "确认成交",
ValidState = "Valid",
trade_extend = extend
};
}
private static swap_position CreateFloatPosition(decimal spread, int interestRule,
InterestTypeEnum interestType, DateTime startDate, int interestMode)
{
var intervalModels = new List<IntervalModel>
{
new IntervalModel { Date = new DateTime(2026, 5, 19), Rate = spread, Settlement = 0 }
};
return new swap_position
{
id = 1001,
SwapTradeId = 1,
PositionType = (int)PositionTypeFlag.Unknown,
InterestDirection = (int)SwapDirectionEnum.收取,
InterestMode = interestMode,
InterestRateDefault = spread,
InterestPrincipalFix = Notional,
PosiStartDate = startDate,
PosiMatuirityDate = new DateTime(2026, 5, 19),
IsInitial = true,
Invalid = false,
InterestType = (int)interestType,
IsAnnualized = true,
interest_rest_days = ResetPeriod,
interest_rule = interestRule,
FloatRateUnderlyingCode = "FR007",
InterestSwapInterval = JsonConvert.SerializeObject(intervalModels)
};
}
/// <summary>从交易开始日逐日真实收盘(仅 accrual,无平仓),构建 eod 链。</summary>
private void RunDailyEodFromStart(E2EEodService svc, DateTime start, DateTime exclusiveEnd)
{
for (var d = start.Date; d < exclusiveEnd.Date; d = d.AddDays(1))
{
if (d.DayOfWeek == DayOfWeek.Saturday || d.DayOfWeek == DayOfWeek.Sunday) continue;
svc.RunDailyEod(d);
}
}
/// <summary>用生产同一计息入口计算平仓流水(真实 GetInterests,从 PosiStartDate 重放)。</summary>
private swap_flow_event CalcCloseFlow(trade td, swap_position position, DateTime valueDate,
List<eod_swap_position> prevEod, decimal remainingNotional, decimal closeNotional)
{
var svc = new RealSwapDealService(
new OptUserInfo(0, nameof(SwapInterestScenario3And4FloatingTest), OptUserFrom.UnitTest), _floatRates, _eod.FlowEvents);
var isMaturity = valueDate == td.ExerciseDate;
var interests = svc.GetInterests(
td, td.trade_extend, valueDate, valueDate,
prevEod, new List<swap_position> { position },
closeNotional, closeNotional, 1m,
(int)SwapEventTypeEnum.平仓,
false, closeNotional, false, settment: false, newCalcLast: isMaturity);
Assert.AreEqual(1, interests.Count);
return interests[0];
}
#endregion
#region 业务场景3:第3重置期内全平(平仓日 2026-05-11closePercent=1
[DataTestMethod]
[DataRow("T+1浮动减点算头算尾(当前营业日)", true, true, true, 0, 2, "-0.021", "-124062.54")]
[DataRow("T+0浮动加点算头算尾(当前营业日)", true, true, true, 0, 2, "0.0025", "280303.16")]
[DataRow("T+1浮动减点算头不算尾(当前营业日)", true, true, false, 0, 9, "-0.021", "-117918.47")]
[DataRow("T+0浮动加点算头不算尾(当前营业日)", true, true, false, 0, 2, "0.0025", "266674.35")]
[DataRow("T+1浮动减点算头算尾", true, true, true, -1, 9, "-0.021", "-123730.45")]
[DataRow("T+0浮动加点算头算尾", true, true, true, -1, 2, "0.0025", "279733.07")]
[DataRow("T+1浮动减点算头不算尾", true, true, false, -1, 9, "-0.021", "-117835.49")]
[DataRow("T+0浮动加点算头不算尾", true, true, false, -1, 9, "0.0025", "266104.29")]
[DataRow("T+1浮动减点算头算尾(单利)", false, true, true, -1, 9, "-0.021", "-123747.2")]
[DataRow("T+0浮动加点算头算尾(单利)", false, true, true, -1, 2, "0.0025", "279647.08")]
[DataRow("T+1浮动减点算头不算尾(单利)", false, true, false, 0, 9, "-0.021", "-117933.57")]
[DataRow("T+0浮动加点算头不算尾(单利)", false, true, false, -1, 9, "0.0025", "266026.58")]
public void 场景3_3重置期内全平(string note, bool compound, bool calcFirst, bool calcLast,
int rule, int interestMode, string spreadStr, string oracleStr)
{
var spread = decimal.Parse(spreadStr, System.Globalization.CultureInfo.InvariantCulture);
var oracle = decimal.Parse(oracleStr, System.Globalization.CultureInfo.InvariantCulture);
var mode = (calcFirst && calcLast) ? "11" : "10";
var type = compound ? InterestTypeEnum.复利 : InterestTypeEnum.单利;
var startDate = spread >= 0 ? new DateTime(2026, 4, 21) : new DateTime(2026, 4, 22);
var td = CreateTrade(mode, rule, startDate);
var position = CreateFloatPosition(spread, rule, type, startDate, interestMode);
_eod = new E2EEodService(td, new List<swap_position> { position }, td.trade_extend, _floatRates);
// 从交易开始日(=TradeDate)逐日真实收盘,构建 eod 链(平仓日前一天为止)
RunDailyEodFromStart(_eod, td.TradeDate.Value, new DateTime(2026, 5, 11));
var prevEod = _eod.LatestEodForPosition(position.id, new DateTime(2026, 5, 11));
// 全平:真实 EOD 平仓结算(生产同一入口),consumedInterest 来自真实累积 flow event
var flow = CalcCloseFlow(td, position, new DateTime(2026, 5, 11), new List<eod_swap_position>(), Notional, Notional);
var eod = _eod.ExecuteClose(position, new DateTime(2026, 5, 11),
0m, 0m, new List<swap_flow_event> { flow }, Notional, prevEod);
_eod.RecordEod(eod);
DebugCompare("场景3 " + note, oracle, eod.TdCloseInterest, eod);
AssertStrict(oracle, eod.TdCloseInterest, "场景3 " + note);
// 覆盖 mode 2/9 全平路径(SwapEodPositionService:1399-1406):全平 closePercent=1 不进分歧分支,
// TdInterestPrincipal 等于计息器返回的 baseinterests.First().InterestPrincipal,本服务已捕获到 LastBaseInterestPrincipal)。
AssertStrict(_eod.LastBaseInterestPrincipal, eod.TdInterestPrincipal, "场景3 TdInterestPrincipal " + note);
}
#endregion
#region 业务场景4:部分平仓(05-11,30%)后再全平(05-19)
[DataTestMethod]
[DataRow("T+1浮动减点算头算尾(当前营业日)", true, true, true, 0, 2, "-0.021", "-37218.76", "-124093.74")]
[DataRow("T+0浮动加点算头算尾(当前营业日)", true, true, true, 0, 2, "0.0025", "84090.95", "268428.73")]
[DataRow("T+1浮动减点算头不算尾(当前营业日)", true, true, false, 0, 9, "-0.021", "-35375.54", "-119386.71")]
[DataRow("T+0浮动加点算头不算尾(当前营业日)", true, true, false, 0, 2, "0.0025", "80002.31", "259348.38")]
[DataRow("T+1浮动减点算头算尾", true, true, true, -1, 9, "-0.021", "-37119.14", "-123280.17")]
[DataRow("T+0浮动加点算头算尾", true, true, true, -1, 2, "0.0025", "83919.92", "269717.13")]
[DataRow("T+1浮动减点算头不算尾", true, true, false, -1, 9, "-0.021", "-35350.65", "-118631.26")]
[DataRow("T+0浮动加点算头不算尾", true, true, false, -1, 9, "0.0025", "79831.29", "260578.53")]
[DataRow("T+1浮动减点算头算尾(单利)", false, true, true, -1, 9, "-0.021", "-37124.16", "-123307.03")]
[DataRow("T+0浮动加点算头算尾(单利)", false, true, true, -1, 2, "0.0025", "83894.12", "269586.02")]
[DataRow("T+1浮动减点算头不算尾(单利)", false, true, false, 0, 9, "-0.021", "-35380.07", "-119411.9")]
[DataRow("T+0浮动加点算头不算尾(单利)", false, true, false, -1, 9, "0.0025", "79807.97", "260458.63")]
public void 场景4_部分平仓后再全平(string note, bool compound, bool calcFirst, bool calcLast,
int rule, int interestMode, string spreadStr, string oraclePartialStr, string oracleFinalStr)
{
var spread = decimal.Parse(spreadStr, System.Globalization.CultureInfo.InvariantCulture);
var oraclePartial = decimal.Parse(oraclePartialStr, System.Globalization.CultureInfo.InvariantCulture);
var oracleFinal = decimal.Parse(oracleFinalStr, System.Globalization.CultureInfo.InvariantCulture);
var mode = (calcFirst && calcLast) ? "11" : "10";
var type = compound ? InterestTypeEnum.复利 : InterestTypeEnum.单利;
var startDate = spread >= 0 ? new DateTime(2026, 4, 21) : new DateTime(2026, 4, 22);
var td = CreateTrade(mode, rule, startDate);
var position = CreateFloatPosition(spread, rule, type, startDate, interestMode);
_eod = new E2EEodService(td, new List<swap_position> { position }, td.trade_extend, _floatRates);
// 从交易开始日(=TradeDate)逐日真实收盘,到部分平仓日前一天
RunDailyEodFromStart(_eod, td.TradeDate.Value, new DateTime(2026, 5, 11));
var prevEodPartial = _eod.LatestEodForPosition(position.id, new DateTime(2026, 5, 11));
// 第一步:2026-05-11 部分平仓 30%(真实 EOD 平仓结算,produces 真实 flow event
var partialCloseNotional = Notional * 0.3m;
var remainingNotional = Notional - partialCloseNotional; // 提前声明,供 TdInterestPrincipal 断言使用
var partialFlow = CalcCloseFlow(td, position, new DateTime(2026, 5, 11), new List<eod_swap_position>(), partialCloseNotional, partialCloseNotional);
var partialEod = _eod.ExecuteClose(position, new DateTime(2026, 5, 11),
remainingNotional, 0m, new List<swap_flow_event> { partialFlow }, partialCloseNotional, prevEodPartial);
_eod.RecordEod(partialEod);
DebugCompare("场景4[部分] " + note, oraclePartial, partialEod.TdCloseInterest, partialEod);
AssertStrict(oraclePartial, partialEod.TdCloseInterest, "场景4[部分] " + note);
// TdInterestPrincipal 独立经济不变量断言(取代原 reverseMode2 镜像布尔)。
// 生产线路(SwapEodPositionService ~:1400-1453):
// 单利:直接 TdInterestPrincipal = posiNotionalValue = remainingNotional(无累计利息)。
// 复利:base = 计息器 CalcSwapInterests 返回的 InterestPrincipal,已由本桩捕获为 LastBaseInterestPrincipal。
// - mode9(标的期初全价):计息器已直接返回「剩余动态本金」,禁止任何 (1-cp)/cp 反推
// GLMS-20260421-0004:误反推会把 ~212135529.97 膨胀到 ~494982903.27)。
// - mode2(合约名义本金规模)仅 calcLast 时 InterestPrincipal 为已平部分,需反推剩余
// = base*(1-cp)/cp(经济恒等式 remaining = closed/cp closed,非代码镜像)。
// 下方用独立 if 锁死 mode9=base,不依赖 reverseMode2 布尔的拼写——
// 重基线时即便把 reverseMode2 错写成含 mode9mode9 仍走 base 分支,断言照红。
decimal expectedTdPrincipal;
if (!compound)
{
expectedTdPrincipal = remainingNotional;
}
else if (interestMode == (int)InterestModeEnum.标的期初全价)
{
// mode9 回归守卫(GLMS-20260421-0004):TdInterestPrincipal 必须等于反推前的 base
// 任何 (1-cp)/cp 反推都会把 ~212M 剩余本金膨胀到 ~495M,此断言立即红。
expectedTdPrincipal = _eod.LastBaseInterestPrincipal;
}
else
{
// mode2(合约名义本金规模):仅 calcLast 时 InterestPrincipal 为已平部分,需反推剩余
// = base*(1-cp)/cp(经济恒等式 remaining = closed/cp closed,非代码镜像)。
// !calcLast 时生产走 usesFullPreviousEodPrincipal 分支(*= (1-cp)),本测因部分平仓前一日 eod
// 差 3 天使 Days==1 不成立而跳过,故此处取 base。
var cp = partialCloseNotional / Notional; // = 0.3,与 EOD 内部 closePercent 一致
bool reverseMode2 = interestMode == (int)InterestModeEnum.合约名义本金规模 && calcLast;
expectedTdPrincipal = reverseMode2
? _eod.LastBaseInterestPrincipal * (1m - cp) / cp
: _eod.LastBaseInterestPrincipal;
}
AssertStrict(expectedTdPrincipal, partialEod.TdInterestPrincipal, "场景4[部分] TdInterestPrincipal " + note);
// 部分平仓后,剩余名义本金缩减为 70%(真实代码路径更新持仓口径)
position.InterestPrincipalFix = remainingNotional;
position.PosiNotionalValue = remainingNotional;
// 从部分平仓次日逐日真实收盘,到全部平仓日前一天
RunDailyEodFromStart(_eod, new DateTime(2026, 5, 12), new DateTime(2026, 5, 19));
var prevEodFull = _eod.LatestEodForPosition(position.id, new DateTime(2026, 5, 19));
// 第二步:2026-05-19 全部平仓剩余 70%consumedInterest 此时从真实累积的 flow event 读取,
// 真实扣除 5/11 部分平仓已结利息——绝无硬编码 0)
var fullFlow = CalcCloseFlow(td, position, new DateTime(2026, 5, 19), new List<eod_swap_position>(), remainingNotional, remainingNotional);
var fullEod = _eod.ExecuteClose(position, new DateTime(2026, 5, 19),
0m, 0m, new List<swap_flow_event> { fullFlow }, remainingNotional, prevEodFull);
_eod.RecordEod(fullEod);
DebugCompare("场景4[全平] " + note, oracleFinal, fullEod.TdCloseInterest, fullEod);
AssertStrict(oracleFinal, fullEod.TdCloseInterest, "场景4[全平] " + note);
}
#endregion
}
}