增强回测引擎第二版策略与快照层

This commit is contained in:
zsb
2026-04-07 00:34:52 -07:00
parent 334864cbc5
commit d039c4e741
10 changed files with 244 additions and 86 deletions

View File

@@ -26,14 +26,21 @@ pub struct StrategyDecision {
pub target_weights: BTreeMap<String, f64>,
pub exit_symbols: BTreeSet<String>,
pub notes: Vec<String>,
pub diagnostics: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CnSmallCapRotationConfig {
pub rebalance_every_n_days: usize,
pub max_positions: usize,
pub refresh_rate: usize,
pub stocknum: usize,
pub xs: f64,
pub base_index_level: f64,
pub base_cap_floor: f64,
pub cap_span: f64,
pub short_ma_days: usize,
pub long_ma_days: usize,
pub rsi_rate: f64,
pub trade_rate: f64,
pub stop_loss_pct: f64,
pub take_profit_pct: f64,
}
@@ -41,10 +48,16 @@ pub struct CnSmallCapRotationConfig {
impl CnSmallCapRotationConfig {
pub fn demo() -> Self {
Self {
rebalance_every_n_days: 3,
max_positions: 2,
refresh_rate: 3,
stocknum: 2,
xs: 4.0 / 500.0,
base_index_level: 2000.0,
base_cap_floor: 7.0,
cap_span: 10.0,
short_ma_days: 3,
long_ma_days: 5,
rsi_rate: 1.0001,
trade_rate: 0.5,
stop_loss_pct: 0.08,
take_profit_pct: 0.10,
}
@@ -60,7 +73,13 @@ pub struct CnSmallCapRotationStrategy {
impl CnSmallCapRotationStrategy {
pub fn new(config: CnSmallCapRotationConfig) -> Self {
Self {
selector: DynamicMarketCapBandSelector::demo(config.max_positions),
selector: DynamicMarketCapBandSelector::new(
config.base_index_level,
config.base_cap_floor,
config.cap_span,
config.xs,
config.stocknum,
),
config,
last_gross_exposure: None,
}
@@ -86,12 +105,12 @@ impl CnSmallCapRotationStrategy {
let short_ma = Self::moving_average(closes, self.config.short_ma_days);
let long_ma = Self::moving_average(closes, self.config.long_ma_days);
if current >= long_ma && short_ma >= long_ma {
if short_ma < long_ma * self.config.rsi_rate {
self.config.trade_rate
} else if current >= long_ma {
1.0
} else if current >= long_ma || short_ma >= long_ma {
0.5
} else {
0.0
self.config.trade_rate
}
}
@@ -142,7 +161,7 @@ impl Strategy for CnSmallCapRotationStrategy {
.data
.benchmark_closes_up_to(ctx.decision_date, self.config.long_ma_days);
let gross_exposure = self.gross_exposure(&benchmark_closes);
let periodic_rebalance = ctx.decision_index % self.config.rebalance_every_n_days == 0;
let periodic_rebalance = ctx.decision_index % self.config.refresh_rate == 0;
let exposure_changed = self
.last_gross_exposure
.map(|previous| (previous - gross_exposure).abs() > f64::EPSILON)
@@ -155,6 +174,14 @@ impl Strategy for CnSmallCapRotationStrategy {
"decision={} exec={} exposure={:.2}",
ctx.decision_date, ctx.execution_date, gross_exposure
)];
let mut diagnostics = vec![format!(
"benchmark_close={:.2} refresh_rate={} stocknum={} short_ma_days={} long_ma_days={}",
benchmark.close,
self.config.refresh_rate,
self.config.stocknum,
self.config.short_ma_days,
self.config.long_ma_days,
)];
if rebalance && gross_exposure > 0.0 {
let selected = self.selector.select(&SelectionContext {
@@ -165,9 +192,21 @@ impl Strategy for CnSmallCapRotationStrategy {
if !selected.is_empty() {
let per_name_weight = gross_exposure / selected.len() as f64;
for candidate in selected {
for candidate in &selected {
target_weights.insert(candidate.symbol.clone(), per_name_weight);
}
diagnostics.push(format!(
"selected={} cap_band={:.2}-{:.2} sample={}",
selected.len(),
selected.first().map(|x| x.band_low).unwrap_or_default(),
selected.first().map(|x| x.band_high).unwrap_or_default(),
selected
.iter()
.take(5)
.map(|x| format!("{}:{:.2}", x.symbol, x.market_cap_bn))
.collect::<Vec<_>>()
.join("|")
));
}
notes.push(format!("rebalance names={}", target_weights.len()));
@@ -175,6 +214,10 @@ impl Strategy for CnSmallCapRotationStrategy {
if !exit_symbols.is_empty() {
notes.push(format!("exit hooks={}", exit_symbols.len()));
diagnostics.push(format!(
"exit_symbols={}",
exit_symbols.iter().cloned().collect::<Vec<_>>().join("|")
));
}
if rebalance && gross_exposure == 0.0 {
notes.push("risk throttle forced all-cash".to_string());
@@ -187,6 +230,7 @@ impl Strategy for CnSmallCapRotationStrategy {
target_weights,
exit_symbols,
notes,
diagnostics,
})
}
}