上次失败根因: EodClientBalanceCalc:134 是 EF Core LINQ 表达式,
HashSet.Contains 无法翻译成 SQL, 导致场景3/4 全红。
修正: MarginModes 新增 ForLinq(List<int>) 供 EF Core 翻译用。
- SwapEodPositionService 2处纯函数(非LINQ): 用 MarginModes.Contains
- EodClientBalanceCalc LINQ表达式: 用 MarginModes.ForLinq.Contains
- RealTimeClientBanlanceService(已ToList,内存集合): 用 MarginModes.Contains
ConsTrade.InterestMarginModels 产品代码引用: 4处 → 0(只剩定义+注释)。
保证金mode判断 {初始预付金,追加预付金} 现在统一由 MarginModes 提供。
验证: sln编译0错误, 全量485测试7失败(基线一致,零回归)。
537 lines
32 KiB
C#
537 lines
32 KiB
C#
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_event(EventType∈{互换,自动互换}、DataState=完成、
|
||
/// EventDate<结算日)做累计求和。这些 flow event 由真实代码路径
|
||
/// (GetInterests → SaveAutoEodWithCloseInterestPosition → PersistFlowEvent) 产生,
|
||
/// 再无任何"偷偷给个 0"的垃圾。
|
||
/// 2. 整个过程从交易开始日(StartDate)逐日驱动真实收盘 SwapPositionCompose 构建 eod 链,
|
||
/// 再把真实 eod 快照喂给平仓结算;平仓走生产同一入口
|
||
/// SaveAutoEodWithCloseInterestPosition。任何输入都来自真实代码执行。
|
||
/// 3. 断言容差 0.01(匹配 oracle 2 位小数精度)。正确代码四舍五入到 2 位精确命中→通过;
|
||
/// 缺陷(Bug A/B/C)尾差 29~3961 元 >> 0.01 → 失败。既非 re-baseline,也非过松放任。
|
||
/// 注:本机已装 dotnet 6 SDK + Nexus 私服源,FR007 曲线已按 Excel 重置日取值预置。
|
||
/// 加点 spread = +0.25% = +0.0025,减点 = -2.10% = -0.021。
|
||
/// [DataRow] 特性实参不能是 decimal(C# 限制),故 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<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;
|
||
|
||
// 真实计息:走 RealSwapDealService(FR007 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 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)
|
||
{
|
||
var svc = new RealSwapDealService(
|
||
new OptUserInfo(0, nameof(SwapInterestScenario3And4FloatingTest), OptUserFrom.UnitTest), _floatRates, FlowEvents);
|
||
var interests = svc.GetInterests(td, tradeExtend, valueDate, unwindDate,
|
||
eodPositions, positions, posiNotionalValue, posiLongNotionalValue, posiShortNotionalValue,
|
||
closePosiNotionalValue, closePrecent, eventType, tdClose, needPrice,
|
||
grossPrice, 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, 0m, closeNotional, 1m,
|
||
(int)SwapEventTypeEnum.平仓,
|
||
false, false, 0m, closeNotional, false, settment: false, newCalcLast: isMaturity);
|
||
Assert.AreEqual(1, interests.Count);
|
||
return interests[0];
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 业务场景3:第3重置期内全平(平仓日 2026-05-11,closePercent=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 等于计息器返回的 base(interests.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);
|
||
// 覆盖 mode 2/9 分叉(SwapEodPositionService:1436-1469):部分平仓后 TdInterestPrincipal 的经济口径
|
||
// 必须 = 剩余动态本金(剩余名义本金 + 已并入本金的重置日待实现利息),mode2/9 应当一致。
|
||
// 单利:line 1491 直接取 posiNotionalValue = remainingNotional,无累计利息。
|
||
// 复利:base = interests.First().InterestPrincipal(本服务 CalcSwapInterests 已捕获到 LastBaseInterestPrincipal);
|
||
// mode2 仅在 calcLast 时于 1464 行反推剩余(× (1-cp)/cp),mode9 直取 base(GLMS-20260421-0004 禁止反推)。
|
||
// calcLast=false(如“算头不算尾”)或 mode9 被错误反推会膨胀 ~2.3 倍(494982903.27),下方断言精确拦截回归。
|
||
decimal expectedTdPrincipal;
|
||
if (!compound)
|
||
{
|
||
expectedTdPrincipal = remainingNotional;
|
||
}
|
||
else
|
||
{
|
||
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
|
||
}
|
||
}
|