修正FIDC选股阶段风控语义
This commit is contained in:
@@ -1506,7 +1506,6 @@ impl Strategy for CnSmallCapRotationStrategy {
|
||||
data: ctx.data,
|
||||
dynamic_universe: ctx.dynamic_universe,
|
||||
risk_config: Some(&self.config.risk_config),
|
||||
defer_selection_risk: ctx.is_lagged_execution(),
|
||||
});
|
||||
let before_ma_count = selected_before_ma.len();
|
||||
let mut ma_rejects = Vec::new();
|
||||
@@ -2474,11 +2473,7 @@ impl OmniMicroCapStrategy {
|
||||
&self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
date: NaiveDate,
|
||||
defer_selection_risk: bool,
|
||||
) -> Vec<FidcRiskDecisionAudit> {
|
||||
if defer_selection_risk {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut decisions = Vec::new();
|
||||
for factor in ctx.data.factor_snapshots_on(date) {
|
||||
if ctx.has_dynamic_universe() && !ctx.dynamic_universe_contains(&factor.symbol) {
|
||||
@@ -2538,13 +2533,8 @@ impl OmniMicroCapStrategy {
|
||||
date: NaiveDate,
|
||||
band_low: f64,
|
||||
band_high: f64,
|
||||
defer_selection_risk: bool,
|
||||
) -> Result<(Vec<String>, Vec<String>), BacktestError> {
|
||||
let universe = if defer_selection_risk {
|
||||
ctx.fundamental_universe_on(date)
|
||||
} else {
|
||||
ctx.eligible_universe_on_with_risk_config(date, &self.config.risk_config)
|
||||
};
|
||||
let universe = ctx.eligible_universe_on_with_risk_config(date, &self.config.risk_config);
|
||||
let mut diagnostics = Vec::new();
|
||||
let mut selected = Vec::new();
|
||||
let start = lower_bound_eligible(&universe, band_low);
|
||||
@@ -2553,12 +2543,8 @@ impl OmniMicroCapStrategy {
|
||||
if candidate.market_cap_bn > band_high {
|
||||
break;
|
||||
}
|
||||
let rejection = if defer_selection_risk {
|
||||
(!self.stock_passes_ma_filter(ctx, date, &candidate.symbol))
|
||||
.then_some("ma_filter".to_string())
|
||||
} else {
|
||||
self.buy_rejection_reason(ctx, date, &candidate.symbol)?
|
||||
};
|
||||
let rejection = (!self.stock_passes_ma_filter(ctx, date, &candidate.symbol))
|
||||
.then_some("ma_filter".to_string());
|
||||
if let Some(reason) = rejection {
|
||||
if diagnostics.len() < 12 {
|
||||
diagnostics.push(format!("{} rejected by {}", candidate.symbol, reason));
|
||||
@@ -2584,7 +2570,7 @@ impl Strategy for OmniMicroCapStrategy {
|
||||
fn on_day(&mut self, ctx: &StrategyContext<'_>) -> Result<StrategyDecision, BacktestError> {
|
||||
let signal_date = ctx.decision_date;
|
||||
let execution_date = ctx.execution_date;
|
||||
let defer_selection_risk = ctx.is_lagged_execution();
|
||||
let lagged_execution = ctx.is_lagged_execution();
|
||||
if self.config.in_skip_window(signal_date) {
|
||||
return Ok(StrategyDecision {
|
||||
rebalance: false,
|
||||
@@ -2630,8 +2616,8 @@ impl Strategy for OmniMicroCapStrategy {
|
||||
// 使用前一交易日的指数价格计算市值区间(模拟实盘场景)
|
||||
let (band_low, band_high) = self.market_cap_band(prev_index_level);
|
||||
let (stock_list, selection_notes) =
|
||||
self.select_symbols(ctx, signal_date, band_low, band_high, defer_selection_risk)?;
|
||||
let risk_decisions = self.selection_risk_decisions(ctx, signal_date, defer_selection_risk);
|
||||
self.select_symbols(ctx, signal_date, band_low, band_high)?;
|
||||
let risk_decisions = self.selection_risk_decisions(ctx, signal_date);
|
||||
let periodic_rebalance = ctx.decision_index % self.config.refresh_rate == 0;
|
||||
let projection_date = signal_date;
|
||||
let mut projected = ctx.portfolio.clone();
|
||||
@@ -2656,8 +2642,8 @@ impl Strategy for OmniMicroCapStrategy {
|
||||
<= position.average_cost * self.config.stop_loss_ratio
|
||||
+ self.stop_loss_tolerance(market);
|
||||
let profit_hit = current_price / position.average_cost > self.config.take_profit_ratio;
|
||||
let can_sell = defer_selection_risk
|
||||
|| self.can_sell_position(ctx, execution_date, &position.symbol);
|
||||
let can_sell =
|
||||
lagged_execution || self.can_sell_position(ctx, execution_date, &position.symbol);
|
||||
let at_upper_limit = market.is_at_upper_limit_price(current_price);
|
||||
if stop_hit || (profit_hit && !at_upper_limit) {
|
||||
let sell_reason = if stop_hit {
|
||||
@@ -2693,7 +2679,7 @@ impl Strategy for OmniMicroCapStrategy {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if !defer_selection_risk
|
||||
if !lagged_execution
|
||||
&& self
|
||||
.buy_rejection_reason(ctx, execution_date, symbol)?
|
||||
.is_some()
|
||||
@@ -2731,7 +2717,7 @@ impl Strategy for OmniMicroCapStrategy {
|
||||
if stock_list.iter().any(|candidate| candidate == symbol) {
|
||||
continue;
|
||||
}
|
||||
if !defer_selection_risk && !self.can_sell_position(ctx, execution_date, symbol) {
|
||||
if !lagged_execution && !self.can_sell_position(ctx, execution_date, symbol) {
|
||||
continue;
|
||||
}
|
||||
order_intents.push(OrderIntent::TargetValue {
|
||||
@@ -2759,7 +2745,7 @@ impl Strategy for OmniMicroCapStrategy {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if !defer_selection_risk
|
||||
if !lagged_execution
|
||||
&& self
|
||||
.buy_rejection_reason(ctx, execution_date, symbol)?
|
||||
.is_some()
|
||||
@@ -2990,11 +2976,10 @@ mod tests {
|
||||
default_cfg.stock_long_ma_days = 3;
|
||||
let default_strategy = OmniMicroCapStrategy::new(default_cfg.clone());
|
||||
let (default_selected, _) = default_strategy
|
||||
.select_symbols(&ctx, dates[2], 0.0, 100.0, false)
|
||||
.select_symbols(&ctx, dates[2], 0.0, 100.0)
|
||||
.expect("default selection");
|
||||
assert!(default_selected.is_empty());
|
||||
let default_risk_decisions =
|
||||
default_strategy.selection_risk_decisions(&ctx, dates[2], false);
|
||||
let default_risk_decisions = default_strategy.selection_risk_decisions(&ctx, dates[2]);
|
||||
assert_eq!(default_risk_decisions.len(), 1);
|
||||
assert_eq!(default_risk_decisions[0].symbol, symbol);
|
||||
assert_eq!(default_risk_decisions[0].rule_code, "kcb");
|
||||
@@ -3009,11 +2994,11 @@ mod tests {
|
||||
cfg.risk_config.static_rules.reject_kcb_buy = false;
|
||||
let configured_strategy = OmniMicroCapStrategy::new(cfg);
|
||||
let (selected, _) = configured_strategy
|
||||
.select_symbols(&ctx, dates[2], 0.0, 100.0, false)
|
||||
.select_symbols(&ctx, dates[2], 0.0, 100.0)
|
||||
.expect("configured selection");
|
||||
assert!(
|
||||
configured_strategy
|
||||
.selection_risk_decisions(&ctx, dates[2], false)
|
||||
.selection_risk_decisions(&ctx, dates[2])
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
|
||||
@@ -50,20 +50,15 @@ pub struct SelectionContext<'a> {
|
||||
pub data: &'a DataSet,
|
||||
pub dynamic_universe: Option<&'a BTreeSet<String>>,
|
||||
pub risk_config: Option<&'a FidcRiskControlConfig>,
|
||||
pub defer_selection_risk: bool,
|
||||
}
|
||||
|
||||
impl SelectionContext<'_> {
|
||||
fn eligible_universe(&self) -> Vec<EligibleUniverseSnapshot> {
|
||||
let eligible = if self.defer_selection_risk {
|
||||
self.data.fundamental_universe_on(self.decision_date)
|
||||
} else {
|
||||
match self.risk_config {
|
||||
Some(risk_config) => self
|
||||
.data
|
||||
.eligible_universe_on_with_risk_config(self.decision_date, risk_config),
|
||||
None => self.data.eligible_universe_on(self.decision_date).to_vec(),
|
||||
}
|
||||
let eligible = match self.risk_config {
|
||||
Some(risk_config) => self
|
||||
.data
|
||||
.eligible_universe_on_with_risk_config(self.decision_date, risk_config),
|
||||
None => self.data.eligible_universe_on(self.decision_date).to_vec(),
|
||||
};
|
||||
match self.dynamic_universe {
|
||||
Some(symbols) if !symbols.is_empty() => eligible
|
||||
@@ -75,9 +70,6 @@ impl SelectionContext<'_> {
|
||||
}
|
||||
|
||||
fn selection_risk_decisions(&self) -> Vec<FidcRiskDecisionAudit> {
|
||||
if self.defer_selection_risk {
|
||||
return Vec::new();
|
||||
}
|
||||
let default_risk_config;
|
||||
let risk_config = match self.risk_config {
|
||||
Some(value) => value,
|
||||
@@ -411,7 +403,6 @@ mod tests {
|
||||
data: &data,
|
||||
dynamic_universe: None,
|
||||
risk_config: Some(&FidcRiskControlConfig::default()),
|
||||
defer_selection_risk: false,
|
||||
});
|
||||
|
||||
let rules = diagnostics
|
||||
@@ -433,7 +424,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selector_defers_static_execution_risk_for_lagged_execution() {
|
||||
fn selector_applies_configured_selection_risk_on_decision_date() {
|
||||
let data = DataSet::from_components(
|
||||
vec![
|
||||
instrument("000001.SZ"),
|
||||
@@ -466,17 +457,22 @@ mod tests {
|
||||
data: &data,
|
||||
dynamic_universe: None,
|
||||
risk_config: Some(&FidcRiskControlConfig::default()),
|
||||
defer_selection_risk: true,
|
||||
});
|
||||
|
||||
let selected_symbols = selected
|
||||
.iter()
|
||||
.map(|candidate| candidate.symbol.as_str())
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert!(selected_symbols.contains("000001.SZ"));
|
||||
assert!(selected_symbols.contains("688001.SH"));
|
||||
assert!(!selected_symbols.contains("000001.SZ"));
|
||||
assert!(!selected_symbols.contains("688001.SH"));
|
||||
assert!(selected_symbols.contains("000002.SZ"));
|
||||
assert_eq!(diagnostics.not_eligible_count, 0);
|
||||
assert!(diagnostics.risk_decisions.is_empty());
|
||||
assert_eq!(diagnostics.not_eligible_count, 2);
|
||||
let rules = diagnostics
|
||||
.risk_decisions
|
||||
.iter()
|
||||
.map(|decision| decision.rule_code.as_str())
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert!(rules.contains("st"), "{:?}", diagnostics.risk_decisions);
|
||||
assert!(rules.contains("kcb"), "{:?}", diagnostics.risk_decisions);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user