修复FIDC顺延调仓目标丢失
This commit is contained in:
@@ -787,10 +787,12 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
|||||||
if let Some(denial) = denial {
|
if let Some(denial) = denial {
|
||||||
report.diagnostics.push(format!("etf_daily_open_fallback:protected symbol={} date={date} reason={denial}", target.symbol));
|
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};未提交委托。"));
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
if target.side == pool::OrderSide::Buy && before_quantity == 0 && Self::positive_position_count(portfolio) >= target.max_positions {
|
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));
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
let value = target.target_value.to_f64().ok_or_else(|| BacktestError::Execution("etf_daily_open_fallback: target value out of range".into()))?;
|
let value = target.target_value.to_f64().ok_or_else(|| BacktestError::Execution("etf_daily_open_fallback: target value out of range".into()))?;
|
||||||
|
|||||||
@@ -81,6 +81,15 @@ impl DeferredEtfTargets {
|
|||||||
*existing = row;
|
*existing = row;
|
||||||
} else { self.rows.push(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> {
|
pub fn take_due(&mut self, date: NaiveDate) -> Vec<DeferredEtfTarget> {
|
||||||
let mut due = Vec::new();
|
let mut due = Vec::new();
|
||||||
self.rows.retain(|row| {
|
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!(queue.take_due(NaiveDate::from_ymd_opt(2026,2,1).unwrap()).is_empty());
|
||||||
assert_eq!(queue.len(),1);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user