Compare commits

...

1 Commits

Author SHA1 Message Date
boris 1dc1e36aa8 修复FIDC顺延调仓目标丢失 2026-10-10 13:33:53 -07:00
2 changed files with 27 additions and 0 deletions
@@ -787,10 +787,12 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
if let Some(denial) = denial {
report.diagnostics.push(format!("etf_daily_open_fallback:protected symbol={} date={date} reason={denial}", target.symbol));
etf_activity(&mut report,date,&target.symbol,target.side,format!("ETF 顺延目标受持有保护限制:{denial};未提交委托。"));
self.deferred_etf_targets.borrow_mut().defer_to_next_session(target, date, &dates);
continue;
}
if target.side == pool::OrderSide::Buy && before_quantity == 0 && Self::positive_position_count(portfolio) >= target.max_positions {
report.diagnostics.push(format!("etf_daily_open_fallback:blocked symbol={} reason=occupied_position_slots", target.symbol));
self.deferred_etf_targets.borrow_mut().defer_to_next_session(target, date, &dates);
continue;
}
let value = target.target_value.to_f64().ok_or_else(|| BacktestError::Execution("etf_daily_open_fallback: target value out of range".into()))?;
+25
View File
@@ -81,6 +81,15 @@ impl DeferredEtfTargets {
*existing = row;
} else { self.rows.push(row); }
}
/// Keep a due target when execution was intentionally deferred by a
/// transient portfolio condition (for example an occupied slot or a
/// holding-protection window). The next official session is the only
/// valid retry clock; a natural-day retry would create a second decision
/// for the same market session.
pub fn defer_to_next_session(&mut self, mut row: DeferredEtfTarget, date: NaiveDate, dates: &[NaiveDate]) {
row.execute_on = dates.iter().copied().find(|candidate| *candidate > date);
self.upsert(row);
}
pub fn take_due(&mut self, date: NaiveDate) -> Vec<DeferredEtfTarget> {
let mut due = Vec::new();
self.rows.retain(|row| {
@@ -126,4 +135,20 @@ mod tests {
assert!(queue.take_due(NaiveDate::from_ymd_opt(2026,2,1).unwrap()).is_empty());
assert_eq!(queue.len(),1);
}
#[test]
fn deferred_target_moves_to_next_official_session_instead_of_being_lost() {
let mut queue=DeferredEtfTargets::default();
queue.replace_generation("pool","v1");
queue.upsert(target("510300.SH",crate::stock_pool_execution::OrderSide::Buy,"v1"));
let date=NaiveDate::from_ymd_opt(2026,1,5).unwrap();
let due=queue.take_due(date);
assert_eq!(due.len(),1);
queue.defer_to_next_session(due.into_iter().next().unwrap(),date,&[
NaiveDate::from_ymd_opt(2026,1,5).unwrap(),
NaiveDate::from_ymd_opt(2026,1,6).unwrap(),
]);
assert!(queue.take_due(date).is_empty());
assert_eq!(queue.take_due(NaiveDate::from_ymd_opt(2026,1,6).unwrap()).len(),1);
}
}