修复FIDC默认选股池依赖风控事实

This commit is contained in:
boris
2026-07-05 17:13:51 +08:00
parent c557656040
commit 289448d196
+113 -23
View File
@@ -2493,14 +2493,7 @@ impl DataSet {
pub fn eligible_universe_on(&self, date: NaiveDate) -> &[EligibleUniverseSnapshot] {
self.eligible_universe_by_date
.get_or_init(|| {
build_eligible_universe(
&self.factor_by_date,
&self.candidate_by_date,
&self.market_by_date,
&self.instruments,
)
})
.get_or_init(|| build_eligible_universe(&self.factor_by_date, &self.market_by_date))
.get(&date)
.map(Vec::as_slice)
.unwrap_or(&[])
@@ -3033,22 +3026,12 @@ fn build_order_book_depth_index(
fn build_eligible_universe(
factor_by_date: &BTreeMap<NaiveDate, Vec<Arc<DailyFactorSnapshot>>>,
candidate_by_date: &BTreeMap<NaiveDate, Vec<Arc<CandidateEligibility>>>,
market_by_date: &BTreeMap<NaiveDate, Vec<Arc<DailyMarketSnapshot>>>,
instruments: &HashMap<String, Instrument>,
) -> BTreeMap<NaiveDate, Vec<EligibleUniverseSnapshot>> {
let mut per_date = BTreeMap::<NaiveDate, Vec<EligibleUniverseSnapshot>>::new();
let risk_config = FidcRiskControlConfig::default();
for (date, factors) in factor_by_date {
let rows = build_eligible_universe_for_date_from_factors(
*date,
factors,
candidate_by_date,
market_by_date,
instruments,
&risk_config,
);
for date in factor_by_date.keys() {
let rows = build_fundamental_universe_for_date(*date, factor_by_date, market_by_date);
per_date.insert(*date, rows);
}
@@ -3126,11 +3109,15 @@ fn build_eligible_universe_for_date_from_factors(
if factor.market_cap_bn <= 0.0 || !factor.market_cap_bn.is_finite() {
continue;
}
let Some(candidate) = candidate_by_date
let synthetic_candidate;
let candidate = if let Some(candidate) = candidate_by_date
.get(&date)
.and_then(|rows| find_arc_by_symbol(rows, &factor.symbol, |row| row.symbol.as_str()))
else {
continue;
{
candidate
} else {
synthetic_candidate = missing_candidate_risk_state(date, &factor.symbol);
&synthetic_candidate
};
let Some(market) = market_by_date
.get(&date)
@@ -3169,6 +3156,25 @@ fn build_eligible_universe_for_date_from_factors(
rows
}
fn missing_candidate_risk_state(date: NaiveDate, symbol: &str) -> CandidateEligibility {
CandidateEligibility {
date,
symbol: symbol.to_string(),
is_st: false,
is_star_st: false,
is_new_listing: false,
is_paused: false,
allow_buy: true,
allow_sell: true,
is_kcb: false,
is_one_yuan: false,
risk_level_code: Some(
"missing_risk_state:is_st,is_star_st,is_paused,listed_days,is_kcb,is_one_yuan"
.to_string(),
),
}
}
#[cfg(test)]
fn instrument_passes_baseline_selection(instrument: Option<&Instrument>, date: NaiveDate) -> bool {
ChinaAShareRiskControl::instrument_rejection_reason(instrument, date).is_none()
@@ -3445,6 +3451,90 @@ mod tests {
assert!((rows[1].market_cap_bn - 10.0).abs() < 1e-9);
}
#[test]
fn eligible_universe_does_not_require_candidate_risk_state_when_selection_risk_is_disabled() {
let date = NaiveDate::parse_from_str("2025-01-06", "%Y-%m-%d").unwrap();
let symbol = "000001.SZ";
let data = DataSet::from_components(
vec![Instrument {
symbol: symbol.to_string(),
name: symbol.to_string(),
board: "SZ".to_string(),
round_lot: 100,
listed_at: Some(NaiveDate::parse_from_str("2020-01-01", "%Y-%m-%d").unwrap()),
delisted_at: None,
status: "active".to_string(),
}],
vec![DailyMarketSnapshot {
date,
symbol: symbol.to_string(),
timestamp: Some("2025-01-06 10:18:00".to_string()),
day_open: 10.0,
open: 10.0,
high: 10.2,
low: 9.8,
close: 10.1,
last_price: 10.1,
bid1: 10.0,
ask1: 10.1,
prev_close: 10.0,
volume: 100_000,
minute_volume: 1_000,
bid1_volume: 1_000,
ask1_volume: 1_000,
trading_phase: Some("continuous".to_string()),
paused: false,
upper_limit: 11.0,
lower_limit: 9.0,
price_tick: 0.01,
}],
vec![DailyFactorSnapshot {
date,
symbol: symbol.to_string(),
market_cap_bn: 10.0,
free_float_cap_bn: 9.0,
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
extra_factors: BTreeMap::new(),
}],
Vec::new(),
vec![BenchmarkSnapshot {
date,
benchmark: "000852.SH".to_string(),
open: 100.0,
close: 101.0,
prev_close: 99.0,
volume: 1_000_000,
}],
)
.expect("dataset");
assert_eq!(
data.eligible_universe_on(date)
.iter()
.map(|row| row.symbol.as_str())
.collect::<Vec<_>>(),
vec![symbol]
);
assert_eq!(
data.eligible_universe_on_with_risk_config(date, &FidcRiskControlConfig::default())
.iter()
.map(|row| row.symbol.as_str())
.collect::<Vec<_>>(),
vec![symbol],
"execution-risk defaults must not make selection depend on candidate risk facts"
);
let mut selection_risk_config = FidcRiskControlConfig::default();
selection_risk_config.static_rules.reject_st_selection = true;
assert!(
data.eligible_universe_on_with_risk_config(date, &selection_risk_config)
.is_empty(),
"explicit selection risk must reject when required candidate facts are missing"
);
}
#[test]
fn eligible_universe_can_use_configured_risk_policy() {
let date = NaiveDate::parse_from_str("2025-01-06", "%Y-%m-%d").unwrap();