实现类型化订单有效期合同
This commit is contained in:
+346
-35
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user