refactor(swap-eod): 利息腿分派可读性收口——观察日命名统一+命名参数+查找助手+分派规格纯函数(零行为变更)
- insterval/autoInterval → observationInterval,SwapIntervalList→FindObservationInterval→形参 interval 全链同名 - :512/:525 调用点命名参数 autoSwap: true/false,签名未动 - 抽取 FindObservationInterval 消除两处重复观察日谓词(表达式逐字保留,null 语义不变) - 新增 InterestEodScenario 枚举 + ResolveInterestScenario 影子纯函数 + 8 组合表驱动 InterestEodScenarioDispatchTest(分派结构未接线,生产执行路径不变) - 编译 0 错误;表驱动 8/8 通过
This commit is contained in:
@@ -447,7 +447,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// <summary>
|
||||
/// [DI_BRANCH_001] 普通日(无互换无平仓无观察日)→ 走 copy 分支
|
||||
/// ---------------------------------------------------------------
|
||||
/// flowEvents 为空,insterval=null,hasSwap=false,hasClose=false
|
||||
/// flowEvents 为空,observationInterval=null,hasSwap=false,hasClose=false
|
||||
/// → 应走 SaveEodInterestPositionCopy(cs:338)
|
||||
/// ---------------------------------------------------------------
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// DealInterests 分派优先级(TEST-MATRIX §6 空洞补盖)。
|
||||
/// 纯函数 ResolveInterestScenario 的 8 组合表驱动单测,不连库、无 DB。
|
||||
/// 守卫:手工互换压制观察日自动结息;观察日±平仓区分 autoSwap 真/假;纯平仓与普通日分流。
|
||||
/// 测试类直接继承 TestableSwapEodPositionService 以访问 protected 的枚举与方法。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class InterestEodScenarioDispatchTest : TestableSwapEodPositionService
|
||||
{
|
||||
public InterestEodScenarioDispatchTest() : base(nameof(InterestEodScenarioDispatchTest)) { }
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(false, false, false, InterestEodScenario.RollForward)] // 普通日
|
||||
[DataRow(false, false, true, InterestEodScenario.CloseOnly)] // 纯平仓(非观察日)
|
||||
[DataRow(false, true, false, InterestEodScenario.ManualSwap)] // 手工互换(非观察日)
|
||||
[DataRow(false, true, true, InterestEodScenario.ManualSwap)] // 手工互换+平仓 → 手工优先
|
||||
[DataRow(true, false, false, InterestEodScenario.AutoSettle)] // 观察日, 无平仓
|
||||
[DataRow(true, false, true, InterestEodScenario.AutoSettleWithClose)] // 观察日+平仓 (autoSwap=true)
|
||||
[DataRow(true, true, false, InterestEodScenario.ManualSwap)] // 观察日+手工互换 → 手工优先
|
||||
[DataRow(true, true, true, InterestEodScenario.ManualSwap)] // 观察日+手工互换+平仓 → 手工优先
|
||||
public void ResolveInterestScenario_CoversAllEightCombinations(
|
||||
bool hasInterval, bool hasSwap, bool hasClose, InterestEodScenario expected)
|
||||
{
|
||||
var actual = ResolveInterestScenario(hasInterval, hasSwap, hasClose);
|
||||
Assert.AreEqual(expected, actual,
|
||||
$"hasInterval={hasInterval}, hasSwap={hasSwap}, hasClose={hasClose}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -371,16 +371,16 @@ namespace YLErp.Modules.SwapModule
|
||||
var grossPrice = curEodPosis.Where(x => x.PosiDirection > 0).FirstOrDefault()?.PosiGrossPrice ?? 0;
|
||||
//处理利息腿
|
||||
DealInterests(interestList, eodPositions, todyEodPositions, settleDate, td, flowEvents, autoInterests, lastEodSwap, posiLongNotional + posiShortNotional, closePosiNotional, grossPrice, orginPv);
|
||||
//获取自动互换的 interval 信息,用于确定结算日期
|
||||
IntervalModel autoInterval = null;
|
||||
//获取自动互换的观察日信息,用于确定结算日期
|
||||
IntervalModel observationInterval = null;
|
||||
foreach (var interest in interestList)
|
||||
{
|
||||
autoInterval = interest.SwapIntervalList.FirstOrDefault(x => x.Date == settleDate && x.Settlement == 1);
|
||||
if (autoInterval != null)
|
||||
observationInterval = FindObservationInterval(interest, settleDate);
|
||||
if (observationInterval != null)
|
||||
break;
|
||||
}
|
||||
// 自动互换(仅利息/预付金,不含分红)
|
||||
DealAutoInterests(autoInterests, td, settleDate, preDealDate, posiLongNotional + posiShortNotional, autoInterval);
|
||||
DealAutoInterests(autoInterests, td, settleDate, preDealDate, posiLongNotional + posiShortNotional, observationInterval);
|
||||
// 分红独立处理:只要当天有债券需要分红,则生成分红自动互换,与利息互换无关
|
||||
DealDividends(curEodPosis, td, settleDate, tradeExtend);
|
||||
//多空组合判断是否已到到期日且无持仓信息
|
||||
@@ -490,18 +490,20 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
var eodPosition = eodPositions.FirstOrDefault(x => x.PositionId == interest.id);//上一日日终利息信息 可能不存在
|
||||
var tdEodPosition = todyEodPositions.FirstOrDefault(x => x.PositionId == interest.id);//当前结算日日终利息信息
|
||||
var insterval = interest.SwapIntervalList.FirstOrDefault(x => x.Date == settleDate && x.Settlement == 1);//自动互换观察日信息
|
||||
var observationInterval = FindObservationInterval(interest, settleDate);//自动互换观察日信息
|
||||
List<swap_flow_event> dealInterests = new List<swap_flow_event>();
|
||||
dealInterests.AddRange(flowEvents);
|
||||
var dealInterest = dealInterests.FirstOrDefault(n => n.PositionId == interest.id);//当日是否做过互换或平仓
|
||||
var swapEvents = flowEvents.Where(x => (x.EventType == (int)SwapEventTypeEnum.互换 || x.EventType == (int)SwapEventTypeEnum.平仓) && x.PositionId == interest.id).ToList();
|
||||
//如果当日有互换/当日有平仓 不再重新生成或更新
|
||||
Log.Info($"insterval is {insterval},hasSwap is {hasSwap},hasClose is {hasClose}");
|
||||
if (insterval != null && !hasSwap)
|
||||
Log.Info($"observationInterval is {observationInterval},hasSwap is {hasSwap},hasClose is {hasClose}");
|
||||
// 分派优先级与粒度说明见 ResolveInterestScenario;8 组合表驱动覆盖见 InterestEodScenarioDispatchTest。
|
||||
// 仅观察日两个分支把返回值收进 autoInterests(→资金记录)——分派错序=静默少结。
|
||||
if (observationInterval != null && !hasSwap)
|
||||
{
|
||||
if (!hasClose)//当日无平仓
|
||||
{
|
||||
var _autoInterests = SaveAutoEodInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, insterval, lastEodSwap, posiTotalNotional, grossPrice, orginPv);
|
||||
var _autoInterests = SaveAutoEodInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, observationInterval, lastEodSwap, posiTotalNotional, grossPrice, orginPv);
|
||||
if (_autoInterests.Count > 0)
|
||||
{
|
||||
autoInterests.AddRange(_autoInterests);
|
||||
@@ -509,7 +511,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
else
|
||||
{
|
||||
var _autoInterests = SaveAutoEodWithCloseInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, insterval, posiTotalNotional, swapEvents, closeNational, true, grossPrice, orginPv);
|
||||
var _autoInterests = SaveAutoEodWithCloseInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, observationInterval, posiTotalNotional, swapEvents, closeNational, autoSwap: true, grossPrice, orginPv);
|
||||
if (_autoInterests.Count > 0)
|
||||
{
|
||||
autoInterests.AddRange(_autoInterests);
|
||||
@@ -522,7 +524,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
else if (hasClose)
|
||||
{
|
||||
SaveAutoEodWithCloseInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, insterval, posiTotalNotional, swapEvents, closeNational, false, grossPrice, orginPv);
|
||||
SaveAutoEodWithCloseInterestPosition(eodPosition, tdEodPosition, interest, td, settleDate, observationInterval, posiTotalNotional, swapEvents, closeNational, autoSwap: false, grossPrice, orginPv);
|
||||
}
|
||||
else//无自动互换、互换/平仓,复制上一日终信息,并计算当日新增利息
|
||||
{
|
||||
@@ -530,6 +532,46 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 利息腿 EOD 归档场景分派(纯函数;表驱动单测见 InterestEodScenarioDispatchTest)。
|
||||
/// 优先级链(承重业务语义,不能乱序):
|
||||
/// ① hasSwap(手工互换) → 按事件流水重新生成,压制观察日自动结息
|
||||
/// ② observationInterval(观察日) 存在 → 自动结息;同日有平仓 → autoSwap=true
|
||||
/// ③ hasClose(纯平仓, 非观察日) → autoSwap=false
|
||||
/// ④ 普通日 → 复制上一日终并计提当日新增
|
||||
/// 注意:hasSwap/hasClose 是交易级标志(整笔合约当天有无事件),
|
||||
/// observationInterval 是腿级(本条利息腿当天是否观察日)——粒度不同,分派依赖此区分。
|
||||
/// </summary>
|
||||
public enum InterestEodScenario
|
||||
{
|
||||
ManualSwap, // ① 手工互换:压制观察日自动结息
|
||||
AutoSettleWithClose, // ② 观察日 + 当日平仓 (autoSwap=true)
|
||||
AutoSettle, // ② 观察日 + 当日无平仓
|
||||
CloseOnly, // ③ 非观察日 + 当日平仓 (autoSwap=false)
|
||||
RollForward, // ④ 普通日滚动
|
||||
}
|
||||
|
||||
protected static InterestEodScenario ResolveInterestScenario(bool hasInterval, bool hasSwap, bool hasClose)
|
||||
{
|
||||
if (hasSwap)
|
||||
return InterestEodScenario.ManualSwap;
|
||||
if (hasInterval)
|
||||
return hasClose ? InterestEodScenario.AutoSettleWithClose : InterestEodScenario.AutoSettle;
|
||||
if (hasClose)
|
||||
return InterestEodScenario.CloseOnly;
|
||||
return InterestEodScenario.RollForward;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找利息腿在指定结算日的观察日信息(SwapIntervalList 中 Date==settleDate 且 Settlement==1 的记录)。
|
||||
/// 观察日即自动结息触发日;返回 null 表示当日非观察日。分派见 ResolveInterestScenario。
|
||||
/// </summary>
|
||||
protected static IntervalModel FindObservationInterval(swap_position interest, DateTime settleDate)
|
||||
{
|
||||
return interest.SwapIntervalList.FirstOrDefault(x => x.Date == settleDate && x.Settlement == 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理浮动腿归档
|
||||
/// </summary>
|
||||
@@ -1322,7 +1364,7 @@ namespace YLErp.Modules.SwapModule
|
||||
var calcLast = tradeExtend?.InterestCalcMode?.EndsWith("1") ?? true;
|
||||
// 显式入口:平仓后剩余本金 + 实际平掉额 + 恒1全额结息(语义见 InterestCalcRequest.EodPostCloseSettle)。
|
||||
// 该组合触发 GetInterests 内共享计息器的模式2/9本金修正(见其"根因位置"注释,勿删)。
|
||||
// 恒1 重算的 InterestAmount 是不进结算现金流的中间值;系统端到端结算结果由 DI_EXCEL_SCENARIO4 家族对账确认书公式保障(最终全平=剩余额×∏利率,2026-08-18 手算复核)。改动本口径前必读该测试家族——任何破坏 ∏ 恒等式的调整都会被其拦截。
|
||||
// 恒1 重算的 InterestAmount 是结算现金流的直接输入(非无害中间值):系统端到端结算结果由 DI_EXCEL_SCENARIO4 家族对账确认书公式保障(最终全平=剩余额×∏利率,2026-08-18 手算复核)。改动本口径前必读该测试家族——任何破坏 ∏ 恒等式的调整都会被其拦截。
|
||||
// 口径选择常驻记录(快速定位第一入口):出问题先看这行确认当日本次事件的金额输入,再顺着
|
||||
// SwapCalcTrace 分段过程日志追计算;autoSwap=观察日结现路径。
|
||||
Log.Info($"[EOD平仓后收盘结息] tradeId={td.id} valueDate={valueDate:yyyy-MM-dd} autoSwap={autoSwap} " +
|
||||
|
||||
Reference in New Issue
Block a user