修复FIDC默认选股池依赖风控事实
This commit is contained in:
+113
-23
@@ -2493,14 +2493,7 @@ impl DataSet {
|
|||||||
|
|
||||||
pub fn eligible_universe_on(&self, date: NaiveDate) -> &[EligibleUniverseSnapshot] {
|
pub fn eligible_universe_on(&self, date: NaiveDate) -> &[EligibleUniverseSnapshot] {
|
||||||
self.eligible_universe_by_date
|
self.eligible_universe_by_date
|
||||||
.get_or_init(|| {
|
.get_or_init(|| build_eligible_universe(&self.factor_by_date, &self.market_by_date))
|
||||||
build_eligible_universe(
|
|
||||||
&self.factor_by_date,
|
|
||||||
&self.candidate_by_date,
|
|
||||||
&self.market_by_date,
|
|
||||||
&self.instruments,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.get(&date)
|
.get(&date)
|
||||||
.map(Vec::as_slice)
|
.map(Vec::as_slice)
|
||||||
.unwrap_or(&[])
|
.unwrap_or(&[])
|
||||||
@@ -3033,22 +3026,12 @@ fn build_order_book_depth_index(
|
|||||||
|
|
||||||
fn build_eligible_universe(
|
fn build_eligible_universe(
|
||||||
factor_by_date: &BTreeMap<NaiveDate, Vec<Arc<DailyFactorSnapshot>>>,
|
factor_by_date: &BTreeMap<NaiveDate, Vec<Arc<DailyFactorSnapshot>>>,
|
||||||
candidate_by_date: &BTreeMap<NaiveDate, Vec<Arc<CandidateEligibility>>>,
|
|
||||||
market_by_date: &BTreeMap<NaiveDate, Vec<Arc<DailyMarketSnapshot>>>,
|
market_by_date: &BTreeMap<NaiveDate, Vec<Arc<DailyMarketSnapshot>>>,
|
||||||
instruments: &HashMap<String, Instrument>,
|
|
||||||
) -> BTreeMap<NaiveDate, Vec<EligibleUniverseSnapshot>> {
|
) -> BTreeMap<NaiveDate, Vec<EligibleUniverseSnapshot>> {
|
||||||
let mut per_date = BTreeMap::<NaiveDate, Vec<EligibleUniverseSnapshot>>::new();
|
let mut per_date = BTreeMap::<NaiveDate, Vec<EligibleUniverseSnapshot>>::new();
|
||||||
let risk_config = FidcRiskControlConfig::default();
|
|
||||||
|
|
||||||
for (date, factors) in factor_by_date {
|
for date in factor_by_date.keys() {
|
||||||
let rows = build_eligible_universe_for_date_from_factors(
|
let rows = build_fundamental_universe_for_date(*date, factor_by_date, market_by_date);
|
||||||
*date,
|
|
||||||
factors,
|
|
||||||
candidate_by_date,
|
|
||||||
market_by_date,
|
|
||||||
instruments,
|
|
||||||
&risk_config,
|
|
||||||
);
|
|
||||||
per_date.insert(*date, rows);
|
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() {
|
if factor.market_cap_bn <= 0.0 || !factor.market_cap_bn.is_finite() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(candidate) = candidate_by_date
|
let synthetic_candidate;
|
||||||
|
let candidate = if let Some(candidate) = candidate_by_date
|
||||||
.get(&date)
|
.get(&date)
|
||||||
.and_then(|rows| find_arc_by_symbol(rows, &factor.symbol, |row| row.symbol.as_str()))
|
.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
|
let Some(market) = market_by_date
|
||||||
.get(&date)
|
.get(&date)
|
||||||
@@ -3169,6 +3156,25 @@ fn build_eligible_universe_for_date_from_factors(
|
|||||||
rows
|
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)]
|
#[cfg(test)]
|
||||||
fn instrument_passes_baseline_selection(instrument: Option<&Instrument>, date: NaiveDate) -> bool {
|
fn instrument_passes_baseline_selection(instrument: Option<&Instrument>, date: NaiveDate) -> bool {
|
||||||
ChinaAShareRiskControl::instrument_rejection_reason(instrument, date).is_none()
|
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);
|
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]
|
#[test]
|
||||||
fn eligible_universe_can_use_configured_risk_policy() {
|
fn eligible_universe_can_use_configured_risk_policy() {
|
||||||
let date = NaiveDate::parse_from_str("2025-01-06", "%Y-%m-%d").unwrap();
|
let date = NaiveDate::parse_from_str("2025-01-06", "%Y-%m-%d").unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user