rustrade_execution/order/request.rs
1use crate::{
2 error::OrderError,
3 margin::MarginEffect,
4 market::MarketSnapshot,
5 order::{
6 OrderEvent, OrderKind, TimeInForce,
7 id::{PositionId, VenueOrderId},
8 state::Cancelled,
9 },
10};
11use derive_more::Constructor;
12use rust_decimal::Decimal;
13use rustrade_instrument::{
14 Side,
15 asset::{AssetIndex, name::AssetNameExchange},
16 exchange::{ExchangeId, ExchangeIndex},
17 instrument::{InstrumentIndex, name::InstrumentNameExchange},
18};
19use serde::{Deserialize, Deserializer, Serialize};
20use std::fmt;
21
22pub type OrderRequestOpen<ExchangeKey = ExchangeIndex, InstrumentKey = InstrumentIndex> =
23 OrderEvent<RequestOpen, ExchangeKey, InstrumentKey>;
24
25pub type OrderRequestCancel<ExchangeKey = ExchangeIndex, InstrumentKey = InstrumentIndex> =
26 OrderEvent<RequestCancel, ExchangeKey, InstrumentKey>;
27
28pub type OrderResponseCancel<
29 ExchangeKey = ExchangeIndex,
30 AssetKey = AssetIndex,
31 InstrumentKey = InstrumentIndex,
32> = OrderEvent<Result<Cancelled, OrderError<AssetKey, InstrumentKey>>, ExchangeKey, InstrumentKey>;
33
34pub type UnindexedOrderResponseCancel =
35 OrderResponseCancel<ExchangeId, AssetNameExchange, InstrumentNameExchange>;
36
37/// Parameters for opening a new order.
38///
39/// # Warning: `reduce_only` Default Behavior
40///
41/// The `reduce_only` field defaults to [`ReduceOnly::No`], which means:
42/// - A `Sell` order defaults to `SellToOpen` (open short / write option)
43/// - A `Buy` order defaults to `BuyToOpen` (open long)
44///
45/// **For closing positions, callers MUST explicitly set `reduce_only` to
46/// [`ReduceOnly::Position`]** (or [`ReduceOnly::PositionAndFreeBase`]).
47/// Failure to do so on non-crypto instruments (equities, options) will:
48/// - On non-margin accounts: result in a 422 rejection from the exchange
49/// - On margin accounts: silently open a short position instead of closing the long
50///
51/// The `close_open_positions_with_market_orders` helper sets this correctly. A caller building
52/// its own closing request must set it with [`with_reduce_only`](Self::with_reduce_only).
53///
54/// `#[non_exhaustive]`, so a field can be added without breaking a caller. Outside this crate,
55/// build one with [`RequestOpen::new`] and the `with_*` methods, and change a copy by assigning
56/// its public fields.
57#[non_exhaustive]
58#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
59pub struct RequestOpen {
60 pub side: Side,
61 /// Limit price for the order. Required for Limit/StopLimit orders, and for
62 /// TrailingStopFixedLimit orders, whose limit stays fixed while the trigger trails.
63 /// `None` for Market/Stop/TrailingStop orders (which execute at market price when triggered),
64 /// and for TrailingStopLimit orders, whose limit price follows from their stop and
65 /// `limit_offset`. A price given with a Market order anyway is ignored, by the simulated venue
66 /// and by the live clients that accept market orders.
67 pub price: Option<Decimal>,
68 pub quantity: Decimal,
69 pub kind: OrderKind,
70 pub time_in_force: TimeInForce,
71 /// Target `PositionId` for this order in `OmsMode::Hedging`.
72 ///
73 /// For opening orders: the position this fill should open or add to.
74 /// For closing orders: the position this fill should reduce or close.
75 /// In `OmsMode::Netting`, leave as `None` (ignored).
76 #[serde(default)]
77 pub position_id: Option<PositionId>,
78 /// Constrain this order to only reduce existing positions, never open new ones: see
79 /// [`ReduceOnly`].
80 ///
81 /// Used by exchanges that require explicit open/close intent (Alpaca, Interactive
82 /// Brokers, Schwab). Adapters derive venue-specific semantics from
83 /// [`ReduceOnly::is_reduce_only`] + `side`, so both reduce-only values map alike:
84 /// - not reduce-only, Buy → BuyToOpen (open long / add to long)
85 /// - not reduce-only, Sell → SellToOpen (open short / write option)
86 /// - reduce-only, Buy → BuyToClose (close short)
87 /// - reduce-only, Sell → SellToClose (close long)
88 ///
89 /// Exchanges that infer intent from positions (Binance, Deribit) may map this to
90 /// their `reduceOnly` parameter or ignore it entirely.
91 ///
92 /// The `rustrade` engine enforces it on fills, whatever the venue did with it: a fill of a
93 /// reduce-only order never grows or flips the engine's position. A fill larger than the
94 /// opposite position is cut so the position lands flat, and the excess is reported rather
95 /// than applied. See [`Trade::holding_move`](crate::trade::Trade::holding_move).
96 ///
97 /// `#[serde(default)]` so requests serialised before this field existed still load, and a
98 /// `true` or `false` serialised before it was a [`ReduceOnly`] loads as
99 /// [`ReduceOnly::Position`] or [`ReduceOnly::No`].
100 #[serde(default)]
101 pub reduce_only: ReduceOnly,
102
103 /// Market state observed at the instant this request was created, if any was sampled.
104 ///
105 /// This is **decision-time provenance**, not an instruction to the venue. It records what the
106 /// sender was looking at when it chose to trade, which is the one price a fill can be measured
107 /// against after the fact — implementation shortfall is the difference between this and the
108 /// price actually obtained.
109 ///
110 /// # Live venues must ignore this field
111 ///
112 /// No live exchange adapter reads it and none should: a venue prices a fill from its own book,
113 /// and an adapter that forwarded these prices would be sending the client's view of the market
114 /// back as an instruction. Every adapter in this crate leaves it untouched.
115 ///
116 /// # A simulated venue fills from it only when it has no book of its own
117 ///
118 /// [`SimulatedVenue`](crate::exchange::mock::SimulatedVenue) reads this **only** in
119 /// [`VenueRegime::RequestPriced`](crate::exchange::mock::venue::VenueRegime::RequestPriced), where it
120 /// has no market feed and this snapshot is its only price source. That is the regime
121 /// [`MockExecution`](crate::client::mock::MockExecution) and
122 /// [`MockExchange`](crate::exchange::mock::MockExchange) use: the request's snapshot is priced
123 /// through the venue's [`FillModel`](crate::fill::FillModel), and without it a market order
124 /// has no limit price to fall back on — the venue ignores `price` on an
125 /// [`OrderKind::Market`] — so the venue can only reject.
126 ///
127 /// A [`MarketDriven`](crate::exchange::mock::venue::VenueRegime::MarketDriven) venue ignores it and
128 /// prices from its own book, like a live one. This field is then decision-time provenance
129 /// alone, and the difference between it and the price obtained is implementation shortfall.
130 ///
131 /// Sampling it at creation rather than at fill time is deliberate. It is the one instant with a
132 /// well-defined position on a simulated timeline *for the sender*: everything the sender had
133 /// seen by then is in it, and nothing it had not seen can be. What makes it safe for a venue to
134 /// hold a book instead is that its driver paces the feed — routing each market event to the
135 /// venue before the client that reacts to it sees the same event, and booking each request at
136 /// the instant it arrives. A venue that sampled a stream it shares with the engine but does not
137 /// pace could fill against prices from arbitrarily far in the future, which is why the regime
138 /// is a property of how the venue is driven rather than a configuration flag.
139 ///
140 /// # `None` versus an empty snapshot
141 ///
142 /// `None` means no snapshot was taken — a live path, or a caller that constructed the request
143 /// directly. `Some` holding a [`MarketSnapshot::is_empty`] snapshot means one was taken and the
144 /// instrument had no price yet, which is what a cold start looks like. A `RequestPriced`
145 /// simulated venue distinguishes the two in its rejection, because the fixes differ.
146 ///
147 /// `#[serde(default)]` so requests serialised before this field existed still load.
148 #[serde(default)]
149 pub market: Option<MarketSnapshot>,
150
151 /// What this order does to a margin account's loans, or `None` to take the client's default.
152 ///
153 /// Only a margin client or a simulated venue with spot margin acts on a borrow or a repay; see
154 /// [`MarginEffect`] for the three values. A client that cannot honour a `Some` refuses the
155 /// order unsent with
156 /// [`OrderError::UnsupportedMarginEffect`]:
157 /// a cash or spot-only account takes `Some(NoSideEffect)` alone, and a client that cannot
158 /// promise it (account-level margin, or implicit borrowing as under Hyperliquid's portfolio
159 /// margin) takes no `Some` at all, `NoSideEffect` included. A caller that sets an effect on
160 /// every order must leave it `None` for those venues.
161 ///
162 /// `#[serde(default)]` so requests serialised before this field existed still load.
163 #[serde(default)]
164 pub margin_effect: Option<MarginEffect>,
165}
166
167impl RequestOpen {
168 /// A request with no [`position_id`](Self::position_id), not
169 /// [`reduce_only`](Self::reduce_only), and no [`market`](Self::market) snapshot. Set those
170 /// with the `with_*` methods.
171 pub fn new(
172 side: Side,
173 price: Option<Decimal>,
174 quantity: Decimal,
175 kind: OrderKind,
176 time_in_force: TimeInForce,
177 ) -> Self {
178 Self {
179 side,
180 price,
181 quantity,
182 kind,
183 time_in_force,
184 position_id: None,
185 reduce_only: ReduceOnly::No,
186 market: None,
187 margin_effect: None,
188 }
189 }
190
191 /// This request with the [`position_id`](Self::position_id) it targets in
192 /// `OmsMode::Hedging`.
193 #[must_use]
194 pub fn with_position_id(self, position_id: PositionId) -> Self {
195 Self {
196 position_id: Some(position_id),
197 ..self
198 }
199 }
200
201 /// This request with its [`reduce_only`](Self::reduce_only) set to `reduce_only`.
202 #[must_use]
203 pub fn with_reduce_only(self, reduce_only: ReduceOnly) -> Self {
204 Self {
205 reduce_only,
206 ..self
207 }
208 }
209
210 /// This request with the [`market`](Self::market) its sender observed when it created it.
211 #[must_use]
212 pub fn with_market(self, market: MarketSnapshot) -> Self {
213 Self {
214 market: Some(market),
215 ..self
216 }
217 }
218
219 /// This request with its [`margin_effect`](Self::margin_effect) set to `margin_effect`,
220 /// instead of the client's default.
221 #[must_use]
222 pub fn with_margin_effect(self, margin_effect: MarginEffect) -> Self {
223 Self {
224 margin_effect: Some(margin_effect),
225 ..self
226 }
227 }
228}
229
230/// Whether an open order may only reduce the position it closes, and whether it may also trade
231/// the account's free base beyond it. Set on [`RequestOpen::reduce_only`].
232///
233/// A venue that takes an open or close intent, or a reduce-only flag, is sent the same for both
234/// reduce-only values: see [`Self::is_reduce_only`]. They differ in how the `rustrade` engine
235/// books a fill larger than the position. It cuts the fill so the position lands flat either way
236/// (see [`Trade::holding_move`](crate::trade::Trade::holding_move)), and then:
237/// - for [`Position`](Self::Position), reads the excess as a mistake: logged at WARN, and
238/// counted as a reduce-only fill cut;
239/// - for [`PositionAndFreeBase`](Self::PositionAndFreeBase) on a spot instrument, reads it as a
240/// trade of free base the order was sized to make, when the fill moved against the position or
241/// the position was flat: logged at INFO, and counted apart. A fill that only grows the
242/// position is read as a mistake, as for [`Position`](Self::Position).
243///
244/// # Serialised form
245/// `"no"`, `"position"` or `"position_and_free_base"`. In a human-readable format such as JSON, a
246/// `false` or `true`, the form before this type replaced a flag, loads as [`No`](Self::No) or
247/// [`Position`](Self::Position). A binary format, which cannot say which of the two it holds,
248/// reads the variant alone, as serde derives it.
249///
250/// `#[non_exhaustive]`, so a value can be added without breaking a caller. An adapter reads
251/// [`Self::is_reduce_only`] rather than matching the values.
252#[non_exhaustive]
253#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
254pub enum ReduceOnly {
255 /// The order may open or grow a position.
256 #[default]
257 No,
258 /// The order only reduces the position it closes. Excess over the position is a mistake.
259 Position,
260 /// The order reduces the position it closes, and may trade base beyond it out of, or into,
261 /// the account's free balance. Meant for spot instruments, spot margin included.
262 ///
263 /// The excess must continue the way the order reduces: past the position, or from flat.
264 /// - A sell above a long, or from flat, sells free base: the base-fee remainder that a spot
265 /// position leaves out (see the `rustrade` engine's `EngineState::free_base`), or base the
266 /// account holds for another reason.
267 /// - A buy above a short, or from flat, buys base the account owes outside the position, as
268 /// one sized to cover a short's loan and its interest in base does.
269 ///
270 /// Size it from the base the account holds, or owes, outside the position, so the excess
271 /// never sells more than the account has. The engine does not check the size: it takes the
272 /// excess as the order was sized. A fill that only grows the position, a buy while long or a
273 /// sell while short, reduces nothing, and is booked as for [`Position`](Self::Position).
274 ///
275 /// A venue that enforces reduce-only itself, rather than ignoring it, may refuse or trim
276 /// the excess. On a derivative, which has no free base, the engine books excess as for
277 /// [`Position`](Self::Position).
278 PositionAndFreeBase,
279}
280
281impl ReduceOnly {
282 /// Whether the order may only reduce a position: anything but [`No`](Self::No). The intent
283 /// or flag an adapter sends the venue.
284 pub const fn is_reduce_only(self) -> bool {
285 !matches!(self, Self::No)
286 }
287}
288
289/// [`ReduceOnly`]'s serialised form: every value is written through it, and read back through it
290/// in a format that is not human-readable. A binary format stores a variant's index, so a value
291/// is only ever added at the end, and never reordered or removed.
292#[derive(Deserialize, Serialize)]
293#[serde(rename = "ReduceOnly", rename_all = "snake_case")]
294enum ReduceOnlyVariant {
295 No,
296 Position,
297 PositionAndFreeBase,
298}
299
300// Matched exhaustively, so a value added to `ReduceOnly` does not build until it is added here.
301impl From<ReduceOnly> for ReduceOnlyVariant {
302 fn from(reduce_only: ReduceOnly) -> Self {
303 match reduce_only {
304 ReduceOnly::No => Self::No,
305 ReduceOnly::Position => Self::Position,
306 ReduceOnly::PositionAndFreeBase => Self::PositionAndFreeBase,
307 }
308 }
309}
310
311impl From<ReduceOnlyVariant> for ReduceOnly {
312 fn from(variant: ReduceOnlyVariant) -> Self {
313 match variant {
314 ReduceOnlyVariant::No => Self::No,
315 ReduceOnlyVariant::Position => Self::Position,
316 ReduceOnlyVariant::PositionAndFreeBase => Self::PositionAndFreeBase,
317 }
318 }
319}
320
321impl Serialize for ReduceOnly {
322 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
323 ReduceOnlyVariant::from(*self).serialize(serializer)
324 }
325}
326
327impl<'de> Deserialize<'de> for ReduceOnly {
328 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
329 struct ReduceOnlyVisitor;
330
331 impl serde::de::Visitor<'_> for ReduceOnlyVisitor {
332 type Value = ReduceOnly;
333
334 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
335 formatter.write_str(r#""no", "position", "position_and_free_base", or a bool"#)
336 }
337
338 fn visit_bool<E: serde::de::Error>(self, reduce_only: bool) -> Result<Self::Value, E> {
339 Ok(if reduce_only {
340 ReduceOnly::Position
341 } else {
342 ReduceOnly::No
343 })
344 }
345
346 fn visit_str<E: serde::de::Error>(self, name: &str) -> Result<Self::Value, E> {
347 match name {
348 "no" => Ok(ReduceOnly::No),
349 "position" => Ok(ReduceOnly::Position),
350 "position_and_free_base" => Ok(ReduceOnly::PositionAndFreeBase),
351 _ => Err(E::unknown_variant(
352 name,
353 &["no", "position", "position_and_free_base"],
354 )),
355 }
356 }
357 }
358
359 // Only a self-describing format can say whether it holds a flag or a name.
360 if deserializer.is_human_readable() {
361 deserializer.deserialize_any(ReduceOnlyVisitor)
362 } else {
363 ReduceOnlyVariant::deserialize(deserializer).map(Self::from)
364 }
365 }
366}
367
368#[derive(
369 Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Default, Deserialize, Serialize, Constructor,
370)]
371pub struct RequestCancel {
372 /// How the venue addresses the order to cancel, when that is known.
373 ///
374 /// Three cases, and a client must distinguish all three:
375 /// - `None` -- the order is still in flight, so the venue has acknowledged nothing and there
376 /// is no identifier to cancel by.
377 /// - `Some(VenueOrderId::Assigned(id))` -- cancel by the venue's identifier.
378 /// - `Some(VenueOrderId::ClientAssigned)` -- the venue accepted the order without assigning an
379 /// identifier; cancel by the `ClientOrderId` in the request's key.
380 ///
381 /// Collapsing the last two into `None` would leave a client unable to tell "nothing to cancel"
382 /// from "cancel by client id".
383 pub id: Option<VenueOrderId>,
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use rust_decimal_macros::dec;
390
391 fn limit() -> RequestOpen {
392 RequestOpen::new(
393 Side::Sell,
394 Some(dec!(101)),
395 dec!(2),
396 OrderKind::Limit,
397 TimeInForce::GoodUntilCancelled { post_only: false },
398 )
399 }
400
401 #[test]
402 fn new_sets_the_given_fields_and_defaults_the_rest() {
403 let request = limit();
404
405 assert_eq!(request.side, Side::Sell);
406 assert_eq!(request.price, Some(dec!(101)));
407 assert_eq!(request.quantity, dec!(2));
408 assert_eq!(request.kind, OrderKind::Limit);
409 assert_eq!(
410 request.time_in_force,
411 TimeInForce::GoodUntilCancelled { post_only: false }
412 );
413 assert_eq!(request.position_id, None);
414 assert_eq!(request.reduce_only, ReduceOnly::No);
415 assert_eq!(request.market, None);
416 assert_eq!(request.margin_effect, None);
417 }
418
419 #[test]
420 #[allow(clippy::unwrap_used)] // Test code: panics are the correct failure mode
421 fn a_request_serialised_without_a_margin_effect_still_loads() {
422 let mut json = serde_json::to_value(limit()).unwrap();
423 json.as_object_mut().unwrap().remove("margin_effect");
424
425 let request: RequestOpen = serde_json::from_value(json).unwrap();
426
427 assert_eq!(request.margin_effect, None);
428 }
429
430 #[test]
431 #[allow(clippy::unwrap_used)] // Test code: panics are the correct failure mode
432 fn reduce_only_round_trips_by_name_and_loads_a_flag() {
433 for (reduce_only, name) in [
434 (ReduceOnly::No, "no"),
435 (ReduceOnly::Position, "position"),
436 (ReduceOnly::PositionAndFreeBase, "position_and_free_base"),
437 ] {
438 let json = serde_json::to_value(limit().with_reduce_only(reduce_only)).unwrap();
439 assert_eq!(json["reduce_only"], name);
440 let loaded: RequestOpen = serde_json::from_value(json).unwrap();
441 assert_eq!(loaded.reduce_only, reduce_only);
442 }
443
444 for (flag, reduce_only) in [(false, ReduceOnly::No), (true, ReduceOnly::Position)] {
445 let mut json = serde_json::to_value(limit()).unwrap();
446 json["reduce_only"] = flag.into();
447 let loaded: RequestOpen = serde_json::from_value(json).unwrap();
448 assert_eq!(loaded.reduce_only, reduce_only);
449 }
450
451 let mut json = serde_json::to_value(limit()).unwrap();
452 json.as_object_mut().unwrap().remove("reduce_only");
453 let loaded: RequestOpen = serde_json::from_value(json).unwrap();
454 assert_eq!(loaded.reduce_only, ReduceOnly::No);
455
456 let mut json = serde_json::to_value(limit()).unwrap();
457 json["reduce_only"] = "sometimes".into();
458 let error = serde_json::from_value::<RequestOpen>(json).unwrap_err();
459 assert!(error.to_string().contains("sometimes"), "{error}");
460
461 // Neither a name nor a flag: the error says what would have been read.
462 let mut json = serde_json::to_value(limit()).unwrap();
463 json["reduce_only"] = 1.into();
464 let error = serde_json::from_value::<RequestOpen>(json).unwrap_err();
465 assert!(
466 error
467 .to_string()
468 .contains(r#""no", "position", "position_and_free_base", or a bool"#),
469 "{error}"
470 );
471 }
472
473 /// A deserializer that reads its inner one as a binary format would: not human-readable, so
474 /// it reads a value by the shape asked for, as an enum.
475 struct NotHumanReadable<D>(D);
476
477 impl<'de, D: Deserializer<'de>> Deserializer<'de> for NotHumanReadable<D> {
478 type Error = D::Error;
479
480 fn deserialize_any<V: serde::de::Visitor<'de>>(
481 self,
482 _: V,
483 ) -> Result<V::Value, Self::Error> {
484 Err(serde::de::Error::custom(
485 "a binary format cannot deserialize_any",
486 ))
487 }
488
489 fn deserialize_enum<V: serde::de::Visitor<'de>>(
490 self,
491 name: &'static str,
492 variants: &'static [&'static str],
493 visitor: V,
494 ) -> Result<V::Value, Self::Error> {
495 self.0.deserialize_enum(name, variants, visitor)
496 }
497
498 fn is_human_readable(&self) -> bool {
499 false
500 }
501
502 serde::forward_to_deserialize_any! {
503 bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string bytes byte_buf
504 option unit unit_struct newtype_struct seq tuple tuple_struct map struct identifier
505 ignored_any
506 }
507 }
508
509 #[test]
510 #[allow(clippy::unwrap_used)] // Test code: panics are the correct failure mode
511 fn a_binary_format_reads_reduce_only_as_a_variant() {
512 for reduce_only in [
513 ReduceOnly::No,
514 ReduceOnly::Position,
515 ReduceOnly::PositionAndFreeBase,
516 ] {
517 let written = serde_json::to_value(reduce_only).unwrap();
518 assert_eq!(
519 ReduceOnly::deserialize(NotHumanReadable(&written)).unwrap(),
520 reduce_only
521 );
522 }
523 // A flag has no variant to read.
524 assert!(ReduceOnly::deserialize(NotHumanReadable(&serde_json::Value::Bool(true))).is_err());
525 }
526
527 /// A binary format stores a variant's index, not its name, so the indices are pinned: a
528 /// reordered value would read stored data back as another.
529 #[test]
530 #[allow(clippy::unwrap_used)] // Test code: panics are the correct failure mode
531 fn a_binary_format_reads_reduce_only_by_its_variant_index() {
532 use serde::de::value::{Error, U32Deserializer};
533
534 let read =
535 |index| ReduceOnly::deserialize(NotHumanReadable(U32Deserializer::<Error>::new(index)));
536 assert_eq!(read(0).unwrap(), ReduceOnly::No);
537 assert_eq!(read(1).unwrap(), ReduceOnly::Position);
538 assert_eq!(read(2).unwrap(), ReduceOnly::PositionAndFreeBase);
539 assert!(read(3).is_err());
540 }
541
542 #[test]
543 fn only_no_is_not_reduce_only() {
544 assert!(!ReduceOnly::No.is_reduce_only());
545 assert!(ReduceOnly::Position.is_reduce_only());
546 assert!(ReduceOnly::PositionAndFreeBase.is_reduce_only());
547 }
548
549 #[test]
550 fn with_methods_set_only_their_own_field() {
551 // Every optional field away from its default, so a builder that reset another field
552 // would show.
553 let set = RequestOpen {
554 position_id: Some(PositionId::new("a")),
555 reduce_only: ReduceOnly::Position,
556 market: Some(MarketSnapshot::from_last_price(Some(dec!(100)))),
557 margin_effect: Some(MarginEffect::Borrow),
558 ..limit()
559 };
560
561 assert_eq!(
562 set.clone().with_position_id(PositionId::new("b")),
563 RequestOpen {
564 position_id: Some(PositionId::new("b")),
565 ..set.clone()
566 }
567 );
568 assert_eq!(
569 set.clone()
570 .with_reduce_only(ReduceOnly::PositionAndFreeBase),
571 RequestOpen {
572 reduce_only: ReduceOnly::PositionAndFreeBase,
573 ..set.clone()
574 }
575 );
576 let market = MarketSnapshot::from_last_price(Some(dec!(200)));
577 assert_eq!(
578 set.clone().with_market(market),
579 RequestOpen {
580 market: Some(market),
581 ..set.clone()
582 }
583 );
584 assert_eq!(
585 set.clone().with_margin_effect(MarginEffect::Repay),
586 RequestOpen {
587 margin_effect: Some(MarginEffect::Repay),
588 ..set
589 }
590 );
591
592 // And each sets its field from the default.
593 assert_eq!(
594 limit().with_position_id(PositionId::NETTING).position_id,
595 Some(PositionId::NETTING)
596 );
597 assert_eq!(
598 limit().with_reduce_only(ReduceOnly::Position).reduce_only,
599 ReduceOnly::Position
600 );
601 assert_eq!(limit().with_market(market).market, Some(market));
602 assert_eq!(
603 limit()
604 .with_margin_effect(MarginEffect::Repay)
605 .margin_effect,
606 Some(MarginEffect::Repay)
607 );
608 }
609}