From f41411485dbce57ef5592d7b422a260bec1e8d84 Mon Sep 17 00:00:00 2001 From: ruisu Date: Fri, 14 Aug 2026 15:19:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E9=A3=8E=E6=8E=A7?= =?UTF-8?q?=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- YLErpDAL/Modules/RiskEngine/测试用例.md | 872 +++++++++++++++++++++--- 1 file changed, 779 insertions(+), 93 deletions(-) diff --git a/YLErpDAL/Modules/RiskEngine/测试用例.md b/YLErpDAL/Modules/RiskEngine/测试用例.md index a268de5b..67f25636 100644 --- a/YLErpDAL/Modules/RiskEngine/测试用例.md +++ b/YLErpDAL/Modules/RiskEngine/测试用例.md @@ -2214,7 +2214,20 @@ WHERE t.id = @TradeId; ``` -变量 Roslyn 示例:变量名为“平仓支付日期为银行间交易日”,DataType 为 Bool;规则前端配置“平仓支付日期为银行间交易日 == true”。该变量必须使用本次平仓请求中的 PayDate,不能查询 swap_flow_event 历史平仓事件,否则首次提交和修改支付日期后的试算会取不到当前页面值。 +变量配置:规则 8 需要同时准备日期变量和布尔判断变量,规则前端配置时使用“平仓支付日期为银行间交易日 isTrue”;如果需要与平仓日期组合比较,可使用“平仓支付日期”日期变量。 + +变量 1:平仓支付日期,Category 为 BookingElement,DataType 为 Date,ValueDomain 为 yyyy-MM-dd。该变量必须使用本次平仓请求中的 PayDate,不能查询 swap_flow_event 历史平仓事件,否则首次提交和修改支付日期后的试算会取不到当前页面值。 + +```csharp +if (!PayDate.HasValue) + throw new Exception("平仓支付日期为空"); + +return new RiskVariableValueDetail( + PayDate.Value.Date, + $"交易ID {TradeId},本次平仓支付日期为{PayDate.Value:yyyy-MM-dd}"); +``` + +变量 2:平仓支付日期为银行间交易日,Category 为 BooleanCheck,DataType 为 Boolean,ValueDomain 为 true/false。 ```csharp if (!PayDate.HasValue) @@ -2285,17 +2298,18 @@ WHERE t.id = @TradeId; ## 20. 规则 9 查询结果排查 SQL -规则 9:到期日为银行间交易日(本地)。用于核对新增互换交易的到期日期是否为银行间交易日;若到期日期未落在 IB 日历的非交易日列表中,则命中。 +规则 9:到期日为银行间交易日(本地)。用于核对新增互换交易页面本次提交的到期日期是否为银行间交易日;若到期日期未落在 IB 日历的非交易日列表中,则命中。到期日期指 swapTradeEdit.js 中绑定的 trade.ExerciseDate。 取数流程: ```text -1. 根据 TradeId 查 swap_position。 -2. 限定 SwapTradeId = TradeId、IsInitial = 1,取新增互换交易生成的期初持仓。 -3. 取 swap_position.PosiMatuirityDate,作为新增互换交易的到期日期。 -4. 根据 PosiMatuirityDate.Value.Year 查询 calendar 表中 Country = 'IB' 的银行间日历。 -5. calendar.HolidayJson 存储该年非交易日,格式为 yyyy,MM,dd。 -6. 若 HolidayJson 不包含到期日期对应的 yyyy,MM,dd,则说明到期日期是银行间交易日,命中规则。 +1. 前端互换交易编辑页 swapTradeEdit.js 使用 trade.ExerciseDate 作为到期日期。 +2. 新增互换交易提交后,后端交易模型承接为 trade.ExerciseDate。 +3. 确认成交触发风控时,初始 swap_position 已由 trade.ExerciseDate 生成,读取 swap_position.PosiMatuirityDate。 +4. 限定 SwapTradeId = TradeId、IsInitial = 1、Invalid = 0、PosiMatuirityDate 不为空,按 id 取第一条初始持仓。 +5. 根据 PosiMatuirityDate.Value.Year 查询 calendar 表中 Country = 'IB' 的银行间日历。 +6. calendar.HolidayJson 存储该年非交易日,格式为 yyyy,MM,dd。 +7. 若 HolidayJson 不包含到期日期对应的 yyyy,MM,dd,则说明到期日期是银行间交易日,命中规则。 ``` 规则公式: @@ -2307,19 +2321,43 @@ WHERE t.id = @TradeId; 规则字段口径: ```text -到期日期:swap_position.PosiMatuirityDate,对应新增互换交易的到期日期 -期初持仓:swap_position.IsInitial = 1 +到期日期:swapTradeEdit.js 的 trade.ExerciseDate,对应新增互换交易页面的“到期日期” +确认成交风控取值:swap_position.PosiMatuirityDate,由 Trade.ExerciseDate 生成 +初始持仓:swap_position.SwapTradeId = TradeId、IsInitial = 1、Invalid = 0 银行间日历:calendar.Country = 'IB' 日历年份:calendar.Year = swap_position.PosiMatuirityDate.Year 非交易日:calendar.HolidayJson ``` -变量 Roslyn 示例:变量名为“互换到期日期为银行间交易日”,DataType 为 Bool;规则前端配置“互换到期日期为银行间交易日 == true”。该变量取新增互换交易生成的期初持仓到期日期,即 swap_position.PosiMatuirityDate。 +变量配置:规则 9 需要同时准备日期变量和布尔判断变量,规则前端配置时使用“互换到期日期为银行间交易日 isTrue”;如果需要与成交日、起始日组合比较,可使用“互换到期日期”日期变量。 + +变量 1:互换到期日期,Category 为 BookingElement,DataType 为 Date,ValueDomain 为 yyyy-MM-dd。确认成交触发点使用 swap_position.PosiMatuirityDate,需限定初始有效持仓,避免后续互换/平仓产生的非初始持仓干扰。 ```csharp var maturityDate = DbContext.swap_position .Where(p => p.SwapTradeId == TradeId && p.IsInitial + && !p.Invalid + && p.PosiMatuirityDate.HasValue) + .OrderBy(p => p.id) + .Select(p => p.PosiMatuirityDate) + .FirstOrDefault(); + +if (!maturityDate.HasValue) + throw new Exception("互换到期日期为空"); + +return new RiskVariableValueDetail( + maturityDate.Value.Date, + $"交易ID {TradeId},本次互换到期日期为{maturityDate.Value:yyyy-MM-dd}"); +``` + +变量 2:互换到期日期为银行间交易日,Category 为 BooleanCheck,DataType 为 Boolean,ValueDomain 为 true/false。 + +```csharp +var maturityDate = DbContext.swap_position + .Where(p => p.SwapTradeId == TradeId + && p.IsInitial + && !p.Invalid && p.PosiMatuirityDate.HasValue) .OrderBy(p => p.id) .Select(p => p.PosiMatuirityDate) @@ -2330,7 +2368,7 @@ if (!maturityDate.HasValue) return new RiskVariableValueDetail( RiskCalendarHelper.IsInterbankTradingDay(DbContext, maturityDate.Value), - $"交易ID {TradeId},互换到期日期为{maturityDate.Value:yyyy-MM-dd}"); + $"交易ID {TradeId},本次互换到期日期为{maturityDate.Value:yyyy-MM-dd}"); ``` 注释规则定义: @@ -2340,8 +2378,8 @@ return new RiskVariableValueDetail( //{ // Id = 1000009, // RuleName = "到期日为银行间交易日(本地)", -// RuleText = "取值字段:通过 DbContext.swap_position 按 SwapTradeId=TradeId、IsInitial=true 取新增互换交易生成的期初持仓 PosiMatuirityDate,PosiMatuirityDate 对应新增互换交易的到期日期;通过 DbContext.calendar 按 Country='IB' 且 Year=PosiMatuirityDate.Year 取银行间日历,HolidayJson 存储该年非交易日。计算逻辑:若到期日期对应的 yyyy,MM,dd 不存在于 IB 日历 HolidayJson 中,则说明到期日期是银行间交易日,触发审批。", -// RuleExpr = "DbContext.swap_position.Any(p => p.SwapTradeId == TradeId && p.IsInitial && p.PosiMatuirityDate.HasValue) && RiskCalendarHelper.IsInterbankTradingDay(DbContext, DbContext.swap_position.Where(p => p.SwapTradeId == TradeId && p.IsInitial && p.PosiMatuirityDate.HasValue).OrderBy(p => p.id).First().PosiMatuirityDate.Value)", +// RuleText = "取值字段:确认成交触发点下初始 swap_position 已由 trade.ExerciseDate 生成,通过 DbContext.swap_position 按 SwapTradeId=TradeId、IsInitial=true、Invalid=false 取 PosiMatuirityDate,PosiMatuirityDate 对应 swapTradeEdit.js 页面上的到期日期;通过 DbContext.calendar 按 Country='IB' 且 Year=PosiMatuirityDate.Year 取银行间日历,HolidayJson 存储该年非交易日。计算逻辑:若到期日期对应的 yyyy,MM,dd 不存在于 IB 日历 HolidayJson 中,则说明到期日期是银行间交易日,触发审批。", +// RuleExpr = "DbContext.swap_position.Any(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiMatuirityDate.HasValue) && RiskCalendarHelper.IsInterbankTradingDay(DbContext, DbContext.swap_position.Where(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiMatuirityDate.HasValue).OrderBy(p => p.id).First().PosiMatuirityDate.Value)", // Version = 1, // Status = RiskRuleStatus.Active, // OptId = 0, @@ -2359,8 +2397,10 @@ SET @TradeId = 3001699; SELECT t.id AS TradeId, t.TradeType, + t.ExerciseDate AS TradeExerciseDate, sp.id AS SwapPositionId, sp.IsInitial, + sp.Invalid, sp.PosiMatuirityDate AS MaturityDate, c.Country AS CalendarCountry, c.Year AS CalendarYear, @@ -2376,6 +2416,7 @@ LEFT JOIN ( FROM swap_position WHERE SwapTradeId = @TradeId AND IsInitial = 1 + AND Invalid = 0 AND PosiMatuirityDate IS NOT NULL ORDER BY id LIMIT 1 @@ -2425,7 +2466,20 @@ WHERE t.id = @TradeId; ``` -变量 Roslyn 示例:变量名为“平仓日期为银行间交易日”,DataType 为 Bool;规则前端配置“平仓日期为银行间交易日 == true”。该变量必须使用本次平仓请求中的 UnwindDate,不能查询 swap_flow_event 历史平仓事件,否则首次提交和修改平仓日期后的试算会取不到当前页面值。 +变量配置:规则 10 需要同时准备日期变量和布尔判断变量,规则前端配置时使用“平仓日期为银行间交易日 isTrue”;如果需要与支付日期组合比较,可使用“平仓日期”日期变量。 + +变量 1:平仓日期,Category 为 BookingElement,DataType 为 Date,ValueDomain 为 yyyy-MM-dd。该变量必须使用本次平仓请求中的 UnwindDate,不能查询 swap_flow_event 历史平仓事件,否则首次提交和修改平仓日期后的试算会取不到当前页面值。 + +```csharp +if (!UnwindDate.HasValue) + throw new Exception("平仓日期为空"); + +return new RiskVariableValueDetail( + UnwindDate.Value.Date, + $"交易ID {TradeId},本次平仓日期为{UnwindDate.Value:yyyy-MM-dd}"); +``` + +变量 2:平仓日期为银行间交易日,Category 为 BooleanCheck,DataType 为 Boolean,ValueDomain 为 true/false。 ```csharp if (!UnwindDate.HasValue) @@ -3515,22 +3569,27 @@ ORDER BY ## 27. 规则 16 查询结果排查 SQL -规则 16:多头支付固定端利率偏离(本地)。用于核对利息端收入固定利息方向的点差百分比绝对值是否大于阈值。 +规则 16:多头支付固定端利率偏离(本地)。用于核对多头浮动端对应利息端点差百分比绝对值是否大于阈值,利息端不按 InterestDirection 区分收取或支付。 取数流程: ```text 1. 根据 TradeId 查 swap_position。 -2. 限定 InterestDirection=1,取固定利息端收取方向记录。 -3. 从该利息端记录取 InterestRateDefault。 -4. InterestRateDefault 只代表利率文本框中 + 号后的点差,不包含 FR007 基准利率,库内为小数原值,界面按百分比显示。 -5. 计算 ABS(InterestRateDefault * 100),大于 5 时命中。 +2. 先限定 IsInitial=1、Invalid=0、PosiDirection>0、PositionType=1,取多头浮动端记录。 +3. 若不存在多头浮动端记录,说明当前 trade 不适用规则16,变量返回 0,不触发风控。 +4. 再取同一 TradeId、同为初始有效、PosiDirection=0、PositionType=0、UnderlyingCode 为空、InterestMode=9 的利息端记录,不按 InterestDirection 区分收取或支付。 +5. 该识别口径来自当前建仓保存逻辑:浮动端记录会赋值 PosiDirection、PositionType、UnderlyingCode;普通利息端记录单独创建,InterestMode 设置为 9(标的期初全价),只赋值 InterestDirection、InterestRateDefault、InterestMode、FloatRateUnderlyingCode 等利息字段,未赋值浮动端字段时落库为默认的 0、0、空;保证金/预付金记录用 InterestMode=5 或 6,不纳入规则16利息端。 +6. 从该利息端记录取 InterestRateDefault 和 FloatRateUnderlyingCode。 +7. InterestRateDefault 只代表利率文本框中 + 号后的点差,不包含 FR007 基准利率,库内为小数原值,界面按百分比显示。 +8. 基准利率保护:FloatRateUnderlyingCode 必须为 FR007;为空或不是 FR007 时,说明规则16的 FR007 抵消前提不成立,抛异常触发风控报警。 +9. 所有普通利息端都要检查;若存在基准利率非法记录,异常信息同时列出基准利率非法记录,以及已满足 FR007 的记录偏离明细,避免只看到第一类问题。 +10. 若基准利率全部为 FR007,则逐条计算 ABS(InterestRateDefault * 100),取最大值与规则阈值比较;同时返回逐条明细,由结构化执行器按当前规则阈值拆分展示超限记录和未超限记录。 ``` 规则公式: ```text -ABS(利息端.InterestRateDefault * 100) > 5 +存在多头浮动端时,逐条检查普通利息端;若 FloatRateUnderlyingCode 为空或不是 FR007,则抛异常触发风控报警,异常信息同时列出基准利率非法记录和已满足 FR007 的记录偏离明细;若基准利率全部为 FR007,则取 ABS(InterestRateDefault * 100) 的最大值与阈值比较,并由结构化执行器按规则阈值拆分展示超限记录和未超限记录;不存在多头浮动端时返回 0,不触发 ``` 注释规则定义: @@ -3540,8 +3599,8 @@ ABS(利息端.InterestRateDefault * 100) > 5 //{ // Id = 1000016, // RuleName = "多头支付固定端利率偏离(本地)", -// RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 取利息端收入固定利息方向记录的 InterestRateDefault。InterestRateDefault 只代表利率文本框中 + 号后的点差,不包含 FR007 基准利率。计算逻辑:按 ABS(InterestRateDefault) 计算点差绝对值,绝对值小于 5% 时触发审批。", -// RuleExpr = "Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId && p.InterestDirection == 1).InterestRateDefault)*100m > 5m", +// RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 先取 IsInitial=true、Invalid=false、PosiDirection>0、PositionType=1 的多头浮动端记录;若不存在多头浮动端记录,说明当前 trade 不适用规则16,变量返回 0,不触发风控;若存在多头浮动端记录,再取同一 TradeId、同为初始有效、PosiDirection=0、PositionType=0、UnderlyingCode 为空、InterestMode=9 的普通利息端记录,不按 InterestDirection 区分收取或支付,排除 InterestMode=5/6 的保证金/预付金记录。InterestRateDefault 只代表利率文本框中 + 号后的点差,不包含 FR007 基准利率。基准利率保护:FloatRateUnderlyingCode 必须为 FR007;为空或不是 FR007 时,说明规则16的 FR007 抵消前提不成立,抛异常触发风控报警,异常信息同时列出基准利率非法记录和已满足 FR007 的记录偏离明细。计算逻辑:基准利率全部校验通过后,按 ABS(InterestRateDefault * 100) 计算点差百分比绝对值,取最大值与阈值比较;变量明细逐条返回偏离值,结构化执行器按规则阈值拆分展示超限记录和未超限记录。", +// RuleExpr = "DbContext.swap_position.Any(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection > 0 && p.PositionType == 1) && !DbContext.swap_position.Where(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 0 && p.PositionType == 0 && string.IsNullOrEmpty(p.UnderlyingCode) && p.InterestMode == 9).Any(p => string.IsNullOrEmpty(p.FloatRateUnderlyingCode) || p.FloatRateUnderlyingCode != \"FR007\") && DbContext.swap_position.Where(p => p.SwapTradeId == TradeId && p.IsInitial && !p.Invalid && p.PosiDirection == 0 && p.PositionType == 0 && string.IsNullOrEmpty(p.UnderlyingCode) && p.InterestMode == 9).Max(p => Math.Abs(p.InterestRateDefault * 100m)) > 5m", // Version = 1, // Status = RiskRuleStatus.Active, // OptId = 0, @@ -3555,113 +3614,265 @@ ABS(利息端.InterestRateDefault * 100) > 5 变量形式: -变量名:固定端利率偏离绝对值 +变量名:多头支付固定端利率偏离值 DataType:Numeric -规则配置:固定端利率偏离绝对值 > 阈值(示例 5) +规则配置:多头支付固定端利率偏离值 > 阈值(示例 5) 变量取值表达式: ```csharp +var longFloatItems = DbContext.swap_position + .Where(p => p.SwapTradeId == TradeId + && p.IsInitial + && !p.Invalid + && p.PosiDirection > 0 + && p.PositionType == 1) + .Select(p => new { p.id }) + .ToList(); + +if (!longFloatItems.Any()) + return new RiskVariableValueDetail( + 0m, + $"交易ID {TradeId} 不存在多头浮动端记录,规则16不适用,不触发风控"); + var interestItems = DbContext.swap_position .Where(p => p.SwapTradeId == TradeId - && p.InterestDirection == 1) + && p.IsInitial + && !p.Invalid + && p.PosiDirection == 0 + && p.PositionType == 0 + && string.IsNullOrEmpty(p.UnderlyingCode) + && p.InterestMode == 9) .Select(p => new { p.id, + p.InterestMode, + p.InterestDirection, + p.FloatRateUnderlyingCode, p.InterestRateDefault, InterestRateDeviation = Math.Abs(p.InterestRateDefault * 100m) }) .ToList(); if (!interestItems.Any()) - throw new Exception("利息端收取方向记录不存在"); + throw new Exception("同次录入的利息端记录不存在"); -var maxDeviationItem = interestItems +var invalidFloatRateItems = interestItems + .Where(p => string.IsNullOrEmpty(p.FloatRateUnderlyingCode) || p.FloatRateUnderlyingCode != "FR007") + .ToList(); + +if (invalidFloatRateItems.Any()) +{ + var invalidFloatRateMessage = "基准利率不合法记录:" + string.Join(";", invalidFloatRateItems + .OrderBy(p => p.id) + .Select(p => $"记录ID为{p.id},基准利率为{(string.IsNullOrEmpty(p.FloatRateUnderlyingCode) ? "空" : p.FloatRateUnderlyingCode)}")); + + var validDeviationItems = interestItems + .Where(p => p.FloatRateUnderlyingCode == "FR007") + .OrderByDescending(p => p.InterestRateDeviation) + .ThenBy(p => p.id) + .ToList(); + + var validDeviationMessage = validDeviationItems.Any() + ? ";FR007记录偏离明细:" + string.Join(";", validDeviationItems.Select(p => $"记录ID为{p.id},点差为{p.InterestRateDefault * 100m:0.#########}%,偏离绝对值为{p.InterestRateDeviation:0.#########}%,收支方向为{(p.InterestDirection == 1 ? "收取" : p.InterestDirection == 2 ? "支付" : "未知:" + p.InterestDirection)}")) + : string.Empty; + + throw new Exception($"基准利率保护触发:规则16要求基准利率必须为FR007。{invalidFloatRateMessage}{validDeviationMessage}"); +} + +var orderedInterestItems = interestItems .OrderByDescending(p => p.InterestRateDeviation) .ThenBy(p => p.id) - .First(); + .ToList(); + +var maxDeviationItem = orderedInterestItems.First(); + +var interestDetails = orderedInterestItems + .Select(p => new RiskVariableValueDetailItem( + p.InterestRateDeviation, + $"记录ID为{p.id},基准利率为{(string.IsNullOrEmpty(p.FloatRateUnderlyingCode) ? "空" : p.FloatRateUnderlyingCode)},点差为{p.InterestRateDefault * 100m:0.#########}%,偏离绝对值为{p.InterestRateDeviation:0.#########}%,收支方向为{(p.InterestDirection == 1 ? "收取" : p.InterestDirection == 2 ? "支付" : "未知:" + p.InterestDirection)}")) + .ToList(); return new RiskVariableValueDetail( maxDeviationItem.InterestRateDeviation, - $"交易ID {TradeId},利息端记录ID为{maxDeviationItem.id},点差为{maxDeviationItem.InterestRateDefault * 100m:0.#########}%,最大偏离绝对值为{maxDeviationItem.InterestRateDeviation:0.#########}%"); + $"交易ID {TradeId},多头浮动端记录ID为{string.Join("、", longFloatItems.Select(p => p.id))},普通利息端共{orderedInterestItems.Count}条,最大偏离绝对值为{maxDeviationItem.InterestRateDeviation:0.#########}%", + interestDetails); ``` -汇总 SQL:与变量一样取全部符合条件记录中的最大偏离值;命中说明只展示产生最大偏离值的记录。 +汇总 SQL:先确认存在多头浮动端,再按同次录入普通利息端记录取最大偏离值用于规则比较;若存在基准利率为空或不是 FR007 的记录则实际执行时会抛异常触发风控报警,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 + WHEN COUNT(DISTINCT fp.id) = 0 THEN NULL + ELSE MAX(ABS(ip.InterestRateDefault * 100)) + END AS MaxInterestRateDeviation, + CASE + WHEN COUNT(DISTINCT fp.id) = 0 THEN 0 + WHEN MAX(ABS(ip.InterestRateDefault * 100)) > @Threshold THEN 1 + ELSE 0 + END AS IsGreaterThanThreshold, + SUM(CASE WHEN ip.FloatRateUnderlyingCode IS NULL OR ip.FloatRateUnderlyingCode = '' OR ip.FloatRateUnderlyingCode <> 'FR007' THEN 1 ELSE 0 END) AS InvalidFloatRateCodeCount +FROM swap_position fp +LEFT JOIN swap_position ip + ON ip.SwapTradeId = fp.SwapTradeId + AND ip.IsInitial = fp.IsInitial + AND ip.Invalid = fp.Invalid + AND ip.PosiDirection = 0 + AND ip.PositionType = 0 + AND (ip.UnderlyingCode IS NULL OR ip.UnderlyingCode = '') + AND ip.InterestMode = 9 +WHERE fp.SwapTradeId = @TradeId + AND fp.IsInitial = 1 + AND fp.Invalid = 0 + AND fp.PosiDirection > 0 + AND fp.PositionType = 1; +``` + +明细 SQL:用于排查多头浮动端及其同次录入普通利息端记录;按偏离绝对值倒序展示所有纳入变量计算的普通利息端记录,基准利率为空或不是 FR007 的记录会标记 IsProtectionTrigger=1,实际执行时抛异常触发风控报警。 + +```sql +SET @TradeId = 3001699; +SET @Threshold = 5; + +SELECT + fp.id AS FloatPositionId, + ip.id AS InterestPositionId, + fp.SwapTradeId, + fp.IsInitial, + fp.Invalid, + fp.PosiDirection, + fp.PositionType, + ip.InterestMode, + ip.InterestDirection, + ip.FloatRateUnderlyingCode, + CASE + WHEN ip.FloatRateUnderlyingCode IS NULL OR ip.FloatRateUnderlyingCode = '' OR ip.FloatRateUnderlyingCode <> 'FR007' THEN 1 + ELSE 0 + END AS IsProtectionTrigger, + ip.InterestRateDefault, + ip.InterestRateDefault * 100 AS InterestRateDefault_100, + ABS(ip.InterestRateDefault * 100) AS DiffAbs, + CASE + WHEN ABS(ip.InterestRateDefault * 100) > @Threshold THEN 1 ELSE 0 END AS IsGreaterThanThreshold -FROM swap_position sp -WHERE sp.SwapTradeId = @TradeId - AND sp.InterestDirection = 1; +FROM swap_position fp +LEFT JOIN swap_position ip + ON ip.SwapTradeId = fp.SwapTradeId + AND ip.IsInitial = fp.IsInitial + AND ip.Invalid = fp.Invalid + AND ip.PosiDirection = 0 + AND ip.PositionType = 0 + AND (ip.UnderlyingCode IS NULL OR ip.UnderlyingCode = '') + AND ip.InterestMode = 9 +WHERE fp.SwapTradeId = @TradeId + AND fp.IsInitial = 1 + AND fp.Invalid = 0 + AND fp.PosiDirection > 0 + AND fp.PositionType = 1 +ORDER BY DiffAbs DESC, ip.id ASC; ``` -明细 SQL:用于排查全部利息端记录;变量命中说明只展示产生最大偏离值的记录。 +异常排查 SQL:用于排查变量执行失败“同次录入的利息端记录不存在”。重点看是否存在多头浮动端,以及是否存在同一 TradeId 下初始有效、PosiDirection=0、PositionType=0、UnderlyingCode 为空、InterestMode=9 的普通利息端记录;InterestMode=5/6 为保证金/预付金,不应计入规则16利息端。 ```sql SET @TradeId = 3001699; -SET @Threshold = 5; SELECT - sp.id AS SwapPositionId, + '当前交易初始有效持仓概览' AS CheckItem, + sp.id, sp.SwapTradeId, + sp.IsInitial, + sp.Invalid, + sp.PosiDirection, + sp.PositionType, + CASE sp.PositionType + WHEN 1 THEN '多头' + WHEN 2 THEN '空头' + ELSE CONCAT('未知:', sp.PositionType) + END AS PositionTypeName, + sp.InterestMode, + CASE sp.InterestMode + WHEN 5 THEN '初始预付金' + WHEN 6 THEN '追加预付金' + WHEN 9 THEN '标的期初全价' + ELSE CONCAT('未知:', sp.InterestMode) + END AS InterestModeName, sp.InterestDirection, + CASE sp.InterestDirection + WHEN 1 THEN '收取' + WHEN 2 THEN '支付' + ELSE CONCAT('未知:', sp.InterestDirection) + END AS InterestDirectionName, + sp.UnderlyingCode, + sp.FloatRateUnderlyingCode, sp.InterestRateDefault, sp.InterestRateDefault * 100 AS InterestRateDefault_100, - ABS(sp.InterestRateDefault * 100) AS DiffAbs, - CASE - WHEN ABS(sp.InterestRateDefault * 100) > @Threshold THEN 1 - ELSE 0 - END AS IsGreaterThanThreshold + ABS(sp.InterestRateDefault * 100) AS DiffAbs FROM swap_position sp WHERE sp.SwapTradeId = @TradeId - AND sp.InterestDirection = 1 -ORDER BY DiffAbs DESC, sp.id ASC; + AND sp.IsInitial = 1 + AND sp.Invalid = 0 +ORDER BY + sp.PosiDirection DESC, + sp.PositionType, + sp.InterestDirection, + sp.id; + +SELECT + SUM(CASE WHEN sp.PosiDirection > 0 AND sp.PositionType = 1 THEN 1 ELSE 0 END) AS LongFloatCount, + SUM(CASE WHEN sp.PosiDirection = 0 AND sp.PositionType = 0 AND (sp.UnderlyingCode IS NULL OR sp.UnderlyingCode = '') AND sp.InterestMode = 9 THEN 1 ELSE 0 END) AS InterestLegCount, + SUM(CASE WHEN sp.PosiDirection = 0 AND sp.PositionType = 0 AND (sp.UnderlyingCode IS NULL OR sp.UnderlyingCode = '') AND sp.InterestMode IN (5, 6) THEN 1 ELSE 0 END) AS MarginLegCount, + SUM(CASE WHEN sp.PosiDirection = 0 AND sp.PositionType = 0 AND (sp.UnderlyingCode IS NULL OR sp.UnderlyingCode = '') AND sp.InterestMode = 9 AND sp.InterestDirection = 1 THEN 1 ELSE 0 END) AS InterestLegDirection1Count, + SUM(CASE WHEN sp.PosiDirection = 0 AND sp.PositionType = 0 AND (sp.UnderlyingCode IS NULL OR sp.UnderlyingCode = '') AND sp.InterestMode = 9 AND sp.InterestDirection = 2 THEN 1 ELSE 0 END) AS InterestLegDirection2Count, + SUM(CASE WHEN sp.PosiDirection = 0 AND sp.PositionType = 0 AND (sp.UnderlyingCode IS NULL OR sp.UnderlyingCode = '') AND sp.InterestMode = 9 AND (sp.FloatRateUnderlyingCode IS NULL OR sp.FloatRateUnderlyingCode = '' OR sp.FloatRateUnderlyingCode <> 'FR007') THEN 1 ELSE 0 END) AS InvalidFloatRateCodeCount, + CASE + WHEN SUM(CASE WHEN sp.PosiDirection > 0 AND sp.PositionType = 1 THEN 1 ELSE 0 END) = 0 + THEN '不存在多头浮动端,规则16不适用,应返回0不触发' + WHEN SUM(CASE WHEN sp.PosiDirection = 0 AND sp.PositionType = 0 AND (sp.UnderlyingCode IS NULL OR sp.UnderlyingCode = '') AND sp.InterestMode = 9 THEN 1 ELSE 0 END) = 0 + THEN '存在多头浮动端,但不存在对应普通利息端记录;需检查同次录入利息端是否满足 PosiDirection=0、PositionType=0、UnderlyingCode 为空、InterestMode=9,不能把 InterestMode=5/6 的保证金/预付金当作利息端' + WHEN SUM(CASE WHEN sp.PosiDirection = 0 AND sp.PositionType = 0 AND (sp.UnderlyingCode IS NULL OR sp.UnderlyingCode = '') AND sp.InterestMode = 9 AND (sp.FloatRateUnderlyingCode IS NULL OR sp.FloatRateUnderlyingCode = '' OR sp.FloatRateUnderlyingCode <> 'FR007') THEN 1 ELSE 0 END) > 0 + THEN '存在普通利息端基准利率为空或不是FR007,规则16应触发基准利率保护异常,不应继续计算点差偏离' + ELSE '存在多头浮动端且存在对应普通利息端记录,基准利率均为FR007,可以计算规则16偏离值;规则16不按 InterestDirection 区分收取或支付,并排除保证金/预付金记录' + END AS DiagnosticResult +FROM swap_position sp +WHERE sp.SwapTradeId = @TradeId + AND sp.IsInitial = 1 + AND sp.Invalid = 0; ``` --- -## 28. 规则 18 查询结果排查 SQL +## 28. 规则 17 查询结果排查 SQL -规则 18:账户授权收支方向不匹配(本地)。用于核对账户授权的收支方向与交易浮动端多空方向是否匹配;不符合允许场景时命中。 - -校验内容: - -```text -不符合以下场景则不通过: -1. 利息端 = 收取,且浮动端 = 空头 -2. 利息端 = 支付,且浮动端 = 多头 -``` +规则 17:空头利息端利率与借贷加权费率偏离(本地)。用于核对空头浮动端同次录入的普通利息端利率与标的借贷加权费率的偏离绝对值是否大于阈值,利息端不按 InterestDirection 区分收取或支付。 取数流程: ```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. 若不是“利息端收取且浮动端空头”,也不是“利息端支付且浮动端多头”,则命中。 +2. 先限定 IsInitial=1、Invalid=0、PosiDirection>0、PositionType=2,取空头浮动端记录,得到标的 UnderlyingCode。 +3. 若不存在空头浮动端记录,说明当前 trade 不适用规则17,变量返回 0,不触发风控。 +4. 空头浮动端标的必须为“数字.IB”或“数字.BC”格式;合法时取点号前数字作为借贷费率查询标的,其他格式抛异常触发风控报警。 +5. 再取同一 TradeId、同为初始有效、PosiDirection=0、PositionType=0、UnderlyingCode 为空、InterestMode=9 的普通利息端记录,不按 InterestDirection 区分收取或支付,排除 InterestMode=5/6 的保证金/预付金记录。 +6. 从普通利息端记录取 InterestRateDefault 和 FloatRateUnderlyingCode。 +7. InterestRateDefault 只代表利率文本框中 + 号后的点差,库内为小数原值,界面按百分比显示。 +8. 基准利率保护:FloatRateUnderlyingCode 必须为 FR007;为空或不是 FR007 时抛异常触发风控报警。 +9. 所有空头浮动端和普通利息端都要检查;若存在空头浮动端标的非法或基准利率不是 FR007 的记录,异常信息同时列出非法记录,以及合法记录组合后的偏离明细,避免只看到第一类问题。 +10. 从 eod_commodity_future_price 取该基准利率代码在 TradeDate 之前最近一条的 ReferencePrice(小数口径,如 0.025 表示 2.5%)。 +11. 从 eod_bond_lending_rate 取空头浮动端标的点号前数字在 TradeDate 之前最近一条的 WeightedAvgRate(百分比值,如 2.5 表示 2.5%)。 +12. 利息端利率(%) = (基准利率 + 点差) * 100,借贷加权费率(%) = WeightedAvgRate。 +13. 若空头浮动端标的和基准利率均合法,则对所有空头浮动端和普通利息端组合逐条计算 ABS(利息端利率(%) - 借贷加权费率(%)),取最大值与规则阈值比较;同时返回逐条明细,由结构化执行器按当前规则阈值拆分展示超限记录和未超限记录。 ``` 规则公式: ```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 +存在空头浮动端时,逐条检查空头浮动端和普通利息端;若空头浮动端标的不是“数字.IB”或“数字.BC”格式,或 FloatRateUnderlyingCode 为空/不是 FR007,则抛异常触发风控报警,异常信息同时列出非法记录和合法记录组合后的偏离明细;若空头浮动端标的和基准利率均合法,则用空头浮动端标的点号前数字查借贷加权费率,并取所有空头浮动端和普通利息端组合 ABS((基准利率 + InterestRateDefault) * 100 - 借贷加权费率) 的最大值与阈值比较;变量明细逐条返回偏离值,由结构化执行器按规则阈值拆分展示超限记录和未超限记录;不存在空头浮动端时返回 0,不触发 ``` 注释规则定义: @@ -3669,10 +3880,10 @@ NOT ((InterestDirection = 1 AND PositionType = 2) OR (InterestDirection = 2 AND ```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)))", +// Id = 1000017, +// RuleName = "空头利息端利率与借贷加权费率偏离(本地)", +// RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 先取 IsInitial=true、Invalid=false、PosiDirection>0、PositionType=2 的空头浮动端记录;若不存在空头浮动端记录,说明当前 trade 不适用规则17,变量返回 0,不触发风控;若存在空头浮动端记录,再取同一 TradeId、同为初始有效、PosiDirection=0、PositionType=0、UnderlyingCode 为空、InterestMode=9 的普通利息端记录,不按 InterestDirection 区分收取或支付,排除 InterestMode=5/6 的保证金/预付金记录。利息端利率 = 基准利率 + 点差,基准利率取 eod_commodity_future_price 中 FloatRateUnderlyingCode 在 TradeDate 前最近一条的 ReferencePrice(小数口径);FloatRateUnderlyingCode 必须为 FR007,为空或不是 FR007 时抛异常触发风控报警,异常信息同时列出基准利率非法记录和基准利率为 FR007 的合法记录与空头浮动端组合后的偏离明细。点差为 InterestRateDefault(小数原值)。空头浮动端 UnderlyingCode 必须为“数字.IB”或“数字.BC”格式;合法时取点号前数字作为 eod_bond_lending_rate.UnderlyingSecurityId 查询 TradeDate 前最近一条 WeightedAvgRate(百分比值),其他格式抛异常触发风控报警。计算逻辑:对所有合法空头浮动端和普通利息端组合逐条按 ABS((基准利率 + 点差) * 100 - 借贷加权费率) 计算偏离绝对值,取最大值与阈值比较;变量明细逐条返回偏离值,结构化执行器按规则阈值拆分展示超限记录和未超限记录。", +// RuleExpr = "", // Version = 1, // Status = RiskRuleStatus.Active, // OptId = 0, @@ -3684,35 +3895,510 @@ NOT ((InterestDirection = 1 AND PositionType = 2) OR (InterestDirection = 2 AND //}); ``` +变量形式: + +变量名:利息端利率与借贷加权费率偏离度 +DataType:Numeric +规则配置:利息端利率与借贷加权费率偏离度 > 阈值(示例 5) + +变量取值表达式: + +```csharp +var shortFloatItems = DbContext.swap_position + .Where(p => p.SwapTradeId == TradeId + && p.IsInitial + && !p.Invalid + && p.PosiDirection > 0 + && p.PositionType == 2) + .Select(p => new { p.id, p.UnderlyingCode }) + .ToList(); + +if (!shortFloatItems.Any()) + return new RiskVariableValueDetail( + 0m, + $"交易ID {TradeId} 不存在空头浮动端记录,规则17不适用,不触发风控"); + +var invalidShortFloatItems = shortFloatItems + .Where(p => string.IsNullOrEmpty(p.UnderlyingCode) + || !(p.UnderlyingCode.EndsWith(".IB") || p.UnderlyingCode.EndsWith(".BC")) + || p.UnderlyingCode.LastIndexOf(".") <= 0 + || !p.UnderlyingCode.Substring(0, p.UnderlyingCode.LastIndexOf(".")).All(c => char.IsDigit(c))) + .ToList(); + +var validShortFloatItems = shortFloatItems + .Where(p => !string.IsNullOrEmpty(p.UnderlyingCode) + && (p.UnderlyingCode.EndsWith(".IB") || p.UnderlyingCode.EndsWith(".BC")) + && p.UnderlyingCode.LastIndexOf(".") > 0 + && p.UnderlyingCode.Substring(0, p.UnderlyingCode.LastIndexOf(".")).All(c => char.IsDigit(c))) + .Select(p => new + { + p.id, + p.UnderlyingCode, + LendingRateUnderlyingCode = p.UnderlyingCode.Substring(0, p.UnderlyingCode.LastIndexOf(".")) + }) + .ToList(); + +var interestItems = DbContext.swap_position + .Where(p => p.SwapTradeId == TradeId + && p.IsInitial + && !p.Invalid + && p.PosiDirection == 0 + && p.PositionType == 0 + && string.IsNullOrEmpty(p.UnderlyingCode) + && p.InterestMode == 9) + .Select(p => new { p.id, p.InterestMode, p.InterestDirection, p.InterestRateDefault, p.FloatRateUnderlyingCode }) + .ToList(); + +if (!interestItems.Any()) + throw new Exception("同次录入的普通利息端记录不存在"); + +var invalidFloatRateItems = interestItems + .Where(p => string.IsNullOrEmpty(p.FloatRateUnderlyingCode) || p.FloatRateUnderlyingCode != "FR007") + .ToList(); + +var validInterestItems = interestItems + .Where(p => p.FloatRateUnderlyingCode == "FR007") + .ToList(); + +if ((invalidShortFloatItems.Any() && !validShortFloatItems.Any()) || (invalidFloatRateItems.Any() && !validInterestItems.Any())) +{ + var invalidShortFloatMessage = invalidShortFloatItems.Any() + ? "空头浮动端标的不合法记录:" + string.Join(";", invalidShortFloatItems + .OrderBy(p => p.id) + .Select(p => $"记录ID为{p.id},标的为{(string.IsNullOrEmpty(p.UnderlyingCode) ? "空" : p.UnderlyingCode)},仅允许数字.IB或数字.BC格式")) + : string.Empty; + + var invalidFloatRateMessage = invalidFloatRateItems.Any() + ? (string.IsNullOrEmpty(invalidShortFloatMessage) ? string.Empty : ";") + "基准利率非法记录:" + string.Join(";", invalidFloatRateItems + .OrderBy(p => p.id) + .Select(p => $"记录ID为{p.id},基准利率为{(string.IsNullOrEmpty(p.FloatRateUnderlyingCode) ? "空" : p.FloatRateUnderlyingCode)},点差为{p.InterestRateDefault * 100m:0.#########}%,收支方向为{(p.InterestDirection == 1 ? "收取" : p.InterestDirection == 2 ? "支付" : "未知:" + p.InterestDirection)}")) + : string.Empty; + + throw new Exception($"规则17保护触发:空头浮动端标的仅允许数字.IB或数字.BC格式,普通利息端基准利率必须为FR007。{invalidShortFloatMessage}{invalidFloatRateMessage}"); +} + +var calculationInterestItems = invalidFloatRateItems.Any() ? validInterestItems : interestItems; + +var tradeDate = DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date; + +var orderedDeviationItems = validShortFloatItems + .OrderBy(p => p.id) + .SelectMany(shortFloatItem => + { + var lendingRate = DbContext.eod_bond_lending_rate + .Where(x => x.UnderlyingSecurityId == shortFloatItem.LendingRateUnderlyingCode && x.ValueDate <= tradeDate) + .OrderByDescending(x => x.ValueDate) + .FirstOrDefault(); + + if (lendingRate == null || !lendingRate.WeightedAvgRate.HasValue) + throw new Exception($"标的 {shortFloatItem.UnderlyingCode} 对应借贷费率查询标的 {shortFloatItem.LendingRateUnderlyingCode} 在交易日 {tradeDate:yyyy-MM-dd} 前未找到借贷加权费率"); + + var lendingRatePercent = lendingRate.WeightedAvgRate.Value; + + return calculationInterestItems + .OrderBy(p => p.id) + .Select(interestItem => + { + var floatRateCode = interestItem.FloatRateUnderlyingCode; + + var floatRatePrice = DbContext.eod_commodity_future_price + .Where(e => e.UnderlyingCode == floatRateCode && e.ValueDate <= tradeDate) + .OrderByDescending(e => e.ValueDate) + .FirstOrDefault(); + + var floatRate = floatRatePrice != null && floatRatePrice.ReferencePrice.HasValue ? Convert.ToDecimal(floatRatePrice.ReferencePrice.Value) : 0m; + var interestRatePercent = (floatRate + interestItem.InterestRateDefault) * 100m; + var deviation = Math.Abs(interestRatePercent - lendingRatePercent); + + return new + { + FloatPositionId = shortFloatItem.id, + shortFloatItem.UnderlyingCode, + shortFloatItem.LendingRateUnderlyingCode, + InterestPositionId = interestItem.id, + interestItem.InterestMode, + interestItem.InterestDirection, + interestItem.InterestRateDefault, + FloatRateCode = floatRateCode, + FloatRate = floatRate, + InterestRatePercent = interestRatePercent, + LendingRatePercent = lendingRatePercent, + Deviation = deviation + }; + }); + }) + .OrderByDescending(p => p.Deviation) + .ThenBy(p => p.FloatPositionId) + .ThenBy(p => p.InterestPositionId) + .ToList(); + +if (invalidShortFloatItems.Any() || invalidFloatRateItems.Any()) +{ + var invalidShortFloatMessage = invalidShortFloatItems.Any() + ? "空头浮动端标的不合法记录:" + string.Join(";", invalidShortFloatItems + .OrderBy(p => p.id) + .Select(p => $"记录ID为{p.id},标的为{(string.IsNullOrEmpty(p.UnderlyingCode) ? "空" : p.UnderlyingCode)},仅允许数字.IB或数字.BC格式")) + : string.Empty; + + var invalidFloatRateMessage = invalidFloatRateItems.Any() + ? (string.IsNullOrEmpty(invalidShortFloatMessage) ? string.Empty : ";") + "基准利率非法记录:" + string.Join(";", invalidFloatRateItems + .OrderBy(p => p.id) + .Select(p => $"记录ID为{p.id},基准利率为{(string.IsNullOrEmpty(p.FloatRateUnderlyingCode) ? "空" : p.FloatRateUnderlyingCode)},点差为{p.InterestRateDefault * 100m:0.#########}%,收支方向为{(p.InterestDirection == 1 ? "收取" : p.InterestDirection == 2 ? "支付" : "未知:" + p.InterestDirection)}")) + : string.Empty; + + var validDeviationMessage = orderedDeviationItems.Any() + ? ";合法记录偏离明细:" + string.Join(";", orderedDeviationItems.Select(p => $"空头浮动端记录ID为{p.FloatPositionId},标的{p.UnderlyingCode},借贷费率查询标的{p.LendingRateUnderlyingCode},普通利息端记录ID为{p.InterestPositionId},基准利率{p.FloatRateCode}为{p.FloatRate * 100m:0.#########}%,点差为{p.InterestRateDefault * 100m:0.#########}%,利息端利率为{p.InterestRatePercent:0.#########}%,借贷加权费率为{p.LendingRatePercent:0.#########}%,偏离绝对值为{p.Deviation:0.#########}%,收支方向为{(p.InterestDirection == 1 ? "收取" : p.InterestDirection == 2 ? "支付" : "未知:" + p.InterestDirection)}")) + : string.Empty; + + throw new Exception($"规则17保护触发:空头浮动端标的仅允许数字.IB或数字.BC格式,普通利息端基准利率必须为FR007。{invalidShortFloatMessage}{invalidFloatRateMessage}{validDeviationMessage}"); +} + +var maxDeviationItem = orderedDeviationItems.First(); + +var detailItems = orderedDeviationItems + .Select(p => new RiskVariableValueDetailItem( + p.Deviation, + $"空头浮动端记录ID为{p.FloatPositionId},标的{p.UnderlyingCode},借贷费率查询标的{p.LendingRateUnderlyingCode},普通利息端记录ID为{p.InterestPositionId},基准利率{p.FloatRateCode}为{p.FloatRate * 100m:0.#########}%,点差为{p.InterestRateDefault * 100m:0.#########}%,利息端利率为{p.InterestRatePercent:0.#########}%,借贷加权费率为{p.LendingRatePercent:0.#########}%,偏离绝对值为{p.Deviation:0.#########}%,收支方向为{(p.InterestDirection == 1 ? "收取" : p.InterestDirection == 2 ? "支付" : "未知:" + p.InterestDirection)}")) + .ToList(); + +return new RiskVariableValueDetail( + maxDeviationItem.Deviation, + $"交易ID {TradeId},空头浮动端共{shortFloatItems.Count}条,普通利息端共{interestItems.Count}条,最大偏离绝对值为{maxDeviationItem.Deviation:0.#########}%", + detailItems); +``` + +汇总 SQL:先确认存在空头浮动端,再按合法空头浮动端和同次录入普通利息端组合及对应行情数据计算最大偏离值;借贷费率查询标的取空头浮动端 UnderlyingCode 点号前数字;若存在空头浮动端标的非法或基准利率不是 FR007 的记录则实际执行时会抛异常触发风控报警,SQL 中单独列出此类记录数供排查。 + +```sql +SET @TradeId = 3001699; +SET @Threshold = 5; + +SELECT + CASE + WHEN COUNT(DISTINCT fp.id) = 0 THEN NULL + ELSE MAX(CASE WHEN fp.UnderlyingCode REGEXP '^[0-9]+[.](IB|BC)$' AND ip.FloatRateUnderlyingCode = 'FR007' THEN ABS((IFNULL(fr.ReferencePrice, 0) + ip.InterestRateDefault) * 100 - blr.WeightedAvgRate) END) + END AS MaxDeviation, + CASE + WHEN COUNT(DISTINCT fp.id) = 0 THEN 0 + WHEN MAX(CASE WHEN fp.UnderlyingCode REGEXP '^[0-9]+[.](IB|BC)$' AND ip.FloatRateUnderlyingCode = 'FR007' THEN ABS((IFNULL(fr.ReferencePrice, 0) + ip.InterestRateDefault) * 100 - blr.WeightedAvgRate) END) > @Threshold THEN 1 + ELSE 0 + END AS IsGreaterThanThreshold, + COUNT(DISTINCT CASE WHEN fp.id IS NOT NULL AND (fp.UnderlyingCode IS NULL OR fp.UnderlyingCode = '' OR NOT (fp.UnderlyingCode REGEXP '^[0-9]+[.](IB|BC)$')) THEN fp.id END) AS InvalidShortUnderlyingCodeCount, + COUNT(DISTINCT CASE WHEN ip.id IS NOT NULL AND IFNULL(ip.FloatRateUnderlyingCode, '') <> 'FR007' THEN ip.id END) AS InvalidFloatRateCodeCount +FROM swap_position fp +LEFT JOIN swap_position ip + ON ip.SwapTradeId = fp.SwapTradeId + AND ip.IsInitial = fp.IsInitial + AND ip.Invalid = fp.Invalid + AND ip.PosiDirection = 0 + AND ip.PositionType = 0 + AND (ip.UnderlyingCode IS NULL OR ip.UnderlyingCode = '') + AND ip.InterestMode = 9 +LEFT JOIN trade t + ON t.id = fp.SwapTradeId +LEFT JOIN eod_commodity_future_price fr + ON fr.FutureContractId = ip.FloatRateUnderlyingCode + AND fr.ValueDate = ( + SELECT MAX(e.ValueDate) + FROM eod_commodity_future_price e + WHERE e.FutureContractId = ip.FloatRateUnderlyingCode + AND e.ValueDate <= t.TradeDate + ) +LEFT JOIN eod_bond_lending_rate blr + ON blr.UnderlyingSecurityId = SUBSTRING_INDEX(fp.UnderlyingCode, '.', 1) + AND fp.UnderlyingCode REGEXP '^[0-9]+[.](IB|BC)$' + AND blr.ValueDate = ( + SELECT MAX(b.ValueDate) + FROM eod_bond_lending_rate b + WHERE b.UnderlyingSecurityId = SUBSTRING_INDEX(fp.UnderlyingCode, '.', 1) + AND fp.UnderlyingCode REGEXP '^[0-9]+[.](IB|BC)$' + AND b.ValueDate <= t.TradeDate + ) +WHERE fp.SwapTradeId = @TradeId + AND fp.IsInitial = 1 + AND fp.Invalid = 0 + AND fp.PosiDirection > 0 + AND fp.PositionType = 2; +``` + +明细 SQL:用于排查空头浮动端及其同次录入普通利息端记录、基准利率、借贷加权费率;展示空头浮动端原始标的、借贷费率查询标的和保护标记,基准利率不是 FR007 或空头浮动端标的非法的记录会标记保护字段,实际执行时抛异常触发风控报警。 + +```sql +SET @TradeId = 3001699; +SET @Threshold = 5; + +SELECT + fp.id AS FloatPositionId, + ip.id AS InterestPositionId, + fp.SwapTradeId, + fp.IsInitial, + fp.Invalid, + fp.PosiDirection, + fp.PositionType, + fp.UnderlyingCode, + CASE + WHEN fp.UnderlyingCode REGEXP '^[0-9]+[.](IB|BC)$' THEN SUBSTRING_INDEX(fp.UnderlyingCode, '.', 1) + ELSE NULL + END AS LendingRateUnderlyingCode, + CASE + WHEN fp.UnderlyingCode IS NULL OR fp.UnderlyingCode = '' OR NOT (fp.UnderlyingCode REGEXP '^[0-9]+[.](IB|BC)$') THEN 1 + ELSE 0 + END AS IsShortUnderlyingProtectionTrigger, + ip.InterestMode, + ip.InterestDirection, + CASE ip.InterestDirection + WHEN 1 THEN '收取' + WHEN 2 THEN '支付' + ELSE CONCAT('未知:', ip.InterestDirection) + END AS InterestDirectionName, + ip.FloatRateUnderlyingCode, + CASE + WHEN ip.id IS NOT NULL AND IFNULL(ip.FloatRateUnderlyingCode, '') <> 'FR007' THEN 1 + ELSE 0 + END AS IsProtectionTrigger, + ip.InterestRateDefault, + ip.InterestRateDefault * 100 AS InterestRateDefault_100, + fr.ReferencePrice AS FloatRateReferencePrice, + fr.ValueDate AS FloatRateValueDate, + (IFNULL(fr.ReferencePrice, 0) + ip.InterestRateDefault) * 100 AS InterestRatePercent, + blr.WeightedAvgRate AS LendingWeightedAvgRate, + blr.ValueDate AS LendingValueDate, + CASE WHEN fp.UnderlyingCode REGEXP '^[0-9]+[.](IB|BC)$' AND ip.FloatRateUnderlyingCode = 'FR007' THEN ABS((IFNULL(fr.ReferencePrice, 0) + ip.InterestRateDefault) * 100 - blr.WeightedAvgRate) END AS DiffAbs, + CASE + WHEN fp.UnderlyingCode REGEXP '^[0-9]+[.](IB|BC)$' AND ip.FloatRateUnderlyingCode = 'FR007' AND ABS((IFNULL(fr.ReferencePrice, 0) + ip.InterestRateDefault) * 100 - blr.WeightedAvgRate) > @Threshold THEN 1 + ELSE 0 + END AS IsGreaterThanThreshold +FROM swap_position fp +LEFT JOIN swap_position ip + ON ip.SwapTradeId = fp.SwapTradeId + AND ip.IsInitial = fp.IsInitial + AND ip.Invalid = fp.Invalid + AND ip.PosiDirection = 0 + AND ip.PositionType = 0 + AND (ip.UnderlyingCode IS NULL OR ip.UnderlyingCode = '') + AND ip.InterestMode = 9 +LEFT JOIN trade t + ON t.id = fp.SwapTradeId +LEFT JOIN eod_commodity_future_price fr + ON fr.FutureContractId = ip.FloatRateUnderlyingCode + AND fr.ValueDate = ( + SELECT MAX(e.ValueDate) + FROM eod_commodity_future_price e + WHERE e.FutureContractId = ip.FloatRateUnderlyingCode + AND e.ValueDate <= t.TradeDate + ) +LEFT JOIN eod_bond_lending_rate blr + ON blr.UnderlyingSecurityId = SUBSTRING_INDEX(fp.UnderlyingCode, '.', 1) + AND fp.UnderlyingCode REGEXP '^[0-9]+[.](IB|BC)$' + AND blr.ValueDate = ( + SELECT MAX(b.ValueDate) + FROM eod_bond_lending_rate b + WHERE b.UnderlyingSecurityId = SUBSTRING_INDEX(fp.UnderlyingCode, '.', 1) + AND fp.UnderlyingCode REGEXP '^[0-9]+[.](IB|BC)$' + AND b.ValueDate <= t.TradeDate + ) +WHERE fp.SwapTradeId = @TradeId + AND fp.IsInitial = 1 + AND fp.Invalid = 0 + AND fp.PosiDirection > 0 + AND fp.PositionType = 2 +ORDER BY DiffAbs DESC, fp.id ASC, ip.id ASC; +``` + +--- + +## 29. 规则 18 查询结果排查 SQL + +规则 18:账户授权收支方向不匹配(本地)。用于核对账户授权的收支方向与交易浮动端多空方向是否匹配;不符合允许场景时命中。 + +校验内容: + +```text +只允许利息端收取且浮动端空头或者利息端支付且浮动端多头。 +不符合以下场景则不通过: +1. 利息端 = 收取,且浮动端 = 空头 +2. 利息端 = 支付,且浮动端 = 多头 +``` + +取数流程: + +```text +1. 根据 TradeId 查 swap_position。 +2. 限定 IsInitial=1、Invalid=0、PosiDirection>0,取浮动端初始持仓记录,取 PositionType 作为浮动端多空方向,1=多头,2=空头。 +3. 参考规则16、17的同次录入口径,再取同一 TradeId、同为初始有效、PosiDirection=0、PositionType=0、UnderlyingCode 为空、InterestMode=9 的普通利息端记录,取 InterestDirection 作为利息端收支方向,1=收取,2=支付。 +4. 利息端可能有多条,需将所有浮动端与所有普通利息端逐条组合检查。 +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 +普通利息端记录:swap_position.IsInitial=1、Invalid=0、PosiDirection=0、PositionType=0、UnderlyingCode 为空、InterestMode=9 +``` + +注释规则定义: + +```csharp +//rules.Add(new RiskRule +//{ +// Id = 1000018, +// RuleName = "账户授权收支方向不匹配(本地)", +// RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 取 IsInitial=true、Invalid=false、PosiDirection>0 的浮动端初始持仓记录,PositionType 表示浮动端多空方向,1=多头、2=空头;参考规则16、17的同次录入口径,再取同一 TradeId、同为初始有效、PosiDirection=0、PositionType=0、UnderlyingCode 为空、InterestMode=9 的普通利息端记录,InterestDirection 表示利息端收支方向,1=收取、2=支付。计算逻辑:将所有浮动端与所有普通利息端逐条组合检查,仅允许利息端=收取且浮动端=空头,或利息端=支付且浮动端=多头;任一组合不符合则触发审批。", +// RuleExpr = "", +// Version = 1, +// Status = RiskRuleStatus.Active, +// OptId = 0, +// OptName = "system", +// OptDate = DateTime.Now, +// UpdateOptId = 0, +// UpdateOptName = "system", +// UpdateDate = DateTime.Now +//}); +``` + +变量形式: + +变量名:账户授权收支方向不匹配标记 +DataType:Numeric +规则配置:账户授权收支方向不匹配标记 > 0 + +变量取值表达式: + +```csharp +var floatItems = DbContext.swap_position + .Where(p => p.SwapTradeId == TradeId + && p.IsInitial + && !p.Invalid + && p.PosiDirection > 0) + .Select(p => new + { + p.id, + p.PosiDirection, + p.PositionType + }) + .ToList(); + +if (!floatItems.Any()) + throw new Exception("浮动端初始持仓记录不存在"); + +var interestItems = DbContext.swap_position + .Where(p => p.SwapTradeId == TradeId + && p.IsInitial + && !p.Invalid + && p.PosiDirection == 0 + && p.PositionType == 0 + && string.IsNullOrEmpty(p.UnderlyingCode) + && p.InterestMode == 9) + .Select(p => new + { + p.id, + p.InterestMode, + p.InterestDirection + }) + .ToList(); + +if (!interestItems.Any()) + throw new Exception("同次录入的普通利息端记录不存在"); + +var directionItems = floatItems + .SelectMany(floatItem => interestItems.Select(interestItem => new + { + FloatPositionId = floatItem.id, + floatItem.PosiDirection, + floatItem.PositionType, + PositionTypeName = floatItem.PositionType == 1 ? "多头" : floatItem.PositionType == 2 ? "空头" : "未知:" + floatItem.PositionType, + InterestPositionId = interestItem.id, + interestItem.InterestMode, + interestItem.InterestDirection, + InterestDirectionName = interestItem.InterestDirection == 1 ? "收取" : interestItem.InterestDirection == 2 ? "支付" : "未知:" + interestItem.InterestDirection, + IsMismatch = !((interestItem.InterestDirection == 1 && floatItem.PositionType == 2) || (interestItem.InterestDirection == 2 && floatItem.PositionType == 1)) + })) + .OrderByDescending(p => p.IsMismatch) + .ThenBy(p => p.FloatPositionId) + .ThenBy(p => p.InterestPositionId) + .ToList(); + +var mismatchItems = directionItems + .Where(p => p.IsMismatch) + .ToList(); + +var detailItems = directionItems + .Select(p => new RiskVariableValueDetailItem( + p.IsMismatch ? 1m : 0m, + $"浮动端记录ID为{p.FloatPositionId},PosiDirection为{p.PosiDirection},浮动端为{p.PositionTypeName};普通利息端记录ID为{p.InterestPositionId},InterestMode为{p.InterestMode},利息端为{p.InterestDirectionName},{(p.IsMismatch ? "不符合允许场景" : "符合允许场景")}")) + .ToList(); + +if (mismatchItems.Any()) +{ + var mismatchMessage = string.Join(";", mismatchItems + .Select(p => $"浮动端记录ID为{p.FloatPositionId},浮动端为{p.PositionTypeName},普通利息端记录ID为{p.InterestPositionId},利息端为{p.InterestDirectionName}")); + + return new RiskVariableValueDetail( + 1m, + $"交易ID {TradeId} 存在账户授权收支方向不匹配组合:{mismatchMessage}。只允许利息端收取且浮动端空头,或利息端支付且浮动端多头", + detailItems); +} + +return new RiskVariableValueDetail( + 0m, + $"交易ID {TradeId} 的账户授权收支方向均符合允许场景:浮动端共{floatItems.Count}条,普通利息端共{interestItems.Count}条,已逐条组合检查;只允许利息端收取且浮动端空头,或利息端支付且浮动端多头", + detailItems); +``` + ```sql SET @TradeId = 3001699; SELECT - sp.id AS SwapPositionId, - sp.SwapTradeId, - sp.IsInitial, - sp.Invalid, - sp.PosiDirection, - sp.InterestDirection, - CASE sp.InterestDirection + fp.id AS FloatPositionId, + ip.id AS InterestPositionId, + fp.SwapTradeId, + fp.IsInitial, + fp.Invalid, + fp.PosiDirection AS FloatPosiDirection, + fp.PositionType AS FloatPositionType, + CASE fp.PositionType + WHEN 1 THEN '多头' + WHEN 2 THEN '空头' + ELSE '未知' + END AS FloatPositionTypeText, + ip.PosiDirection AS InterestPosiDirection, + ip.PositionType AS InterestPositionType, + ip.InterestMode, + ip.InterestDirection, + CASE ip.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 + WHEN (ip.InterestDirection = 1 AND fp.PositionType = 2) + OR (ip.InterestDirection = 2 AND fp.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; +FROM swap_position fp +INNER JOIN swap_position ip + ON ip.SwapTradeId = fp.SwapTradeId + AND ip.IsInitial = fp.IsInitial + AND ip.Invalid = fp.Invalid + AND ip.PosiDirection = 0 + AND ip.PositionType = 0 + AND (ip.UnderlyingCode IS NULL OR ip.UnderlyingCode = '') + AND ip.InterestMode = 9 +WHERE fp.SwapTradeId = @TradeId + AND fp.IsInitial = 1 + AND fp.Invalid = 0 + AND fp.PosiDirection > 0 +ORDER BY + IsDirectionMismatch DESC, + fp.id, + ip.id; ```