diff --git a/YLErpDAL/Modules/RiskEngine/测试用例.md b/YLErpDAL/Modules/RiskEngine/测试用例.md index 00050f5c..7d29142a 100644 --- a/YLErpDAL/Modules/RiskEngine/测试用例.md +++ b/YLErpDAL/Modules/RiskEngine/测试用例.md @@ -1313,7 +1313,987 @@ ignoreRiskRuleIds = A --- -## 12. 规则 12 查询结果排查 SQL +## 12. 规则 1 查询结果排查 SQL + +规则 1:挂钩标的集中度(本地)。用于核对同一标的在本笔及所有存续/审批中交易中的总名义本金占该标的发行余额的比例。 + +取数流程: + +```text +1. 根据 TradeId 查当前 trade,取 UnderlyingId、UnderlyingCode。 +2. 查询 trade 表中同一 UnderlyingId 的有效交易。 +3. 限定 ParentTradeId = 0,且交易状态为存续状态或“审批中”。 +4. 汇总这些交易的 StockEqvNotional,作为同一标的在本笔及所有存续/审批中交易中的总名义本金。 +5. 根据当前交易 UnderlyingCode 查 underlying_manager.ExJson 中债券 IssueSize。 +6. IssueSize 库内单位为亿,乘以 100000000 还原为元,作为该标的发行余额。 +7. 计算 总名义本金 ÷ 发行余额 × 100%,大于 30% 时命中。 +``` + +规则公式: + +```text +同一标的在本笔及所有存续/审批中交易中的总名义本金 ÷ 该标的发行余额 × 100% > 30% +``` + +注释规则定义: + +```csharp +//rules.Add(new RiskRule +//{ +// Id = 1000001, +// RuleName = "挂钩标的集中度校验(本地)", +// RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易;分子查询 trade 表同一标的存续/审批中交易 StockEqvNotional 汇总;分母查询 underlying_manager.ExJson 中债券 IssueSize(亿)。计算逻辑:同一标的总名义本金 ÷ 发行量 × 100%,发行量乘 100000000 还原为元,结果大于 30% 时触发审批。", +// RuleExpr = "Convert.ToDecimal(DbContext.trade.Where(t => t.ValidState != \"InValid\" && t.UnderlyingId == DbContext.trade.First(x => x.id == TradeId).UnderlyingId && t.ParentTradeId == 0 && (ConsTrade.NeedMarginTradeStatusList.Contains(t.TradeStatus) || t.TradeStatus == \"审批中\")).Sum(t => (double?)t.StockEqvNotional) ?? 0d) / (JsonConvert.DeserializeObject(DbContext.underlying_manager.Where(u => u.UnderlyingCode == DbContext.trade.First(x => x.id == TradeId).UnderlyingCode).Select(u => u.ExJson).FirstOrDefault()).IssueSize.Value * 100000000m) * 100m > 30m", +// Version = 1, +// Status = RiskRuleStatus.Active, +// OptId = 0, +// OptName = "system", +// OptDate = DateTime.Now, +// UpdateOptId = 0, +// UpdateOptName = "system", +// UpdateDate = DateTime.Now +//}); +``` + +```sql +SET @TradeId = 3001699; + +SELECT + cur.id AS CurrentTradeId, + cur.UnderlyingId, + cur.UnderlyingCode, + + SUM(IFNULL(t.StockEqvNotional, 0)) AS TotalStockEqvNotional, + CAST(JSON_UNQUOTE(JSON_EXTRACT(um.ExJson, '$.IssueSize')) AS DECIMAL(30, 10)) AS IssueSizeYi, + CAST(JSON_UNQUOTE(JSON_EXTRACT(um.ExJson, '$.IssueSize')) AS DECIMAL(30, 10)) * 100000000 AS IssueSizeAmount, + + SUM(IFNULL(t.StockEqvNotional, 0)) + / (CAST(JSON_UNQUOTE(JSON_EXTRACT(um.ExJson, '$.IssueSize')) AS DECIMAL(30, 10)) * 100000000) + * 100 AS ConcentrationPercent, + CASE + WHEN SUM(IFNULL(t.StockEqvNotional, 0)) + / (CAST(JSON_UNQUOTE(JSON_EXTRACT(um.ExJson, '$.IssueSize')) AS DECIMAL(30, 10)) * 100000000) + * 100 > 30 THEN 1 + ELSE 0 + END AS IsGreaterThan30 +FROM trade cur +INNER JOIN trade t + ON t.UnderlyingId = cur.UnderlyingId + AND t.ValidState <> 'InValid' + AND t.ParentTradeId = 0 + AND t.TradeStatus IN ('确认成交', '平仓待复核', '提前终止拒绝', '行权待复核', '互换待复核', '审批中') +LEFT JOIN underlying_manager um + ON um.UnderlyingCode = cur.UnderlyingCode +WHERE cur.id = @TradeId +GROUP BY + cur.id, + cur.UnderlyingId, + cur.UnderlyingCode, + um.ExJson; +``` + +交易明细 SQL:用于核对分子中纳入汇总的交易明细;是否超限以汇总 SQL 的 `ConcentrationPercent` 为准。 + +```sql +SET @TradeId = 3001699; + +SELECT + cur.id AS CurrentTradeId, + cur.UnderlyingId, + cur.UnderlyingCode, + + t.id AS RelatedTradeId, + t.TradeStatus, + t.ValidState, + t.ParentTradeId, + t.StockEqvNotional +FROM trade cur +INNER JOIN trade t + ON t.UnderlyingId = cur.UnderlyingId + AND t.ValidState <> 'InValid' + AND t.ParentTradeId = 0 + AND t.TradeStatus IN ('确认成交', '平仓待复核', '提前终止拒绝', '行权待复核', '互换待复核', '审批中') +WHERE cur.id = @TradeId +ORDER BY + t.id; +``` + +--- + +## 13. 规则 2 查询结果排查 SQL + +规则 2:挂钩标的到期日小于合约到期日(本地)。用于核对当前交易的合约到期日是否晚于挂钩标的到期日。 + +取数流程: + +```text +1. 根据 TradeId 查当前 trade,取 UnderlyingCode、ExerciseDate。 +2. 根据 trade.UnderlyingCode 关联 underlying_manager。 +3. 从 underlying_manager 取 MaturityDate,作为挂钩标的到期日。 +4. 比较 underlying_manager.MaturityDate 和 trade.ExerciseDate。 +5. 当挂钩标的到期日 < 合约到期日时命中。 +``` + +规则公式: + +```text +挂钩标的到期日 < 合约到期日 +``` + +规则字段口径: + +```text +挂钩标的到期日:underlying_manager.MaturityDate +合约到期日:trade.ExerciseDate +``` + +注释规则定义: + +```csharp +//rules.Add(new RiskRule +//{ +// Id = 1000002, +// RuleName = "挂钩标的到期日小于合约到期日(本地)", +// RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 UnderlyingCode 和 ExerciseDate,ExerciseDate 对应合约到期日;通过 DbContext.underlying_manager 按 UnderlyingCode 取 MaturityDate,MaturityDate 对应挂钩标的到期日。计算逻辑:挂钩标的到期日小于合约到期日时触发禁止。", +// RuleExpr = "DbContext.underlying_manager.First(u => u.UnderlyingCode == DbContext.trade.First(t => t.id == TradeId).UnderlyingCode).MaturityDate.Value < DbContext.trade.First(t => t.id == TradeId).ExerciseDate.Value", +// Version = 1, +// Status = RiskRuleStatus.Active, +// OptId = 0, +// OptName = "system", +// OptDate = DateTime.Now, +// UpdateOptId = 0, +// UpdateOptName = "system", +// UpdateDate = DateTime.Now +//}); +``` + +```sql +SET @TradeId = 3001699; + +SELECT + t.id AS TradeId, + t.TradeType, + t.UnderlyingCode, + t.ExerciseDate AS ContractExerciseDate, + + um.id AS UnderlyingManagerId, + um.UnderlyingCode AS UnderlyingManagerCode, + um.UnderlyingName, + um.MaturityDate AS UnderlyingMaturityDate, + + CASE + WHEN um.MaturityDate < t.ExerciseDate THEN 1 + ELSE 0 + END AS IsUnderlyingMaturityLessThanContractExerciseDate +FROM trade t +LEFT JOIN underlying_manager um + ON um.UnderlyingCode = t.UnderlyingCode +WHERE t.id = @TradeId; +``` + +--- + +## 14. 规则 3 查询结果排查 SQL + +规则 3:名义本金超阈值(本地)。用于核对当前交易的开仓名义本金是否超过阈值。 + +取数流程: + +```text +1. 根据 TradeId 查当前 trade。 +2. 取 trade.OriginalStockEqvNotional,作为开仓名义本金。 +3. 当 OriginalStockEqvNotional > 100000000 时命中。 +``` + +规则公式: + +```text +开仓名义本金 > 100000000 +``` + +规则字段口径: + +```text +开仓名义本金:trade.OriginalStockEqvNotional +``` + +注释规则定义: + +```csharp +//rules.Add(new RiskRule +//{ +// Id = 1000003, +// RuleName = "名义本金超阈值(本地)", +// RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 OriginalStockEqvNotional,对应 trade 表开仓名义本金字段。计算逻辑:OriginalStockEqvNotional 大于 100000000 时触发审批。", +// RuleExpr = "DbContext.trade.First(t => t.id == TradeId).OriginalStockEqvNotional > 100000000", +// Version = 1, +// Status = RiskRuleStatus.Active, +// OptId = 0, +// OptName = "system", +// OptDate = DateTime.Now, +// UpdateOptId = 0, +// UpdateOptName = "system", +// UpdateDate = DateTime.Now +//}); +``` + +```sql +SET @TradeId = 3001699; + +SELECT + t.id AS TradeId, + t.TradeType, + t.OriginalStockEqvNotional, + CASE + WHEN t.OriginalStockEqvNotional > 100000000 THEN 1 + ELSE 0 + END AS IsOriginalStockEqvNotionalGreaterThan100Million +FROM trade t +WHERE t.id = @TradeId; +``` + +--- + +## 15. 规则 4 查询结果排查 SQL + +规则 4:保证金支付比例超阈值(本地)。用于核对当前交易预付金区域中支付方向的保证金金额占开仓名义本金的比例是否超过阈值。 + +取数流程: + +```text +1. 根据 TradeId 查当前 trade,取 OriginalStockEqvNotional,作为开仓名义本金。 +2. 查询 swap_position 中当前交易的预付金行。 +3. 预付金行口径:SwapTradeId = TradeId、IsInitial = 1、Invalid = 0、InterestMode in (5, 6)。 +4. 支付方向口径:InterestDirection = 2。 +5. 汇总 InterestPrincipalFix,作为保证金支付金额。 +6. 计算 保证金支付金额 ÷ 开仓名义本金,大于 0.5 时命中。 +``` + +规则公式: + +```text +保证金支付金额 ÷ 开仓名义本金 > 50% +``` + +规则字段口径: + +```text +保证金金额:swap_position.InterestPrincipalFix +保证金方向:swap_position.InterestDirection,1=收取,2=支付 +保证金类型:swap_position.InterestMode,5=初始预付金,6=追加预付金 +开仓名义本金:trade.OriginalStockEqvNotional +``` + +注释规则定义: + +```csharp +//rules.Add(new RiskRule +//{ +// 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", +// Version = 1, +// Status = RiskRuleStatus.Active, +// OptId = 0, +// OptName = "system", +// OptDate = DateTime.Now, +// UpdateOptId = 0, +// UpdateOptName = "system", +// UpdateDate = DateTime.Now +//}); +``` + +汇总 SQL: + +```sql +SET @TradeId = 3001699; + +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 + ELSE 0 + END AS IsGreaterThan50Percent +FROM trade t +LEFT JOIN swap_position sp + ON sp.SwapTradeId = t.id + AND sp.IsInitial = 1 + AND sp.Invalid = 0 + AND sp.InterestMode IN (5, 6) + AND sp.InterestDirection = 2 +WHERE t.id = @TradeId +GROUP BY + t.id, + t.OriginalStockEqvNotional; +``` + +明细 SQL:用于查看纳入保证金支付金额汇总的预付金明细;是否超限以汇总 SQL 的 `PayMarginRate` 为准。 + +```sql +SET @TradeId = 3001699; + +SELECT + sp.id AS SwapPositionId, + sp.SwapTradeId, + sp.IsInitial, + sp.Invalid, + sp.InterestMode, + sp.InterestDirection, + sp.InterestPrincipalFix, + sp.InterestRateDefault, + sp.HappenDate +FROM swap_position sp +WHERE sp.SwapTradeId = @TradeId + AND sp.IsInitial = 1 + AND sp.Invalid = 0 + AND sp.InterestMode IN (5, 6) + AND sp.InterestDirection = 2 +ORDER BY + sp.HappenDate, + sp.id; +``` + +--- + +## 16. 规则 5 查询结果排查 SQL + +规则 5:保证金利率偏离(本地)。用于核对当前交易预付金区域中的预付金返息率是否偏离基准值。 + +取数流程: + +```text +1. 查询 swap_position 中当前交易的预付金行。 +2. 预付金行口径:SwapTradeId = TradeId、IsInitial = 1、Invalid = 0、InterestMode in (5, 6)。 +3. 取 swap_position.InterestRateDefault,作为预付金返息率数据库原值。 +4. 数据库保存的是原值,公式计算时先和 1 比较,再乘以 100 还原为百分比偏离值。 +5. 计算 ABS((InterestRateDefault - 1) * 100),大于阈值时命中。 +``` + +规则公式: + +```text +ABS((预付金返息率 - 1) * 100) > 阈值 +``` + +规则字段口径: + +```text +预付金返息率:swap_position.InterestRateDefault +保证金类型:swap_position.InterestMode,5=初始预付金,6=追加预付金 +``` + +注释规则定义: + +```csharp +//rules.Add(new RiskRule +//{ +// 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) > 阈值", +// Version = 1, +// Status = RiskRuleStatus.Active, +// OptId = 0, +// OptName = "system", +// OptDate = DateTime.Now, +// UpdateOptId = 0, +// UpdateOptName = "system", +// UpdateDate = DateTime.Now +//}); +``` + +汇总 SQL:用于查看当前交易预付金返息率偏离情况;执行前将 `@Threshold` 替换为实际阈值。 + +```sql +SET @TradeId = 3001699; +SET @Threshold = 0.5; + +SELECT + sp.SwapTradeId AS TradeId, + MAX(ABS((IFNULL(sp.InterestRateDefault, 0) - 1) * 100)) AS MaxInterestRateDeviation, + CASE + WHEN MAX(ABS((IFNULL(sp.InterestRateDefault, 0) - 1) * 100)) > @Threshold THEN 1 + ELSE 0 + END AS IsInterestRateDeviationGreaterThanThreshold +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; +``` + +明细 SQL:用于查看每条预付金记录的返息率原值、乘以 100 后的页面口径及偏离值。 + +```sql +SET @TradeId = 3001699; +SET @Threshold = 0.5; + +SELECT + sp.id AS SwapPositionId, + sp.SwapTradeId, + sp.IsInitial, + sp.Invalid, + sp.InterestMode, + sp.InterestDirection, + sp.InterestPrincipalFix, + sp.InterestRateDefault AS InterestRateRawValue, + sp.InterestRateDefault * 100 AS InterestRatePercentValue, + ABS((IFNULL(sp.InterestRateDefault, 0) - 1) * 100) AS InterestRateDeviation, + CASE + WHEN ABS((IFNULL(sp.InterestRateDefault, 0) - 1) * 100) > @Threshold THEN 1 + ELSE 0 + END AS IsGreaterThanThreshold, + sp.HappenDate +FROM swap_position sp +WHERE sp.SwapTradeId = @TradeId + AND sp.IsInitial = 1 + AND sp.Invalid = 0 + AND sp.InterestMode IN (5, 6) +ORDER BY + sp.HappenDate, + sp.id; +``` + +--- + +## 17. 规则 6 查询结果排查 SQL + +规则 6:保证金收取比例低于最低标准(本地)。用于核对当前交易预付金区域中收取方向的保证金金额占开仓名义本金的比例是否低于最低标准。 + +取数流程: + +```text +1. 根据 TradeId 查当前 trade,取 OriginalStockEqvNotional,作为开仓名义本金。 +2. 查询 swap_position 中当前交易的预付金行。 +3. 预付金行口径:SwapTradeId = TradeId、IsInitial = 1、Invalid = 0、InterestMode in (5, 6)。 +4. 收取方向口径:InterestDirection = 1。 +5. 汇总 InterestPrincipalFix,作为保证金收取金额。 +6. 计算 保证金收取金额 ÷ 开仓名义本金,小于 0.2 时命中。 +``` + +规则公式: + +```text +保证金收取金额 ÷ 开仓名义本金 < 20% +``` + +规则字段口径: + +```text +保证金金额:swap_position.InterestPrincipalFix +保证金方向:swap_position.InterestDirection,1=收取,2=支付 +保证金类型:swap_position.InterestMode,5=初始预付金,6=追加预付金 +开仓名义本金:trade.OriginalStockEqvNotional +``` + +注释规则定义: + +```csharp +//rules.Add(new RiskRule +//{ +// 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", +// Version = 1, +// Status = RiskRuleStatus.Active, +// OptId = 0, +// OptName = "system", +// OptDate = DateTime.Now, +// UpdateOptId = 0, +// UpdateOptName = "system", +// UpdateDate = DateTime.Now +//}); +``` + +汇总 SQL: + +```sql +SET @TradeId = 3001699; + +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 + ELSE 0 + END AS IsLessThan20Percent +FROM trade t +LEFT JOIN swap_position sp + ON sp.SwapTradeId = t.id + AND sp.IsInitial = 1 + AND sp.Invalid = 0 + AND sp.InterestMode IN (5, 6) + AND sp.InterestDirection = 1 +WHERE t.id = @TradeId +GROUP BY + t.id, + t.OriginalStockEqvNotional; +``` + +明细 SQL:用于查看纳入保证金收取金额汇总的预付金明细;是否低于最低标准以汇总 SQL 的 `ReceiveMarginRate` 为准。 + +```sql +SET @TradeId = 3001699; + +SELECT + sp.id AS SwapPositionId, + sp.SwapTradeId, + sp.IsInitial, + sp.Invalid, + sp.InterestMode, + sp.InterestDirection, + sp.InterestPrincipalFix, + sp.InterestRateDefault, + sp.HappenDate +FROM swap_position sp +WHERE sp.SwapTradeId = @TradeId + AND sp.IsInitial = 1 + AND sp.Invalid = 0 + AND sp.InterestMode IN (5, 6) + AND sp.InterestDirection = 1 +ORDER BY + sp.HappenDate, + sp.id; +``` + +--- + +## 18. 规则 7 查询结果排查 SQL + +规则 7:起息日早于当前日期(本地)。用于核对互换交易界面的开始日期是否早于当前日期。 + +取数流程: + +```text +1. 根据 TradeId 查当前 trade。 +2. 取 trade.StartDate,作为互换交易界面的开始日期,即起息日。 +3. 取系统当前日期 DateTime.Today。 +4. 当 StartDate 有值且 StartDate.Date < DateTime.Today 时命中。 +``` + +规则公式: + +```text +起息日 < 当前日期 +``` + +规则字段口径: + +```text +起息日:trade.StartDate,对应互换交易界面的开始日期 +当前日期:DateTime.Today +``` + +注释规则定义: + +```csharp +//rules.Add(new RiskRule +//{ +// Id = 1000007, +// RuleName = "起息日早于当前日期(本地)", +// RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 StartDate,StartDate 对应互换交易界面的开始日期,即起息日。计算逻辑:StartDate 有值且日期早于系统当天 DateTime.Today 时触发审批。", +// RuleExpr = "DbContext.trade.First(t => t.id == TradeId).StartDate.HasValue && DbContext.trade.First(t => t.id == TradeId).StartDate.Value.Date < DateTime.Today", +// Version = 1, +// Status = RiskRuleStatus.Active, +// OptId = 0, +// OptName = "system", +// OptDate = DateTime.Now, +// UpdateOptId = 0, +// UpdateOptName = "system", +// UpdateDate = DateTime.Now +//}); +``` + +```sql +SET @TradeId = 3001699; + +SELECT + t.id AS TradeId, + t.TradeType, + t.TradeDate, + t.StartDate AS ValueDate, + CURDATE() AS CurrentDate, + CASE + WHEN t.StartDate IS NOT NULL AND DATE(t.StartDate) < CURDATE() THEN 1 + ELSE 0 + END AS IsStartDateLessThanCurrentDate +FROM trade t +WHERE t.id = @TradeId; +``` + +--- + +## 19. 规则 8 查询结果排查 SQL + +规则 8:支付日为银行间交易日(本地)。用于核对交易平仓窗口的支付日期是否为银行间交易日;若支付日期未落在 IB 日历的非交易日列表中,则命中。 + +取数流程: + +```text +1. 根据 TradeId 查 swap_flow_event。 +2. 限定 SwapTradeId = TradeId、EventType = 2、DataState <> 0,取平仓事件。 +3. 取 swap_flow_event.PayDate,作为交易平仓窗口的支付日期。 +4. 根据 PayDate.Value.Year 查询 calendar 表中 Country = 'IB' 的银行间日历。 +5. calendar.HolidayJson 存储该年非交易日,格式为 yyyy,MM,dd。 +6. 若 HolidayJson 不包含支付日期对应的 yyyy,MM,dd,则说明支付日期是银行间交易日,命中规则。 +``` + +规则公式: + +```text +支付日期 ∉ IB 日历非交易日 +``` + +规则字段口径: + +```text +支付日期:swap_flow_event.PayDate,对应交易平仓窗口的支付日期 +平仓事件:swap_flow_event.EventType = 2 +银行间日历:calendar.Country = 'IB' +日历年份:calendar.Year = swap_flow_event.PayDate.Year +非交易日:calendar.HolidayJson +``` + +参考页面字段: + +```cshtml + + +``` + +注释规则定义: + +```csharp +//rules.Add(new RiskRule +//{ +// Id = 1000008, +// RuleName = "支付日为银行间交易日(本地)", +// RuleText = "取值字段:通过 DbContext.swap_flow_event 按 SwapTradeId=TradeId、EventType=2、DataState<>0 取平仓事件的 PayDate,PayDate 对应交易平仓窗口的支付日期;通过 DbContext.calendar 按 Country='IB' 且 Year=PayDate.Year 取银行间日历,HolidayJson 存储该年非交易日。计算逻辑:若支付日期对应的 yyyy,MM,dd 不存在于 IB 日历 HolidayJson 中,则说明支付日期是银行间交易日,触发审批。", +// RuleExpr = "DbContext.swap_flow_event.Any(e => e.SwapTradeId == TradeId && e.EventType == 2 && e.DataState != 0 && e.PayDate.HasValue) && !DbContext.calendar.First(c => c.Country == \"IB\" && c.Year == DbContext.swap_flow_event.Where(e => e.SwapTradeId == TradeId && e.EventType == 2 && e.DataState != 0 && e.PayDate.HasValue).OrderByDescending(e => e.id).First().PayDate.Value.Year && c.ValidState != \"InValid\").HolidayJson.Contains(DbContext.swap_flow_event.Where(e => e.SwapTradeId == TradeId && e.EventType == 2 && e.DataState != 0 && e.PayDate.HasValue).OrderByDescending(e => e.id).First().PayDate.Value.ToString(\"yyyy,MM,dd\"))", +// Version = 1, +// Status = RiskRuleStatus.Active, +// OptId = 0, +// OptName = "system", +// OptDate = DateTime.Now, +// UpdateOptId = 0, +// UpdateOptName = "system", +// UpdateDate = DateTime.Now +//}); +``` + +```sql +SET @TradeId = 3001699; + +SELECT + t.id AS TradeId, + sfe.id AS SwapFlowEventId, + sfe.EventType, + sfe.DataState, + sfe.PayDate, + c.Country AS CalendarCountry, + c.Year AS CalendarYear, + DATE_FORMAT(sfe.PayDate, '%Y,%m,%d') AS PayDateText, + CASE + WHEN sfe.PayDate IS NOT NULL + AND c.HolidayJson NOT LIKE CONCAT('%', DATE_FORMAT(sfe.PayDate, '%Y,%m,%d'), '%') THEN 1 + ELSE 0 + END AS IsPayDateIBTradingDay +FROM trade t +LEFT JOIN ( + SELECT * + FROM swap_flow_event + WHERE SwapTradeId = @TradeId + AND EventType = 2 + AND DataState <> 0 + ORDER BY id DESC + LIMIT 1 +) sfe ON sfe.SwapTradeId = t.id +LEFT JOIN calendar c + ON c.Country = 'IB' + AND c.Year = YEAR(sfe.PayDate) + AND c.ValidState <> 'InValid' +WHERE t.id = @TradeId; +``` + +--- + +## 20. 规则 9 查询结果排查 SQL + +规则 9:到期日为银行间交易日(本地)。用于核对互换交易界面的到期日是否为银行间交易日;若到期日未落在 IB 日历的非交易日列表中,则命中。 + +取数流程: + +```text +1. 根据 TradeId 查当前 trade。 +2. 取 trade.ExerciseDate,作为互换交易界面的到期日。 +3. 根据 ExerciseDate.Value.Year 查询 calendar 表中 Country = 'IB' 的银行间日历。 +4. calendar.HolidayJson 存储该年非交易日,格式为 yyyy,MM,dd。 +5. 若 HolidayJson 不包含到期日对应的 yyyy,MM,dd,则说明到期日是银行间交易日,命中规则。 +``` + +规则公式: + +```text +到期日 ∉ IB 日历非交易日 +``` + +规则字段口径: + +```text +到期日:trade.ExerciseDate,对应互换交易界面的到期日 +银行间日历:calendar.Country = 'IB' +日历年份:calendar.Year = trade.ExerciseDate.Year +非交易日:calendar.HolidayJson +``` + +注释规则定义: + +```csharp +//rules.Add(new RiskRule +//{ +// Id = 1000009, +// RuleName = "到期日为银行间交易日(本地)", +// RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 ExerciseDate,ExerciseDate 对应互换交易界面的到期日;通过 DbContext.calendar 按 Country='IB' 且 Year=ExerciseDate.Year 取银行间日历,HolidayJson 存储该年非交易日。计算逻辑:若到期日对应的 yyyy,MM,dd 不存在于 IB 日历 HolidayJson 中,则说明到期日是银行间交易日,触发审批。", +// RuleExpr = "DbContext.trade.First(t => t.id == TradeId).ExerciseDate.HasValue && !DbContext.calendar.First(c => c.Country == \"IB\" && c.Year == DbContext.trade.First(t => t.id == TradeId).ExerciseDate.Value.Year && c.ValidState != \"InValid\").HolidayJson.Contains(DbContext.trade.First(t => t.id == TradeId).ExerciseDate.Value.ToString(\"yyyy,MM,dd\"))", +// Version = 1, +// Status = RiskRuleStatus.Active, +// OptId = 0, +// OptName = "system", +// OptDate = DateTime.Now, +// UpdateOptId = 0, +// UpdateOptName = "system", +// UpdateDate = DateTime.Now +//}); +``` + +```sql +SET @TradeId = 3001699; + +SELECT + t.id AS TradeId, + t.TradeType, + t.ExerciseDate AS MaturityDate, + c.Country AS CalendarCountry, + c.Year AS CalendarYear, + DATE_FORMAT(t.ExerciseDate, '%Y,%m,%d') AS MaturityDateText, + CASE + WHEN t.ExerciseDate IS NOT NULL + AND c.HolidayJson NOT LIKE CONCAT('%', DATE_FORMAT(t.ExerciseDate, '%Y,%m,%d'), '%') THEN 1 + ELSE 0 + END AS IsMaturityDateIBTradingDay +FROM trade t +LEFT JOIN calendar c + ON c.Country = 'IB' + AND c.Year = YEAR(t.ExerciseDate) + AND c.ValidState <> 'InValid' +WHERE t.id = @TradeId; +``` + +--- + +## 21. 规则 10 查询结果排查 SQL + +规则 10:平仓日为银行间交易日(本地)。用于核对交易平仓窗口的平仓日期是否为银行间交易日;若平仓日期未落在 IB 日历的非交易日列表中,则命中。 + +取数流程: + +```text +1. 根据 TradeId 查 swap_flow_event。 +2. 限定 SwapTradeId = TradeId、EventType = 2、DataState <> 0,取平仓事件。 +3. 取 swap_flow_event.UnwindDate,作为交易平仓窗口的平仓日期。 +4. 根据 UnwindDate.Value.Year 查询 calendar 表中 Country = 'IB' 的银行间日历。 +5. calendar.HolidayJson 存储该年非交易日,格式为 yyyy,MM,dd。 +6. 若 HolidayJson 不包含平仓日期对应的 yyyy,MM,dd,则说明平仓日期是银行间交易日,命中规则。 +``` + +规则公式: + +```text +平仓日期 ∉ IB 日历非交易日 +``` + +规则字段口径: + +```text +平仓日期:swap_flow_event.UnwindDate,对应交易平仓窗口的平仓日期 +平仓事件:swap_flow_event.EventType = 2 +银行间日历:calendar.Country = 'IB' +日历年份:calendar.Year = swap_flow_event.UnwindDate.Year +非交易日:calendar.HolidayJson +``` + +参考页面字段: + +```cshtml + + +``` + +注释规则定义: + +```csharp +//rules.Add(new RiskRule +//{ +// Id = 1000010, +// RuleName = "平仓日为银行间交易日(本地)", +// RuleText = "取值字段:通过 DbContext.swap_flow_event 按 SwapTradeId=TradeId、EventType=2、DataState<>0 取平仓事件的 UnwindDate,UnwindDate 对应交易平仓窗口的平仓日期;通过 DbContext.calendar 按 Country='IB' 且 Year=UnwindDate.Year 取银行间日历,HolidayJson 存储该年非交易日。计算逻辑:若平仓日期对应的 yyyy,MM,dd 不存在于 IB 日历 HolidayJson 中,则说明平仓日期是银行间交易日,触发审批。", +// RuleExpr = "DbContext.swap_flow_event.Any(e => e.SwapTradeId == TradeId && e.EventType == 2 && e.DataState != 0 && e.UnwindDate.HasValue) && !DbContext.calendar.First(c => c.Country == \"IB\" && c.Year == DbContext.swap_flow_event.Where(e => e.SwapTradeId == TradeId && e.EventType == 2 && e.DataState != 0 && e.UnwindDate.HasValue).OrderByDescending(e => e.id).First().UnwindDate.Value.Year && c.ValidState != \"InValid\").HolidayJson.Contains(DbContext.swap_flow_event.Where(e => e.SwapTradeId == TradeId && e.EventType == 2 && e.DataState != 0 && e.UnwindDate.HasValue).OrderByDescending(e => e.id).First().UnwindDate.Value.ToString(\"yyyy,MM,dd\"))", +// Version = 1, +// Status = RiskRuleStatus.Active, +// OptId = 0, +// OptName = "system", +// OptDate = DateTime.Now, +// UpdateOptId = 0, +// UpdateOptName = "system", +// UpdateDate = DateTime.Now +//}); +``` + +```sql +SET @TradeId = 3001699; + +SELECT + t.id AS TradeId, + sfe.id AS SwapFlowEventId, + sfe.EventType, + sfe.DataState, + sfe.UnwindDate, + c.Country AS CalendarCountry, + c.Year AS CalendarYear, + DATE_FORMAT(sfe.UnwindDate, '%Y,%m,%d') AS UnwindDateText, + CASE + WHEN sfe.UnwindDate IS NOT NULL + AND c.HolidayJson NOT LIKE CONCAT('%', DATE_FORMAT(sfe.UnwindDate, '%Y,%m,%d'), '%') THEN 1 + ELSE 0 + END AS IsUnwindDateIBTradingDay +FROM trade t +LEFT JOIN ( + SELECT * + FROM swap_flow_event + WHERE SwapTradeId = @TradeId + AND EventType = 2 + AND DataState <> 0 + ORDER BY id DESC + LIMIT 1 +) sfe ON sfe.SwapTradeId = t.id +LEFT JOIN calendar c + ON c.Country = 'IB' + AND c.Year = YEAR(sfe.UnwindDate) + AND c.ValidState <> 'InValid' +WHERE t.id = @TradeId; +``` + +--- + +## 22. 规则 11 查询结果排查 SQL + +规则 11:合约期限超阈值(本地)。用于核对互换交易合约期限;合约期限按起始日与到期日之间的自然日天数计算,并根据计息方式决定是否算头、算尾,超过阈值时命中。 + +取数流程: + +```text +1. 根据 TradeId 查 trade 表,取 StartDate 作为起始日,取 ExerciseDate 作为到期日。 +2. 根据 TradeId 查 trade_extend 表,取 ExtendJson 中的 InterestCalcMode 作为计息方式。 +3. InterestCalcMode 口径:00=不算头不算尾,01=不算头算尾,10=算头不算尾,11=算头算尾,缺省值=11。 +4. 先计算自然日基础天数:(到期日.Date - 起始日.Date).Days。 +5. 基础天数按“不算头算尾”口径理解;若算头则 +1,若不算尾则 -1。 +6. 计算结果大于阈值时命中。 +``` + +规则公式: + +```text +(到期日.Date - 起始日.Date).Days + 是否算头 - 是否不算尾 > 阈值 +``` + +规则字段口径: + +```text +起始日:trade.StartDate,对应互换交易界面的开始日期 +到期日:trade.ExerciseDate,对应互换交易界面的到期日期 +计息方式:trade_extend.ExtendJson.InterestCalcMode +计息方式枚举:00=不算头不算尾,01=不算头算尾,10=算头不算尾,11=算头算尾 +``` + +注释规则定义: + +```csharp +//rules.Add(new RiskRule +//{ +// Id = 1000011, +// RuleName = "合约期限超阈值(本地)", +// RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 StartDate 和 ExerciseDate;通过 DbContext.trade_extend 按 TradeId 取 ExtendJson 中的 InterestCalcMode,InterestCalcMode 口径为 00=不算头不算尾、01=不算头算尾、10=算头不算尾、11=算头算尾。计算逻辑:先计算 (ExerciseDate.Date-StartDate.Date).Days 作为自然日基础天数,基础天数按不算头算尾口径理解;若 InterestCalcMode 首位为 1 则算头加 1,若末位为 0 则不算尾减 1,最终合约期限大于阈值时触发审批。", +// RuleExpr = "DbContext.trade.First(t => t.id == TradeId).StartDate.HasValue && DbContext.trade.First(t => t.id == TradeId).ExerciseDate.HasValue && ((DbContext.trade.First(t => t.id == TradeId).ExerciseDate.Value.Date - DbContext.trade.First(t => t.id == TradeId).StartDate.Value.Date).Days + (DbContext.trade_extend.First(e => e.TradeId == TradeId).ExtendObj.InterestCalcMode.StartsWith(\"1\") ? 1 : 0) - (DbContext.trade_extend.First(e => e.TradeId == TradeId).ExtendObj.InterestCalcMode.EndsWith(\"1\") ? 0 : 1)) > 阈值", +// Version = 1, +// Status = RiskRuleStatus.Active, +// OptId = 0, +// OptName = "system", +// OptDate = DateTime.Now, +// UpdateOptId = 0, +// UpdateOptName = "system", +// UpdateDate = DateTime.Now +//}); +``` + +```sql +SET @TradeId = 3001699; +SET @Threshold = 365; + +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, + CASE + WHEN LEFT(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(te.ExtendJson, '$.InterestCalcMode')), '11'), 1) = '1' THEN 1 + ELSE 0 + END AS CalcFirstDays, + 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 + ELSE 0 + END AS IsContractTermGreaterThanThreshold +FROM trade t +LEFT JOIN trade_extend te + ON te.TradeId = t.id +WHERE t.id = @TradeId; +``` + +--- + +## 23. 规则 12 查询结果排查 SQL 规则 12:债券类净价偏离(本地)。用于核对浮动支付端期初交割净价和交易日前中债估值净价;同一标的同一估值日存在不同可信度时,优先取 credibility=1。 @@ -1403,7 +2383,7 @@ LIMIT 1; --- -## 13. 规则 13 查询结果排查 SQL +## 24. 规则 13 查询结果排查 SQL 规则 13:债券类收益率偏离(本地)。用于核对浮动支付端期初成交收益率和交易日前中债估值收益率;同一标的同一估值日存在不同可信度时,优先取 credibility=1。 @@ -1493,7 +2473,7 @@ LIMIT 1; --- -## 14. 规则 14 查询结果排查 SQL +## 22. 规则 14 查询结果排查 SQL 规则 14:非债券类价格偏离(本地)。用于核对普通收益互换页面填写的期初标的价格和交易日前上一日收盘价。 @@ -1581,7 +2561,7 @@ LIMIT 1; --- -## 15. 规则 15 查询结果排查 SQL +## 23. 规则 15 查询结果排查 SQL 规则 15:单一交易对手累计标的数量超阈值(本地)。用于核对同一交易对手实时存续持仓下的去重标的数量是否超过 10 个。 @@ -1690,7 +2670,7 @@ ORDER BY --- -## 16. 规则 16 查询结果排查 SQL +## 24. 规则 16 查询结果排查 SQL 规则 16:多头支付固定端利率偏离(本地)。用于核对利息端收入固定利息方向的点差百分比绝对值是否低于阈值。 @@ -1752,3 +2732,94 @@ FROM swap_position sp WHERE sp.SwapTradeId = @TradeId AND sp.InterestDirection = 1; ``` + +--- + +## 28. 规则 18 查询结果排查 SQL + +规则 18:账户授权收支方向不匹配(本地)。用于核对账户授权的收支方向与交易浮动端多空方向是否匹配;不符合允许场景时命中。 + +校验内容: + +```text +不符合以下场景则不通过: +1. 利息端 = 收取,且浮动端 = 空头 +2. 利息端 = 支付,且浮动端 = 多头 +``` + +取数流程: + +```text +1. 根据 TradeId 查 swap_position。 +2. 限定 IsInitial=1、Invalid=0、PosiDirection>0,取浮动端初始持仓记录。 +3. 取 swap_position.InterestDirection 作为利息端收支方向,1=收取,2=支付。 +4. 取 swap_position.PositionType 作为浮动端多空方向,1=多头,2=空头。 +5. 若不是“利息端收取且浮动端空头”,也不是“利息端支付且浮动端多头”,则命中。 +``` + +规则公式: + +```text +NOT ((InterestDirection = 1 AND PositionType = 2) OR (InterestDirection = 2 AND PositionType = 1)) +``` + +规则字段口径: + +```text +利息端收支方向:swap_position.InterestDirection,1=收取,2=支付 +浮动端多空方向:swap_position.PositionType,1=多头,2=空头 +浮动端记录:swap_position.IsInitial=1、Invalid=0、PosiDirection>0 +``` + +注释规则定义: + +```csharp +//rules.Add(new RiskRule +//{ +// Id = 1000018, +// RuleName = "账户授权收支方向不匹配(本地)", +// RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 取 IsInitial=true、Invalid=false、PosiDirection>0 的浮动端初始持仓记录;InterestDirection 表示利息端收支方向,1=收取、2=支付;PositionType 表示浮动端多空方向,1=多头、2=空头。计算逻辑:仅允许利息端=收取且浮动端=空头,或利息端=支付且浮动端=多头;其他组合触发审批。", +// RuleExpr = "!(((DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection > 0).InterestDirection == 1) && (DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection > 0).PositionType == 2)) || ((DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection > 0).InterestDirection == 2) && (DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection > 0).PositionType == 1)))", +// Version = 1, +// Status = RiskRuleStatus.Active, +// OptId = 0, +// OptName = "system", +// OptDate = DateTime.Now, +// UpdateOptId = 0, +// UpdateOptName = "system", +// UpdateDate = DateTime.Now +//}); +``` + +```sql +SET @TradeId = 3001699; + +SELECT + sp.id AS SwapPositionId, + sp.SwapTradeId, + sp.IsInitial, + sp.Invalid, + sp.PosiDirection, + sp.InterestDirection, + CASE sp.InterestDirection + WHEN 1 THEN '收取' + WHEN 2 THEN '支付' + ELSE '未知' + END AS InterestDirectionText, + sp.PositionType, + CASE sp.PositionType + WHEN 1 THEN '多头' + WHEN 2 THEN '空头' + ELSE '未知' + END AS PositionTypeText, + CASE + WHEN (sp.InterestDirection = 1 AND sp.PositionType = 2) + OR (sp.InterestDirection = 2 AND sp.PositionType = 1) THEN 0 + ELSE 1 + END AS IsDirectionMismatch +FROM swap_position sp +WHERE sp.SwapTradeId = @TradeId + AND sp.IsInitial = 1 + AND sp.Invalid = 0 + AND sp.PosiDirection > 0; +```