use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use super::Money;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OrderDirection {
Buy,
Sell,
}
impl OrderDirection {
#[must_use]
pub const fn opposite(self) -> Self {
match self {
Self::Buy => Self::Sell,
Self::Sell => Self::Buy,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OrderType {
Market,
Limit,
Stop,
StopLimit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OrderStatus {
Pending,
Submitted,
PartiallyFilled,
Filled,
Cancelled,
Rejected,
Expired,
}
impl OrderStatus {
#[must_use]
pub const fn is_terminal(self) -> bool {
matches!(
self,
Self::Filled | Self::Cancelled | Self::Rejected | Self::Expired
)
}
#[must_use]
pub const fn is_active(self) -> bool {
matches!(
self,
Self::Pending | Self::Submitted | Self::PartiallyFilled
)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Order {
id: String,
exchange_order_id: Option<String>,
symbol: String,
exchange: String,
direction: OrderDirection,
order_type: OrderType,
quantity_lots: u32,
limit_price: Option<Money>,
stop_price: Option<Money>,
status: OrderStatus,
filled_lots: u32,
average_fill_price: Option<Money>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
message: Option<String>,
}
impl Order {
#[must_use]
pub fn market(
id: impl Into<String>,
symbol: impl Into<String>,
exchange: impl Into<String>,
direction: OrderDirection,
quantity_lots: u32,
) -> Self {
let now = Utc::now();
Self {
id: id.into(),
exchange_order_id: None,
symbol: symbol.into(),
exchange: exchange.into(),
direction,
order_type: OrderType::Market,
quantity_lots,
limit_price: None,
stop_price: None,
status: OrderStatus::Pending,
filled_lots: 0,
average_fill_price: None,
created_at: now,
updated_at: now,
message: None,
}
}
#[must_use]
pub fn limit(
id: impl Into<String>,
symbol: impl Into<String>,
exchange: impl Into<String>,
direction: OrderDirection,
quantity_lots: u32,
limit_price: Money,
) -> Self {
let now = Utc::now();
Self {
id: id.into(),
exchange_order_id: None,
symbol: symbol.into(),
exchange: exchange.into(),
direction,
order_type: OrderType::Limit,
quantity_lots,
limit_price: Some(limit_price),
stop_price: None,
status: OrderStatus::Pending,
filled_lots: 0,
average_fill_price: None,
created_at: now,
updated_at: now,
message: None,
}
}
#[must_use]
pub fn id(&self) -> &str {
&self.id
}
#[must_use]
pub fn exchange_order_id(&self) -> Option<&str> {
self.exchange_order_id.as_deref()
}
#[must_use]
pub fn symbol(&self) -> &str {
&self.symbol
}
#[must_use]
pub fn exchange(&self) -> &str {
&self.exchange
}
#[must_use]
pub const fn direction(&self) -> OrderDirection {
self.direction
}
#[must_use]
pub const fn order_type(&self) -> OrderType {
self.order_type
}
#[must_use]
pub const fn quantity_lots(&self) -> u32 {
self.quantity_lots
}
#[must_use]
pub const fn limit_price(&self) -> Option<&Money> {
self.limit_price.as_ref()
}
#[must_use]
pub const fn status(&self) -> OrderStatus {
self.status
}
#[must_use]
pub const fn filled_lots(&self) -> u32 {
self.filled_lots
}
#[must_use]
pub const fn remaining_lots(&self) -> u32 {
self.quantity_lots - self.filled_lots
}
#[must_use]
pub const fn average_fill_price(&self) -> Option<&Money> {
self.average_fill_price.as_ref()
}
#[must_use]
pub const fn created_at(&self) -> DateTime<Utc> {
self.created_at
}
#[must_use]
pub const fn updated_at(&self) -> DateTime<Utc> {
self.updated_at
}
#[must_use]
pub fn message(&self) -> Option<&str> {
self.message.as_deref()
}
#[must_use]
pub fn fill_percentage(&self) -> Decimal {
if self.quantity_lots == 0 {
return Decimal::ZERO;
}
Decimal::from(self.filled_lots) / Decimal::from(self.quantity_lots) * Decimal::from(100)
}
pub fn set_status(&mut self, status: OrderStatus) {
self.status = status;
self.updated_at = Utc::now();
}
pub fn set_exchange_order_id(&mut self, id: impl Into<String>) {
self.exchange_order_id = Some(id.into());
self.updated_at = Utc::now();
}
pub fn record_fill(&mut self, lots: u32, price: Money) {
let prev_filled = self.filled_lots;
self.filled_lots = (self.filled_lots + lots).min(self.quantity_lots);
if let Some(avg) = &self.average_fill_price {
if prev_filled > 0 {
let prev_value = avg.amount() * Decimal::from(prev_filled);
let new_value = price.amount() * Decimal::from(lots);
let total_lots = Decimal::from(self.filled_lots);
let new_avg = (prev_value + new_value) / total_lots;
self.average_fill_price = Some(Money::new(new_avg, price.currency()));
}
} else {
self.average_fill_price = Some(price);
}
if self.filled_lots >= self.quantity_lots {
self.status = OrderStatus::Filled;
} else if self.filled_lots > 0 {
self.status = OrderStatus::PartiallyFilled;
}
self.updated_at = Utc::now();
}
pub fn set_message(&mut self, message: impl Into<String>) {
self.message = Some(message.into());
self.updated_at = Utc::now();
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn make_usd(amount: Decimal) -> Money {
Money::new(amount, "USD")
}
#[test]
fn test_market_order_creation() {
let order = Order::market("ORD001", "AAPL", "NASDAQ", OrderDirection::Buy, 10);
assert_eq!(order.id(), "ORD001");
assert_eq!(order.symbol(), "AAPL");
assert_eq!(order.direction(), OrderDirection::Buy);
assert_eq!(order.order_type(), OrderType::Market);
assert_eq!(order.quantity_lots(), 10);
assert_eq!(order.status(), OrderStatus::Pending);
}
#[test]
fn test_limit_order_creation() {
let order = Order::limit(
"ORD002",
"SBER",
"MOEX",
OrderDirection::Sell,
5,
make_usd(dec!(250)),
);
assert_eq!(order.order_type(), OrderType::Limit);
assert_eq!(order.limit_price().unwrap().amount(), dec!(250));
}
#[test]
fn test_order_fill() {
let mut order = Order::market("ORD001", "AAPL", "NASDAQ", OrderDirection::Buy, 10);
order.record_fill(3, make_usd(dec!(150)));
assert_eq!(order.filled_lots(), 3);
assert_eq!(order.status(), OrderStatus::PartiallyFilled);
assert_eq!(order.average_fill_price().unwrap().amount(), dec!(150));
order.record_fill(7, make_usd(dec!(151)));
assert_eq!(order.filled_lots(), 10);
assert_eq!(order.status(), OrderStatus::Filled);
}
#[test]
fn test_order_status_terminal() {
assert!(!OrderStatus::Pending.is_terminal());
assert!(!OrderStatus::Submitted.is_terminal());
assert!(!OrderStatus::PartiallyFilled.is_terminal());
assert!(OrderStatus::Filled.is_terminal());
assert!(OrderStatus::Cancelled.is_terminal());
assert!(OrderStatus::Rejected.is_terminal());
assert!(OrderStatus::Expired.is_terminal());
}
#[test]
fn test_direction_opposite() {
assert_eq!(OrderDirection::Buy.opposite(), OrderDirection::Sell);
assert_eq!(OrderDirection::Sell.opposite(), OrderDirection::Buy);
}
}