初始化回测核心引擎骨架

This commit is contained in:
zsb
2026-04-06 23:56:37 -07:00
commit 334864cbc5
25 changed files with 2878 additions and 0 deletions

View 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
}
}