实现类型化订单有效期合同

This commit is contained in:
boris
2026-08-26 19:48:21 +08:00
parent bc0f5f6089
commit 8e238f9131
10 changed files with 1025 additions and 73 deletions
+346 -35
View File
@@ -16,7 +16,8 @@ use crate::portfolio::PortfolioState;
use crate::risk_control::{ChinaAShareRiskControl, FidcRiskControlConfig, RiskCheckScope};
use crate::rules::{EquityRuleHooks, RuleCheck};
use crate::strategy::{
AlgoOrderStyle, OpenOrderView, OrderIntent, StrategyDecision, TargetPortfolioOrderPricing,
AlgoOrderStyle, OpenOrderView, OrderIntent, OrderTimeInForce, StrategyDecision,
TargetPortfolioOrderPricing,
};
#[derive(Debug, Default)]
@@ -64,6 +65,9 @@ struct OpenOrder {
filled_quantity: u32,
remaining_quantity: u32,
limit_price: f64,
time_in_force: OrderTimeInForce,
commission_remaining: Option<f64>,
execution_cursor: Option<NaiveDateTime>,
reason: String,
}
@@ -98,6 +102,14 @@ pub enum RebalanceCashMode {
PreOpenCash,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RemainderPolicy {
Cancel,
KeepUntilClose,
KeepUntilCanceled,
FillOrKill,
}
impl Default for RebalanceCashMode {
fn default() -> Self {
Self::SellThenBuy
@@ -205,6 +217,7 @@ pub struct BrokerSimulator<C, R> {
runtime_order_created_date: Cell<Option<NaiveDate>>,
runtime_decision_total_equity: Cell<Option<f64>>,
runtime_target_position_limit: Cell<Option<usize>>,
runtime_time_in_force: Cell<Option<OrderTimeInForce>>,
next_order_id: Cell<u64>,
open_orders: RefCell<Vec<OpenOrder>>,
}
@@ -235,6 +248,7 @@ impl<C, R> BrokerSimulator<C, R> {
runtime_order_created_date: Cell::new(None),
runtime_decision_total_equity: Cell::new(None),
runtime_target_position_limit: Cell::new(None),
runtime_time_in_force: Cell::new(None),
next_order_id: Cell::new(1),
open_orders: RefCell::new(Vec::new()),
}
@@ -269,6 +283,7 @@ impl<C, R> BrokerSimulator<C, R> {
runtime_order_created_date: Cell::new(None),
runtime_decision_total_equity: Cell::new(None),
runtime_target_position_limit: Cell::new(None),
runtime_time_in_force: Cell::new(None),
next_order_id: Cell::new(1),
open_orders: RefCell::new(Vec::new()),
}
@@ -366,6 +381,35 @@ impl<C, R> BrokerSimulator<C, R> {
}
}
fn effective_remainder_policy(&self, allow_pending_limit: bool) -> RemainderPolicy {
match self.runtime_time_in_force.get() {
Some(OrderTimeInForce::Fok) => RemainderPolicy::FillOrKill,
Some(OrderTimeInForce::Gtc) => RemainderPolicy::KeepUntilCanceled,
Some(OrderTimeInForce::Day) if allow_pending_limit => RemainderPolicy::KeepUntilClose,
Some(OrderTimeInForce::Day) => RemainderPolicy::Cancel,
Some(OrderTimeInForce::Ioc) => RemainderPolicy::Cancel,
None if allow_pending_limit => RemainderPolicy::KeepUntilClose,
None => RemainderPolicy::Cancel,
}
}
fn pending_time_in_force(remainder_policy: RemainderPolicy) -> OrderTimeInForce {
match remainder_policy {
RemainderPolicy::KeepUntilClose => OrderTimeInForce::Day,
RemainderPolicy::KeepUntilCanceled => OrderTimeInForce::Gtc,
RemainderPolicy::Cancel | RemainderPolicy::FillOrKill => {
unreachable!("non-pending policy cannot create an open order")
}
}
}
fn keeps_remainder_open(remainder_policy: RemainderPolicy) -> bool {
matches!(
remainder_policy,
RemainderPolicy::KeepUntilClose | RemainderPolicy::KeepUntilCanceled
)
}
pub fn execution_price_field(&self) -> PriceField {
self.execution_price_field
}
@@ -507,7 +551,7 @@ where
}
};
match intent {
match intent.unwrapped() {
OrderIntent::Shares { quantity, .. } | OrderIntent::LimitShares { quantity, .. } => {
Some(if *quantity < 0 {
OrderSide::Sell
@@ -596,7 +640,7 @@ where
}
fn target_position_intent(intent: &OrderIntent) -> Option<(&str, bool)> {
match intent {
match intent.unwrapped() {
OrderIntent::TargetShares {
symbol,
target_quantity,
@@ -1248,7 +1292,39 @@ where
commission_state: &mut BTreeMap<u64, f64>,
report: &mut BrokerExecutionReport,
) -> Result<(), BacktestError> {
if let OrderIntent::WithTimeInForce {
intent: wrapped,
time_in_force,
} = intent
{
if self.runtime_time_in_force.get().is_some() {
return Err(BacktestError::Execution(
"nested time-in-force wrappers are not allowed".to_string(),
));
}
if !wrapped.supports_time_in_force(*time_in_force) {
return Err(BacktestError::Execution(format!(
"time_in_force={} is not supported for this order intent",
time_in_force.as_str()
)));
}
let previous = self.runtime_time_in_force.replace(Some(*time_in_force));
let result = self.process_order_intent(
date,
portfolio,
data,
wrapped,
intraday_turnover,
execution_cursors,
global_execution_cursor,
commission_state,
report,
);
self.runtime_time_in_force.set(previous);
return result;
}
match intent {
OrderIntent::WithTimeInForce { .. } => unreachable!("wrapper handled above"),
OrderIntent::Shares {
symbol,
quantity,
@@ -1883,6 +1959,47 @@ where
.retain(|existing| existing.order_id != order_id);
}
fn emit_fill_or_kill_canceled(
report: &mut BrokerExecutionReport,
date: NaiveDate,
order_id: u64,
symbol: &str,
side: OrderSide,
requested_quantity: u32,
possible_quantity: u32,
reason: &str,
) {
let detail = format!(
"{reason}: FOK not fully fillable requested={requested_quantity} possible={possible_quantity}"
);
report.order_events.push(OrderEvent {
date,
decision_date: None,
order_created_date: None,
execution_date: None,
order_id: Some(order_id),
symbol: symbol.to_string(),
side,
requested_quantity,
filled_quantity: 0,
status: OrderStatus::Canceled,
reason: detail.clone(),
});
Self::emit_order_process_event(
report,
date,
ProcessEventKind::OrderUnsolicitedUpdate,
order_id,
symbol,
side,
format!("status=Canceled reason={detail}"),
);
report.diagnostics.push(format!(
"fok_order_canceled symbol={symbol} side={} requested={requested_quantity} possible={possible_quantity}",
side.as_str()
));
}
fn mark_same_day_sold(&self, date: NaiveDate, symbol: &str) {
self.same_day_sold_symbols
.borrow_mut()
@@ -1954,12 +2071,29 @@ where
for order in pending_orders {
let order_event_start = report.order_events.len();
let fill_event_start = report.fill_events.len();
if let Some(commission_remaining) = order.commission_remaining {
commission_state.insert(order.order_id, commission_remaining);
}
if let Some(cursor) = order.execution_cursor {
execution_cursors
.entry(order.symbol.clone())
.and_modify(|existing| *existing = (*existing).max(cursor))
.or_insert(cursor);
if self.uses_serial_execution_cursor(&order.reason)
&& global_execution_cursor.is_none_or(|existing| cursor > existing)
{
*global_execution_cursor = Some(cursor);
}
}
let signed_quantity = if order.side == OrderSide::Buy {
order.remaining_quantity as i32
} else {
-(order.remaining_quantity as i32)
};
self.process_limit_shares_internal(
let previous_time_in_force = self
.runtime_time_in_force
.replace(Some(order.time_in_force));
let execution_result = self.process_limit_shares_internal(
date,
portfolio,
data,
@@ -1974,7 +2108,80 @@ where
global_execution_cursor,
commission_state,
report,
)?;
);
self.runtime_time_in_force.set(previous_time_in_force);
execution_result?;
let attempt_filled = report.fill_events[fill_event_start..]
.iter()
.filter(|fill| fill.order_id == Some(order.order_id))
.map(|fill| fill.quantity)
.sum::<u32>();
let cumulative_filled = order.filled_quantity.saturating_add(attempt_filled);
let remaining_quantity = order.requested_quantity.saturating_sub(cumulative_filled);
let mut remains_open = false;
{
let mut open_orders = self.open_orders.borrow_mut();
if let Some(reopened) = open_orders
.iter_mut()
.find(|reopened| reopened.order_id == order.order_id)
{
remains_open = remaining_quantity > 0;
reopened.decision_date = order.decision_date;
reopened.order_created_date = order.order_created_date;
reopened.requested_quantity = order.requested_quantity;
reopened.filled_quantity = cumulative_filled;
reopened.remaining_quantity = remaining_quantity;
reopened.time_in_force = order.time_in_force;
reopened.commission_remaining = commission_state.get(&order.order_id).copied();
reopened.execution_cursor = execution_cursors.get(&order.symbol).copied();
}
if !remains_open {
open_orders.retain(|open| open.order_id != order.order_id);
}
}
if report.order_events.len() == order_event_start && !remains_open {
report.order_events.push(OrderEvent {
date,
decision_date: order.decision_date,
order_created_date: order.order_created_date,
execution_date: Some(date),
order_id: Some(order.order_id),
symbol: order.symbol.clone(),
side: order.side,
requested_quantity: order.requested_quantity,
filled_quantity: cumulative_filled,
status: OrderStatus::Canceled,
reason: format!(
"{}: open order remainder canceled because no executable position remained",
order.reason
),
});
Self::emit_order_process_event(
report,
date,
ProcessEventKind::OrderUnsolicitedUpdate,
order.order_id,
&order.symbol,
order.side,
"status=Canceled reason=no executable position remained",
);
}
for event in &mut report.order_events[order_event_start..] {
if event.order_id != Some(order.order_id) {
continue;
}
event.requested_quantity = order.requested_quantity;
event.filled_quantity = cumulative_filled;
if remains_open {
event.status = if cumulative_filled == 0 {
OrderStatus::Pending
} else {
OrderStatus::PartiallyFilled
};
} else if cumulative_filled > 0 && event.status == OrderStatus::Rejected {
event.status = OrderStatus::Canceled;
}
}
Self::annotate_report_range(
report,
order_event_start,
@@ -2130,9 +2337,13 @@ where
std::mem::take(&mut *open_orders)
};
for order in pending {
if order.time_in_force == OrderTimeInForce::Gtc {
self.upsert_open_order(order);
continue;
}
let market_close_reason = format!(
"Order Rejected: {} can not match. Market close.",
order.symbol
"DAY order expired at market close: {} remaining_quantity={}",
order.symbol, order.remaining_quantity
);
report.order_events.push(OrderEvent {
date,
@@ -2144,7 +2355,7 @@ where
side: order.side,
requested_quantity: order.requested_quantity,
filled_quantity: order.filled_quantity,
status: OrderStatus::Rejected,
status: OrderStatus::Expired,
reason: market_close_reason.clone(),
});
Self::emit_order_process_event(
@@ -2155,7 +2366,7 @@ where
&order.symbol,
order.side,
format!(
"status=Rejected requested_quantity={} filled_quantity={} reason={market_close_reason}",
"status=Expired requested_quantity={} filled_quantity={} reason={market_close_reason}",
order.requested_quantity, order.filled_quantity
),
);
@@ -3339,6 +3550,7 @@ where
algo_request: Option<&AlgoExecutionRequest>,
report: &mut BrokerExecutionReport,
) -> Result<(), BacktestError> {
let remainder_policy = self.effective_remainder_policy(allow_pending_limit);
let Some(position) = portfolio.position(symbol) else {
return Ok(());
};
@@ -3479,7 +3691,20 @@ where
quantity
}
Err(limit_reason) => {
if allow_pending_limit {
if remainder_policy == RemainderPolicy::FillOrKill {
Self::emit_fill_or_kill_canceled(
report,
date,
order_id,
symbol,
OrderSide::Sell,
requested_qty,
0,
reason,
);
return Ok(());
}
if Self::keeps_remainder_open(remainder_policy) {
self.upsert_open_order(OpenOrder {
order_id,
decision_date: Some(self.current_decision_date(date)),
@@ -3490,6 +3715,9 @@ where
filled_quantity: 0,
remaining_quantity: requested_qty,
limit_price: limit_price.expect("limit price for pending limit sell"),
time_in_force: Self::pending_time_in_force(remainder_policy),
commission_remaining: commission_state.get(&order_id).copied(),
execution_cursor: execution_cursors.get(symbol).copied(),
reason: reason.to_string(),
});
report.order_events.push(OrderEvent {
@@ -3545,7 +3773,20 @@ where
}
};
if fillable_qty == 0 {
if allow_pending_limit {
if remainder_policy == RemainderPolicy::FillOrKill {
Self::emit_fill_or_kill_canceled(
report,
date,
order_id,
symbol,
OrderSide::Sell,
requested_qty,
0,
reason,
);
return Ok(());
}
if Self::keeps_remainder_open(remainder_policy) {
let detail = partial_fill_reason
.as_deref()
.unwrap_or("no sellable quantity");
@@ -3559,6 +3800,9 @@ where
filled_quantity: 0,
remaining_quantity: requested_qty,
limit_price: limit_price.expect("limit price for pending limit sell"),
time_in_force: Self::pending_time_in_force(remainder_policy),
commission_remaining: commission_state.get(&order_id).copied(),
execution_cursor: execution_cursors.get(symbol).copied(),
reason: reason.to_string(),
});
report.order_events.push(OrderEvent {
@@ -3628,14 +3872,10 @@ where
algo_request,
limit_price,
);
let (filled_qty, execution_legs) = if let Some(fill) = fill {
execution_cursors.insert(symbol.to_string(), fill.next_cursor);
if self.uses_serial_execution_cursor(reason) {
*global_execution_cursor = Some(fill.next_cursor);
}
let (filled_qty, execution_legs, next_cursor) = if let Some(fill) = fill {
partial_fill_reason =
merge_partial_fill_reason(partial_fill_reason, fill.unfilled_reason);
(fill.quantity, fill.legs)
(fill.quantity, fill.legs, Some(fill.next_cursor))
} else {
let execution_price =
self.snapshot_execution_price(snapshot, OrderSide::Sell, Some(fillable_qty));
@@ -3643,7 +3883,7 @@ where
self.execution_limit_rejection_reason(snapshot, OrderSide::Sell, execution_price)
{
partial_fill_reason = merge_partial_fill_reason(partial_fill_reason, Some(reason));
(0, Vec::new())
(0, Vec::new(), None)
} else if !self.price_satisfies_limit(
OrderSide::Sell,
execution_price,
@@ -3654,7 +3894,7 @@ where
partial_fill_reason,
Some("limit price not marketable yet"),
);
(0, Vec::new())
(0, Vec::new(), None)
} else {
match self.execution_price_with_limit_slippage_or_rejection(
snapshot,
@@ -3669,20 +3909,43 @@ where
mark_price: self.snapshot_mark_price(snapshot, OrderSide::Sell),
quantity: fillable_qty,
}],
None,
),
Err(reason) => {
partial_fill_reason =
merge_partial_fill_reason(partial_fill_reason, Some(reason));
(0, Vec::new())
(0, Vec::new(), None)
}
}
}
};
if remainder_policy == RemainderPolicy::FillOrKill && filled_qty < requested_qty {
self.clear_open_order(order_id);
Self::emit_fill_or_kill_canceled(
report,
date,
order_id,
symbol,
OrderSide::Sell,
requested_qty,
filled_qty,
reason,
);
return Ok(());
}
if let Some(next_cursor) = next_cursor {
execution_cursors.insert(symbol.to_string(), next_cursor);
if self.uses_serial_execution_cursor(reason) {
*global_execution_cursor = Some(next_cursor);
}
}
if filled_qty == 0 {
let detail = partial_fill_reason
.as_deref()
.unwrap_or("limit price not marketable yet");
if allow_pending_limit && Self::limit_order_can_remain_open(Some(detail)) {
if Self::keeps_remainder_open(remainder_policy)
&& Self::limit_order_can_remain_open(Some(detail))
{
self.upsert_open_order(OpenOrder {
order_id,
decision_date: Some(self.current_decision_date(date)),
@@ -3693,6 +3956,9 @@ where
filled_quantity: 0,
remaining_quantity: requested_qty,
limit_price: limit_price.expect("limit price for pending limit sell"),
time_in_force: Self::pending_time_in_force(remainder_policy),
commission_remaining: commission_state.get(&order_id).copied(),
execution_cursor: execution_cursors.get(symbol).copied(),
reason: reason.to_string(),
});
report.order_events.push(OrderEvent {
@@ -3838,7 +4104,7 @@ where
*intraday_turnover.entry(symbol.to_string()).or_default() += filled_qty;
let remaining_qty = requested_qty.saturating_sub(filled_qty);
let keep_open = allow_pending_limit
let keep_open = Self::keeps_remainder_open(remainder_policy)
&& remaining_qty > 0
&& Self::limit_order_can_remain_open(partial_fill_reason.as_deref());
if keep_open {
@@ -3852,6 +4118,9 @@ where
filled_quantity: filled_qty,
remaining_quantity: remaining_qty,
limit_price: limit_price.expect("limit price for pending limit sell"),
time_in_force: Self::pending_time_in_force(remainder_policy),
commission_remaining: commission_state.get(&order_id).copied(),
execution_cursor: execution_cursors.get(symbol).copied(),
reason: reason.to_string(),
});
} else {
@@ -4979,6 +5248,7 @@ where
algo_request: Option<&AlgoExecutionRequest>,
report: &mut BrokerExecutionReport,
) -> Result<(), BacktestError> {
let remainder_policy = self.effective_remainder_policy(allow_pending_limit);
if portfolio
.position(symbol)
.is_none_or(|position| position.quantity == 0)
@@ -5130,7 +5400,20 @@ where
quantity
}
Err(limit_reason) => {
if allow_pending_limit {
if remainder_policy == RemainderPolicy::FillOrKill {
Self::emit_fill_or_kill_canceled(
report,
date,
order_id,
symbol,
OrderSide::Buy,
requested_qty,
0,
reason,
);
return Ok(());
}
if Self::keeps_remainder_open(remainder_policy) {
self.upsert_open_order(OpenOrder {
order_id,
decision_date: Some(self.current_decision_date(date)),
@@ -5141,6 +5424,9 @@ where
filled_quantity: 0,
remaining_quantity: requested_qty,
limit_price: limit_price.expect("limit price for pending limit buy"),
time_in_force: Self::pending_time_in_force(remainder_policy),
commission_remaining: commission_state.get(&order_id).copied(),
execution_cursor: execution_cursors.get(symbol).copied(),
reason: reason.to_string(),
});
report.order_events.push(OrderEvent {
@@ -5223,14 +5509,10 @@ where
algo_request,
limit_price,
);
let (filled_qty, execution_legs) = if let Some(fill) = fill {
execution_cursors.insert(symbol.to_string(), fill.next_cursor);
if self.uses_serial_execution_cursor(reason) {
*global_execution_cursor = Some(fill.next_cursor);
}
let (filled_qty, execution_legs, next_cursor) = if let Some(fill) = fill {
partial_fill_reason =
merge_partial_fill_reason(partial_fill_reason, fill.unfilled_reason);
(fill.quantity, fill.legs)
(fill.quantity, fill.legs, Some(fill.next_cursor))
} else {
let execution_price =
self.snapshot_execution_price(snapshot, OrderSide::Buy, Some(constrained_qty));
@@ -5238,7 +5520,7 @@ where
self.execution_limit_rejection_reason(snapshot, OrderSide::Buy, execution_price)
{
partial_fill_reason = merge_partial_fill_reason(partial_fill_reason, Some(reason));
(0, Vec::new())
(0, Vec::new(), None)
} else if !self.price_satisfies_limit(
OrderSide::Buy,
execution_price,
@@ -5249,7 +5531,7 @@ where
partial_fill_reason,
Some("limit price not marketable yet"),
);
(0, Vec::new())
(0, Vec::new(), None)
} else {
match self.execution_price_with_limit_slippage_or_rejection(
snapshot,
@@ -5260,7 +5542,7 @@ where
Err(reason) => {
partial_fill_reason =
merge_partial_fill_reason(partial_fill_reason, Some(reason));
(0, Vec::new())
(0, Vec::new(), None)
}
Ok(mut execution_price) => {
let mut filled_qty = self.affordable_buy_quantity(
@@ -5297,7 +5579,7 @@ where
}
}
if blocked_by_final_price {
(0, Vec::new())
(0, Vec::new(), None)
} else {
if filled_qty < constrained_qty {
partial_fill_reason = merge_partial_fill_reason(
@@ -5318,17 +5600,40 @@ where
mark_price: self.snapshot_mark_price(snapshot, OrderSide::Buy),
quantity: filled_qty,
}],
None,
)
}
}
}
}
};
if remainder_policy == RemainderPolicy::FillOrKill && filled_qty < requested_qty {
self.clear_open_order(order_id);
Self::emit_fill_or_kill_canceled(
report,
date,
order_id,
symbol,
OrderSide::Buy,
requested_qty,
filled_qty,
reason,
);
return Ok(());
}
if let Some(next_cursor) = next_cursor {
execution_cursors.insert(symbol.to_string(), next_cursor);
if self.uses_serial_execution_cursor(reason) {
*global_execution_cursor = Some(next_cursor);
}
}
if filled_qty == 0 {
let detail = partial_fill_reason
.as_deref()
.unwrap_or("insufficient cash after fees");
if allow_pending_limit && Self::limit_order_can_remain_open(Some(detail)) {
if Self::keeps_remainder_open(remainder_policy)
&& Self::limit_order_can_remain_open(Some(detail))
{
self.upsert_open_order(OpenOrder {
order_id,
decision_date: Some(self.current_decision_date(date)),
@@ -5339,6 +5644,9 @@ where
filled_quantity: 0,
remaining_quantity: requested_qty,
limit_price: limit_price.expect("limit price for pending limit buy"),
time_in_force: Self::pending_time_in_force(remainder_policy),
commission_remaining: commission_state.get(&order_id).copied(),
execution_cursor: execution_cursors.get(symbol).copied(),
reason: reason.to_string(),
});
report.order_events.push(OrderEvent {
@@ -5486,7 +5794,7 @@ where
*intraday_turnover.entry(symbol.to_string()).or_default() += filled_qty;
let remaining_qty = requested_qty.saturating_sub(filled_qty);
let keep_open = allow_pending_limit
let keep_open = Self::keeps_remainder_open(remainder_policy)
&& remaining_qty > 0
&& Self::limit_order_can_remain_open(partial_fill_reason.as_deref());
if keep_open {
@@ -5500,6 +5808,9 @@ where
filled_quantity: filled_qty,
remaining_quantity: remaining_qty,
limit_price: limit_price.expect("limit price for pending limit buy"),
time_in_force: Self::pending_time_in_force(remainder_policy),
commission_remaining: commission_state.get(&order_id).copied(),
execution_cursor: execution_cursors.get(symbol).copied(),
reason: reason.to_string(),
});
} else {