Skip to main content

rithmic_rs/
types.rs

1//! Order enums with serde support and protobuf conversions.
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6use std::fmt;
7use std::str::FromStr;
8
9use crate::{
10    error::RithmicError,
11    rti::{
12        request_account_rms_updates, request_bracket_order, request_cancel_all_orders,
13        request_cancel_order, request_easy_to_borrow_list, request_exit_position,
14        request_modify_order, request_new_order, request_oco_order,
15    },
16};
17
18/// The unit a time bar covers: second, minute, day or week.
19///
20/// An alias for the generated `request_time_bar_replay::BarType`, under a name
21/// that reads better on [`TimeBarReplayRequest`].
22pub use crate::rti::request_time_bar_replay::BarType as TimeBarType;
23
24/// Buy or sell.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
26#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
27#[non_exhaustive]
28pub enum OrderSide {
29    /// Buy side.
30    #[default]
31    Buy,
32    /// Sell side.
33    Sell,
34}
35
36impl OrderSide {
37    /// The protobuf spelling, as `TransactionType::as_str_name` writes it.
38    pub fn as_str_name(&self) -> &'static str {
39        match self {
40            Self::Buy => "BUY",
41            Self::Sell => "SELL",
42        }
43    }
44}
45
46impl fmt::Display for OrderSide {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        f.write_str(self.as_str_name())
49    }
50}
51
52/// Error returned when parsing an invalid [`OrderSide`] string.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct ParseOrderSideError(String);
55
56impl fmt::Display for ParseOrderSideError {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        write!(f, "invalid order side: '{}'", self.0)
59    }
60}
61
62impl std::error::Error for ParseOrderSideError {}
63
64impl FromStr for OrderSide {
65    type Err = ParseOrderSideError;
66
67    fn from_str(s: &str) -> Result<Self, Self::Err> {
68        match s.to_uppercase().as_str() {
69            "BUY" | "B" => Ok(Self::Buy),
70            "SELL" | "S" => Ok(Self::Sell),
71            _ => Err(ParseOrderSideError(s.to_string())),
72        }
73    }
74}
75
76impl From<OrderSide> for request_new_order::TransactionType {
77    fn from(side: OrderSide) -> Self {
78        match side {
79            OrderSide::Buy => Self::Buy,
80            OrderSide::Sell => Self::Sell,
81        }
82    }
83}
84
85impl From<OrderSide> for request_bracket_order::TransactionType {
86    fn from(side: OrderSide) -> Self {
87        match side {
88            OrderSide::Buy => Self::Buy,
89            OrderSide::Sell => Self::Sell,
90        }
91    }
92}
93
94impl From<OrderSide> for request_oco_order::TransactionType {
95    fn from(side: OrderSide) -> Self {
96        match side {
97            OrderSide::Buy => Self::Buy,
98            OrderSide::Sell => Self::Sell,
99        }
100    }
101}
102
103/// Order price type.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
105#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
106#[non_exhaustive]
107pub enum OrderType {
108    /// Market order — executes immediately at the best available price.
109    Market,
110    /// Limit order — executes at the specified price or better.
111    #[default]
112    Limit,
113    /// Stop market order — becomes a market order when the stop price is reached.
114    StopMarket,
115    /// Stop limit order — becomes a limit order when the stop price is reached.
116    StopLimit,
117    /// Market order released when the trigger price is touched.
118    MarketIfTouched,
119    /// Limit order released when the trigger price is touched.
120    LimitIfTouched,
121}
122
123impl OrderType {
124    /// The protobuf spelling, as `PriceType::as_str_name` writes it.
125    pub fn as_str_name(&self) -> &'static str {
126        match self {
127            Self::Market => "MARKET",
128            Self::Limit => "LIMIT",
129            Self::StopMarket => "STOP_MARKET",
130            Self::StopLimit => "STOP_LIMIT",
131            Self::MarketIfTouched => "MARKET_IF_TOUCHED",
132            Self::LimitIfTouched => "LIMIT_IF_TOUCHED",
133        }
134    }
135}
136
137impl fmt::Display for OrderType {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        f.write_str(self.as_str_name())
140    }
141}
142
143/// Error returned when parsing an invalid [`OrderType`] string.
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct ParseOrderTypeError(String);
146
147impl fmt::Display for ParseOrderTypeError {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        write!(f, "invalid order type: '{}'", self.0)
150    }
151}
152
153impl std::error::Error for ParseOrderTypeError {}
154
155impl FromStr for OrderType {
156    type Err = ParseOrderTypeError;
157
158    fn from_str(s: &str) -> Result<Self, Self::Err> {
159        match s.to_uppercase().as_str() {
160            "MARKET" | "MKT" => Ok(Self::Market),
161            "LIMIT" | "LMT" => Ok(Self::Limit),
162            "STOPMARKET" | "STPMKT" | "STOP_MARKET" | "STOP-MARKET" => Ok(Self::StopMarket),
163            "STOPLIMIT" | "STPLMT" | "STOP_LIMIT" | "STOP-LIMIT" => Ok(Self::StopLimit),
164            "MARKETIFTOUCHED" | "MIT" | "MARKET_IF_TOUCHED" | "MARKET-IF-TOUCHED" => {
165                Ok(Self::MarketIfTouched)
166            }
167            "LIMITIFTOUCHED" | "LIT" | "LIMIT_IF_TOUCHED" | "LIMIT-IF-TOUCHED" => {
168                Ok(Self::LimitIfTouched)
169            }
170            _ => Err(ParseOrderTypeError(s.to_string())),
171        }
172    }
173}
174
175impl From<OrderType> for request_new_order::PriceType {
176    fn from(order_type: OrderType) -> Self {
177        match order_type {
178            OrderType::Market => Self::Market,
179            OrderType::Limit => Self::Limit,
180            OrderType::StopMarket => Self::StopMarket,
181            OrderType::StopLimit => Self::StopLimit,
182            OrderType::MarketIfTouched => Self::MarketIfTouched,
183            OrderType::LimitIfTouched => Self::LimitIfTouched,
184        }
185    }
186}
187
188impl From<OrderType> for request_modify_order::PriceType {
189    fn from(order_type: OrderType) -> Self {
190        match order_type {
191            OrderType::Market => Self::Market,
192            OrderType::Limit => Self::Limit,
193            OrderType::StopMarket => Self::StopMarket,
194            OrderType::StopLimit => Self::StopLimit,
195            OrderType::MarketIfTouched => Self::MarketIfTouched,
196            OrderType::LimitIfTouched => Self::LimitIfTouched,
197        }
198    }
199}
200
201impl From<OrderType> for request_bracket_order::PriceType {
202    fn from(order_type: OrderType) -> Self {
203        match order_type {
204            OrderType::Market => Self::Market,
205            OrderType::Limit => Self::Limit,
206            OrderType::StopMarket => Self::StopMarket,
207            OrderType::StopLimit => Self::StopLimit,
208            OrderType::MarketIfTouched => Self::MarketIfTouched,
209            OrderType::LimitIfTouched => Self::LimitIfTouched,
210        }
211    }
212}
213
214/// An OCO leg cannot be if-touched: template 328 has no such price type, so
215/// [`OrderType::MarketIfTouched`] and [`OrderType::LimitIfTouched`] are rejected.
216impl TryFrom<OrderType> for request_oco_order::PriceType {
217    type Error = RithmicError;
218
219    fn try_from(order_type: OrderType) -> Result<Self, Self::Error> {
220        match order_type {
221            OrderType::Market => Ok(Self::Market),
222            OrderType::Limit => Ok(Self::Limit),
223            OrderType::StopMarket => Ok(Self::StopMarket),
224            OrderType::StopLimit => Ok(Self::StopLimit),
225            OrderType::MarketIfTouched | OrderType::LimitIfTouched => {
226                Err(RithmicError::InvalidArgument(format!(
227                    "price_type {} is not available on an OCO leg",
228                    order_type.as_str_name()
229                )))
230            }
231        }
232    }
233}
234
235/// How long an order remains active before expiring.
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
237#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
238#[non_exhaustive]
239pub enum TimeInForce {
240    /// Good for the current trading day only.
241    #[default]
242    Day,
243    /// Good till cancelled.
244    Gtc,
245    /// Immediate or cancel — fill what you can, cancel the rest.
246    Ioc,
247    /// Fill or kill — fill the entire order or cancel it.
248    Fok,
249}
250
251impl TimeInForce {
252    /// The protobuf spelling, as `Duration::as_str_name` writes it.
253    pub fn as_str_name(&self) -> &'static str {
254        match self {
255            Self::Day => "DAY",
256            Self::Gtc => "GTC",
257            Self::Ioc => "IOC",
258            Self::Fok => "FOK",
259        }
260    }
261}
262
263impl fmt::Display for TimeInForce {
264    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265        f.write_str(self.as_str_name())
266    }
267}
268
269/// Error returned when parsing an invalid [`TimeInForce`] string.
270#[derive(Debug, Clone, PartialEq, Eq)]
271pub struct ParseTimeInForceError(String);
272
273impl fmt::Display for ParseTimeInForceError {
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        write!(f, "invalid time-in-force: '{}'", self.0)
276    }
277}
278
279impl std::error::Error for ParseTimeInForceError {}
280
281impl FromStr for TimeInForce {
282    type Err = ParseTimeInForceError;
283
284    fn from_str(s: &str) -> Result<Self, Self::Err> {
285        match s.to_uppercase().as_str() {
286            "DAY" => Ok(Self::Day),
287            "GTC" | "GOODTILLCANCELLED" | "GOOD_TILL_CANCELLED" | "GOOD-TILL-CANCELLED" => {
288                Ok(Self::Gtc)
289            }
290            "IOC" | "IMMEDIATEORCANCEL" | "IMMEDIATE_OR_CANCEL" | "IMMEDIATE-OR-CANCEL" => {
291                Ok(Self::Ioc)
292            }
293            "FOK" | "FILLORKILL" | "FILL_OR_KILL" | "FILL-OR-KILL" => Ok(Self::Fok),
294            _ => Err(ParseTimeInForceError(s.to_string())),
295        }
296    }
297}
298
299impl From<TimeInForce> for request_new_order::Duration {
300    fn from(tif: TimeInForce) -> Self {
301        match tif {
302            TimeInForce::Day => Self::Day,
303            TimeInForce::Gtc => Self::Gtc,
304            TimeInForce::Ioc => Self::Ioc,
305            TimeInForce::Fok => Self::Fok,
306        }
307    }
308}
309
310impl From<TimeInForce> for request_bracket_order::Duration {
311    fn from(tif: TimeInForce) -> Self {
312        match tif {
313            TimeInForce::Day => Self::Day,
314            TimeInForce::Gtc => Self::Gtc,
315            TimeInForce::Ioc => Self::Ioc,
316            TimeInForce::Fok => Self::Fok,
317        }
318    }
319}
320
321impl From<TimeInForce> for request_oco_order::Duration {
322    fn from(tif: TimeInForce) -> Self {
323        match tif {
324            TimeInForce::Day => Self::Day,
325            TimeInForce::Gtc => Self::Gtc,
326            TimeInForce::Ioc => Self::Ioc,
327            TimeInForce::Fok => Self::Fok,
328        }
329    }
330}
331
332/// Whether an order was placed by a human or automatically. Defaults to `Auto`.
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
334#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
335#[non_exhaustive]
336pub enum ManualOrAutoEntry {
337    /// A person placed this.
338    Manual,
339    /// An algorithm placed this.
340    #[default]
341    Auto,
342}
343
344impl ManualOrAutoEntry {
345    /// The protobuf spelling, as `OrderPlacement::as_str_name` writes it.
346    pub fn as_str_name(&self) -> &'static str {
347        match self {
348            Self::Manual => "MANUAL",
349            Self::Auto => "AUTO",
350        }
351    }
352}
353
354impl fmt::Display for ManualOrAutoEntry {
355    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
356        f.write_str(self.as_str_name())
357    }
358}
359
360impl From<ManualOrAutoEntry> for request_new_order::OrderPlacement {
361    fn from(entry: ManualOrAutoEntry) -> Self {
362        match entry {
363            ManualOrAutoEntry::Manual => Self::Manual,
364            ManualOrAutoEntry::Auto => Self::Auto,
365        }
366    }
367}
368
369impl From<ManualOrAutoEntry> for request_bracket_order::OrderPlacement {
370    fn from(entry: ManualOrAutoEntry) -> Self {
371        match entry {
372            ManualOrAutoEntry::Manual => Self::Manual,
373            ManualOrAutoEntry::Auto => Self::Auto,
374        }
375    }
376}
377
378impl From<ManualOrAutoEntry> for request_oco_order::OrderPlacement {
379    fn from(entry: ManualOrAutoEntry) -> Self {
380        match entry {
381            ManualOrAutoEntry::Manual => Self::Manual,
382            ManualOrAutoEntry::Auto => Self::Auto,
383        }
384    }
385}
386
387impl From<ManualOrAutoEntry> for request_modify_order::OrderPlacement {
388    fn from(entry: ManualOrAutoEntry) -> Self {
389        match entry {
390            ManualOrAutoEntry::Manual => Self::Manual,
391            ManualOrAutoEntry::Auto => Self::Auto,
392        }
393    }
394}
395
396impl From<ManualOrAutoEntry> for request_cancel_order::OrderPlacement {
397    fn from(entry: ManualOrAutoEntry) -> Self {
398        match entry {
399            ManualOrAutoEntry::Manual => Self::Manual,
400            ManualOrAutoEntry::Auto => Self::Auto,
401        }
402    }
403}
404
405impl From<ManualOrAutoEntry> for request_cancel_all_orders::OrderPlacement {
406    fn from(entry: ManualOrAutoEntry) -> Self {
407        match entry {
408            ManualOrAutoEntry::Manual => Self::Manual,
409            ManualOrAutoEntry::Auto => Self::Auto,
410        }
411    }
412}
413
414impl From<ManualOrAutoEntry> for request_exit_position::OrderPlacement {
415    fn from(entry: ManualOrAutoEntry) -> Self {
416        match entry {
417            ManualOrAutoEntry::Manual => Self::Manual,
418            ManualOrAutoEntry::Auto => Self::Auto,
419        }
420    }
421}
422
423/// The shape of a bracket order's exit legs.
424///
425/// Rithmic does not document the difference between the plain and `Static`
426/// variants. Our reading is that the `Static` variants hold their tick distances
427/// fixed relative to the entry, while the others let Rithmic manage them.
428#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
429#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
430#[non_exhaustive]
431pub enum BracketType {
432    /// Stop legs only.
433    StopOnly,
434    /// Target legs only.
435    TargetOnly,
436    /// Both target and stop legs.
437    TargetAndStop,
438    /// Stop legs only, at fixed tick distances.
439    StopOnlyStatic,
440    /// Target legs only, at fixed tick distances.
441    TargetOnlyStatic,
442    /// Both target and stop legs, at fixed tick distances.
443    TargetAndStopStatic,
444}
445
446impl BracketType {
447    /// The protobuf spelling, as `BracketType::as_str_name` writes it.
448    pub fn as_str_name(&self) -> &'static str {
449        match self {
450            Self::StopOnly => "STOP_ONLY",
451            Self::TargetOnly => "TARGET_ONLY",
452            Self::TargetAndStop => "TARGET_AND_STOP",
453            Self::StopOnlyStatic => "STOP_ONLY_STATIC",
454            Self::TargetOnlyStatic => "TARGET_ONLY_STATIC",
455            Self::TargetAndStopStatic => "TARGET_AND_STOP_STATIC",
456        }
457    }
458}
459
460impl fmt::Display for BracketType {
461    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
462        f.write_str(self.as_str_name())
463    }
464}
465
466impl From<BracketType> for request_bracket_order::BracketType {
467    fn from(bracket_type: BracketType) -> Self {
468        match bracket_type {
469            BracketType::StopOnly => Self::StopOnly,
470            BracketType::TargetOnly => Self::TargetOnly,
471            BracketType::TargetAndStop => Self::TargetAndStop,
472            BracketType::StopOnlyStatic => Self::StopOnlyStatic,
473            BracketType::TargetOnlyStatic => Self::TargetOnlyStatic,
474            BracketType::TargetAndStopStatic => Self::TargetAndStopStatic,
475        }
476    }
477}
478
479/// The `order_operation_type` of a bracket order, added in template
480/// version 5.37: which event on one order of the bracket cancels the rest.
481///
482/// Rithmic documents only the wire spellings — "AFOCCA, FOCCA, CCA, FCA or
483/// OCA". The reading on each variant is async_rithmic's annotation of the
484/// same field, not Rithmic's own words; Rithmic's C++ SDK declares the same
485/// constants without comment.
486///
487/// Leave [`RithmicBracketOrder::operation_type`] unset unless a specific
488/// grouping is wanted — the server then applies its default, and
489/// async_rithmic reverted sending `OCA` on every bracket after it broke
490/// bracket orders.
491///
492/// [`RithmicBracketOrder::operation_type`]: crate::RithmicBracketOrder::operation_type
493#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
494#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
495#[non_exhaustive]
496pub enum BracketOperationType {
497    /// Sent as `AFOCCA` — read as "all fill or cancel cancels all".
498    Afocca,
499    /// Sent as `FOCCA` — read as "fill or cancel cancels all".
500    Focca,
501    /// Sent as `CCA` — read as "cancel cancels all".
502    Cca,
503    /// Sent as `FCA` — read as "fill cancels all".
504    Fca,
505    /// Sent as `OCA` — read as "one cancels all", the classic OCO grouping.
506    /// The one value Rithmic's C++ SDK declares no constant for.
507    Oca,
508}
509
510impl BracketOperationType {
511    /// The spelling sent on the wire.
512    pub fn as_str_name(&self) -> &'static str {
513        match self {
514            Self::Afocca => "AFOCCA",
515            Self::Focca => "FOCCA",
516            Self::Cca => "CCA",
517            Self::Fca => "FCA",
518            Self::Oca => "OCA",
519        }
520    }
521}
522
523impl fmt::Display for BracketOperationType {
524    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
525        f.write_str(self.as_str_name())
526    }
527}
528
529/// The window of a fill-history request, in the two index formats
530/// template 3512 accepts.
531#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
532#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
533#[non_exhaustive]
534pub enum FillHistoryRange {
535    /// Bounds are seconds since the beginning of the epoch.
536    #[non_exhaustive]
537    Ssboe {
538        /// Start of the window, in seconds since the beginning of the epoch.
539        start: i32,
540        /// End of the window, in seconds since the beginning of the epoch.
541        finish: i32,
542    },
543    /// Bounds are trade dates written as CCYYMMDD, e.g. `20260804`.
544    #[non_exhaustive]
545    TradeDate {
546        /// First trade date of the window, as CCYYMMDD.
547        start: i32,
548        /// Last trade date of the window, as CCYYMMDD.
549        finish: i32,
550    },
551}
552
553impl FillHistoryRange {
554    /// A window bounded in seconds since the beginning of the epoch.
555    pub fn ssboe(start: i32, finish: i32) -> Self {
556        Self::Ssboe { start, finish }
557    }
558
559    /// A window bounded by trade dates written as CCYYMMDD, e.g. `20260804`.
560    pub fn trade_date(start: i32, finish: i32) -> Self {
561        Self::TradeDate { start, finish }
562    }
563
564    /// The `index_format` spelling sent on the wire.
565    pub fn index_format(&self) -> &'static str {
566        match self {
567            Self::Ssboe { .. } => "ssboe",
568            Self::TradeDate { .. } => "trade_date",
569        }
570    }
571
572    /// The start of the window, in this range's index format.
573    pub fn start(&self) -> i32 {
574        match self {
575            Self::Ssboe { start, .. } | Self::TradeDate { start, .. } => *start,
576        }
577    }
578
579    /// The end of the window, in this range's index format.
580    pub fn finish(&self) -> i32 {
581        match self {
582            Self::Ssboe { finish, .. } | Self::TradeDate { finish, .. } => *finish,
583        }
584    }
585}
586
587/// Subscribe to or unsubscribe from the easy-to-borrow list (template 348).
588#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
589#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
590#[non_exhaustive]
591pub enum EasyToBorrowRequest {
592    /// Request the current list and receive updates as it changes.
593    Subscribe,
594    /// Stop receiving easy-to-borrow updates.
595    Unsubscribe,
596}
597
598impl EasyToBorrowRequest {
599    /// The protobuf spelling, as the generated enum's `as_str_name` writes it.
600    pub fn as_str_name(&self) -> &'static str {
601        match self {
602            Self::Subscribe => "SUBSCRIBE",
603            Self::Unsubscribe => "UNSUBSCRIBE",
604        }
605    }
606}
607
608impl fmt::Display for EasyToBorrowRequest {
609    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
610        f.write_str(self.as_str_name())
611    }
612}
613
614impl From<EasyToBorrowRequest> for request_easy_to_borrow_list::Request {
615    fn from(request: EasyToBorrowRequest) -> Self {
616        match request {
617            EasyToBorrowRequest::Subscribe => Self::Subscribe,
618            EasyToBorrowRequest::Unsubscribe => Self::Unsubscribe,
619        }
620    }
621}
622
623/// Selects which RMS fields to stream via
624/// [`subscribe_account_rms_updates`](crate::RithmicOrderPlantHandle::subscribe_account_rms_updates).
625///
626/// Pass one or more selectors; they are combined into the request's
627/// `update_bits` bitmask. An empty selection leaves the field off the
628/// request.
629#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
630#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
631#[non_exhaustive]
632pub enum RmsUpdateBits {
633    /// Stream `auto_liq_threshold_current_value` updates.
634    AutoLiqThresholdCurrentValue,
635}
636
637impl RmsUpdateBits {
638    /// The protobuf spelling, as the generated enum's `as_str_name` writes it.
639    pub fn as_str_name(&self) -> &'static str {
640        match self {
641            Self::AutoLiqThresholdCurrentValue => "AUTO_LIQ_THRESHOLD_CURRENT_VALUE",
642        }
643    }
644}
645
646impl fmt::Display for RmsUpdateBits {
647    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
648        f.write_str(self.as_str_name())
649    }
650}
651
652impl From<RmsUpdateBits> for request_account_rms_updates::UpdateBits {
653    fn from(bits: RmsUpdateBits) -> Self {
654        match bits {
655            RmsUpdateBits::AutoLiqThresholdCurrentValue => Self::AutoLiqThresholdCurrentValue,
656        }
657    }
658}
659
660/// A volume-profile minute-bars request, passed to
661/// [`load_volume_profile_minute_bars`].
662///
663/// # Example
664///
665/// ```
666/// use rithmic_rs::VolumeProfileMinuteBarsRequest;
667/// # fn main() -> Result<(), rithmic_rs::RithmicError> {
668/// let request = VolumeProfileMinuteBarsRequest::new()
669///     .symbol("ESH6")
670///     .exchange("CME")
671///     .bar_type_period(5)
672///     .start_time_sec(1_750_000_000)
673///     .end_time_sec(1_750_003_600)
674///     .build()?;
675/// # Ok(())
676/// # }
677/// ```
678///
679/// [`load_volume_profile_minute_bars`]: crate::RithmicHistoryPlantHandle::load_volume_profile_minute_bars
680#[derive(Debug, Clone, Default, PartialEq)]
681#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
682#[non_exhaustive]
683#[must_use = "a request does nothing until passed to the history handle"]
684pub struct VolumeProfileMinuteBarsRequest {
685    /// The trading symbol, e.g. `"ESH6"`.
686    pub symbol: String,
687    /// The exchange code, e.g. `"CME"`.
688    pub exchange: String,
689    /// Number of minutes each bar aggregates.
690    pub bar_type_period: i32,
691    /// Start of the window as a Unix timestamp in seconds.
692    pub start_time_sec: i32,
693    /// End of the window as a Unix timestamp in seconds.
694    pub end_time_sec: i32,
695    /// Maximum number of bars to return; the server applies its own default
696    /// when unset.
697    pub user_max_count: Option<i32>,
698    /// Whether to resume from a previous request.
699    pub resume_bars: Option<bool>,
700}
701
702impl VolumeProfileMinuteBarsRequest {
703    /// Start an empty request.
704    pub fn new() -> Self {
705        Self::default()
706    }
707
708    /// The trading symbol, e.g. `"ESH6"`.
709    pub fn symbol(mut self, symbol: impl Into<String>) -> Self {
710        self.symbol = symbol.into();
711        self
712    }
713
714    /// The exchange code, e.g. `"CME"`.
715    pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
716        self.exchange = exchange.into();
717        self
718    }
719
720    /// Number of minutes each bar aggregates.
721    pub fn bar_type_period(mut self, bar_type_period: i32) -> Self {
722        self.bar_type_period = bar_type_period;
723        self
724    }
725
726    /// Start of the window as a Unix timestamp in seconds.
727    pub fn start_time_sec(mut self, start_time_sec: i32) -> Self {
728        self.start_time_sec = start_time_sec;
729        self
730    }
731
732    /// End of the window as a Unix timestamp in seconds.
733    pub fn end_time_sec(mut self, end_time_sec: i32) -> Self {
734        self.end_time_sec = end_time_sec;
735        self
736    }
737
738    /// Maximum number of bars to return.
739    pub fn user_max_count(mut self, user_max_count: i32) -> Self {
740        self.user_max_count = Some(user_max_count);
741        self
742    }
743
744    /// Whether to resume from a previous request.
745    pub fn resume_bars(mut self, resume_bars: bool) -> Self {
746        self.resume_bars = Some(resume_bars);
747        self
748    }
749
750    /// Requires a symbol, an exchange, a bar period, and an ordered time
751    /// window.
752    pub fn validate(&self) -> Result<(), RithmicError> {
753        validate_replay_window(
754            "volume-profile",
755            &self.symbol,
756            &self.exchange,
757            self.start_time_sec,
758            self.end_time_sec,
759        )?;
760
761        if self.bar_type_period < 1 {
762            return Err(RithmicError::InvalidArgument(
763                "bar_type_period must be at least 1".to_string(),
764            ));
765        }
766        Ok(())
767    }
768
769    /// Requires a symbol, an exchange, a bar period, and an ordered time
770    /// window.
771    pub fn build(self) -> Result<Self, RithmicError> {
772        self.validate()?;
773        Ok(self)
774    }
775}
776
777/// The instrument and time window every replay request needs. `kind` names the
778/// request in the error message.
779fn validate_replay_window(
780    kind: &str,
781    symbol: &str,
782    exchange: &str,
783    start_time_sec: i32,
784    end_time_sec: i32,
785) -> Result<(), RithmicError> {
786    if symbol.is_empty() {
787        return Err(RithmicError::InvalidArgument(format!(
788            "a {kind} request requires a symbol"
789        )));
790    }
791
792    if exchange.is_empty() {
793        return Err(RithmicError::InvalidArgument(format!(
794            "a {kind} request requires an exchange"
795        )));
796    }
797
798    if start_time_sec < 1 || end_time_sec < 1 {
799        return Err(RithmicError::InvalidArgument(
800            "start_time_sec and end_time_sec are both required, as positive Unix timestamps"
801                .to_string(),
802        ));
803    }
804
805    if end_time_sec < start_time_sec {
806        return Err(RithmicError::InvalidArgument(
807            "end_time_sec must not precede start_time_sec".to_string(),
808        ));
809    }
810    Ok(())
811}
812
813/// A tick bar replay request, passed to [`load_tick_bars`] and its siblings.
814///
815/// A tick bar groups a fixed number of trades. [`bar_length`](Self::bar_length)
816/// of 1 gives one bar per trade — the raw tape.
817///
818/// # Example
819///
820/// ```
821/// use rithmic_rs::TickBarReplayRequest;
822/// # fn main() -> Result<(), rithmic_rs::RithmicError> {
823/// let request = TickBarReplayRequest::new()
824///     .symbol("ESU6")
825///     .exchange("CME")
826///     .bar_length(1)
827///     .start_time_sec(1_750_000_000)
828///     .end_time_sec(1_750_003_600)
829///     .resume_bars(true)
830///     .build()?;
831/// # Ok(())
832/// # }
833/// ```
834///
835/// [`load_tick_bars`]: crate::RithmicHistoryPlantHandle::load_tick_bars
836#[derive(Debug, Clone, Default, PartialEq)]
837#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
838#[non_exhaustive]
839#[must_use = "a request does nothing until passed to the history handle"]
840pub struct TickBarReplayRequest {
841    /// The trading symbol, e.g. `"ESU6"`.
842    pub symbol: String,
843    /// The exchange code, e.g. `"CME"`.
844    pub exchange: String,
845    /// Trades per bar, as the string Rithmic expects. Set it with
846    /// [`bar_length`](Self::bar_length) unless you need the raw form.
847    pub bar_type_specifier: String,
848    /// Start of the window as a Unix timestamp in seconds.
849    pub start_time_sec: i32,
850    /// End of the window as a Unix timestamp in seconds.
851    pub end_time_sec: i32,
852    /// Cap on records returned. Leaving this unset lets the server apply its
853    /// own cap of 10,000, silently.
854    pub user_max_count: Option<i32>,
855    /// `Some(true)` lifts the server's 10,000 record cap, so the whole window
856    /// replays on this one request.
857    pub resume_bars: Option<bool>,
858}
859
860impl TickBarReplayRequest {
861    /// Start an empty request.
862    pub fn new() -> Self {
863        Self::default()
864    }
865
866    /// The trading symbol, e.g. `"ESU6"`.
867    pub fn symbol(mut self, symbol: impl Into<String>) -> Self {
868        self.symbol = symbol.into();
869        self
870    }
871
872    /// The exchange code, e.g. `"CME"`.
873    pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
874        self.exchange = exchange.into();
875        self
876    }
877
878    /// How many trades go into each bar. 1 gives one bar per trade.
879    pub fn bar_length(mut self, bar_length: u32) -> Self {
880        self.bar_type_specifier = bar_length.to_string();
881        self
882    }
883
884    /// The raw `bar_type_specifier` Rithmic expects, for a value
885    /// [`bar_length`](Self::bar_length) cannot express.
886    pub fn bar_type_specifier(mut self, bar_type_specifier: impl Into<String>) -> Self {
887        self.bar_type_specifier = bar_type_specifier.into();
888        self
889    }
890
891    /// Start of the window as a Unix timestamp in seconds.
892    pub fn start_time_sec(mut self, start_time_sec: i32) -> Self {
893        self.start_time_sec = start_time_sec;
894        self
895    }
896
897    /// End of the window as a Unix timestamp in seconds.
898    pub fn end_time_sec(mut self, end_time_sec: i32) -> Self {
899        self.end_time_sec = end_time_sec;
900        self
901    }
902
903    /// Cap the records returned.
904    pub fn user_max_count(mut self, user_max_count: i32) -> Self {
905        self.user_max_count = Some(user_max_count);
906        self
907    }
908
909    /// Lift the server's 10,000 record cap so the whole window replays at once.
910    pub fn resume_bars(mut self, resume_bars: bool) -> Self {
911        self.resume_bars = Some(resume_bars);
912        self
913    }
914
915    /// Requires a symbol, an exchange, a bar length of at least 1, and an
916    /// ordered time window.
917    pub fn validate(&self) -> Result<(), RithmicError> {
918        validate_replay_window(
919            "tick bar replay",
920            &self.symbol,
921            &self.exchange,
922            self.start_time_sec,
923            self.end_time_sec,
924        )?;
925
926        match self.bar_type_specifier.parse::<u32>() {
927            Ok(length) if length >= 1 => Ok(()),
928            _ => Err(RithmicError::InvalidArgument(
929                "bar_length must be at least 1".to_string(),
930            )),
931        }
932    }
933
934    /// Requires a symbol, an exchange, a bar length of at least 1, and an
935    /// ordered time window.
936    pub fn build(self) -> Result<Self, RithmicError> {
937        self.validate()?;
938        Ok(self)
939    }
940}
941
942/// A time bar replay request, passed to [`load_time_bars`] and its siblings.
943///
944/// A time bar covers a fixed span: [`bar_type`](Self::bar_type) picks the unit
945/// and [`bar_type_period`](Self::bar_type_period) how many of them per bar.
946///
947/// # Example
948///
949/// ```
950/// use rithmic_rs::{TimeBarReplayRequest, rti::request_time_bar_replay::BarType};
951/// # fn main() -> Result<(), rithmic_rs::RithmicError> {
952/// let request = TimeBarReplayRequest::new()
953///     .symbol("ESU6")
954///     .exchange("CME")
955///     .bar_type(BarType::MinuteBar)
956///     .bar_type_period(5)
957///     .start_time_sec(1_750_000_000)
958///     .end_time_sec(1_750_003_600)
959///     .build()?;
960/// # Ok(())
961/// # }
962/// ```
963///
964/// [`load_time_bars`]: crate::RithmicHistoryPlantHandle::load_time_bars
965//
966// No serde derive: `bar_type` is a generated protobuf enum, which does not
967// implement `Serialize`.
968#[derive(Debug, Clone, Default, PartialEq)]
969#[non_exhaustive]
970#[must_use = "a request does nothing until passed to the history handle"]
971pub struct TimeBarReplayRequest {
972    /// The trading symbol, e.g. `"ESU6"`.
973    pub symbol: String,
974    /// The exchange code, e.g. `"CME"`.
975    pub exchange: String,
976    /// Second, minute, day or week. Required.
977    pub bar_type: Option<TimeBarType>,
978    /// How many of those units each bar covers.
979    pub bar_type_period: i32,
980    /// Start of the window as a Unix timestamp in seconds.
981    pub start_time_sec: i32,
982    /// End of the window as a Unix timestamp in seconds.
983    pub end_time_sec: i32,
984    /// Cap on records returned. Leaving this unset lets the server apply its
985    /// own cap of 10,000, silently.
986    pub user_max_count: Option<i32>,
987    /// `Some(true)` lifts the server's 10,000 record cap, so the whole window
988    /// replays on this one request.
989    pub resume_bars: Option<bool>,
990}
991
992impl TimeBarReplayRequest {
993    /// Start an empty request.
994    pub fn new() -> Self {
995        Self::default()
996    }
997
998    /// The trading symbol, e.g. `"ESU6"`.
999    pub fn symbol(mut self, symbol: impl Into<String>) -> Self {
1000        self.symbol = symbol.into();
1001        self
1002    }
1003
1004    /// The exchange code, e.g. `"CME"`.
1005    pub fn exchange(mut self, exchange: impl Into<String>) -> Self {
1006        self.exchange = exchange.into();
1007        self
1008    }
1009
1010    /// Second, minute, day or week.
1011    pub fn bar_type(mut self, bar_type: TimeBarType) -> Self {
1012        self.bar_type = Some(bar_type);
1013        self
1014    }
1015
1016    /// How many of those units each bar covers.
1017    pub fn bar_type_period(mut self, bar_type_period: i32) -> Self {
1018        self.bar_type_period = bar_type_period;
1019        self
1020    }
1021
1022    /// Start of the window as a Unix timestamp in seconds.
1023    pub fn start_time_sec(mut self, start_time_sec: i32) -> Self {
1024        self.start_time_sec = start_time_sec;
1025        self
1026    }
1027
1028    /// End of the window as a Unix timestamp in seconds.
1029    pub fn end_time_sec(mut self, end_time_sec: i32) -> Self {
1030        self.end_time_sec = end_time_sec;
1031        self
1032    }
1033
1034    /// Cap the records returned.
1035    pub fn user_max_count(mut self, user_max_count: i32) -> Self {
1036        self.user_max_count = Some(user_max_count);
1037        self
1038    }
1039
1040    /// Lift the server's 10,000 record cap so the whole window replays at once.
1041    pub fn resume_bars(mut self, resume_bars: bool) -> Self {
1042        self.resume_bars = Some(resume_bars);
1043        self
1044    }
1045
1046    /// Requires a symbol, an exchange, a bar type, a bar period, and an ordered
1047    /// time window.
1048    pub fn validate(&self) -> Result<(), RithmicError> {
1049        validate_replay_window(
1050            "time bar replay",
1051            &self.symbol,
1052            &self.exchange,
1053            self.start_time_sec,
1054            self.end_time_sec,
1055        )?;
1056
1057        if self.bar_type.is_none() {
1058            return Err(RithmicError::InvalidArgument(
1059                "a time bar replay request requires a bar_type".to_string(),
1060            ));
1061        }
1062
1063        if self.bar_type_period < 1 {
1064            return Err(RithmicError::InvalidArgument(
1065                "bar_type_period must be at least 1".to_string(),
1066            ));
1067        }
1068        Ok(())
1069    }
1070
1071    /// Requires a symbol, an exchange, a bar type, a bar period, and an ordered
1072    /// time window.
1073    pub fn build(self) -> Result<Self, RithmicError> {
1074        self.validate()?;
1075        Ok(self)
1076    }
1077}
1078
1079/// Comparison operator for an if-touched trigger. Defaults to
1080/// `GreaterThanEqualTo`.
1081#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1082#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1083#[non_exhaustive]
1084pub enum OrderCondition {
1085    /// Fires when the price field equals the threshold.
1086    EqualTo,
1087    /// Fires when the price field differs from the threshold.
1088    NotEqualTo,
1089    /// Fires when the price field is above the threshold.
1090    GreaterThan,
1091    /// Fires when the price field is at or above the threshold.
1092    #[default]
1093    GreaterThanEqualTo,
1094    /// Fires when the price field is below the threshold.
1095    LesserThan,
1096    /// Fires when the price field is at or below the threshold.
1097    LesserThanEqualTo,
1098}
1099
1100impl OrderCondition {
1101    /// The protobuf spelling, as `Condition::as_str_name` writes it.
1102    pub fn as_str_name(&self) -> &'static str {
1103        match self {
1104            Self::EqualTo => "EQUAL_TO",
1105            Self::NotEqualTo => "NOT_EQUAL_TO",
1106            Self::GreaterThan => "GREATER_THAN",
1107            Self::GreaterThanEqualTo => "GREATER_THAN_EQUAL_TO",
1108            Self::LesserThan => "LESSER_THAN",
1109            Self::LesserThanEqualTo => "LESSER_THAN_EQUAL_TO",
1110        }
1111    }
1112}
1113
1114impl fmt::Display for OrderCondition {
1115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1116        f.write_str(self.as_str_name())
1117    }
1118}
1119
1120impl From<OrderCondition> for request_new_order::Condition {
1121    fn from(condition: OrderCondition) -> Self {
1122        match condition {
1123            OrderCondition::EqualTo => Self::EqualTo,
1124            OrderCondition::NotEqualTo => Self::NotEqualTo,
1125            OrderCondition::GreaterThan => Self::GreaterThan,
1126            OrderCondition::GreaterThanEqualTo => Self::GreaterThanEqualTo,
1127            OrderCondition::LesserThan => Self::LesserThan,
1128            OrderCondition::LesserThanEqualTo => Self::LesserThanEqualTo,
1129        }
1130    }
1131}
1132
1133impl From<OrderCondition> for request_bracket_order::Condition {
1134    fn from(condition: OrderCondition) -> Self {
1135        match condition {
1136            OrderCondition::EqualTo => Self::EqualTo,
1137            OrderCondition::NotEqualTo => Self::NotEqualTo,
1138            OrderCondition::GreaterThan => Self::GreaterThan,
1139            OrderCondition::GreaterThanEqualTo => Self::GreaterThanEqualTo,
1140            OrderCondition::LesserThan => Self::LesserThan,
1141            OrderCondition::LesserThanEqualTo => Self::LesserThanEqualTo,
1142        }
1143    }
1144}
1145
1146impl From<OrderCondition> for request_modify_order::Condition {
1147    fn from(condition: OrderCondition) -> Self {
1148        match condition {
1149            OrderCondition::EqualTo => Self::EqualTo,
1150            OrderCondition::NotEqualTo => Self::NotEqualTo,
1151            OrderCondition::GreaterThan => Self::GreaterThan,
1152            OrderCondition::GreaterThanEqualTo => Self::GreaterThanEqualTo,
1153            OrderCondition::LesserThan => Self::LesserThan,
1154            OrderCondition::LesserThanEqualTo => Self::LesserThanEqualTo,
1155        }
1156    }
1157}
1158
1159/// Which price an if-touched trigger watches. Defaults to `TradePrice`.
1160#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1161#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1162#[non_exhaustive]
1163pub enum OrderPriceField {
1164    /// The best bid.
1165    BidPrice,
1166    /// The best offer.
1167    OfferPrice,
1168    /// The last trade price.
1169    #[default]
1170    TradePrice,
1171    /// The lean price.
1172    LeanPrice,
1173}
1174
1175impl OrderPriceField {
1176    /// The protobuf spelling, as `PriceField::as_str_name` writes it.
1177    pub fn as_str_name(&self) -> &'static str {
1178        match self {
1179            Self::BidPrice => "BID_PRICE",
1180            Self::OfferPrice => "OFFER_PRICE",
1181            Self::TradePrice => "TRADE_PRICE",
1182            Self::LeanPrice => "LEAN_PRICE",
1183        }
1184    }
1185}
1186
1187impl fmt::Display for OrderPriceField {
1188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1189        f.write_str(self.as_str_name())
1190    }
1191}
1192
1193impl From<OrderPriceField> for request_new_order::PriceField {
1194    fn from(price_field: OrderPriceField) -> Self {
1195        match price_field {
1196            OrderPriceField::BidPrice => Self::BidPrice,
1197            OrderPriceField::OfferPrice => Self::OfferPrice,
1198            OrderPriceField::TradePrice => Self::TradePrice,
1199            OrderPriceField::LeanPrice => Self::LeanPrice,
1200        }
1201    }
1202}
1203
1204impl From<OrderPriceField> for request_bracket_order::PriceField {
1205    fn from(price_field: OrderPriceField) -> Self {
1206        match price_field {
1207            OrderPriceField::BidPrice => Self::BidPrice,
1208            OrderPriceField::OfferPrice => Self::OfferPrice,
1209            OrderPriceField::TradePrice => Self::TradePrice,
1210            OrderPriceField::LeanPrice => Self::LeanPrice,
1211        }
1212    }
1213}
1214
1215impl From<OrderPriceField> for request_modify_order::PriceField {
1216    fn from(price_field: OrderPriceField) -> Self {
1217        match price_field {
1218            OrderPriceField::BidPrice => Self::BidPrice,
1219            OrderPriceField::OfferPrice => Self::OfferPrice,
1220            OrderPriceField::TradePrice => Self::TradePrice,
1221            OrderPriceField::LeanPrice => Self::LeanPrice,
1222        }
1223    }
1224}
1225
1226#[cfg(test)]
1227mod tests {
1228    use super::*;
1229
1230    /// `RequestOcoOrder` stops at four price types; the if-touched pair has to be
1231    /// rejected rather than remapped onto something the caller did not ask for.
1232    #[test]
1233    fn the_oco_price_type_rejects_the_if_touched_pair() {
1234        for order_type in [
1235            OrderType::Market,
1236            OrderType::Limit,
1237            OrderType::StopMarket,
1238            OrderType::StopLimit,
1239        ] {
1240            let converted = request_oco_order::PriceType::try_from(order_type).unwrap();
1241            assert_eq!(converted.as_str_name(), order_type.as_str_name());
1242        }
1243
1244        for order_type in [OrderType::MarketIfTouched, OrderType::LimitIfTouched] {
1245            let err = request_oco_order::PriceType::try_from(order_type)
1246                .unwrap_err()
1247                .to_string();
1248            assert!(err.contains("is not available on an OCO leg"), "{err}");
1249            assert!(err.contains(order_type.as_str_name()), "{err}");
1250        }
1251    }
1252
1253    #[test]
1254    fn volume_profile_request_rejects_a_missing_or_reversed_window() {
1255        let request = VolumeProfileMinuteBarsRequest::new()
1256            .symbol("ESH6")
1257            .exchange("CME")
1258            .bar_type_period(5);
1259
1260        let err = request
1261            .clone()
1262            .start_time_sec(-10)
1263            .end_time_sec(1_750_003_600)
1264            .build()
1265            .unwrap_err()
1266            .to_string();
1267        assert!(err.contains("both required"), "{err}");
1268
1269        let err = request
1270            .clone()
1271            .start_time_sec(1_750_003_600)
1272            .end_time_sec(1_750_000_000)
1273            .build()
1274            .unwrap_err()
1275            .to_string();
1276        assert!(err.contains("must not precede"), "{err}");
1277
1278        assert!(
1279            request
1280                .start_time_sec(1_750_000_000)
1281                .end_time_sec(1_750_000_000)
1282                .build()
1283                .is_ok()
1284        );
1285    }
1286
1287    #[test]
1288    fn order_type_round_trips_through_its_string_forms() {
1289        for order_type in [
1290            OrderType::Market,
1291            OrderType::Limit,
1292            OrderType::StopMarket,
1293            OrderType::StopLimit,
1294            OrderType::MarketIfTouched,
1295            OrderType::LimitIfTouched,
1296        ] {
1297            assert_eq!(order_type.to_string(), order_type.as_str_name());
1298            assert_eq!(
1299                order_type.to_string().parse::<OrderType>().unwrap(),
1300                order_type
1301            );
1302        }
1303
1304        assert_eq!(
1305            "mit".parse::<OrderType>().unwrap(),
1306            OrderType::MarketIfTouched
1307        );
1308        assert_eq!(
1309            "lit".parse::<OrderType>().unwrap(),
1310            OrderType::LimitIfTouched
1311        );
1312        assert_eq!(
1313            "market-if-touched".parse::<OrderType>().unwrap(),
1314            OrderType::MarketIfTouched
1315        );
1316        assert_eq!(
1317            "limit-if-touched".parse::<OrderType>().unwrap(),
1318            OrderType::LimitIfTouched
1319        );
1320    }
1321
1322    fn tick_replay() -> TickBarReplayRequest {
1323        TickBarReplayRequest::new()
1324            .symbol("ESU6")
1325            .exchange("CME")
1326            .bar_length(1)
1327            .start_time_sec(1_750_000_000)
1328            .end_time_sec(1_750_003_600)
1329    }
1330
1331    fn time_replay() -> TimeBarReplayRequest {
1332        TimeBarReplayRequest::new()
1333            .symbol("ESU6")
1334            .exchange("CME")
1335            .bar_type(TimeBarType::MinuteBar)
1336            .bar_type_period(5)
1337            .start_time_sec(1_750_000_000)
1338            .end_time_sec(1_750_003_600)
1339    }
1340
1341    #[test]
1342    fn a_tick_replay_request_needs_an_instrument_and_an_ordered_window() {
1343        assert!(tick_replay().validate().is_ok());
1344
1345        let err = TickBarReplayRequest {
1346            symbol: String::new(),
1347            ..tick_replay()
1348        }
1349        .validate()
1350        .unwrap_err()
1351        .to_string();
1352        assert!(
1353            err.contains("tick bar replay request requires a symbol"),
1354            "{err}"
1355        );
1356
1357        let err = TickBarReplayRequest {
1358            exchange: String::new(),
1359            ..tick_replay()
1360        }
1361        .validate()
1362        .unwrap_err()
1363        .to_string();
1364        assert!(
1365            err.contains("tick bar replay request requires an exchange"),
1366            "{err}"
1367        );
1368
1369        let err = tick_replay()
1370            .end_time_sec(1_749_999_999)
1371            .build()
1372            .unwrap_err()
1373            .to_string();
1374        assert!(err.contains("must not precede"), "{err}");
1375    }
1376
1377    /// `bar_length` reaches the wire as a string, so a zero or an unparseable
1378    /// specifier both have to be caught before the request is sent.
1379    #[test]
1380    fn a_tick_replay_request_needs_a_bar_length_of_at_least_one() {
1381        for specifier in ["0", "", "lots"] {
1382            let err = tick_replay()
1383                .bar_type_specifier(specifier)
1384                .build()
1385                .unwrap_err()
1386                .to_string();
1387            assert!(
1388                err.contains("bar_length must be at least 1"),
1389                "{specifier}: {err}"
1390            );
1391        }
1392
1393        assert_eq!(tick_replay().bar_length(5).bar_type_specifier, "5");
1394    }
1395
1396    #[test]
1397    fn a_time_replay_request_needs_a_bar_type_and_period() {
1398        assert!(time_replay().validate().is_ok());
1399
1400        let err = TimeBarReplayRequest {
1401            bar_type: None,
1402            ..time_replay()
1403        }
1404        .validate()
1405        .unwrap_err()
1406        .to_string();
1407        assert!(err.contains("requires a bar_type"), "{err}");
1408
1409        let err = time_replay()
1410            .bar_type_period(0)
1411            .build()
1412            .unwrap_err()
1413            .to_string();
1414        assert!(err.contains("bar_type_period must be at least 1"), "{err}");
1415    }
1416}