初始化回测核心引擎骨架
This commit is contained in:
110
crates/fidc-core/src/universe.rs
Normal file
110
crates/fidc-core/src/universe.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use crate::data::{BenchmarkSnapshot, DataSet};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BandRegime {
|
||||
Bullish,
|
||||
Neutral,
|
||||
Defensive,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UniverseCandidate {
|
||||
pub symbol: String,
|
||||
pub market_cap_bn: f64,
|
||||
pub free_float_cap_bn: f64,
|
||||
}
|
||||
|
||||
pub struct SelectionContext<'a> {
|
||||
pub decision_date: NaiveDate,
|
||||
pub benchmark: &'a BenchmarkSnapshot,
|
||||
pub data: &'a DataSet,
|
||||
}
|
||||
|
||||
pub trait UniverseSelector {
|
||||
fn select(&self, ctx: &SelectionContext<'_>) -> Vec<UniverseCandidate>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DynamicMarketCapBandSelector {
|
||||
pub base_index_level: f64,
|
||||
pub bullish_threshold: f64,
|
||||
pub neutral_threshold: f64,
|
||||
pub bullish_band: (f64, f64),
|
||||
pub neutral_band: (f64, f64),
|
||||
pub defensive_band: (f64, f64),
|
||||
pub top_n: usize,
|
||||
}
|
||||
|
||||
impl DynamicMarketCapBandSelector {
|
||||
pub fn demo(top_n: usize) -> Self {
|
||||
Self {
|
||||
base_index_level: 3000.0,
|
||||
bullish_threshold: 1.02,
|
||||
neutral_threshold: 1.0,
|
||||
bullish_band: (30.0, 60.0),
|
||||
neutral_band: (40.0, 90.0),
|
||||
defensive_band: (60.0, 120.0),
|
||||
top_n,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn regime(&self, benchmark_level: f64) -> BandRegime {
|
||||
let ratio = benchmark_level / self.base_index_level;
|
||||
if ratio >= self.bullish_threshold {
|
||||
BandRegime::Bullish
|
||||
} else if ratio >= self.neutral_threshold {
|
||||
BandRegime::Neutral
|
||||
} else {
|
||||
BandRegime::Defensive
|
||||
}
|
||||
}
|
||||
|
||||
fn band(&self, regime: BandRegime) -> (f64, f64) {
|
||||
match regime {
|
||||
BandRegime::Bullish => self.bullish_band,
|
||||
BandRegime::Neutral => self.neutral_band,
|
||||
BandRegime::Defensive => self.defensive_band,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UniverseSelector for DynamicMarketCapBandSelector {
|
||||
fn select(&self, ctx: &SelectionContext<'_>) -> Vec<UniverseCandidate> {
|
||||
let regime = self.regime(ctx.benchmark.close);
|
||||
let (min_cap, max_cap) = self.band(regime);
|
||||
|
||||
let mut selected = ctx
|
||||
.data
|
||||
.factor_snapshots_on(ctx.decision_date)
|
||||
.into_iter()
|
||||
.filter_map(|factor| {
|
||||
let candidate = ctx.data.candidate(ctx.decision_date, &factor.symbol)?;
|
||||
let market = ctx.data.market(ctx.decision_date, &factor.symbol)?;
|
||||
|
||||
if !candidate.eligible_for_selection() || market.paused {
|
||||
return None;
|
||||
}
|
||||
if factor.market_cap_bn < min_cap || factor.market_cap_bn > max_cap {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(UniverseCandidate {
|
||||
symbol: factor.symbol.clone(),
|
||||
market_cap_bn: factor.market_cap_bn,
|
||||
free_float_cap_bn: factor.free_float_cap_bn,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
selected.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))
|
||||
});
|
||||
selected.truncate(self.top_n);
|
||||
selected
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user