增强回测引擎第二版策略与快照层
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user