共享选股状态并合并风控扫描

This commit is contained in:
boris
2026-08-25 08:53:34 +08:00
parent 85cfdca14c
commit 5ff8ddca92
+198 -112
View File
@@ -952,7 +952,7 @@ pub struct PlatformExprStrategy {
stock_text_factors_required: bool,
stock_state_cache_date: RefCell<Option<NaiveDate>>,
stock_state_cache: RefCell<
AHashMap<(NaiveDate, NaiveDate, u32, Option<NaiveTime>, bool), StockExpressionState>,
AHashMap<(NaiveDate, NaiveDate, u32, Option<NaiveTime>, bool), Arc<StockExpressionState>>,
>,
}
@@ -3155,7 +3155,7 @@ impl PlatformExprStrategy {
ctx,
execution_date,
symbol,
&self.stock_state(ctx, execution_date, symbol)?,
self.stock_state(ctx, execution_date, symbol)?.as_ref(),
)?
.is_some()
{
@@ -3614,7 +3614,7 @@ impl PlatformExprStrategy {
ctx: &StrategyContext<'_>,
date: NaiveDate,
symbol: &str,
) -> Result<StockExpressionState, BacktestError> {
) -> Result<Arc<StockExpressionState>, BacktestError> {
self.stock_state_with_factor_date(ctx, date, date, symbol)
}
@@ -3624,7 +3624,7 @@ impl PlatformExprStrategy {
date: NaiveDate,
factor_date: NaiveDate,
symbol: &str,
) -> Result<StockExpressionState, BacktestError> {
) -> Result<Arc<StockExpressionState>, BacktestError> {
self.stock_state_with_factor_date_and_time(ctx, date, factor_date, symbol, None, true)
}
@@ -3634,7 +3634,7 @@ impl PlatformExprStrategy {
date: NaiveDate,
factor_date: NaiveDate,
symbol: &str,
) -> Result<StockExpressionState, BacktestError> {
) -> Result<Arc<StockExpressionState>, BacktestError> {
let use_intraday_quote = self.selection_quote_usage != StockFilterQuoteUsage::DailyOnly;
self.stock_state_with_factor_date_and_time(
ctx,
@@ -3681,7 +3681,7 @@ impl PlatformExprStrategy {
date: NaiveDate,
symbol: &str,
execution_time: Option<NaiveTime>,
) -> Result<StockExpressionState, BacktestError> {
) -> Result<Arc<StockExpressionState>, BacktestError> {
self.stock_state_with_factor_date_and_time(ctx, date, date, symbol, execution_time, true)
}
@@ -3745,7 +3745,7 @@ impl PlatformExprStrategy {
symbol: &str,
execution_time: Option<NaiveTime>,
use_intraday_quote: bool,
) -> Result<StockExpressionState, BacktestError> {
) -> Result<Arc<StockExpressionState>, BacktestError> {
let symbol_id = ctx.data.symbol_id(symbol).ok_or_else(|| {
BacktestError::Data(crate::data::DataSetError::MissingSnapshot {
kind: "symbol_index",
@@ -3768,7 +3768,7 @@ impl PlatformExprStrategy {
use_intraday_quote,
);
if let Some(state) = self.stock_state_cache.borrow().get(&cache_key) {
return Ok(state.clone());
return Ok(Arc::clone(state));
}
let market = ctx
@@ -3921,7 +3921,7 @@ impl PlatformExprStrategy {
BTreeMap::new()
};
let state = StockExpressionState {
let state = Arc::new(StockExpressionState {
symbol: symbol.to_string(),
symbol_id,
market_cap,
@@ -3997,10 +3997,10 @@ impl PlatformExprStrategy {
} else {
BTreeMap::new()
},
};
});
self.stock_state_cache
.borrow_mut()
.insert(cache_key, state.clone());
.insert(cache_key, Arc::clone(&state));
Ok(state)
}
@@ -7752,7 +7752,7 @@ impl PlatformExprStrategy {
ctx: &StrategyContext<'_>,
date: NaiveDate,
symbol: Option<&str>,
) -> Result<Option<StockExpressionState>, BacktestError> {
) -> Result<Option<Arc<StockExpressionState>>, BacktestError> {
let Some(symbol) = symbol else {
return Ok(None);
};
@@ -7802,7 +7802,7 @@ impl PlatformExprStrategy {
if !self.action_when_matches(
ctx,
day,
stock_state.as_ref(),
stock_state.as_deref(),
when_expr.as_deref(),
)? {
continue;
@@ -7817,7 +7817,7 @@ impl PlatformExprStrategy {
match kind {
PlatformExplicitOrderKind::Shares => {
let quantity =
self.eval_i32(ctx, amount_expr, day, stock_state.as_ref(), None)?;
self.eval_i32(ctx, amount_expr, day, stock_state.as_deref(), None)?;
if quantity == 0 {
continue;
}
@@ -7829,7 +7829,7 @@ impl PlatformExprStrategy {
}
PlatformExplicitOrderKind::LimitShares => {
let quantity =
self.eval_i32(ctx, amount_expr, day, stock_state.as_ref(), None)?;
self.eval_i32(ctx, amount_expr, day, stock_state.as_deref(), None)?;
if quantity == 0 {
continue;
}
@@ -7837,7 +7837,7 @@ impl PlatformExprStrategy {
ctx,
limit_price_expr.as_deref().unwrap_or_default(),
day,
stock_state.as_ref(),
stock_state.as_deref(),
None,
)?;
intents.push(OrderIntent::LimitShares {
@@ -7849,7 +7849,7 @@ impl PlatformExprStrategy {
}
PlatformExplicitOrderKind::Lots => {
let lots =
self.eval_i32(ctx, amount_expr, day, stock_state.as_ref(), None)?;
self.eval_i32(ctx, amount_expr, day, stock_state.as_deref(), None)?;
if lots == 0 {
continue;
}
@@ -7861,7 +7861,7 @@ impl PlatformExprStrategy {
}
PlatformExplicitOrderKind::LimitLots => {
let lots =
self.eval_i32(ctx, amount_expr, day, stock_state.as_ref(), None)?;
self.eval_i32(ctx, amount_expr, day, stock_state.as_deref(), None)?;
if lots == 0 {
continue;
}
@@ -7869,7 +7869,7 @@ impl PlatformExprStrategy {
ctx,
limit_price_expr.as_deref().unwrap_or_default(),
day,
stock_state.as_ref(),
stock_state.as_deref(),
None,
)?;
intents.push(OrderIntent::LimitLots {
@@ -7881,7 +7881,7 @@ impl PlatformExprStrategy {
}
PlatformExplicitOrderKind::TargetShares => {
let target_quantity =
self.eval_i32(ctx, amount_expr, day, stock_state.as_ref(), None)?;
self.eval_i32(ctx, amount_expr, day, stock_state.as_deref(), None)?;
intents.push(OrderIntent::TargetShares {
symbol: symbol.clone(),
target_quantity,
@@ -7890,12 +7890,12 @@ impl PlatformExprStrategy {
}
PlatformExplicitOrderKind::LimitTargetShares => {
let target_quantity =
self.eval_i32(ctx, amount_expr, day, stock_state.as_ref(), None)?;
self.eval_i32(ctx, amount_expr, day, stock_state.as_deref(), None)?;
let limit_price = self.eval_float(
ctx,
limit_price_expr.as_deref().unwrap_or_default(),
day,
stock_state.as_ref(),
stock_state.as_deref(),
None,
)?;
intents.push(OrderIntent::LimitTargetShares {
@@ -7906,8 +7906,13 @@ impl PlatformExprStrategy {
});
}
PlatformExplicitOrderKind::Value => {
let value =
self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?;
let value = self.eval_float(
ctx,
amount_expr,
day,
stock_state.as_deref(),
None,
)?;
if value.abs() <= f64::EPSILON {
continue;
}
@@ -7919,21 +7924,38 @@ impl PlatformExprStrategy {
}
PlatformExplicitOrderKind::VwapValue
| PlatformExplicitOrderKind::TwapValue => {
let value =
self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?;
let value = self.eval_float(
ctx,
amount_expr,
day,
stock_state.as_deref(),
None,
)?;
if value.abs() <= f64::EPSILON {
continue;
}
let start_time = start_time_expr
.as_deref()
.map(|expr| {
self.eval_time_expr(ctx, expr, day, stock_state.as_ref(), None)
self.eval_time_expr(
ctx,
expr,
day,
stock_state.as_deref(),
None,
)
})
.transpose()?;
let end_time = end_time_expr
.as_deref()
.map(|expr| {
self.eval_time_expr(ctx, expr, day, stock_state.as_ref(), None)
self.eval_time_expr(
ctx,
expr,
day,
stock_state.as_deref(),
None,
)
})
.transpose()?;
intents.push(OrderIntent::AlgoValue {
@@ -7950,8 +7972,13 @@ impl PlatformExprStrategy {
});
}
PlatformExplicitOrderKind::LimitValue => {
let value =
self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?;
let value = self.eval_float(
ctx,
amount_expr,
day,
stock_state.as_deref(),
None,
)?;
if value.abs() <= f64::EPSILON {
continue;
}
@@ -7959,7 +7986,7 @@ impl PlatformExprStrategy {
ctx,
limit_price_expr.as_deref().unwrap_or_default(),
day,
stock_state.as_ref(),
stock_state.as_deref(),
None,
)?;
intents.push(OrderIntent::LimitValue {
@@ -7970,8 +7997,13 @@ impl PlatformExprStrategy {
});
}
PlatformExplicitOrderKind::Percent => {
let percent =
self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?;
let percent = self.eval_float(
ctx,
amount_expr,
day,
stock_state.as_deref(),
None,
)?;
if percent.abs() <= f64::EPSILON {
continue;
}
@@ -7983,21 +8015,38 @@ impl PlatformExprStrategy {
}
PlatformExplicitOrderKind::VwapPercent
| PlatformExplicitOrderKind::TwapPercent => {
let percent =
self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?;
let percent = self.eval_float(
ctx,
amount_expr,
day,
stock_state.as_deref(),
None,
)?;
if percent.abs() <= f64::EPSILON {
continue;
}
let start_time = start_time_expr
.as_deref()
.map(|expr| {
self.eval_time_expr(ctx, expr, day, stock_state.as_ref(), None)
self.eval_time_expr(
ctx,
expr,
day,
stock_state.as_deref(),
None,
)
})
.transpose()?;
let end_time = end_time_expr
.as_deref()
.map(|expr| {
self.eval_time_expr(ctx, expr, day, stock_state.as_ref(), None)
self.eval_time_expr(
ctx,
expr,
day,
stock_state.as_deref(),
None,
)
})
.transpose()?;
intents.push(OrderIntent::AlgoPercent {
@@ -8014,8 +8063,13 @@ impl PlatformExprStrategy {
});
}
PlatformExplicitOrderKind::LimitPercent => {
let percent =
self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?;
let percent = self.eval_float(
ctx,
amount_expr,
day,
stock_state.as_deref(),
None,
)?;
if percent.abs() <= f64::EPSILON {
continue;
}
@@ -8023,7 +8077,7 @@ impl PlatformExprStrategy {
ctx,
limit_price_expr.as_deref().unwrap_or_default(),
day,
stock_state.as_ref(),
stock_state.as_deref(),
None,
)?;
intents.push(OrderIntent::LimitPercent {
@@ -8034,8 +8088,13 @@ impl PlatformExprStrategy {
});
}
PlatformExplicitOrderKind::TargetValue => {
let target_value =
self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?;
let target_value = self.eval_float(
ctx,
amount_expr,
day,
stock_state.as_deref(),
None,
)?;
intents.push(OrderIntent::TargetValue {
symbol: symbol.clone(),
target_value,
@@ -8043,13 +8102,18 @@ impl PlatformExprStrategy {
});
}
PlatformExplicitOrderKind::LimitTargetValue => {
let target_value =
self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?;
let target_value = self.eval_float(
ctx,
amount_expr,
day,
stock_state.as_deref(),
None,
)?;
let limit_price = self.eval_float(
ctx,
limit_price_expr.as_deref().unwrap_or_default(),
day,
stock_state.as_ref(),
stock_state.as_deref(),
None,
)?;
intents.push(OrderIntent::LimitTargetValue {
@@ -8060,8 +8124,13 @@ impl PlatformExprStrategy {
});
}
PlatformExplicitOrderKind::TargetPercent => {
let target_percent =
self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?;
let target_percent = self.eval_float(
ctx,
amount_expr,
day,
stock_state.as_deref(),
None,
)?;
intents.push(OrderIntent::TargetPercent {
symbol: symbol.clone(),
target_percent,
@@ -8069,13 +8138,18 @@ impl PlatformExprStrategy {
});
}
PlatformExplicitOrderKind::LimitTargetPercent => {
let target_percent =
self.eval_float(ctx, amount_expr, day, stock_state.as_ref(), None)?;
let target_percent = self.eval_float(
ctx,
amount_expr,
day,
stock_state.as_deref(),
None,
)?;
let limit_price = self.eval_float(
ctx,
limit_price_expr.as_deref().unwrap_or_default(),
day,
stock_state.as_ref(),
stock_state.as_deref(),
None,
)?;
intents.push(OrderIntent::LimitTargetPercent {
@@ -8098,7 +8172,7 @@ impl PlatformExprStrategy {
if !self.action_when_matches(
ctx,
day,
stock_state.as_ref(),
stock_state.as_deref(),
when_expr.as_deref(),
)? {
continue;
@@ -8109,7 +8183,7 @@ impl PlatformExprStrategy {
ctx,
order_id_expr.as_deref().unwrap_or_default(),
day,
stock_state.as_ref(),
stock_state.as_deref(),
None,
)?;
if order_id == 0 {
@@ -8503,6 +8577,7 @@ impl PlatformExprStrategy {
}
}
#[cfg(test)]
fn selectable_universe_on(
&self,
ctx: &StrategyContext<'_>,
@@ -8524,14 +8599,30 @@ impl PlatformExprStrategy {
factor_date: NaiveDate,
selection_risk_deferral: SelectionRiskDeferral,
) -> Vec<EligibleUniverseSnapshot> {
self.selection_universe_and_risk_decisions_with_options(
ctx,
date,
factor_date,
selection_risk_deferral,
false,
)
.0
}
fn selection_universe_and_risk_decisions_with_options(
&self,
ctx: &StrategyContext<'_>,
date: NaiveDate,
factor_date: NaiveDate,
selection_risk_deferral: SelectionRiskDeferral,
collect_risk_decisions: bool,
) -> (Vec<EligibleUniverseSnapshot>, Vec<FidcRiskDecisionAudit>) {
let mut rows = Vec::new();
let mut decisions = Vec::new();
let factor_rows = ctx.data.factor_snapshot_rows_on(factor_date);
let factor_symbol_ids = ctx.data.factor_symbol_ids_on(factor_date);
debug_assert_eq!(factor_rows.len(), factor_symbol_ids.len());
for (factor, symbol_id) in factor_rows.iter().zip(factor_symbol_ids.iter().copied()) {
if factor.market_cap_bn <= 0.0 || !factor.market_cap_bn.is_finite() {
continue;
}
if ctx.has_dynamic_universe() && !ctx.dynamic_universe_contains(&factor.symbol) {
continue;
}
@@ -8547,12 +8638,43 @@ impl PlatformExprStrategy {
let Some(market) = ctx.data.market_by_symbol_id(date, symbol_id) else {
continue;
};
if let Some(_reason) =
self.selection_risk_rejection_reason(ctx, date, &factor.symbol, candidate, market)
let (reject_from_universe, selection_decision) = if collect_risk_decisions {
let decision = ChinaAShareRiskControl::selection_rejection_decision_with_config(
date,
candidate,
market,
ctx.data.instrument(&factor.symbol),
&self.config.risk_config,
);
let rejected = decision.as_ref().is_some_and(|decision| {
!selection_risk_deferral.should_defer_rejection(&decision.rule_code)
});
(rejected, decision)
} else {
let reason = ChinaAShareRiskControl::selection_rejection_reason_with_config(
date,
candidate,
market,
ctx.data.instrument(&factor.symbol),
&self.config.risk_config,
);
(
reason.is_some_and(|reason| {
!selection_risk_deferral.should_defer_rejection(reason)
}),
None,
)
};
if let Some(decision) = selection_decision
&& !selection_risk_deferral.should_suppress_diagnostic(&decision.rule_code)
{
if !selection_risk_deferral.should_defer_rejection(_reason) {
decisions.push(decision);
}
if reject_from_universe {
continue;
}
if factor.market_cap_bn <= 0.0 || !factor.market_cap_bn.is_finite() {
continue;
}
if !self.stock_passes_universe_exclude(candidate, market) {
continue;
@@ -8574,9 +8696,10 @@ impl PlatformExprStrategy {
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| left.symbol.cmp(&right.symbol))
});
rows
(rows, decisions)
}
#[cfg(test)]
fn selection_risk_decisions(
&self,
ctx: &StrategyContext<'_>,
@@ -8591,6 +8714,7 @@ impl PlatformExprStrategy {
)
}
#[cfg(test)]
fn selection_risk_decisions_with_options(
&self,
ctx: &StrategyContext<'_>,
@@ -8598,40 +8722,14 @@ impl PlatformExprStrategy {
factor_date: NaiveDate,
selection_risk_deferral: SelectionRiskDeferral,
) -> Vec<FidcRiskDecisionAudit> {
let mut decisions = Vec::new();
let factor_rows = ctx.data.factor_snapshot_rows_on(factor_date);
let factor_symbol_ids = ctx.data.factor_symbol_ids_on(factor_date);
debug_assert_eq!(factor_rows.len(), factor_symbol_ids.len());
for (factor, symbol_id) in factor_rows.iter().zip(factor_symbol_ids.iter().copied()) {
if ctx.has_dynamic_universe() && !ctx.dynamic_universe_contains(&factor.symbol) {
continue;
}
let synthetic_candidate;
let candidate =
if let Some(candidate) = ctx.data.candidate_by_symbol_id(date, symbol_id) {
candidate
} else {
synthetic_candidate =
crate::data::missing_candidate_risk_state(date, &factor.symbol);
&synthetic_candidate
};
let Some(market) = ctx.data.market_by_symbol_id(date, symbol_id) else {
continue;
};
if let Some(decision) = ChinaAShareRiskControl::selection_rejection_decision_with_config(
self.selection_universe_and_risk_decisions_with_options(
ctx,
date,
candidate,
market,
ctx.data.instrument(&factor.symbol),
&self.config.risk_config,
) {
if selection_risk_deferral.should_suppress_diagnostic(&decision.rule_code) {
continue;
}
decisions.push(decision);
}
}
decisions
factor_date,
selection_risk_deferral,
true,
)
.1
}
fn selection_risk_decision_diagnostics(
@@ -8667,23 +8765,6 @@ impl PlatformExprStrategy {
diagnostics
}
fn selection_risk_rejection_reason(
&self,
ctx: &StrategyContext<'_>,
date: NaiveDate,
symbol: &str,
candidate: &crate::data::CandidateEligibility,
market: &DailyMarketSnapshot,
) -> Option<&'static str> {
ChinaAShareRiskControl::selection_rejection_reason_with_config(
date,
candidate,
market,
ctx.data.instrument(symbol),
&self.config.risk_config,
)
}
fn stock_selection_limit_rejection_reason(
&self,
stock: &StockExpressionState,
@@ -9013,8 +9094,13 @@ impl PlatformExprStrategy {
// Selection policy is evaluated on the signal day. Lagged execution only
// defers buy/sell risk to the actual execution bar; it must not disable an
// explicitly configured signal-day universe filter.
let universe = self.selectable_universe_on(ctx, date, universe_factor_date);
let risk_decisions = self.selection_risk_decisions(ctx, date, universe_factor_date);
let (universe, risk_decisions) = self.selection_universe_and_risk_decisions_with_options(
ctx,
date,
universe_factor_date,
SelectionRiskDeferral::None,
true,
);
let mut diagnostics = Self::selection_risk_decision_diagnostics(
&risk_decisions,
date,
@@ -11534,7 +11620,7 @@ impl Strategy for PlatformExprStrategy {
ctx,
execution_date,
symbol,
&self.stock_state(ctx, execution_date, symbol)?,
self.stock_state(ctx, execution_date, symbol)?.as_ref(),
)?
.is_some()
{