fix(eod): FR007收盘检查日按计息取价日回拨,抽出EodCheckFr007Price
- 检查日改用 IndexFixerBase.GetFixingDate(与计息取价同源): 交易日=当日(行为零变更),周末/节假日=上一交易日, 解除非交易日收盘"必报FR007未入库"死锁(2026-08-28:0523周六收盘) - 缺价报错附回拨说明与涉及FR007浮动的交易编号(上限20笔), 交易日缺价文案与旧版格式一致 - FR007检查从EodCheckSettlePrice抽出为独立检查服务EodCheckFr007Price, 报错沿用原步骤名;新增8个无库单测
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
using YLErp.Modules.EodModule.SettlementModule;
|
||||||
|
|
||||||
|
namespace YLErp.Modules.EodModule
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 收盘 FR007 定盘检查(EodCheckFr007Price)行为契约(纯内存,不连库):
|
||||||
|
/// ① 检查日回拨:交易日 = 收盘日自身(对既有交易日收盘行为零变更);
|
||||||
|
/// 周末/节假日 = 上一交易日——非交易日无 FR007 定盘,按裸收盘日检查会形成
|
||||||
|
/// "收盘必报缺价"死锁(2026-08-28:0523 周六收盘即此症);
|
||||||
|
/// ② 回拨规则与计息取价同源(IndexFixerBase.GetFixingDate),检查的日期
|
||||||
|
/// 就是计息路径实际会读的日期;
|
||||||
|
/// ③ 报错文案:交易日缺价保持旧格式,非交易日注明回拨,附交易编号供定位。
|
||||||
|
/// </summary>
|
||||||
|
[TestClass]
|
||||||
|
public class EodCheckFr007PriceTest
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void 检查日_交易日收盘_等于收盘日自身()
|
||||||
|
{
|
||||||
|
// 2026-05-22 周五、2026-08-28 周五:正常交易日收盘,行为必须与改造前一致
|
||||||
|
Assert.AreEqual(new DateTime(2026, 5, 22), EodCheckFr007Price.GetCheckDate(new DateTime(2026, 5, 22)));
|
||||||
|
Assert.AreEqual(new DateTime(2026, 8, 28), EodCheckFr007Price.GetCheckDate(new DateTime(2026, 8, 28)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void 检查日_周六收盘_回拨到上一交易日周五()
|
||||||
|
{
|
||||||
|
// 2026-08-28 缺陷场景:0523 为周六,FR007 非发布日永无定盘行,
|
||||||
|
// 旧逻辑检查裸收盘日必报"未入库"且无法通过补交易要素绕过
|
||||||
|
Assert.AreEqual(new DateTime(2026, 5, 22), EodCheckFr007Price.GetCheckDate(new DateTime(2026, 5, 23)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void 检查日_周日收盘_回拨跳过周末到周五()
|
||||||
|
{
|
||||||
|
// 与 GetInterestsUnitTest_T0 记录的取价回拨先例一致:GetNonHolidayDefore(4/26周日)→4/24周五
|
||||||
|
Assert.AreEqual(new DateTime(2026, 4, 24), EodCheckFr007Price.GetCheckDate(new DateTime(2026, 4, 26)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void 检查日_与计息取价日同源()
|
||||||
|
{
|
||||||
|
// 同一收盘日,检查日必须等于计息路径 interest_rule=0 腿实际读的取价日
|
||||||
|
var settleDate = new DateTime(2026, 5, 23);
|
||||||
|
var interestPathFixingDate = YLErp.Derivatives.Interest.IndexFixerBase.GetFixingDate(settleDate, 0);
|
||||||
|
Assert.AreEqual(interestPathFixingDate, EodCheckFr007Price.GetCheckDate(settleDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void 报错文案_交易日缺价_格式与旧版一致()
|
||||||
|
{
|
||||||
|
// 交易日缺价:不带回拨说明、无交易则不带交易段,保持"yyyy年MM月dd日FR007价格未入库"原格式
|
||||||
|
var msg = EodCheckFr007Price.BuildMissingMessage(new DateTime(2026, 5, 22), new DateTime(2026, 5, 22), Array.Empty<string>());
|
||||||
|
Assert.AreEqual("2026年05月22日FR007价格未入库", msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void 报错文案_非交易日回拨缺价_注明回拨()
|
||||||
|
{
|
||||||
|
var msg = EodCheckFr007Price.BuildMissingMessage(new DateTime(2026, 5, 23), new DateTime(2026, 5, 22), null);
|
||||||
|
StringAssert.Contains(msg, "2026年05月22日FR007价格未入库");
|
||||||
|
StringAssert.Contains(msg, "收盘日2026年05月23日为非交易日,已回拨至上一交易日检查");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void 报错文案_带交易编号供定位()
|
||||||
|
{
|
||||||
|
var msg = EodCheckFr007Price.BuildMissingMessage(
|
||||||
|
new DateTime(2026, 5, 22), new DateTime(2026, 5, 22),
|
||||||
|
new[] { "TRS-001", "TRS-002" });
|
||||||
|
StringAssert.EndsWith(msg, ";涉及FR007浮动的交易: TRS-001、TRS-002");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void 报错文案_交易超过上限_截断并以等N笔收尾()
|
||||||
|
{
|
||||||
|
var trades = Enumerable.Range(1, 25).Select(i => $"TRS-{i:D3}").ToArray();
|
||||||
|
var msg = EodCheckFr007Price.BuildMissingMessage(
|
||||||
|
new DateTime(2026, 5, 22), new DateTime(2026, 5, 22), trades);
|
||||||
|
StringAssert.Contains(msg, $"TRS-{EodCheckFr007Price.MaxTradesInMessage:D3}");
|
||||||
|
StringAssert.Contains(msg, "等25笔");
|
||||||
|
Assert.IsFalse(msg.Contains("TRS-021"), "超过上限的交易编号不应完整列出");
|
||||||
|
Assert.IsFalse(msg.Contains("TRS-025、"), "截断后不应再拼接后续编号");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using YLErp.Derivatives.Interest;
|
||||||
|
using YLErp.Modules.DataProviderModule;
|
||||||
|
|
||||||
|
namespace YLErp.Modules.EodModule.SettlementModule
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 收盘 FR007 定盘入库检查(独立检查服务,由 EodCheckSettlePrice 在
|
||||||
|
/// "检查标的结算价格缺失"步骤内编排调用——报错前缀沿用该步骤名,保持既有报错口径)。
|
||||||
|
///
|
||||||
|
/// 职责边界:只回答"计息会读的那个取价日,FR007 定盘行是否已入库"。
|
||||||
|
/// 日期推导与计息路径共用同一套日历函数(IndexFixerBase.GetFixingDate),
|
||||||
|
/// 保证检查口径 ≡ 取数口径。第二步完整版(按各浮动腿重置日推导所需日期集合 +
|
||||||
|
/// 批量校验 + Fr007FixingCache 复用)也落在本类,不回填 EodCheckSettlePrice。
|
||||||
|
/// </summary>
|
||||||
|
public class EodCheckFr007Price : EodSettleServiceBaseV2
|
||||||
|
{
|
||||||
|
/// <summary>报错里最多列出的交易编号数,超出以"等N笔"收尾,防刷屏。</summary>
|
||||||
|
internal const int MaxTradesInMessage = 20;
|
||||||
|
|
||||||
|
public EodCheckFr007Price(EodSettlementContextV2 context) : base(context)
|
||||||
|
{
|
||||||
|
ResetDbContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查结算日(回拨后的取价日)FR007 定盘是否入库;缺价抛 EodSettleException 终止收盘。
|
||||||
|
/// step 由调用方传入:本检查隶属于"检查标的结算价格缺失"步骤,报错前缀与其保持一致。
|
||||||
|
/// </summary>
|
||||||
|
public void Execute(string step)
|
||||||
|
{
|
||||||
|
var checkDate = GetCheckDate(_context.SettleDate);
|
||||||
|
if (!EodPriceQueryService.CheckFR007Price(checkDate))
|
||||||
|
{
|
||||||
|
var tradeNumbers = GetFloatTradeNumbers(); // 仅缺价时才查,正常收盘零开销
|
||||||
|
_context.RaiseError(step, BuildMissingMessage(_context.SettleDate, checkDate, tradeNumbers));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 收盘 FR007 检查日:与计息取价共用同一套日历函数(IndexFixerBase.GetFixingDate),
|
||||||
|
/// 保证"检查的日期"与"计息实际读的日期"始终一致。
|
||||||
|
///
|
||||||
|
/// 回拨发生在日历层(GetNonHolidayDefore),与 interest_rule 取值无关:
|
||||||
|
/// 交易日 → 当日自身(工作日收盘行为零变更);
|
||||||
|
/// 周末/节假日 → 上一交易日(非交易日无定盘,按裸收盘日检查会形成
|
||||||
|
/// "非交易日收盘必报缺价"的死锁——2026-08-28 缺陷:0523 周六收盘
|
||||||
|
/// 报"0523 FR007未入库",且检查只看收盘日不看任何交易,改交易要素无法绕过)。
|
||||||
|
/// 抽成纯函数便于无数据库单测。
|
||||||
|
/// </summary>
|
||||||
|
internal static DateTime GetCheckDate(DateTime settleDate)
|
||||||
|
=> IndexFixerBase.GetFixingDate(settleDate.Date, 0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 组装 FR007 缺价报错文案(纯函数便于单测):
|
||||||
|
/// 显示实际检查日(= 计息会读的取价日);收盘日为非交易日时注明已回拨;
|
||||||
|
/// 附涉及 FR007 浮动的交易编号供定位——注意这是"涉及"而非"精确需要该日定盘"
|
||||||
|
/// (精确归因需按各腿重置日推导,属第二步完整版改造,此处宁多报不漏报)。
|
||||||
|
/// </summary>
|
||||||
|
internal static string BuildMissingMessage(DateTime settleDate, DateTime checkDate, IReadOnlyList<string> fr007TradeNumbers)
|
||||||
|
{
|
||||||
|
var rollbackNote = checkDate != settleDate.Date
|
||||||
|
? $"(收盘日{settleDate:yyyy年MM月dd日}为非交易日,已回拨至上一交易日检查)"
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
string tradeNote;
|
||||||
|
if (fr007TradeNumbers == null || fr007TradeNumbers.Count == 0)
|
||||||
|
{
|
||||||
|
tradeNote = string.Empty;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var listed = string.Join("、", fr007TradeNumbers.Take(MaxTradesInMessage));
|
||||||
|
tradeNote = fr007TradeNumbers.Count > MaxTradesInMessage
|
||||||
|
? $";涉及FR007浮动的交易: {listed} 等{fr007TradeNumbers.Count}笔"
|
||||||
|
: $";涉及FR007浮动的交易: {listed}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{checkDate:yyyy年MM月dd日}FR007价格未入库{rollbackNote}{tradeNote}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结算范围内挂 FR007 浮动利率的交易编号。仅在缺价报错时调用(正常收盘零开销)。
|
||||||
|
/// 口径是"结算范围内 FR007 浮动腿所属交易",不含重置日归因——非重置日的交易当日
|
||||||
|
/// 不取价、也一并列出,定位时宁多报不漏报。
|
||||||
|
/// </summary>
|
||||||
|
private string[] GetFloatTradeNumbers()
|
||||||
|
{
|
||||||
|
var tradePredicate = _context.PredicateBuilder.GetOtcSwapTradePredicate();
|
||||||
|
return (from t in DbContext.trade.Where(tradePredicate)
|
||||||
|
join p in DbContext.swap_position on t.id equals p.SwapTradeId
|
||||||
|
where !p.Invalid && p.FloatRateUnderlyingCode == "FR007"
|
||||||
|
select t.TradeNumber)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(n => n)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,10 +32,9 @@ namespace YLErp.Modules.EodModule.SettlementModule
|
|||||||
var clienIds = _context.Request.ClientIds;
|
var clienIds = _context.Request.ClientIds;
|
||||||
|
|
||||||
IQueryable<string> allQuery = null;
|
IQueryable<string> allQuery = null;
|
||||||
if (!EodPriceQueryService.CheckFR007Price(_context.SettleDate))
|
// FR007 定盘检查委托给独立检查服务(职责分离):检查日=计息实际读取的取价日,
|
||||||
{
|
// 交易日=当日、周末/节假日=上一交易日,报错沿用本步骤名保持既有口径。
|
||||||
_context.RaiseError(Step, $"{_context.SettleDate:yyyy年MM月dd日}FR007价格未入库");
|
new EodCheckFr007Price(_context).Execute(Step);
|
||||||
}
|
|
||||||
//判断当日结算价是否已经入库
|
//判断当日结算价是否已经入库
|
||||||
if (!EodPriceQueryService.CheckDbExists(_context.SettleDate))
|
if (!EodPriceQueryService.CheckDbExists(_context.SettleDate))
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user