- 检查日改用 IndexFixerBase.GetFixingDate(与计息取价同源): 交易日=当日(行为零变更),周末/节假日=上一交易日, 解除非交易日收盘"必报FR007未入库"死锁(2026-08-28:0523周六收盘) - 缺价报错附回拨说明与涉及FR007浮动的交易编号(上限20笔), 交易日缺价文案与旧版格式一致 - FR007检查从EodCheckSettlePrice抽出为独立检查服务EodCheckFr007Price, 报错沿用原步骤名;新增8个无库单测
181 lines
8.5 KiB
C#
181 lines
8.5 KiB
C#
using YLErp.BLL;
|
|
using YLErp.Configuration.Enums;
|
|
using YLErp.Modules.DataProviderModule;
|
|
|
|
namespace YLErp.Modules.EodModule.SettlementModule
|
|
{
|
|
/// <summary>
|
|
/// 检查当日结算价,是否全部进系统。
|
|
/// </summary>
|
|
public class EodCheckSettlePrice : EodSettleServiceBaseV2
|
|
{
|
|
public const string Step = "检查标的结算价格缺失";
|
|
|
|
public EodCheckSettlePrice(EodSettlementContextV2 context) : base(context)
|
|
{
|
|
ResetDbContext();
|
|
//DbContext.SetDebugLog();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 检查收盘价
|
|
/// </summary>
|
|
public void Execute()
|
|
{
|
|
if (!_context.OtcTrades.Any() && !hasSwaptrade())
|
|
{
|
|
return;
|
|
}
|
|
|
|
var request = _context.Request;
|
|
var settleDate = _context.SettleDate;
|
|
var clienIds = _context.Request.ClientIds;
|
|
|
|
IQueryable<string> allQuery = null;
|
|
// FR007 定盘检查委托给独立检查服务(职责分离):检查日=计息实际读取的取价日,
|
|
// 交易日=当日、周末/节假日=上一交易日,报错沿用本步骤名保持既有口径。
|
|
new EodCheckFr007Price(_context).Execute(Step);
|
|
//判断当日结算价是否已经入库
|
|
if (!EodPriceQueryService.CheckDbExists(_context.SettleDate))
|
|
{
|
|
_context.RaiseError(Step, "结算价格未入库");
|
|
}
|
|
|
|
//检查是否所有待结算交易标的价格都存在
|
|
//获取所有现存(抵押状态)抵押品信息 增加抵押品价格检查
|
|
|
|
if (request.IsSettleOtcTrades)
|
|
{
|
|
var tradePredicate = _context.PredicateBuilder.GetOtcTradePredicate()
|
|
.And(t => t.TradeType != "自定义交易" && t.UnderlyingCode != null);
|
|
|
|
//新增客户筛选 tw
|
|
if (clienIds != null)
|
|
{
|
|
tradePredicate = tradePredicate.And(l => clienIds.Contains(l.ClientId));
|
|
}
|
|
|
|
allQuery = DbContext.trade.Where(tradePredicate).Select(n => n.UnderlyingCode).Distinct();
|
|
|
|
if (PS.Config.ErpElement.ForwardTradePriceModel == ForwardTradePriceModel.STANDARD)
|
|
{
|
|
allQuery = allQuery.Union(DbContext.trade.Where(tradePredicate).Where(x => x.BasisUnderlyingCode != null && x.BasisUnderlyingCode != "").Select(n => n.BasisUnderlyingCode).Distinct());
|
|
}
|
|
|
|
var clientProductPredicate = _context.PredicateBuilder.GetClientProductPredicate();
|
|
|
|
//新增客户筛选 tw
|
|
if (clienIds != null)
|
|
{
|
|
clientProductPredicate = clientProductPredicate.And(l => clienIds.Contains(l.ClientId));
|
|
}
|
|
|
|
var clientProductQuery = from t in DbContext.clientcashincashout_product.Where(clientProductPredicate)
|
|
join um in DbContext.underlying_manager on t.UnderlyingId equals um.id
|
|
select um.UnderlyingCode;
|
|
|
|
allQuery = allQuery.Union(clientProductQuery.Distinct());
|
|
}
|
|
|
|
//检查场内标的
|
|
if (request.IsSettleExchangeTrades)
|
|
{
|
|
var exTradePredicate = _context.PredicateBuilder.GetExchangeTradePredicate();
|
|
|
|
var exTradeQuery = DbContext.ExchangeTrade.Where(exTradePredicate).Select(n => n.UnderlyingCode);
|
|
|
|
allQuery = allQuery == null ? exTradeQuery.Distinct() : allQuery.Union(exTradeQuery.Distinct());
|
|
|
|
var preSettleDate = _context.PreSettleDate;
|
|
// 持仓分支传入 clienIds:使上一交易日持仓标的按当前收盘客户作用域收敛(防御点详见 QueryPositionUnderlyingCodes)。
|
|
var positionQuery = QueryPositionUnderlyingCodes(DbContext, preSettleDate, settleDate, clienIds);
|
|
|
|
allQuery = allQuery.Union(positionQuery.Distinct());
|
|
}
|
|
|
|
|
|
|
|
if (allQuery == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
//检查是否所有标的都在结算日有结算价格
|
|
|
|
var umCodeArr = allQuery.ToArray();
|
|
|
|
var eodPriceProvider = _context.GetEodPriceProvider().Initialize(umCodeArr);
|
|
var umCodes = umCodeArr.Where(n => !string.IsNullOrEmpty(n) && !eodPriceProvider.HasValue(n)).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
|
|
//排除掉节假日不需要结算的交易
|
|
foreach (var t in _context.HolidayTrades)
|
|
{
|
|
if (!string.IsNullOrEmpty(t.UnderlyingCode))
|
|
{
|
|
umCodes.Remove(t.UnderlyingCode);
|
|
}
|
|
}
|
|
|
|
if (umCodes.Any())
|
|
{
|
|
var codes = string.Join(", ", umCodes);
|
|
|
|
_context.RaiseError(Step, "标的代码:" + codes);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 取上一交易日持仓中需结算价的标的(持仓分支)。
|
|
/// 抽成 static 以便单测直接覆盖 ClientId 收敛语义。
|
|
///
|
|
/// ── 防御行为(EQD-6967)─────────────────────────────────────────────
|
|
/// 收盘缺失价检查在“按客户作用域结算(ClientIds 非 null)”时,本应只校验
|
|
/// 当前收盘客户自己的标的,而不应把“其他客户”或“系统级(ClientId=0)”的
|
|
/// 上一交易日持仓标的误报为缺失价。历史上持仓分支完全未引用 ClientId,
|
|
/// 导致客户 A 收盘时被其他客户/系统级持仓的标的噪声干扰(见缺陷现象)。
|
|
///
|
|
/// 本方法通过 clienIds 过滤做收敛,与 OTC 分支、客户产品分支已有的
|
|
/// ClientId 过滤语义保持一致:
|
|
/// · clienIds == null → 系统级全量结算,短路为 true(不收窄,零回归);
|
|
/// · clienIds != null → 仅返回属于指定客户的持仓标的(Contains(t.ClientId)),
|
|
/// 过滤掉其他客户及系统级(ClientId=0)持仓噪声。
|
|
/// 注意:场外交易结算模式(IsSettleExchangeTrades=false)下持仓分支整块跳过,
|
|
/// 此时本方法不会被调用,属操作层面的规避而非修复。
|
|
/// ───────────────────────────────────────────────────────────────────
|
|
/// </summary>
|
|
public static IQueryable<string> QueryPositionUnderlyingCodes(YLContext db, DateTime preSettleDate, DateTime settleDate, IEnumerable<int> clienIds)
|
|
{
|
|
var futureTypes = ConsGlobal.InstrumentType.GetFutureTypes();
|
|
return from t in db.eod_trade_position
|
|
join um in db.underlying_manager on t.UnderlyingCode equals um.UnderlyingCode
|
|
where t.ValueDate == preSettleDate && t.Amount != 0
|
|
&& ConsTrade.TradeTypesForHedge.Contains(t.TradeType)
|
|
&& (!futureTypes.Contains(um.UnderlyingInstrumentType) || um.MaturityDate >= settleDate)
|
|
// 【防御点·EQD-6967】按客户作用域收敛持仓标的:
|
|
// clienIds==null → 系统级全量结算,不收窄(零回归);
|
|
// clienIds!=null → 仅保留指定客户持仓,过滤掉其他客户/系统级(ClientId=0)持仓噪声。
|
|
&& (clienIds == null || clienIds.Contains(t.ClientId))
|
|
select t.UnderlyingCode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取互换标的
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
private string[] GetSwapUnderlyingCodes()
|
|
{
|
|
var tradePredicate = _context.PredicateBuilder.GetOtcSwapTradePredicate();
|
|
var query = from t in DbContext.trade.Where(tradePredicate)
|
|
join p in DbContext.swap_position on t.id equals p.SwapTradeId
|
|
where p.IsInitial && !string.IsNullOrEmpty(p.UnderlyingCode) && !p.Invalid
|
|
select p.UnderlyingCode;
|
|
return query.Distinct().ToArray();
|
|
}
|
|
private bool hasSwaptrade()
|
|
{
|
|
var tradePredicate = _context.PredicateBuilder.GetOtcSwapTradePredicate();
|
|
return DbContext.trade.Where(tradePredicate).Any();
|
|
}
|
|
}
|
|
}
|