实现FIDC配置化风控与交易成本

This commit is contained in:
boris
2026-07-02 07:16:47 +08:00
parent 754fc91376
commit 7db0e8da1d
17 changed files with 2689 additions and 249 deletions
+183 -43
View File
@@ -10,7 +10,7 @@ use thiserror::Error;
use crate::calendar::TradingCalendar;
use crate::futures::{FuturesCommissionType, FuturesTradingParameter};
use crate::instrument::Instrument;
use crate::risk_control::ChinaAShareRiskControl;
use crate::risk_control::{ChinaAShareRiskControl, FidcRiskControlConfig};
mod date_format {
use chrono::NaiveDate;
@@ -2607,6 +2607,21 @@ impl DataSet {
.unwrap_or(&[])
}
pub fn eligible_universe_on_with_risk_config(
&self,
date: NaiveDate,
risk_config: &FidcRiskControlConfig,
) -> Vec<EligibleUniverseSnapshot> {
build_eligible_universe_for_date(
date,
&self.factor_by_date,
&self.candidate_by_date,
&self.market_by_date,
&self.instruments,
risk_config,
)
}
pub fn require_market(
&self,
date: NaiveDate,
@@ -3647,56 +3662,102 @@ fn build_eligible_universe(
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 mut rows = Vec::new();
for factor in factors {
if factor.market_cap_bn <= 0.0 || !factor.market_cap_bn.is_finite() {
continue;
}
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;
};
let Some(market) = market_by_date.get(date).and_then(|rows| {
find_arc_by_symbol(rows, &factor.symbol, |row| row.symbol.as_str())
}) else {
continue;
};
if ChinaAShareRiskControl::selection_rejection_reason(
*date,
candidate,
market,
instruments.get(&factor.symbol),
)
.is_some()
{
continue;
}
let market_cap_bn = decision_market_cap_bn(factor, market);
if market_cap_bn <= 0.0 || !market_cap_bn.is_finite() {
continue;
}
let free_float_cap_bn = decision_free_float_cap_bn(factor, market);
rows.push(EligibleUniverseSnapshot {
symbol: factor.symbol.clone(),
market_cap_bn,
free_float_cap_bn,
});
}
rows.sort_by(|left, right| {
left.market_cap_bn
.partial_cmp(&right.market_cap_bn)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| left.symbol.cmp(&right.symbol))
});
let rows = build_eligible_universe_for_date_from_factors(
*date,
factors,
candidate_by_date,
market_by_date,
instruments,
&risk_config,
);
per_date.insert(*date, rows);
}
per_date
}
fn build_eligible_universe_for_date(
date: NaiveDate,
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>,
risk_config: &FidcRiskControlConfig,
) -> Vec<EligibleUniverseSnapshot> {
factor_by_date
.get(&date)
.map(|factors| {
build_eligible_universe_for_date_from_factors(
date,
factors,
candidate_by_date,
market_by_date,
instruments,
risk_config,
)
})
.unwrap_or_default()
}
fn build_eligible_universe_for_date_from_factors(
date: NaiveDate,
factors: &[Arc<DailyFactorSnapshot>],
candidate_by_date: &BTreeMap<NaiveDate, Vec<Arc<CandidateEligibility>>>,
market_by_date: &BTreeMap<NaiveDate, Vec<Arc<DailyMarketSnapshot>>>,
instruments: &HashMap<String, Instrument>,
risk_config: &FidcRiskControlConfig,
) -> Vec<EligibleUniverseSnapshot> {
let mut rows = Vec::new();
for factor in factors {
if factor.market_cap_bn <= 0.0 || !factor.market_cap_bn.is_finite() {
continue;
}
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;
};
let Some(market) = market_by_date
.get(&date)
.and_then(|rows| find_arc_by_symbol(rows, &factor.symbol, |row| row.symbol.as_str()))
else {
continue;
};
if ChinaAShareRiskControl::selection_rejection_reason_with_config(
date,
candidate,
market,
instruments.get(&factor.symbol),
risk_config,
)
.is_some()
{
continue;
}
let market_cap_bn = decision_market_cap_bn(factor, market);
if market_cap_bn <= 0.0 || !market_cap_bn.is_finite() {
continue;
}
let free_float_cap_bn = decision_free_float_cap_bn(factor, market);
rows.push(EligibleUniverseSnapshot {
symbol: factor.symbol.clone(),
market_cap_bn,
free_float_cap_bn,
});
}
rows.sort_by(|left, right| {
left.market_cap_bn
.partial_cmp(&right.market_cap_bn)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| left.symbol.cmp(&right.symbol))
});
rows
}
#[cfg(test)]
fn instrument_passes_baseline_selection(instrument: Option<&Instrument>, date: NaiveDate) -> bool {
ChinaAShareRiskControl::instrument_rejection_reason(instrument, date).is_none()
@@ -3981,6 +4042,85 @@ mod tests {
assert!((rows[1].market_cap_bn - 10.0).abs() < 1e-9);
}
#[test]
fn eligible_universe_can_use_configured_risk_policy() {
let date = NaiveDate::parse_from_str("2025-01-06", "%Y-%m-%d").unwrap();
let symbol = "688001.SH";
let data = DataSet::from_components(
vec![Instrument {
symbol: symbol.to_string(),
name: symbol.to_string(),
board: "SH".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![CandidateEligibility {
date,
symbol: symbol.to_string(),
is_st: false,
is_new_listing: false,
is_paused: false,
allow_buy: true,
allow_sell: true,
is_kcb: true,
is_one_yuan: false,
risk_level_code: None,
}],
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!(data.eligible_universe_on(date).is_empty());
let mut risk_config = FidcRiskControlConfig::default();
risk_config.static_rules.reject_kcb_selection = false;
let rows = data.eligible_universe_on_with_risk_config(date, &risk_config);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].symbol, symbol);
}
#[test]
fn decision_market_cap_keeps_pre_adjusted_factor() {
let date = NaiveDate::parse_from_str("2025-01-06", "%Y-%m-%d").unwrap();