实现FIDC配置化风控与交易成本
This commit is contained in:
+183
-43
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user