chore: 更新 fidc-backtest-engine - 2026-05-07

This commit is contained in:
boris
2026-05-07 17:12:49 -07:00
parent adc2f12ddf
commit a47c7c3e49

View File

@@ -508,16 +508,24 @@ impl PlatformExprStrategy {
scope: &mut Scope<'_>,
script: &str,
) -> Result<Dynamic, BacktestError> {
// 注意HashMap key 借用即可命中,避免重复克隆 String。
if let Some(ast) = self.compiled_cache.borrow().get(script) {
*self.cache_hits.borrow_mut() += 1;
return self
.engine
.eval_ast_with_scope::<Dynamic>(scope, ast)
.map_err(|error| {
BacktestError::Execution(format!("platform expr eval failed: {}", error))
});
// 命中分支:先借用 cache 拿到 AST做完 eval 再 drop borrow避免与
// cache_hits 的 borrow_mut 冲突(虽然是不同 RefCell但显式作用域更清晰
{
let cache = self.compiled_cache.borrow();
if let Some(ast) = cache.get(script) {
*self.cache_hits.borrow_mut() += 1;
return self
.engine
.eval_ast_with_scope::<Dynamic>(scope, ast)
.map_err(|error| {
BacktestError::Execution(format!(
"platform expr eval failed: {}",
error
))
});
}
}
// 未命中:先编译再插入缓存。
*self.cache_misses.borrow_mut() += 1;
let ast = self.engine.compile(script).map_err(|error| {
BacktestError::Execution(format!("platform expr compile failed: {}", error))
@@ -7509,4 +7517,133 @@ mod tests {
let decision = strategy.on_day(&ctx).expect("platform decision");
assert_eq!(decision.order_intents.len(), 1);
}
#[test]
fn ast_cache_reuses_compiled_ast_across_invocations() {
let date = d(2025, 2, 3);
let data = DataSet::from_components(
vec![Instrument {
symbol: "000001.SZ".to_string(),
name: "Ping An Bank".to_string(),
board: "SZSE".to_string(),
round_lot: 100,
listed_at: Some(d(2010, 1, 1)),
delisted_at: None,
status: "active".to_string(),
}],
vec![DailyMarketSnapshot {
date,
symbol: "000001.SZ".to_string(),
timestamp: Some("10:18:00".to_string()),
day_open: 10.0,
open: 10.0,
high: 10.2,
low: 9.9,
close: 10.1,
last_price: 10.05,
bid1: 10.04,
ask1: 10.05,
prev_close: 9.95,
volume: 1_000_000,
tick_volume: 5_000,
bid1_volume: 1_000,
ask1_volume: 1_000,
trading_phase: Some("continuous".to_string()),
paused: false,
upper_limit: 10.94,
lower_limit: 8.96,
price_tick: 0.01,
}],
vec![DailyFactorSnapshot {
date,
symbol: "000001.SZ".to_string(),
market_cap_bn: 12.0,
free_float_cap_bn: 10.0,
pe_ttm: 8.0,
turnover_ratio: Some(22.0),
effective_turnover_ratio: Some(18.0),
extra_factors: BTreeMap::new(),
}],
vec![CandidateEligibility {
date,
symbol: "000001.SZ".to_string(),
is_st: false,
is_new_listing: false,
is_paused: false,
allow_buy: true,
allow_sell: true,
is_kcb: false,
is_one_yuan: false,
}],
vec![BenchmarkSnapshot {
date,
benchmark: "000852.SH".to_string(),
open: 1000.0,
close: 1002.0,
prev_close: 998.0,
volume: 1_000_000,
}],
)
.expect("dataset");
let portfolio = PortfolioState::new(1_000_000.0);
let subscriptions = BTreeSet::new();
let ctx = StrategyContext {
execution_date: date,
decision_date: date,
decision_index: 0,
data: &data,
portfolio: &portfolio,
futures_account: None,
open_orders: &[],
dynamic_universe: None,
subscriptions: &subscriptions,
process_events: &[],
active_process_event: None,
active_datetime: None,
order_events: &[],
fills: &[],
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.signal_symbol = "000001.SZ".to_string();
cfg.rotation_enabled = false;
cfg.benchmark_short_ma_days = 1;
cfg.benchmark_long_ma_days = 1;
cfg.explicit_actions = vec![PlatformTradeAction::Order {
kind: PlatformExplicitOrderKind::Value,
symbol: "000001.SZ".to_string(),
amount_expr: "cash * 0.1".to_string(),
limit_price_expr: None,
start_time_expr: None,
end_time_expr: None,
when_expr: Some("allow_buy".to_string()),
reason: "ast_cache_reuse".to_string(),
}];
let mut strategy = PlatformExprStrategy::new(cfg);
// 第一次调用:所有表达式 cache miss。
let _ = strategy.on_day(&ctx).expect("first decision");
let misses_after_first = strategy.ast_cache_misses();
let hits_after_first = strategy.ast_cache_hits();
assert!(
misses_after_first > 0,
"first run should populate cache, misses={}",
misses_after_first
);
// 第二次调用相同表达式cache hit 数应当 > 第一次。
let _ = strategy.on_day(&ctx).expect("second decision");
let misses_after_second = strategy.ast_cache_misses();
let hits_after_second = strategy.ast_cache_hits();
assert!(
hits_after_second > hits_after_first,
"second run should reuse cached AST, hits {} -> {}",
hits_after_first,
hits_after_second
);
// 缓存条目数不应该再增长(相同 scriptmisses 不再增加。
assert_eq!(
misses_after_second, misses_after_first,
"second run should not introduce new misses for same scripts"
);
}
}