Skip to main content

trader_bot/domain/
order.rs

1//! Order types for trading operations.
2
3use chrono::{DateTime, Utc};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6
7use super::Money;
8
9/// Direction of the order.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum OrderDirection {
13    /// Buy order.
14    Buy,
15    /// Sell order.
16    Sell,
17}
18
19impl OrderDirection {
20    /// Returns the opposite direction.
21    #[must_use]
22    pub const fn opposite(self) -> Self {
23        match self {
24            Self::Buy => Self::Sell,
25            Self::Sell => Self::Buy,
26        }
27    }
28}
29
30/// Type of the order.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum OrderType {
34    /// Market order - executes at best available price.
35    Market,
36    /// Limit order - executes at specified price or better.
37    Limit,
38    /// Stop order - triggers market order when price reaches stop price.
39    Stop,
40    /// Stop limit order - triggers limit order when price reaches stop price.
41    StopLimit,
42}
43
44/// Current status of an order.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(rename_all = "snake_case")]
47pub enum OrderStatus {
48    /// Order has been created but not yet submitted.
49    Pending,
50    /// Order has been submitted to the exchange.
51    Submitted,
52    /// Order is partially filled.
53    PartiallyFilled,
54    /// Order has been completely filled.
55    Filled,
56    /// Order has been cancelled.
57    Cancelled,
58    /// Order has been rejected by the exchange.
59    Rejected,
60    /// Order has expired.
61    Expired,
62}
63
64impl OrderStatus {
65    /// Returns true if the order is in a terminal state.
66    #[must_use]
67    pub const fn is_terminal(self) -> bool {
68        matches!(
69            self,
70            Self::Filled | Self::Cancelled | Self::Rejected | Self::Expired
71        )
72    }
73
74    /// Returns true if the order is still active.
75    #[must_use]
76    pub const fn is_active(self) -> bool {
77        matches!(
78            self,
79            Self::Pending | Self::Submitted | Self::PartiallyFilled
80        )
81    }
82}
83
84/// Represents a trading order.
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86pub struct Order {
87    /// Unique order identifier (client-side).
88    id: String,
89    /// Exchange-assigned order identifier (set after submission).
90    exchange_order_id: Option<String>,
91    /// The trading symbol.
92    symbol: String,
93    /// The exchange where the order is placed.
94    exchange: String,
95    /// Order direction (buy/sell).
96    direction: OrderDirection,
97    /// Order type.
98    order_type: OrderType,
99    /// Quantity in lots.
100    quantity_lots: u32,
101    /// Limit price (for limit orders).
102    limit_price: Option<Money>,
103    /// Stop price (for stop orders).
104    stop_price: Option<Money>,
105    /// Current order status.
106    status: OrderStatus,
107    /// Quantity filled so far (in lots).
108    filled_lots: u32,
109    /// Average fill price.
110    average_fill_price: Option<Money>,
111    /// Timestamp when order was created.
112    created_at: DateTime<Utc>,
113    /// Timestamp when order was last updated.
114    updated_at: DateTime<Utc>,
115    /// Optional message (e.g., rejection reason).
116    message: Option<String>,
117}
118
119impl Order {
120    /// Creates a new market order.
121    #[must_use]
122    pub fn market(
123        id: impl Into<String>,
124        symbol: impl Into<String>,
125        exchange: impl Into<String>,
126        direction: OrderDirection,
127        quantity_lots: u32,
128    ) -> Self {
129        let now = Utc::now();
130        Self {
131            id: id.into(),
132            exchange_order_id: None,
133            symbol: symbol.into(),
134            exchange: exchange.into(),
135            direction,
136            order_type: OrderType::Market,
137            quantity_lots,
138            limit_price: None,
139            stop_price: None,
140            status: OrderStatus::Pending,
141            filled_lots: 0,
142            average_fill_price: None,
143            created_at: now,
144            updated_at: now,
145            message: None,
146        }
147    }
148
149    /// Creates a new limit order.
150    #[must_use]
151    pub fn limit(
152        id: impl Into<String>,
153        symbol: impl Into<String>,
154        exchange: impl Into<String>,
155        direction: OrderDirection,
156        quantity_lots: u32,
157        limit_price: Money,
158    ) -> Self {
159        let now = Utc::now();
160        Self {
161            id: id.into(),
162            exchange_order_id: None,
163            symbol: symbol.into(),
164            exchange: exchange.into(),
165            direction,
166            order_type: OrderType::Limit,
167            quantity_lots,
168            limit_price: Some(limit_price),
169            stop_price: None,
170            status: OrderStatus::Pending,
171            filled_lots: 0,
172            average_fill_price: None,
173            created_at: now,
174            updated_at: now,
175            message: None,
176        }
177    }
178
179    /// Returns the order ID.
180    #[must_use]
181    pub fn id(&self) -> &str {
182        &self.id
183    }
184
185    /// Returns the exchange order ID if set.
186    #[must_use]
187    pub fn exchange_order_id(&self) -> Option<&str> {
188        self.exchange_order_id.as_deref()
189    }
190
191    /// Returns the symbol.
192    #[must_use]
193    pub fn symbol(&self) -> &str {
194        &self.symbol
195    }
196
197    /// Returns the exchange.
198    #[must_use]
199    pub fn exchange(&self) -> &str {
200        &self.exchange
201    }
202
203    /// Returns the order direction.
204    #[must_use]
205    pub const fn direction(&self) -> OrderDirection {
206        self.direction
207    }
208
209    /// Returns the order type.
210    #[must_use]
211    pub const fn order_type(&self) -> OrderType {
212        self.order_type
213    }
214
215    /// Returns the quantity in lots.
216    #[must_use]
217    pub const fn quantity_lots(&self) -> u32 {
218        self.quantity_lots
219    }
220
221    /// Returns the limit price if set.
222    #[must_use]
223    pub const fn limit_price(&self) -> Option<&Money> {
224        self.limit_price.as_ref()
225    }
226
227    /// Returns the current status.
228    #[must_use]
229    pub const fn status(&self) -> OrderStatus {
230        self.status
231    }
232
233    /// Returns the number of lots filled.
234    #[must_use]
235    pub const fn filled_lots(&self) -> u32 {
236        self.filled_lots
237    }
238
239    /// Returns the number of lots remaining.
240    #[must_use]
241    pub const fn remaining_lots(&self) -> u32 {
242        self.quantity_lots - self.filled_lots
243    }
244
245    /// Returns the average fill price if any fills occurred.
246    #[must_use]
247    pub const fn average_fill_price(&self) -> Option<&Money> {
248        self.average_fill_price.as_ref()
249    }
250
251    /// Returns the creation timestamp.
252    #[must_use]
253    pub const fn created_at(&self) -> DateTime<Utc> {
254        self.created_at
255    }
256
257    /// Returns the last update timestamp.
258    #[must_use]
259    pub const fn updated_at(&self) -> DateTime<Utc> {
260        self.updated_at
261    }
262
263    /// Returns the message if set.
264    #[must_use]
265    pub fn message(&self) -> Option<&str> {
266        self.message.as_deref()
267    }
268
269    /// Calculates the fill percentage.
270    #[must_use]
271    pub fn fill_percentage(&self) -> Decimal {
272        if self.quantity_lots == 0 {
273            return Decimal::ZERO;
274        }
275        Decimal::from(self.filled_lots) / Decimal::from(self.quantity_lots) * Decimal::from(100)
276    }
277
278    /// Updates the order status.
279    pub fn set_status(&mut self, status: OrderStatus) {
280        self.status = status;
281        self.updated_at = Utc::now();
282    }
283
284    /// Sets the exchange order ID.
285    pub fn set_exchange_order_id(&mut self, id: impl Into<String>) {
286        self.exchange_order_id = Some(id.into());
287        self.updated_at = Utc::now();
288    }
289
290    /// Records a fill.
291    pub fn record_fill(&mut self, lots: u32, price: Money) {
292        let prev_filled = self.filled_lots;
293        self.filled_lots = (self.filled_lots + lots).min(self.quantity_lots);
294
295        // Update average fill price
296        if let Some(avg) = &self.average_fill_price {
297            if prev_filled > 0 {
298                let prev_value = avg.amount() * Decimal::from(prev_filled);
299                let new_value = price.amount() * Decimal::from(lots);
300                let total_lots = Decimal::from(self.filled_lots);
301                let new_avg = (prev_value + new_value) / total_lots;
302                self.average_fill_price = Some(Money::new(new_avg, price.currency()));
303            }
304        } else {
305            self.average_fill_price = Some(price);
306        }
307
308        // Update status
309        if self.filled_lots >= self.quantity_lots {
310            self.status = OrderStatus::Filled;
311        } else if self.filled_lots > 0 {
312            self.status = OrderStatus::PartiallyFilled;
313        }
314
315        self.updated_at = Utc::now();
316    }
317
318    /// Sets a message (e.g., rejection reason).
319    pub fn set_message(&mut self, message: impl Into<String>) {
320        self.message = Some(message.into());
321        self.updated_at = Utc::now();
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use rust_decimal_macros::dec;
329
330    fn make_usd(amount: Decimal) -> Money {
331        Money::new(amount, "USD")
332    }
333
334    #[test]
335    fn test_market_order_creation() {
336        let order = Order::market("ORD001", "AAPL", "NASDAQ", OrderDirection::Buy, 10);
337        assert_eq!(order.id(), "ORD001");
338        assert_eq!(order.symbol(), "AAPL");
339        assert_eq!(order.direction(), OrderDirection::Buy);
340        assert_eq!(order.order_type(), OrderType::Market);
341        assert_eq!(order.quantity_lots(), 10);
342        assert_eq!(order.status(), OrderStatus::Pending);
343    }
344
345    #[test]
346    fn test_limit_order_creation() {
347        let order = Order::limit(
348            "ORD002",
349            "SBER",
350            "MOEX",
351            OrderDirection::Sell,
352            5,
353            make_usd(dec!(250)),
354        );
355        assert_eq!(order.order_type(), OrderType::Limit);
356        assert_eq!(order.limit_price().unwrap().amount(), dec!(250));
357    }
358
359    #[test]
360    fn test_order_fill() {
361        let mut order = Order::market("ORD001", "AAPL", "NASDAQ", OrderDirection::Buy, 10);
362
363        order.record_fill(3, make_usd(dec!(150)));
364        assert_eq!(order.filled_lots(), 3);
365        assert_eq!(order.status(), OrderStatus::PartiallyFilled);
366        assert_eq!(order.average_fill_price().unwrap().amount(), dec!(150));
367
368        order.record_fill(7, make_usd(dec!(151)));
369        assert_eq!(order.filled_lots(), 10);
370        assert_eq!(order.status(), OrderStatus::Filled);
371    }
372
373    #[test]
374    fn test_order_status_terminal() {
375        assert!(!OrderStatus::Pending.is_terminal());
376        assert!(!OrderStatus::Submitted.is_terminal());
377        assert!(!OrderStatus::PartiallyFilled.is_terminal());
378        assert!(OrderStatus::Filled.is_terminal());
379        assert!(OrderStatus::Cancelled.is_terminal());
380        assert!(OrderStatus::Rejected.is_terminal());
381        assert!(OrderStatus::Expired.is_terminal());
382    }
383
384    #[test]
385    fn test_direction_opposite() {
386        assert_eq!(OrderDirection::Buy.opposite(), OrderDirection::Sell);
387        assert_eq!(OrderDirection::Sell.opposite(), OrderDirection::Buy);
388    }
389}