1use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8use smartstring::{LazyCompact, SmartString};
9use std::fmt::{self, Display};
10use uuid::Uuid;
11
12use rusty_common::types::Exchange as Venue;
14use rusty_model::enums::{OrderSide, OrderStatus, OrderType, TimeInForce};
15
16type OmsString = SmartString<LazyCompact>;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub enum OmsOrderStatus {
24 New,
26 Accepted,
28 Sent,
30 Active,
32 ExchangeRejected,
34 Unknown,
36}
37
38impl From<OrderStatus> for OmsOrderStatus {
39 fn from(status: OrderStatus) -> Self {
40 match status {
41 OrderStatus::New => Self::New,
42 OrderStatus::Open => Self::Active,
43 OrderStatus::Rejected => Self::ExchangeRejected,
44 _ => Self::Unknown,
45 }
46 }
47}
48
49impl Display for OmsOrderStatus {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match self {
52 Self::New => write!(f, "New"),
53 Self::Accepted => write!(f, "Accepted"),
54 Self::Sent => write!(f, "Sent"),
55 Self::Active => write!(f, "Active"),
56 Self::ExchangeRejected => write!(f, "ExchangeRejected"),
57 Self::Unknown => write!(f, "Unknown"),
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
64pub enum RejectionReason {
65 InvalidParameters(String),
67 RiskCheckFailed(String),
69 InsufficientBalance,
71 ExchangeRejected(String),
73 RateLimitExceeded,
75 Unknown,
77}
78
79impl Display for RejectionReason {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 match self {
82 Self::InvalidParameters(s) => write!(f, "Invalid parameters: {s}"),
83 Self::RiskCheckFailed(s) => write!(f, "Risk check failed: {s}"),
84 Self::InsufficientBalance => write!(f, "Insufficient balance"),
85 Self::ExchangeRejected(s) => write!(f, "Exchange rejected: {s}"),
86 Self::RateLimitExceeded => write!(f, "Rate limit exceeded"),
87 Self::Unknown => write!(f, "Unknown failure"),
88 }
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct OrderRequest {
95 pub client_order_id: OmsString,
97 pub venue: Venue,
99 pub symbol: OmsString,
101 pub side: OrderSide,
103 pub order_type: OrderType,
105 pub quantity: Decimal,
107 pub price: Option<Decimal>,
109 pub stop_price: Option<Decimal>,
111 pub time_in_force: Option<TimeInForce>,
113 pub reduce_only: bool,
115 pub timestamp_ns: u64,
117 pub strategy_id: OmsString,
119 pub extra_params: simd_json::OwnedValue,
121 pub take_profit: Option<Decimal>,
123 pub stop_loss: Option<Decimal>,
125}
126
127impl OrderRequest {
128 #[must_use]
130 #[allow(clippy::too_many_arguments)]
131 pub fn new(
132 client_order_id: impl Into<OmsString>,
133 venue: Venue,
134 symbol: impl Into<OmsString>,
135 side: OrderSide,
136 order_type: OrderType,
137 quantity: Decimal,
138 price: Option<Decimal>,
139 strategy_id: impl Into<OmsString>,
140 ) -> Self {
141 let timestamp_ns = rusty_common::time::get_timestamp_ns_result().unwrap_or_else(|_| {
142 std::time::SystemTime::now()
143 .duration_since(std::time::UNIX_EPOCH)
144 .unwrap_or_default()
145 .as_nanos() as u64
146 });
147
148 Self {
149 client_order_id: client_order_id.into(),
150 venue,
151 symbol: symbol.into(),
152 side,
153 order_type,
154 quantity,
155 price,
156 stop_price: None,
157 time_in_force: None,
158 reduce_only: false,
159 timestamp_ns,
160 strategy_id: strategy_id.into(),
161 extra_params: simd_json::OwnedValue::from(()),
162 take_profit: None,
163 stop_loss: None,
164 }
165 }
166
167 pub fn market_buy(
169 venue: Venue,
170 symbol: impl Into<OmsString>,
171 quantity: Decimal,
172 strategy_id: impl Into<OmsString>,
173 ) -> Self {
174 Self::new(
175 OmsString::from(Uuid::new_v4().to_string()),
176 venue,
177 symbol,
178 OrderSide::Buy,
179 OrderType::Market,
180 quantity,
181 None,
182 strategy_id,
183 )
184 }
185
186 pub fn market_sell(
188 venue: Venue,
189 symbol: impl Into<OmsString>,
190 quantity: Decimal,
191 strategy_id: impl Into<OmsString>,
192 ) -> Self {
193 Self::new(
194 OmsString::from(Uuid::new_v4().to_string()),
195 venue,
196 symbol,
197 OrderSide::Sell,
198 OrderType::Market,
199 quantity,
200 None,
201 strategy_id,
202 )
203 }
204
205 pub fn limit_buy(
207 venue: Venue,
208 symbol: impl Into<OmsString>,
209 quantity: Decimal,
210 price: Decimal,
211 strategy_id: impl Into<OmsString>,
212 ) -> Self {
213 Self::new(
214 OmsString::from(Uuid::new_v4().to_string()),
215 venue,
216 symbol,
217 OrderSide::Buy,
218 OrderType::Limit,
219 quantity,
220 Some(price),
221 strategy_id,
222 )
223 }
224
225 pub fn limit_sell(
227 venue: Venue,
228 symbol: impl Into<OmsString>,
229 quantity: Decimal,
230 price: Decimal,
231 strategy_id: impl Into<OmsString>,
232 ) -> Self {
233 Self::new(
234 OmsString::from(Uuid::new_v4().to_string()),
235 venue,
236 symbol,
237 OrderSide::Sell,
238 OrderType::Limit,
239 quantity,
240 Some(price),
241 strategy_id,
242 )
243 }
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct Order {
249 pub id: Uuid,
251 pub client_order_id: OmsString,
253 pub exchange_order_id: Option<OmsString>,
255 pub venue: Venue,
257 pub symbol: OmsString,
259 pub side: OrderSide,
261 pub order_type: OrderType,
263 pub price: Option<Decimal>,
265 pub stop_price: Option<Decimal>,
267 pub quantity: Decimal,
269 pub filled_quantity: Decimal,
271 pub average_fill_price: Option<Decimal>,
273 pub status: OmsOrderStatus,
275 pub time_in_force: Option<TimeInForce>,
277 pub reduce_only: bool,
279 pub creation_time_ns: u64,
281 pub update_time_ns: u64,
283 pub strategy_id: OmsString,
285 pub rejection_reason: Option<RejectionReason>,
287 pub metadata: simd_json::OwnedValue,
289 pub take_profit: Option<Decimal>,
291 pub stop_loss: Option<Decimal>,
293}
294
295impl Order {
296 #[must_use]
298 pub fn from_request(request: &OrderRequest) -> Self {
299 let now = rusty_common::time::get_timestamp_ns_result().unwrap_or_else(|_| {
300 std::time::SystemTime::now()
301 .duration_since(std::time::UNIX_EPOCH)
302 .unwrap_or_default()
303 .as_nanos() as u64
304 });
305
306 Self {
307 id: Uuid::new_v4(),
308 client_order_id: request.client_order_id.clone(),
309 exchange_order_id: None,
310 venue: request.venue,
311 symbol: request.symbol.clone(),
312 side: request.side,
313 order_type: request.order_type,
314 price: request.price,
315 stop_price: request.stop_price,
316 quantity: request.quantity,
317 filled_quantity: Decimal::ZERO,
318 average_fill_price: None,
319 status: OmsOrderStatus::New,
320 time_in_force: request.time_in_force,
321 reduce_only: request.reduce_only,
322 creation_time_ns: now,
323 update_time_ns: now,
324 strategy_id: request.strategy_id.clone(),
325 rejection_reason: None,
326 metadata: request.extra_params.clone(),
327 take_profit: request.take_profit,
328 stop_loss: request.stop_loss,
329 }
330 }
331
332 #[must_use]
334 pub fn is_filled(&self) -> bool {
335 self.filled_quantity >= self.quantity
336 }
337
338 #[must_use]
340 pub fn remaining_quantity(&self) -> Decimal {
341 self.quantity - self.filled_quantity
342 }
343
344 pub fn update_status(&mut self, status: OmsOrderStatus) {
346 self.status = status;
347 self.update_time_ns = rusty_common::time::get_timestamp_ns_result().unwrap_or_else(|_| {
348 std::time::SystemTime::now()
349 .duration_since(std::time::UNIX_EPOCH)
350 .unwrap_or_default()
351 .as_nanos() as u64
352 });
353 }
354
355 pub fn reject(&mut self, reason: RejectionReason) {
357 self.status = OmsOrderStatus::ExchangeRejected;
358 self.rejection_reason = Some(reason);
359 self.update_time_ns = rusty_common::time::get_timestamp_ns_result().unwrap_or_else(|_| {
360 std::time::SystemTime::now()
361 .duration_since(std::time::UNIX_EPOCH)
362 .unwrap_or_default()
363 .as_nanos() as u64
364 });
365 }
366
367 pub fn add_fill(&mut self, fill_quantity: Decimal, fill_price: Decimal) {
369 let old_fill_qty = self.filled_quantity;
370 self.filled_quantity += fill_quantity;
371
372 if let Some(avg_price) = self.average_fill_price {
374 let total_value = avg_price * old_fill_qty + fill_price * fill_quantity;
376 self.average_fill_price = Some(total_value / self.filled_quantity);
377 } else {
378 self.average_fill_price = Some(fill_price);
379 }
380
381 if self.filled_quantity >= self.quantity {
383 }
386
387 self.update_time_ns = rusty_common::time::get_timestamp_ns_result().unwrap_or_else(|_| {
388 std::time::SystemTime::now()
389 .duration_since(std::time::UNIX_EPOCH)
390 .unwrap_or_default()
391 .as_nanos() as u64
392 });
393 }
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize)]
398pub struct OrderResponse {
399 pub order_id: Uuid,
401 pub client_order_id: OmsString,
403 pub status: OmsOrderStatus,
405 pub message: OmsString,
407 pub rejection_reason: Option<RejectionReason>,
409 pub timestamp_ns: u64,
411}
412
413impl OrderResponse {
414 #[must_use]
416 pub fn success(order: &Order) -> Self {
417 let now = rusty_common::time::get_timestamp_ns_result().unwrap_or_else(|_| {
418 std::time::SystemTime::now()
419 .duration_since(std::time::UNIX_EPOCH)
420 .unwrap_or_default()
421 .as_nanos() as u64
422 });
423
424 Self {
425 order_id: order.id,
426 client_order_id: order.client_order_id.clone(),
427 status: order.status,
428 message: format!("Order {id} accepted", id = order.id).into(),
429 rejection_reason: None,
430 timestamp_ns: now,
431 }
432 }
433
434 pub fn rejected(
436 order_id: Uuid,
437 client_order_id: impl Into<OmsString>,
438 reason: RejectionReason,
439 ) -> Self {
440 let now = rusty_common::time::get_timestamp_ns_result().unwrap_or_else(|_| {
441 std::time::SystemTime::now()
442 .duration_since(std::time::UNIX_EPOCH)
443 .unwrap_or_default()
444 .as_nanos() as u64
445 });
446
447 Self {
448 order_id,
449 client_order_id: client_order_id.into(),
450 status: OmsOrderStatus::ExchangeRejected,
451 message: format!("Order rejected: {reason}").into(),
452 rejection_reason: Some(reason),
453 timestamp_ns: now,
454 }
455 }
456}
457
458#[derive(Debug, Clone, Serialize, Deserialize)]
460pub struct OrderUpdate {
461 pub order_id: Uuid,
463 pub client_order_id: OmsString,
465 pub exchange_order_id: Option<OmsString>,
467 pub status: OmsOrderStatus,
469 pub filled_quantity: Decimal,
471 pub average_fill_price: Option<Decimal>,
473 pub update_time_ns: u64,
475 pub rejection_reason: Option<RejectionReason>,
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct OrderBook {
482 pub bids: Vec<(Decimal, Decimal)>,
484 pub asks: Vec<(Decimal, Decimal)>,
486}
487
488impl OrderUpdate {
489 #[must_use]
491 pub fn from_order(order: &Order) -> Self {
492 Self {
493 order_id: order.id,
494 client_order_id: order.client_order_id.clone(),
495 exchange_order_id: order.exchange_order_id.clone(),
496 status: order.status,
497 filled_quantity: order.filled_quantity,
498 average_fill_price: order.average_fill_price,
499 update_time_ns: order.update_time_ns,
500 rejection_reason: order.rejection_reason.clone(),
501 }
502 }
503}
504
505#[derive(Debug, Clone, Serialize, Deserialize)]
507pub enum OrderEvent {
508 New(OrderRequest),
510 Accepted(Uuid),
512 Sent {
514 order_id: Uuid,
516 exchange_order_id: Option<String>,
518 },
519 Active(Uuid),
521 PartialFill {
523 order_id: Uuid,
525 fill_quantity: Decimal,
527 fill_price: Decimal,
529 timestamp_ns: u64,
531 },
532 Filled {
534 order_id: Uuid,
536 fill_quantity: Decimal,
538 fill_price: Decimal,
540 timestamp_ns: u64,
542 },
543 Canceled {
545 order_id: Uuid,
547 remaining_quantity: Decimal,
549 timestamp_ns: u64,
551 },
552 Rejected {
554 order_id: Uuid,
556 reason: RejectionReason,
558 timestamp_ns: u64,
560 },
561}