1use chrono::{DateTime, Utc};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6
7use super::Money;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum OrderDirection {
13 Buy,
15 Sell,
17}
18
19impl OrderDirection {
20 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum OrderType {
34 Market,
36 Limit,
38 Stop,
40 StopLimit,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(rename_all = "snake_case")]
47pub enum OrderStatus {
48 Pending,
50 Submitted,
52 PartiallyFilled,
54 Filled,
56 Cancelled,
58 Rejected,
60 Expired,
62}
63
64impl OrderStatus {
65 #[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 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86pub struct Order {
87 id: String,
89 exchange_order_id: Option<String>,
91 symbol: String,
93 exchange: String,
95 direction: OrderDirection,
97 order_type: OrderType,
99 quantity_lots: u32,
101 limit_price: Option<Money>,
103 stop_price: Option<Money>,
105 status: OrderStatus,
107 filled_lots: u32,
109 average_fill_price: Option<Money>,
111 created_at: DateTime<Utc>,
113 updated_at: DateTime<Utc>,
115 message: Option<String>,
117}
118
119impl Order {
120 #[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 #[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 #[must_use]
181 pub fn id(&self) -> &str {
182 &self.id
183 }
184
185 #[must_use]
187 pub fn exchange_order_id(&self) -> Option<&str> {
188 self.exchange_order_id.as_deref()
189 }
190
191 #[must_use]
193 pub fn symbol(&self) -> &str {
194 &self.symbol
195 }
196
197 #[must_use]
199 pub fn exchange(&self) -> &str {
200 &self.exchange
201 }
202
203 #[must_use]
205 pub const fn direction(&self) -> OrderDirection {
206 self.direction
207 }
208
209 #[must_use]
211 pub const fn order_type(&self) -> OrderType {
212 self.order_type
213 }
214
215 #[must_use]
217 pub const fn quantity_lots(&self) -> u32 {
218 self.quantity_lots
219 }
220
221 #[must_use]
223 pub const fn limit_price(&self) -> Option<&Money> {
224 self.limit_price.as_ref()
225 }
226
227 #[must_use]
229 pub const fn status(&self) -> OrderStatus {
230 self.status
231 }
232
233 #[must_use]
235 pub const fn filled_lots(&self) -> u32 {
236 self.filled_lots
237 }
238
239 #[must_use]
241 pub const fn remaining_lots(&self) -> u32 {
242 self.quantity_lots - self.filled_lots
243 }
244
245 #[must_use]
247 pub const fn average_fill_price(&self) -> Option<&Money> {
248 self.average_fill_price.as_ref()
249 }
250
251 #[must_use]
253 pub const fn created_at(&self) -> DateTime<Utc> {
254 self.created_at
255 }
256
257 #[must_use]
259 pub const fn updated_at(&self) -> DateTime<Utc> {
260 self.updated_at
261 }
262
263 #[must_use]
265 pub fn message(&self) -> Option<&str> {
266 self.message.as_deref()
267 }
268
269 #[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 pub fn set_status(&mut self, status: OrderStatus) {
280 self.status = status;
281 self.updated_at = Utc::now();
282 }
283
284 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 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 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 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 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}