Skip to main content

rhood_core/models/
order.rs

1//! Order-related model types for the Robinhood API.
2//!
3//! Contains enums for order parameters (side, type, time-in-force) as well as
4//! response structs for stock and option orders, and their corresponding
5//! request builders.
6
7use std::fmt;
8
9use serde::{Deserialize, Serialize};
10
11/// Represents the side (direction) of a trade.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
14#[serde(rename_all = "lowercase")]
15pub enum Side {
16    /// Indicates a buy order to acquire shares or contracts.
17    Buy,
18    /// Indicates a sell order to dispose of shares or contracts.
19    Sell,
20}
21
22/// Specifies whether an option order opens a new position or closes an existing one.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
25#[serde(rename_all = "lowercase")]
26pub enum OptionPositionEffect {
27    /// Open a new long or short option position.
28    Open,
29    /// Close an existing long or short option position.
30    Close,
31}
32
33impl fmt::Display for OptionPositionEffect {
34    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Self::Open => write!(formatter, "open"),
37            Self::Close => write!(formatter, "close"),
38        }
39    }
40}
41
42/// Represents the execution type of an order.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
45#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
46#[serde(rename_all = "lowercase")]
47pub enum OrderType {
48    /// Executes immediately at the best available market price.
49    Market,
50    /// Executes only at the specified limit price or better.
51    Limit,
52}
53
54impl fmt::Display for OrderType {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match self {
57            Self::Market => write!(f, "market"),
58            Self::Limit => write!(f, "limit"),
59        }
60    }
61}
62
63/// Represents the duration policy for an order.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
66#[serde(rename_all = "lowercase")]
67pub enum TimeInForce {
68    /// Good for day -- the order expires at the end of the current trading session.
69    Gfd,
70    /// Good till cancelled -- the order remains active until explicitly cancelled.
71    Gtc,
72}
73
74/// Specifies when the order should trigger execution.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
76#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
77#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
78#[serde(rename_all = "lowercase")]
79pub enum Trigger {
80    /// Execute the order immediately (standard market/limit behavior).
81    Immediate,
82    /// Execute only after the stop price is reached.
83    Stop,
84}
85
86impl fmt::Display for Trigger {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        match self {
89            Self::Immediate => write!(f, "immediate"),
90            Self::Stop => write!(f, "stop"),
91        }
92    }
93}
94
95/// Specifies which trading session the order is eligible for.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
97#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
98#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
99#[serde(rename_all = "snake_case")]
100pub enum MarketHours {
101    /// Standard market hours (9:30 AM - 4:00 PM ET).
102    #[cfg_attr(feature = "clap", value(name = "regular"))]
103    RegularHours,
104    /// Pre-market and after-hours sessions.
105    #[cfg_attr(feature = "clap", value(name = "extended"))]
106    ExtendedHours,
107    /// 24-hour trading session (available for select stocks).
108    #[cfg_attr(feature = "clap", value(name = "all-day"))]
109    AllDayHours,
110}
111
112impl fmt::Display for MarketHours {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        match self {
115            Self::RegularHours => write!(f, "regular"),
116            Self::ExtendedHours => write!(f, "extended"),
117            Self::AllDayHours => write!(f, "all-day"),
118        }
119    }
120}
121
122/// Specifies the quantity semantics for a stock order.
123#[derive(Debug, Clone, Copy, PartialEq)]
124pub enum OrderAmount {
125    /// Whole or fractional share quantity (e.g., 10.0 shares, 0.5 shares).
126    Quantity(f64),
127    /// Dollar amount to invest - broker calculates share count (e.g., $50.00).
128    DollarAmount(f64),
129}
130
131/// The outcome of attempting to cancel every open order.
132#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
133pub struct CancelAllOutcome {
134    /// IDs of orders that were successfully cancelled.
135    pub cancelled: Vec<String>,
136    /// Orders that could not be cancelled, with the reason for each failure.
137    pub failed: Vec<CancelAllFailure>,
138}
139
140/// A single order that could not be cancelled as part of a bulk cancellation.
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142pub struct CancelAllFailure {
143    /// The order ID, or `None` when the open-order response did not include one.
144    pub order_id: Option<String>,
145    /// The cancellation error, or the reason an attempt could not be made.
146    pub error: String,
147}
148
149/// Represents a stock order response from the Robinhood API.
150#[derive(Debug, Clone, Default, Serialize, Deserialize)]
151pub struct StockOrder {
152    /// Unique identifier for the order.
153    pub id: Option<String>,
154    /// API URL for the account that placed the order.
155    pub account: Option<String>,
156    /// API URL for the instrument being ordered.
157    pub instrument: Option<String>,
158    /// Ticker symbol of the stock.
159    pub symbol: Option<String>,
160    /// Order side ("buy" or "sell").
161    pub side: Option<String>,
162    /// Requested quantity of shares.
163    pub quantity: Option<String>,
164    /// Limit price, if applicable.
165    pub price: Option<String>,
166    /// Average execution price of filled shares.
167    pub average_price: Option<String>,
168    /// Cumulative quantity of shares filled so far.
169    pub cumulative_quantity: Option<String>,
170    /// Current state of the order (e.g., "queued", "filled", "cancelled").
171    pub state: Option<String>,
172    /// Timestamp when the order was created.
173    pub created_at: Option<String>,
174    /// Timestamp when the order was last updated.
175    pub updated_at: Option<String>,
176    /// API URL to cancel this order, if cancellation is available.
177    pub cancel: Option<String>,
178    /// Type of order ("market" or "limit").
179    #[serde(rename = "type")]
180    pub order_type: Option<String>,
181    /// Time-in-force policy for the order.
182    pub time_in_force: Option<String>,
183    /// Indicates whether the order is eligible for extended-hours trading.
184    pub extended_hours: Option<bool>,
185    /// Stop price, if this is a stop or stop-limit order.
186    pub stop_price: Option<String>,
187    /// Trigger type for the order ("immediate" or "stop").
188    pub trigger: Option<String>,
189}
190
191/// Represents an option order response from the Robinhood API.
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct OptionOrder {
194    /// Unique identifier for the order.
195    pub id: Option<String>,
196    /// Identifier for the option chain.
197    pub chain_id: Option<String>,
198    /// Ticker symbol of the underlying stock.
199    pub chain_symbol: Option<String>,
200    /// Direction of the order (e.g., "debit", "credit").
201    pub direction: Option<String>,
202    /// Individual legs of the option order.
203    pub legs: Option<Vec<serde_json::Value>>,
204    /// Estimated premium for the order.
205    pub premium: Option<String>,
206    /// Limit price per contract.
207    pub price: Option<String>,
208    /// Actual premium processed upon execution.
209    pub processed_premium: Option<String>,
210    /// Number of contracts in the order.
211    pub quantity: Option<String>,
212    /// Current state of the order (e.g., "queued", "filled", "cancelled").
213    pub state: Option<String>,
214    /// Time-in-force policy for the order.
215    pub time_in_force: Option<String>,
216    /// Type of order ("market" or "limit").
217    #[serde(rename = "type")]
218    pub order_type: Option<String>,
219    /// Timestamp when the order was created.
220    pub created_at: Option<String>,
221    /// Timestamp when the order was last updated.
222    pub updated_at: Option<String>,
223    /// API URL to cancel this order, if cancellation is available.
224    pub cancel_url: Option<String>,
225}
226
227/// Represents a request to place a stock order.
228#[derive(Debug, Clone)]
229pub struct StockOrderRequest {
230    /// Ticker symbol of the stock to trade.
231    pub symbol: String,
232    /// Order amount - either a share quantity or a dollar amount.
233    pub amount: OrderAmount,
234    /// Side of the trade (buy or sell).
235    pub side: Side,
236    /// Execution type (market or limit).
237    pub order_type: OrderType,
238    /// Limit price per share; required when `order_type` is [`OrderType::Limit`].
239    pub limit_price: Option<f64>,
240    /// Trigger condition for the order.
241    pub trigger: Trigger,
242    /// Stop price; required when `trigger` is [`Trigger::Stop`].
243    pub stop_price: Option<f64>,
244    /// Duration policy for the order.
245    pub time_in_force: TimeInForce,
246    /// Trading session eligibility for the order.
247    pub market_hours: MarketHours,
248}
249
250/// Represents a request to place an option order.
251#[derive(Debug, Clone)]
252pub struct OptionOrderRequest {
253    /// Ticker symbol of the underlying stock.
254    pub symbol: String,
255    /// Expiration date of the option contract (YYYY-MM-DD).
256    pub expiration_date: String,
257    /// Strike price of the option contract.
258    pub strike_price: f64,
259    /// Type of option contract ("call" or "put").
260    pub option_type: String,
261    /// Side of the trade (buy or sell).
262    pub side: Side,
263    /// Number of contracts to trade.
264    pub quantity: f64,
265    /// Limit price per contract.
266    pub limit_price: f64,
267    /// Whether this order opens a new position or closes an existing one.
268    pub position_effect: OptionPositionEffect,
269    /// Duration policy for the order.
270    pub time_in_force: TimeInForce,
271}
272
273/// Dollar-based amount for fractional/dollar orders.
274#[derive(Debug, Serialize)]
275pub(crate) struct DollarBasedAmount {
276    pub amount: String,
277    pub currency_code: String,
278}
279
280#[derive(Debug, Serialize)]
281pub(crate) struct StockOrderPayload {
282    pub account: String,
283    pub instrument: String,
284    pub symbol: String,
285    pub quantity: String,
286    pub side: Side,
287    #[serde(rename = "type")]
288    pub order_type: OrderType,
289    pub time_in_force: TimeInForce,
290    pub trigger: Trigger,
291    pub market_hours: MarketHours,
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub price: Option<String>,
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub stop_price: Option<String>,
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub dollar_based_amount: Option<DollarBasedAmount>,
298}
299
300#[derive(Debug, Serialize)]
301pub(crate) struct OptionOrderPayload {
302    pub account: String,
303    pub direction: String,
304    pub time_in_force: TimeInForce,
305    pub legs: Vec<OptionLeg>,
306    #[serde(rename = "type")]
307    pub order_type: &'static str,
308    pub trigger: &'static str,
309    pub price: String,
310    pub quantity: String,
311    pub override_day_trade_checks: bool,
312    pub override_dtbp_checks: bool,
313    pub ref_id: String,
314}
315
316#[derive(Debug, Serialize)]
317pub(crate) struct OptionLeg {
318    pub position_effect: OptionPositionEffect,
319    pub side: Side,
320    pub ratio_quantity: u32,
321    pub option: String,
322}
323
324#[cfg(test)]
325mod tests {
326    use super::{CancelAllFailure, CancelAllOutcome};
327
328    #[test]
329    fn cancel_all_outcome_serializes_round_trip() {
330        let outcome = CancelAllOutcome {
331            cancelled: vec!["order-1".to_string(), "order-2".to_string()],
332            failed: vec![
333                CancelAllFailure {
334                    order_id: Some("order-3".to_string()),
335                    error: "already filled".to_string(),
336                },
337                CancelAllFailure {
338                    order_id: None,
339                    error: "open order was missing an ID".to_string(),
340                },
341            ],
342        };
343
344        let json = serde_json::to_string(&outcome).unwrap();
345        let round_tripped: CancelAllOutcome = serde_json::from_str(&json).unwrap();
346
347        assert_eq!(round_tripped, outcome);
348    }
349}