Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4664f1a2d3 |
@@ -3375,6 +3375,12 @@ impl DataSet {
|
||||
.unwrap_or(&[])
|
||||
}
|
||||
|
||||
pub fn is_reference_only_benchmark(&self, symbol: &str) -> bool {
|
||||
if symbol != self.benchmark_code() { return false; }
|
||||
let Some(symbol_id) = self.symbol_id(symbol) else { return true; };
|
||||
!self.candidate_symbol_ids_by_date.values().any(|ids| ids.contains(&symbol_id))
|
||||
}
|
||||
|
||||
pub fn bundle_on(&self, date: NaiveDate) -> Result<DailySnapshotBundle, DataSetError> {
|
||||
let benchmark = self
|
||||
.benchmark(date)
|
||||
|
||||
@@ -474,7 +474,9 @@ pub struct BacktestEngine<S, C, R> {
|
||||
}
|
||||
|
||||
fn all_instruments_have_dated_absence(data: &DataSet, date: NaiveDate) -> bool {
|
||||
!data.instruments().is_empty() && data.instruments().values().all(|instrument| instrument.dated_market_absence_reason(date).is_some())
|
||||
let mut instruments = data.instruments().values()
|
||||
.filter(|instrument| !data.is_reference_only_benchmark(&instrument.symbol)).peekable();
|
||||
instruments.peek().is_some() && instruments.all(|instrument| instrument.dated_market_absence_reason(date).is_some())
|
||||
}
|
||||
|
||||
fn backtest_execution_schedule(
|
||||
@@ -5580,9 +5582,11 @@ mod tests {
|
||||
let dates = [d(2025, 1, 2), d(2025, 1, 3), d(2025, 1, 6)];
|
||||
let mut engine = engine_with_matching(MatchingType::CurrentBarClose, PriceField::Close, 0);
|
||||
engine.config.end_date = Some(dates[2]);
|
||||
let mut markets = vec![market(dates[2], 10.0, 10.0)];
|
||||
markets.extend(dates.iter().map(|date| DailyMarketSnapshot { symbol: "000852.SH".into(), ..market(*date, 1000.0, 1000.0) }));
|
||||
engine.data = DataSet::from_components(
|
||||
vec![Instrument { listed_at: Some(dates[2]), ..default_instrument() }],
|
||||
vec![market(dates[2], 10.0, 10.0)], vec![factor(dates[2])], vec![candidate(dates[2])],
|
||||
vec![Instrument { listed_at: Some(dates[2]), ..default_instrument() }, Instrument { symbol: "000852.SH".into(), listed_at: None, ..default_instrument() }],
|
||||
markets, vec![factor(dates[2])], vec![candidate(dates[2])],
|
||||
dates.iter().map(|date| benchmark(*date)).collect(),
|
||||
).unwrap();
|
||||
assert_eq!(super::backtest_execution_dates(&engine.data, Some(dates[0]), Some(dates[2]), 0), dates);
|
||||
|
||||
@@ -3853,16 +3853,9 @@ impl PlatformExprStrategy {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if !defer_execution_risk
|
||||
&& self
|
||||
.buy_rejection_reason(
|
||||
ctx,
|
||||
execution_date,
|
||||
symbol,
|
||||
self.stock_state(ctx, execution_date, symbol)?.as_ref(),
|
||||
)?
|
||||
.is_some()
|
||||
{
|
||||
if !defer_execution_risk && self.buy_rejection_reason(
|
||||
ctx, execution_date, symbol, self.stock_state(ctx, execution_date, symbol)?.as_ref(),
|
||||
)?.is_some() {
|
||||
continue;
|
||||
}
|
||||
let decision_stock = self.stock_state_with_factor_date(
|
||||
@@ -14013,16 +14006,10 @@ impl PlatformExprStrategy {
|
||||
if target_value <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
if !defer_execution_risk
|
||||
&& self
|
||||
.buy_rejection_reason(
|
||||
ctx,
|
||||
execution_date,
|
||||
symbol,
|
||||
self.stock_state(ctx, execution_date, symbol)?.as_ref(),
|
||||
)?
|
||||
.is_some()
|
||||
{
|
||||
if !defer_execution_risk && let Some(reason) = self.buy_rejection_reason(
|
||||
ctx, execution_date, symbol, self.stock_state(ctx, execution_date, symbol)?.as_ref(),
|
||||
)? {
|
||||
risk_decisions.push(FidcRiskDecisionAudit::rejected_buy_plan(execution_date, symbol, &reason));
|
||||
continue;
|
||||
}
|
||||
if !self.stock_passes_expr(ctx, &day, &decision_stock)? {
|
||||
@@ -14299,6 +14286,37 @@ mod tests {
|
||||
assert_eq!(strategy.selection_quote_usage, StockFilterQuoteUsage::DailyOnly);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn periodic_selected_bjse_buy_rejection_is_audited_without_creating_an_order() {
|
||||
let dates = [d(2026, 8, 5), d(2026, 8, 6)];
|
||||
let symbol = "920038.BJ";
|
||||
let data = single_symbol_platform_data(&dates, symbol);
|
||||
let portfolio = PortfolioState::new(100_000.0);
|
||||
let subscriptions = BTreeSet::new();
|
||||
let ctx = StrategyContext {
|
||||
execution_date: dates[1], decision_date: dates[1], decision_index: 1, 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::generic();
|
||||
cfg.signal_symbol = symbol.into();
|
||||
cfg.stock_filter_expr = "close > 0".into();
|
||||
cfg.hold_until_exit_enabled = true;
|
||||
cfg.target_portfolio_daily_enabled = true;
|
||||
cfg.daily_top_up_enabled = true;
|
||||
cfg.daily_position_target_adjust_enabled = true;
|
||||
cfg.rebalance_existing_positions = true;
|
||||
cfg.risk_config.static_rules.reject_bjse_selection = false;
|
||||
cfg.risk_config.static_rules.reject_bjse_buy = true;
|
||||
let decision = PlatformExprStrategy::new(cfg.clone()).on_day(&ctx).unwrap();
|
||||
assert!(decision.order_intents.is_empty());
|
||||
assert!(decision.risk_decisions.iter().any(|audit| audit.symbol == symbol && audit.stage == "buy_planning" && audit.rule_code == "bjse" && !audit.accepted));
|
||||
cfg.risk_config.static_rules.reject_bjse_buy = false;
|
||||
let allowed = PlatformExprStrategy::new(cfg).on_day(&ctx).unwrap();
|
||||
assert!(!allowed.order_intents.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn daily_pattern_runtime_uses_the_shared_kernel_and_rejects_early_visibility() {
|
||||
let dates=(0..21).map(|n|d(2025,1,1)+chrono::Duration::days(n)).collect::<Vec<_>>();
|
||||
|
||||
@@ -138,6 +138,16 @@ pub struct FidcRiskDecisionAudit {
|
||||
}
|
||||
|
||||
impl FidcRiskDecisionAudit {
|
||||
pub fn rejected_buy_plan(date: NaiveDate, symbol: &str, reason: &str) -> Self {
|
||||
Self {
|
||||
date, symbol: symbol.into(), scope: RiskCheckScope::Buy,
|
||||
stage: "buy_planning".into(), accepted: false,
|
||||
rule_code: reason.into(), reason: reason.into(),
|
||||
config_version: Some("inline_risk_policy".into()), data_epoch: date.to_string(),
|
||||
selection_batch_id: None, order_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rejected_selection(
|
||||
date: NaiveDate,
|
||||
symbol: impl Into<String>,
|
||||
|
||||
@@ -7,3 +7,9 @@
|
||||
整个明确证券范围尚未上市时保留官方日历内现金净值点,不缩短回测范围,不伪造成交或 OHLCV。基准只在首个基线点归一,后续无交易日不反复重置。
|
||||
|
||||
513 项核心测试通过,6 项原有测试忽略。新增验证包含沪深北股票和 ETF 上市前、实际摘牌日、未知证券身份、候选缺失、正式停牌和普通价格缺口、全池上市前现金期间。对单个正式分区的数据缺口仍需数据源修复,不从这些测试外推全市场完整性。
|
||||
|
||||
## 真实边界回放补充
|
||||
|
||||
177 回测 `btr_1789041425783_797911_1`:920038.BJ,2026-08-04 至 08-07。真实上市日08-05,原结果只保留08-05至08-07三个净值点。原因是准备面同时加载基准000300.SH,基准不是交易候选但参与了“全部证券生命周期外”的判定。现在只排除已声明且没有交易候选记录的基准,不按代码或名称猜测指数,也不把真实候选排除;补充真实准备结构的回归后,4日现金区间完整保留。
|
||||
|
||||
该草稿沿用源池 `rejectBjseSelection=false`、`rejectBjseBuy=true`,所以选中北交所但不下单符合其买入政策;原规划阶段没有记录拒绝原因则是审计缺项。新增 `scope=buy, stage=buy_planning` 审计,不伪造订单ID,不把买入否决改写成选股排除。测试验证禁止时无订单且有bjse原因,放开买入政策时正常生成意图。最新核心514项通过、6项原有忽略。
|
||||
|
||||
Reference in New Issue
Block a user