diff --git a/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiler.cs b/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiler.cs
index e70e7648..6d48174c 100644
--- a/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiler.cs
+++ b/YLErpDAL/Modules/RiskEngine/Compile/RuleCompiler.cs
@@ -1,23 +1,32 @@
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
-using Newtonsoft.Json.Linq;
+using Newtonsoft.Json;
using System;
-using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
+using YLErp.BLL;
+using YLErp.QdpModule;
namespace YLErp.Modules.RiskEngine
{
///
/// Roslyn 脚本全局变量容器
- /// CSharpScript 执行时通过此对象传递 DataMap
///
public class ScriptGlobals
{
///
- /// 数据字典:key=表名/前缀,value=对应数据对象
+ /// 当前交易ID
///
- public Dictionary DataMap { get; set; }
+ public int TradeId { get; set; }
+
+ ///
+ /// 当前触发时点
+ ///
+ public string TriggerPoint { get; set; }
+
+ ///
+ /// 数据库上下文,供规则公式直接查询数据库
+ ///
+ public YLContext DbContext { get; set; }
}
///
@@ -32,29 +41,12 @@ namespace YLErp.Modules.RiskEngine
/// - 可调试:生成的脚本代码可直接阅读和理解
///
/// 示例:
- /// RuleExpr: Convert.ToDecimal(((YLErp.DBModels.trade)DataMap["trade"]).StockEqvNotional) > 100000000m
+ /// RuleExpr: Convert.ToDecimal(DbContext.trade.First(t => t.id == TradeId).StockEqvNotional) > 100000000m
/// 编译结果:Func<RiskContext, bool>(内部通过 Roslyn 编译缓存)
///
public static class RuleCompiler
{
- ///
- /// 变量编码 → 类型全名的映射
- /// 用于 Roslyn 脚本中的类型强转,如 ((YLErp.DBModels.trade)DataMap["trade"])
- ///
static IYcLogger _logger = LogFactory.GetLogger("RuleCompiler");
- private static readonly Dictionary VariableTypeMap = new Dictionary(StringComparer.OrdinalIgnoreCase)
- {
- ["trade"] = "YLErp.DBModels.trade",
- ["swap_position"] = "YLErp.DBModels.swap_position",
- ["client"] = "YLErp.DBModels.client",
- ["credit"] = "YLErp.DBModels.credit",
- ["client_marginrate"] = "YLErp.DBModels.client_marginrate",
- ["market"] = "YLErp.DBModels.market",
- ["underlying_manager"] = "YLErp.DBModels.underlying_manager",
- ["sys"] = "YLErp.DBModels.sys",
- ["eod_swap_position"] = "YLErp.DBModels.eod_swap_position",
- ["realtime_trade_risk"] = "YLErp.DBModels.realtime_trade_risk",
- };
///
/// 校验并编译规则表达式。
@@ -114,7 +106,6 @@ namespace YLErp.Modules.RiskEngine
{
_logger.Error($"规则校验失败 - RuleId: {rule.Id}, Error: {result.ErrorMessage}");
rule.CompiledScript = null;
- RuleCompiledCache.Remove(rule.Id.ToString());
return RuleCompileResult.Fail($"规则[{rule.Id}]编译失败:{result.ErrorMessage}");
}
@@ -135,9 +126,14 @@ namespace YLErp.Modules.RiskEngine
var options = ScriptOptions.Default
.WithReferences(
typeof(RiskContext).Assembly,
- typeof(YLErp.DBModels.trade).Assembly
+ typeof(YLContext).Assembly,
+ typeof(YLErp.DBModels.trade).Assembly,
+ typeof(QdpCalendarHelper).Assembly,
+ typeof(JsonConvert).Assembly,
+ typeof(Microsoft.EntityFrameworkCore.DbContext).Assembly,
+ typeof(Queryable).Assembly
)
- .WithImports("System");
+ .WithImports("System", "System.Linq", "Newtonsoft.Json", "YLErp.DBModels", "YLErp.QdpModule");
// 创建脚本(尚未执行,仅编译)
var script = CSharpScript.Create(scriptCode, options, globalsType: typeof(ScriptGlobals));
@@ -162,7 +158,12 @@ namespace YLErp.Modules.RiskEngine
{
try
{
- var globals = new ScriptGlobals { DataMap = ctx.DataMap };
+ var globals = new ScriptGlobals
+ {
+ TradeId = ctx.TradeId,
+ TriggerPoint = ctx.TriggerPoint,
+ DbContext = ctx.DbContext
+ };
return runner(globals).GetAwaiter().GetResult();
}
catch (Exception ex)
diff --git a/YLErpDAL/Modules/RiskEngine/RiskContext.cs b/YLErpDAL/Modules/RiskEngine/RiskContext.cs
index 7e9e550e..9686f980 100644
--- a/YLErpDAL/Modules/RiskEngine/RiskContext.cs
+++ b/YLErpDAL/Modules/RiskEngine/RiskContext.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using YLErp.BLL;
namespace YLErp.Modules.RiskEngine
{
@@ -18,8 +18,8 @@ namespace YLErp.Modules.RiskEngine
public string TriggerPoint { get; set; }
///
- /// 数据字典:key=表名/前缀,value=对应数据对象
+ /// 数据库上下文,供规则公式直接查询数据库
///
- public Dictionary DataMap { get; set; } = new Dictionary();
+ public YLContext DbContext { get; set; }
}
}
diff --git a/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs b/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs
index f48bcc78..efd8b91d 100644
--- a/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs
+++ b/YLErpDAL/Modules/RiskEngine/RiskEngineService.cs
@@ -22,9 +22,9 @@ using YLErp.Model;
- RiskRuleApplication(规则应用):与规则分离,承载启用状态、控制策略、触发时点、
适用范围(全局 / 账户 / 客户 / 标的类型 / 合约类型)。
- RuleExpr:当前唯一主编译入口。要求内容是 Roslyn 可直接执行的 C# bool 表达式,
- 例如直接访问 DataMap["trade"] 后做数值或日期比较。
+ 例如通过 DbContext 和 TradeId 查询数据库后做数值或日期比较。
- ConditionJson:当前已在规则定义中保留,但不参与主执行链路。
- - RiskContext:一次风控检查的数据上下文,包含 TradeId、TriggerPoint 和 DataMap。
+ - RiskContext:一次风控检查的数据上下文,包含 TradeId、TriggerPoint 和 DbContext。
- RuleCompiledCache:进程内编译结果缓存,按规则 Id 缓存 Func。
当前编译流程:
@@ -69,12 +69,12 @@ using YLErp.Model;
- RuleCompiledCache.cs:编译结果缓存
当前集成点:
- - QuotaMonitorService.cs:构造 RiskContext,并将 trade 对象放入 DataMap["trade"]
+ - QuotaMonitorService.cs:构造 RiskContext,并传入当前 TradeId
【当前进度】(截至 2026-06-24)
✅ 已完成:
1. RiskEngine 第一版执行链路已跑通:QuotaMonitorService -> RiskEngineService -> RuleCompiler
- 2. 风控上下文通过 DataMap 传入 trade 对象,支持规则脚本直接访问交易字段
+ 2. 风控上下文提供 DbContext 和 TradeId,支持规则脚本直接查询数据库
3. RuleExpr 直编译方案已接入,支持数值比较和日期比较
4. Application 通用维度匹配已支持:全局 / 账户 / 客户 / 标的类型 / 合约类型
5. 维度组合逻辑已按文档确认:同维度 OR,不同维度 AND
@@ -102,7 +102,7 @@ using YLErp.Model;
【注意事项】
1. 当前 RuleExpr 必须是 Roslyn 最终可执行的 C# bool 表达式,不是业务语义短句
- 2. DataMap 中的值由宿主业务代码准备,编译器与执行器本身不负责查库补数
+ 2. RuleExpr 可直接使用 DbContext 查询数据库,当前交易通过 TradeId 定位
3. 当前缓存的是 Func,这是规则判断函数,不是事件处理器
4. 业务可预期失败优先走结果返回,不要把高频校验失败都设计成异常
================================================================================
@@ -164,16 +164,12 @@ namespace YLErp.Modules.RiskEngine
//非活跃的rule不编译
if (rule.Status != RiskRuleStatus.Active)
{
- _logger.Info($"[风控引擎] 规则非活跃,跳过预编译 - RuleId: {rule.Id}, Status: {rule.Status}");
- continue;
- }
- //已存在编译缓存的规则直接跳过,避免重复编译
- if (RuleCompiledCache.TryGet(ruleId, out _))
- {
- _logger.Info($"[风控引擎] 规则已存在编译缓存,跳过预编译 - RuleId: {rule.Id}");
+ RuleCompiledCache.Remove(rule.Id.ToString());
+ _logger.Info($"[风控引擎] 规则非活跃,已移除预编译缓存 - RuleId: {rule.Id}, Status: {rule.Status}");
continue;
}
+
var compileResult = RuleCompiler.ValidateAndCompileRule(rule);
if (compileResult.Success)
{
@@ -193,12 +189,12 @@ namespace YLErp.Modules.RiskEngine
}
///
- /// 刷新缓存:清空已编译委托与内存缓存后重新预加载。
+ /// 刷新缓存:刷新规则与应用内存缓存,预编译成功时覆盖旧编译缓存。
/// 规则/应用配置更新后调用。
///
public void RefreshCache()
{
- _logger.Info("[风控引擎] RefreshCache 被调用 - 清空缓存并重新预加载");
+ _logger.Info("[风控引擎] RefreshCache 被调用 - 刷新规则与应用缓存并重新预加载");
lock (_cacheLock)
{
_cachedRules = null;
@@ -210,7 +206,7 @@ namespace YLErp.Modules.RiskEngine
}
///
- /// 刷新单条规则缓存:重新加载规则与应用列表,只编译指定规则。
+ /// 刷新单条规则缓存:重新加载规则列表,只编译指定规则。
///
public RuleCompileResult RefreshOneRuleCache(long ruleId)
{
@@ -314,6 +310,10 @@ namespace YLErp.Modules.RiskEngine
try
{
_logger.Info($"[风控引擎] EvaluateRisk 开始 - TradeId: {context?.TradeId}, TriggerPoint: {triggerPoint}");
+ if (context != null && context.DbContext == null)
+ {
+ context.DbContext = DbContext;
+ }
// ============================================================
// Step 1: 从内存缓存读取规则定义和规则应用(启动时已预热)
@@ -343,8 +343,8 @@ namespace YLErp.Modules.RiskEngine
// 2. 同一维度内多选按并集处理;
// 3. 不同维度之间按交集处理;
// 4. 某维度留空表示该维度不限制。
- var trade = context?.DataMap != null && context.DataMap.ContainsKey("trade")
- ? context.DataMap["trade"] as YLErp.DBModels.trade
+ var trade = context != null && context.TradeId > 0
+ ? DbContext.trade.FirstOrDefault(t => t.id == context.TradeId)
: null;
var matchedApplications = triggerMatchedApps
@@ -544,8 +544,24 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000001,
// RuleName = "挂钩标的集中度校验(本地)",
- // RuleText = "取值字段:分子取 QuotaMonitorService 已注入 DataMap[same_underlying_total_notional],该值按存续交易口径汇总同一标的 StockEqvNotional;分母取 DataMap[underlying_manager].IssueSize,债券标的场景下该值由 underlying_manager.ExJson 反序列化回填,含义按当前业务测试口径使用发行量(亿)。为什么这么取:当前上下文已经稳定注入了这两个值,且与集中度规则最接近正式口径。计算逻辑:同一标的总名义本金 ÷ 发行量 × 100%,发行量需先乘 100000000 还原为元,结果大于 30% 时触发审批。",
- // RuleExpr = "DataMap.ContainsKey("same_underlying_total_notional") && DataMap["same_underlying_total_notional"] != null && DataMap.ContainsKey("underlying_manager") && DataMap["underlying_manager"] != null && ((YLErp.DBModels.underlying_manager)DataMap["underlying_manager"]).IssueSize.HasValue && ((YLErp.DBModels.underlying_manager)DataMap["underlying_manager"]).IssueSize.Value > 0 && Convert.ToDecimal(DataMap["same_underlying_total_notional"]) / (((YLErp.DBModels.underlying_manager)DataMap["underlying_manager"]).IssueSize.Value * 100000000m) * 100m > 30m",
+ // 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
+ //});
+
+ //rules.Add(new RiskRule
+ //{
+ // Id = 1000003,
+ // RuleName = "名义本金超阈值(本地)",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 StockEqvNotional,对应 trade 表名义本金字段。计算逻辑:StockEqvNotional 大于 100000000 时触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).StockEqvNotional > 100000000",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -560,8 +576,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000004,
// RuleName = "保证金支付比例超阈值(本地)",
- // RuleText = "取值字段:直接取 DataMap[trade].MarginRate,对应 trade 表保证金率字段。为什么这么取:该字段已经在当前上下文稳定注入,且 seed 规则 4 的判断核心也是保证金比例。计算逻辑:本地测试按数值型比例直接比较,MarginRate 大于 0.5 视为超过 50%,触发审批。",
- // RuleExpr = "DataMap.ContainsKey("trade") && DataMap["trade"] != null && ((YLErp.DBModels.trade)DataMap["trade"]).MarginRate > 0.5",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 MarginRate,对应 trade 表保证金率字段。计算逻辑:本地测试按数值型比例直接比较,MarginRate 大于 0.5 视为超过 50%,触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).MarginRate > 0.5",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -576,8 +592,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000005,
// RuleName = "保证金利率偏离(本地)",
- // RuleText = "取值字段:正式口径应取收益互换扩展数据中的保证金利率字段,但当前 RiskContext 未注入 trade_swap,因此本地测试先取 DataMap[trade].MarginRate 近似代替。为什么这么取:当前上下文只有 trade 可直接取值,先保证规则链路可验证。计算逻辑:若近似保证金利率不在 2% 到 5% 区间内,即小于 0.02 或大于 0.05,则触发审批。",
- // RuleExpr = "DataMap.ContainsKey("trade") && DataMap["trade"] != null && ((((YLErp.DBModels.trade)DataMap["trade"]).MarginRate < 0.02) || (((YLErp.DBModels.trade)DataMap["trade"]).MarginRate > 0.05))",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 MarginRate,作为保证金利率本地测试字段。计算逻辑:若 MarginRate 小于 0.02 或大于 0.05,则触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).MarginRate < 0.02 || DbContext.trade.First(t => t.id == TradeId).MarginRate > 0.05",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -592,8 +608,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000006,
// RuleName = "保证金收取比例低于最低标准(本地)",
- // RuleText = "取值字段:正式口径应取配置项 MinMarginRate 或客户/品种最低保证金率,当前上下文未注入配置对象,因此本地测试仍取 DataMap[trade].MarginRate 做比较。为什么这么取:trade.MarginRate 是当前唯一稳定可得且能反映保证金比例的字段。计算逻辑:先以 20% 作为本地测试最低标准,MarginRate 小于 0.2 时触发审批,后续接入正式配置后再替换阈值来源。",
- // RuleExpr = "DataMap.ContainsKey("trade") && DataMap["trade"] != null && ((YLErp.DBModels.trade)DataMap["trade"]).MarginRate < 0.2",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 MarginRate 做本地测试比较。计算逻辑:先以 20% 作为本地测试最低标准,MarginRate 小于 0.2 时触发审批,后续接入正式配置后再替换阈值来源。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).MarginRate < 0.2",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -608,8 +624,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000007,
// RuleName = "起息日早于当前日期(本地)",
- // RuleText = "取值字段:取 DataMap[trade].StartDate,对应 trade 表开始日。为什么这么取:seed 规则 7 直接使用 StartDate 与当前日期比较,当前上下文也已注入 trade。计算逻辑:StartDate 有值且日期早于系统当天 DateTime.Today 时触发审批。",
- // RuleExpr = "DataMap.ContainsKey("trade") && DataMap["trade"] != null && ((YLErp.DBModels.trade)DataMap["trade"]).StartDate.HasValue && ((YLErp.DBModels.trade)DataMap["trade"]).StartDate.Value.Date < DateTime.Today",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 StartDate,对应 trade 表开始日。计算逻辑: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,
@@ -624,8 +640,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000008,
// RuleName = "支付日为银行间交易日(本地)",
- // RuleText = "取值字段:取 DataMap[trade].SettlementDate,对应 trade 表结算日期。为什么这么取:当前代码中支付相关日期可直接从 trade 取得,且项目已有 QdpCalendarHelper.GetNonHolidayDefore 可用于交易日校验。计算逻辑:若 SettlementDate 有值,且向前修正到最近交易日后的结果不等于原日期,则说明原日期不是银行间交易日,触发审批。",
- // RuleExpr = "DataMap.ContainsKey("trade") && DataMap["trade"] != null && ((YLErp.DBModels.trade)DataMap["trade"]).SettlementDate.HasValue && QdpCalendarHelper.GetNonHolidayDefore(((YLErp.DBModels.trade)DataMap["trade"]).SettlementDate.Value.Date) != ((YLErp.DBModels.trade)DataMap["trade"]).SettlementDate.Value.Date",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 SettlementDate,对应 trade 表结算日期;调用 QdpCalendarHelper.GetNonHolidayDefore 做交易日校验。计算逻辑:若 SettlementDate 有值,且向前修正到最近交易日后的结果不等于原日期,则说明原日期不是银行间交易日,触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).SettlementDate.HasValue && QdpCalendarHelper.GetNonHolidayDefore(DbContext.trade.First(t => t.id == TradeId).SettlementDate.Value.Date) != DbContext.trade.First(t => t.id == TradeId).SettlementDate.Value.Date",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -640,8 +656,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000009,
// RuleName = "到期日为银行间交易日(本地)",
- // RuleText = "取值字段:取 DataMap[trade].ExerciseDate,对应当前交易里更接近业务到期/行权日的字段。为什么这么取:TradeBase 中 MaturityDate 注释已提示容易与 ExerciseDate 混淆,当前测试按交易实际到期处理字段 ExerciseDate 落地,避免先取错口径。计算逻辑:ExerciseDate 有值且向前修正到最近交易日后的结果不等于原日期时,视为不是银行间交易日,触发审批。",
- // RuleExpr = "DataMap.ContainsKey("trade") && DataMap["trade"] != null && ((YLErp.DBModels.trade)DataMap["trade"]).ExerciseDate.HasValue && QdpCalendarHelper.GetNonHolidayDefore(((YLErp.DBModels.trade)DataMap["trade"]).ExerciseDate.Value.Date) != ((YLErp.DBModels.trade)DataMap["trade"]).ExerciseDate.Value.Date",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 ExerciseDate,对应当前交易里更接近业务到期/行权日的字段;调用 QdpCalendarHelper.GetNonHolidayDefore 做交易日校验。计算逻辑:ExerciseDate 有值且向前修正到最近交易日后的结果不等于原日期时,视为不是银行间交易日,触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).ExerciseDate.HasValue && QdpCalendarHelper.GetNonHolidayDefore(DbContext.trade.First(t => t.id == TradeId).ExerciseDate.Value.Date) != DbContext.trade.First(t => t.id == TradeId).ExerciseDate.Value.Date",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -656,8 +672,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000010,
// RuleName = "平仓日为银行间交易日(本地)",
- // RuleText = "取值字段:取 DataMap[trade].UnWindDate,对应 trade 表平仓日。为什么这么取:当前上下文已注入该字段,且 seed 规则中的平仓日判断在本地最接近该口径。计算逻辑:UnWindDate 有值且向前修正到最近交易日后的结果不等于原日期时,视为不是银行间交易日,触发审批。",
- // RuleExpr = "DataMap.ContainsKey("trade") && DataMap["trade"] != null && ((YLErp.DBModels.trade)DataMap["trade"]).UnWindDate.HasValue && QdpCalendarHelper.GetNonHolidayDefore(((YLErp.DBModels.trade)DataMap["trade"]).UnWindDate.Value.Date) != ((YLErp.DBModels.trade)DataMap["trade"]).UnWindDate.Value.Date",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 UnWindDate,对应 trade 表平仓日;调用 QdpCalendarHelper.GetNonHolidayDefore 做交易日校验。计算逻辑:UnWindDate 有值且向前修正到最近交易日后的结果不等于原日期时,视为不是银行间交易日,触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).UnWindDate.HasValue && QdpCalendarHelper.GetNonHolidayDefore(DbContext.trade.First(t => t.id == TradeId).UnWindDate.Value.Date) != DbContext.trade.First(t => t.id == TradeId).UnWindDate.Value.Date",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -672,8 +688,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000011,
// RuleName = "合约期限超阈值(本地)",
- // RuleText = "取值字段:取 DataMap[trade].StartDate 和 DataMap[trade].ExerciseDate。为什么这么取:seed 规则 11 本质是计算合约剩余天数,当前 trade 中最稳定可得且最接近交易起止区间的就是开始日和到期/行权日。计算逻辑:当 StartDate 和 ExerciseDate 都有值时,用 ExerciseDate.Date 减 StartDate.Date 的总天数,若大于 365 天则触发审批。",
- // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).StartDate.HasValue && ((YLErp.DBModels.trade)DataMap[\"trade\"]).ExerciseDate.HasValue && (((YLErp.DBModels.trade)DataMap[\"trade\"]).ExerciseDate.Value.Date - ((YLErp.DBModels.trade)DataMap[\"trade\"]).StartDate.Value.Date).TotalDays > 365d",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 StartDate 和 ExerciseDate。计算逻辑:当 StartDate 和 ExerciseDate 都有值时,用 ExerciseDate.Date 减 StartDate.Date 的总天数,若大于 365 天则触发审批。",
+ // 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).TotalDays > 365d",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -688,8 +704,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000012,
// RuleName = "债券类净价偏离(本地)",
- // RuleText = "取值字段:当前上下文未注入债券估值净价 market.CBValuationPrice,因此本地测试先取 DataMap[trade].SpotPrice 作为可直接获得的价格字段。为什么这么取:债券类正式估值未接入前,需要用交易上已有价格先验证偏离类规则链路。计算逻辑:若 SpotPrice 有值且不为 0,则按 ABS(SpotPrice-100)/100×100% 计算相对面值 100 的偏离率,偏离率大于 5% 时触发审批。",
- // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.HasValue && Math.Abs(((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.Value - 100d) / 100d * 100d > 5d",
+ // RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 取 PosiNetNoFeePrice,对应债券类标的期初交割净价,库内为 1 左右原值;通过 DbContext.china_bond_valuation 按当前交易标的和交易日前日期取上一收盘日 net_price,库内为 100 左右报价。计算逻辑:按 ABS(PosiNetNoFeePrice×100-net_price) 计算绝对价差,价差大于 5 元时触发审批。",
+ // RuleExpr = "Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId).PosiNetNoFeePrice.Value * 100m - DbContext.china_bond_valuation.Where(v => v.bond_id == DbContext.trade.First(t => t.id == TradeId).UnderlyingCode && v.valuation_date < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderByDescending(v => v.credibility).ThenByDescending(v => v.valuation_date).First().net_price.Value) > 5m",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -704,8 +720,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000013,
// RuleName = "债券类收益率偏离(本地)",
- // RuleText = "取值字段:正式口径应比较 DataMap[trade].InitYtm 与市场估值收益率 market.CBValuationYtm,但当前未注入 market,因此本地测试直接取 trade.InitYtm。为什么这么取:InitYtm 是 trade 上已有且与收益率偏离最接近的字段。计算逻辑:先以 2.5% 作为本地测试基准收益率,若 InitYtm 有值且 ABS(InitYtm-0.025)/0.025×100% 大于 5%,则触发审批。",
- // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).InitYtm.HasValue && Math.Abs((((YLErp.DBModels.trade)DataMap[\"trade\"]).InitYtm.Value - 0.025d) / 0.025d) * 100d > 5d",
+ // RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 取 InitYtm,对应债券类标的期初成交收益率,库内为原值;通过 DbContext.china_bond_valuation 按当前交易标的和交易日前日期取上一收盘日 yield,库内为 1.5 到 2.2 左右百分数。计算逻辑:按 ABS(InitYtm×100-yield) 计算收益率绝对差,差值大于 1 时触发审批。",
+ // RuleExpr = "Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId).InitYtm.Value * 100m - DbContext.china_bond_valuation.Where(v => v.bond_id == DbContext.trade.First(t => t.id == TradeId).UnderlyingCode && v.valuation_date < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderByDescending(v => v.credibility).ThenByDescending(v => v.valuation_date).First().yield.Value) > 1m",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -720,8 +736,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000014,
// RuleName = "非债券类价格偏离(本地)",
- // RuleText = "取值字段:正式口径应比较 trade.TradePrice 与市场参考价 market.ReferencePrice,当前未注入 market,因此本地测试直接取 trade.TradePrice 与 trade.SpotPrice 互相比对。为什么这么取:这两个字段都来自 trade,且能够表达成交价相对现价的偏离。计算逻辑:当 TradePrice 和 SpotPrice 都有值且 SpotPrice 不为 0 时,按 ABS(TradePrice/SpotPrice-1)×100% 计算偏离率,大于 5% 时触发审批。",
- // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).TradePrice.HasValue && ((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.HasValue && ((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.Value != 0 && Math.Abs((((YLErp.DBModels.trade)DataMap[\"trade\"]).TradePrice.Value / ((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.Value) - 1d) * 100d > 5d",
+ // RuleText = "取值字段:通过 DbContext.swap_position 按 TradeId 取 PosiGrossPrice,对应普通收益互换页面填写的期初标的交割全价%,库内为 1 左右原值;通过 DbContext.eod_stock_price / eod_commodity_future_price 按当前交易标的和交易日前日期取上一日收盘价 ClosePrice。计算逻辑:按 ABS(PosiGrossPrice×100-ClosePrice) 计算绝对价差,价差大于 5 时触发审批。",
+ // RuleExpr = "Math.Abs(DbContext.swap_position.First(p => p.SwapTradeId == TradeId).PosiGrossPrice * 100m - Convert.ToDecimal((DbContext.eod_stock_price.Where(e => e.UnderlyingCode == DbContext.trade.First(t => t.id == TradeId).UnderlyingCode && e.ValueDate < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderByDescending(e => e.ValueDate).Select(e => (double?)e.ClosePrice).FirstOrDefault() ?? DbContext.eod_commodity_future_price.Where(e => e.UnderlyingCode == DbContext.trade.First(t => t.id == TradeId).UnderlyingCode && e.ValueDate < DbContext.trade.First(t => t.id == TradeId).TradeDate.Value.Date).OrderByDescending(e => e.ValueDate).Select(e => (double?)e.ClosePrice).FirstOrDefault()).Value)) > 5m",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -736,8 +752,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000015,
// RuleName = "单一交易对手累计标的数量超阈值(本地)",
- // RuleText = "取值字段:取 DataMap[trade].ClientId 作为交易对手标识,并在表达式里直接查询 trade 表的 UnderlyingId。为什么这么取:当前上下文尚未预先注入该聚合值,但 DbContext 在脚本环境可用,且项目已有存续口径可以复用。计算逻辑:按 ValidState 不等于 InValid、ParentTradeId 等于 0、TradeStatus 属于 NeedMarginTradeStatusList 或审批中 的存续口径,统计同一 ClientId 下去重后的 UnderlyingId 数量,超过 10 个时触发审批。",
- // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && DbContext.trade.Where(t => t.ValidState != \"InValid\" && t.ClientId == ((YLErp.DBModels.trade)DataMap[\"trade\"]).ClientId && t.ParentTradeId == 0 && (ConsTrade.NeedMarginTradeStatusList.Contains(t.TradeStatus) || t.TradeStatus == \"审批中\")).Select(t => t.UnderlyingId).Distinct().Count() > 10",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易对手 ClientId,再查询同一交易对手存续/审批中交易的去重标的数量。计算逻辑:同一交易对手累计标的数量超过 10 个时触发审批。",
+ // RuleExpr = "DbContext.trade.Where(t => t.ValidState != \"InValid\" && t.ClientId == DbContext.trade.First(x => x.id == TradeId).ClientId && t.ParentTradeId == 0 && (ConsTrade.NeedMarginTradeStatusList.Contains(t.TradeStatus) || t.TradeStatus == \"审批中\")).Select(t => t.UnderlyingId).Distinct().Count() > 10",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -752,8 +768,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000016,
// RuleName = "多头支付固定端利率偏离(本地)",
- // RuleText = "取值字段:正式口径应同时取 trade_swap 固定端方向、固定利率以及市场基准利率,当前未注入 trade_swap 和 market,因此本地测试先取 DataMap[trade].FixedRate,并结合 BuySell 判断多头方向。为什么这么取:FixedRate 和 BuySell 都已在 trade 上可取,能先验证方向类利率规则链路。计算逻辑:当 BuySell 表示多头且 FixedRate 有值时,先以 2.5% 作为本地测试基准,若 ABS(FixedRate-0.025)/0.025×100% 大于 5%,则触发审批。",
- // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).BuySell == \"Buy\" && ((YLErp.DBModels.trade)DataMap[\"trade\"]).FixedRate.HasValue && Math.Abs((((YLErp.DBModels.trade)DataMap[\"trade\"]).FixedRate.Value - 0.025d) / 0.025d) * 100d > 5d",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 FixedRate,并结合 BuySell 判断多头方向。计算逻辑:当 BuySell 表示多头且 FixedRate 有值时,先以 2.5% 作为本地测试基准,若 ABS(FixedRate-0.025)/0.025×100% 大于 5%,则触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).BuySell == \"Buy\" && DbContext.trade.First(t => t.id == TradeId).FixedRate.HasValue && Math.Abs((DbContext.trade.First(t => t.id == TradeId).FixedRate.Value - 0.025d) / 0.025d) * 100d > 5d",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -768,8 +784,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000017,
// RuleName = "空头利率减点借贷加权偏离(本地)",
- // RuleText = "取值字段:正式口径应取 trade_swap 空头端利率减点、借贷成本和加权基准,当前未注入这些对象,因此本地测试仍取 DataMap[trade].FixedRate,并结合 BuySell 判断空头方向。为什么这么取:当前 trade 上只有 FixedRate 可稳定表达利率数值,先用于验证空头分支规则。计算逻辑:当 BuySell 表示空头且 FixedRate 有值时,先以 2% 作为本地测试基准,若 ABS(FixedRate-0.02)/0.02×100% 大于 5%,则触发审批。",
- // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).BuySell == \"Sell\" && ((YLErp.DBModels.trade)DataMap[\"trade\"]).FixedRate.HasValue && Math.Abs((((YLErp.DBModels.trade)DataMap[\"trade\"]).FixedRate.Value - 0.02d) / 0.02d) * 100d > 5d",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 FixedRate,并结合 BuySell 判断空头方向。计算逻辑:当 BuySell 表示空头且 FixedRate 有值时,先以 2% 作为本地测试基准,若 ABS(FixedRate-0.02)/0.02×100% 大于 5%,则触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).BuySell == \"Sell\" && DbContext.trade.First(t => t.id == TradeId).FixedRate.HasValue && Math.Abs((DbContext.trade.First(t => t.id == TradeId).FixedRate.Value - 0.02d) / 0.02d) * 100d > 5d",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -784,8 +800,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000018,
// RuleName = "账户授权收支方向不匹配(本地)",
- // RuleText = "取值字段:正式口径应取账户授权配置中的收支方向和当前交易实际收支方向,当前上下文未注入账户授权对象,因此本地测试先用 trade.OpponentRole 与 trade.BuySell 做占位判断。为什么这么取:当前只有 trade 上的方向类字段可直接取得,先用于验证禁止类规则是否能命中。计算逻辑:当 OpponentRole 和 BuySell 都有值,且出现本地定义的不允许组合时触发禁止;当前测试口径先将 OpponentRole 为 Pay 且 BuySell 为 Buy 视为方向不匹配。",
- // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && !string.IsNullOrWhiteSpace(((YLErp.DBModels.trade)DataMap[\"trade\"]).OpponentRole) && !string.IsNullOrWhiteSpace(((YLErp.DBModels.trade)DataMap[\"trade\"]).BuySell) && ((YLErp.DBModels.trade)DataMap[\"trade\"]).OpponentRole == \"Pay\" && ((YLErp.DBModels.trade)DataMap[\"trade\"]).BuySell == \"Buy\"",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 OpponentRole 与 BuySell 做本地测试占位判断。计算逻辑:当 OpponentRole 和 BuySell 都有值,且 OpponentRole 为 Pay 且 BuySell 为 Buy 时视为方向不匹配,触发禁止。",
+ // RuleExpr = "!string.IsNullOrWhiteSpace(DbContext.trade.First(t => t.id == TradeId).OpponentRole) && !string.IsNullOrWhiteSpace(DbContext.trade.First(t => t.id == TradeId).BuySell) && DbContext.trade.First(t => t.id == TradeId).OpponentRole == \"Pay\" && DbContext.trade.First(t => t.id == TradeId).BuySell == \"Buy\"",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -800,8 +816,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000019,
// RuleName = "执行价偏离超阈值(本地)",
- // RuleText = "取值字段:取 DataMap[trade].Strike 和 DataMap[trade].SpotPrice,分别对应行权价与现价。为什么这么取:当前项目已有使用 Strike 与 SpotPrice 做偏离判断的场景,且这两个字段都已在 trade 上可直接获取。计算逻辑:当 Strike 和 SpotPrice 都有值且 SpotPrice 不为 0 时,按 ABS(Strike/SpotPrice-1)×100% 计算执行价相对现价的偏离率,大于 5% 时触发审批。",
- // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).Strike.HasValue && ((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.HasValue && ((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.Value != 0 && Math.Abs((((YLErp.DBModels.trade)DataMap[\"trade\"]).Strike.Value / ((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.Value) - 1d) * 100d > 5d",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 Strike 和 SpotPrice,分别对应行权价与现价。计算逻辑:当 Strike 和 SpotPrice 都有值且 SpotPrice 不为 0 时,按 ABS(Strike/SpotPrice-1)×100% 计算执行价相对现价的偏离率,大于 5% 时触发审批。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).Strike.HasValue && DbContext.trade.First(t => t.id == TradeId).SpotPrice.HasValue && DbContext.trade.First(t => t.id == TradeId).SpotPrice.Value != 0 && Math.Abs((DbContext.trade.First(t => t.id == TradeId).Strike.Value / DbContext.trade.First(t => t.id == TradeId).SpotPrice.Value) - 1d) * 100d > 5d",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
@@ -816,8 +832,8 @@ namespace YLErp.Modules.RiskEngine
//{
// Id = 1000021,
// RuleName = "接近/触发敲入敲出价(本地)",
- // RuleText = "取值字段:正式口径应取产品条款中的敲入价/敲出价以及现价,当前上下文未注入条款对象,因此本地测试先取 DataMap[trade].Strike 和 DataMap[trade].SpotPrice 近似模拟触发价与现价。为什么这么取:这两个字段当前即可直接取得,适合先验证提示类规则链路。计算逻辑:当 Strike 和 SpotPrice 都有值且 Strike 不为 0 时,按 ABS(SpotPrice/Strike-1)×100% 计算两者距离,距离小于等于 2% 时视为接近触发价,给出提示。",
- // RuleExpr = "DataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).Strike.HasValue && ((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.HasValue && ((YLErp.DBModels.trade)DataMap[\"trade\"]).Strike.Value != 0 && Math.Abs((((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.Value / ((YLErp.DBModels.trade)DataMap[\"trade\"]).Strike.Value) - 1d) * 100d <= 2d",
+ // RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 Strike 和 SpotPrice,近似模拟触发价与现价。计算逻辑:当 Strike 和 SpotPrice 都有值且 Strike 不为 0 时,按 ABS(SpotPrice/Strike-1)×100% 计算两者距离,距离小于等于 2% 时视为接近触发价,给出提示。",
+ // RuleExpr = "DbContext.trade.First(t => t.id == TradeId).Strike.HasValue && DbContext.trade.First(t => t.id == TradeId).SpotPrice.HasValue && DbContext.trade.First(t => t.id == TradeId).Strike.Value != 0 && Math.Abs((DbContext.trade.First(t => t.id == TradeId).SpotPrice.Value / DbContext.trade.First(t => t.id == TradeId).Strike.Value) - 1d) * 100d <= 2d",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,
diff --git a/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs b/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs
index 41b62b1b..a0817047 100644
--- a/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs
+++ b/YLErpDAL/Modules/RiskModule/QuotaMonitorService.cs
@@ -5183,29 +5183,6 @@ namespace YLErp.Modules.RiskModule
TradeId = tradeId,
TriggerPoint = "BOOK_CONFIRM"
};
- riskContext.DataMap["trade"] = tradeObj;
- var sameUnderlyingTotalNotional = DbContext.trade
- .Where(t => t.ValidState != "InValid"
- && t.UnderlyingId == tradeObj.UnderlyingId
- && t.ParentTradeId == 0
- && (ConsTrade.NeedMarginTradeStatusList.Contains(t.TradeStatus) || t.TradeStatus == "审批中"))
- .Sum(t => (double?)t.StockEqvNotional) ?? 0d;
- riskContext.DataMap["same_underlying_total_notional"] = sameUnderlyingTotalNotional;
- var underlying = DataCacheProvider.GetUnderlyingDataSource().GetData(tradeObj.UnderlyingCode);
- if (underlying != null)
- {
- if (underlying.IsBond() && !string.IsNullOrEmpty(underlying.ExJson))
- {
- var bond = JsonHelper.Deserialize(underlying.ExJson);
- if (bond != null)
- {
- underlying.IssueSize = bond.IssueSize;
- underlying.UnderlyingFullName = bond.UnderlyingFullName;
- underlying.UnderlyingIssuer = bond.UnderlyingIssuer;
- }
- }
- riskContext.DataMap["underlying_manager"] = underlying;
- }
var riskResult = riskEngine.EvaluateRisk(riskContext, "BOOK_CONFIRM");
_logger.Info($"[风控引擎] 簿记交易确认 - TradeId: {tradeId}, Passed: {riskResult.Passed}, Blocked: {riskResult.Blocked}, NeedApproval: {riskResult.NeedApproval}, ShowTip: {riskResult.ShowTip}");