修复FIDC默认选股池依赖风控事实
This commit is contained in:
+113
-23
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user