chore: 更新 fidc-backtest-engine - 2026-05-07
This commit is contained in:
@@ -508,16 +508,24 @@ impl PlatformExprStrategy {
|
|||||||
scope: &mut Scope<'_>,
|
scope: &mut Scope<'_>,
|
||||||
script: &str,
|
script: &str,
|
||||||
) -> Result<Dynamic, BacktestError> {
|
) -> Result<Dynamic, BacktestError> {
|
||||||
// 注意:HashMap key 借用即可命中,避免重复克隆 String。
|
// 命中分支:先借用 cache 拿到 AST,做完 eval 再 drop borrow,避免与
|
||||||
if let Some(ast) = self.compiled_cache.borrow().get(script) {
|
// cache_hits 的 borrow_mut 冲突(虽然是不同 RefCell,但显式作用域更清晰)。
|
||||||
*self.cache_hits.borrow_mut() += 1;
|
{
|
||||||
return self
|
let cache = self.compiled_cache.borrow();
|
||||||
.engine
|
if let Some(ast) = cache.get(script) {
|
||||||
.eval_ast_with_scope::<Dynamic>(scope, ast)
|
*self.cache_hits.borrow_mut() += 1;
|
||||||
.map_err(|error| {
|
return self
|
||||||
BacktestError::Execution(format!("platform expr eval failed: {}", error))
|
.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;
|
*self.cache_misses.borrow_mut() += 1;
|
||||||
let ast = self.engine.compile(script).map_err(|error| {
|
let ast = self.engine.compile(script).map_err(|error| {
|
||||||
BacktestError::Execution(format!("platform expr compile failed: {}", error))
|
BacktestError::Execution(format!("platform expr compile failed: {}", error))
|
||||||
@@ -7509,4 +7517,133 @@ mod tests {
|
|||||||
let decision = strategy.on_day(&ctx).expect("platform decision");
|
let decision = strategy.on_day(&ctx).expect("platform decision");
|
||||||
assert_eq!(decision.order_intents.len(), 1);
|
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
|
||||||
|
);
|
||||||
|
// 缓存条目数不应该再增长(相同 script):misses 不再增加。
|
||||||
|
assert_eq!(
|
||||||
|
misses_after_second, misses_after_first,
|
||||||
|
"second run should not introduce new misses for same scripts"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user