Skip to main content

rustrade_execution/order/
request.rs

1use crate::{
2    error::OrderError,
3    market::MarketSnapshot,
4    order::{
5        OrderEvent, OrderKind, TimeInForce,
6        id::{PositionId, VenueOrderId},
7        state::Cancelled,
8    },
9};
10use derive_more::Constructor;
11use rust_decimal::Decimal;
12use rustrade_instrument::{
13    Side,
14    asset::{AssetIndex, name::AssetNameExchange},
15    exchange::{ExchangeId, ExchangeIndex},
16    instrument::{InstrumentIndex, name::InstrumentNameExchange},
17};
18use serde::{Deserialize, Serialize};
19
20pub type OrderRequestOpen<ExchangeKey = ExchangeIndex, InstrumentKey = InstrumentIndex> =
21    OrderEvent<RequestOpen, ExchangeKey, InstrumentKey>;
22
23pub type OrderRequestCancel<ExchangeKey = ExchangeIndex, InstrumentKey = InstrumentIndex> =
24    OrderEvent<RequestCancel, ExchangeKey, InstrumentKey>;
25
26pub type OrderResponseCancel<
27    ExchangeKey = ExchangeIndex,
28    AssetKey = AssetIndex,
29    InstrumentKey = InstrumentIndex,
30> = OrderEvent<Result<Cancelled, OrderError<AssetKey, InstrumentKey>>, ExchangeKey, InstrumentKey>;
31
32pub type UnindexedOrderResponseCancel =
33    OrderResponseCancel<ExchangeId, AssetNameExchange, InstrumentNameExchange>;
34
35/// Parameters for opening a new order.
36///
37/// # Warning: `reduce_only` Default Behavior
38///
39/// The `reduce_only` field defaults to `false`, which means:
40/// - A `Sell` order defaults to `SellToOpen` (open short / write option)
41/// - A `Buy` order defaults to `BuyToOpen` (open long)
42///
43/// **For closing positions, callers MUST explicitly set `reduce_only: true`.**
44/// Failure to do so on non-crypto instruments (equities, options) will:
45/// - On non-margin accounts: result in a 422 rejection from the exchange
46/// - On margin accounts: silently open a short position instead of closing the long
47///
48/// The `close_open_positions_with_market_orders`
49/// helper sets this correctly. Direct `RequestOpen` construction must handle it manually.
50#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
51pub struct RequestOpen {
52    pub side: Side,
53    /// Limit price for the order. Required for Limit/StopLimit orders, and for
54    /// TrailingStopFixedLimit orders, whose limit stays fixed while the trigger trails.
55    /// `None` for Market/Stop/TrailingStop orders (which execute at market price when triggered),
56    /// and for TrailingStopLimit orders, whose limit price follows from their stop and
57    /// `limit_offset`.
58    pub price: Option<Decimal>,
59    pub quantity: Decimal,
60    pub kind: OrderKind,
61    pub time_in_force: TimeInForce,
62    /// Target `PositionId` for this order in `OmsMode::Hedging`.
63    ///
64    /// For opening orders: the position this fill should open or add to.
65    /// For closing orders: the position this fill should reduce or close.
66    /// In `OmsMode::Netting`, leave as `None` (ignored).
67    #[serde(default)]
68    pub position_id: Option<PositionId>,
69    /// Constrain this order to only reduce existing positions, never open new ones.
70    ///
71    /// Used by exchanges that require explicit open/close intent (Alpaca, Interactive
72    /// Brokers, Schwab). Adapters derive venue-specific semantics from this flag + `side`:
73    /// - `reduce_only=false, Buy`  → BuyToOpen (open long / add to long)
74    /// - `reduce_only=false, Sell` → SellToOpen (open short / write option)
75    /// - `reduce_only=true, Buy`   → BuyToClose (close short)
76    /// - `reduce_only=true, Sell`  → SellToClose (close long)
77    ///
78    /// Exchanges that infer intent from positions (Binance, Deribit) may map this to
79    /// their `reduceOnly` parameter or ignore it entirely.
80    #[serde(default)]
81    pub reduce_only: bool,
82
83    /// Market state observed at the instant this request was created, if any was sampled.
84    ///
85    /// This is **decision-time provenance**, not an instruction to the venue. It records what the
86    /// sender was looking at when it chose to trade, which is the one price a fill can be measured
87    /// against after the fact — implementation shortfall is the difference between this and the
88    /// price actually obtained.
89    ///
90    /// # Live venues must ignore this field
91    ///
92    /// No live exchange adapter reads it and none should: a venue prices a fill from its own book,
93    /// and an adapter that forwarded these prices would be sending the client's view of the market
94    /// back as an instruction. Every adapter in this crate leaves it untouched.
95    ///
96    /// # A simulated venue fills from it only when it has no book of its own
97    ///
98    /// [`SimulatedVenue`](crate::exchange::mock::SimulatedVenue) reads this **only** in
99    /// [`VenueRegime::RequestPriced`](crate::exchange::mock::venue::VenueRegime::RequestPriced), where it
100    /// has no market feed and this snapshot is its only price source. That is the regime
101    /// [`MockExecution`](crate::client::mock::MockExecution) and
102    /// [`MockExchange`](crate::exchange::mock::MockExchange) use: the request's snapshot is priced
103    /// through the venue's [`FillModel`](crate::fill::FillModel), and without it a market order
104    /// carries no price at all — `price` is `None` by construction for [`OrderKind::Market`] — so
105    /// the venue can only reject.
106    ///
107    /// A [`MarketDriven`](crate::exchange::mock::venue::VenueRegime::MarketDriven) venue ignores it and
108    /// prices from its own book, like a live one. This field is then decision-time provenance
109    /// alone, and the difference between it and the price obtained is implementation shortfall.
110    ///
111    /// Sampling it at creation rather than at fill time is deliberate. It is the one instant with a
112    /// well-defined position on a simulated timeline *for the sender*: everything the sender had
113    /// seen by then is in it, and nothing it had not seen can be. What makes it safe for a venue to
114    /// hold a book instead is that its driver paces the feed — routing each market event to the
115    /// venue before the client that reacts to it sees the same event, and booking each request at
116    /// the instant it arrives. A venue that sampled a stream it shares with the engine but does not
117    /// pace could fill against prices from arbitrarily far in the future, which is why the regime
118    /// is a property of how the venue is driven rather than a configuration flag.
119    ///
120    /// # `None` versus an empty snapshot
121    ///
122    /// `None` means no snapshot was taken — a live path, or a caller that constructed the request
123    /// directly. `Some` holding a [`MarketSnapshot::is_empty`] snapshot means one was taken and the
124    /// instrument had no price yet, which is what a cold start looks like. A `RequestPriced`
125    /// simulated venue distinguishes the two in its rejection, because the fixes differ.
126    ///
127    /// `#[serde(default)]` so requests serialised before this field existed still load.
128    #[serde(default)]
129    pub market: Option<MarketSnapshot>,
130}
131
132#[derive(
133    Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Default, Deserialize, Serialize, Constructor,
134)]
135pub struct RequestCancel {
136    /// How the venue addresses the order to cancel, when that is known.
137    ///
138    /// Three cases, and a client must distinguish all three:
139    /// - `None` -- the order is still in flight, so the venue has acknowledged nothing and there
140    ///   is no identifier to cancel by.
141    /// - `Some(VenueOrderId::Assigned(id))` -- cancel by the venue's identifier.
142    /// - `Some(VenueOrderId::ClientAssigned)` -- the venue accepted the order without assigning an
143    ///   identifier; cancel by the `ClientOrderId` in the request's key.
144    ///
145    /// Collapsing the last two into `None` would leave a client unable to tell "nothing to cancel"
146    /// from "cancel by client id".
147    pub id: Option<VenueOrderId>,
148}