diff --git a/YLErpDAL/Modules/RiskEngine/测试用例.md b/YLErpDAL/Modules/RiskEngine/测试用例.md index f3397446..cd706142 100644 --- a/YLErpDAL/Modules/RiskEngine/测试用例.md +++ b/YLErpDAL/Modules/RiskEngine/测试用例.md @@ -1379,7 +1379,7 @@ SELECT FROM trade cur INNER JOIN trade t ON t.UnderlyingId = cur.UnderlyingId - AND t.ValidState <> 'InValid' + AND (t.ValidState IS NULL OR t.ValidState <> 'InValid') AND t.ParentTradeId = 0 AND t.TradeStatus IN ('确认成交', '平仓待复核', '提前终止拒绝', '行权待复核', '互换待复核', '审批中') LEFT JOIN underlying_manager um @@ -1410,7 +1410,7 @@ SELECT FROM trade cur INNER JOIN trade t ON t.UnderlyingId = cur.UnderlyingId - AND t.ValidState <> 'InValid' + AND (t.ValidState IS NULL OR t.ValidState <> 'InValid') AND t.ParentTradeId = 0 AND t.TradeStatus IN ('确认成交', '平仓待复核', '提前终止拒绝', '行权待复核', '互换待复核', '审批中') WHERE cur.id = @TradeId @@ -1458,7 +1458,9 @@ var maturityDate = DbContext.underlying_manager if (!maturityDate.HasValue) throw new Exception("挂钩标的到期日为空"); -return maturityDate.Value.Date; +return new RiskVariableValueDetail( + maturityDate.Value.Date, + $"交易ID {TradeId},挂钩标的到期日为{maturityDate.Value.Date:yyyy-MM-dd}"); ``` 变量 2:当前交易合约到期日,DataType 为 Date。 @@ -1472,7 +1474,9 @@ var exerciseDate = DbContext.trade if (!exerciseDate.HasValue) throw new Exception("当前交易合约到期日为空"); -return exerciseDate.Value.Date; +return new RiskVariableValueDetail( + exerciseDate.Value.Date, + $"交易ID {TradeId},当前交易合约到期日为{exerciseDate.Value.Date:yyyy-MM-dd}"); ``` 规则字段口径: @@ -1517,7 +1521,7 @@ SELECT um.MaturityDate AS UnderlyingMaturityDate, CASE - WHEN um.MaturityDate < t.ExerciseDate THEN 1 + WHEN DATE(um.MaturityDate) < DATE(t.ExerciseDate) THEN 1 ELSE 0 END AS IsUnderlyingMaturityLessThanContractExerciseDate FROM trade t @@ -1558,14 +1562,14 @@ if (!openingNotionalRaw.HasValue) { return new RiskVariableValueDetail( 0m, - "开仓名义本金为空,本规则不命中"); + $"交易ID {TradeId},开仓名义本金为空,本规则不命中"); } decimal openingNotional = (decimal)openingNotionalRaw.Value; return new RiskVariableValueDetail( openingNotional, - $"开仓名义本金为{openingNotional}"); + $"交易ID {TradeId},开仓名义本金为{openingNotional}"); ``` 规则字段口径: @@ -1679,7 +1683,7 @@ string marginPaymentMessage = marginPaymentDetails.Any() return new RiskVariableValueDetail( marginPaymentRatio, - $"保证金支付总金额为{marginPaymentAmount},开仓名义本金为{openingNotional},{marginPaymentMessage}"); + $"交易ID {TradeId},保证金支付总金额为{marginPaymentAmount},开仓名义本金为{openingNotional},{marginPaymentMessage}"); ``` 规则字段口径: @@ -1698,8 +1702,8 @@ return new RiskVariableValueDetail( //{ // Id = 1000004, // RuleName = "保证金支付比例超阈值(本地)", -// RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 OriginalStockEqvNotional,作为开仓名义本金;通过 DbContext.swap_position 按 TradeId 取 IsInitial=true、Invalid=false、InterestMode 为 5 或 6 且 InterestDirection=2 的预付金支付方向记录,汇总 InterestPrincipalFix 作为保证金支付金额。计算逻辑:保证金支付金额 ÷ 开仓名义本金 大于 50% 时触发审批。", -// RuleExpr = "DbContext.swap_position.Where(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && (p.InterestMode == 5 || p.InterestMode == 6) && p.InterestDirection == 2).Sum(p => (decimal?)p.InterestPrincipalFix).Value / Convert.ToDecimal(DbContext.trade.First(t => t.id == TradeId).OriginalStockEqvNotional) > 0.5m", +// RuleText = "变量口径:使用数值型变量“保证金支付比例”。变量通过 DbContext.trade 按 TradeId 取当前交易的 OriginalStockEqvNotional 作为开仓名义本金;通过 DbContext.swap_position 按 TradeId 取 IsInitial=true、Invalid=false、InterestMode 为 5 或 6 且 InterestDirection=2 的预付金支付方向记录,汇总 InterestPrincipalFix 作为保证金支付金额;变量返回 保证金支付金额 ÷ 开仓名义本金。规则配置:保证金支付比例 > 0.5 时触发审批。", +// RuleExpr = "保证金支付比例 > 0.5", // Version = 1, // Status = RiskRuleStatus.Active, // OptId = 0, @@ -1711,21 +1715,26 @@ return new RiskVariableValueDetail( //}); ``` -汇总 SQL: +变量值核对 SQL:用于核对变量“保证金支付比例”的返回值,`MarginPaymentRatio` 即变量值;执行前将 `@Threshold` 替换为规则配置阈值。 ```sql -SET @TradeId = 3001699; +SET @TradeId = 3001749; +SET @Threshold = 0.5; SELECT t.id AS TradeId, t.OriginalStockEqvNotional, - - SUM(IFNULL(sp.InterestPrincipalFix, 0)) AS PayMarginAmount, - SUM(IFNULL(sp.InterestPrincipalFix, 0)) / t.OriginalStockEqvNotional AS PayMarginRate, CASE - WHEN SUM(IFNULL(sp.InterestPrincipalFix, 0)) / t.OriginalStockEqvNotional > 0.5 THEN 1 + WHEN t.OriginalStockEqvNotional IS NULL THEN '开仓名义本金为空,变量会抛出异常' + WHEN t.OriginalStockEqvNotional = 0 THEN '开仓名义本金为0,变量会抛出异常' + ELSE '正常' + END AS VariableStatus, + SUM(IFNULL(sp.InterestPrincipalFix, 0)) AS MarginPaymentAmount, + SUM(IFNULL(sp.InterestPrincipalFix, 0)) / NULLIF(t.OriginalStockEqvNotional, 0) AS MarginPaymentRatio, + CASE + WHEN SUM(IFNULL(sp.InterestPrincipalFix, 0)) / NULLIF(t.OriginalStockEqvNotional, 0) > @Threshold THEN 1 ELSE 0 - END AS IsGreaterThan50Percent + END AS IsVariableGreaterThanThreshold FROM trade t LEFT JOIN swap_position sp ON sp.SwapTradeId = t.id @@ -1739,10 +1748,10 @@ GROUP BY t.OriginalStockEqvNotional; ``` -明细 SQL:用于查看纳入保证金支付金额汇总的预付金明细;是否超限以汇总 SQL 的 `PayMarginRate` 为准。 +明细 SQL:用于查看变量“保证金支付比例”纳入汇总的预付金支付明细;是否超限以变量值核对 SQL 的 `MarginPaymentRatio` 为准。 ```sql -SET @TradeId = 3001699; +SET @TradeId = 3001749; SELECT sp.id AS SwapPositionId, @@ -1787,7 +1796,7 @@ ORDER BY ABS((预付金返息率 - 1) * 100) > 阈值 ``` -变量 Roslyn 示例:变量名为“保证金利率偏离”,DataType 为 Numeric;规则前端仍配置“保证金利率偏离 > 阈值”。脚本会查询当前 TradeId 下所有符合口径的保证金记录,变量值返回最大偏离值用于判断,命中说明列出所有存在偏离的记录。`InterestRateDefault` 实体字段类型为 decimal,可直接参与 decimal 计算。 +变量 Roslyn 示例:变量名为“保证金利率偏离”,DataType 为 Numeric;规则前端仍配置“保证金利率偏离 > 阈值”。脚本会查询当前 TradeId 下所有符合口径的保证金记录,变量值返回最大偏离值用于判断;变量明细逐条返回偏离值,结构化执行器会按规则阈值优先列出真正超过阈值的记录,再列出未超过阈值的其他偏离记录。`InterestRateDefault` 实体字段类型为 decimal,可直接参与 decimal 计算。 ```csharp var marginRateItems = DbContext.swap_position @@ -1815,16 +1824,17 @@ var deviatedItems = marginRateItems .Where(p => p.InterestRateDeviation > 0m) .OrderByDescending(p => p.InterestRateDeviation) .ThenBy(p => p.id) - .Select(p => $"记录ID {p.id}:返息率{(p.InterestRateDefault * 100m).ToString("0.#########")}%,偏离{p.InterestRateDeviation.ToString("0.#########")}%") + .Select(p => new RiskVariableValueDetailItem( + p.InterestRateDeviation, + $"记录ID {p.id}:返息率{(p.InterestRateDefault * 100m).ToString("0.#########")}%,偏离{p.InterestRateDeviation.ToString("0.#########")}%")) .ToList(); -string deviationMessage = deviatedItems.Any() - ? "存在偏离的预付金记录:" + string.Join(";", deviatedItems) - : "未发现保证金利率偏离记录"; +string deviationMessage = $"交易ID {TradeId}"; return new RiskVariableValueDetail( maxDeviationItem.InterestRateDeviation, - deviationMessage); + deviationMessage, + deviatedItems); ``` 规则字段口径: @@ -1841,8 +1851,8 @@ return new RiskVariableValueDetail( //{ // Id = 1000005, // RuleName = "保证金利率偏离(本地)", -// RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 取 IsInitial=true、Invalid=false、InterestMode 为 5 或 6 的预付金记录,取 InterestRateDefault 作为预付金返息率数据库原值。计算逻辑:数据库保存的是原值,公式中先和 1 比较,再乘以 100 还原为百分比偏离值,即 ABS((预付金返息率 - 1) * 100),结果大于阈值时触发审批。", -// RuleExpr = "Math.Abs((Convert.ToDecimal(DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && (p.InterestMode == 5 || p.InterestMode == 6)).InterestRateDefault) - 1m) * 100m) > 阈值", +// RuleText = "变量口径:使用数值型变量“保证金利率偏离”。变量通过 DbContext.swap_position 按 TradeId 取 IsInitial=true、Invalid=false、InterestMode 为 5 或 6 的全部预付金记录,逐条计算 ABS((InterestRateDefault - 1) * 100),返回最大偏离值;变量明细逐条返回偏离值,结构化执行器按规则阈值优先展示超过阈值的记录。规则配置:保证金利率偏离 > 阈值 时触发审批。", +// RuleExpr = "保证金利率偏离 > 阈值", // Version = 1, // Status = RiskRuleStatus.Active, // OptId = 0, @@ -1854,29 +1864,31 @@ return new RiskVariableValueDetail( //}); ``` -汇总 SQL:用于查看当前交易预付金返息率偏离情况;执行前将 `@Threshold` 替换为实际阈值。 +变量值核对 SQL:用于核对变量“保证金利率偏离”的返回值,`MaxInterestRateDeviation` 即变量值;执行前将 `@Threshold` 替换为规则配置阈值。 ```sql SET @TradeId = 3001699; SET @Threshold = 0.5; SELECT - sp.SwapTradeId AS TradeId, - MAX(ABS((IFNULL(sp.InterestRateDefault, 0) - 1) * 100)) AS MaxInterestRateDeviation, + @TradeId AS TradeId, CASE - WHEN MAX(ABS((IFNULL(sp.InterestRateDefault, 0) - 1) * 100)) > @Threshold THEN 1 + WHEN COUNT(*) = 0 THEN '预付金记录不存在,变量会抛出异常' + ELSE '正常' + END AS VariableStatus, + MAX(ABS((sp.InterestRateDefault - 1) * 100)) AS MaxInterestRateDeviation, + CASE + WHEN MAX(ABS((sp.InterestRateDefault - 1) * 100)) > @Threshold THEN 1 ELSE 0 - END AS IsInterestRateDeviationGreaterThanThreshold + END AS IsVariableGreaterThanThreshold FROM swap_position sp WHERE sp.SwapTradeId = @TradeId AND sp.IsInitial = 1 AND sp.Invalid = 0 - AND sp.InterestMode IN (5, 6) -GROUP BY - sp.SwapTradeId; + AND sp.InterestMode IN (5, 6); ``` -明细 SQL:用于查看每条预付金记录的返息率原值、乘以 100 后的页面口径及偏离值。 +明细 SQL:用于查看变量“保证金利率偏离”纳入计算的每条预付金记录返息率原值、乘以 100 后的页面口径及偏离值。 ```sql SET @TradeId = 3001699; @@ -1892,9 +1904,9 @@ SELECT sp.InterestPrincipalFix, sp.InterestRateDefault AS InterestRateRawValue, sp.InterestRateDefault * 100 AS InterestRatePercentValue, - ABS((IFNULL(sp.InterestRateDefault, 0) - 1) * 100) AS InterestRateDeviation, + ABS((sp.InterestRateDefault - 1) * 100) AS InterestRateDeviation, CASE - WHEN ABS((IFNULL(sp.InterestRateDefault, 0) - 1) * 100) > @Threshold THEN 1 + WHEN ABS((sp.InterestRateDefault - 1) * 100) > @Threshold THEN 1 ELSE 0 END AS IsGreaterThanThreshold, sp.HappenDate @@ -1978,7 +1990,7 @@ string marginReceiveMessage = marginReceiveDetails.Any() return new RiskVariableValueDetail( marginReceiveRatio, - $"保证金收取总金额为{marginReceiveAmount},开仓名义本金为{openingNotional},{marginReceiveMessage}"); + $"交易ID {TradeId},保证金收取总金额为{marginReceiveAmount},开仓名义本金为{openingNotional},{marginReceiveMessage}"); ``` 规则字段口径: @@ -1997,8 +2009,8 @@ return new RiskVariableValueDetail( //{ // Id = 1000006, // RuleName = "保证金收取比例低于最低标准(本地)", -// RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 OriginalStockEqvNotional,作为开仓名义本金;通过 DbContext.swap_position 按 TradeId 取 IsInitial=true、Invalid=false、InterestMode 为 5 或 6 且 InterestDirection=1 的预付金收取方向记录,汇总 InterestPrincipalFix 作为保证金收取金额。计算逻辑:保证金收取金额 ÷ 开仓名义本金 小于 20% 时触发审批。", -// RuleExpr = "DbContext.swap_position.Where(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && (p.InterestMode == 5 || p.InterestMode == 6) && p.InterestDirection == 1).Sum(p => (decimal?)p.InterestPrincipalFix).Value / Convert.ToDecimal(DbContext.trade.First(t => t.id == TradeId).OriginalStockEqvNotional) < 0.2m", +// RuleText = "变量口径:使用数值型变量“保证金收取比例”。变量通过 DbContext.trade 按 TradeId 取当前交易的 OriginalStockEqvNotional 作为开仓名义本金;通过 DbContext.swap_position 按 TradeId 取 IsInitial=true、Invalid=false、InterestMode 为 5 或 6 且 InterestDirection=1 的预付金收取方向记录,汇总 InterestPrincipalFix 作为保证金收取金额;变量返回 保证金收取金额 ÷ 开仓名义本金。规则配置:保证金收取比例 < 0.2 时触发审批。", +// RuleExpr = "保证金收取比例 < 0.2", // Version = 1, // Status = RiskRuleStatus.Active, // OptId = 0, @@ -2010,21 +2022,26 @@ return new RiskVariableValueDetail( //}); ``` -汇总 SQL: +变量值核对 SQL:用于核对变量“保证金收取比例”的返回值,`MarginReceiveRatio` 即变量值;执行前将 `@Threshold` 替换为规则配置阈值。 ```sql SET @TradeId = 3001699; +SET @Threshold = 0.2; SELECT t.id AS TradeId, t.OriginalStockEqvNotional, - - SUM(IFNULL(sp.InterestPrincipalFix, 0)) AS ReceiveMarginAmount, - SUM(IFNULL(sp.InterestPrincipalFix, 0)) / t.OriginalStockEqvNotional AS ReceiveMarginRate, CASE - WHEN SUM(IFNULL(sp.InterestPrincipalFix, 0)) / t.OriginalStockEqvNotional < 0.2 THEN 1 + WHEN t.OriginalStockEqvNotional IS NULL THEN '开仓名义本金为空,变量会抛出异常' + WHEN t.OriginalStockEqvNotional = 0 THEN '开仓名义本金为0,变量会抛出异常' + ELSE '正常' + END AS VariableStatus, + SUM(IFNULL(sp.InterestPrincipalFix, 0)) AS MarginReceiveAmount, + SUM(IFNULL(sp.InterestPrincipalFix, 0)) / NULLIF(t.OriginalStockEqvNotional, 0) AS MarginReceiveRatio, + CASE + WHEN SUM(IFNULL(sp.InterestPrincipalFix, 0)) / NULLIF(t.OriginalStockEqvNotional, 0) < @Threshold THEN 1 ELSE 0 - END AS IsLessThan20Percent + END AS IsVariableLessThanThreshold FROM trade t LEFT JOIN swap_position sp ON sp.SwapTradeId = t.id @@ -2038,7 +2055,7 @@ GROUP BY t.OriginalStockEqvNotional; ``` -明细 SQL:用于查看纳入保证金收取金额汇总的预付金明细;是否低于最低标准以汇总 SQL 的 `ReceiveMarginRate` 为准。 +明细 SQL:用于查看变量“保证金收取比例”纳入汇总的预付金收取明细;是否低于最低标准以变量值核对 SQL 的 `MarginReceiveRatio` 为准。 ```sql SET @TradeId = 3001699; @@ -2099,12 +2116,12 @@ if (!startDate.HasValue) { return new RiskVariableValueDetail( DateTime.Today, - "起息日为空,本规则不命中"); + $"交易ID {TradeId},起息日为空,本规则不命中"); } return new RiskVariableValueDetail( startDate.Value.Date, - $"起息日为{startDate.Value.Date:yyyy-MM-dd}"); + $"交易ID {TradeId},起息日为{startDate.Value.Date:yyyy-MM-dd}"); ``` 变量 2:变量名为“今日”,DataType 为 Date。 @@ -2112,7 +2129,7 @@ return new RiskVariableValueDetail( ```csharp return new RiskVariableValueDetail( DateTime.Today, - $"当前日期为{DateTime.Today:yyyy-MM-dd}"); + $"交易ID {TradeId},当前日期为{DateTime.Today:yyyy-MM-dd}"); ``` 规则字段口径: @@ -2463,7 +2480,7 @@ if (!tradeInfo.StartDate.HasValue || !tradeInfo.ExerciseDate.HasValue) { return new RiskVariableValueDetail( 0m, - $"起始日或到期日为空,起始日为{tradeInfo.StartDate?.ToString("yyyy-MM-dd") ?? "空"},到期日为{tradeInfo.ExerciseDate?.ToString("yyyy-MM-dd") ?? "空"},本规则不命中"); + $"交易ID {TradeId},起始日或到期日为空,起始日为{tradeInfo.StartDate?.ToString("yyyy-MM-dd") ?? "空"},到期日为{tradeInfo.ExerciseDate?.ToString("yyyy-MM-dd") ?? "空"},本规则不命中"); } var tradeExtend = DbContext.trade_extend @@ -2503,7 +2520,7 @@ string interestCalcModeDescription = interestCalcMode == "00" return new RiskVariableValueDetail( contractNaturalDays, - $"起始日为{startDate:yyyy-MM-dd},到期日为{exerciseDate:yyyy-MM-dd},计息方式为{interestCalcModeText},{interestCalcModeDescription}"); + $"交易ID {TradeId},起始日为{startDate:yyyy-MM-dd},到期日为{exerciseDate:yyyy-MM-dd},计息方式为{interestCalcModeText},{interestCalcModeDescription}"); ``` 规则字段口径: @@ -2539,64 +2556,61 @@ return new RiskVariableValueDetail( SET @TradeId = 3001699; SET @Threshold = 365; +WITH trade_info AS ( + SELECT + t.id AS TradeId, + t.StartDate, + t.ExerciseDate, + CASE + WHEN TRIM(JSON_UNQUOTE(JSON_EXTRACT(te.ExtendJson, '$.InterestCalcMode'))) = '' THEN '11' + ELSE COALESCE(JSON_UNQUOTE(JSON_EXTRACT(te.ExtendJson, '$.InterestCalcMode')), '11') + END AS InterestCalcMode + FROM trade t + LEFT JOIN trade_extend te + ON te.TradeId = t.id + WHERE t.id = @TradeId +) SELECT - t.id AS TradeId, - t.StartDate, - t.ExerciseDate, - COALESCE(JSON_UNQUOTE(JSON_EXTRACT(te.ExtendJson, '$.InterestCalcMode')), '11') AS InterestCalcMode, - DATEDIFF(DATE(t.ExerciseDate), DATE(t.StartDate)) AS BaseNaturalDays, + ti.TradeId, + ti.StartDate, + ti.ExerciseDate, + ti.InterestCalcMode, CASE - WHEN LEFT(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(te.ExtendJson, '$.InterestCalcMode')), '11'), 1) = '1' THEN 1 - ELSE 0 - END AS CalcFirstDays, + WHEN ti.InterestCalcMode IN ('00', '01', '10', '11') THEN '正常' + ELSE '计息方式不合法,变量会抛出异常' + END AS VariableStatus, + DATEDIFF(DATE(ti.ExerciseDate), DATE(ti.StartDate)) AS BaseNaturalDays, + CASE WHEN LEFT(ti.InterestCalcMode, 1) = '1' THEN 1 ELSE 0 END AS CalcFirstDays, + CASE WHEN RIGHT(ti.InterestCalcMode, 1) = '1' THEN 0 ELSE -1 END AS NotCalcLastDays, + DATEDIFF(DATE(ti.ExerciseDate), DATE(ti.StartDate)) + + CASE WHEN LEFT(ti.InterestCalcMode, 1) = '1' THEN 1 ELSE 0 END + + CASE WHEN RIGHT(ti.InterestCalcMode, 1) = '1' THEN 0 ELSE -1 END AS ContractNaturalDays, CASE - WHEN RIGHT(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(te.ExtendJson, '$.InterestCalcMode')), '11'), 1) = '1' THEN 0 - ELSE -1 - END AS NotCalcLastDays, - DATEDIFF(DATE(t.ExerciseDate), DATE(t.StartDate)) - + CASE - WHEN LEFT(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(te.ExtendJson, '$.InterestCalcMode')), '11'), 1) = '1' THEN 1 - ELSE 0 - END - + CASE - WHEN RIGHT(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(te.ExtendJson, '$.InterestCalcMode')), '11'), 1) = '1' THEN 0 - ELSE -1 - END AS ContractNaturalDays, - CASE - WHEN DATEDIFF(DATE(t.ExerciseDate), DATE(t.StartDate)) - + CASE - WHEN LEFT(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(te.ExtendJson, '$.InterestCalcMode')), '11'), 1) = '1' THEN 1 - ELSE 0 - END - + CASE - WHEN RIGHT(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(te.ExtendJson, '$.InterestCalcMode')), '11'), 1) = '1' THEN 0 - ELSE -1 - END > @Threshold THEN 1 + WHEN ti.InterestCalcMode NOT IN ('00', '01', '10', '11') THEN NULL + WHEN DATEDIFF(DATE(ti.ExerciseDate), DATE(ti.StartDate)) + + CASE WHEN LEFT(ti.InterestCalcMode, 1) = '1' THEN 1 ELSE 0 END + + CASE WHEN RIGHT(ti.InterestCalcMode, 1) = '1' THEN 0 ELSE -1 END > @Threshold THEN 1 ELSE 0 END AS IsContractTermGreaterThanThreshold -FROM trade t -LEFT JOIN trade_extend te - ON te.TradeId = t.id -WHERE t.id = @TradeId; +FROM trade_info ti; ``` --- ## 23. 规则 12 查询结果排查 SQL -规则 12:债券类净价偏离(本地)。用于核对浮动支付端期初交割净价和交易日前中债估值净价;同一标的同一估值日存在不同可信度时,优先取 credibility=1。 +规则 12:债券类净价偏离(本地)。用于核对浮动支付端期初交割净价和中债估值净价;同一标的同一估值日存在不同可信度时,优先取 credibility=1。 取数流程: ```text -1. 根据 TradeId 查 trade.TradeDate。 -2. 通过银行间日历 Country=IB 计算交易日的上一银行间交易日,查不到日历或上一交易日时报异常。 -3. 根据 TradeId 查 swap_position。 -4. 限定 IsInitial=1、Invalid=0、PosiDirection=2、UnderlyingCode 非空,取所有浮动支付端。 -5. 从浮动支付端取 PosiNetNoFeePrice 和 UnderlyingCode。 -6. 用 swap_position.UnderlyingCode 关联 china_bond_valuation.bond_id。 -7. 限定 valuation_date 为上一银行间交易日当天,优先取 credibility=1。 -8. 计算 ABS(PosiNetNoFeePrice * 100 - net_price),大于 5 则命中。 +1. 根据 TradeId 查 trade.StartDate 和 trade.TradeDate。 +2. 通过银行间日历 Country=IB 分别计算开始日期和成交日期的上一银行间交易日。 +3. 根据 TradeId 查所有符合条件的浮动支付端,取 PosiNetNoFeePrice 和 UnderlyingCode。 +4. 用 swap_position.UnderlyingCode 关联 china_bond_valuation.bond_id。 +5. 对每条浮动支付端记录独立匹配行情:优先取开始日期上一交易日的 net_price;该日无数据时,改取成交日期上一交易日的数据;两日均无数据才报错。 +6. 每条浮动支付端对应的同一标的、同一估值日存在多条行情时,按 credibility ASC、ID ASC 取优先记录;相同标的的多条浮动支付端不会合并。 +7. 逐条计算 ABS(PosiNetNoFeePrice * 100 - net_price),变量返回全部记录中的最大偏离值,命中说明只展示产生最大偏离值的记录。 ``` 规则公式: @@ -2605,10 +2619,97 @@ WHERE t.id = @TradeId; ABS(浮动支付端.PosiNetNoFeePrice * 100 - 上一收盘日中债估值.net_price) > 5 ``` -变量 Roslyn 示例:变量名为“债券类净价偏离值”,DataType 为 Numeric;规则前端配置“债券类净价偏离值 > 阈值”。具体取数、上一银行间交易日确认、估值匹配、债券/非债券差异处理统一放在 `RiskMarketDeviationHelper` 中,变量公式只保留公共方法调用。 +变量 Roslyn 示例:变量名为“债券类净价偏离值”,DataType 为 Numeric;规则前端配置“债券类净价偏离值 > 阈值”。变量公式不依赖 `RiskMarketDeviationHelper`。 ```csharp -return YLErp.Modules.RiskEngine.RiskMarketDeviationHelper.GetBondNetPriceDeviation(DbContext, TradeId); +var tradeDates = DbContext.trade + .Where(t => t.id == TradeId) + .Select(t => new { t.StartDate, t.TradeDate }) + .FirstOrDefault(); + +if (tradeDates == null) + throw new Exception("交易不存在"); +if (!tradeDates.TradeDate.HasValue) + throw new Exception("成交日期为空"); + +DateTime? startPreviousTradingDay = tradeDates.StartDate.HasValue + ? YLErp.Modules.RiskEngine.RiskCalendarHelper.GetPreviousInterbankTradingDay(DbContext, tradeDates.StartDate.Value.Date) + : (DateTime?)null; +DateTime tradePreviousTradingDay = YLErp.Modules.RiskEngine.RiskCalendarHelper.GetPreviousInterbankTradingDay(DbContext, tradeDates.TradeDate.Value.Date); +DateTime primaryValuationDate = startPreviousTradingDay ?? tradePreviousTradingDay; +DateTime primaryValuationDateExclusive = primaryValuationDate.AddDays(1); +DateTime tradeValuationDateExclusive = tradePreviousTradingDay.AddDays(1); + +var positionItems = DbContext.swap_position + .Where(p => p.SwapTradeId == TradeId + && p.IsInitial + && !p.Invalid + && p.PosiDirection == 2 + && !string.IsNullOrEmpty(p.UnderlyingCode)) + .Select(p => new { p.id, p.UnderlyingCode, p.PosiNetNoFeePrice }) + .ToList(); + +if (!positionItems.Any()) + throw new Exception("浮动支付端记录不存在"); + +var missingPositionValueIds = positionItems + .Where(p => !p.PosiNetNoFeePrice.HasValue) + .Select(p => p.id.ToString()) + .ToList(); +if (missingPositionValueIds.Any()) + throw new Exception("浮动支付端期初交割净价为空,记录ID:" + string.Join("、", missingPositionValueIds)); + +var underlyingCodes = positionItems.Select(p => p.UnderlyingCode).Distinct().ToList(); +var valuationRows = DbContext.china_bond_valuation + .Where(v => underlyingCodes.Contains(v.bond_id) + && v.net_price.HasValue + && ((v.valuation_date >= primaryValuationDate && v.valuation_date < primaryValuationDateExclusive) + || (v.valuation_date >= tradePreviousTradingDay && v.valuation_date < tradeValuationDateExclusive))) + .Select(v => new { v.id, v.bond_id, v.valuation_date, v.credibility, v.net_price }) + .ToList(); + +var selectedItems = positionItems + .Select(p => new + { + Position = p, + Valuation = valuationRows + .Where(v => v.bond_id == p.UnderlyingCode) + .OrderBy(v => v.valuation_date.Date == primaryValuationDate ? 0 : 1) + .ThenBy(v => v.credibility) + .ThenBy(v => v.id) + .FirstOrDefault() + }) + .ToList(); + +var missingValuationItems = selectedItems + .Where(x => x.Valuation == null) + .Select(x => $"记录ID {x.Position.id},标的{x.Position.UnderlyingCode}") + .ToList(); +if (missingValuationItems.Any()) + throw new Exception($"未找到开始日期上一交易日{primaryValuationDate:yyyy-MM-dd}及成交日期上一交易日{tradePreviousTradingDay:yyyy-MM-dd}的中债估值净价:" + string.Join(";", missingValuationItems)); + +var deviationItems = selectedItems + .Select(x => new + { + x.Position.id, + x.Position.UnderlyingCode, + PositionValue = x.Position.PosiNetNoFeePrice.Value * 100m, + MarketDate = x.Valuation.valuation_date, + MarketDateSource = startPreviousTradingDay.HasValue + && x.Valuation.valuation_date.Date == startPreviousTradingDay.Value + ? "开始日期上一交易日" + : "成交日期上一交易日", + MarketValue = x.Valuation.net_price.Value, + DiffAbs = Math.Abs(x.Position.PosiNetNoFeePrice.Value * 100m - x.Valuation.net_price.Value) + }) + .OrderByDescending(x => x.DiffAbs) + .ThenBy(x => x.id) + .ToList(); + +var maxDeviationItem = deviationItems.First(); +string detailMessage = $"交易ID {TradeId},浮动支付端记录ID {maxDeviationItem.id},标的{maxDeviationItem.UnderlyingCode}:期初交割净价{maxDeviationItem.PositionValue:0.#########},按{maxDeviationItem.MarketDateSource}取{maxDeviationItem.MarketDate:yyyy-MM-dd}中债估值净价{maxDeviationItem.MarketValue:0.#########},最大偏离{maxDeviationItem.DiffAbs:0.#########}"; + +return new RiskVariableValueDetail(maxDeviationItem.DiffAbs, detailMessage); ``` @@ -2633,100 +2734,121 @@ return YLErp.Modules.RiskEngine.RiskMarketDeviationHelper.GetBondNetPriceDeviati ``` ```sql -SET @TradeId = 3001699; +SET @TradeId = 3001744; +SET @Threshold = 5; -WITH RECURSIVE candidate_dates AS ( - SELECT DATE(t.TradeDate) - INTERVAL 1 DAY AS CandidateDate - FROM trade t - WHERE t.id = @TradeId - - UNION ALL - - SELECT CandidateDate - INTERVAL 1 DAY - FROM candidate_dates - WHERE CandidateDate > DATE_SUB((SELECT DATE(TradeDate) FROM trade WHERE id = @TradeId), INTERVAL 370 DAY) +WITH RECURSIVE trade_dates AS ( + SELECT id, DATE(StartDate) AS StartDate, DATE(TradeDate) AS TradeDate + FROM trade + WHERE id = @TradeId ), -previous_trading_day AS ( - SELECT cd.CandidateDate +candidate_dates AS ( + SELECT 'StartDate' AS DateType, StartDate AS BaseDate, StartDate - INTERVAL 1 DAY AS CandidateDate + FROM trade_dates + WHERE StartDate IS NOT NULL + UNION ALL + SELECT 'TradeDate', TradeDate, TradeDate - INTERVAL 1 DAY + FROM trade_dates + WHERE TradeDate IS NOT NULL + UNION ALL + SELECT DateType, BaseDate, CandidateDate - INTERVAL 1 DAY + FROM candidate_dates + WHERE CandidateDate > DATE_SUB(BaseDate, INTERVAL 370 DAY) +), +trading_day_ranked AS ( + SELECT cd.DateType, cd.CandidateDate, + ROW_NUMBER() OVER (PARTITION BY cd.DateType ORDER BY cd.CandidateDate DESC) AS RowNo FROM candidate_dates cd INNER JOIN calendar c ON c.Year = YEAR(cd.CandidateDate) AND UPPER(c.Country) = 'IB' AND (c.ValidState IS NULL OR c.ValidState <> 'InValid') - WHERE NOT JSON_CONTAINS(c.HolidayJson, JSON_QUOTE(DATE_FORMAT(cd.CandidateDate, '%Y,%m,%d'))) - ORDER BY cd.CandidateDate DESC - LIMIT 1 + WHERE JSON_VALID(c.HolidayJson) = 1 + AND JSON_TYPE(c.HolidayJson) = 'ARRAY' + AND JSON_CONTAINS(c.HolidayJson, JSON_QUOTE(DATE_FORMAT(cd.CandidateDate, '%Y,%m,%d'))) = 0 ), -bond_valuation_ranked AS ( +reference_days AS ( SELECT + MAX(CASE WHEN DateType = 'StartDate' AND RowNo = 1 THEN CandidateDate END) AS StartPreviousTradingDay, + MAX(CASE WHEN DateType = 'TradeDate' AND RowNo = 1 THEN CandidateDate END) AS TradePreviousTradingDay + FROM trading_day_ranked +), +position_items AS ( + SELECT sp.* + FROM swap_position sp + WHERE sp.SwapTradeId = @TradeId + AND sp.IsInitial = 1 + AND sp.Invalid = 0 + AND sp.PosiDirection = 2 + AND sp.UnderlyingCode IS NOT NULL + AND sp.UnderlyingCode <> '' +), +valuation_ranked AS ( + SELECT + sp.id AS SwapPositionId, bv.*, - ROW_NUMBER() OVER (PARTITION BY bv.bond_id ORDER BY bv.credibility ASC, bv.id ASC) AS RowNo - FROM china_bond_valuation bv - INNER JOIN previous_trading_day ptd - ON bv.valuation_date >= ptd.CandidateDate - AND bv.valuation_date < DATE_ADD(ptd.CandidateDate, INTERVAL 1 DAY) - WHERE bv.net_price IS NOT NULL + CASE WHEN rd.StartPreviousTradingDay IS NOT NULL AND DATE(bv.valuation_date) = rd.StartPreviousTradingDay THEN '开始日期上一交易日' ELSE '成交日期上一交易日' END AS DataSource, + ROW_NUMBER() OVER ( + PARTITION BY sp.id + ORDER BY + CASE WHEN rd.StartPreviousTradingDay IS NOT NULL AND DATE(bv.valuation_date) = rd.StartPreviousTradingDay THEN 0 ELSE 1 END, + bv.credibility ASC, + bv.id ASC + ) AS RowNo + FROM position_items sp + CROSS JOIN reference_days rd + INNER JOIN china_bond_valuation bv + ON bv.bond_id = sp.UnderlyingCode + AND bv.net_price IS NOT NULL + AND DATE(bv.valuation_date) IN (COALESCE(rd.StartPreviousTradingDay, rd.TradePreviousTradingDay), rd.TradePreviousTradingDay) ) SELECT - t.id AS TradeId, - t.TradeDate, - ptd.CandidateDate AS PreviousInterbankTradingDay, - + td.id AS TradeId, + td.StartDate, + td.TradeDate, + rd.StartPreviousTradingDay, + rd.TradePreviousTradingDay, + CASE + WHEN td.TradeDate IS NULL THEN '成交日期为空,变量会抛出异常' + WHEN rd.TradePreviousTradingDay IS NULL THEN '银行间日历缺失或HolidayJson无效,变量会抛出异常' + WHEN sp.PosiNetNoFeePrice IS NULL THEN '浮动支付端期初交割净价为空,变量会抛出异常' + WHEN bv.id IS NULL THEN '开始日期及成交日期的上一交易日均无行情,变量会抛出异常' + ELSE '正常' + END AS VariableStatus, sp.id AS SwapPositionId, - sp.SwapTradeId, - sp.IsInitial, - sp.Invalid, - sp.PosiDirection, sp.UnderlyingCode, - sp.UnderlyingInstrumentType, sp.PosiNetNoFeePrice, sp.PosiNetNoFeePrice * 100 AS PosiNetNoFeePrice_100, - bv.id AS BondValuationId, - bv.bond_id, bv.valuation_date, bv.credibility, bv.net_price AS BondValuationNetPrice, - + bv.DataSource, ABS(sp.PosiNetNoFeePrice * 100 - bv.net_price) AS DiffAbs, - CASE - WHEN ABS(sp.PosiNetNoFeePrice * 100 - bv.net_price) > 5 THEN 1 - ELSE 0 - END AS IsGreaterThan5 -FROM trade t -CROSS JOIN previous_trading_day ptd -INNER JOIN swap_position sp - ON sp.SwapTradeId = t.id - AND sp.IsInitial = 1 - AND sp.Invalid = 0 - AND sp.PosiDirection = 2 - AND sp.UnderlyingCode IS NOT NULL - AND sp.UnderlyingCode <> '' -LEFT JOIN bond_valuation_ranked bv - ON bv.bond_id = sp.UnderlyingCode - AND bv.RowNo = 1 -WHERE t.id = @TradeId -ORDER BY - ABS(sp.PosiNetNoFeePrice * 100 - bv.net_price) DESC, - sp.id ASC; + CASE WHEN ABS(sp.PosiNetNoFeePrice * 100 - bv.net_price) > @Threshold THEN 1 ELSE 0 END AS IsGreaterThanThreshold +FROM trade_dates td +CROSS JOIN reference_days rd +INNER JOIN position_items sp ON 1 = 1 +LEFT JOIN valuation_ranked bv ON bv.SwapPositionId = sp.id AND bv.RowNo = 1 +ORDER BY DiffAbs DESC, sp.id ASC; ``` --- ## 24. 规则 13 查询结果排查 SQL -规则 13:债券类收益率偏离(本地)。用于核对浮动支付端期初成交收益率和交易日前中债估值收益率;同一标的同一估值日存在不同可信度时,优先取 credibility=1。 +规则 13:债券类收益率偏离(本地)。用于核对浮动支付端期初成交收益率和中债估值收益率;同一标的同一估值日存在不同可信度时,优先取 credibility=1。 取数流程: ```text -1. 根据 TradeId 查 swap_position。 -2. 限定 IsInitial=1、Invalid=0、PosiDirection=2、UnderlyingCode 非空,取浮动支付端。 -3. 从浮动支付端取 InitYtm 和 UnderlyingCode。 +1. 根据 TradeId 查 trade.StartDate 和 trade.TradeDate。 +2. 通过银行间日历 Country=IB 分别计算开始日期和成交日期的上一银行间交易日。 +3. 根据 TradeId 查所有符合条件的浮动支付端,取 InitYtm 和 UnderlyingCode。 4. 用 swap_position.UnderlyingCode 关联 china_bond_valuation.bond_id。 -5. 限定 valuation_date < trade.TradeDate,取交易日前估值。 -6. 按 credibility ASC、valuation_date DESC 排序,优先 credibility=1,再取最近估值日。 -7. 计算 ABS(InitYtm * 100 - yield),大于 1 则命中。 +5. 对每条浮动支付端记录独立匹配行情:优先取开始日期上一交易日的 yield;该日无数据时,改取成交日期上一交易日的数据;两日均无数据才报错。 +6. 每条浮动支付端对应的同一标的、同一估值日存在多条行情时,按 credibility ASC、ID ASC 取优先记录;相同标的的多条浮动支付端不会合并。 +7. 逐条计算 ABS(InitYtm * 100 - yield),变量返回全部记录中的最大偏离值,命中说明只展示产生最大偏离值的记录。 ``` 规则公式: @@ -2735,6 +2857,105 @@ ORDER BY ABS(浮动支付端.InitYtm * 100 - 上一收盘日中债估值.yield) > 1 ``` +变量形式: + +变量名:债券类收益率偏离值 +DataType:Numeric +规则配置:债券类收益率偏离值 > 阈值(示例 1) + +变量取值表达式: + +```csharp +var tradeDates = DbContext.trade + .Where(t => t.id == TradeId) + .Select(t => new { t.StartDate, t.TradeDate }) + .FirstOrDefault(); + +if (tradeDates == null) + throw new Exception("交易不存在"); +if (!tradeDates.TradeDate.HasValue) + throw new Exception("成交日期为空"); + +DateTime? startPreviousTradingDay = tradeDates.StartDate.HasValue + ? YLErp.Modules.RiskEngine.RiskCalendarHelper.GetPreviousInterbankTradingDay(DbContext, tradeDates.StartDate.Value.Date) + : (DateTime?)null; +DateTime tradePreviousTradingDay = YLErp.Modules.RiskEngine.RiskCalendarHelper.GetPreviousInterbankTradingDay(DbContext, tradeDates.TradeDate.Value.Date); +DateTime primaryValuationDate = startPreviousTradingDay ?? tradePreviousTradingDay; +DateTime primaryValuationDateExclusive = primaryValuationDate.AddDays(1); +DateTime tradeValuationDateExclusive = tradePreviousTradingDay.AddDays(1); + +var positionItems = DbContext.swap_position + .Where(p => p.SwapTradeId == TradeId + && p.IsInitial + && !p.Invalid + && p.PosiDirection == 2 + && !string.IsNullOrEmpty(p.UnderlyingCode)) + .Select(p => new { p.id, p.UnderlyingCode, p.InitYtm }) + .ToList(); + +if (!positionItems.Any()) + throw new Exception("浮动支付端记录不存在"); + +var missingPositionValueIds = positionItems + .Where(p => !p.InitYtm.HasValue) + .Select(p => p.id.ToString()) + .ToList(); +if (missingPositionValueIds.Any()) + throw new Exception("浮动支付端期初成交收益率为空,记录ID:" + string.Join("、", missingPositionValueIds)); + +var underlyingCodes = positionItems.Select(p => p.UnderlyingCode).Distinct().ToList(); +var valuationRows = DbContext.china_bond_valuation + .Where(v => underlyingCodes.Contains(v.bond_id) + && v.yield.HasValue + && ((v.valuation_date >= primaryValuationDate && v.valuation_date < primaryValuationDateExclusive) + || (v.valuation_date >= tradePreviousTradingDay && v.valuation_date < tradeValuationDateExclusive))) + .Select(v => new { v.id, v.bond_id, v.valuation_date, v.credibility, v.yield }) + .ToList(); + +var selectedItems = positionItems + .Select(p => new + { + Position = p, + Valuation = valuationRows + .Where(v => v.bond_id == p.UnderlyingCode) + .OrderBy(v => v.valuation_date.Date == primaryValuationDate ? 0 : 1) + .ThenBy(v => v.credibility) + .ThenBy(v => v.id) + .FirstOrDefault() + }) + .ToList(); + +var missingValuationItems = selectedItems + .Where(x => x.Valuation == null) + .Select(x => $"记录ID {x.Position.id},标的{x.Position.UnderlyingCode}") + .ToList(); +if (missingValuationItems.Any()) + throw new Exception($"未找到开始日期上一交易日{primaryValuationDate:yyyy-MM-dd}及成交日期上一交易日{tradePreviousTradingDay:yyyy-MM-dd}的中债估值收益率:" + string.Join(";", missingValuationItems)); + +var deviationItems = selectedItems + .Select(x => new + { + x.Position.id, + x.Position.UnderlyingCode, + PositionValue = x.Position.InitYtm.Value * 100m, + MarketDate = x.Valuation.valuation_date, + MarketDateSource = startPreviousTradingDay.HasValue + && x.Valuation.valuation_date.Date == startPreviousTradingDay.Value + ? "开始日期上一交易日" + : "成交日期上一交易日", + MarketValue = x.Valuation.yield.Value, + DiffAbs = Math.Abs(x.Position.InitYtm.Value * 100m - x.Valuation.yield.Value) + }) + .OrderByDescending(x => x.DiffAbs) + .ThenBy(x => x.id) + .ToList(); + +var maxDeviationItem = deviationItems.First(); +string detailMessage = $"交易ID {TradeId},浮动支付端记录ID {maxDeviationItem.id},标的{maxDeviationItem.UnderlyingCode}:期初成交收益率{maxDeviationItem.PositionValue:0.#########}%,按{maxDeviationItem.MarketDateSource}取{maxDeviationItem.MarketDate:yyyy-MM-dd}中债估值收益率{maxDeviationItem.MarketValue:0.#########}%,最大偏离{maxDeviationItem.DiffAbs:0.#########}%"; + +return new RiskVariableValueDetail(maxDeviationItem.DiffAbs, detailMessage); +``` + 注释规则定义: ```csharp @@ -2756,73 +2977,218 @@ ABS(浮动支付端.InitYtm * 100 - 上一收盘日中债估值.yield) > 1 ``` ```sql -SET @TradeId = 3001699; +SET @TradeId = 3001744; +SET @Threshold = 3; +WITH RECURSIVE trade_dates AS ( + SELECT id, DATE(StartDate) AS StartDate, DATE(TradeDate) AS TradeDate + FROM trade + WHERE id = @TradeId +), +candidate_dates AS ( + SELECT 'StartDate' AS DateType, StartDate AS BaseDate, StartDate - INTERVAL 1 DAY AS CandidateDate + FROM trade_dates + WHERE StartDate IS NOT NULL + UNION ALL + SELECT 'TradeDate', TradeDate, TradeDate - INTERVAL 1 DAY + FROM trade_dates + WHERE TradeDate IS NOT NULL + UNION ALL + SELECT DateType, BaseDate, CandidateDate - INTERVAL 1 DAY + FROM candidate_dates + WHERE CandidateDate > DATE_SUB(BaseDate, INTERVAL 370 DAY) +), +trading_day_ranked AS ( + SELECT cd.DateType, cd.CandidateDate, + ROW_NUMBER() OVER (PARTITION BY cd.DateType ORDER BY cd.CandidateDate DESC) AS RowNo + FROM candidate_dates cd + INNER JOIN calendar c + ON c.Year = YEAR(cd.CandidateDate) + AND UPPER(c.Country) = 'IB' + AND (c.ValidState IS NULL OR c.ValidState <> 'InValid') + WHERE JSON_VALID(c.HolidayJson) = 1 + AND JSON_TYPE(c.HolidayJson) = 'ARRAY' + AND JSON_CONTAINS(c.HolidayJson, JSON_QUOTE(DATE_FORMAT(cd.CandidateDate, '%Y,%m,%d'))) = 0 +), +reference_days AS ( + SELECT + MAX(CASE WHEN DateType = 'StartDate' AND RowNo = 1 THEN CandidateDate END) AS StartPreviousTradingDay, + MAX(CASE WHEN DateType = 'TradeDate' AND RowNo = 1 THEN CandidateDate END) AS TradePreviousTradingDay + FROM trading_day_ranked +), +position_items AS ( + SELECT sp.* + FROM swap_position sp + WHERE sp.SwapTradeId = @TradeId + AND sp.IsInitial = 1 + AND sp.Invalid = 0 + AND sp.PosiDirection = 2 + AND sp.UnderlyingCode IS NOT NULL + AND sp.UnderlyingCode <> '' +), +valuation_ranked AS ( + SELECT + sp.id AS SwapPositionId, + bv.*, + CASE WHEN rd.StartPreviousTradingDay IS NOT NULL AND DATE(bv.valuation_date) = rd.StartPreviousTradingDay THEN '开始日期上一交易日' ELSE '成交日期上一交易日' END AS DataSource, + ROW_NUMBER() OVER ( + PARTITION BY sp.id + ORDER BY + CASE WHEN rd.StartPreviousTradingDay IS NOT NULL AND DATE(bv.valuation_date) = rd.StartPreviousTradingDay THEN 0 ELSE 1 END, + bv.credibility ASC, + bv.id ASC + ) AS RowNo + FROM position_items sp + CROSS JOIN reference_days rd + INNER JOIN china_bond_valuation bv + ON bv.bond_id = sp.UnderlyingCode + AND bv.yield IS NOT NULL + AND DATE(bv.valuation_date) IN (COALESCE(rd.StartPreviousTradingDay, rd.TradePreviousTradingDay), rd.TradePreviousTradingDay) +) SELECT - t.id AS TradeId, - t.TradeDate, - + td.id AS TradeId, + td.StartDate, + td.TradeDate, + rd.StartPreviousTradingDay, + rd.TradePreviousTradingDay, + CASE + WHEN td.TradeDate IS NULL THEN '成交日期为空,变量会抛出异常' + WHEN rd.TradePreviousTradingDay IS NULL THEN '银行间日历缺失或HolidayJson无效,变量会抛出异常' + WHEN sp.InitYtm IS NULL THEN '浮动支付端期初成交收益率为空,变量会抛出异常' + WHEN bv.id IS NULL THEN '开始日期及成交日期的上一交易日均无行情,变量会抛出异常' + ELSE '正常' + END AS VariableStatus, sp.id AS SwapPositionId, - sp.SwapTradeId, - sp.IsInitial, - sp.Invalid, - sp.PosiDirection, sp.UnderlyingCode, - sp.UnderlyingInstrumentType, sp.InitYtm, sp.InitYtm * 100 AS InitYtm_100, - bv.id AS BondValuationId, - bv.bond_id, bv.valuation_date, bv.credibility, bv.yield AS BondValuationYield, - + bv.DataSource, ABS(sp.InitYtm * 100 - bv.yield) AS DiffAbs, - CASE - WHEN ABS(sp.InitYtm * 100 - bv.yield) > 1 THEN 1 - ELSE 0 - END AS IsGreaterThan1 -FROM trade t -INNER JOIN swap_position sp - ON sp.SwapTradeId = t.id - AND sp.IsInitial = 1 - AND sp.Invalid = 0 - AND sp.PosiDirection = 2 - AND sp.UnderlyingCode IS NOT NULL - AND sp.UnderlyingCode <> '' -LEFT JOIN china_bond_valuation bv - ON bv.bond_id = sp.UnderlyingCode - AND bv.valuation_date < DATE(t.TradeDate) -WHERE t.id = @TradeId -ORDER BY - bv.credibility ASC, - bv.valuation_date DESC -LIMIT 1; + CASE WHEN ABS(sp.InitYtm * 100 - bv.yield) > @Threshold THEN 1 ELSE 0 END AS IsGreaterThanThreshold +FROM trade_dates td +CROSS JOIN reference_days rd +INNER JOIN position_items sp ON 1 = 1 +LEFT JOIN valuation_ranked bv ON bv.SwapPositionId = sp.id AND bv.RowNo = 1 +ORDER BY DiffAbs DESC, sp.id ASC; ``` --- -## 22. 规则 14 查询结果排查 SQL +## 25. 规则 14 查询结果排查 SQL -规则 14:非债券类价格偏离(本地)。用于核对普通收益互换页面填写的期初标的价格和交易日前上一日收盘价。 +规则 14:非债券类价格偏离(本地)。用于核对普通收益互换页面填写的期初标的价格和上一交易日标的收盘价。 取数流程: ```text -1. 根据 TradeId 查 trade。 -2. 根据 TradeId 查 swap_positionce od_commodity_flyingCode 实际映射数据库列 FutureContractId,数据库排查时看 FutureContractId。 -3. 限定 ValueDate < trade.TradeDate,取交易日前行情。 -5. 按 ValueDate DESC 排序,取最近一日收盘价。 -4. 计算 ABS(PosiGrossPrice * 100 - ClosePrice),大于 5 则命中。 -5`` -6 +1. 根据 TradeId 查 trade.StartDate 和 trade.TradeDate。 +2. 通过交易所日历 Country=CHN 分别计算开始日期和成交日期的上一交易日。 +3. 根据 TradeId 查所有符合条件的浮动支付端,取 PosiGrossPrice 和 UnderlyingCode。 +4. 用 swap_position.UnderlyingCode 关联 eod_commodity_future_price.UnderlyingCode;该模型属性映射数据库列 FutureContractId。 +5. 每个标的优先取开始日期上一交易日的 ClosePrice;该日无数据时,改取成交日期上一交易日的数据;两日均无数据才报错。 +6. 逐条计算 ABS(PosiGrossPrice * 100 - ClosePrice),变量返回全部记录中的最大偏离值,命中说明只展示产生最大偏离值的记录。 +``` + 规则公式: ```text ABS(浮动支付端.PosiGrossPrice * 100 - 上一日行情.ClosePrice) > 5 ``` +变量形式: + +变量名:非债券类价格偏离值 +DataType:Numeric +规则配置:非债券类价格偏离值 > 阈值(示例 5) + +变量取值表达式: + +```csharp +var tradeDates = DbContext.trade + .Where(t => t.id == TradeId) + .Select(t => new { t.StartDate, t.TradeDate }) + .FirstOrDefault(); + +if (tradeDates == null) + throw new Exception("交易不存在"); +if (!tradeDates.TradeDate.HasValue) + throw new Exception("成交日期为空"); + +DateTime? startPreviousTradingDay = tradeDates.StartDate.HasValue + ? YLErp.Modules.RiskEngine.RiskCalendarHelper.GetPreviousExchangeTradingDay(DbContext, tradeDates.StartDate.Value.Date) + : (DateTime?)null; +DateTime tradePreviousTradingDay = YLErp.Modules.RiskEngine.RiskCalendarHelper.GetPreviousExchangeTradingDay(DbContext, tradeDates.TradeDate.Value.Date); +DateTime primaryMarketDate = startPreviousTradingDay ?? tradePreviousTradingDay; +DateTime primaryMarketDateExclusive = primaryMarketDate.AddDays(1); +DateTime tradeMarketDateExclusive = tradePreviousTradingDay.AddDays(1); + +var positionItems = DbContext.swap_position + .Where(p => p.SwapTradeId == TradeId + && p.IsInitial + && !p.Invalid + && p.PosiDirection == 2 + && !string.IsNullOrEmpty(p.UnderlyingCode)) + .Select(p => new { p.id, p.UnderlyingCode, p.PosiGrossPrice }) + .ToList(); + +if (!positionItems.Any()) + throw new Exception("浮动支付端记录不存在"); + +var underlyingCodes = positionItems.Select(p => p.UnderlyingCode).Distinct().ToList(); +var marketRows = DbContext.eod_commodity_future_price + .Where(e => underlyingCodes.Contains(e.UnderlyingCode) + && ((e.ValueDate >= primaryMarketDate && e.ValueDate < primaryMarketDateExclusive) + || (e.ValueDate >= tradePreviousTradingDay && e.ValueDate < tradeMarketDateExclusive))) + .Select(e => new { e.id, e.UnderlyingCode, e.ValueDate, e.ClosePrice }) + .ToList(); + +var selectedItems = positionItems + .Select(p => new + { + Position = p, + Market = marketRows + .Where(e => e.UnderlyingCode == p.UnderlyingCode) + .OrderBy(e => e.ValueDate.Date == primaryMarketDate ? 0 : 1) + .ThenBy(e => e.id) + .FirstOrDefault() + }) + .ToList(); + +var missingMarketItems = selectedItems + .Where(x => x.Market == null) + .Select(x => $"记录ID {x.Position.id},标的{x.Position.UnderlyingCode}") + .ToList(); +if (missingMarketItems.Any()) + throw new Exception($"未找到开始日期上一交易日{primaryMarketDate:yyyy-MM-dd}及成交日期上一交易日{tradePreviousTradingDay:yyyy-MM-dd}的标的收盘价:" + string.Join(";", missingMarketItems)); + +var deviationItems = selectedItems + .Select(x => new + { + x.Position.id, + x.Position.UnderlyingCode, + PositionValue = x.Position.PosiGrossPrice * 100m, + MarketDate = x.Market.ValueDate, + MarketDateSource = startPreviousTradingDay.HasValue + && x.Market.ValueDate.Date == startPreviousTradingDay.Value + ? "开始日期上一交易日" + : "成交日期上一交易日", + MarketValue = Convert.ToDecimal(x.Market.ClosePrice), + DiffAbs = Math.Abs(x.Position.PosiGrossPrice * 100m - Convert.ToDecimal(x.Market.ClosePrice)) + }) + .OrderByDescending(x => x.DiffAbs) + .ThenBy(x => x.id) + .ToList(); + +var maxDeviationItem = deviationItems.First(); +string detailMessage = $"交易ID {TradeId},浮动支付端记录ID {maxDeviationItem.id},标的{maxDeviationItem.UnderlyingCode}:期初标的价格{maxDeviationItem.PositionValue:0.#########},按{maxDeviationItem.MarketDateSource}取{maxDeviationItem.MarketDate:yyyy-MM-dd}收盘价{maxDeviationItem.MarketValue:0.#########},最大偏离{maxDeviationItem.DiffAbs:0.#########}"; + +return new RiskVariableValueDetail(maxDeviationItem.DiffAbs, detailMessage); +``` + 注释规则定义: ```csharp @@ -2846,62 +3212,114 @@ ABS(浮动支付端.PosiGrossPrice * 100 - 上一日行情.ClosePrice) > 5 ```sql SET @TradeId = 3001699; +SET @Threshold = 5; +WITH RECURSIVE trade_dates AS ( + SELECT id, DATE(StartDate) AS StartDate, DATE(TradeDate) AS TradeDate + FROM trade + WHERE id = @TradeId +), +candidate_dates AS ( + SELECT 'StartDate' AS DateType, StartDate AS BaseDate, StartDate - INTERVAL 1 DAY AS CandidateDate + FROM trade_dates + WHERE StartDate IS NOT NULL + UNION ALL + SELECT 'TradeDate', TradeDate, TradeDate - INTERVAL 1 DAY + FROM trade_dates + WHERE TradeDate IS NOT NULL + UNION ALL + SELECT DateType, BaseDate, CandidateDate - INTERVAL 1 DAY + FROM candidate_dates + WHERE CandidateDate > DATE_SUB(BaseDate, INTERVAL 370 DAY) +), +trading_day_ranked AS ( + SELECT cd.DateType, cd.CandidateDate, + ROW_NUMBER() OVER (PARTITION BY cd.DateType ORDER BY cd.CandidateDate DESC) AS RowNo + FROM candidate_dates cd + INNER JOIN calendar c + ON c.Year = YEAR(cd.CandidateDate) + AND UPPER(c.Country) = 'CHN' + AND (c.ValidState IS NULL OR c.ValidState <> 'InValid') + WHERE JSON_VALID(c.HolidayJson) = 1 + AND JSON_TYPE(c.HolidayJson) = 'ARRAY' + AND JSON_CONTAINS(c.HolidayJson, JSON_QUOTE(DATE_FORMAT(cd.CandidateDate, '%Y,%m,%d'))) = 0 +), +reference_days AS ( + SELECT + MAX(CASE WHEN DateType = 'StartDate' AND RowNo = 1 THEN CandidateDate END) AS StartPreviousTradingDay, + MAX(CASE WHEN DateType = 'TradeDate' AND RowNo = 1 THEN CandidateDate END) AS TradePreviousTradingDay + FROM trading_day_ranked +), +position_items AS ( + SELECT sp.* + FROM swap_position sp + WHERE sp.SwapTradeId = @TradeId + AND sp.IsInitial = 1 + AND sp.Invalid = 0 + AND sp.PosiDirection = 2 + AND sp.UnderlyingCode IS NOT NULL + AND sp.UnderlyingCode <> '' +), +market_ranked AS ( + SELECT + sp.id AS SwapPositionId, + e.*, + CASE WHEN rd.StartPreviousTradingDay IS NOT NULL AND DATE(e.ValueDate) = rd.StartPreviousTradingDay THEN '开始日期上一交易日' ELSE '成交日期上一交易日' END AS DataSource, + ROW_NUMBER() OVER ( + PARTITION BY sp.id + ORDER BY + CASE WHEN rd.StartPreviousTradingDay IS NOT NULL AND DATE(e.ValueDate) = rd.StartPreviousTradingDay THEN 0 ELSE 1 END, + e.id ASC + ) AS RowNo + FROM position_items sp + CROSS JOIN reference_days rd + INNER JOIN eod_commodity_future_price e + ON e.FutureContractId = sp.UnderlyingCode + AND DATE(e.ValueDate) IN (COALESCE(rd.StartPreviousTradingDay, rd.TradePreviousTradingDay), rd.TradePreviousTradingDay) +) SELECT - t.id AS TradeId, - t.UnderlyingCode AS TradeUnderlyingCode, - t.TradeDate, - + td.id AS TradeId, + td.StartDate, + td.TradeDate, + rd.StartPreviousTradingDay, + rd.TradePreviousTradingDay, + CASE + WHEN td.TradeDate IS NULL THEN '成交日期为空,变量会抛出异常' + WHEN rd.TradePreviousTradingDay IS NULL THEN '交易所日历缺失或HolidayJson无效,变量会抛出异常' + WHEN e.id IS NULL THEN '开始日期及成交日期的上一交易日均无行情,变量会抛出异常' + ELSE '正常' + END AS VariableStatus, sp.id AS SwapPositionId, - sp.SwapTradeId, - sp.IsInitial, - sp.Invalid, - sp.PosiDirection, sp.UnderlyingCode AS SwapPositionUnderlyingCode, - sp.UnderlyingInstrumentType, sp.PosiGrossPrice, sp.PosiGrossPrice * 100 AS PosiGrossPrice_100, - e.id AS EodPriceId, e.FutureContractId AS EodFutureContractId, e.ValueDate, e.ClosePrice, - + e.DataSource, ABS(sp.PosiGrossPrice * 100 - e.ClosePrice) AS DiffAbs, - CASE - WHEN ABS(sp.PosiGrossPrice * 100 - e.ClosePrice) > 5 THEN 1 - ELSE 0 - END AS IsGreaterThan5 -FROM trade t -INNER JOIN swap_position sp - ON sp.SwapTradeId = t.id - AND sp.IsInitial = 1 - AND sp.Invalid = 0 - AND sp.PosiDirection = 2 - AND sp.UnderlyingCode IS NOT NULL - AND sp.UnderlyingCode <> '' -LEFT JOIN eod_commodity_future_price e - ON e.FutureContractId = sp.UnderlyingCode - AND e.ValueDate < DATE(t.TradeDate) -WHERE t.id = @TradeId -ORDER BY - e.ValueDate DESC -LIMIT 1; + CASE WHEN ABS(sp.PosiGrossPrice * 100 - e.ClosePrice) > @Threshold THEN 1 ELSE 0 END AS IsGreaterThanThreshold +FROM trade_dates td +CROSS JOIN reference_days rd +INNER JOIN position_items sp ON 1 = 1 +LEFT JOIN market_ranked e ON e.SwapPositionId = sp.id AND e.RowNo = 1 +ORDER BY DiffAbs DESC, sp.id ASC; ``` --- -## 23. 规则 15 查询结果排查 SQL +## 26. 规则 15 查询结果排查 SQL -规则 15:单一交易对手累计标的数量超阈值(本地)。用于核对同一交易对手实时存续持仓下的去重标的数量是否超过 10 个。 +规则 15:单一交易对手累计标的数量超阈值(本地)。用于核对同一交易对手所有有效交易的实时存续持仓下去重标的数量是否超过 10 个;当前交易如果已满足有效交易和实时存续持仓口径,则会一并计入。 取数流程: ```text 1. 根据当前 TradeId 查 trade,取当前交易对手 ClientId。 -2. 查询同一 ClientId 下的交易。 +2. 查询同一 ClientId 下的全部交易,包含当前交易本身。 3. 限定交易有效:ValidState != 'InValid'。 -4. 关联 swap_position,按实时持仓判断是否存续:IsInitial=0、PosiQuantity>0、Invalid=0、PosiDirection>0。 +4. 关联 swap_position,按实时持仓判断是否存续:IsInitial=0、PosiQuantity>0、Invalid=0、PosiDirection>0;当前交易只有存在符合该口径的持仓行时才会贡献标的数量。 5. 限定 UnderlyingCode 非空。 6. 对 swap_position.UnderlyingCode 去重计数。 7. 去重数量 > 10 时命中。 @@ -2920,7 +3338,7 @@ COUNT(DISTINCT swap_position.UnderlyingCode) > 10 //{ // Id = 1000015, // RuleName = "单一交易对手累计标的数量超阈值(本地)", -// RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易对手 ClientId,再查询同一交易对手有效交易对应的实时存续持仓 swap_position.UnderlyingCode 去重数量。实时存续持仓口径:IsInitial=false、PosiQuantity>0、Invalid=false、PosiDirection>0 且 UnderlyingCode 非空。计算逻辑:同一交易对手累计标的数量超过 10 个时触发审批。", +// RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易对手 ClientId,再查询同一交易对手全部有效交易对应的实时存续持仓 swap_position.UnderlyingCode 去重数量;当前交易如果已满足有效交易和实时存续持仓口径,则会一并计入。实时存续持仓口径:IsInitial=false、PosiQuantity>0、Invalid=false、PosiDirection>0 且 UnderlyingCode 非空。计算逻辑:同一交易对手累计标的数量超过 10 个时触发审批。", // RuleExpr= DbContext.swap_position.Where(p => !string.IsNullOrEmpty(p.UnderlyingCode) && !p.IsInitial && p.PosiQuantity > 0 && !p.Invalid && p.PosiDirection > 0 && DbContext.trade.Any(t => t.id == p.SwapTradeId && t.ValidState != "InValid" && t.ClientId == DbContext.trade.First(x => x.id == TradeId).ClientId)).Select(p => p.UnderlyingCode).Distinct().Count() > 10 // Version = 1, // Status = RiskRuleStatus.Active, @@ -2972,7 +3390,7 @@ string underlyingList = underlyingCodes.Any() return new RiskVariableValueDetail( distinctCount, - $"交易对手ID为{currentClientId},存续标的共{distinctCount}个:{underlyingList}"); + $"交易ID {TradeId},交易对手ID为{currentClientId},存续标的共{distinctCount}个:{underlyingList}"); ``` 汇总 SQL: @@ -2989,10 +3407,10 @@ SELECT ELSE 0 END AS IsGreaterThan10 FROM trade cur -INNER JOIN trade t +LEFT JOIN trade t ON t.ClientId = cur.ClientId - AND t.ValidState <> 'InValid' -INNER JOIN swap_position sp + AND (t.ValidState IS NULL OR t.ValidState <> 'InValid') +LEFT JOIN swap_position sp ON sp.SwapTradeId = t.id AND sp.IsInitial = 0 AND sp.PosiQuantity > 0 @@ -3022,7 +3440,7 @@ SELECT FROM trade cur INNER JOIN trade t ON t.ClientId = cur.ClientId - AND t.ValidState <> 'InValid' + AND (t.ValidState IS NULL OR t.ValidState <> 'InValid') INNER JOIN swap_position sp ON sp.SwapTradeId = t.id AND sp.IsInitial = 0 @@ -3042,9 +3460,9 @@ ORDER BY --- -## 24. 规则 16 查询结果排查 SQL +## 27. 规则 16 查询结果排查 SQL -规则 16:多头支付固定端利率偏离(本地)。用于核对利息端收入固定利息方向的点差百分比绝对值是否低于阈值。 +规则 16:多头支付固定端利率偏离(本地)。用于核对利息端收入固定利息方向的点差百分比绝对值是否大于阈值。 取数流程: @@ -3082,27 +3500,77 @@ ABS(利息端.InterestRateDefault * 100) > 5 //}); ``` +变量形式: + +变量名:固定端利率偏离绝对值 +DataType:Numeric +规则配置:固定端利率偏离绝对值 > 阈值(示例 5) + +变量取值表达式: + +```csharp +var interestItems = DbContext.swap_position + .Where(p => p.SwapTradeId == TradeId + && p.InterestDirection == 1) + .Select(p => new + { + p.id, + p.InterestRateDefault, + InterestRateDeviation = Math.Abs(p.InterestRateDefault * 100m) + }) + .ToList(); + +if (!interestItems.Any()) + throw new Exception("利息端收取方向记录不存在"); + +var maxDeviationItem = interestItems + .OrderByDescending(p => p.InterestRateDeviation) + .ThenBy(p => p.id) + .First(); + +return new RiskVariableValueDetail( + maxDeviationItem.InterestRateDeviation, + $"交易ID {TradeId},利息端记录ID为{maxDeviationItem.id},点差为{maxDeviationItem.InterestRateDefault * 100m:0.#########}%,最大偏离绝对值为{maxDeviationItem.InterestRateDeviation:0.#########}%"); +``` + +汇总 SQL:与变量一样取全部符合条件记录中的最大偏离值;命中说明只展示产生最大偏离值的记录。 + ```sql SET @TradeId = 3001699; +SET @Threshold = 5; + +SELECT + MAX(ABS(sp.InterestRateDefault * 100)) AS MaxInterestRateDeviation, + CASE + WHEN MAX(ABS(sp.InterestRateDefault * 100)) > @Threshold THEN 1 + ELSE 0 + END AS IsGreaterThanThreshold +FROM swap_position sp +WHERE sp.SwapTradeId = @TradeId + AND sp.InterestDirection = 1; +``` + +明细 SQL:用于排查全部利息端记录;变量命中说明只展示产生最大偏离值的记录。 + +```sql +SET @TradeId = 3001699; +SET @Threshold = 5; SELECT sp.id AS SwapPositionId, sp.SwapTradeId, - sp.IsInitial, - sp.Invalid, - sp.PosiDirection, sp.InterestDirection, - sp.FloatRateUnderlyingCode, sp.InterestRateDefault, sp.InterestRateDefault * 100 AS InterestRateDefault_100, ABS(sp.InterestRateDefault * 100) AS DiffAbs, CASE - WHEN ABS(sp.InterestRateDefault * 100) > 3 THEN 1 + WHEN ABS(sp.InterestRateDefault * 100) > @Threshold THEN 1 ELSE 0 - END AS IsGreaterThan + END AS IsGreaterThanThreshold FROM swap_position sp WHERE sp.SwapTradeId = @TradeId - AND sp.InterestDirection = 1; + AND sp.InterestDirection = 1 +ORDER BY DiffAbs DESC, sp.id ASC; ``` ---