chore: 更新 fidc-backtest-engine - 2026-05-08
This commit is contained in:
@@ -110,6 +110,7 @@ pub struct BrokerSimulator<C, R> {
|
||||
volume_limit: bool,
|
||||
inactive_limit: bool,
|
||||
liquidity_limit: bool,
|
||||
strict_value_budget: bool,
|
||||
intraday_execution_start_time: Option<NaiveTime>,
|
||||
runtime_intraday_start_time: Cell<Option<NaiveTime>>,
|
||||
runtime_intraday_end_time: Cell<Option<NaiveTime>>,
|
||||
@@ -130,6 +131,7 @@ impl<C, R> BrokerSimulator<C, R> {
|
||||
volume_limit: true,
|
||||
inactive_limit: true,
|
||||
liquidity_limit: true,
|
||||
strict_value_budget: false,
|
||||
intraday_execution_start_time: None,
|
||||
runtime_intraday_start_time: Cell::new(None),
|
||||
runtime_intraday_end_time: Cell::new(None),
|
||||
@@ -154,6 +156,7 @@ impl<C, R> BrokerSimulator<C, R> {
|
||||
volume_limit: true,
|
||||
inactive_limit: true,
|
||||
liquidity_limit: true,
|
||||
strict_value_budget: false,
|
||||
intraday_execution_start_time: None,
|
||||
runtime_intraday_start_time: Cell::new(None),
|
||||
runtime_intraday_end_time: Cell::new(None),
|
||||
@@ -177,6 +180,11 @@ impl<C, R> BrokerSimulator<C, R> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_strict_value_budget(mut self, enabled: bool) -> Self {
|
||||
self.strict_value_budget = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_volume_percent(mut self, volume_percent: f64) -> Self {
|
||||
self.volume_percent = volume_percent;
|
||||
self
|
||||
@@ -3388,6 +3396,16 @@ where
|
||||
requested_qty
|
||||
}
|
||||
|
||||
fn value_budget_gross_limit(&self, value_budget: Option<f64>) -> Option<f64> {
|
||||
value_budget.map(|budget| {
|
||||
if self.strict_value_budget {
|
||||
budget
|
||||
} else {
|
||||
budget + 400.0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn process_buy(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
@@ -3559,7 +3577,7 @@ where
|
||||
execution_cursors,
|
||||
None,
|
||||
Some(portfolio.cash()),
|
||||
value_budget.map(|budget| budget + 400.0),
|
||||
self.value_budget_gross_limit(value_budget),
|
||||
algo_request,
|
||||
limit_price,
|
||||
);
|
||||
@@ -3590,7 +3608,7 @@ where
|
||||
let filled_qty = self.affordable_buy_quantity(
|
||||
date,
|
||||
portfolio.cash(),
|
||||
value_budget.map(|budget| budget + 400.0),
|
||||
self.value_budget_gross_limit(value_budget),
|
||||
execution_price,
|
||||
constrained_qty,
|
||||
self.minimum_order_quantity(data, symbol),
|
||||
@@ -3601,7 +3619,7 @@ where
|
||||
partial_fill_reason,
|
||||
self.buy_reduction_reason(
|
||||
portfolio.cash(),
|
||||
value_budget.map(|budget| budget + 400.0),
|
||||
self.value_budget_gross_limit(value_budget),
|
||||
execution_price,
|
||||
constrained_qty,
|
||||
filled_qty,
|
||||
@@ -3660,7 +3678,7 @@ where
|
||||
side: OrderSide::Buy,
|
||||
requested_quantity: requested_qty,
|
||||
filled_quantity: 0,
|
||||
status: OrderStatus::Rejected,
|
||||
status: zero_fill_status_for_reason(detail),
|
||||
reason: format!("{reason}: {detail}"),
|
||||
});
|
||||
Self::emit_order_process_event(
|
||||
@@ -3670,7 +3688,10 @@ where
|
||||
order_id,
|
||||
symbol,
|
||||
OrderSide::Buy,
|
||||
format!("status=Rejected reason={detail}"),
|
||||
format!(
|
||||
"status={:?} reason={detail}",
|
||||
zero_fill_status_for_reason(detail)
|
||||
),
|
||||
);
|
||||
self.clear_open_order(order_id);
|
||||
return Ok(());
|
||||
@@ -4255,57 +4276,43 @@ where
|
||||
}
|
||||
|
||||
if algo_request.is_some() || self.intraday_execution_start_time.is_some() {
|
||||
let execution_price = self.snapshot_execution_price(snapshot, side);
|
||||
if !self.price_satisfies_limit(
|
||||
side,
|
||||
execution_price,
|
||||
limit_price,
|
||||
snapshot.effective_price_tick(),
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
let execution_price =
|
||||
self.execution_price_with_limit_slippage(execution_price, limit_price);
|
||||
let quantity = match side {
|
||||
OrderSide::Buy => self.affordable_buy_quantity(
|
||||
date,
|
||||
cash_limit.unwrap_or(f64::INFINITY),
|
||||
gross_limit,
|
||||
execution_price,
|
||||
requested_qty,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
),
|
||||
OrderSide::Sell => requested_qty,
|
||||
};
|
||||
if quantity == 0 {
|
||||
return None;
|
||||
}
|
||||
let next_cursor = algo_request
|
||||
.and_then(|request| request.start_time)
|
||||
.or(self.intraday_execution_start_time)
|
||||
.map(|start_time| date.and_time(start_time) + Duration::seconds(1))
|
||||
.unwrap_or_else(|| date.and_hms_opt(0, 0, 1).expect("valid midnight"));
|
||||
return Some(ExecutionFill {
|
||||
quantity,
|
||||
quantity: 0,
|
||||
next_cursor,
|
||||
legs: vec![ExecutionLeg {
|
||||
price: execution_price,
|
||||
quantity,
|
||||
}],
|
||||
unfilled_reason: self.buy_reduction_reason(
|
||||
cash_limit.unwrap_or(f64::INFINITY),
|
||||
gross_limit,
|
||||
execution_price,
|
||||
requested_qty,
|
||||
quantity,
|
||||
),
|
||||
legs: Vec::new(),
|
||||
unfilled_reason: Some(self.empty_intraday_quote_reason(
|
||||
quotes,
|
||||
start_cursor,
|
||||
end_cursor,
|
||||
)),
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn empty_intraday_quote_reason(
|
||||
&self,
|
||||
quotes: &[IntradayExecutionQuote],
|
||||
start_cursor: Option<NaiveDateTime>,
|
||||
end_cursor: Option<NaiveDateTime>,
|
||||
) -> &'static str {
|
||||
let saw_quote_in_window = quotes.iter().any(|quote| {
|
||||
!start_cursor.is_some_and(|cursor| quote.timestamp < cursor)
|
||||
&& !end_cursor.is_some_and(|cursor| quote.timestamp > cursor)
|
||||
});
|
||||
if saw_quote_in_window {
|
||||
"intraday quote liquidity exhausted"
|
||||
} else {
|
||||
"no execution quotes after start"
|
||||
}
|
||||
}
|
||||
|
||||
fn select_execution_fill(
|
||||
&self,
|
||||
snapshot: &crate::data::DailyMarketSnapshot,
|
||||
@@ -4487,7 +4494,10 @@ fn merge_partial_fill_reason(current: Option<String>, next: Option<&str>) -> Opt
|
||||
|
||||
fn zero_fill_status_for_reason(reason: &str) -> OrderStatus {
|
||||
match reason {
|
||||
"tick no volume" | "tick volume limit" => OrderStatus::Canceled,
|
||||
"tick no volume"
|
||||
| "tick volume limit"
|
||||
| "intraday quote liquidity exhausted"
|
||||
| "no execution quotes after start" => OrderStatus::Canceled,
|
||||
_ => OrderStatus::Rejected,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user