@@ -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<RiskContext, bool>。
当前编译流程:
@@ -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 传入 t rade 对象 ,支持规则脚本直接访问交易字段
2. 风控上下文提供 DbContext 和 T radeId ,支持规则脚本直接查询数据库
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<RiskContext, bool>,这是规则判断函数,不是事件处理器
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
}
/// <summary>
/// 刷新缓存:清空已编译委托与内存缓存后重新预加载 。
/// 刷新缓存:刷新规则与应用内存缓存,预编译成功时覆盖旧编译缓存 。
/// 规则/应用配置更新后调用。
/// </summary>
public void RefreshCache ( )
{
_logger . Info ( "[风控引擎] RefreshCache 被调用 - 清空 缓存并重新预加载" ) ;
_logger . Info ( "[风控引擎] RefreshCache 被调用 - 刷新规则与应用 缓存并重新预加载" ) ;
lock ( _cacheLock )
{
_cachedRules = null ;
@@ -210,7 +206,7 @@ namespace YLErp.Modules.RiskEngine
}
/// <summary>
/// 刷新单条规则缓存:重新加载规则与应用 列表,只编译指定规则。
/// 刷新单条规则缓存:重新加载规则列表,只编译指定规则。
/// </summary>
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.u nderlying_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< YLErp.DBModels.U nderlyingBond>(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["t rade"] ).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 == T radeId ).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["t rade"] ).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 == T radeId ).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["t rade"] ).SettlementDate.Value.Date) != ((YLErp.DBModels.trade)DataMap["t rade"] ).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 == T radeId ).SettlementDate.Value.Date) != DbContext.trade.First(t => t.id == T radeId ).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["t rade"] ).ExerciseDate.Value.Date) != ((YLErp.DBModels.trade)DataMap["t rade"] ).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 == T radeId ).ExerciseDate.Value.Date) != DbContext.trade.First(t => t.id == T radeId ).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["t rade"] ).UnWindDate.Value.Date) != ((YLErp.DBModels.trade)DataMap["t rade"] ).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 == T radeId ).UnWindDate.Value.Date) != DbContext.trade.First(t => t.id == T radeId ).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[\"t rade\"] ).ExerciseDate.HasValue && (((YLErp.DBModels.trade)DataMap[\"t rade\"] ).ExerciseDate.Value.Date - ((YLErp.DBModels.trade)DataMap[\"t rade\"] ).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 == T radeId ).ExerciseDate.HasValue && (DbContext.trade.First(t => t.id == T radeId ).ExerciseDate.Value.Date - DbContext.trade.First(t => t.id == T radeId ).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 = "D ataMap.ContainsKey(\"trade\") && DataMap[\"trade\"] != null && ((YLErp.DBModels.trade)DataMap[\"trade\"]).SpotPrice.HasValue && Math.Abs(((YLErp.DBModels.t rade) 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 = "M ath.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).T radeDate.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 = "D ataMap.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 = "M ath.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[\"t rade\"] ).TradePric e.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 == T radeId ).TradeDat e.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[\"t rade\"] ).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 == T radeId ).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[\"t rade\"] ).FixedRate.HasValue && Math.Abs((((YLErp.DBModels.trade)DataMap[\"t rade\"] ).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 == T radeId ).FixedRate.HasValue && Math.Abs((DbContext.trade.First(t => t.id == T radeId ).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[\"t rade\"] ).FixedRate.HasValue && Math.Abs((((YLErp.DBModels.trade)DataMap[\"t rade\"] ).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 == T radeId ).FixedRate.HasValue && Math.Abs((DbContext.trade.First(t => t.id == T radeId ).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[\"t rade\"] ).OpponentRole) && !string.IsNullOrWhiteSpace(((YLErp.DBModels.trade)DataMap[\"t rade\"] ).BuySell) && ((YLErp.DBModels.trade)DataMap[\"t rade\"] ).OpponentRole == \"Pay\" && ((YLErp.DBModels.trade)DataMap[\"t rade\"] ).BuySell == \"Buy\"",
// RuleText = "取值字段:通过 DbContext.trade 按 TradeId 取当前交易的 OpponentRole 与 BuySell 做本地测试 占位判断。计算逻辑:当 OpponentRole 和 BuySell 都有值,且 OpponentRole 为 Pay 且 BuySell 为 Buy 时 视为方向不匹配,触发禁止 。",
// RuleExpr = "!string.IsNullOrWhiteSpace(DbContext.trade.First(t => t.id == T radeId ).OpponentRole) && !string.IsNullOrWhiteSpace(DbContext.trade.First(t => t.id == T radeId ).BuySell) && DbContext.trade.First(t => t.id == T radeId ).OpponentRole == \"Pay\" && DbContext.trade.First(t => t.id == T radeId ).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[\"t rade\"] ).SpotPrice.HasValue && ((YLErp.DBModels.trade)DataMap[\"t rade\"] ).SpotPrice.Value != 0 && Math.Abs((((YLErp.DBModels.trade)DataMap[\"t rade\"] ).Strike.Value / ((YLErp.DBModels.trade)DataMap[\"t rade\"] ).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 == T radeId ).SpotPrice.HasValue && DbContext.trade.First(t => t.id == T radeId ).SpotPrice.Value != 0 && Math.Abs((DbContext.trade.First(t => t.id == T radeId ).Strike.Value / DbContext.trade.First(t => t.id == T radeId ).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[\"t rade\"] ).SpotPrice.HasValue && ((YLErp.DBModels.trade)DataMap[\"t rade\"] ).Strike.Value != 0 && Math.Abs((((YLErp.DBModels.trade)DataMap[\"t rade\"] ).SpotPrice.Value / ((YLErp.DBModels.trade)DataMap[\"t rade\"] ).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 == T radeId ).SpotPrice.HasValue && DbContext.trade.First(t => t.id == T radeId ).Strike.Value != 0 && Math.Abs((DbContext.trade.First(t => t.id == T radeId ).SpotPrice.Value / DbContext.trade.First(t => t.id == T radeId ).Strike.Value) - 1d) * 100d <= 2d",
// Version = 1,
// Status = RiskRuleStatus.Active,
// OptId = 0,