From c2118b3073bbc4fb899b59af34fe556b4a20f2ba Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Sun, 1 Mar 2026 20:32:52 +0100 Subject: [PATCH] refactor: split common/types.rs (4909 LOC) into 10 focused modules Split god file into: aliases.rs, domain.rs, events.rs, identifiers.rs, market_data.rs, market.rs, service.rs, trading_enums.rs, type_error.rs. Re-exports via mod.rs maintain backward compatibility. Co-Authored-By: Claude Opus 4.6 --- crates/common/src/types.rs | 4909 ---------------------- crates/common/src/types/aliases.rs | 61 + crates/common/src/types/domain.rs | 2360 +++++++++++ crates/common/src/types/events.rs | 54 + crates/common/src/types/identifiers.rs | 248 ++ crates/common/src/types/market.rs | 472 +++ crates/common/src/types/market_data.rs | 562 +++ crates/common/src/types/mod.rs | 507 +++ crates/common/src/types/service.rs | 165 + crates/common/src/types/trading_enums.rs | 295 ++ crates/common/src/types/type_error.rs | 283 ++ 11 files changed, 5007 insertions(+), 4909 deletions(-) delete mode 100644 crates/common/src/types.rs create mode 100644 crates/common/src/types/aliases.rs create mode 100644 crates/common/src/types/domain.rs create mode 100644 crates/common/src/types/events.rs create mode 100644 crates/common/src/types/identifiers.rs create mode 100644 crates/common/src/types/market.rs create mode 100644 crates/common/src/types/market_data.rs create mode 100644 crates/common/src/types/mod.rs create mode 100644 crates/common/src/types/service.rs create mode 100644 crates/common/src/types/trading_enums.rs create mode 100644 crates/common/src/types/type_error.rs diff --git a/crates/common/src/types.rs b/crates/common/src/types.rs deleted file mode 100644 index f176ae335..000000000 --- a/crates/common/src/types.rs +++ /dev/null @@ -1,4909 +0,0 @@ -//! Common data types used across services -//! -//! This module provides shared data types that are used throughout -//! the Foxhunt HFT trading system. This includes both infrastructure types -//! and core trading types migrated from foxhunt-common-types. - -use crate::error::ErrorCategory; -use chrono::{DateTime, Utc}; -// ELIMINATED: Re-exports removed to force explicit imports -// NO RE-EXPORTS: Import rust_decimal::Decimal directly in each crate that needs it -use rust_decimal::Decimal; // Internal use only - other crates must import directly -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::collections::HashMap; -use std::sync::{Arc, Mutex, RwLock}; - -use crate::error::{CommonError, ErrorCategory as CommonErrorCategory}; -use std::convert::TryFrom; -use std::fmt; -use std::iter::Sum; -use std::num::ParseIntError; -use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; -use std::str::FromStr; -use uuid::Uuid; - -// ============================================================================= -// Type Aliases for Complex Types -// ============================================================================= - -/// Common error type for async operations -pub type AsyncResult = Result>; - -/// Thread-safe hash map for shared state -pub type SharedHashMap = Arc>>; - -/// Thread-safe hash map with Mutex for shared state -pub type MutexHashMap = Arc>>; - -/// Thread-safe container for any value -pub type SharedValue = Arc>; - -/// Thread-safe container with Mutex for any value -pub type MutexValue = Arc>; - -// Trading-specific type aliases -/// Map of positions by symbol -pub type PositionMap = SharedHashMap; - -/// Map of orders by order ID -pub type OrderMap = SharedHashMap; - -/// Map of accounts by account ID -pub type AccountMap = SharedHashMap; - -/// Map of instruments by instrument ID -pub type InstrumentMap = SharedHashMap; - -/// Map of market data by symbol -pub type MarketDataMap = SharedHashMap; - -/// Cache entry with timestamp -pub type CacheEntry = (T, DateTime); - -/// Cache map with timestamped entries -pub type CacheMap = SharedHashMap>; - -/// Risk factor loadings by instrument -pub type RiskFactorMap = SharedHashMap>; - -/// Performance metrics history -pub type PerformanceHistory = SharedHashMap>; - -/// Model registry for ML models -pub type ModelRegistry = SharedHashMap; - -/// Generic configuration cache -pub type ConfigCache = SharedHashMap; - -// ============================================================================= -// Event Types - Moved from trading_engine to enforce pure client architecture -// ============================================================================= - -/// Order events for the complete order lifecycle -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OrderEvent { - /// Unique identifier for the order - pub order_id: OrderId, - /// Trading symbol for the order - pub symbol: Symbol, - /// Type of order (market, limit, stop, etc.) - pub order_type: OrderType, - /// Order side (buy or sell) - pub side: OrderSide, - /// Order quantity - pub quantity: Quantity, - /// Order price (None for market orders) - pub price: Option, - /// Timestamp when the event occurred - pub timestamp: DateTime, - /// Strategy or client identifier - pub strategy_id: String, - /// Order event type (placed, modified, cancelled) - pub event_type: OrderEventType, - /// Previous quantity for modifications - pub previous_quantity: Option, - /// Previous price for modifications - pub previous_price: Option, - /// Reason for cancellation or modification - pub reason: Option, -} - -/// Types of order events -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum OrderEventType { - /// Order was placed - Placed, - /// Order was modified - Modified, - /// Order was cancelled - Cancelled, - /// Order was rejected - Rejected, -} - -// ============================================================================= -// Core Data Types -// ============================================================================= - -/// Unique identifier for services -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct ServiceId(pub String); - -impl ServiceId { - /// Create a new service ID - pub fn new>(id: S) -> Self { - Self(id.into()) - } - - /// Get the inner string value - /// - /// Get the execution ID as a string slice - /// - /// Get execution ID as string slice - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl fmt::Display for ServiceId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From<&str> for ServiceId { - fn from(s: &str) -> Self { - Self(s.to_owned()) - } -} - -impl From for ServiceId { - fn from(s: String) -> Self { - Self(s) - } -} - -/// Service status enumeration -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum ServiceStatus { - /// Service is starting up - Starting, - /// Service is running normally - Running, - /// Service is degraded but functional - Degraded, - /// Service is stopping - Stopping, - /// Service is stopped - Stopped, - /// Service has encountered an error - Error, - /// Service is in maintenance mode - Maintenance, -} - -impl fmt::Display for ServiceStatus { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Starting => write!(f, "STARTING"), - Self::Running => write!(f, "RUNNING"), - Self::Degraded => write!(f, "DEGRADED"), - Self::Stopping => write!(f, "STOPPING"), - Self::Stopped => write!(f, "STOPPED"), - Self::Error => write!(f, "ERROR"), - Self::Maintenance => write!(f, "MAINTENANCE"), - } - } -} - -impl ServiceStatus { - /// Check if the service is healthy - pub const fn is_healthy(&self) -> bool { - matches!(self, Self::Running | Self::Starting) - } - - /// Check if the service is available for requests - pub const fn is_available(&self) -> bool { - matches!(self, Self::Running | Self::Degraded) - } -} - -/// Configuration version for tracking changes -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ConfigVersion { - /// Version number - pub version: u64, - /// Timestamp when version was created - pub timestamp: DateTime, - /// Optional description of changes - pub description: Option, -} - -impl ConfigVersion { - /// Create a new config version - pub fn new(version: u64) -> Self { - Self { - version, - timestamp: Utc::now(), - description: None, - } - } - - /// Create a new config version with description - pub fn with_description>(version: u64, description: S) -> Self { - Self { - version, - timestamp: Utc::now(), - description: Some(description.into()), - } - } -} - -// TECHNICAL DEBT ELIMINATED - Use DateTime directly instead of Timestamp alias - -/// Timestamp type alias for consistency across the system -pub type Timestamp = DateTime; - -/// Request ID for tracing and correlation -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct RequestId(pub Uuid); - -impl Default for RequestId { - /// Create a default request ID with a new UUID - fn default() -> Self { - Self::new() - } -} - -impl RequestId { - /// Generate a new random request ID - pub fn new() -> Self { - Self(Uuid::new_v4()) - } - - /// Create from UUID - pub const fn from_uuid(uuid: Uuid) -> Self { - Self(uuid) - } - - /// Get the inner UUID - pub const fn as_uuid(&self) -> Uuid { - self.0 - } -} - -// Default implementation is now in the derive macro above - -// ============================================================================= -// MARKET DATA EVENT TYPES (Consolidated from data and trading_engine crates) -// ============================================================================= - -/// Market data event types - CANONICAL DEFINITION -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum MarketDataEvent { - /// Quote update (bid/ask) - Quote(QuoteEvent), - /// Trade execution - Trade(TradeEvent), - /// Aggregate trade data - Aggregate(Aggregate), - /// Bar/candle data - Bar(BarEvent), - /// Level 2 market data update - Level2(Level2Update), - /// Market status update - Status(MarketStatus), - /// Connection status updates - ConnectionStatus(ConnectionEvent), - /// Error events with details - Error(ErrorEvent), - /// Order book update - OrderBook(OrderBookEvent), - /// Level 2 order book snapshot - OrderBookL2Snapshot(OrderBookSnapshot), - /// Level 2 order book incremental update - OrderBookL2Update(OrderBookUpdate), -} - -/// Quote event structure - CANONICAL DEFINITION -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct QuoteEvent { - /// Symbol - pub symbol: String, - /// Bid price - pub bid: Option, - /// Ask price - pub ask: Option, - /// Bid size - pub bid_size: Option, - /// Ask size - pub ask_size: Option, - /// Exchange - pub exchange: Option, - /// Bid exchange - pub bid_exchange: Option, - /// Ask exchange - pub ask_exchange: Option, - /// Quote conditions - pub conditions: Vec, - /// Timestamp - pub timestamp: DateTime, - /// Sequence number - pub sequence: u64, -} - -impl QuoteEvent { - /// Create a new quote event - #[must_use] - pub const fn new(symbol: String, timestamp: DateTime) -> Self { - Self { - symbol, - bid: None, - ask: None, - bid_size: None, - ask_size: None, - exchange: None, - bid_exchange: None, - ask_exchange: None, - conditions: Vec::new(), - timestamp, - sequence: 0, - } - } - - /// Set bid price and size - pub const fn with_bid(mut self, price: Decimal, size: Decimal) -> Self { - self.bid = Some(price); - self.bid_size = Some(size); - self - } - - /// Set ask price and size - pub const fn with_ask(mut self, price: Decimal, size: Decimal) -> Self { - self.ask = Some(price); - self.ask_size = Some(size); - self - } - - /// Set exchange - pub fn with_exchange>(mut self, exchange: S) -> Self { - self.exchange = Some(exchange.into()); - self - } - - /// Set bid exchange - pub fn with_bid_exchange>(mut self, exchange: S) -> Self { - self.bid_exchange = Some(exchange.into()); - self - } - - /// Set ask exchange - pub fn with_ask_exchange>(mut self, exchange: S) -> Self { - self.ask_exchange = Some(exchange.into()); - self - } - - /// Add quote condition - pub fn with_condition>(mut self, condition: S) -> Self { - self.conditions.push(condition.into()); - self - } - - /// Set sequence number - pub const fn with_sequence(mut self, sequence: u64) -> Self { - self.sequence = sequence; - self - } - - /// Get mid price - pub fn mid_price(&self) -> Option { - match (self.bid, self.ask) { - (Some(bid), Some(ask)) => { - let sum = bid.checked_add(ask)?; - let two = Decimal::from(2); - sum.checked_div(two) - }, - _ => None, - } - } - - /// Get spread - pub fn spread(&self) -> Option { - match (self.bid, self.ask) { - (Some(bid), Some(ask)) => ask.checked_sub(bid), - _ => None, - } - } -} - -/// Trade event structure - CANONICAL DEFINITION -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TradeEvent { - /// Symbol - pub symbol: String, - /// Trade price - pub price: Decimal, - /// Trade size - pub size: Decimal, - /// Trade ID - pub trade_id: Option, - /// Exchange - pub exchange: Option, - /// Trade conditions - pub conditions: Vec, - /// Timestamp - pub timestamp: DateTime, - /// Sequence number - pub sequence: u64, -} - -impl TradeEvent { - /// Create a new trade event - #[must_use] - pub const fn new( - symbol: String, - price: Decimal, - size: Decimal, - timestamp: DateTime, - ) -> Self { - Self { - symbol, - price, - size, - trade_id: None, - exchange: None, - conditions: Vec::new(), - timestamp, - sequence: 0, - } - } - - /// Set trade ID - pub fn with_trade_id>(mut self, trade_id: S) -> Self { - self.trade_id = Some(trade_id.into()); - self - } - - /// Set exchange - pub fn with_exchange>(mut self, exchange: S) -> Self { - self.exchange = Some(exchange.into()); - self - } - - /// Add trade condition - pub fn with_condition>(mut self, condition: S) -> Self { - self.conditions.push(condition.into()); - self - } - - /// Set sequence number - pub const fn with_sequence(mut self, sequence: u64) -> Self { - self.sequence = sequence; - self - } - - /// Get notional value - pub fn notional_value(&self) -> Decimal { - self.price.checked_mul(self.size).unwrap_or(Decimal::ZERO) - } -} - -/// Aggregate trade data -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Aggregate { - /// Symbol - pub symbol: String, - /// Open price - pub open: Decimal, - /// High price - pub high: Decimal, - /// Low price - pub low: Decimal, - /// Close price - pub close: Decimal, - /// Volume - pub volume: Decimal, - /// Volume weighted average price - pub vwap: Option, - /// Start timestamp - pub start_timestamp: DateTime, - /// End timestamp - pub end_timestamp: DateTime, -} - -/// Bar/candle event structure -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BarEvent { - /// Symbol - pub symbol: String, - /// Open price - pub open: Decimal, - /// High price - pub high: Decimal, - /// Low price - pub low: Decimal, - /// Close price - pub close: Decimal, - /// Volume - pub volume: Decimal, - /// Volume weighted average price - pub vwap: Option, - /// Start timestamp - pub start_timestamp: DateTime, - /// End timestamp - pub end_timestamp: DateTime, - /// Timeframe (e.g., "1m", "5m", "1h") - pub timeframe: String, -} - -/// Level 2 market data update -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Level2Update { - /// Symbol - pub symbol: String, - /// Bid levels - pub bids: Vec, - /// Ask levels - pub asks: Vec, - /// Timestamp - pub timestamp: DateTime, -} - -/// Price level for order book -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PriceLevel { - /// Price - pub price: Decimal, - /// Size at this price level - pub size: Decimal, -} - -/// Order book snapshot from providers -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OrderBookSnapshot { - /// Symbol - pub symbol: String, - /// Bid levels (price, size) sorted by price descending - pub bids: Vec, - /// Ask levels (price, size) sorted by price ascending - pub asks: Vec, - /// Exchange - pub exchange: String, - /// Timestamp of snapshot - pub timestamp: DateTime, - /// Sequence number - pub sequence: u64, -} - -/// Incremental order book update from providers -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OrderBookUpdate { - /// Symbol - pub symbol: String, - /// Changes to bid levels - pub bid_changes: Vec, - /// Changes to ask levels - pub ask_changes: Vec, - /// Exchange - pub exchange: String, - /// Timestamp of update - pub timestamp: DateTime, - /// Sequence number - pub sequence: u64, -} - -/// Change to a price level -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PriceLevelChange { - /// Price level being modified - pub price: Decimal, - /// New size (0 = remove level) - pub size: Decimal, - /// Type of change - pub change_type: PriceLevelChangeType, - /// Side (bid or ask) - pub side: OrderBookSide, -} - -/// Type of price level change -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] -pub enum PriceLevelChangeType { - /// Add new price level - Add, - /// Update existing price level - Update, - /// Remove price level - Delete, -} - -/// Order book side -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] -pub enum OrderBookSide { - /// Bid side - Bid, - /// Ask side - Ask, -} - -/// Market status information -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MarketStatus { - /// Market - pub market: String, - /// Status (open, closed, `early_hours`, etc.) - pub status: String, - /// Timestamp - pub timestamp: DateTime, -} - -/// Connection event for status updates -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ConnectionEvent { - /// Provider name - pub provider: String, - /// Connection status - pub status: ConnectionStatus, - /// Optional message - pub message: Option, - /// Timestamp - pub timestamp: DateTime, -} - -/// Connection status enumeration -/// -/// Connection status for data providers and brokers -#[derive(Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "database", derive(sqlx::Type))] -#[cfg_attr( - feature = "database", - sqlx(type_name = "connection_status", rename_all = "snake_case") -)] -pub enum ConnectionStatus { - /// Successfully connected and operational - Connected, - /// Disconnected from the service - Disconnected, - /// Currently attempting to reconnect - Reconnecting, -} - -/// Error event structure -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ErrorEvent { - /// Provider name - pub provider: String, - /// Error message - pub message: String, - /// Error category - pub category: ErrorCategory, - /// Timestamp - pub timestamp: DateTime, -} - -// ErrorCategory is imported from crate::error as CommonErrorCategory - -/// Order book event -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OrderBookEvent { - /// Symbol - pub symbol: String, - /// Timestamp - pub timestamp: DateTime, - /// Bid levels - pub bids: Vec<(Price, Quantity)>, - /// Ask levels - pub asks: Vec<(Price, Quantity)>, -} - -/// Data types for subscription -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum DataType { - /// Real-time quotes - Quotes, - /// Real-time trades - Trades, - /// Aggregate/minute bars - Aggregates, - /// Level 2 order book - Level2, - /// Market status - Status, - /// Historical bars/aggregates - Bars, - /// Order book data - OrderBook, - /// Volume data - Volume, -} - -/// Market data subscription request -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Subscription { - /// Symbols to subscribe to - pub symbols: Vec, - /// Data types to subscribe to - pub data_types: Vec, - /// Exchange filter (optional) - pub exchanges: Vec, -} - -impl MarketDataEvent { - /// Get the symbol for any market data event - pub fn symbol(&self) -> &str { - match self { - MarketDataEvent::Quote(q) => &q.symbol, - MarketDataEvent::Trade(t) => &t.symbol, - MarketDataEvent::Aggregate(a) => &a.symbol, - MarketDataEvent::Bar(b) => &b.symbol, - MarketDataEvent::Level2(l) => &l.symbol, - MarketDataEvent::Status(s) => &s.market, - MarketDataEvent::ConnectionStatus(_) => "", - MarketDataEvent::Error(_) => "", - MarketDataEvent::OrderBook(o) => &o.symbol, - MarketDataEvent::OrderBookL2Snapshot(s) => &s.symbol, - MarketDataEvent::OrderBookL2Update(u) => &u.symbol, - } - } - - /// Get the timestamp for any market data event - pub const fn timestamp(&self) -> Option> { - match self { - MarketDataEvent::Quote(q) => Some(q.timestamp), - MarketDataEvent::Trade(t) => Some(t.timestamp), - MarketDataEvent::Aggregate(a) => Some(a.end_timestamp), - MarketDataEvent::Bar(b) => Some(b.end_timestamp), - MarketDataEvent::Level2(l) => Some(l.timestamp), - MarketDataEvent::Status(s) => Some(s.timestamp), - MarketDataEvent::ConnectionStatus(c) => Some(c.timestamp), - MarketDataEvent::Error(e) => Some(e.timestamp), - MarketDataEvent::OrderBook(o) => Some(o.timestamp), - MarketDataEvent::OrderBookL2Snapshot(s) => Some(s.timestamp), - MarketDataEvent::OrderBookL2Update(u) => Some(u.timestamp), - } - } -} -impl fmt::Display for RequestId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -/// Connection information for services -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ConnectionInfo { - /// Host address - pub host: String, - /// Port number - pub port: u16, - /// Whether TLS is enabled - pub tls: bool, - /// Connection timeout in milliseconds - pub timeout_ms: u64, -} - -impl ConnectionInfo { - /// Create new connection info - pub fn new>(host: S, port: u16) -> Self { - Self { - host: host.into(), - port, - tls: false, - timeout_ms: 5000, - } - } - - /// Enable TLS - pub const fn with_tls(mut self) -> Self { - self.tls = true; - self - } - - /// Set timeout - pub const fn with_timeout(mut self, timeout_ms: u64) -> Self { - self.timeout_ms = timeout_ms; - self - } - - /// Get connection URL - pub fn url(&self) -> String { - let scheme = if self.tls { "https" } else { "http" }; - format!("{}://{}:{}", scheme, self.host, self.port) - } -} - -/// Resource limits for services -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct ResourceLimits { - /// Maximum memory usage in bytes - pub max_memory_bytes: Option, - /// Maximum CPU usage as percentage (0-100) - pub max_cpu_percent: Option, - /// Maximum number of open file descriptors - pub max_file_descriptors: Option, - /// Maximum number of network connections - pub max_connections: Option, -} - -// ============================================================================= -// TRADING TYPES (Migrated from foxhunt-common-types) -// ============================================================================= - -/// Common error types for trading operations -/// -/// This error type implements Send + Sync for use in async contexts -#[derive(thiserror::Error, Debug)] -pub enum CommonTypeError { - /// Invalid price value - #[error("Invalid price: {value} - {reason}")] - InvalidPrice { - /// The invalid price value as string - value: String, - /// Reason why the price is invalid - reason: String, - }, - - /// Invalid quantity value - #[error("Invalid quantity: {value} - {reason}")] - InvalidQuantity { - /// The invalid quantity value as string - value: String, - /// Reason why the quantity is invalid - reason: String, - }, - - /// Invalid identifier - #[error("Invalid {field}: {reason}")] - InvalidIdentifier { - /// The field name that contains the invalid identifier - field: String, - /// Reason why the identifier is invalid - reason: String, - }, - - /// Validation error - #[error("Validation error for {field}: {reason}")] - ValidationError { - /// The field name that failed validation - field: String, - /// Reason why the validation failed - reason: String, - }, - - /// Conversion error - #[error("Conversion error: {message}")] - ConversionError { - /// Detailed error message describing the conversion failure - message: String, - }, - - /// I/O error - #[error("I/O error: {0}")] - IoError(#[from] std::io::Error), - - /// JSON serialization/deserialization error - #[error("JSON error: {0}")] - JsonError(#[from] serde_json::Error), - - /// Float parsing error - #[error("Float parsing error: {0}")] - ParseFloatError(#[from] std::num::ParseFloatError), - - /// Integer parsing error - #[error("Integer parsing error: {0}")] - ParseIntError(#[from] std::num::ParseIntError), -} - -// Manual trait implementations for CommonTypeError -// (Cannot derive Clone, PartialEq, Eq, Serialize due to std::io::Error and serde_json::Error) - -impl Clone for CommonTypeError { - /// Clone the error, converting IO and JSON errors to conversion errors - fn clone(&self) -> Self { - match self { - Self::InvalidPrice { value, reason } => Self::InvalidPrice { - value: value.clone(), - reason: reason.clone(), - }, - Self::InvalidQuantity { value, reason } => Self::InvalidQuantity { - value: value.clone(), - reason: reason.clone(), - }, - Self::InvalidIdentifier { field, reason } => Self::InvalidIdentifier { - field: field.clone(), - reason: reason.clone(), - }, - Self::ValidationError { field, reason } => Self::ValidationError { - field: field.clone(), - reason: reason.clone(), - }, - Self::ConversionError { message } => Self::ConversionError { - message: message.clone(), - }, - // Cannot clone std::io::Error or serde_json::Error, so create new instances - Self::IoError(e) => Self::ConversionError { - message: format!("I/O error: {}", e), - }, - Self::JsonError(e) => Self::ConversionError { - message: format!("JSON error: {}", e), - }, - Self::ParseFloatError(e) => Self::ParseFloatError(e.clone()), - Self::ParseIntError(e) => Self::ParseIntError(e.clone()), - } - } -} -impl PartialEq for CommonTypeError { - /// Compare two errors for equality - fn eq(&self, other: &Self) -> bool { - match (self, other) { - ( - Self::InvalidPrice { - value: v1, - reason: r1, - }, - Self::InvalidPrice { - value: v2, - reason: r2, - }, - ) => v1 == v2 && r1 == r2, - ( - Self::InvalidQuantity { - value: v1, - reason: r1, - }, - Self::InvalidQuantity { - value: v2, - reason: r2, - }, - ) => v1 == v2 && r1 == r2, - ( - Self::InvalidIdentifier { - field: f1, - reason: r1, - }, - Self::InvalidIdentifier { - field: f2, - reason: r2, - }, - ) => f1 == f2 && r1 == r2, - ( - Self::ValidationError { - field: f1, - reason: r1, - }, - Self::ValidationError { - field: f2, - reason: r2, - }, - ) => f1 == f2 && r1 == r2, - (Self::ConversionError { message: m1 }, Self::ConversionError { message: m2 }) => { - m1 == m2 - }, - (Self::ParseFloatError(e1), Self::ParseFloatError(e2)) => e1 == e2, - (Self::ParseIntError(e1), Self::ParseIntError(e2)) => e1 == e2, - // std::io::Error and serde_json::Error don't implement PartialEq, so they're never equal - (Self::IoError(_), Self::IoError(_)) => false, - (Self::JsonError(_), Self::JsonError(_)) => false, - _ => false, - } - } -} - -impl Eq for CommonTypeError {} - -// Note: Display is automatically implemented by thiserror::Error derive -// based on the #[error("...")] attributes on each variant -impl Serialize for CommonTypeError { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - match self { - Self::InvalidPrice { value, reason } => { - let mut state = serializer.serialize_struct("InvalidPrice", 2)?; - state.serialize_field("value", value)?; - state.serialize_field("reason", reason)?; - state.end() - }, - Self::InvalidQuantity { value, reason } => { - let mut state = serializer.serialize_struct("InvalidQuantity", 2)?; - state.serialize_field("value", value)?; - state.serialize_field("reason", reason)?; - state.end() - }, - Self::InvalidIdentifier { field, reason } => { - let mut state = serializer.serialize_struct("InvalidIdentifier", 2)?; - state.serialize_field("field", field)?; - state.serialize_field("reason", reason)?; - state.end() - }, - Self::ValidationError { field, reason } => { - let mut state = serializer.serialize_struct("ValidationError", 2)?; - state.serialize_field("field", field)?; - state.serialize_field("reason", reason)?; - state.end() - }, - Self::ConversionError { message } => { - let mut state = serializer.serialize_struct("ConversionError", 1)?; - state.serialize_field("message", message)?; - state.end() - }, - Self::IoError(e) => { - let mut state = serializer.serialize_struct("IoError", 1)?; - state.serialize_field("message", &format!("I/O error: {}", e))?; - state.end() - }, - Self::JsonError(e) => { - let mut state = serializer.serialize_struct("JsonError", 1)?; - state.serialize_field("message", &format!("JSON error: {}", e))?; - state.end() - }, - Self::ParseFloatError(e) => { - let mut state = serializer.serialize_struct("ParseFloatError", 1)?; - state.serialize_field("message", &format!("Float parsing error: {}", e))?; - state.end() - }, - Self::ParseIntError(e) => { - let mut state = serializer.serialize_struct("ParseIntError", 1)?; - state.serialize_field("message", &format!("Integer parsing error: {}", e))?; - state.end() - }, - } - } -} - -impl<'de> Deserialize<'de> for CommonTypeError { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - // For deserialization, we'll convert everything to ConversionError since - // we can't reconstruct std::io::Error or serde_json::Error from serialized form - use serde::de::{MapAccess, Visitor}; - use std::fmt; - - struct CommonTypeErrorVisitor; - - impl<'de> Visitor<'de> for CommonTypeErrorVisitor { - type Value = CommonTypeError; - - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("a CommonTypeError") - } - - fn visit_map(self, mut map: V) -> Result - where - V: MapAccess<'de>, - { - // For simplicity, deserialize everything as ConversionError - let mut message = String::new(); - while let Some(key) = map.next_key::()? { - let value: serde_json::Value = map.next_value()?; - if key == "message" { - if let Some(msg) = value.as_str() { - message = msg.to_owned(); - } - } else { - message = format!("Deserialized error: {}: {}", key, value); - } - } - if message.is_empty() { - message = "Unknown deserialized error".to_owned(); - } - Ok(CommonTypeError::ConversionError { message }) - } - } - - deserializer.deserialize_struct( - "CommonTypeError", - &["value", "reason", "field", "message"], - CommonTypeErrorVisitor, - ) - } -} - -// ============================================================================= -// ORDER TYPES (Moved from trading_engine) -// ============================================================================= - -/// Order type specifying execution behavior - CANONICAL DEFINITION -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[non_exhaustive] -pub enum OrderType { - /// Market order - executes immediately at current market price - Market, - /// Limit order - executes only at specified price or better - Limit, - /// Stop order - becomes market order when stop price is reached - Stop, - /// Stop-limit order - becomes limit order when stop price is reached - StopLimit, - /// Iceberg order - large order split into smaller visible portions - Iceberg, - /// Trailing stop order - stop price adjusts with favorable price movement - TrailingStop, - /// Hidden order - not displayed in order book - Hidden, -} - -impl fmt::Display for OrderType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Market => write!(f, "MARKET"), - Self::Limit => write!(f, "LIMIT"), - Self::Stop => write!(f, "STOP"), - Self::StopLimit => write!(f, "STOP_LIMIT"), - Self::Iceberg => write!(f, "ICEBERG"), - Self::TrailingStop => write!(f, "TRAILING_STOP"), - Self::Hidden => write!(f, "HIDDEN"), - } - } -} - -impl Default for OrderType { - /// Returns the default order type (Market) - fn default() -> Self { - Self::Market - } -} - -impl TryFrom for OrderType { - type Error = String; - - fn try_from(value: i32) -> Result { - match value { - 0 => Ok(OrderType::Market), - 1 => Ok(OrderType::Limit), - 2 => Ok(OrderType::Stop), - 3 => Ok(OrderType::StopLimit), - 4 => Ok(OrderType::Iceberg), - 5 => Ok(OrderType::TrailingStop), - 6 => Ok(OrderType::Hidden), - _ => Err(format!("Invalid OrderType: {}", value)), - } - } -} - -/// Supported broker types - CANONICAL DEFINITION -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum BrokerType { - /// Interactive Brokers TWS/API - InteractiveBrokers, - /// IC Markets FIX API - ICMarkets, - /// Paper trading simulation - PaperTrading, - /// Demo/Test broker - Demo, -} - -impl Default for BrokerType { - /// Returns the default broker type (`InteractiveBrokers`) - fn default() -> Self { - Self::InteractiveBrokers - } -} - -/// Order status throughout its lifecycle - CANONICAL DEFINITION -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[non_exhaustive] -pub enum OrderStatus { - /// Order has been created but not yet submitted to broker - Created, - /// Order has been submitted to broker for execution - Submitted, - /// Order has been partially executed with remaining quantity - PartiallyFilled, - /// Order has been completely executed - Filled, - /// Order was rejected by broker or exchange - Rejected, - /// Order was cancelled by user or system - Cancelled, - /// New order accepted by broker - New, - /// Order expired due to time restrictions - Expired, - /// Order is pending broker acceptance - Pending, - /// Order is actively working in the market - Working, - /// Order status is unknown or not yet determined - Unknown, - /// Order is temporarily suspended - Suspended, - /// Order cancellation is pending - PendingCancel, - /// Order modification is pending - PendingReplace, -} -impl fmt::Display for OrderStatus { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Created => write!(f, "CREATED"), - Self::Submitted => write!(f, "SUBMITTED"), - Self::PartiallyFilled => write!(f, "PARTIALLY_FILLED"), - Self::Filled => write!(f, "FILLED"), - Self::Rejected => write!(f, "REJECTED"), - Self::Cancelled => write!(f, "CANCELLED"), - Self::New => write!(f, "NEW"), - Self::Expired => write!(f, "EXPIRED"), - Self::Pending => write!(f, "PENDING"), - Self::Working => write!(f, "WORKING"), - Self::Unknown => write!(f, "UNKNOWN"), - Self::Suspended => write!(f, "SUSPENDED"), - Self::PendingCancel => write!(f, "PENDING_CANCEL"), - Self::PendingReplace => write!(f, "PENDING_REPLACE"), - } - } -} - -impl Default for OrderStatus { - /// Returns the default order status (Created) - fn default() -> Self { - Self::Created - } -} - -impl TryFrom for OrderStatus { - type Error = String; - - fn try_from(value: i32) -> Result { - match value { - 0 => Ok(OrderStatus::Created), - 1 => Ok(OrderStatus::Submitted), - 2 => Ok(OrderStatus::PartiallyFilled), - 3 => Ok(OrderStatus::Filled), - 4 => Ok(OrderStatus::Rejected), - 5 => Ok(OrderStatus::Cancelled), - 6 => Ok(OrderStatus::New), - 7 => Ok(OrderStatus::Expired), - 8 => Ok(OrderStatus::Pending), - 9 => Ok(OrderStatus::Working), - 10 => Ok(OrderStatus::Unknown), - 11 => Ok(OrderStatus::Suspended), - 12 => Ok(OrderStatus::PendingCancel), - 13 => Ok(OrderStatus::PendingReplace), - _ => Err(format!("Invalid OrderStatus: {}", value)), - } - } -} - -/// Order side - whether the order is a buy or sell - CANONICAL DEFINITION -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum OrderSide { - /// Buy order - purchasing securities - Buy, - /// Sell order - selling securities - Sell, -} - -impl fmt::Display for OrderSide { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Buy => write!(f, "BUY"), - Self::Sell => write!(f, "SELL"), - } - } -} - -impl Default for OrderSide { - /// Returns the default order side (Buy) - fn default() -> Self { - Self::Buy - } -} - -impl TryFrom for OrderSide { - type Error = String; - - fn try_from(value: i32) -> Result { - match value { - 0 => Ok(OrderSide::Buy), - 1 => Ok(OrderSide::Sell), - _ => Err(format!("Invalid OrderSide: {}", value)), - } - } -} - -// REMOVED: Side alias - use OrderSide directly - -/// Currency enumeration - CANONICAL DEFINITION -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] -#[cfg_attr(feature = "database", derive(sqlx::Type))] -pub enum Currency { - /// US Dollar - USD, - /// Euro - EUR, - /// British Pound Sterling - GBP, - /// Japanese Yen - JPY, - /// Swiss Franc - CHF, - /// Canadian Dollar - CAD, - /// Australian Dollar - AUD, - /// New Zealand Dollar - NZD, - /// Bitcoin - BTC, - /// Ethereum - ETH, -} - -impl fmt::Display for Currency { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::USD => write!(f, "USD"), - Self::EUR => write!(f, "EUR"), - Self::GBP => write!(f, "GBP"), - Self::JPY => write!(f, "JPY"), - Self::CHF => write!(f, "CHF"), - Self::CAD => write!(f, "CAD"), - Self::AUD => write!(f, "AUD"), - Self::NZD => write!(f, "NZD"), - Self::BTC => write!(f, "BTC"), - Self::ETH => write!(f, "ETH"), - } - } -} - -impl Default for Currency { - /// Returns the default currency (USD) - fn default() -> Self { - Self::USD - } -} - -/// Time in force enumeration - CANONICAL DEFINITION -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum TimeInForce { - /// Order is valid for the current trading day only - Day, - /// Order remains active until explicitly cancelled - GoodTillCancel, - /// Order must be executed immediately or cancelled - ImmediateOrCancel, - /// Order must be executed completely or cancelled - FillOrKill, -} - -impl fmt::Display for TimeInForce { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Day => write!(f, "DAY"), - Self::GoodTillCancel => write!(f, "GTC"), - Self::ImmediateOrCancel => write!(f, "IOC"), - Self::FillOrKill => write!(f, "FOK"), - } - } -} - -impl Default for TimeInForce { - /// Returns the default time in force (Day) - fn default() -> Self { - Self::Day - } -} - -// ============================================================================= -// CORE ID TYPES (MIGRATED FROM TRADING_ENGINE) -// ============================================================================= - -// Duplicate TradeId removed - using definition from line 1008 - -/// Event identifier for tracking system events -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct EventId(String); - -impl EventId { - /// Create a new random event ID - pub fn new() -> Self { - use uuid::Uuid; - Self(Uuid::new_v4().to_string()) - } - - /// Create an event ID from a string, generating new if empty - pub fn from_string>(id: S) -> Self { - let id = id.into(); - if id.is_empty() { - Self::new() // Generate new ID if empty - } else { - Self(id) - } - } - - /// Get the string value of the event ID - pub fn value(&self) -> &str { - &self.0 - } -} - -impl fmt::Display for EventId { - /// Format the event ID for display - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for EventId { - /// Create an `EventId` from a String - fn from(s: String) -> Self { - Self(s) - } -} - -impl Default for EventId { - /// Create a default `EventId` with a new UUID - fn default() -> Self { - Self::new() - } -} - -// ============================================================================= -// Decimal Extension Trait -// ============================================================================= - -/// Extension trait for `rust_decimal::Decimal` with convenience methods -pub trait DecimalExt { - /// Create Decimal from f64, compatible with legacy `from_f64` usage - fn from_f64(value: f64) -> Option - where - Self: Sized; - - /// Calculate square root of Decimal - fn sqrt(&self) -> Option - where - Self: Sized; -} - -impl DecimalExt for Decimal { - fn from_f64(value: f64) -> Option { - Decimal::from_f64_retain(value) - } - - fn sqrt(&self) -> Option { - if self.is_sign_negative() { - return None; - } - let value_f64: f64 = self.to_string().parse().ok()?; - let sqrt_f64 = value_f64.sqrt(); - Decimal::from_f64_retain(sqrt_f64) - } -} - -/// Fill identifier with validation -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct FillId(String); - -impl FillId { - /// Create a new fill ID with validation - /// - /// # Errors - /// Returns error if the operation fails - pub fn new>(id: S) -> Result { - let id = id.into(); - if id.is_empty() { - return Err(CommonTypeError::ValidationError { - field: "fill_id".to_owned(), - reason: "Fill ID cannot be empty".to_owned(), - }); - } - Ok(Self(id)) - } - - /// Get the fill ID as a string slice - pub fn as_str(&self) -> &str { - &self.0 - } - /// Convert the fill ID into an owned string - /// - /// Convert the execution ID into an owned string - /// - /// Convert execution ID into owned string - pub fn into_string(self) -> String { - self.0 - } -} - -impl fmt::Display for FillId { - /// Format the fill ID for display - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -/// Aggregate identifier with validation -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct AggregateId(String); - -impl AggregateId { - /// Create a new aggregate ID with validation - /// - /// # Errors - /// Returns error if the operation fails - pub fn new>(id: S) -> Result { - let id = id.into(); - if id.is_empty() { - return Err(CommonTypeError::ValidationError { - field: "aggregate_id".to_owned(), - reason: "Aggregate ID cannot be empty".to_owned(), - }); - } - Ok(Self(id)) - } - - /// Get the aggregate ID as a string slice - pub fn as_str(&self) -> &str { - &self.0 - } - /// Convert the aggregate ID into an owned string - pub fn into_string(self) -> String { - self.0 - } -} - -impl fmt::Display for AggregateId { - /// Format the aggregate ID for display - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -/// Asset identifier with validation -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct AssetId(String); - -impl AssetId { - /// Create a new asset ID with validation - /// - /// # Errors - /// Returns error if the operation fails - pub fn new>(id: S) -> Result { - let id = id.into(); - if id.is_empty() { - return Err(CommonTypeError::ValidationError { - field: "asset_id".to_owned(), - reason: "Asset ID cannot be empty".to_owned(), - }); - } - Ok(Self(id)) - } - - /// Get the asset ID as a string slice - pub fn as_str(&self) -> &str { - &self.0 - } - /// Convert the asset ID into an owned string - pub fn into_string(self) -> String { - self.0 - } -} - -impl fmt::Display for AssetId { - /// Format the asset ID for display - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -/// Client identifier with validation -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct ClientId(String); - -impl ClientId { - /// Create a new client ID with validation - /// - /// # Errors - /// Returns error if the operation fails - pub fn new>(id: S) -> Result { - let id = id.into(); - if id.is_empty() { - return Err(CommonTypeError::ValidationError { - field: "client_id".to_owned(), - reason: "Client ID cannot be empty".to_owned(), - }); - } - Ok(Self(id)) - } - - /// Get the client ID as a string slice - pub fn as_str(&self) -> &str { - &self.0 - } - /// Convert the client ID into an owned string - pub fn into_string(self) -> String { - self.0 - } -} - -impl fmt::Display for ClientId { - /// Format the client ID for display - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -// ============================================================================= -// CORE TRADING TYPES - MIGRATED FROM TRADING_ENGINE -// ============================================================================= - -/// Canonical Order struct - UNIFIED DEFINITION based on Agent 1's comprehensive analysis -/// -/// This represents the single source of truth for Order across all services -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[cfg_attr(feature = "database", derive(sqlx::FromRow))] -pub struct Order { - // Core Identity - /// Unique order identifier - pub id: OrderId, - /// Client-provided order identifier - pub client_order_id: Option, - /// Broker-assigned order identifier - pub broker_order_id: Option, - /// Account identifier for the order - pub account_id: Option, - - // Trading Details - /// Trading symbol for the order - pub symbol: Symbol, - /// Order side (buy or sell) - pub side: OrderSide, - /// Type of order (market, limit, etc.) - pub order_type: OrderType, - /// Current status of the order - pub status: OrderStatus, - /// Time in force policy - pub time_in_force: TimeInForce, - - // Quantities & Pricing - /// Total order quantity - pub quantity: Quantity, - /// Limit price for the order - pub price: Option, - /// Stop price for stop orders - pub stop_price: Option, - /// Quantity that has been filled - pub filled_quantity: Quantity, - /// Remaining quantity to be filled - pub remaining_quantity: Quantity, - /// Average execution price - pub average_price: Option, - /// Alias for `average_price` for database compatibility - pub avg_fill_price: Option, - /// Alias for `average_price` for API compatibility - pub average_fill_price: Option, - /// Exchange-assigned order identifier - pub exchange_order_id: Option, - - // Strategy Fields (from Agent 1) - /// Parent order ID for iceberg/algo orders - pub parent_id: Option, - /// Execution algorithm name - pub execution_algorithm: Option, - /// Execution algorithm parameters stored as JSON - pub execution_params: Value, - - // Risk Management (from Agent 1) - /// Stop loss price for risk management - pub stop_loss: Option, - /// Take profit price for profit taking - pub take_profit: Option, - - // Timestamps - /// Order creation timestamp - pub created_at: HftTimestamp, - /// Last update timestamp - pub updated_at: Option, - /// Order expiration timestamp - pub expires_at: Option, - - // Extensibility - /// Additional order metadata stored as JSON - pub metadata: Value, -} - -impl Order { - /// Create a new order with canonical fields - pub fn new( - symbol: Symbol, - side: OrderSide, - quantity: Quantity, - price: Option, - order_type: OrderType, - ) -> Self { - let now = HftTimestamp::now_or_zero(); - Self { - // Core Identity - id: OrderId::new(), - client_order_id: None, - broker_order_id: None, - account_id: None, - - // Trading Details - symbol, - side, - order_type, - status: OrderStatus::Created, - time_in_force: TimeInForce::default(), - - // Quantities & Pricing - quantity, - price, - stop_price: None, - filled_quantity: Quantity::ZERO, - remaining_quantity: quantity, - average_price: None, - avg_fill_price: None, // Database compatibility alias - average_fill_price: None, // API compatibility alias - exchange_order_id: None, - - // Strategy Fields - parent_id: None, - execution_algorithm: None, - execution_params: serde_json::json!({}), - - // Risk Management - stop_loss: None, - take_profit: None, - - // Timestamps - created_at: now, - updated_at: None, - expires_at: None, - - // Extensibility - metadata: serde_json::json!({}), - } - } - - /// Check if the order is fully filled - pub fn is_filled(&self) -> bool { - self.filled_quantity == self.quantity - } - - /// Check if the order is partially filled - pub fn is_partially_filled(&self) -> bool { - self.filled_quantity > Quantity::ZERO && self.filled_quantity < self.quantity - } - - /// Calculate fill percentage - #[allow(clippy::float_arithmetic)] - pub fn fill_percentage(&self) -> f64 { - if self.quantity.is_zero() { - 0.0 - } else { - (self.filled_quantity.to_f64() / self.quantity.to_f64()) * 100.0 - } - } - - /// Set client order ID for tracking - pub fn with_client_order_id(mut self, client_order_id: String) -> Self { - self.client_order_id = Some(client_order_id); - self - } - - /// Set account ID - pub fn with_account_id(mut self, account_id: String) -> Self { - self.account_id = Some(account_id); - self - } - - /// Set time in force - pub const fn with_time_in_force(mut self, time_in_force: TimeInForce) -> Self { - self.time_in_force = time_in_force; - self - } - - /// Set stop price - pub const fn with_stop_price(mut self, stop_price: Price) -> Self { - self.stop_price = Some(stop_price); - self - } - - /// Set execution algorithm - pub fn with_execution_algorithm(mut self, algorithm: String) -> Self { - self.execution_algorithm = Some(algorithm); - self - } - - /// Add execution parameter - pub fn with_execution_param(mut self, key: String, value: f64) -> Self { - if let Some(obj) = self.execution_params.as_object_mut() { - obj.insert(key, serde_json::to_value(value).unwrap_or(Value::Null)); - } else { - let mut map = serde_json::Map::new(); - map.insert(key, serde_json::to_value(value).unwrap_or(Value::Null)); - self.execution_params = Value::Object(map); - } - self - } - - /// Set stop loss - pub const fn with_stop_loss(mut self, stop_loss: Price) -> Self { - self.stop_loss = Some(stop_loss); - self - } - - /// Set take profit - pub const fn with_take_profit(mut self, take_profit: Price) -> Self { - self.take_profit = Some(take_profit); - self - } - - /// Add metadata - pub fn with_metadata(mut self, key: String, value: String) -> Self { - if let Some(obj) = self.metadata.as_object_mut() { - obj.insert(key, Value::String(value)); - } else { - let mut map = serde_json::Map::new(); - map.insert(key, Value::String(value)); - self.metadata = Value::Object(map); - } - self - } - - /// Update order status and timestamp - pub fn update_status(&mut self, status: OrderStatus) { - self.status = status; - self.updated_at = Some(HftTimestamp::now_or_zero()); - } - - /// Fill order with given quantity and price - /// - /// # Errors - /// Returns error if the operation fails - #[allow(clippy::float_arithmetic)] - pub fn fill( - &mut self, - fill_quantity: Quantity, - fill_price: Price, - ) -> Result<(), CommonTypeError> { - // Use Add trait instead of checked_add (Quantity doesn't have checked_add method) - let new_filled_value = self.filled_quantity.to_f64() + fill_quantity.to_f64(); - if !new_filled_value.is_finite() || new_filled_value < 0.0 { - return Err(CommonTypeError::ValidationError { - field: "fill_quantity".to_owned(), - reason: "Fill quantity overflow".to_owned(), - }); - } - let new_filled = - Quantity::from_f64(new_filled_value).map_err(|e| CommonTypeError::ValidationError { - field: "fill_quantity".to_owned(), - reason: format!("Fill quantity overflow: {}", e), - })?; - if new_filled > self.quantity { - return Err(CommonTypeError::ValidationError { - field: "fill_quantity".to_owned(), - reason: "Fill quantity exceeds remaining quantity".to_owned(), - }); - } - - // Update filled quantity - let previous_filled = self.filled_quantity; - let new_filled_value = self - .filled_quantity - .value - .checked_add(fill_quantity.value) - .ok_or_else(|| CommonTypeError::ValidationError { - field: "filled_quantity".to_owned(), - reason: "Filled quantity overflow".to_owned(), - })?; - self.filled_quantity = Quantity { - value: new_filled_value, - }; - - let new_remaining_value = self - .quantity - .value - .checked_sub(self.filled_quantity.value) - .ok_or_else(|| CommonTypeError::ValidationError { - field: "remaining_quantity".to_owned(), - reason: "Remaining quantity underflow".to_owned(), - })?; - self.remaining_quantity = Quantity { - value: new_remaining_value, - }; - - // Update average price - if let Some(avg_price) = self.average_price { - let total_value = avg_price.to_f64() * previous_filled.to_f64() - + fill_price.to_f64() * fill_quantity.to_f64(); - let new_avg = Some( - Price::from_f64(total_value / self.filled_quantity.to_f64()).unwrap_or(fill_price), - ); - self.average_price = new_avg; - self.avg_fill_price = new_avg; // Keep in sync - } else { - self.average_price = Some(fill_price); - self.avg_fill_price = Some(fill_price); // Keep in sync - } - - // Update status - if self.is_filled() { - self.update_status(OrderStatus::Filled); - } else { - self.update_status(OrderStatus::PartiallyFilled); - } - - Ok(()) - } - - /// Create a limit order - convenience constructor - pub fn limit(symbol: Symbol, side: OrderSide, quantity: Quantity, price: Price) -> Self { - Self::new(symbol, side, quantity, Some(price), OrderType::Limit) - } - - /// Create a market order - convenience constructor - pub fn market(symbol: Symbol, side: OrderSide, quantity: Quantity) -> Self { - Self::new(symbol, side, quantity, None, OrderType::Market) - } - - /// Get symbol hash for performance-critical operations - pub fn symbol_hash(&self) -> i64 { - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; - - let mut hasher = DefaultHasher::new(); - self.symbol.as_str().hash(&mut hasher); - i64::try_from(hasher.finish()).unwrap_or(0) - } - - /// Get order timestamp - pub const fn timestamp(&self) -> HftTimestamp { - self.created_at - } -} - -impl Default for Order { - fn default() -> Self { - Self { - id: OrderId::new(), - client_order_id: None, - broker_order_id: None, - account_id: None, - - symbol: Symbol::from("DEFAULT"), - side: OrderSide::Buy, - order_type: OrderType::Market, - status: OrderStatus::Created, - time_in_force: TimeInForce::Day, - - quantity: Quantity::ONE, - price: None, - stop_price: None, - filled_quantity: Quantity::ZERO, - remaining_quantity: Quantity::ONE, - average_price: None, - avg_fill_price: None, - average_fill_price: None, - exchange_order_id: None, - - parent_id: None, - execution_algorithm: None, - execution_params: serde_json::json!({}), - - stop_loss: None, - take_profit: None, - - created_at: HftTimestamp::now().unwrap_or(HftTimestamp { nanos: 0 }), - updated_at: None, - expires_at: None, - - metadata: serde_json::json!({}), - } - } -} - -/// Represents a trading position - CANONICAL DEFINITION -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr(feature = "database", derive(sqlx::FromRow))] -pub struct Position { - /// Unique position identifier - pub id: Uuid, - - /// Trading symbol - pub symbol: String, - - /// Position quantity (positive for long, negative for short) - pub quantity: Decimal, - - /// Average entry price - pub avg_price: Decimal, - - /// Average cost per share - pub avg_cost: Decimal, - - /// Cost basis for tax calculations - pub basis: Decimal, - - /// Average entry price - pub average_price: Decimal, - - /// Market value of position - pub market_value: Decimal, - - /// Unrealized P&L - pub unrealized_pnl: Decimal, - - /// Realized P&L - pub realized_pnl: Decimal, - - /// Position creation timestamp - pub created_at: DateTime, - - /// Last update timestamp - pub updated_at: DateTime, - - /// Last updated timestamp - pub last_updated: DateTime, - - /// Current market price (for P&L calculation) - pub current_price: Option, - - /// Position size in base currency - pub notional_value: Decimal, - - /// Margin requirement - pub margin_requirement: Decimal, -} - -impl Position { - /// Create a new position - pub fn new(symbol: String, quantity: Decimal, avg_price: Decimal) -> Self { - let now = Utc::now(); - let notional_value = quantity - .abs() - .checked_mul(avg_price) - .unwrap_or(Decimal::ZERO); - - Self { - id: Uuid::new_v4(), - symbol, - quantity, - avg_price, - avg_cost: avg_price, // Keep avg_cost synchronized with avg_price - basis: quantity.checked_mul(avg_price).unwrap_or(Decimal::ZERO), // Cost basis calculation - average_price: avg_price, // Same as avg_price for compatibility - market_value: notional_value, // Initialize market value to notional value - unrealized_pnl: Decimal::ZERO, - realized_pnl: Decimal::ZERO, - created_at: now, - updated_at: now, - last_updated: now, // Same as updated_at for compatibility - current_price: None, - notional_value, - margin_requirement: notional_value - .checked_mul(Decimal::from_str_exact("0.02").unwrap_or(Decimal::ZERO)) - .unwrap_or(Decimal::ZERO), // 2% margin - } - } - - /// Check if position is long - pub fn is_long(&self) -> bool { - self.quantity > Decimal::ZERO - } - - /// Check if position is short - pub fn is_short(&self) -> bool { - self.quantity < Decimal::ZERO - } - - /// Calculate unrealized P&L based on current price - pub fn calculate_unrealized_pnl(&mut self, current_price: Decimal) { - self.current_price = Some(current_price); - self.market_value = self - .quantity - .abs() - .checked_mul(current_price) - .unwrap_or(Decimal::ZERO); - // For both long and short: quantity * (current_price - avg_price) - let price_diff = current_price - .checked_sub(self.avg_price) - .unwrap_or(Decimal::ZERO); - self.unrealized_pnl = self - .quantity - .checked_mul(price_diff) - .unwrap_or(Decimal::ZERO); - let now = Utc::now(); - self.updated_at = now; - self.last_updated = now; // Keep alias synchronized - } - - /// Get total P&L (realized + unrealized) - pub fn total_pnl(&self) -> Decimal { - self.realized_pnl - .checked_add(self.unrealized_pnl) - .unwrap_or(Decimal::ZERO) - } - - /// Calculate return on investment percentage - pub fn roi_percentage(&self) -> Decimal { - if self.notional_value.is_zero() { - Decimal::ZERO - } else { - let pnl_ratio = self - .total_pnl() - .checked_div(self.notional_value) - .unwrap_or(Decimal::ZERO); - pnl_ratio - .checked_mul(Decimal::from(100)) - .unwrap_or(Decimal::ZERO) - } - } -} - -/// Represents a trade execution - CANONICAL DEFINITION -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr(feature = "database", derive(sqlx::FromRow))] -pub struct Execution { - /// Unique execution identifier - pub id: Uuid, - - /// Related order ID - pub order_id: Uuid, - - /// Trading symbol - pub symbol: String, - - /// Executed quantity - pub quantity: Decimal, - - /// Execution price - pub price: Decimal, - - /// Execution side - pub side: OrderSide, - - /// Trading fees - pub fees: Decimal, - - /// Fee currency - pub fee_currency: String, - - /// Execution timestamp - pub executed_at: DateTime, - - /// Execution timestamp - pub timestamp: DateTime, - - /// Symbol hash for performance - pub symbol_hash: i64, - - /// Broker execution ID - pub broker_execution_id: Option, - - /// Counterparty information - pub counterparty: Option, - - /// Trade venue - pub venue: Option, - - /// Gross trade value - pub gross_value: Decimal, - - /// Net trade value (after fees) - pub net_value: Decimal, -} - -impl Execution { - /// Create a new execution - pub fn new( - order_id: Uuid, - symbol: String, - quantity: Decimal, - price: Decimal, - side: OrderSide, - fees: Decimal, - ) -> Self { - let gross_value = quantity.checked_mul(price).unwrap_or(Decimal::ZERO); - let net_value = if side == OrderSide::Buy { - gross_value.checked_add(fees).unwrap_or(gross_value) - } else { - gross_value.checked_sub(fees).unwrap_or(gross_value) - }; - let now = Utc::now(); - let symbol_hash = Self::hash_symbol(&symbol); - - Self { - id: Uuid::new_v4(), - order_id, - symbol, - quantity, - price, - side, - fees, - fee_currency: "USD".to_owned(), // Default to USD - executed_at: now, - timestamp: now, // Same as executed_at for compatibility - symbol_hash, - broker_execution_id: None, - counterparty: None, - venue: None, - gross_value, - net_value, - } - } - - /// Calculate effective price including fees - pub fn effective_price(&self) -> Decimal { - if self.quantity.is_zero() { - self.price - } else { - self.net_value - .checked_div(self.quantity) - .unwrap_or(self.price) - } - } - - /// Hash symbol for performance - fn hash_symbol(symbol: &str) -> i64 { - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; - - let mut hasher = DefaultHasher::new(); - symbol.hash(&mut hasher); - i64::try_from(hasher.finish()).unwrap_or(0) - } -} - -/// Core Price type using fixed-point arithmetic for precision -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct Price { - value: u64, -} - -impl Price { - /// Zero price constant - pub const ZERO: Self = Self { value: 0 }; - /// One unit price constant (1.0) - pub const ONE: Self = Self { value: 100_000_000 }; - /// One cent constant (0.01) - pub const CENT: Self = Self { value: 1_000_000 }; - /// Maximum price value - pub const MAX: Self = Self { value: u64::MAX }; - - /// Create a Price from a floating-point value - /// - /// # Errors - /// Returns error if the operation fails - #[allow(clippy::as_conversions)] - #[allow(clippy::float_arithmetic)] - pub fn from_f64(value: f64) -> Result { - if value < 0.0_f64 || !value.is_finite() { - return Err(CommonTypeError::InvalidPrice { - value: value.to_string(), - reason: "Price validation failed".to_owned(), - }); - } - Ok(Self { - #[allow(clippy::as_conversions)] - #[allow(clippy::float_arithmetic)] - value: (value * 100_000_000.0).round() as u64, - }) - } - - /// Convert to floating-point representation - #[must_use] - #[allow(clippy::float_arithmetic)] - /// Convert the quantity to a floating point value - #[allow(clippy::as_conversions)] - pub fn to_f64(&self) -> f64 { - #[allow(clippy::as_conversions)] - { - self.value as f64 / 100_000_000.0 - } - } - - /// Get floating-point representation (alias for `to_f64`) - /// - /// Convert quantity to f64 representation - /// - /// Convert quantity to f64 representation - /// - /// Convert quantity to f64 representation - #[must_use] - pub fn as_f64(&self) -> f64 { - self.to_f64() - } - - /// Create a zero price - /// - /// Create a zero quantity - /// - /// Create zero quantity - #[must_use] - pub const fn zero() -> Self { - Self::ZERO - } - - /// Convert to Decimal type for precise calculations - /// - /// # Errors - /// Returns error if the operation fails - pub fn to_decimal(&self) -> Result { - ::from_f64(self.to_f64()).ok_or_else(|| { - CommonTypeError::InvalidPrice { - value: "0.0".to_owned(), - reason: "Price to Decimal conversion failed".to_owned(), - } - }) - } - - /// Create a Price from a Decimal value - #[must_use] - pub fn from_decimal(decimal: Decimal) -> Self { - Self::from(decimal) - } - - /// Create a new Price (alias for `from_f64`) - /// - /// Create a new quantity from a floating point value - /// - /// Create new quantity from f64 value - /// - /// # Errors - /// Returns error if the operation fails - pub fn new(value: f64) -> Result { - Self::from_f64(value) - } - - /// Get the raw internal value representation - /// - /// Get the raw internal value - /// - /// Get the raw internal value representation - #[must_use] - pub const fn raw_value(&self) -> u64 { - self.value - } - - /// Get the price as a u64 value (same as `raw_value`) - /// - /// Convert to u64 representation - /// - /// Convert quantity to u64 representation - #[must_use] - pub const fn as_u64(&self) -> u64 { - self.value - } - - /// Create a Price from a raw u64 value - /// - /// Create a quantity from raw internal value - /// - /// Create quantity from raw u64 value - #[must_use] - pub const fn from_raw(value: u64) -> Self { - Self { value } - } - - /// Convert price to cents (divides by 1M for 8 decimal places) - #[must_use] - #[allow(clippy::integer_division)] - pub const fn to_cents(&self) -> u64 { - self.value / 1_000_000 - } - - /// Create a Price from cents value - #[must_use] - pub const fn from_cents(cents: u64) -> Self { - Self { - value: cents.saturating_mul(1_000_000), - } - } - - /// Check if the price is zero - /// - /// Check if the quantity is zero - /// - /// Check if quantity is zero - #[must_use] - pub const fn is_zero(&self) -> bool { - self.value == 0 - } - - /// Check if the price is non-zero (has some value) - /// - /// Check if the quantity is non-zero (has some value) - /// - /// Check if quantity has a non-zero value - #[must_use] - pub const fn is_some(&self) -> bool { - !self.is_zero() - } - - /// Check if the price is zero (has no value) - /// - /// Check if the quantity is zero (has no value) - /// - /// Check if quantity is zero (none) - #[must_use] - pub const fn is_none(&self) -> bool { - self.is_zero() - } - - /// Get a reference to this price - /// - /// Get a reference to self - /// - /// Get a reference to self - #[must_use] - pub const fn as_ref(&self) -> &Self { - self - } - - /// Get the absolute value of the price (prices are always positive) - /// - /// Get the absolute value (quantities are always positive) - /// - /// Get absolute value (always positive for Quantity) - #[must_use] - pub const fn abs(&self) -> Self { - *self - } - - /// Multiply this price by another price - /// - /// # Errors - /// Returns error if the operation fails - #[allow(clippy::arithmetic_side_effects)] - pub fn multiply(&self, other: Self) -> Result { - #[allow(clippy::arithmetic_side_effects)] - let result = *self * other; - result - } - - /// Subtract another price from this price - /// - /// Subtract another quantity from this quantity - /// - /// Subtract another quantity from this quantity - /// - /// Subtract another quantity from this quantity - /// - /// Subtract another quantity from this quantity - #[must_use] - #[allow(clippy::arithmetic_side_effects)] - pub fn subtract(&self, other: Self) -> Self { - *self - other - } - - /// Divide this price by a floating point divisor - /// - /// # Errors - /// Returns error if the operation fails - pub fn divide(&self, divisor: f64) -> Result { - if divisor == 0.0 { - return Err(CommonTypeError::InvalidPrice { - value: format!("{:?}", self), - reason: "Division by zero".to_owned(), - }); - } - #[allow(clippy::cast_precision_loss)] - #[allow(clippy::arithmetic_side_effects)] - let result = (self.value as f64 / divisor) as u64; - Ok(Self { value: result }) - } -} - -impl fmt::Display for Price { - /// Format the price for display with 8 decimal places - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:.8}", self.to_f64()) - } -} - -impl Default for Price { - /// Returns the default price (zero) - fn default() -> Self { - Self::ZERO - } -} - -impl FromStr for Price { - type Err = CommonTypeError; - - fn from_str(s: &str) -> Result { - let parsed_value = s - .parse::() - .map_err(|e| CommonTypeError::InvalidPrice { - value: s.to_owned(), - reason: format!("Cannot parse '{}' as price: {}", s, e), - })?; - Self::from_f64(parsed_value) - } -} - -impl Add for Price { - type Output = Self; - fn add(self, rhs: Self) -> Self::Output { - Self { - value: self.value.saturating_add(rhs.value), - } - } -} - -impl Sub for Price { - type Output = Self; - fn sub(self, rhs: Self) -> Self::Output { - Self { - value: self.value.saturating_sub(rhs.value), - } - } -} - -impl Mul for Price { - #[allow(clippy::float_arithmetic)] - type Output = Result; - fn mul(self, rhs: f64) -> Self::Output { - Self::from_f64(self.to_f64() * rhs) - } -} - -#[allow(clippy::float_arithmetic)] -impl Div for Price { - type Output = Result; - fn div(self, rhs: f64) -> Self::Output { - if rhs == 0.0_f64 { - return Err(CommonTypeError::ConversionError { - message: "Cannot divide price by zero".to_owned(), - }); - } - Self::from_f64(self.to_f64() / rhs) - } -} - -impl From for Price { - fn from(decimal: Decimal) -> Self { - let f64_val: f64 = TryInto::::try_into(decimal).unwrap_or_else(|_| { - tracing::warn!("Failed to convert Decimal to f64, using 0.0 as fallback"); - 0.0_f64 - }); - Self::from_f64(f64_val).unwrap_or_else(|_| { - tracing::warn!( - "Failed to create Price from f64 value {}, using ZERO", - f64_val - ); - Self::ZERO - }) - } -} - -impl From for Decimal { - fn from(price: Price) -> Self { - price.to_decimal().unwrap_or(Decimal::ZERO) - } -} - -// TryFrom for Decimal removed due to conflicting blanket implementation -// Use qty.to_decimal() directly instead -impl From for Decimal { - fn from(qty: Quantity) -> Self { - qty.to_decimal().unwrap_or(Decimal::ZERO) - } -} - -// TryFrom for Decimal removed due to conflict with From implementation -// Use the From implementation instead which handles errors by returning ZERO - -// ============================================================================= -// Decimal Extension Trait -// ============================================================================= - -impl Mul for Price { - #[allow(clippy::float_arithmetic)] - #[allow(clippy::arithmetic_side_effects)] - type Output = Result; - fn mul(self, rhs: Self) -> Self::Output { - Self::from_f64(self.to_f64() * rhs.to_f64()) - } -} - -impl TryFrom for Price { - type Error = CommonTypeError; - fn try_from(s: String) -> Result { - Self::from_str(&s) - } -} - -impl TryFrom<&str> for Price { - type Error = CommonTypeError; - fn try_from(s: &str) -> Result { - Self::from_str(s) - } -} - -impl PartialEq for Price { - #[allow(clippy::float_arithmetic)] - fn eq(&self, other: &f64) -> bool { - (self.to_f64() - other).abs() < f64::EPSILON - } -} - -impl PartialEq for f64 { - #[allow(clippy::float_arithmetic)] - fn eq(&self, other: &Price) -> bool { - (self - other.to_f64()).abs() < f64::EPSILON - } -} - -impl AddAssign for Price { - fn add_assign(&mut self, rhs: Self) { - self.value = self.value.saturating_add(rhs.value); - } -} - -impl SubAssign for Price { - fn sub_assign(&mut self, rhs: Self) { - self.value = self.value.saturating_sub(rhs.value); - } -} - -impl MulAssign for Price { - fn mul_assign(&mut self, rhs: f64) { - if let Ok(result) = self.mul(rhs) { - *self = result; - } - // If multiplication fails, self remains unchanged - } -} - -impl DivAssign for Price { - fn div_assign(&mut self, rhs: f64) { - if let Ok(result) = self.div(rhs) { - *self = result; - } - // If division fails, self remains unchanged - } -} - -impl PartialOrd for Price { - fn partial_cmp(&self, other: &f64) -> Option { - self.to_f64().partial_cmp(other) - } -} - -impl PartialOrd for f64 { - fn partial_cmp(&self, other: &Price) -> Option { - self.partial_cmp(&other.to_f64()) - } -} - -/// Core Quantity type using fixed-point arithmetic -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct Quantity { - value: u64, -} - -impl Quantity { - /// Zero quantity constant - pub const ZERO: Self = Self { value: 0 }; - /// One unit quantity constant - pub const ONE: Self = Self { value: 100_000_000 }; - /// Maximum possible quantity - pub const MAX: Self = Self { value: u64::MAX }; - - /// Create a Quantity from a floating point value - #[allow(clippy::as_conversions)] - /// - /// # Errors - /// Returns error if the operation fails - #[allow(clippy::float_arithmetic)] - pub fn from_f64(value: f64) -> Result { - if value < 0.0_f64 || !value.is_finite() { - return Err(CommonTypeError::InvalidQuantity { - value: value.to_string(), - reason: "Quantity validation failed".to_owned(), - }); - } - Ok(Self { - value: (value * 100_000_000.0).round() as u64, - }) - } - - /// Convert quantity to floating point representation - #[must_use] - #[allow(clippy::as_conversions)] - #[allow(clippy::float_arithmetic)] - pub fn to_f64(&self) -> f64 { - self.value as f64 / 100_000_000.0 - } - - /// Convert the quantity to a Decimal value - /// - /// # Errors - /// Returns error if the operation fails - pub fn to_decimal(&self) -> Result { - Decimal::from_f64_retain(self.to_f64()).ok_or_else(|| CommonTypeError::InvalidQuantity { - value: "0.0".to_owned(), - reason: "Quantity to Decimal conversion failed".to_owned(), - }) - } - - /// Get the internal value representation - #[must_use] - pub const fn value(&self) -> u64 { - self.value - } - - /// Get the raw internal value representation - #[must_use] - pub const fn raw_value(&self) -> u64 { - self.value - } - - /// Convert quantity to u64 representation - #[must_use] - pub const fn as_u64(&self) -> u64 { - self.value - } - - /// Create quantity from raw u64 value - #[must_use] - pub const fn from_raw(value: u64) -> Self { - Self { value } - } - - /// Create new quantity from f64 value - /// - /// # Errors - /// Returns error if the operation fails - #[allow(clippy::as_conversions)] - pub fn new(value: f64) -> Result { - Self::from_f64(value) - } - - /// Create zero quantity - #[must_use] - pub const fn zero() -> Self { - #[allow(clippy::as_conversions)] - Self::ZERO - } - - /// Create a quantity from an i64 value - /// - /// # Errors - /// Returns error if the operation fails - #[allow(clippy::as_conversions)] - pub fn from_i64(value: i64) -> Result { - Self::from_f64(value as f64) - } - - /// Create a quantity from a u64 value - /// - /// # Errors - #[allow(clippy::as_conversions)] - /// Returns error if the operation fails - pub fn from_u64(value: u64) -> Result { - Self::from_f64(value as f64) - } - - /// Create a quantity from a Decimal value - /// - /// # Errors - /// Returns error if the operation fails - pub fn from_decimal(decimal: Decimal) -> Result { - use std::convert::TryFrom; - Self::try_from(decimal).map_err(|e| CommonTypeError::InvalidQuantity { - value: decimal.to_string(), - reason: format!("Failed to convert Decimal to Quantity: {}", e), - }) - } - - /// Check if quantity is zero - #[must_use] - pub const fn is_zero(&self) -> bool { - self.value == 0 - } - - /// Check if quantity has a non-zero value - #[must_use] - pub const fn is_some(&self) -> bool { - !self.is_zero() - } - - /// Check if quantity is zero (none) - #[must_use] - pub const fn is_none(&self) -> bool { - self.is_zero() - } - - /// Get a reference to self - #[must_use] - pub const fn as_ref(&self) -> &Self { - self - } - - /// Get absolute value (always positive for Quantity) - #[must_use] - pub const fn abs(&self) -> Self { - *self - } - - /// Get the sign of the quantity (1.0 for positive, 0.0 for zero) - #[must_use] - pub const fn signum(&self) -> f64 { - if self.value > 0 { - 1.0 - } else { - 0.0 - } - } - - /// Check if quantity is positive - #[must_use] - pub const fn is_positive(&self) -> bool { - self.value > 0 - } - - /// Check if quantity is negative (always false for Quantity) - #[must_use] - pub const fn is_negative(&self) -> bool { - false - } - - /// Convert quantity to f64 representation - #[must_use] - pub fn as_f64(&self) -> f64 { - self.to_f64() - } - - /// Create quantity from number of shares - #[must_use] - pub const fn from_shares(shares: u64) -> Self { - Self { - value: shares.saturating_mul(100_000_000), - } - } - - /// Convert quantity to number of shares - #[must_use] - #[allow(clippy::integer_division)] - pub const fn to_shares(&self) -> u64 { - self.value / 100_000_000 - } - - /// Multiply this quantity by another quantity - /// - /// # Errors - /// Returns error if the operation fails - #[allow(clippy::float_arithmetic)] - #[allow(clippy::arithmetic_side_effects)] - pub fn multiply(&self, other: Self) -> Result { - Self::from_f64(self.to_f64() * other.to_f64()) - } - - /// Subtract another quantity from this quantity - #[must_use] - pub fn subtract(&self, other: Self) -> Self { - Self { - value: self.value.checked_sub(other.value).unwrap_or_else(|| { - tracing::warn!( - "Quantity subtraction underflow: {} - {}, returning 0", - self.value, - other.value - ); - 0 - }), - } - } - - /// Checked addition with overflow protection - #[must_use] - pub const fn checked_add(&self, other: Self) -> Option { - match self.value.checked_add(other.value) { - Some(value) => Some(Self { value }), - None => None, - } - } - - /// Checked subtraction with underflow protection - #[must_use] - pub const fn checked_sub(&self, other: Self) -> Option { - match self.value.checked_sub(other.value) { - Some(value) => Some(Self { value }), - None => None, - } - } -} - -impl Default for Quantity { - fn default() -> Self { - Self::ZERO - } -} - -impl FromStr for Quantity { - type Err = CommonTypeError; - - fn from_str(s: &str) -> Result { - let parsed_value = s - .parse::() - .map_err(|e| CommonTypeError::InvalidQuantity { - value: s.to_owned(), - reason: format!("Cannot parse '{}' as quantity: {}", s, e), - })?; - Self::from_f64(parsed_value) - } -} - -impl fmt::Display for Quantity { - /// Format the quantity for display with 8 decimal places - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:.8}", self.to_f64()) - } -} - -impl TryFrom for Quantity { - type Error = CommonTypeError; - fn try_from(value: i32) -> Result { - Self::new(f64::from(value)) - } -} - -impl TryFrom for Quantity { - type Error = CommonTypeError; - fn try_from(value: u64) -> Result { - #[allow(clippy::as_conversions)] - let f64_value = value as f64; - Self::new(f64_value) - } -} - -impl TryFrom for Quantity { - type Error = CommonTypeError; - fn try_from(value: f64) -> Result { - Self::new(value) - } -} - -impl TryFrom for Quantity { - type Error = CommonTypeError; - fn try_from(decimal: Decimal) -> Result { - let f64_val: f64 = - TryInto::::try_into(decimal).map_err(|e| CommonTypeError::ConversionError { - message: format!("Failed to convert Decimal to f64: {}", e), - })?; - Self::from_f64(f64_val) - } -} - -impl TryFrom for Quantity { - type Error = CommonTypeError; - fn try_from(s: String) -> Result { - Self::from_str(&s) - } -} - -impl TryFrom<&str> for Quantity { - type Error = CommonTypeError; - fn try_from(s: &str) -> Result { - Self::from_str(s) - } -} - -impl PartialEq for Quantity { - #[allow(clippy::float_arithmetic)] - fn eq(&self, other: &f64) -> bool { - (self.to_f64() - other).abs() < f64::EPSILON - } -} - -impl PartialEq for f64 { - #[allow(clippy::float_arithmetic)] - fn eq(&self, other: &Quantity) -> bool { - (self - other.to_f64()).abs() < f64::EPSILON - } -} - -impl PartialOrd for Quantity { - fn partial_cmp(&self, other: &f64) -> Option { - self.to_f64().partial_cmp(other) - } -} - -impl PartialOrd for f64 { - fn partial_cmp(&self, other: &Quantity) -> Option { - self.partial_cmp(&other.to_f64()) - } -} - -impl Add for Quantity { - type Output = Self; - fn add(self, rhs: Self) -> Self::Output { - Self { - value: self.value.saturating_add(rhs.value), - } - } -} - -impl Sub for Quantity { - type Output = Self; - fn sub(self, rhs: Self) -> Self::Output { - Self { - value: self.value.saturating_sub(rhs.value), - } - } -} - -impl Mul for Quantity { - #[allow(clippy::float_arithmetic)] - type Output = Result; - fn mul(self, rhs: f64) -> Self::Output { - Self::from_f64(self.to_f64() * rhs) - } -} - -impl Div for Quantity { - #[allow(clippy::float_arithmetic)] - type Output = Result; - fn div(self, rhs: f64) -> Self::Output { - if rhs == 0.0_f64 { - return Err(CommonTypeError::ConversionError { - message: "Cannot divide quantity by zero".to_owned(), - }); - } - Self::from_f64(self.to_f64() / rhs) - } -} - -impl Sum for Quantity { - fn sum>(iter: I) -> Self { - iter.fold(Self::ZERO, |acc, x| Self { - value: acc.value.saturating_add(x.value), - }) - } -} - -impl<'quantity> Sum<&'quantity Self> for Quantity { - fn sum>(iter: I) -> Self { - iter.fold(Self::ZERO, |acc, x| Self { - value: acc.value.saturating_add(x.value), - }) - } -} - -// ============================================================================= -// SQLX IMPLEMENTATIONS FOR FINANCIAL TYPES -// ============================================================================= - -#[cfg(feature = "database")] -mod sqlx_impls { - use super::{HftTimestamp, MarketRegime, OrderSide, OrderStatus, OrderType, Price, Quantity}; - use rust_decimal::Decimal as RustDecimal; - use sqlx::{ - decode::Decode, - encode::{Encode, IsNull}, - error::BoxDynError, - postgres::{PgArgumentBuffer, PgTypeInfo, PgValueRef, Postgres}, - Type, - }; - - // SQLx implementations for Price - impl Type for Price { - fn type_info() -> PgTypeInfo { - PgTypeInfo::with_name("NUMERIC") - } - } - - impl<'query> Encode<'query, Postgres> for Price { - fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { - // Convert our fixed-point u64 to rust_decimal::Decimal with 8 decimal places - let decimal_value = RustDecimal::new(i64::try_from(self.raw_value()).unwrap_or(0), 8); - decimal_value.encode_by_ref(buf) - } - } - - impl<'row> Decode<'row, Postgres> for Price { - fn decode(value: PgValueRef<'row>) -> Result { - // Decode from NUMERIC to rust_decimal::Decimal - let decimal_value = >::decode(value)?; - - // Validate scale matches our fixed-point precision (8 decimal places) - if decimal_value.scale() != 8 { - return Err(format!( - "Invalid scale for Price: expected 8, got {}", - decimal_value.scale() - ) - .into()); - } - - // Extract mantissa and convert to our u64 representation - let mantissa = decimal_value.mantissa(); - let inner_val = u64::try_from(mantissa).map_err(|e| { - format!( - "Failed to convert negative or overflowing NUMERIC to Price: {}", - e - ) - })?; - - Ok(Price::from_raw(inner_val)) - } - } - // SQLx implementations for Quantity - impl Type for Quantity { - fn type_info() -> PgTypeInfo { - PgTypeInfo::with_name("NUMERIC") - } - } - - impl<'query> Encode<'query, Postgres> for Quantity { - fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { - // Convert our fixed-point u64 to rust_decimal::Decimal with 8 decimal places - let decimal_value = RustDecimal::new(i64::try_from(self.raw_value()).unwrap_or(0), 8); - decimal_value.encode_by_ref(buf) - } - } - - impl<'row> Decode<'row, Postgres> for Quantity { - fn decode(value: PgValueRef<'row>) -> Result { - // Decode from NUMERIC to rust_decimal::Decimal - let decimal_value = >::decode(value)?; - - // Validate scale matches our fixed-point precision (8 decimal places) - if decimal_value.scale() != 8 { - return Err(format!( - "Invalid scale for Quantity: expected 8, got {}", - decimal_value.scale() - ) - .into()); - } - - // Extract mantissa and convert to our u64 representation - let mantissa = decimal_value.mantissa(); - let inner_val = u64::try_from(mantissa).map_err(|e| { - format!( - "Failed to convert negative or overflowing NUMERIC to Quantity: {}", - e - ) - })?; - - Ok(Quantity::from_raw(inner_val)) - } - } - - // SQLx implementations for TimeInForce - impl Type for super::TimeInForce { - fn type_info() -> PgTypeInfo { - PgTypeInfo::with_name("TEXT") - } - } - - impl<'query> Encode<'query, Postgres> for super::TimeInForce { - fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { - // Use the Display trait to convert enum to string representation - <&str as Encode>::encode(self.to_string().as_str(), buf) - } - } - - impl<'row> Decode<'row, Postgres> for super::TimeInForce { - fn decode(value: PgValueRef<'row>) -> Result { - // Decode from TEXT to string, then parse to enum - let s = <&str as Decode>::decode(value)?; - match s { - "DAY" => Ok(super::TimeInForce::Day), - "GTC" => Ok(super::TimeInForce::GoodTillCancel), - "IOC" => Ok(super::TimeInForce::ImmediateOrCancel), - "FOK" => Ok(super::TimeInForce::FillOrKill), - _ => Err(format!("Invalid TimeInForce value: {}", s).into()), - } - } - } - - // SQLx implementations for OrderStatus - impl Type for OrderStatus { - fn type_info() -> PgTypeInfo { - PgTypeInfo::with_name("TEXT") - } - } - - impl<'query> Encode<'query, Postgres> for OrderStatus { - fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { - let value = match self { - OrderStatus::Created => "CREATED", - OrderStatus::Submitted => "SUBMITTED", - OrderStatus::PartiallyFilled => "PARTIALLY_FILLED", - OrderStatus::Filled => "FILLED", - OrderStatus::Rejected => "REJECTED", - OrderStatus::Cancelled => "CANCELLED", - OrderStatus::New => "NEW", - OrderStatus::Expired => "EXPIRED", - OrderStatus::Pending => "PENDING", - OrderStatus::Working => "WORKING", - OrderStatus::Unknown => "UNKNOWN", - OrderStatus::Suspended => "SUSPENDED", - OrderStatus::PendingCancel => "PENDING_CANCEL", - OrderStatus::PendingReplace => "PENDING_REPLACE", - }; - <&str as Encode>::encode_by_ref(&value, buf) - } - } - - impl<'row> Decode<'row, Postgres> for OrderStatus { - fn decode(value: PgValueRef<'row>) -> Result { - let s = >::decode(value)?; - match s.as_str() { - "CREATED" => Ok(OrderStatus::Created), - "SUBMITTED" => Ok(OrderStatus::Submitted), - "PARTIALLY_FILLED" => Ok(OrderStatus::PartiallyFilled), - "FILLED" => Ok(OrderStatus::Filled), - "REJECTED" => Ok(OrderStatus::Rejected), - "CANCELLED" => Ok(OrderStatus::Cancelled), - "NEW" => Ok(OrderStatus::New), - "EXPIRED" => Ok(OrderStatus::Expired), - "PENDING" => Ok(OrderStatus::Pending), - "WORKING" => Ok(OrderStatus::Working), - "UNKNOWN" => Ok(OrderStatus::Unknown), - "SUSPENDED" => Ok(OrderStatus::Suspended), - "PENDING_CANCEL" => Ok(OrderStatus::PendingCancel), - "PENDING_REPLACE" => Ok(OrderStatus::PendingReplace), - _ => Err(format!("Invalid OrderStatus value: {}", s).into()), - } - } - } - - // SQLx implementations for OrderSide - impl Type for OrderSide { - fn type_info() -> PgTypeInfo { - PgTypeInfo::with_name("TEXT") - } - } - - impl<'query> Encode<'query, Postgres> for OrderSide { - fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { - let value = match self { - OrderSide::Buy => "BUY", - OrderSide::Sell => "SELL", - }; - <&str as Encode>::encode_by_ref(&value, buf) - } - } - - impl<'row> Decode<'row, Postgres> for OrderSide { - fn decode(value: PgValueRef<'row>) -> Result { - let s = >::decode(value)?; - match s.as_str() { - "BUY" => Ok(OrderSide::Buy), - "SELL" => Ok(OrderSide::Sell), - _ => Err(format!("Invalid OrderSide value: {}", s).into()), - } - } - } - - // SQLx implementations for OrderType - impl Type for OrderType { - fn type_info() -> PgTypeInfo { - PgTypeInfo::with_name("TEXT") - } - } - - impl<'query> Encode<'query, Postgres> for OrderType { - fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { - let value = match self { - OrderType::Market => "MARKET", - OrderType::Limit => "LIMIT", - OrderType::Stop => "STOP", - OrderType::StopLimit => "STOP_LIMIT", - OrderType::Iceberg => "ICEBERG", - OrderType::TrailingStop => "TRAILING_STOP", - OrderType::Hidden => "HIDDEN", - }; - <&str as Encode>::encode_by_ref(&value, buf) - } - } - - impl<'row> Decode<'row, Postgres> for OrderType { - fn decode(value: PgValueRef<'row>) -> Result { - let s = >::decode(value)?; - match s.as_str() { - "MARKET" => Ok(OrderType::Market), - "LIMIT" => Ok(OrderType::Limit), - "STOP" => Ok(OrderType::Stop), - "STOP_LIMIT" => Ok(OrderType::StopLimit), - "ICEBERG" => Ok(OrderType::Iceberg), - "TRAILING_STOP" => Ok(OrderType::TrailingStop), - "HIDDEN" => Ok(OrderType::Hidden), - _ => Err(format!("Invalid OrderType value: {}", s).into()), - } - } - } - - // SQLx implementations for MarketRegime - impl Type for MarketRegime { - fn type_info() -> PgTypeInfo { - PgTypeInfo::with_name("TEXT") - } - } - - impl<'query> Encode<'query, Postgres> for MarketRegime { - fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { - let value = match self { - MarketRegime::Normal => "NORMAL", - MarketRegime::Crisis => "CRISIS", - MarketRegime::Trending => "TRENDING", - MarketRegime::Sideways => "SIDEWAYS", - MarketRegime::Bull => "BULL", - MarketRegime::Bear => "BEAR", - MarketRegime::HighVolatility => "HIGH_VOLATILITY", - MarketRegime::LowVolatility => "LOW_VOLATILITY", - MarketRegime::Volatile => "VOLATILE", - MarketRegime::Calm => "CALM", - MarketRegime::Unknown => "UNKNOWN", - MarketRegime::Recovery => "RECOVERY", - MarketRegime::Bubble => "BUBBLE", - MarketRegime::Correction => "CORRECTION", - MarketRegime::Custom(id) => { - return >::encode_by_ref( - &format!("CUSTOM_{}", id), - buf, - ) - }, - }; - <&str as Encode>::encode_by_ref(&value, buf) - } - } - - impl<'row> Decode<'row, Postgres> for MarketRegime { - fn decode(value: PgValueRef<'row>) -> Result { - let s = >::decode(value)?; - match s.as_str() { - "NORMAL" => Ok(MarketRegime::Normal), - "CRISIS" => Ok(MarketRegime::Crisis), - "TRENDING" => Ok(MarketRegime::Trending), - "SIDEWAYS" => Ok(MarketRegime::Sideways), - "BULL" => Ok(MarketRegime::Bull), - "BEAR" => Ok(MarketRegime::Bear), - "HIGH_VOLATILITY" => Ok(MarketRegime::HighVolatility), - "LOW_VOLATILITY" => Ok(MarketRegime::LowVolatility), - "VOLATILE" => Ok(MarketRegime::Volatile), - "CALM" => Ok(MarketRegime::Calm), - "UNKNOWN" => Ok(MarketRegime::Unknown), - "RECOVERY" => Ok(MarketRegime::Recovery), - "BUBBLE" => Ok(MarketRegime::Bubble), - "CORRECTION" => Ok(MarketRegime::Correction), - _ => { - // Handle Custom(id) format - if let Some(id_str) = s.strip_prefix("CUSTOM_") { - if let Ok(id) = id_str.parse::() { - Ok(MarketRegime::Custom(id)) - } else { - Err(format!("Invalid MarketRegime Custom ID: {}", id_str).into()) - } - } else { - Err(format!("Invalid MarketRegime value: {}", s).into()) - } - }, - } - } - } - - // SQLx implementations for HftTimestamp - // Maps to PostgreSQL BIGINT (stores nanoseconds since Unix epoch) - // Note: Limited to i64::MAX nanoseconds (year 2262) due to PostgreSQL BIGINT constraints - impl<'query> Encode<'query, Postgres> for HftTimestamp { - fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { - // Cast u64 to i64 for PostgreSQL BIGINT compatibility - >::encode(i64::try_from(self.nanos()).unwrap_or(0), buf) - } - } - - impl<'row> Decode<'row, Postgres> for HftTimestamp { - fn decode(value: PgValueRef<'row>) -> Result { - let val = >::decode(value)?; - // Cast i64 back to u64 for internal representation - Ok(HftTimestamp::from_nanos(u64::try_from(val).unwrap_or(0))) - } - } - - impl Type for HftTimestamp { - fn type_info() -> ::TypeInfo { - >::type_info() - } - - fn compatible(ty: &::TypeInfo) -> bool { - >::compatible(ty) - } - } - - // SQLx implementations for OrderId (uses BIGINT for u64) - impl Type for super::OrderId { - fn type_info() -> PgTypeInfo { - PgTypeInfo::with_name("BIGINT") - } - } - - impl<'query> Encode<'query, Postgres> for super::OrderId { - fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { - >::encode_by_ref( - &(i64::try_from(self.value()).unwrap_or(0)), - buf, - ) - } - } - - impl<'row> Decode<'row, Postgres> for super::OrderId { - fn decode(value: PgValueRef<'row>) -> Result { - let id = >::decode(value)?; - Ok(super::OrderId::from_u64(u64::try_from(id).unwrap_or(0))) - } - } -} - -/// Volume type - alias for Quantity with the same fixed-point arithmetic -/// -/// `SQLx` traits are automatically inherited from Quantity -pub type Volume = Quantity; - -// ORDER TYPES ALREADY DEFINED ABOVE - No need to re-export from trading_engine -// ============================================================================= -// CORE ID TYPES (MOVED FROM TRADING_ENGINE) -// ============================================================================= - -/// Order identifier with ultra-fast atomic generation -/// -/// Replaces slow UUID generation (1ms+) with atomic increment (~5ns) -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct OrderId(u64); - -impl Default for OrderId { - fn default() -> Self { - Self::new() - } -} - -impl OrderId { - /// Generate next `OrderId` using atomic counter - <50ns performance - pub fn new() -> Self { - use std::sync::atomic::{AtomicU64, Ordering}; - static COUNTER: AtomicU64 = AtomicU64::new(1); - Self(COUNTER.fetch_add(1, Ordering::Relaxed)) - } - - /// Create `OrderId` from u64 value - #[must_use] - pub const fn from_u64(value: u64) -> Self { - Self(value) - } - - /// Get u64 value - #[must_use] - pub const fn value(&self) -> u64 { - self.0 - } - - /// Get u64 value for performance-critical code (alias for value) - #[must_use] - pub const fn as_u64(&self) -> u64 { - self.0 - } - - /// Get as string for compatibility - #[must_use] - pub fn as_str(&self) -> String { - self.0.to_string() - } -} - -impl fmt::Display for OrderId { - /// Format the order ID for display - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for OrderId { - /// Create an `OrderId` from a u64 value - fn from(value: u64) -> Self { - Self(value) - } -} - -impl From for u64 { - /// Convert an `OrderId` to u64 - fn from(order_id: OrderId) -> Self { - order_id.0 - } -} - -impl FromStr for OrderId { - type Err = ParseIntError; - - fn from_str(s: &str) -> Result { - s.parse::().map(OrderId) - } -} - -impl From for OrderId { - /// Create an `OrderId` from a String, generating new ID if parsing fails - fn from(s: String) -> Self { - s.parse().unwrap_or_else(|_| Self::new()) - } -} - -impl From<&str> for OrderId { - /// Create an `OrderId` from a &str, generating new ID if parsing fails - fn from(s: &str) -> Self { - s.parse().unwrap_or_else(|_| Self::new()) - } -} - -/// Execution identifier with validation -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[cfg_attr(feature = "database", derive(sqlx::Type))] -pub struct ExecutionId(String); - -impl ExecutionId { - /// Create a new execution ID with validation - /// - /// # Errors - /// Returns error if the operation fails - pub fn new>(id: S) -> Result { - let id = id.into(); - if id.trim().is_empty() { - return Err(CommonTypeError::ValidationError { - field: "execution_id".to_owned(), - reason: "Execution ID cannot be empty".to_owned(), - }); - } - Ok(Self(id)) - } - - /// Generate a new random execution ID - pub fn generate() -> Self { - Self(uuid::Uuid::new_v4().to_string()) - } - - /// Get execution ID as string slice - pub fn as_str(&self) -> &str { - &self.0 - } - - /// Convert execution ID into owned string - pub fn into_string(self) -> String { - self.0 - } -} - -impl fmt::Display for ExecutionId { - /// Format the execution ID for display - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl FromStr for ExecutionId { - type Err = CommonTypeError; - - fn from_str(s: &str) -> Result { - Self::new(s) - } -} - -/// Trade identifier with validation -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct TradeId(String); - -impl TradeId { - /// Create a new trade ID with validation - /// - /// # Errors - /// Returns error if the operation fails - pub fn new>(id: S) -> Result { - let id = id.into(); - if id.is_empty() { - return Err(CommonTypeError::ValidationError { - field: "trade_id".to_owned(), - reason: "Trade ID cannot be empty".to_owned(), - }); - } - Ok(Self(id)) - } - - /// Get the trade ID as a string slice - pub fn as_str(&self) -> &str { - &self.0 - } - /// Convert the trade ID into an owned string - pub fn into_string(self) -> String { - self.0 - } -} - -impl fmt::Display for TradeId { - /// Format the trade ID for display - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -/// Trading symbol with validation -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[cfg_attr(feature = "database", derive(sqlx::Type))] -pub struct Symbol { - value: String, -} - -impl Symbol { - /// Create a new symbol from a string - #[must_use] - pub const fn new(s: String) -> Self { - Self { value: s } - } - - /// Create a new Symbol with validation - /// - /// # Errors - /// Returns error if the operation fails - pub fn new_validated(s: String) -> Result { - if s.trim().is_empty() { - return Err(CommonTypeError::ValidationError { - field: "symbol".to_owned(), - reason: "Symbol cannot be empty".to_owned(), - }); - } - Ok(Self { value: s }) - } - - /// Create a Symbol from &str with validation - /// - /// # Errors - /// Returns error if the operation fails - pub fn from_str_validated(s: &str) -> Result { - Self::new_validated(s.to_owned()) - } - - /// Get the symbol as a string slice - #[must_use] - pub fn as_str(&self) -> &str { - &self.value - } - /// Get the symbol value as a string slice - #[must_use] - pub fn value(&self) -> &str { - &self.value - } - /// Get the symbol as bytes - #[must_use] - pub fn as_bytes(&self) -> &[u8] { - self.value.as_bytes() - } - /// Check if the symbol is empty - #[must_use] - pub fn is_empty(&self) -> bool { - self.value.is_empty() - } - /// Convert the symbol to uppercase - #[must_use] - pub fn to_uppercase(&self) -> String { - self.value.to_uppercase() - } - /// Replace occurrences in the symbol - #[must_use] - pub fn replace(&self, from: &str, to: &str) -> String { - self.value.replace(from, to) - } - - /// Helper for risk management - creates a 'NONE' symbol - #[must_use] - pub fn none() -> Self { - "NONE".parse().unwrap() - } - - /// Check if the symbol contains a pattern - #[must_use] - pub fn contains(&self, pattern: &str) -> bool { - self.value.contains(pattern) - } -} - -impl FromStr for Symbol { - type Err = std::convert::Infallible; - - fn from_str(s: &str) -> Result { - Ok(Self { - value: s.to_owned(), - }) - } -} - -// Additional implementation to support conversion from &Symbol to &str -impl AsRef for Symbol { - /// Convert symbol to string reference - fn as_ref(&self) -> &str { - &self.value - } -} - -impl fmt::Display for Symbol { - /// Format the symbol for display - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.value) - } -} - -impl From for Symbol { - /// Create a Symbol from a String - fn from(s: String) -> Self { - Self::new(s) - } -} -impl From<&str> for Symbol { - /// Create a Symbol from a &str - fn from(s: &str) -> Self { - Self::new(s.to_owned()) - } -} - -// TryFrom implementations removed due to conflicting blanket implementations -// Use Symbol::new_validated() or Symbol::from_validated() directly instead - -impl Default for Symbol { - /// Returns the default symbol (empty string) - fn default() -> Self { - Self::new(String::new()) - } -} - -impl PartialEq for Symbol { - fn eq(&self, other: &str) -> bool { - self.value == other - } -} - -impl PartialEq<&str> for Symbol { - fn eq(&self, other: &&str) -> bool { - self.value == *other - } -} - -impl PartialEq for Symbol { - fn eq(&self, other: &String) -> bool { - &self.value == other - } -} - -impl PartialEq for &str { - fn eq(&self, other: &Symbol) -> bool { - *self == other.value - } -} - -impl PartialEq for String { - fn eq(&self, other: &Symbol) -> bool { - self == &other.value - } -} - -// TimeInForce moved to canonical source: common::types::TimeInForce - -// Currency moved to canonical source: common::types::Currency - -// Price moved to canonical source: common::types::Price - -// Quantity moved to canonical source: common::types::Quantity -// Volume moved to canonical source: common::types::Quantity (as Volume alias) - -/// Money amount with currency -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Money { - /// The monetary amount - pub amount: Decimal, - /// The currency of the amount - pub currency: Currency, -} - -impl Money { - /// Create new money amount - pub const fn new(amount: Decimal, currency: Currency) -> Self { - Self { amount, currency } - } -} - -impl fmt::Display for Money { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} {}", self.amount, self.currency) - } -} - -// OrderId moved to canonical source: common::types::OrderId - -// TradeId moved to canonical source: common::types::TradeId - -// Symbol moved to canonical source: common::types::Symbol - -/// Type-safe account identifier -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct AccountId(String); - -impl AccountId { - /// Create a new account ID with validation - /// - /// # Errors - /// Returns error if the operation fails - pub fn new>(id: S) -> Result { - let id = id.into(); - if id.trim().is_empty() { - return Err(CommonTypeError::InvalidIdentifier { - field: "account_id".to_owned(), - reason: "Account ID cannot be empty".to_owned(), - }); - } - Ok(Self(id)) - } - - /// Get the ID as a string slice - pub fn as_str(&self) -> &str { - &self.0 - } - - /// Convert to owned String - pub fn into_string(self) -> String { - self.0 - } -} - -impl fmt::Display for AccountId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -/// High-precision timestamp for HFT applications - CANONICAL DEFINITION -/// -/// Robust implementation with error handling for financial safety -#[derive( - Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default, -)] -pub struct HftTimestamp { - nanos: u64, -} - -impl HftTimestamp { - /// Get current timestamp with error handling for financial safety - /// - /// # Errors - /// Returns error if the operation fails - pub fn now() -> Result { - use std::time::{SystemTime, UNIX_EPOCH}; - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|e| CommonError::Service { - category: CommonErrorCategory::System, - message: format!("System time before UNIX epoch: {e}"), - })? - .as_nanos() - .try_into() - .unwrap_or(0_u64); - Ok(Self { nanos }) - } - - /// Get current timestamp with error handling for financial safety (`CommonTypeError` version) - /// - /// # Errors - /// Returns error if the operation fails - pub fn now_common() -> Result { - use std::time::{SystemTime, UNIX_EPOCH}; - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|e| CommonTypeError::ConversionError { - message: format!("System time before UNIX epoch: {e}"), - })? - .as_nanos() - .try_into() - .unwrap_or(0_u64); - Ok(Self { nanos }) - } - - /// Get current timestamp or zero if system time is invalid - #[must_use] - pub fn now_or_zero() -> Self { - Self::now().unwrap_or(Self { nanos: 0 }) - } - - /// Get nanoseconds since epoch - #[must_use] - pub const fn nanos(self) -> u64 { - self.nanos - } - - #[allow(clippy::as_conversions)] - /// Create from nanoseconds since epoch - #[must_use] - pub const fn from_nanos(nanos: u64) -> Self { - Self { nanos } - } - - /// Create from signed nanoseconds (cast to unsigned) - #[must_use] - #[allow(clippy::as_conversions)] - pub const fn from_nanos_i64(nanos: i64) -> Self { - Self { - nanos: nanos as u64, - } - } - - /// Get nanoseconds since epoch - pub const fn as_nanos(&self) -> u64 { - self.nanos - } - - /// Convert to `DateTime` - #[allow(clippy::integer_division)] - pub fn to_datetime(&self) -> DateTime { - let secs = self.nanos / 1_000_000_000; - let nsecs = u32::try_from(self.nanos % 1_000_000_000).unwrap_or(0); - DateTime::from_timestamp(i64::try_from(secs).unwrap_or(0), nsecs).unwrap_or_default() - } -} - -impl fmt::Display for HftTimestamp { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.to_datetime()) - } -} - -/// Generic timestamp for general use cases -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -pub struct GenericTimestamp { - nanos: u64, -} - -impl GenericTimestamp { - /// Create from nanoseconds since epoch - #[must_use] - pub const fn from_nanos(nanos: u64) -> Self { - Self { nanos } - } - - /// Get nanoseconds since epoch - #[must_use] - pub const fn nanos(&self) -> u64 { - self.nanos - } -} - -// ============================================================================= -// MARKET TYPES (MIGRATED FROM TRADING_ENGINE) -// ============================================================================= - -/// Market regime enumeration for position sizing scaling and risk management -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum MarketRegime { - /// Normal market conditions - Normal, - /// Crisis/stress market conditions - Crisis, - /// Trending market (strong directional movement) - Trending, - /// Sideways/ranging market (low volatility) - Sideways, - /// Bull market (sustained upward trend) - Bull, - /// Bear market (sustained downward trend) - Bear, - /// High volatility market conditions - HighVolatility, - /// Low volatility market conditions - LowVolatility, - /// Volatile market conditions (alias for `HighVolatility`) - Volatile, - /// Calm market conditions (alias for `LowVolatility`) - Calm, - /// Unknown/unclassified regime - Unknown, - /// Recovery regime - transitioning from crisis - Recovery, - /// Bubble regime - unsustainable upward movement - Bubble, - /// Correction regime - temporary downward adjustment - Correction, - /// Custom regime with numeric identifier - Custom(usize), -} - -impl Default for MarketRegime { - fn default() -> Self { - Self::Normal - } -} - -impl fmt::Display for MarketRegime { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Normal => write!(f, "Normal"), - Self::Crisis => write!(f, "Crisis"), - Self::Trending => write!(f, "Trending"), - Self::Sideways => write!(f, "Sideways"), - Self::Bull => write!(f, "Bull"), - Self::Bear => write!(f, "Bear"), - Self::HighVolatility => write!(f, "HighVolatility"), - Self::LowVolatility => write!(f, "LowVolatility"), - Self::Volatile => write!(f, "Volatile"), - Self::Calm => write!(f, "Calm"), - Self::Unknown => write!(f, "Unknown"), - Self::Recovery => write!(f, "Recovery"), - Self::Bubble => write!(f, "Bubble"), - Self::Correction => write!(f, "Correction"), - Self::Custom(id) => write!(f, "Custom({id})"), - } - } -} - -/// Tick type enumeration for market data -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[cfg_attr(feature = "database", derive(sqlx::Type))] -#[cfg_attr( - feature = "database", - sqlx(type_name = "tick_type", rename_all = "snake_case") -)] -pub enum TickType { - /// Trade execution tick - Trade, - /// Bid price update tick - Bid, - /// Ask price update tick - Ask, - /// Quote (bid/ask) update tick - Quote, -} - -/// Exchange enumeration for trading venues -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum Exchange { - /// New York Stock Exchange - NYSE, - /// NASDAQ - NASDAQ, - /// Chicago Mercantile Exchange - CME, - /// Intercontinental Exchange - ICE, - /// London Stock Exchange - LSE, - /// Tokyo Stock Exchange - TSE, - /// Hong Kong Stock Exchange - HKEX, - /// Shanghai Stock Exchange - SSE, - /// Shenzhen Stock Exchange - SZSE, - /// Euronext - EURONEXT, - /// Deutsche Börse - XETRA, - /// Chicago Board of Trade - CBOT, - /// Chicago Board Options Exchange - CBOE, - /// BATS Global Markets - BATS, - /// IEX Exchange - IEX, - /// Interactive Brokers - IBKR, - /// IC Markets - ICMARKETS, - /// Forex.com - FOREX, - /// Binance - BINANCE, - /// Coinbase - COINBASE, - /// Kraken - KRAKEN, - /// Unknown or unrecognized exchange - UNKNOWN, -} - -impl Default for Exchange { - fn default() -> Self { - Self::UNKNOWN - } -} - -impl fmt::Display for Exchange { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NYSE => write!(f, "NYSE"), - Self::NASDAQ => write!(f, "NASDAQ"), - Self::CME => write!(f, "CME"), - Self::ICE => write!(f, "ICE"), - Self::LSE => write!(f, "LSE"), - Self::TSE => write!(f, "TSE"), - Self::HKEX => write!(f, "HKEX"), - Self::SSE => write!(f, "SSE"), - Self::SZSE => write!(f, "SZSE"), - Self::EURONEXT => write!(f, "EURONEXT"), - Self::XETRA => write!(f, "XETRA"), - Self::CBOT => write!(f, "CBOT"), - Self::CBOE => write!(f, "CBOE"), - Self::BATS => write!(f, "BATS"), - Self::IEX => write!(f, "IEX"), - Self::IBKR => write!(f, "IBKR"), - Self::ICMARKETS => write!(f, "ICMARKETS"), - Self::FOREX => write!(f, "FOREX"), - Self::BINANCE => write!(f, "BINANCE"), - Self::COINBASE => write!(f, "COINBASE"), - Self::KRAKEN => write!(f, "KRAKEN"), - Self::UNKNOWN => write!(f, "UNKNOWN"), - } - } -} - -impl FromStr for Exchange { - type Err = CommonTypeError; - - fn from_str(s: &str) -> Result { - match s.to_uppercase().as_str() { - "NYSE" => Ok(Self::NYSE), - "NASDAQ" => Ok(Self::NASDAQ), - "CME" => Ok(Self::CME), - "ICE" => Ok(Self::ICE), - "LSE" => Ok(Self::LSE), - "TSE" => Ok(Self::TSE), - "HKEX" => Ok(Self::HKEX), - "SSE" => Ok(Self::SSE), - "SZSE" => Ok(Self::SZSE), - "EURONEXT" => Ok(Self::EURONEXT), - "XETRA" => Ok(Self::XETRA), - "CBOT" => Ok(Self::CBOT), - "CBOE" => Ok(Self::CBOE), - "BATS" => Ok(Self::BATS), - "IEX" => Ok(Self::IEX), - "IBKR" => Ok(Self::IBKR), - "ICMARKETS" => Ok(Self::ICMARKETS), - "FOREX" => Ok(Self::FOREX), - "BINANCE" => Ok(Self::BINANCE), - "COINBASE" => Ok(Self::COINBASE), - "KRAKEN" => Ok(Self::KRAKEN), - "UNKNOWN" => Ok(Self::UNKNOWN), - _ => Ok(Self::UNKNOWN), // Default to UNKNOWN for unrecognized exchanges - } - } -} - -/// Market tick data structure - CANONICAL SINGLE SOURCE OF TRUTH -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct MarketTick { - /// Trading symbol - pub symbol: Symbol, - /// Tick price - pub price: Price, - /// Tick size/quantity - pub size: Quantity, - /// Tick timestamp - pub timestamp: HftTimestamp, - /// Type of tick (trade, bid, ask, quote) - pub tick_type: TickType, - /// Exchange where the tick occurred - pub exchange: Exchange, - /// Sequence number for ordering - pub sequence_number: u64, -} - -impl MarketTick { - /// Create a new market tick with current timestamp - /// - /// # Errors - /// Returns error if the operation fails - pub fn new( - symbol: Symbol, - price: Price, - size: Quantity, - tick_type: TickType, - exchange: Exchange, - sequence_number: u64, - ) -> Result { - Ok(Self { - symbol, - price, - size, - timestamp: HftTimestamp::now()?, - tick_type, - exchange, - sequence_number, - }) - } - - /// Create a new market tick with specified timestamp (for backtesting) - #[must_use] - pub const fn with_timestamp( - symbol: Symbol, - price: Price, - size: Quantity, - timestamp: HftTimestamp, - tick_type: TickType, - exchange: Exchange, - sequence_number: u64, - ) -> Self { - Self { - symbol, - price, - size, - timestamp, - tick_type, - exchange, - sequence_number, - } - } -} - -/// Trading signal for algorithmic trading -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct TradingSignal { - /// Signal ID - pub signal_id: Uuid, - /// Symbol this signal applies to - pub symbol: Symbol, - /// Signal strength (-1.0 to 1.0) - pub strength: f64, - /// Signal direction - pub direction: OrderSide, - /// Confidence level (0.0 to 1.0) - pub confidence: f64, - /// Signal generation timestamp - pub timestamp: HftTimestamp, - /// Signal source/strategy - pub source: String, - /// Additional metadata - pub metadata: std::collections::HashMap, -} - -impl TradingSignal { - /// Create a new trading signal - /// - /// # Errors - /// Returns error if the operation fails - pub fn new( - symbol: Symbol, - strength: f64, - direction: OrderSide, - confidence: f64, - source: String, - ) -> Result { - if !(0.0_f64..=1.0_f64).contains(&confidence) { - return Err(CommonTypeError::ValidationError { - field: "confidence".to_owned(), - reason: "Confidence must be between 0.0 and 1.0".to_owned(), - }); - } - if !(-1.0_f64..=1.0_f64).contains(&strength) { - return Err(CommonTypeError::ValidationError { - field: "strength".to_owned(), - reason: "Strength must be between -1.0 and 1.0".to_owned(), - }); - } - - Ok(Self { - signal_id: Uuid::new_v4(), - symbol, - strength, - direction, - confidence, - timestamp: HftTimestamp::now_common()?, - source, - metadata: std::collections::HashMap::new(), - }) - } - - /// Add metadata to the signal - #[must_use] - pub fn with_metadata(mut self, key: String, value: String) -> Self { - self.metadata.insert(key, value); - self - } -} - -// ============================================================================= -// HIGH-PERFORMANCE TYPES FOR COPY/CLONE OPTIMIZATION -// ============================================================================= - -/// Lightweight Order reference for high-performance contexts requiring Copy trait -/// -/// This struct contains only the essential order data needed for performance-critical -/// operations like `SmallBatchRing` processing, while maintaining Copy semantics. -/// -/// For full order details, use the complete Order struct. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct OrderRef { - /// Order ID (u64 for performance) - pub id: u64, - /// Symbol hash for fast lookups - pub symbol_hash: i64, - /// Order side (Buy/Sell) - pub side: OrderSide, - /// Order type - pub order_type: OrderType, - /// Quantity (fixed-point u64) - pub quantity: u64, - /// Price (fixed-point u64, 0 for market orders) - pub price: u64, - /// Timestamp (nanoseconds since epoch) - pub timestamp: u64, -} - -impl OrderRef { - /// Create `OrderRef` from a full Order struct - #[must_use] - pub fn from_order(order: &Order) -> Self { - Self { - id: order.id.value(), - symbol_hash: order.symbol_hash(), - side: order.side, - order_type: order.order_type, - quantity: order.quantity.raw_value(), - price: order.price.map_or(0, |p| p.raw_value()), - timestamp: order.created_at.nanos(), - } - } - - /// Create a limit order reference - #[must_use] - pub fn limit(symbol_hash: i64, side: OrderSide, quantity: u64, price: u64) -> Self { - Self { - id: OrderId::new().value(), - symbol_hash, - side, - order_type: OrderType::Limit, - quantity, - price, - timestamp: HftTimestamp::now_or_zero().nanos(), - } - } - - /// Create a market order reference - #[must_use] - pub fn market(symbol_hash: i64, side: OrderSide, quantity: u64) -> Self { - Self { - id: OrderId::new().value(), - symbol_hash, - side, - order_type: OrderType::Market, - quantity, - price: 0, - timestamp: HftTimestamp::now_or_zero().nanos(), - } - } - - /// Get quantity as Quantity type - #[must_use] - pub const fn get_quantity(&self) -> Quantity { - Quantity::from_raw(self.quantity) - } - - /// Get price as Price type (None for market orders) - #[must_use] - pub const fn get_price(&self) -> Option { - if self.price == 0 { - None - } else { - Some(Price::from_raw(self.price)) - } - } - - /// Check if this is a buy order - #[must_use] - pub fn is_buy(&self) -> bool { - self.side == OrderSide::Buy - } - - /// Check if this is a sell order - #[must_use] - pub fn is_sell(&self) -> bool { - self.side == OrderSide::Sell - } - - /// Check if this is a market order - #[must_use] - pub fn is_market_order(&self) -> bool { - self.order_type == OrderType::Market || self.price == 0 - } - - /// Check if this is a limit order - #[must_use] - pub fn is_limit_order(&self) -> bool { - self.order_type == OrderType::Limit && self.price > 0 - } -} - -impl Default for OrderRef { - fn default() -> Self { - Self { - id: 0, - symbol_hash: 0, - side: OrderSide::Buy, - order_type: OrderType::Market, - quantity: 0, - price: 0, - timestamp: 0, - } - } -} - -// ============================================================================= -// COMPREHENSIVE TESTS -// ============================================================================= - -#[cfg(test)] -mod tests { - use super::*; - use std::str::FromStr; - - // ============================================================================= - // Price Tests - // ============================================================================= - - #[test] - fn test_price_from_f64_valid() { - let price = Price::from_f64(100.50).unwrap(); - assert_eq!(price.to_f64(), 100.50); - } - - #[test] - fn test_price_from_f64_negative() { - let result = Price::from_f64(-10.0); - assert!(result.is_err()); - } - - #[test] - fn test_price_from_f64_nan() { - let result = Price::from_f64(f64::NAN); - assert!(result.is_err()); - } - - #[test] - fn test_price_from_f64_infinity() { - let result = Price::from_f64(f64::INFINITY); - assert!(result.is_err()); - } - - #[test] - fn test_price_constants() { - assert_eq!(Price::ZERO.to_f64(), 0.0); - assert_eq!(Price::ONE.to_f64(), 1.0); - assert_eq!(Price::CENT.to_f64(), 0.01); - } - - #[test] - fn test_price_addition() { - let p1 = Price::from_f64(10.0).unwrap(); - let p2 = Price::from_f64(5.5).unwrap(); - let result = p1 + p2; - assert!((result.to_f64() - 15.5).abs() < 0.00001); - } - - #[test] - fn test_price_subtraction() { - let p1 = Price::from_f64(10.0).unwrap(); - let p2 = Price::from_f64(5.5).unwrap(); - let result = p1 - p2; - assert!((result.to_f64() - 4.5).abs() < 0.00001); - } - - #[test] - fn test_price_multiplication() { - let price = Price::from_f64(10.0).unwrap(); - let result = (price * 2.5).unwrap(); - assert!((result.to_f64() - 25.0).abs() < 0.00001); - } - - #[test] - fn test_price_division() { - let price = Price::from_f64(10.0).unwrap(); - let result = (price / 2.0).unwrap(); - assert!((result.to_f64() - 5.0).abs() < 0.00001); - } - - #[test] - fn test_price_division_by_zero() { - let price = Price::from_f64(10.0).unwrap(); - let result = price / 0.0; - assert!(result.is_err()); - } - - #[test] - fn test_price_from_cents() { - let price = Price::from_cents(150); - assert!((price.to_f64() - 1.50).abs() < 0.00001); - } - - #[test] - fn test_price_to_cents() { - let price = Price::from_f64(1.50).unwrap(); - assert_eq!(price.to_cents(), 150); - } - - #[test] - fn test_price_is_zero() { - assert!(Price::ZERO.is_zero()); - assert!(!Price::from_f64(1.0).unwrap().is_zero()); - } - - #[test] - fn test_price_from_str() { - let price = Price::from_str("123.45").unwrap(); - assert!((price.to_f64() - 123.45).abs() < 0.00001); - } - - #[test] - fn test_price_from_str_invalid() { - let result = Price::from_str("invalid"); - assert!(result.is_err()); - } - - #[test] - fn test_price_display() { - let price = Price::from_f64(123.456789).unwrap(); - let display = format!("{}", price); - assert!(display.starts_with("123.45678")); - } - - #[test] - fn test_price_partial_eq_f64() { - let price = Price::from_f64(10.0).unwrap(); - assert_eq!(price, 10.0); - assert_eq!(10.0, price); - } - - #[test] - fn test_price_multiply_price() { - let p1 = Price::from_f64(10.0).unwrap(); - let p2 = Price::from_f64(2.5).unwrap(); - let result = p1.multiply(p2).unwrap(); - assert!((result.to_f64() - 25.0).abs() < 0.00001); - } - - // ============================================================================= - // Quantity Tests - // ============================================================================= - - #[test] - fn test_quantity_from_f64_valid() { - let qty = Quantity::from_f64(100.5).unwrap(); - assert_eq!(qty.to_f64(), 100.5); - } - - #[test] - fn test_quantity_from_f64_negative() { - let result = Quantity::from_f64(-10.0); - assert!(result.is_err()); - } - - #[test] - fn test_quantity_from_f64_nan() { - let result = Quantity::from_f64(f64::NAN); - assert!(result.is_err()); - } - - #[test] - fn test_quantity_constants() { - assert_eq!(Quantity::ZERO.to_f64(), 0.0); - assert_eq!(Quantity::ONE.to_f64(), 1.0); - } - - #[test] - fn test_quantity_addition() { - let q1 = Quantity::from_f64(10.0).unwrap(); - let q2 = Quantity::from_f64(5.5).unwrap(); - let result = q1 + q2; - assert!((result.to_f64() - 15.5).abs() < 0.00001); - } - - #[test] - fn test_quantity_subtraction() { - let q1 = Quantity::from_f64(10.0).unwrap(); - let q2 = Quantity::from_f64(5.5).unwrap(); - let result = q1 - q2; - assert!((result.to_f64() - 4.5).abs() < 0.00001); - } - - #[test] - fn test_quantity_multiplication() { - let qty = Quantity::from_f64(10.0).unwrap(); - let result = (qty * 2.5).unwrap(); - assert!((result.to_f64() - 25.0).abs() < 0.00001); - } - - #[test] - fn test_quantity_division() { - let qty = Quantity::from_f64(10.0).unwrap(); - let result = (qty / 2.0).unwrap(); - assert!((result.to_f64() - 5.0).abs() < 0.00001); - } - - #[test] - fn test_quantity_division_by_zero() { - let qty = Quantity::from_f64(10.0).unwrap(); - let result = qty / 0.0; - assert!(result.is_err()); - } - - #[test] - fn test_quantity_is_zero() { - assert!(Quantity::ZERO.is_zero()); - assert!(!Quantity::from_f64(1.0).unwrap().is_zero()); - } - - #[test] - fn test_quantity_is_positive() { - assert!(Quantity::from_f64(1.0).unwrap().is_positive()); - assert!(!Quantity::ZERO.is_positive()); - } - - #[test] - fn test_quantity_is_negative() { - // Quantity is always non-negative - assert!(!Quantity::from_f64(1.0).unwrap().is_negative()); - assert!(!Quantity::ZERO.is_negative()); - } - - #[test] - fn test_quantity_from_shares() { - let qty = Quantity::from_shares(100); - assert_eq!(qty.to_shares(), 100); - } - - #[test] - fn test_quantity_sum() { - let quantities = vec![ - Quantity::from_f64(1.0).unwrap(), - Quantity::from_f64(2.0).unwrap(), - Quantity::from_f64(3.0).unwrap(), - ]; - let sum: Quantity = quantities.into_iter().sum(); - assert!((sum.to_f64() - 6.0).abs() < 0.00001); - } - - #[test] - fn test_quantity_try_from_i32() { - let qty = Quantity::try_from(100i32).unwrap(); - assert_eq!(qty.to_f64(), 100.0); - } - - #[test] - fn test_quantity_try_from_string() { - let qty = Quantity::try_from("123.45").unwrap(); - assert!((qty.to_f64() - 123.45).abs() < 0.00001); - } - - // ============================================================================= - // Money Tests - // ============================================================================= - - #[test] - fn test_money_new() { - let amount = Decimal::from_f64_retain(100.50).unwrap(); - let money = Money::new(amount, Currency::USD); - assert_eq!(money.currency, Currency::USD); - assert_eq!(money.amount, amount); - } - - #[test] - fn test_money_display() { - let amount = Decimal::from_f64_retain(100.50).unwrap(); - let money = Money::new(amount, Currency::USD); - let display = format!("{}", money); - assert!(display.contains("100.5")); - assert!(display.contains("USD")); - } - - // ============================================================================= - // Symbol Tests - // ============================================================================= - - #[test] - fn test_symbol_new() { - let symbol = Symbol::new("AAPL".to_owned()); - assert_eq!(symbol.as_str(), "AAPL"); - } - - #[test] - fn test_symbol_new_validated_valid() { - let symbol = Symbol::new_validated("AAPL".to_owned()).unwrap(); - assert_eq!(symbol.as_str(), "AAPL"); - } - - #[test] - fn test_symbol_new_validated_empty() { - let result = Symbol::new_validated("".to_owned()); - assert!(result.is_err()); - } - - #[test] - fn test_symbol_new_validated_whitespace() { - let result = Symbol::new_validated(" ".to_owned()); - assert!(result.is_err()); - } - - #[test] - fn test_symbol_from_str() { - let symbol = Symbol::from_str("AAPL").unwrap(); - assert_eq!(symbol.as_str(), "AAPL"); - } - - #[test] - fn test_symbol_to_uppercase() { - let symbol = Symbol::from_str("aapl").unwrap(); - assert_eq!(symbol.to_uppercase(), "AAPL"); - } - - #[test] - fn test_symbol_replace() { - let symbol = Symbol::from_str("AAPL.US").unwrap(); - assert_eq!(symbol.replace(".US", ""), "AAPL"); - } - - #[test] - fn test_symbol_contains() { - let symbol = Symbol::from_str("AAPL.US").unwrap(); - assert!(symbol.contains("AAPL")); - assert!(!symbol.contains("MSFT")); - } - - #[test] - fn test_symbol_partial_eq_str() { - let symbol = Symbol::from_str("AAPL").unwrap(); - assert_eq!("AAPL", symbol); - assert_eq!(symbol.as_str(), "AAPL"); - } - - #[test] - fn test_symbol_none() { - let symbol = Symbol::none(); - assert_eq!(symbol.as_str(), "NONE"); - } - - // ============================================================================= - // TimeInForce Tests - // ============================================================================= - - #[test] - fn test_time_in_force_display() { - assert_eq!(format!("{}", TimeInForce::Day), "DAY"); - assert_eq!(format!("{}", TimeInForce::GoodTillCancel), "GTC"); - assert_eq!(format!("{}", TimeInForce::ImmediateOrCancel), "IOC"); - assert_eq!(format!("{}", TimeInForce::FillOrKill), "FOK"); - } - - #[test] - fn test_time_in_force_default() { - assert_eq!(TimeInForce::default(), TimeInForce::Day); - } - - // ============================================================================= - // OrderType Tests - // ============================================================================= - - #[test] - fn test_order_type_display() { - assert_eq!(format!("{}", OrderType::Market), "MARKET"); - assert_eq!(format!("{}", OrderType::Limit), "LIMIT"); - assert_eq!(format!("{}", OrderType::Stop), "STOP"); - assert_eq!(format!("{}", OrderType::StopLimit), "STOP_LIMIT"); - } - - #[test] - fn test_order_type_try_from_i32_valid() { - assert_eq!(OrderType::try_from(0).unwrap(), OrderType::Market); - assert_eq!(OrderType::try_from(1).unwrap(), OrderType::Limit); - assert_eq!(OrderType::try_from(2).unwrap(), OrderType::Stop); - } - - #[test] - fn test_order_type_try_from_i32_invalid() { - let result = OrderType::try_from(99); - assert!(result.is_err()); - } - - #[test] - fn test_order_type_default() { - assert_eq!(OrderType::default(), OrderType::Market); - } - - // ============================================================================= - // OrderStatus Tests - // ============================================================================= - - #[test] - fn test_order_status_display() { - assert_eq!(format!("{}", OrderStatus::Created), "CREATED"); - assert_eq!(format!("{}", OrderStatus::Filled), "FILLED"); - assert_eq!(format!("{}", OrderStatus::Cancelled), "CANCELLED"); - } - - #[test] - fn test_order_status_try_from_i32_valid() { - assert_eq!(OrderStatus::try_from(0).unwrap(), OrderStatus::Created); - assert_eq!(OrderStatus::try_from(3).unwrap(), OrderStatus::Filled); - assert_eq!(OrderStatus::try_from(5).unwrap(), OrderStatus::Cancelled); - } - - #[test] - fn test_order_status_try_from_i32_invalid() { - let result = OrderStatus::try_from(99); - assert!(result.is_err()); - } - - // ============================================================================= - // OrderSide Tests - // ============================================================================= - - #[test] - fn test_order_side_display() { - assert_eq!(format!("{}", OrderSide::Buy), "BUY"); - assert_eq!(format!("{}", OrderSide::Sell), "SELL"); - } - - #[test] - fn test_order_side_try_from_i32_valid() { - assert_eq!(OrderSide::try_from(0).unwrap(), OrderSide::Buy); - assert_eq!(OrderSide::try_from(1).unwrap(), OrderSide::Sell); - } - - #[test] - fn test_order_side_try_from_i32_invalid() { - let result = OrderSide::try_from(99); - assert!(result.is_err()); - } - - #[test] - fn test_order_side_default() { - assert_eq!(OrderSide::default(), OrderSide::Buy); - } - - // ============================================================================= - // Currency Tests - // ============================================================================= - - #[test] - fn test_currency_display() { - assert_eq!(format!("{}", Currency::USD), "USD"); - assert_eq!(format!("{}", Currency::EUR), "EUR"); - assert_eq!(format!("{}", Currency::BTC), "BTC"); - } - - #[test] - fn test_currency_default() { - assert_eq!(Currency::default(), Currency::USD); - } - - // ============================================================================= - // Error Type Tests - // ============================================================================= - - #[test] - fn test_common_type_error_invalid_price() { - let error = CommonTypeError::InvalidPrice { - value: "abc".to_owned(), - reason: "not a number".to_owned(), - }; - let display = format!("{}", error); - assert!(display.contains("abc")); - } - - #[test] - fn test_common_type_error_invalid_quantity() { - let error = CommonTypeError::InvalidQuantity { - value: "xyz".to_owned(), - reason: "not a number".to_owned(), - }; - let display = format!("{}", error); - assert!(display.contains("xyz")); - } - - #[test] - fn test_common_type_error_validation() { - let error = CommonTypeError::ValidationError { - field: "symbol".to_owned(), - reason: "cannot be empty".to_owned(), - }; - let display = format!("{}", error); - assert!(display.contains("symbol")); - } -} diff --git a/crates/common/src/types/aliases.rs b/crates/common/src/types/aliases.rs new file mode 100644 index 000000000..01d4084d0 --- /dev/null +++ b/crates/common/src/types/aliases.rs @@ -0,0 +1,61 @@ +//! Type aliases for complex shared types +//! +//! Common type aliases used throughout the Foxhunt HFT trading system. + +use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; +use std::collections::HashMap; +use std::sync::{Arc, Mutex, RwLock}; + +// ============================================================================= +// Type Aliases for Complex Types +// ============================================================================= + +/// Common error type for async operations +pub type AsyncResult = Result>; + +/// Thread-safe hash map for shared state +pub type SharedHashMap = Arc>>; + +/// Thread-safe hash map with Mutex for shared state +pub type MutexHashMap = Arc>>; + +/// Thread-safe container for any value +pub type SharedValue = Arc>; + +/// Thread-safe container with Mutex for any value +pub type MutexValue = Arc>; + +// Trading-specific type aliases +/// Map of positions by symbol +pub type PositionMap = SharedHashMap; + +/// Map of orders by order ID +pub type OrderMap = SharedHashMap; + +/// Map of accounts by account ID +pub type AccountMap = SharedHashMap; + +/// Map of instruments by instrument ID +pub type InstrumentMap = SharedHashMap; + +/// Map of market data by symbol +pub type MarketDataMap = SharedHashMap; + +/// Cache entry with timestamp +pub type CacheEntry = (T, DateTime); + +/// Cache map with timestamped entries +pub type CacheMap = SharedHashMap>; + +/// Risk factor loadings by instrument +pub type RiskFactorMap = SharedHashMap>; + +/// Performance metrics history +pub type PerformanceHistory = SharedHashMap>; + +/// Model registry for ML models +pub type ModelRegistry = SharedHashMap; + +/// Generic configuration cache +pub type ConfigCache = SharedHashMap; diff --git a/crates/common/src/types/domain.rs b/crates/common/src/types/domain.rs new file mode 100644 index 000000000..84de2c3b5 --- /dev/null +++ b/crates/common/src/types/domain.rs @@ -0,0 +1,2360 @@ +//! Core domain types for trading +//! +//! Contains Order, Position, Execution, Price, Quantity, Money, Symbol, +//! and associated identifier types (OrderId, ExecutionId, TradeId, AccountId). +//! Also includes HftTimestamp, GenericTimestamp, and Volume alias. + +use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::convert::TryFrom; +use std::fmt; +use std::iter::Sum; +use std::num::ParseIntError; +use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; +use std::str::FromStr; +use uuid::Uuid; + +use crate::error::{CommonError, ErrorCategory as CommonErrorCategory}; +use super::{CommonTypeError, Currency, DecimalExt, OrderSide, OrderStatus, OrderType, TimeInForce}; + +// ============================================================================= +// CORE TRADING TYPES - MIGRATED FROM TRADING_ENGINE +// ============================================================================= + +/// Canonical Order struct - UNIFIED DEFINITION based on Agent 1's comprehensive analysis +/// +/// This represents the single source of truth for Order across all services +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "database", derive(sqlx::FromRow))] +pub struct Order { + // Core Identity + /// Unique order identifier + pub id: OrderId, + /// Client-provided order identifier + pub client_order_id: Option, + /// Broker-assigned order identifier + pub broker_order_id: Option, + /// Account identifier for the order + pub account_id: Option, + + // Trading Details + /// Trading symbol for the order + pub symbol: Symbol, + /// Order side (buy or sell) + pub side: OrderSide, + /// Type of order (market, limit, etc.) + pub order_type: OrderType, + /// Current status of the order + pub status: OrderStatus, + /// Time in force policy + pub time_in_force: TimeInForce, + + // Quantities & Pricing + /// Total order quantity + pub quantity: Quantity, + /// Limit price for the order + pub price: Option, + /// Stop price for stop orders + pub stop_price: Option, + /// Quantity that has been filled + pub filled_quantity: Quantity, + /// Remaining quantity to be filled + pub remaining_quantity: Quantity, + /// Average execution price + pub average_price: Option, + /// Alias for `average_price` for database compatibility + pub avg_fill_price: Option, + /// Alias for `average_price` for API compatibility + pub average_fill_price: Option, + /// Exchange-assigned order identifier + pub exchange_order_id: Option, + + // Strategy Fields (from Agent 1) + /// Parent order ID for iceberg/algo orders + pub parent_id: Option, + /// Execution algorithm name + pub execution_algorithm: Option, + /// Execution algorithm parameters stored as JSON + pub execution_params: Value, + + // Risk Management (from Agent 1) + /// Stop loss price for risk management + pub stop_loss: Option, + /// Take profit price for profit taking + pub take_profit: Option, + + // Timestamps + /// Order creation timestamp + pub created_at: HftTimestamp, + /// Last update timestamp + pub updated_at: Option, + /// Order expiration timestamp + pub expires_at: Option, + + // Extensibility + /// Additional order metadata stored as JSON + pub metadata: Value, +} + +impl Order { + /// Create a new order with canonical fields + pub fn new( + symbol: Symbol, + side: OrderSide, + quantity: Quantity, + price: Option, + order_type: OrderType, + ) -> Self { + let now = HftTimestamp::now_or_zero(); + Self { + // Core Identity + id: OrderId::new(), + client_order_id: None, + broker_order_id: None, + account_id: None, + + // Trading Details + symbol, + side, + order_type, + status: OrderStatus::Created, + time_in_force: TimeInForce::default(), + + // Quantities & Pricing + quantity, + price, + stop_price: None, + filled_quantity: Quantity::ZERO, + remaining_quantity: quantity, + average_price: None, + avg_fill_price: None, // Database compatibility alias + average_fill_price: None, // API compatibility alias + exchange_order_id: None, + + // Strategy Fields + parent_id: None, + execution_algorithm: None, + execution_params: serde_json::json!({}), + + // Risk Management + stop_loss: None, + take_profit: None, + + // Timestamps + created_at: now, + updated_at: None, + expires_at: None, + + // Extensibility + metadata: serde_json::json!({}), + } + } + + /// Check if the order is fully filled + pub fn is_filled(&self) -> bool { + self.filled_quantity == self.quantity + } + + /// Check if the order is partially filled + pub fn is_partially_filled(&self) -> bool { + self.filled_quantity > Quantity::ZERO && self.filled_quantity < self.quantity + } + + /// Calculate fill percentage + #[allow(clippy::float_arithmetic)] + pub fn fill_percentage(&self) -> f64 { + if self.quantity.is_zero() { + 0.0 + } else { + (self.filled_quantity.to_f64() / self.quantity.to_f64()) * 100.0 + } + } + + /// Set client order ID for tracking + pub fn with_client_order_id(mut self, client_order_id: String) -> Self { + self.client_order_id = Some(client_order_id); + self + } + + /// Set account ID + pub fn with_account_id(mut self, account_id: String) -> Self { + self.account_id = Some(account_id); + self + } + + /// Set time in force + pub const fn with_time_in_force(mut self, time_in_force: TimeInForce) -> Self { + self.time_in_force = time_in_force; + self + } + + /// Set stop price + pub const fn with_stop_price(mut self, stop_price: Price) -> Self { + self.stop_price = Some(stop_price); + self + } + + /// Set execution algorithm + pub fn with_execution_algorithm(mut self, algorithm: String) -> Self { + self.execution_algorithm = Some(algorithm); + self + } + + /// Add execution parameter + pub fn with_execution_param(mut self, key: String, value: f64) -> Self { + if let Some(obj) = self.execution_params.as_object_mut() { + obj.insert(key, serde_json::to_value(value).unwrap_or(Value::Null)); + } else { + let mut map = serde_json::Map::new(); + map.insert(key, serde_json::to_value(value).unwrap_or(Value::Null)); + self.execution_params = Value::Object(map); + } + self + } + + /// Set stop loss + pub const fn with_stop_loss(mut self, stop_loss: Price) -> Self { + self.stop_loss = Some(stop_loss); + self + } + + /// Set take profit + pub const fn with_take_profit(mut self, take_profit: Price) -> Self { + self.take_profit = Some(take_profit); + self + } + + /// Add metadata + pub fn with_metadata(mut self, key: String, value: String) -> Self { + if let Some(obj) = self.metadata.as_object_mut() { + obj.insert(key, Value::String(value)); + } else { + let mut map = serde_json::Map::new(); + map.insert(key, Value::String(value)); + self.metadata = Value::Object(map); + } + self + } + + /// Update order status and timestamp + pub fn update_status(&mut self, status: OrderStatus) { + self.status = status; + self.updated_at = Some(HftTimestamp::now_or_zero()); + } + + /// Fill order with given quantity and price + /// + /// # Errors + /// Returns error if the operation fails + #[allow(clippy::float_arithmetic)] + pub fn fill( + &mut self, + fill_quantity: Quantity, + fill_price: Price, + ) -> Result<(), CommonTypeError> { + // Use Add trait instead of checked_add (Quantity doesn't have checked_add method) + let new_filled_value = self.filled_quantity.to_f64() + fill_quantity.to_f64(); + if !new_filled_value.is_finite() || new_filled_value < 0.0 { + return Err(CommonTypeError::ValidationError { + field: "fill_quantity".to_owned(), + reason: "Fill quantity overflow".to_owned(), + }); + } + let new_filled = + Quantity::from_f64(new_filled_value).map_err(|e| CommonTypeError::ValidationError { + field: "fill_quantity".to_owned(), + reason: format!("Fill quantity overflow: {}", e), + })?; + if new_filled > self.quantity { + return Err(CommonTypeError::ValidationError { + field: "fill_quantity".to_owned(), + reason: "Fill quantity exceeds remaining quantity".to_owned(), + }); + } + + // Update filled quantity + let previous_filled = self.filled_quantity; + let new_filled_value = self + .filled_quantity + .value + .checked_add(fill_quantity.value) + .ok_or_else(|| CommonTypeError::ValidationError { + field: "filled_quantity".to_owned(), + reason: "Filled quantity overflow".to_owned(), + })?; + self.filled_quantity = Quantity { + value: new_filled_value, + }; + + let new_remaining_value = self + .quantity + .value + .checked_sub(self.filled_quantity.value) + .ok_or_else(|| CommonTypeError::ValidationError { + field: "remaining_quantity".to_owned(), + reason: "Remaining quantity underflow".to_owned(), + })?; + self.remaining_quantity = Quantity { + value: new_remaining_value, + }; + + // Update average price + if let Some(avg_price) = self.average_price { + let total_value = avg_price.to_f64() * previous_filled.to_f64() + + fill_price.to_f64() * fill_quantity.to_f64(); + let new_avg = Some( + Price::from_f64(total_value / self.filled_quantity.to_f64()).unwrap_or(fill_price), + ); + self.average_price = new_avg; + self.avg_fill_price = new_avg; // Keep in sync + } else { + self.average_price = Some(fill_price); + self.avg_fill_price = Some(fill_price); // Keep in sync + } + + // Update status + if self.is_filled() { + self.update_status(OrderStatus::Filled); + } else { + self.update_status(OrderStatus::PartiallyFilled); + } + + Ok(()) + } + + /// Create a limit order - convenience constructor + pub fn limit(symbol: Symbol, side: OrderSide, quantity: Quantity, price: Price) -> Self { + Self::new(symbol, side, quantity, Some(price), OrderType::Limit) + } + + /// Create a market order - convenience constructor + pub fn market(symbol: Symbol, side: OrderSide, quantity: Quantity) -> Self { + Self::new(symbol, side, quantity, None, OrderType::Market) + } + + /// Get symbol hash for performance-critical operations + pub fn symbol_hash(&self) -> i64 { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + let mut hasher = DefaultHasher::new(); + self.symbol.as_str().hash(&mut hasher); + i64::try_from(hasher.finish()).unwrap_or(0) + } + + /// Get order timestamp + pub const fn timestamp(&self) -> HftTimestamp { + self.created_at + } +} + +impl Default for Order { + fn default() -> Self { + Self { + id: OrderId::new(), + client_order_id: None, + broker_order_id: None, + account_id: None, + + symbol: Symbol::from("DEFAULT"), + side: OrderSide::Buy, + order_type: OrderType::Market, + status: OrderStatus::Created, + time_in_force: TimeInForce::Day, + + quantity: Quantity::ONE, + price: None, + stop_price: None, + filled_quantity: Quantity::ZERO, + remaining_quantity: Quantity::ONE, + average_price: None, + avg_fill_price: None, + average_fill_price: None, + exchange_order_id: None, + + parent_id: None, + execution_algorithm: None, + execution_params: serde_json::json!({}), + + stop_loss: None, + take_profit: None, + + created_at: HftTimestamp::now().unwrap_or(HftTimestamp { nanos: 0 }), + updated_at: None, + expires_at: None, + + metadata: serde_json::json!({}), + } + } +} + +/// Represents a trading position - CANONICAL DEFINITION +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "database", derive(sqlx::FromRow))] +pub struct Position { + /// Unique position identifier + pub id: Uuid, + + /// Trading symbol + pub symbol: String, + + /// Position quantity (positive for long, negative for short) + pub quantity: Decimal, + + /// Average entry price + pub avg_price: Decimal, + + /// Average cost per share + pub avg_cost: Decimal, + + /// Cost basis for tax calculations + pub basis: Decimal, + + /// Average entry price + pub average_price: Decimal, + + /// Market value of position + pub market_value: Decimal, + + /// Unrealized P&L + pub unrealized_pnl: Decimal, + + /// Realized P&L + pub realized_pnl: Decimal, + + /// Position creation timestamp + pub created_at: DateTime, + + /// Last update timestamp + pub updated_at: DateTime, + + /// Last updated timestamp + pub last_updated: DateTime, + + /// Current market price (for P&L calculation) + pub current_price: Option, + + /// Position size in base currency + pub notional_value: Decimal, + + /// Margin requirement + pub margin_requirement: Decimal, +} + +impl Position { + /// Create a new position + pub fn new(symbol: String, quantity: Decimal, avg_price: Decimal) -> Self { + let now = Utc::now(); + let notional_value = quantity + .abs() + .checked_mul(avg_price) + .unwrap_or(Decimal::ZERO); + + Self { + id: Uuid::new_v4(), + symbol, + quantity, + avg_price, + avg_cost: avg_price, // Keep avg_cost synchronized with avg_price + basis: quantity.checked_mul(avg_price).unwrap_or(Decimal::ZERO), // Cost basis calculation + average_price: avg_price, // Same as avg_price for compatibility + market_value: notional_value, // Initialize market value to notional value + unrealized_pnl: Decimal::ZERO, + realized_pnl: Decimal::ZERO, + created_at: now, + updated_at: now, + last_updated: now, // Same as updated_at for compatibility + current_price: None, + notional_value, + margin_requirement: notional_value + .checked_mul(Decimal::from_str_exact("0.02").unwrap_or(Decimal::ZERO)) + .unwrap_or(Decimal::ZERO), // 2% margin + } + } + + /// Check if position is long + pub fn is_long(&self) -> bool { + self.quantity > Decimal::ZERO + } + + /// Check if position is short + pub fn is_short(&self) -> bool { + self.quantity < Decimal::ZERO + } + + /// Calculate unrealized P&L based on current price + pub fn calculate_unrealized_pnl(&mut self, current_price: Decimal) { + self.current_price = Some(current_price); + self.market_value = self + .quantity + .abs() + .checked_mul(current_price) + .unwrap_or(Decimal::ZERO); + // For both long and short: quantity * (current_price - avg_price) + let price_diff = current_price + .checked_sub(self.avg_price) + .unwrap_or(Decimal::ZERO); + self.unrealized_pnl = self + .quantity + .checked_mul(price_diff) + .unwrap_or(Decimal::ZERO); + let now = Utc::now(); + self.updated_at = now; + self.last_updated = now; // Keep alias synchronized + } + + /// Get total P&L (realized + unrealized) + pub fn total_pnl(&self) -> Decimal { + self.realized_pnl + .checked_add(self.unrealized_pnl) + .unwrap_or(Decimal::ZERO) + } + + /// Calculate return on investment percentage + pub fn roi_percentage(&self) -> Decimal { + if self.notional_value.is_zero() { + Decimal::ZERO + } else { + let pnl_ratio = self + .total_pnl() + .checked_div(self.notional_value) + .unwrap_or(Decimal::ZERO); + pnl_ratio + .checked_mul(Decimal::from(100)) + .unwrap_or(Decimal::ZERO) + } + } +} + +/// Represents a trade execution - CANONICAL DEFINITION +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "database", derive(sqlx::FromRow))] +pub struct Execution { + /// Unique execution identifier + pub id: Uuid, + + /// Related order ID + pub order_id: Uuid, + + /// Trading symbol + pub symbol: String, + + /// Executed quantity + pub quantity: Decimal, + + /// Execution price + pub price: Decimal, + + /// Execution side + pub side: OrderSide, + + /// Trading fees + pub fees: Decimal, + + /// Fee currency + pub fee_currency: String, + + /// Execution timestamp + pub executed_at: DateTime, + + /// Execution timestamp + pub timestamp: DateTime, + + /// Symbol hash for performance + pub symbol_hash: i64, + + /// Broker execution ID + pub broker_execution_id: Option, + + /// Counterparty information + pub counterparty: Option, + + /// Trade venue + pub venue: Option, + + /// Gross trade value + pub gross_value: Decimal, + + /// Net trade value (after fees) + pub net_value: Decimal, +} + +impl Execution { + /// Create a new execution + pub fn new( + order_id: Uuid, + symbol: String, + quantity: Decimal, + price: Decimal, + side: OrderSide, + fees: Decimal, + ) -> Self { + let gross_value = quantity.checked_mul(price).unwrap_or(Decimal::ZERO); + let net_value = if side == OrderSide::Buy { + gross_value.checked_add(fees).unwrap_or(gross_value) + } else { + gross_value.checked_sub(fees).unwrap_or(gross_value) + }; + let now = Utc::now(); + let symbol_hash = Self::hash_symbol(&symbol); + + Self { + id: Uuid::new_v4(), + order_id, + symbol, + quantity, + price, + side, + fees, + fee_currency: "USD".to_owned(), // Default to USD + executed_at: now, + timestamp: now, // Same as executed_at for compatibility + symbol_hash, + broker_execution_id: None, + counterparty: None, + venue: None, + gross_value, + net_value, + } + } + + /// Calculate effective price including fees + pub fn effective_price(&self) -> Decimal { + if self.quantity.is_zero() { + self.price + } else { + self.net_value + .checked_div(self.quantity) + .unwrap_or(self.price) + } + } + + /// Hash symbol for performance + fn hash_symbol(symbol: &str) -> i64 { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + let mut hasher = DefaultHasher::new(); + symbol.hash(&mut hasher); + i64::try_from(hasher.finish()).unwrap_or(0) + } +} + +/// Core Price type using fixed-point arithmetic for precision +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct Price { + value: u64, +} + +impl Price { + /// Zero price constant + pub const ZERO: Self = Self { value: 0 }; + /// One unit price constant (1.0) + pub const ONE: Self = Self { value: 100_000_000 }; + /// One cent constant (0.01) + pub const CENT: Self = Self { value: 1_000_000 }; + /// Maximum price value + pub const MAX: Self = Self { value: u64::MAX }; + + /// Create a Price from a floating-point value + /// + /// # Errors + /// Returns error if the operation fails + #[allow(clippy::as_conversions)] + #[allow(clippy::float_arithmetic)] + pub fn from_f64(value: f64) -> Result { + if value < 0.0_f64 || !value.is_finite() { + return Err(CommonTypeError::InvalidPrice { + value: value.to_string(), + reason: "Price validation failed".to_owned(), + }); + } + Ok(Self { + #[allow(clippy::as_conversions)] + #[allow(clippy::float_arithmetic)] + value: (value * 100_000_000.0).round() as u64, + }) + } + + /// Convert to floating-point representation + #[must_use] + #[allow(clippy::float_arithmetic)] + /// Convert the quantity to a floating point value + #[allow(clippy::as_conversions)] + pub fn to_f64(&self) -> f64 { + #[allow(clippy::as_conversions)] + { + self.value as f64 / 100_000_000.0 + } + } + + /// Get floating-point representation (alias for `to_f64`) + /// + /// Convert quantity to f64 representation + /// + /// Convert quantity to f64 representation + /// + /// Convert quantity to f64 representation + #[must_use] + pub fn as_f64(&self) -> f64 { + self.to_f64() + } + + /// Create a zero price + /// + /// Create a zero quantity + /// + /// Create zero quantity + #[must_use] + pub const fn zero() -> Self { + Self::ZERO + } + + /// Convert to Decimal type for precise calculations + /// + /// # Errors + /// Returns error if the operation fails + pub fn to_decimal(&self) -> Result { + ::from_f64(self.to_f64()).ok_or_else(|| { + CommonTypeError::InvalidPrice { + value: "0.0".to_owned(), + reason: "Price to Decimal conversion failed".to_owned(), + } + }) + } + + /// Create a Price from a Decimal value + #[must_use] + pub fn from_decimal(decimal: Decimal) -> Self { + Self::from(decimal) + } + + /// Create a new Price (alias for `from_f64`) + /// + /// Create a new quantity from a floating point value + /// + /// Create new quantity from f64 value + /// + /// # Errors + /// Returns error if the operation fails + pub fn new(value: f64) -> Result { + Self::from_f64(value) + } + + /// Get the raw internal value representation + /// + /// Get the raw internal value + /// + /// Get the raw internal value representation + #[must_use] + pub const fn raw_value(&self) -> u64 { + self.value + } + + /// Get the price as a u64 value (same as `raw_value`) + /// + /// Convert to u64 representation + /// + /// Convert quantity to u64 representation + #[must_use] + pub const fn as_u64(&self) -> u64 { + self.value + } + + /// Create a Price from a raw u64 value + /// + /// Create a quantity from raw internal value + /// + /// Create quantity from raw u64 value + #[must_use] + pub const fn from_raw(value: u64) -> Self { + Self { value } + } + + /// Convert price to cents (divides by 1M for 8 decimal places) + #[must_use] + #[allow(clippy::integer_division)] + pub const fn to_cents(&self) -> u64 { + self.value / 1_000_000 + } + + /// Create a Price from cents value + #[must_use] + pub const fn from_cents(cents: u64) -> Self { + Self { + value: cents.saturating_mul(1_000_000), + } + } + + /// Check if the price is zero + /// + /// Check if the quantity is zero + /// + /// Check if quantity is zero + #[must_use] + pub const fn is_zero(&self) -> bool { + self.value == 0 + } + + /// Check if the price is non-zero (has some value) + /// + /// Check if the quantity is non-zero (has some value) + /// + /// Check if quantity has a non-zero value + #[must_use] + pub const fn is_some(&self) -> bool { + !self.is_zero() + } + + /// Check if the price is zero (has no value) + /// + /// Check if the quantity is zero (has no value) + /// + /// Check if quantity is zero (none) + #[must_use] + pub const fn is_none(&self) -> bool { + self.is_zero() + } + + /// Get a reference to this price + /// + /// Get a reference to self + /// + /// Get a reference to self + #[must_use] + pub const fn as_ref(&self) -> &Self { + self + } + + /// Get the absolute value of the price (prices are always positive) + /// + /// Get the absolute value (quantities are always positive) + /// + /// Get absolute value (always positive for Quantity) + #[must_use] + pub const fn abs(&self) -> Self { + *self + } + + /// Multiply this price by another price + /// + /// # Errors + /// Returns error if the operation fails + #[allow(clippy::arithmetic_side_effects)] + pub fn multiply(&self, other: Self) -> Result { + #[allow(clippy::arithmetic_side_effects)] + let result = *self * other; + result + } + + /// Subtract another price from this price + /// + /// Subtract another quantity from this quantity + /// + /// Subtract another quantity from this quantity + /// + /// Subtract another quantity from this quantity + /// + /// Subtract another quantity from this quantity + #[must_use] + #[allow(clippy::arithmetic_side_effects)] + pub fn subtract(&self, other: Self) -> Self { + *self - other + } + + /// Divide this price by a floating point divisor + /// + /// # Errors + /// Returns error if the operation fails + pub fn divide(&self, divisor: f64) -> Result { + if divisor == 0.0 { + return Err(CommonTypeError::InvalidPrice { + value: format!("{:?}", self), + reason: "Division by zero".to_owned(), + }); + } + #[allow(clippy::cast_precision_loss)] + #[allow(clippy::arithmetic_side_effects)] + let result = (self.value as f64 / divisor) as u64; + Ok(Self { value: result }) + } +} + +impl fmt::Display for Price { + /// Format the price for display with 8 decimal places + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:.8}", self.to_f64()) + } +} + +impl Default for Price { + /// Returns the default price (zero) + fn default() -> Self { + Self::ZERO + } +} + +impl FromStr for Price { + type Err = CommonTypeError; + + fn from_str(s: &str) -> Result { + let parsed_value = s + .parse::() + .map_err(|e| CommonTypeError::InvalidPrice { + value: s.to_owned(), + reason: format!("Cannot parse '{}' as price: {}", s, e), + })?; + Self::from_f64(parsed_value) + } +} + +impl Add for Price { + type Output = Self; + fn add(self, rhs: Self) -> Self::Output { + Self { + value: self.value.saturating_add(rhs.value), + } + } +} + +impl Sub for Price { + type Output = Self; + fn sub(self, rhs: Self) -> Self::Output { + Self { + value: self.value.saturating_sub(rhs.value), + } + } +} + +impl Mul for Price { + #[allow(clippy::float_arithmetic)] + type Output = Result; + fn mul(self, rhs: f64) -> Self::Output { + Self::from_f64(self.to_f64() * rhs) + } +} + +#[allow(clippy::float_arithmetic)] +impl Div for Price { + type Output = Result; + fn div(self, rhs: f64) -> Self::Output { + if rhs == 0.0_f64 { + return Err(CommonTypeError::ConversionError { + message: "Cannot divide price by zero".to_owned(), + }); + } + Self::from_f64(self.to_f64() / rhs) + } +} + +impl From for Price { + fn from(decimal: Decimal) -> Self { + let f64_val: f64 = TryInto::::try_into(decimal).unwrap_or_else(|_| { + tracing::warn!("Failed to convert Decimal to f64, using 0.0 as fallback"); + 0.0_f64 + }); + Self::from_f64(f64_val).unwrap_or_else(|_| { + tracing::warn!( + "Failed to create Price from f64 value {}, using ZERO", + f64_val + ); + Self::ZERO + }) + } +} + +impl From for Decimal { + fn from(price: Price) -> Self { + price.to_decimal().unwrap_or(Decimal::ZERO) + } +} + +// TryFrom for Decimal removed due to conflicting blanket implementation +// Use qty.to_decimal() directly instead +impl From for Decimal { + fn from(qty: Quantity) -> Self { + qty.to_decimal().unwrap_or(Decimal::ZERO) + } +} + +// TryFrom for Decimal removed due to conflict with From implementation +// Use the From implementation instead which handles errors by returning ZERO + +// ============================================================================= +// Decimal Extension Trait +// ============================================================================= + +impl Mul for Price { + #[allow(clippy::float_arithmetic)] + #[allow(clippy::arithmetic_side_effects)] + type Output = Result; + fn mul(self, rhs: Self) -> Self::Output { + Self::from_f64(self.to_f64() * rhs.to_f64()) + } +} + +impl TryFrom for Price { + type Error = CommonTypeError; + fn try_from(s: String) -> Result { + Self::from_str(&s) + } +} + +impl TryFrom<&str> for Price { + type Error = CommonTypeError; + fn try_from(s: &str) -> Result { + Self::from_str(s) + } +} + +impl PartialEq for Price { + #[allow(clippy::float_arithmetic)] + fn eq(&self, other: &f64) -> bool { + (self.to_f64() - other).abs() < f64::EPSILON + } +} + +impl PartialEq for f64 { + #[allow(clippy::float_arithmetic)] + fn eq(&self, other: &Price) -> bool { + (self - other.to_f64()).abs() < f64::EPSILON + } +} + +impl AddAssign for Price { + fn add_assign(&mut self, rhs: Self) { + self.value = self.value.saturating_add(rhs.value); + } +} + +impl SubAssign for Price { + fn sub_assign(&mut self, rhs: Self) { + self.value = self.value.saturating_sub(rhs.value); + } +} + +impl MulAssign for Price { + fn mul_assign(&mut self, rhs: f64) { + if let Ok(result) = self.mul(rhs) { + *self = result; + } + // If multiplication fails, self remains unchanged + } +} + +impl DivAssign for Price { + fn div_assign(&mut self, rhs: f64) { + if let Ok(result) = self.div(rhs) { + *self = result; + } + // If division fails, self remains unchanged + } +} + +impl PartialOrd for Price { + fn partial_cmp(&self, other: &f64) -> Option { + self.to_f64().partial_cmp(other) + } +} + +impl PartialOrd for f64 { + fn partial_cmp(&self, other: &Price) -> Option { + self.partial_cmp(&other.to_f64()) + } +} + +/// Core Quantity type using fixed-point arithmetic +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct Quantity { + pub(crate) value: u64, +} + +impl Quantity { + /// Zero quantity constant + pub const ZERO: Self = Self { value: 0 }; + /// One unit quantity constant + pub const ONE: Self = Self { value: 100_000_000 }; + /// Maximum possible quantity + pub const MAX: Self = Self { value: u64::MAX }; + + /// Create a Quantity from a floating point value + #[allow(clippy::as_conversions)] + /// + /// # Errors + /// Returns error if the operation fails + #[allow(clippy::float_arithmetic)] + pub fn from_f64(value: f64) -> Result { + if value < 0.0_f64 || !value.is_finite() { + return Err(CommonTypeError::InvalidQuantity { + value: value.to_string(), + reason: "Quantity validation failed".to_owned(), + }); + } + Ok(Self { + value: (value * 100_000_000.0).round() as u64, + }) + } + + /// Convert quantity to floating point representation + #[must_use] + #[allow(clippy::as_conversions)] + #[allow(clippy::float_arithmetic)] + pub fn to_f64(&self) -> f64 { + self.value as f64 / 100_000_000.0 + } + + /// Convert the quantity to a Decimal value + /// + /// # Errors + /// Returns error if the operation fails + pub fn to_decimal(&self) -> Result { + Decimal::from_f64_retain(self.to_f64()).ok_or_else(|| CommonTypeError::InvalidQuantity { + value: "0.0".to_owned(), + reason: "Quantity to Decimal conversion failed".to_owned(), + }) + } + + /// Get the internal value representation + #[must_use] + pub const fn value(&self) -> u64 { + self.value + } + + /// Get the raw internal value representation + #[must_use] + pub const fn raw_value(&self) -> u64 { + self.value + } + + /// Convert quantity to u64 representation + #[must_use] + pub const fn as_u64(&self) -> u64 { + self.value + } + + /// Create quantity from raw u64 value + #[must_use] + pub const fn from_raw(value: u64) -> Self { + Self { value } + } + + /// Create new quantity from f64 value + /// + /// # Errors + /// Returns error if the operation fails + #[allow(clippy::as_conversions)] + pub fn new(value: f64) -> Result { + Self::from_f64(value) + } + + /// Create zero quantity + #[must_use] + pub const fn zero() -> Self { + #[allow(clippy::as_conversions)] + Self::ZERO + } + + /// Create a quantity from an i64 value + /// + /// # Errors + /// Returns error if the operation fails + #[allow(clippy::as_conversions)] + pub fn from_i64(value: i64) -> Result { + Self::from_f64(value as f64) + } + + /// Create a quantity from a u64 value + /// + /// # Errors + #[allow(clippy::as_conversions)] + /// Returns error if the operation fails + pub fn from_u64(value: u64) -> Result { + Self::from_f64(value as f64) + } + + /// Create a quantity from a Decimal value + /// + /// # Errors + /// Returns error if the operation fails + pub fn from_decimal(decimal: Decimal) -> Result { + use std::convert::TryFrom; + Self::try_from(decimal).map_err(|e| CommonTypeError::InvalidQuantity { + value: decimal.to_string(), + reason: format!("Failed to convert Decimal to Quantity: {}", e), + }) + } + + /// Check if quantity is zero + #[must_use] + pub const fn is_zero(&self) -> bool { + self.value == 0 + } + + /// Check if quantity has a non-zero value + #[must_use] + pub const fn is_some(&self) -> bool { + !self.is_zero() + } + + /// Check if quantity is zero (none) + #[must_use] + pub const fn is_none(&self) -> bool { + self.is_zero() + } + + /// Get a reference to self + #[must_use] + pub const fn as_ref(&self) -> &Self { + self + } + + /// Get absolute value (always positive for Quantity) + #[must_use] + pub const fn abs(&self) -> Self { + *self + } + + /// Get the sign of the quantity (1.0 for positive, 0.0 for zero) + #[must_use] + pub const fn signum(&self) -> f64 { + if self.value > 0 { + 1.0 + } else { + 0.0 + } + } + + /// Check if quantity is positive + #[must_use] + pub const fn is_positive(&self) -> bool { + self.value > 0 + } + + /// Check if quantity is negative (always false for Quantity) + #[must_use] + pub const fn is_negative(&self) -> bool { + false + } + + /// Convert quantity to f64 representation + #[must_use] + pub fn as_f64(&self) -> f64 { + self.to_f64() + } + + /// Create quantity from number of shares + #[must_use] + pub const fn from_shares(shares: u64) -> Self { + Self { + value: shares.saturating_mul(100_000_000), + } + } + + /// Convert quantity to number of shares + #[must_use] + #[allow(clippy::integer_division)] + pub const fn to_shares(&self) -> u64 { + self.value / 100_000_000 + } + + /// Multiply this quantity by another quantity + /// + /// # Errors + /// Returns error if the operation fails + #[allow(clippy::float_arithmetic)] + #[allow(clippy::arithmetic_side_effects)] + pub fn multiply(&self, other: Self) -> Result { + Self::from_f64(self.to_f64() * other.to_f64()) + } + + /// Subtract another quantity from this quantity + #[must_use] + pub fn subtract(&self, other: Self) -> Self { + Self { + value: self.value.checked_sub(other.value).unwrap_or_else(|| { + tracing::warn!( + "Quantity subtraction underflow: {} - {}, returning 0", + self.value, + other.value + ); + 0 + }), + } + } + + /// Checked addition with overflow protection + #[must_use] + pub const fn checked_add(&self, other: Self) -> Option { + match self.value.checked_add(other.value) { + Some(value) => Some(Self { value }), + None => None, + } + } + + /// Checked subtraction with underflow protection + #[must_use] + pub const fn checked_sub(&self, other: Self) -> Option { + match self.value.checked_sub(other.value) { + Some(value) => Some(Self { value }), + None => None, + } + } +} + +impl Default for Quantity { + fn default() -> Self { + Self::ZERO + } +} + +impl FromStr for Quantity { + type Err = CommonTypeError; + + fn from_str(s: &str) -> Result { + let parsed_value = s + .parse::() + .map_err(|e| CommonTypeError::InvalidQuantity { + value: s.to_owned(), + reason: format!("Cannot parse '{}' as quantity: {}", s, e), + })?; + Self::from_f64(parsed_value) + } +} + +impl fmt::Display for Quantity { + /// Format the quantity for display with 8 decimal places + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:.8}", self.to_f64()) + } +} + +impl TryFrom for Quantity { + type Error = CommonTypeError; + fn try_from(value: i32) -> Result { + Self::new(f64::from(value)) + } +} + +impl TryFrom for Quantity { + type Error = CommonTypeError; + fn try_from(value: u64) -> Result { + #[allow(clippy::as_conversions)] + let f64_value = value as f64; + Self::new(f64_value) + } +} + +impl TryFrom for Quantity { + type Error = CommonTypeError; + fn try_from(value: f64) -> Result { + Self::new(value) + } +} + +impl TryFrom for Quantity { + type Error = CommonTypeError; + fn try_from(decimal: Decimal) -> Result { + let f64_val: f64 = + TryInto::::try_into(decimal).map_err(|e| CommonTypeError::ConversionError { + message: format!("Failed to convert Decimal to f64: {}", e), + })?; + Self::from_f64(f64_val) + } +} + +impl TryFrom for Quantity { + type Error = CommonTypeError; + fn try_from(s: String) -> Result { + Self::from_str(&s) + } +} + +impl TryFrom<&str> for Quantity { + type Error = CommonTypeError; + fn try_from(s: &str) -> Result { + Self::from_str(s) + } +} + +impl PartialEq for Quantity { + #[allow(clippy::float_arithmetic)] + fn eq(&self, other: &f64) -> bool { + (self.to_f64() - other).abs() < f64::EPSILON + } +} + +impl PartialEq for f64 { + #[allow(clippy::float_arithmetic)] + fn eq(&self, other: &Quantity) -> bool { + (self - other.to_f64()).abs() < f64::EPSILON + } +} + +impl PartialOrd for Quantity { + fn partial_cmp(&self, other: &f64) -> Option { + self.to_f64().partial_cmp(other) + } +} + +impl PartialOrd for f64 { + fn partial_cmp(&self, other: &Quantity) -> Option { + self.partial_cmp(&other.to_f64()) + } +} + +impl Add for Quantity { + type Output = Self; + fn add(self, rhs: Self) -> Self::Output { + Self { + value: self.value.saturating_add(rhs.value), + } + } +} + +impl Sub for Quantity { + type Output = Self; + fn sub(self, rhs: Self) -> Self::Output { + Self { + value: self.value.saturating_sub(rhs.value), + } + } +} + +impl Mul for Quantity { + #[allow(clippy::float_arithmetic)] + type Output = Result; + fn mul(self, rhs: f64) -> Self::Output { + Self::from_f64(self.to_f64() * rhs) + } +} + +impl Div for Quantity { + #[allow(clippy::float_arithmetic)] + type Output = Result; + fn div(self, rhs: f64) -> Self::Output { + if rhs == 0.0_f64 { + return Err(CommonTypeError::ConversionError { + message: "Cannot divide quantity by zero".to_owned(), + }); + } + Self::from_f64(self.to_f64() / rhs) + } +} + +impl Sum for Quantity { + fn sum>(iter: I) -> Self { + iter.fold(Self::ZERO, |acc, x| Self { + value: acc.value.saturating_add(x.value), + }) + } +} + +impl<'quantity> Sum<&'quantity Self> for Quantity { + fn sum>(iter: I) -> Self { + iter.fold(Self::ZERO, |acc, x| Self { + value: acc.value.saturating_add(x.value), + }) + } +} + +// ============================================================================= +// SQLX IMPLEMENTATIONS FOR FINANCIAL TYPES +// ============================================================================= + +#[cfg(feature = "database")] +mod sqlx_impls { + use super::{HftTimestamp, Price, Quantity}; + use crate::types::{MarketRegime, OrderId, OrderSide, OrderStatus, OrderType}; + use rust_decimal::Decimal as RustDecimal; + use sqlx::{ + decode::Decode, + encode::{Encode, IsNull}, + error::BoxDynError, + postgres::{PgArgumentBuffer, PgTypeInfo, PgValueRef, Postgres}, + Type, + }; + + // SQLx implementations for Price + impl Type for Price { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("NUMERIC") + } + } + + impl<'query> Encode<'query, Postgres> for Price { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + // Convert our fixed-point u64 to rust_decimal::Decimal with 8 decimal places + let decimal_value = RustDecimal::new(i64::try_from(self.raw_value()).unwrap_or(0), 8); + decimal_value.encode_by_ref(buf) + } + } + + impl<'row> Decode<'row, Postgres> for Price { + fn decode(value: PgValueRef<'row>) -> Result { + // Decode from NUMERIC to rust_decimal::Decimal + let decimal_value = >::decode(value)?; + + // Validate scale matches our fixed-point precision (8 decimal places) + if decimal_value.scale() != 8 { + return Err(format!( + "Invalid scale for Price: expected 8, got {}", + decimal_value.scale() + ) + .into()); + } + + // Extract mantissa and convert to our u64 representation + let mantissa = decimal_value.mantissa(); + let inner_val = u64::try_from(mantissa).map_err(|e| { + format!( + "Failed to convert negative or overflowing NUMERIC to Price: {}", + e + ) + })?; + + Ok(Price::from_raw(inner_val)) + } + } + // SQLx implementations for Quantity + impl Type for Quantity { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("NUMERIC") + } + } + + impl<'query> Encode<'query, Postgres> for Quantity { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + // Convert our fixed-point u64 to rust_decimal::Decimal with 8 decimal places + let decimal_value = RustDecimal::new(i64::try_from(self.raw_value()).unwrap_or(0), 8); + decimal_value.encode_by_ref(buf) + } + } + + impl<'row> Decode<'row, Postgres> for Quantity { + fn decode(value: PgValueRef<'row>) -> Result { + // Decode from NUMERIC to rust_decimal::Decimal + let decimal_value = >::decode(value)?; + + // Validate scale matches our fixed-point precision (8 decimal places) + if decimal_value.scale() != 8 { + return Err(format!( + "Invalid scale for Quantity: expected 8, got {}", + decimal_value.scale() + ) + .into()); + } + + // Extract mantissa and convert to our u64 representation + let mantissa = decimal_value.mantissa(); + let inner_val = u64::try_from(mantissa).map_err(|e| { + format!( + "Failed to convert negative or overflowing NUMERIC to Quantity: {}", + e + ) + })?; + + Ok(Quantity::from_raw(inner_val)) + } + } + + // SQLx implementations for TimeInForce + impl Type for crate::types::TimeInForce { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("TEXT") + } + } + + impl<'query> Encode<'query, Postgres> for crate::types::TimeInForce { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + // Use the Display trait to convert enum to string representation + <&str as Encode>::encode(self.to_string().as_str(), buf) + } + } + + impl<'row> Decode<'row, Postgres> for crate::types::TimeInForce { + fn decode(value: PgValueRef<'row>) -> Result { + // Decode from TEXT to string, then parse to enum + let s = <&str as Decode>::decode(value)?; + match s { + "DAY" => Ok(crate::types::TimeInForce::Day), + "GTC" => Ok(crate::types::TimeInForce::GoodTillCancel), + "IOC" => Ok(crate::types::TimeInForce::ImmediateOrCancel), + "FOK" => Ok(crate::types::TimeInForce::FillOrKill), + _ => Err(format!("Invalid TimeInForce value: {}", s).into()), + } + } + } + + // SQLx implementations for OrderStatus + impl Type for OrderStatus { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("TEXT") + } + } + + impl<'query> Encode<'query, Postgres> for OrderStatus { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + let value = match self { + OrderStatus::Created => "CREATED", + OrderStatus::Submitted => "SUBMITTED", + OrderStatus::PartiallyFilled => "PARTIALLY_FILLED", + OrderStatus::Filled => "FILLED", + OrderStatus::Rejected => "REJECTED", + OrderStatus::Cancelled => "CANCELLED", + OrderStatus::New => "NEW", + OrderStatus::Expired => "EXPIRED", + OrderStatus::Pending => "PENDING", + OrderStatus::Working => "WORKING", + OrderStatus::Unknown => "UNKNOWN", + OrderStatus::Suspended => "SUSPENDED", + OrderStatus::PendingCancel => "PENDING_CANCEL", + OrderStatus::PendingReplace => "PENDING_REPLACE", + }; + <&str as Encode>::encode_by_ref(&value, buf) + } + } + + impl<'row> Decode<'row, Postgres> for OrderStatus { + fn decode(value: PgValueRef<'row>) -> Result { + let s = >::decode(value)?; + match s.as_str() { + "CREATED" => Ok(OrderStatus::Created), + "SUBMITTED" => Ok(OrderStatus::Submitted), + "PARTIALLY_FILLED" => Ok(OrderStatus::PartiallyFilled), + "FILLED" => Ok(OrderStatus::Filled), + "REJECTED" => Ok(OrderStatus::Rejected), + "CANCELLED" => Ok(OrderStatus::Cancelled), + "NEW" => Ok(OrderStatus::New), + "EXPIRED" => Ok(OrderStatus::Expired), + "PENDING" => Ok(OrderStatus::Pending), + "WORKING" => Ok(OrderStatus::Working), + "UNKNOWN" => Ok(OrderStatus::Unknown), + "SUSPENDED" => Ok(OrderStatus::Suspended), + "PENDING_CANCEL" => Ok(OrderStatus::PendingCancel), + "PENDING_REPLACE" => Ok(OrderStatus::PendingReplace), + _ => Err(format!("Invalid OrderStatus value: {}", s).into()), + } + } + } + + // SQLx implementations for OrderSide + impl Type for OrderSide { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("TEXT") + } + } + + impl<'query> Encode<'query, Postgres> for OrderSide { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + let value = match self { + OrderSide::Buy => "BUY", + OrderSide::Sell => "SELL", + }; + <&str as Encode>::encode_by_ref(&value, buf) + } + } + + impl<'row> Decode<'row, Postgres> for OrderSide { + fn decode(value: PgValueRef<'row>) -> Result { + let s = >::decode(value)?; + match s.as_str() { + "BUY" => Ok(OrderSide::Buy), + "SELL" => Ok(OrderSide::Sell), + _ => Err(format!("Invalid OrderSide value: {}", s).into()), + } + } + } + + // SQLx implementations for OrderType + impl Type for OrderType { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("TEXT") + } + } + + impl<'query> Encode<'query, Postgres> for OrderType { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + let value = match self { + OrderType::Market => "MARKET", + OrderType::Limit => "LIMIT", + OrderType::Stop => "STOP", + OrderType::StopLimit => "STOP_LIMIT", + OrderType::Iceberg => "ICEBERG", + OrderType::TrailingStop => "TRAILING_STOP", + OrderType::Hidden => "HIDDEN", + }; + <&str as Encode>::encode_by_ref(&value, buf) + } + } + + impl<'row> Decode<'row, Postgres> for OrderType { + fn decode(value: PgValueRef<'row>) -> Result { + let s = >::decode(value)?; + match s.as_str() { + "MARKET" => Ok(OrderType::Market), + "LIMIT" => Ok(OrderType::Limit), + "STOP" => Ok(OrderType::Stop), + "STOP_LIMIT" => Ok(OrderType::StopLimit), + "ICEBERG" => Ok(OrderType::Iceberg), + "TRAILING_STOP" => Ok(OrderType::TrailingStop), + "HIDDEN" => Ok(OrderType::Hidden), + _ => Err(format!("Invalid OrderType value: {}", s).into()), + } + } + } + + // SQLx implementations for MarketRegime + impl Type for MarketRegime { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("TEXT") + } + } + + impl<'query> Encode<'query, Postgres> for MarketRegime { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + let value = match self { + MarketRegime::Normal => "NORMAL", + MarketRegime::Crisis => "CRISIS", + MarketRegime::Trending => "TRENDING", + MarketRegime::Sideways => "SIDEWAYS", + MarketRegime::Bull => "BULL", + MarketRegime::Bear => "BEAR", + MarketRegime::HighVolatility => "HIGH_VOLATILITY", + MarketRegime::LowVolatility => "LOW_VOLATILITY", + MarketRegime::Volatile => "VOLATILE", + MarketRegime::Calm => "CALM", + MarketRegime::Unknown => "UNKNOWN", + MarketRegime::Recovery => "RECOVERY", + MarketRegime::Bubble => "BUBBLE", + MarketRegime::Correction => "CORRECTION", + MarketRegime::Custom(id) => { + return >::encode_by_ref( + &format!("CUSTOM_{}", id), + buf, + ) + }, + }; + <&str as Encode>::encode_by_ref(&value, buf) + } + } + + impl<'row> Decode<'row, Postgres> for MarketRegime { + fn decode(value: PgValueRef<'row>) -> Result { + let s = >::decode(value)?; + match s.as_str() { + "NORMAL" => Ok(MarketRegime::Normal), + "CRISIS" => Ok(MarketRegime::Crisis), + "TRENDING" => Ok(MarketRegime::Trending), + "SIDEWAYS" => Ok(MarketRegime::Sideways), + "BULL" => Ok(MarketRegime::Bull), + "BEAR" => Ok(MarketRegime::Bear), + "HIGH_VOLATILITY" => Ok(MarketRegime::HighVolatility), + "LOW_VOLATILITY" => Ok(MarketRegime::LowVolatility), + "VOLATILE" => Ok(MarketRegime::Volatile), + "CALM" => Ok(MarketRegime::Calm), + "UNKNOWN" => Ok(MarketRegime::Unknown), + "RECOVERY" => Ok(MarketRegime::Recovery), + "BUBBLE" => Ok(MarketRegime::Bubble), + "CORRECTION" => Ok(MarketRegime::Correction), + _ => { + // Handle Custom(id) format + if let Some(id_str) = s.strip_prefix("CUSTOM_") { + if let Ok(id) = id_str.parse::() { + Ok(MarketRegime::Custom(id)) + } else { + Err(format!("Invalid MarketRegime Custom ID: {}", id_str).into()) + } + } else { + Err(format!("Invalid MarketRegime value: {}", s).into()) + } + }, + } + } + } + + // SQLx implementations for HftTimestamp + // Maps to PostgreSQL BIGINT (stores nanoseconds since Unix epoch) + // Note: Limited to i64::MAX nanoseconds (year 2262) due to PostgreSQL BIGINT constraints + impl<'query> Encode<'query, Postgres> for HftTimestamp { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + // Cast u64 to i64 for PostgreSQL BIGINT compatibility + >::encode(i64::try_from(self.nanos()).unwrap_or(0), buf) + } + } + + impl<'row> Decode<'row, Postgres> for HftTimestamp { + fn decode(value: PgValueRef<'row>) -> Result { + let val = >::decode(value)?; + // Cast i64 back to u64 for internal representation + Ok(HftTimestamp::from_nanos(u64::try_from(val).unwrap_or(0))) + } + } + + impl Type for HftTimestamp { + fn type_info() -> ::TypeInfo { + >::type_info() + } + + fn compatible(ty: &::TypeInfo) -> bool { + >::compatible(ty) + } + } + + // SQLx implementations for OrderId (uses BIGINT for u64) + impl Type for OrderId { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("BIGINT") + } + } + + impl<'query> Encode<'query, Postgres> for OrderId { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + >::encode_by_ref( + &(i64::try_from(self.value()).unwrap_or(0)), + buf, + ) + } + } + + impl<'row> Decode<'row, Postgres> for OrderId { + fn decode(value: PgValueRef<'row>) -> Result { + let id = >::decode(value)?; + Ok(OrderId::from_u64(u64::try_from(id).unwrap_or(0))) + } + } +} + +/// Volume type - alias for Quantity with the same fixed-point arithmetic +/// +/// `SQLx` traits are automatically inherited from Quantity +pub type Volume = Quantity; + +// ORDER TYPES ALREADY DEFINED ABOVE - No need to re-export from trading_engine +// ============================================================================= +// CORE ID TYPES (MOVED FROM TRADING_ENGINE) +// ============================================================================= + +/// Order identifier with ultra-fast atomic generation +/// +/// Replaces slow UUID generation (1ms+) with atomic increment (~5ns) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct OrderId(u64); + +impl Default for OrderId { + fn default() -> Self { + Self::new() + } +} + +impl OrderId { + /// Generate next `OrderId` using atomic counter - <50ns performance + pub fn new() -> Self { + use std::sync::atomic::{AtomicU64, Ordering}; + static COUNTER: AtomicU64 = AtomicU64::new(1); + Self(COUNTER.fetch_add(1, Ordering::Relaxed)) + } + + /// Create `OrderId` from u64 value + #[must_use] + pub const fn from_u64(value: u64) -> Self { + Self(value) + } + + /// Get u64 value + #[must_use] + pub const fn value(&self) -> u64 { + self.0 + } + + /// Get u64 value for performance-critical code (alias for value) + #[must_use] + pub const fn as_u64(&self) -> u64 { + self.0 + } + + /// Get as string for compatibility + #[must_use] + pub fn as_str(&self) -> String { + self.0.to_string() + } +} + +impl fmt::Display for OrderId { + /// Format the order ID for display + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for OrderId { + /// Create an `OrderId` from a u64 value + fn from(value: u64) -> Self { + Self(value) + } +} + +impl From for u64 { + /// Convert an `OrderId` to u64 + fn from(order_id: OrderId) -> Self { + order_id.0 + } +} + +impl FromStr for OrderId { + type Err = ParseIntError; + + fn from_str(s: &str) -> Result { + s.parse::().map(OrderId) + } +} + +impl From for OrderId { + /// Create an `OrderId` from a String, generating new ID if parsing fails + fn from(s: String) -> Self { + s.parse().unwrap_or_else(|_| Self::new()) + } +} + +impl From<&str> for OrderId { + /// Create an `OrderId` from a &str, generating new ID if parsing fails + fn from(s: &str) -> Self { + s.parse().unwrap_or_else(|_| Self::new()) + } +} + +/// Execution identifier with validation +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "database", derive(sqlx::Type))] +pub struct ExecutionId(String); + +impl ExecutionId { + /// Create a new execution ID with validation + /// + /// # Errors + /// Returns error if the operation fails + pub fn new>(id: S) -> Result { + let id = id.into(); + if id.trim().is_empty() { + return Err(CommonTypeError::ValidationError { + field: "execution_id".to_owned(), + reason: "Execution ID cannot be empty".to_owned(), + }); + } + Ok(Self(id)) + } + + /// Generate a new random execution ID + pub fn generate() -> Self { + Self(uuid::Uuid::new_v4().to_string()) + } + + /// Get execution ID as string slice + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Convert execution ID into owned string + pub fn into_string(self) -> String { + self.0 + } +} + +impl fmt::Display for ExecutionId { + /// Format the execution ID for display + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for ExecutionId { + type Err = CommonTypeError; + + fn from_str(s: &str) -> Result { + Self::new(s) + } +} + +/// Trade identifier with validation +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct TradeId(String); + +impl TradeId { + /// Create a new trade ID with validation + /// + /// # Errors + /// Returns error if the operation fails + pub fn new>(id: S) -> Result { + let id = id.into(); + if id.is_empty() { + return Err(CommonTypeError::ValidationError { + field: "trade_id".to_owned(), + reason: "Trade ID cannot be empty".to_owned(), + }); + } + Ok(Self(id)) + } + + /// Get the trade ID as a string slice + pub fn as_str(&self) -> &str { + &self.0 + } + /// Convert the trade ID into an owned string + pub fn into_string(self) -> String { + self.0 + } +} + +impl fmt::Display for TradeId { + /// Format the trade ID for display + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Trading symbol with validation +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "database", derive(sqlx::Type))] +pub struct Symbol { + value: String, +} + +impl Symbol { + /// Create a new symbol from a string + #[must_use] + pub const fn new(s: String) -> Self { + Self { value: s } + } + + /// Create a new Symbol with validation + /// + /// # Errors + /// Returns error if the operation fails + pub fn new_validated(s: String) -> Result { + if s.trim().is_empty() { + return Err(CommonTypeError::ValidationError { + field: "symbol".to_owned(), + reason: "Symbol cannot be empty".to_owned(), + }); + } + Ok(Self { value: s }) + } + + /// Create a Symbol from &str with validation + /// + /// # Errors + /// Returns error if the operation fails + pub fn from_str_validated(s: &str) -> Result { + Self::new_validated(s.to_owned()) + } + + /// Get the symbol as a string slice + #[must_use] + pub fn as_str(&self) -> &str { + &self.value + } + /// Get the symbol value as a string slice + #[must_use] + pub fn value(&self) -> &str { + &self.value + } + /// Get the symbol as bytes + #[must_use] + pub fn as_bytes(&self) -> &[u8] { + self.value.as_bytes() + } + /// Check if the symbol is empty + #[must_use] + pub fn is_empty(&self) -> bool { + self.value.is_empty() + } + /// Convert the symbol to uppercase + #[must_use] + pub fn to_uppercase(&self) -> String { + self.value.to_uppercase() + } + /// Replace occurrences in the symbol + #[must_use] + pub fn replace(&self, from: &str, to: &str) -> String { + self.value.replace(from, to) + } + + /// Helper for risk management - creates a 'NONE' symbol + #[must_use] + pub fn none() -> Self { + "NONE".parse().unwrap() + } + + /// Check if the symbol contains a pattern + #[must_use] + pub fn contains(&self, pattern: &str) -> bool { + self.value.contains(pattern) + } +} + +impl FromStr for Symbol { + type Err = std::convert::Infallible; + + fn from_str(s: &str) -> Result { + Ok(Self { + value: s.to_owned(), + }) + } +} + +// Additional implementation to support conversion from &Symbol to &str +impl AsRef for Symbol { + /// Convert symbol to string reference + fn as_ref(&self) -> &str { + &self.value + } +} + +impl fmt::Display for Symbol { + /// Format the symbol for display + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.value) + } +} + +impl From for Symbol { + /// Create a Symbol from a String + fn from(s: String) -> Self { + Self::new(s) + } +} +impl From<&str> for Symbol { + /// Create a Symbol from a &str + fn from(s: &str) -> Self { + Self::new(s.to_owned()) + } +} + +// TryFrom implementations removed due to conflicting blanket implementations +// Use Symbol::new_validated() or Symbol::from_validated() directly instead + +impl Default for Symbol { + /// Returns the default symbol (empty string) + fn default() -> Self { + Self::new(String::new()) + } +} + +impl PartialEq for Symbol { + fn eq(&self, other: &str) -> bool { + self.value == other + } +} + +impl PartialEq<&str> for Symbol { + fn eq(&self, other: &&str) -> bool { + self.value == *other + } +} + +impl PartialEq for Symbol { + fn eq(&self, other: &String) -> bool { + &self.value == other + } +} + +impl PartialEq for &str { + fn eq(&self, other: &Symbol) -> bool { + *self == other.value + } +} + +impl PartialEq for String { + fn eq(&self, other: &Symbol) -> bool { + self == &other.value + } +} + +// TimeInForce moved to canonical source: common::types::TimeInForce + +// Currency moved to canonical source: common::types::Currency + +// Price moved to canonical source: common::types::Price + +// Quantity moved to canonical source: common::types::Quantity +// Volume moved to canonical source: common::types::Quantity (as Volume alias) + +/// Money amount with currency +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Money { + /// The monetary amount + pub amount: Decimal, + /// The currency of the amount + pub currency: Currency, +} + +impl Money { + /// Create new money amount + pub const fn new(amount: Decimal, currency: Currency) -> Self { + Self { amount, currency } + } +} + +impl fmt::Display for Money { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.amount, self.currency) + } +} + +// OrderId moved to canonical source: common::types::OrderId + +// TradeId moved to canonical source: common::types::TradeId + +// Symbol moved to canonical source: common::types::Symbol + +/// Type-safe account identifier +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct AccountId(String); + +impl AccountId { + /// Create a new account ID with validation + /// + /// # Errors + /// Returns error if the operation fails + pub fn new>(id: S) -> Result { + let id = id.into(); + if id.trim().is_empty() { + return Err(CommonTypeError::InvalidIdentifier { + field: "account_id".to_owned(), + reason: "Account ID cannot be empty".to_owned(), + }); + } + Ok(Self(id)) + } + + /// Get the ID as a string slice + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Convert to owned String + pub fn into_string(self) -> String { + self.0 + } +} + +impl fmt::Display for AccountId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// High-precision timestamp for HFT applications - CANONICAL DEFINITION +/// +/// Robust implementation with error handling for financial safety +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default, +)] +pub struct HftTimestamp { + pub(crate) nanos: u64, +} + +impl HftTimestamp { + /// Get current timestamp with error handling for financial safety + /// + /// # Errors + /// Returns error if the operation fails + pub fn now() -> Result { + use std::time::{SystemTime, UNIX_EPOCH}; + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|e| CommonError::Service { + category: CommonErrorCategory::System, + message: format!("System time before UNIX epoch: {e}"), + })? + .as_nanos() + .try_into() + .unwrap_or(0_u64); + Ok(Self { nanos }) + } + + /// Get current timestamp with error handling for financial safety (`CommonTypeError` version) + /// + /// # Errors + /// Returns error if the operation fails + pub fn now_common() -> Result { + use std::time::{SystemTime, UNIX_EPOCH}; + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|e| CommonTypeError::ConversionError { + message: format!("System time before UNIX epoch: {e}"), + })? + .as_nanos() + .try_into() + .unwrap_or(0_u64); + Ok(Self { nanos }) + } + + /// Get current timestamp or zero if system time is invalid + #[must_use] + pub fn now_or_zero() -> Self { + Self::now().unwrap_or(Self { nanos: 0 }) + } + + /// Get nanoseconds since epoch + #[must_use] + pub const fn nanos(self) -> u64 { + self.nanos + } + + #[allow(clippy::as_conversions)] + /// Create from nanoseconds since epoch + #[must_use] + pub const fn from_nanos(nanos: u64) -> Self { + Self { nanos } + } + + /// Create from signed nanoseconds (cast to unsigned) + #[must_use] + #[allow(clippy::as_conversions)] + pub const fn from_nanos_i64(nanos: i64) -> Self { + Self { + nanos: nanos as u64, + } + } + + /// Get nanoseconds since epoch + pub const fn as_nanos(&self) -> u64 { + self.nanos + } + + /// Convert to `DateTime` + #[allow(clippy::integer_division)] + pub fn to_datetime(&self) -> DateTime { + let secs = self.nanos / 1_000_000_000; + let nsecs = u32::try_from(self.nanos % 1_000_000_000).unwrap_or(0); + DateTime::from_timestamp(i64::try_from(secs).unwrap_or(0), nsecs).unwrap_or_default() + } +} + +impl fmt::Display for HftTimestamp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_datetime()) + } +} + +/// Generic timestamp for general use cases +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct GenericTimestamp { + nanos: u64, +} + +impl GenericTimestamp { + /// Create from nanoseconds since epoch + #[must_use] + pub const fn from_nanos(nanos: u64) -> Self { + Self { nanos } + } + + /// Get nanoseconds since epoch + #[must_use] + pub const fn nanos(&self) -> u64 { + self.nanos + } +} diff --git a/crates/common/src/types/events.rs b/crates/common/src/types/events.rs new file mode 100644 index 000000000..8e0770b4c --- /dev/null +++ b/crates/common/src/types/events.rs @@ -0,0 +1,54 @@ +//! Order event types +//! +//! Event types for the complete order lifecycle. + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use super::{OrderId, OrderSide, OrderType, Price, Quantity, Symbol}; + +// ============================================================================= +// Event Types - Moved from trading_engine to enforce pure client architecture +// ============================================================================= + +/// Order events for the complete order lifecycle +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OrderEvent { + /// Unique identifier for the order + pub order_id: OrderId, + /// Trading symbol for the order + pub symbol: Symbol, + /// Type of order (market, limit, stop, etc.) + pub order_type: OrderType, + /// Order side (buy or sell) + pub side: OrderSide, + /// Order quantity + pub quantity: Quantity, + /// Order price (None for market orders) + pub price: Option, + /// Timestamp when the event occurred + pub timestamp: DateTime, + /// Strategy or client identifier + pub strategy_id: String, + /// Order event type (placed, modified, cancelled) + pub event_type: OrderEventType, + /// Previous quantity for modifications + pub previous_quantity: Option, + /// Previous price for modifications + pub previous_price: Option, + /// Reason for cancellation or modification + pub reason: Option, +} + +/// Types of order events +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum OrderEventType { + /// Order was placed + Placed, + /// Order was modified + Modified, + /// Order was cancelled + Cancelled, + /// Order was rejected + Rejected, +} diff --git a/crates/common/src/types/identifiers.rs b/crates/common/src/types/identifiers.rs new file mode 100644 index 000000000..2655bb58c --- /dev/null +++ b/crates/common/src/types/identifiers.rs @@ -0,0 +1,248 @@ +//! Identifier types and decimal extensions +//! +//! Newtype identifiers (EventId, FillId, AggregateId, AssetId, ClientId) +//! and the DecimalExt trait. + +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; +use std::fmt; + +use super::CommonTypeError; + +// ============================================================================= +// CORE ID TYPES (MIGRATED FROM TRADING_ENGINE) +// ============================================================================= + +// Duplicate TradeId removed - using definition from line 1008 + +/// Event identifier for tracking system events +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct EventId(String); + +impl EventId { + /// Create a new random event ID + pub fn new() -> Self { + use uuid::Uuid; + Self(Uuid::new_v4().to_string()) + } + + /// Create an event ID from a string, generating new if empty + pub fn from_string>(id: S) -> Self { + let id = id.into(); + if id.is_empty() { + Self::new() // Generate new ID if empty + } else { + Self(id) + } + } + + /// Get the string value of the event ID + pub fn value(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for EventId { + /// Format the event ID for display + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for EventId { + /// Create an `EventId` from a String + fn from(s: String) -> Self { + Self(s) + } +} + +impl Default for EventId { + /// Create a default `EventId` with a new UUID + fn default() -> Self { + Self::new() + } +} + +// ============================================================================= +// Decimal Extension Trait +// ============================================================================= + +/// Extension trait for `rust_decimal::Decimal` with convenience methods +pub trait DecimalExt { + /// Create Decimal from f64, compatible with legacy `from_f64` usage + fn from_f64(value: f64) -> Option + where + Self: Sized; + + /// Calculate square root of Decimal + fn sqrt(&self) -> Option + where + Self: Sized; +} + +impl DecimalExt for Decimal { + fn from_f64(value: f64) -> Option { + Decimal::from_f64_retain(value) + } + + fn sqrt(&self) -> Option { + if self.is_sign_negative() { + return None; + } + let value_f64: f64 = self.to_string().parse().ok()?; + let sqrt_f64 = value_f64.sqrt(); + Decimal::from_f64_retain(sqrt_f64) + } +} + +/// Fill identifier with validation +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct FillId(String); + +impl FillId { + /// Create a new fill ID with validation + /// + /// # Errors + /// Returns error if the operation fails + pub fn new>(id: S) -> Result { + let id = id.into(); + if id.is_empty() { + return Err(CommonTypeError::ValidationError { + field: "fill_id".to_owned(), + reason: "Fill ID cannot be empty".to_owned(), + }); + } + Ok(Self(id)) + } + + /// Get the fill ID as a string slice + pub fn as_str(&self) -> &str { + &self.0 + } + /// Convert the fill ID into an owned string + /// + /// Convert the execution ID into an owned string + /// + /// Convert execution ID into owned string + pub fn into_string(self) -> String { + self.0 + } +} + +impl fmt::Display for FillId { + /// Format the fill ID for display + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Aggregate identifier with validation +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct AggregateId(String); + +impl AggregateId { + /// Create a new aggregate ID with validation + /// + /// # Errors + /// Returns error if the operation fails + pub fn new>(id: S) -> Result { + let id = id.into(); + if id.is_empty() { + return Err(CommonTypeError::ValidationError { + field: "aggregate_id".to_owned(), + reason: "Aggregate ID cannot be empty".to_owned(), + }); + } + Ok(Self(id)) + } + + /// Get the aggregate ID as a string slice + pub fn as_str(&self) -> &str { + &self.0 + } + /// Convert the aggregate ID into an owned string + pub fn into_string(self) -> String { + self.0 + } +} + +impl fmt::Display for AggregateId { + /// Format the aggregate ID for display + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Asset identifier with validation +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct AssetId(String); + +impl AssetId { + /// Create a new asset ID with validation + /// + /// # Errors + /// Returns error if the operation fails + pub fn new>(id: S) -> Result { + let id = id.into(); + if id.is_empty() { + return Err(CommonTypeError::ValidationError { + field: "asset_id".to_owned(), + reason: "Asset ID cannot be empty".to_owned(), + }); + } + Ok(Self(id)) + } + + /// Get the asset ID as a string slice + pub fn as_str(&self) -> &str { + &self.0 + } + /// Convert the asset ID into an owned string + pub fn into_string(self) -> String { + self.0 + } +} + +impl fmt::Display for AssetId { + /// Format the asset ID for display + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Client identifier with validation +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ClientId(String); + +impl ClientId { + /// Create a new client ID with validation + /// + /// # Errors + /// Returns error if the operation fails + pub fn new>(id: S) -> Result { + let id = id.into(); + if id.is_empty() { + return Err(CommonTypeError::ValidationError { + field: "client_id".to_owned(), + reason: "Client ID cannot be empty".to_owned(), + }); + } + Ok(Self(id)) + } + + /// Get the client ID as a string slice + pub fn as_str(&self) -> &str { + &self.0 + } + /// Convert the client ID into an owned string + pub fn into_string(self) -> String { + self.0 + } +} + +impl fmt::Display for ClientId { + /// Format the client ID for display + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/crates/common/src/types/market.rs b/crates/common/src/types/market.rs new file mode 100644 index 000000000..57e7c7dd2 --- /dev/null +++ b/crates/common/src/types/market.rs @@ -0,0 +1,472 @@ +//! Market types for trading venues and signals +//! +//! Contains MarketRegime, TickType, Exchange, MarketTick, TradingSignal, and OrderRef. + +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::str::FromStr; +use uuid::Uuid; + +use crate::error::CommonError; +use super::{ + CommonTypeError, HftTimestamp, Order, OrderId, OrderSide, OrderType, Price, Quantity, Symbol, +}; + +// ============================================================================= +// MARKET TYPES (MIGRATED FROM TRADING_ENGINE) +// ============================================================================= + +/// Market regime enumeration for position sizing scaling and risk management +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum MarketRegime { + /// Normal market conditions + Normal, + /// Crisis/stress market conditions + Crisis, + /// Trending market (strong directional movement) + Trending, + /// Sideways/ranging market (low volatility) + Sideways, + /// Bull market (sustained upward trend) + Bull, + /// Bear market (sustained downward trend) + Bear, + /// High volatility market conditions + HighVolatility, + /// Low volatility market conditions + LowVolatility, + /// Volatile market conditions (alias for `HighVolatility`) + Volatile, + /// Calm market conditions (alias for `LowVolatility`) + Calm, + /// Unknown/unclassified regime + Unknown, + /// Recovery regime - transitioning from crisis + Recovery, + /// Bubble regime - unsustainable upward movement + Bubble, + /// Correction regime - temporary downward adjustment + Correction, + /// Custom regime with numeric identifier + Custom(usize), +} + +impl Default for MarketRegime { + fn default() -> Self { + Self::Normal + } +} + +impl fmt::Display for MarketRegime { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Normal => write!(f, "Normal"), + Self::Crisis => write!(f, "Crisis"), + Self::Trending => write!(f, "Trending"), + Self::Sideways => write!(f, "Sideways"), + Self::Bull => write!(f, "Bull"), + Self::Bear => write!(f, "Bear"), + Self::HighVolatility => write!(f, "HighVolatility"), + Self::LowVolatility => write!(f, "LowVolatility"), + Self::Volatile => write!(f, "Volatile"), + Self::Calm => write!(f, "Calm"), + Self::Unknown => write!(f, "Unknown"), + Self::Recovery => write!(f, "Recovery"), + Self::Bubble => write!(f, "Bubble"), + Self::Correction => write!(f, "Correction"), + Self::Custom(id) => write!(f, "Custom({id})"), + } + } +} + +/// Tick type enumeration for market data +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "database", derive(sqlx::Type))] +#[cfg_attr( + feature = "database", + sqlx(type_name = "tick_type", rename_all = "snake_case") +)] +pub enum TickType { + /// Trade execution tick + Trade, + /// Bid price update tick + Bid, + /// Ask price update tick + Ask, + /// Quote (bid/ask) update tick + Quote, +} + +/// Exchange enumeration for trading venues +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum Exchange { + /// New York Stock Exchange + NYSE, + /// NASDAQ + NASDAQ, + /// Chicago Mercantile Exchange + CME, + /// Intercontinental Exchange + ICE, + /// London Stock Exchange + LSE, + /// Tokyo Stock Exchange + TSE, + /// Hong Kong Stock Exchange + HKEX, + /// Shanghai Stock Exchange + SSE, + /// Shenzhen Stock Exchange + SZSE, + /// Euronext + EURONEXT, + /// Deutsche Borse + XETRA, + /// Chicago Board of Trade + CBOT, + /// Chicago Board Options Exchange + CBOE, + /// BATS Global Markets + BATS, + /// IEX Exchange + IEX, + /// Interactive Brokers + IBKR, + /// IC Markets + ICMARKETS, + /// Forex.com + FOREX, + /// Binance + BINANCE, + /// Coinbase + COINBASE, + /// Kraken + KRAKEN, + /// Unknown or unrecognized exchange + UNKNOWN, +} + +impl Default for Exchange { + fn default() -> Self { + Self::UNKNOWN + } +} + +impl fmt::Display for Exchange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NYSE => write!(f, "NYSE"), + Self::NASDAQ => write!(f, "NASDAQ"), + Self::CME => write!(f, "CME"), + Self::ICE => write!(f, "ICE"), + Self::LSE => write!(f, "LSE"), + Self::TSE => write!(f, "TSE"), + Self::HKEX => write!(f, "HKEX"), + Self::SSE => write!(f, "SSE"), + Self::SZSE => write!(f, "SZSE"), + Self::EURONEXT => write!(f, "EURONEXT"), + Self::XETRA => write!(f, "XETRA"), + Self::CBOT => write!(f, "CBOT"), + Self::CBOE => write!(f, "CBOE"), + Self::BATS => write!(f, "BATS"), + Self::IEX => write!(f, "IEX"), + Self::IBKR => write!(f, "IBKR"), + Self::ICMARKETS => write!(f, "ICMARKETS"), + Self::FOREX => write!(f, "FOREX"), + Self::BINANCE => write!(f, "BINANCE"), + Self::COINBASE => write!(f, "COINBASE"), + Self::KRAKEN => write!(f, "KRAKEN"), + Self::UNKNOWN => write!(f, "UNKNOWN"), + } + } +} + +impl FromStr for Exchange { + type Err = CommonTypeError; + + fn from_str(s: &str) -> Result { + match s.to_uppercase().as_str() { + "NYSE" => Ok(Self::NYSE), + "NASDAQ" => Ok(Self::NASDAQ), + "CME" => Ok(Self::CME), + "ICE" => Ok(Self::ICE), + "LSE" => Ok(Self::LSE), + "TSE" => Ok(Self::TSE), + "HKEX" => Ok(Self::HKEX), + "SSE" => Ok(Self::SSE), + "SZSE" => Ok(Self::SZSE), + "EURONEXT" => Ok(Self::EURONEXT), + "XETRA" => Ok(Self::XETRA), + "CBOT" => Ok(Self::CBOT), + "CBOE" => Ok(Self::CBOE), + "BATS" => Ok(Self::BATS), + "IEX" => Ok(Self::IEX), + "IBKR" => Ok(Self::IBKR), + "ICMARKETS" => Ok(Self::ICMARKETS), + "FOREX" => Ok(Self::FOREX), + "BINANCE" => Ok(Self::BINANCE), + "COINBASE" => Ok(Self::COINBASE), + "KRAKEN" => Ok(Self::KRAKEN), + "UNKNOWN" => Ok(Self::UNKNOWN), + _ => Ok(Self::UNKNOWN), // Default to UNKNOWN for unrecognized exchanges + } + } +} + +/// Market tick data structure - CANONICAL SINGLE SOURCE OF TRUTH +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MarketTick { + /// Trading symbol + pub symbol: Symbol, + /// Tick price + pub price: Price, + /// Tick size/quantity + pub size: Quantity, + /// Tick timestamp + pub timestamp: HftTimestamp, + /// Type of tick (trade, bid, ask, quote) + pub tick_type: TickType, + /// Exchange where the tick occurred + pub exchange: Exchange, + /// Sequence number for ordering + pub sequence_number: u64, +} + +impl MarketTick { + /// Create a new market tick with current timestamp + /// + /// # Errors + /// Returns error if the operation fails + pub fn new( + symbol: Symbol, + price: Price, + size: Quantity, + tick_type: TickType, + exchange: Exchange, + sequence_number: u64, + ) -> Result { + Ok(Self { + symbol, + price, + size, + timestamp: HftTimestamp::now()?, + tick_type, + exchange, + sequence_number, + }) + } + + /// Create a new market tick with specified timestamp (for backtesting) + #[must_use] + pub const fn with_timestamp( + symbol: Symbol, + price: Price, + size: Quantity, + timestamp: HftTimestamp, + tick_type: TickType, + exchange: Exchange, + sequence_number: u64, + ) -> Self { + Self { + symbol, + price, + size, + timestamp, + tick_type, + exchange, + sequence_number, + } + } +} + +/// Trading signal for algorithmic trading +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TradingSignal { + /// Signal ID + pub signal_id: Uuid, + /// Symbol this signal applies to + pub symbol: Symbol, + /// Signal strength (-1.0 to 1.0) + pub strength: f64, + /// Signal direction + pub direction: OrderSide, + /// Confidence level (0.0 to 1.0) + pub confidence: f64, + /// Signal generation timestamp + pub timestamp: HftTimestamp, + /// Signal source/strategy + pub source: String, + /// Additional metadata + pub metadata: std::collections::HashMap, +} + +impl TradingSignal { + /// Create a new trading signal + /// + /// # Errors + /// Returns error if the operation fails + pub fn new( + symbol: Symbol, + strength: f64, + direction: OrderSide, + confidence: f64, + source: String, + ) -> Result { + if !(0.0_f64..=1.0_f64).contains(&confidence) { + return Err(CommonTypeError::ValidationError { + field: "confidence".to_owned(), + reason: "Confidence must be between 0.0 and 1.0".to_owned(), + }); + } + if !(-1.0_f64..=1.0_f64).contains(&strength) { + return Err(CommonTypeError::ValidationError { + field: "strength".to_owned(), + reason: "Strength must be between -1.0 and 1.0".to_owned(), + }); + } + + Ok(Self { + signal_id: Uuid::new_v4(), + symbol, + strength, + direction, + confidence, + timestamp: HftTimestamp::now_common()?, + source, + metadata: std::collections::HashMap::new(), + }) + } + + /// Add metadata to the signal + #[must_use] + pub fn with_metadata(mut self, key: String, value: String) -> Self { + self.metadata.insert(key, value); + self + } +} + +// ============================================================================= +// HIGH-PERFORMANCE TYPES FOR COPY/CLONE OPTIMIZATION +// ============================================================================= + +/// Lightweight Order reference for high-performance contexts requiring Copy trait +/// +/// This struct contains only the essential order data needed for performance-critical +/// operations like `SmallBatchRing` processing, while maintaining Copy semantics. +/// +/// For full order details, use the complete Order struct. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct OrderRef { + /// Order ID (u64 for performance) + pub id: u64, + /// Symbol hash for fast lookups + pub symbol_hash: i64, + /// Order side (Buy/Sell) + pub side: OrderSide, + /// Order type + pub order_type: OrderType, + /// Quantity (fixed-point u64) + pub quantity: u64, + /// Price (fixed-point u64, 0 for market orders) + pub price: u64, + /// Timestamp (nanoseconds since epoch) + pub timestamp: u64, +} + +impl OrderRef { + /// Create `OrderRef` from a full Order struct + #[must_use] + pub fn from_order(order: &Order) -> Self { + Self { + id: order.id.value(), + symbol_hash: order.symbol_hash(), + side: order.side, + order_type: order.order_type, + quantity: order.quantity.raw_value(), + price: order.price.map_or(0, |p| p.raw_value()), + timestamp: order.created_at.nanos(), + } + } + + /// Create a limit order reference + #[must_use] + pub fn limit(symbol_hash: i64, side: OrderSide, quantity: u64, price: u64) -> Self { + Self { + id: OrderId::new().value(), + symbol_hash, + side, + order_type: OrderType::Limit, + quantity, + price, + timestamp: HftTimestamp::now_or_zero().nanos(), + } + } + + /// Create a market order reference + #[must_use] + pub fn market(symbol_hash: i64, side: OrderSide, quantity: u64) -> Self { + Self { + id: OrderId::new().value(), + symbol_hash, + side, + order_type: OrderType::Market, + quantity, + price: 0, + timestamp: HftTimestamp::now_or_zero().nanos(), + } + } + + /// Get quantity as Quantity type + #[must_use] + pub const fn get_quantity(&self) -> Quantity { + Quantity::from_raw(self.quantity) + } + + /// Get price as Price type (None for market orders) + #[must_use] + pub const fn get_price(&self) -> Option { + if self.price == 0 { + None + } else { + Some(Price::from_raw(self.price)) + } + } + + /// Check if this is a buy order + #[must_use] + pub fn is_buy(&self) -> bool { + self.side == OrderSide::Buy + } + + /// Check if this is a sell order + #[must_use] + pub fn is_sell(&self) -> bool { + self.side == OrderSide::Sell + } + + /// Check if this is a market order + #[must_use] + pub fn is_market_order(&self) -> bool { + self.order_type == OrderType::Market || self.price == 0 + } + + /// Check if this is a limit order + #[must_use] + pub fn is_limit_order(&self) -> bool { + self.order_type == OrderType::Limit && self.price > 0 + } +} + +impl Default for OrderRef { + fn default() -> Self { + Self { + id: 0, + symbol_hash: 0, + side: OrderSide::Buy, + order_type: OrderType::Market, + quantity: 0, + price: 0, + timestamp: 0, + } + } +} diff --git a/crates/common/src/types/market_data.rs b/crates/common/src/types/market_data.rs new file mode 100644 index 000000000..663729c6e --- /dev/null +++ b/crates/common/src/types/market_data.rs @@ -0,0 +1,562 @@ +//! Market data event types +//! +//! Consolidated market data types from data and trading_engine crates. + +use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; +use std::fmt; + +use crate::error::ErrorCategory; +use super::{Price, Quantity}; + +// ============================================================================= +// MARKET DATA EVENT TYPES (Consolidated from data and trading_engine crates) +// ============================================================================= + +/// Market data event types - CANONICAL DEFINITION +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MarketDataEvent { + /// Quote update (bid/ask) + Quote(QuoteEvent), + /// Trade execution + Trade(TradeEvent), + /// Aggregate trade data + Aggregate(Aggregate), + /// Bar/candle data + Bar(BarEvent), + /// Level 2 market data update + Level2(Level2Update), + /// Market status update + Status(MarketStatus), + /// Connection status updates + ConnectionStatus(ConnectionEvent), + /// Error events with details + Error(ErrorEvent), + /// Order book update + OrderBook(OrderBookEvent), + /// Level 2 order book snapshot + OrderBookL2Snapshot(OrderBookSnapshot), + /// Level 2 order book incremental update + OrderBookL2Update(OrderBookUpdate), +} + +/// Quote event structure - CANONICAL DEFINITION +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct QuoteEvent { + /// Symbol + pub symbol: String, + /// Bid price + pub bid: Option, + /// Ask price + pub ask: Option, + /// Bid size + pub bid_size: Option, + /// Ask size + pub ask_size: Option, + /// Exchange + pub exchange: Option, + /// Bid exchange + pub bid_exchange: Option, + /// Ask exchange + pub ask_exchange: Option, + /// Quote conditions + pub conditions: Vec, + /// Timestamp + pub timestamp: DateTime, + /// Sequence number + pub sequence: u64, +} + +impl QuoteEvent { + /// Create a new quote event + #[must_use] + pub const fn new(symbol: String, timestamp: DateTime) -> Self { + Self { + symbol, + bid: None, + ask: None, + bid_size: None, + ask_size: None, + exchange: None, + bid_exchange: None, + ask_exchange: None, + conditions: Vec::new(), + timestamp, + sequence: 0, + } + } + + /// Set bid price and size + pub const fn with_bid(mut self, price: Decimal, size: Decimal) -> Self { + self.bid = Some(price); + self.bid_size = Some(size); + self + } + + /// Set ask price and size + pub const fn with_ask(mut self, price: Decimal, size: Decimal) -> Self { + self.ask = Some(price); + self.ask_size = Some(size); + self + } + + /// Set exchange + pub fn with_exchange>(mut self, exchange: S) -> Self { + self.exchange = Some(exchange.into()); + self + } + + /// Set bid exchange + pub fn with_bid_exchange>(mut self, exchange: S) -> Self { + self.bid_exchange = Some(exchange.into()); + self + } + + /// Set ask exchange + pub fn with_ask_exchange>(mut self, exchange: S) -> Self { + self.ask_exchange = Some(exchange.into()); + self + } + + /// Add quote condition + pub fn with_condition>(mut self, condition: S) -> Self { + self.conditions.push(condition.into()); + self + } + + /// Set sequence number + pub const fn with_sequence(mut self, sequence: u64) -> Self { + self.sequence = sequence; + self + } + + /// Get mid price + pub fn mid_price(&self) -> Option { + match (self.bid, self.ask) { + (Some(bid), Some(ask)) => { + let sum = bid.checked_add(ask)?; + let two = Decimal::from(2); + sum.checked_div(two) + }, + _ => None, + } + } + + /// Get spread + pub fn spread(&self) -> Option { + match (self.bid, self.ask) { + (Some(bid), Some(ask)) => ask.checked_sub(bid), + _ => None, + } + } +} + +/// Trade event structure - CANONICAL DEFINITION +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TradeEvent { + /// Symbol + pub symbol: String, + /// Trade price + pub price: Decimal, + /// Trade size + pub size: Decimal, + /// Trade ID + pub trade_id: Option, + /// Exchange + pub exchange: Option, + /// Trade conditions + pub conditions: Vec, + /// Timestamp + pub timestamp: DateTime, + /// Sequence number + pub sequence: u64, +} + +impl TradeEvent { + /// Create a new trade event + #[must_use] + pub const fn new( + symbol: String, + price: Decimal, + size: Decimal, + timestamp: DateTime, + ) -> Self { + Self { + symbol, + price, + size, + trade_id: None, + exchange: None, + conditions: Vec::new(), + timestamp, + sequence: 0, + } + } + + /// Set trade ID + pub fn with_trade_id>(mut self, trade_id: S) -> Self { + self.trade_id = Some(trade_id.into()); + self + } + + /// Set exchange + pub fn with_exchange>(mut self, exchange: S) -> Self { + self.exchange = Some(exchange.into()); + self + } + + /// Add trade condition + pub fn with_condition>(mut self, condition: S) -> Self { + self.conditions.push(condition.into()); + self + } + + /// Set sequence number + pub const fn with_sequence(mut self, sequence: u64) -> Self { + self.sequence = sequence; + self + } + + /// Get notional value + pub fn notional_value(&self) -> Decimal { + self.price.checked_mul(self.size).unwrap_or(Decimal::ZERO) + } +} + +/// Aggregate trade data +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Aggregate { + /// Symbol + pub symbol: String, + /// Open price + pub open: Decimal, + /// High price + pub high: Decimal, + /// Low price + pub low: Decimal, + /// Close price + pub close: Decimal, + /// Volume + pub volume: Decimal, + /// Volume weighted average price + pub vwap: Option, + /// Start timestamp + pub start_timestamp: DateTime, + /// End timestamp + pub end_timestamp: DateTime, +} + +/// Bar/candle event structure +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BarEvent { + /// Symbol + pub symbol: String, + /// Open price + pub open: Decimal, + /// High price + pub high: Decimal, + /// Low price + pub low: Decimal, + /// Close price + pub close: Decimal, + /// Volume + pub volume: Decimal, + /// Volume weighted average price + pub vwap: Option, + /// Start timestamp + pub start_timestamp: DateTime, + /// End timestamp + pub end_timestamp: DateTime, + /// Timeframe (e.g., "1m", "5m", "1h") + pub timeframe: String, +} + +/// Level 2 market data update +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Level2Update { + /// Symbol + pub symbol: String, + /// Bid levels + pub bids: Vec, + /// Ask levels + pub asks: Vec, + /// Timestamp + pub timestamp: DateTime, +} + +/// Price level for order book +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PriceLevel { + /// Price + pub price: Decimal, + /// Size at this price level + pub size: Decimal, +} + +/// Order book snapshot from providers +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OrderBookSnapshot { + /// Symbol + pub symbol: String, + /// Bid levels (price, size) sorted by price descending + pub bids: Vec, + /// Ask levels (price, size) sorted by price ascending + pub asks: Vec, + /// Exchange + pub exchange: String, + /// Timestamp of snapshot + pub timestamp: DateTime, + /// Sequence number + pub sequence: u64, +} + +/// Incremental order book update from providers +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OrderBookUpdate { + /// Symbol + pub symbol: String, + /// Changes to bid levels + pub bid_changes: Vec, + /// Changes to ask levels + pub ask_changes: Vec, + /// Exchange + pub exchange: String, + /// Timestamp of update + pub timestamp: DateTime, + /// Sequence number + pub sequence: u64, +} + +/// Change to a price level +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PriceLevelChange { + /// Price level being modified + pub price: Decimal, + /// New size (0 = remove level) + pub size: Decimal, + /// Type of change + pub change_type: PriceLevelChangeType, + /// Side (bid or ask) + pub side: OrderBookSide, +} + +/// Type of price level change +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +pub enum PriceLevelChangeType { + /// Add new price level + Add, + /// Update existing price level + Update, + /// Remove price level + Delete, +} + +/// Order book side +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +pub enum OrderBookSide { + /// Bid side + Bid, + /// Ask side + Ask, +} + +/// Market status information +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MarketStatus { + /// Market + pub market: String, + /// Status (open, closed, `early_hours`, etc.) + pub status: String, + /// Timestamp + pub timestamp: DateTime, +} + +/// Connection event for status updates +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConnectionEvent { + /// Provider name + pub provider: String, + /// Connection status + pub status: ConnectionStatus, + /// Optional message + pub message: Option, + /// Timestamp + pub timestamp: DateTime, +} + +/// Connection status enumeration +/// +/// Connection status for data providers and brokers +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "database", derive(sqlx::Type))] +#[cfg_attr( + feature = "database", + sqlx(type_name = "connection_status", rename_all = "snake_case") +)] +pub enum ConnectionStatus { + /// Successfully connected and operational + Connected, + /// Disconnected from the service + Disconnected, + /// Currently attempting to reconnect + Reconnecting, +} + +/// Error event structure +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ErrorEvent { + /// Provider name + pub provider: String, + /// Error message + pub message: String, + /// Error category + pub category: ErrorCategory, + /// Timestamp + pub timestamp: DateTime, +} + +// ErrorCategory is imported from crate::error as CommonErrorCategory + +/// Order book event +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OrderBookEvent { + /// Symbol + pub symbol: String, + /// Timestamp + pub timestamp: DateTime, + /// Bid levels + pub bids: Vec<(Price, Quantity)>, + /// Ask levels + pub asks: Vec<(Price, Quantity)>, +} + +/// Data types for subscription +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DataType { + /// Real-time quotes + Quotes, + /// Real-time trades + Trades, + /// Aggregate/minute bars + Aggregates, + /// Level 2 order book + Level2, + /// Market status + Status, + /// Historical bars/aggregates + Bars, + /// Order book data + OrderBook, + /// Volume data + Volume, +} + +/// Market data subscription request +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Subscription { + /// Symbols to subscribe to + pub symbols: Vec, + /// Data types to subscribe to + pub data_types: Vec, + /// Exchange filter (optional) + pub exchanges: Vec, +} + +impl MarketDataEvent { + /// Get the symbol for any market data event + pub fn symbol(&self) -> &str { + match self { + MarketDataEvent::Quote(q) => &q.symbol, + MarketDataEvent::Trade(t) => &t.symbol, + MarketDataEvent::Aggregate(a) => &a.symbol, + MarketDataEvent::Bar(b) => &b.symbol, + MarketDataEvent::Level2(l) => &l.symbol, + MarketDataEvent::Status(s) => &s.market, + MarketDataEvent::ConnectionStatus(_) => "", + MarketDataEvent::Error(_) => "", + MarketDataEvent::OrderBook(o) => &o.symbol, + MarketDataEvent::OrderBookL2Snapshot(s) => &s.symbol, + MarketDataEvent::OrderBookL2Update(u) => &u.symbol, + } + } + + /// Get the timestamp for any market data event + pub const fn timestamp(&self) -> Option> { + match self { + MarketDataEvent::Quote(q) => Some(q.timestamp), + MarketDataEvent::Trade(t) => Some(t.timestamp), + MarketDataEvent::Aggregate(a) => Some(a.end_timestamp), + MarketDataEvent::Bar(b) => Some(b.end_timestamp), + MarketDataEvent::Level2(l) => Some(l.timestamp), + MarketDataEvent::Status(s) => Some(s.timestamp), + MarketDataEvent::ConnectionStatus(c) => Some(c.timestamp), + MarketDataEvent::Error(e) => Some(e.timestamp), + MarketDataEvent::OrderBook(o) => Some(o.timestamp), + MarketDataEvent::OrderBookL2Snapshot(s) => Some(s.timestamp), + MarketDataEvent::OrderBookL2Update(u) => Some(u.timestamp), + } + } +} + +/// Connection information for services +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConnectionInfo { + /// Host address + pub host: String, + /// Port number + pub port: u16, + /// Whether TLS is enabled + pub tls: bool, + /// Connection timeout in milliseconds + pub timeout_ms: u64, +} + +impl ConnectionInfo { + /// Create new connection info + pub fn new>(host: S, port: u16) -> Self { + Self { + host: host.into(), + port, + tls: false, + timeout_ms: 5000, + } + } + + /// Enable TLS + pub const fn with_tls(mut self) -> Self { + self.tls = true; + self + } + + /// Set timeout + pub const fn with_timeout(mut self, timeout_ms: u64) -> Self { + self.timeout_ms = timeout_ms; + self + } + + /// Get connection URL + pub fn url(&self) -> String { + let scheme = if self.tls { "https" } else { "http" }; + format!("{}://{}:{}", scheme, self.host, self.port) + } +} + +impl fmt::Display for ConnectionInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.url()) + } +} + +/// Resource limits for services +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ResourceLimits { + /// Maximum memory usage in bytes + pub max_memory_bytes: Option, + /// Maximum CPU usage as percentage (0-100) + pub max_cpu_percent: Option, + /// Maximum number of open file descriptors + pub max_file_descriptors: Option, + /// Maximum number of network connections + pub max_connections: Option, +} diff --git a/crates/common/src/types/mod.rs b/crates/common/src/types/mod.rs new file mode 100644 index 000000000..69ebca2ae --- /dev/null +++ b/crates/common/src/types/mod.rs @@ -0,0 +1,507 @@ +//! Common data types used across services +//! +//! This module provides shared data types that are used throughout +//! the Foxhunt HFT trading system. This includes both infrastructure types +//! and core trading types migrated from foxhunt-common-types. + +mod aliases; +mod domain; +mod events; +mod identifiers; +mod market; +mod market_data; +mod service; +mod trading_enums; +mod type_error; + +pub use aliases::*; +pub use domain::*; +pub use events::*; +pub use identifiers::*; +pub use market::*; +pub use market_data::*; +pub use service::*; +pub use trading_enums::*; +pub use type_error::*; + +// ============================================================================= +// COMPREHENSIVE TESTS +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + // ============================================================================= + // Price Tests + // ============================================================================= + + #[test] + fn test_price_from_f64_valid() { + let price = Price::from_f64(100.50).unwrap(); + assert_eq!(price.to_f64(), 100.50); + } + + #[test] + fn test_price_from_f64_negative() { + let result = Price::from_f64(-10.0); + assert!(result.is_err()); + } + + #[test] + fn test_price_from_f64_nan() { + let result = Price::from_f64(f64::NAN); + assert!(result.is_err()); + } + + #[test] + fn test_price_from_f64_infinity() { + let result = Price::from_f64(f64::INFINITY); + assert!(result.is_err()); + } + + #[test] + fn test_price_constants() { + assert_eq!(Price::ZERO.to_f64(), 0.0); + assert_eq!(Price::ONE.to_f64(), 1.0); + assert_eq!(Price::CENT.to_f64(), 0.01); + } + + #[test] + fn test_price_addition() { + let p1 = Price::from_f64(10.0).unwrap(); + let p2 = Price::from_f64(5.5).unwrap(); + let result = p1 + p2; + assert!((result.to_f64() - 15.5).abs() < 0.00001); + } + + #[test] + fn test_price_subtraction() { + let p1 = Price::from_f64(10.0).unwrap(); + let p2 = Price::from_f64(5.5).unwrap(); + let result = p1 - p2; + assert!((result.to_f64() - 4.5).abs() < 0.00001); + } + + #[test] + fn test_price_multiplication() { + let price = Price::from_f64(10.0).unwrap(); + let result = (price * 2.5).unwrap(); + assert!((result.to_f64() - 25.0).abs() < 0.00001); + } + + #[test] + fn test_price_division() { + let price = Price::from_f64(10.0).unwrap(); + let result = (price / 2.0).unwrap(); + assert!((result.to_f64() - 5.0).abs() < 0.00001); + } + + #[test] + fn test_price_division_by_zero() { + let price = Price::from_f64(10.0).unwrap(); + let result = price / 0.0; + assert!(result.is_err()); + } + + #[test] + fn test_price_from_cents() { + let price = Price::from_cents(150); + assert!((price.to_f64() - 1.50).abs() < 0.00001); + } + + #[test] + fn test_price_to_cents() { + let price = Price::from_f64(1.50).unwrap(); + assert_eq!(price.to_cents(), 150); + } + + #[test] + fn test_price_is_zero() { + assert!(Price::ZERO.is_zero()); + assert!(!Price::from_f64(1.0).unwrap().is_zero()); + } + + #[test] + fn test_price_from_str() { + let price = Price::from_str("123.45").unwrap(); + assert!((price.to_f64() - 123.45).abs() < 0.00001); + } + + #[test] + fn test_price_from_str_invalid() { + let result = Price::from_str("invalid"); + assert!(result.is_err()); + } + + #[test] + fn test_price_display() { + let price = Price::from_f64(123.456789).unwrap(); + let display = format!("{}", price); + assert!(display.starts_with("123.45678")); + } + + #[test] + fn test_price_partial_eq_f64() { + let price = Price::from_f64(10.0).unwrap(); + assert_eq!(price, 10.0); + assert_eq!(10.0, price); + } + + #[test] + fn test_price_multiply_price() { + let p1 = Price::from_f64(10.0).unwrap(); + let p2 = Price::from_f64(2.5).unwrap(); + let result = p1.multiply(p2).unwrap(); + assert!((result.to_f64() - 25.0).abs() < 0.00001); + } + + // ============================================================================= + // Quantity Tests + // ============================================================================= + + #[test] + fn test_quantity_from_f64_valid() { + let qty = Quantity::from_f64(100.5).unwrap(); + assert_eq!(qty.to_f64(), 100.5); + } + + #[test] + fn test_quantity_from_f64_negative() { + let result = Quantity::from_f64(-10.0); + assert!(result.is_err()); + } + + #[test] + fn test_quantity_from_f64_nan() { + let result = Quantity::from_f64(f64::NAN); + assert!(result.is_err()); + } + + #[test] + fn test_quantity_constants() { + assert_eq!(Quantity::ZERO.to_f64(), 0.0); + assert_eq!(Quantity::ONE.to_f64(), 1.0); + } + + #[test] + fn test_quantity_addition() { + let q1 = Quantity::from_f64(10.0).unwrap(); + let q2 = Quantity::from_f64(5.5).unwrap(); + let result = q1 + q2; + assert!((result.to_f64() - 15.5).abs() < 0.00001); + } + + #[test] + fn test_quantity_subtraction() { + let q1 = Quantity::from_f64(10.0).unwrap(); + let q2 = Quantity::from_f64(5.5).unwrap(); + let result = q1 - q2; + assert!((result.to_f64() - 4.5).abs() < 0.00001); + } + + #[test] + fn test_quantity_multiplication() { + let qty = Quantity::from_f64(10.0).unwrap(); + let result = (qty * 2.5).unwrap(); + assert!((result.to_f64() - 25.0).abs() < 0.00001); + } + + #[test] + fn test_quantity_division() { + let qty = Quantity::from_f64(10.0).unwrap(); + let result = (qty / 2.0).unwrap(); + assert!((result.to_f64() - 5.0).abs() < 0.00001); + } + + #[test] + fn test_quantity_division_by_zero() { + let qty = Quantity::from_f64(10.0).unwrap(); + let result = qty / 0.0; + assert!(result.is_err()); + } + + #[test] + fn test_quantity_is_zero() { + assert!(Quantity::ZERO.is_zero()); + assert!(!Quantity::from_f64(1.0).unwrap().is_zero()); + } + + #[test] + fn test_quantity_is_positive() { + assert!(Quantity::from_f64(1.0).unwrap().is_positive()); + assert!(!Quantity::ZERO.is_positive()); + } + + #[test] + fn test_quantity_is_negative() { + // Quantity is always non-negative + assert!(!Quantity::from_f64(1.0).unwrap().is_negative()); + assert!(!Quantity::ZERO.is_negative()); + } + + #[test] + fn test_quantity_from_shares() { + let qty = Quantity::from_shares(100); + assert_eq!(qty.to_shares(), 100); + } + + #[test] + fn test_quantity_sum() { + let quantities = vec![ + Quantity::from_f64(1.0).unwrap(), + Quantity::from_f64(2.0).unwrap(), + Quantity::from_f64(3.0).unwrap(), + ]; + let sum: Quantity = quantities.into_iter().sum(); + assert!((sum.to_f64() - 6.0).abs() < 0.00001); + } + + #[test] + fn test_quantity_try_from_i32() { + let qty = Quantity::try_from(100i32).unwrap(); + assert_eq!(qty.to_f64(), 100.0); + } + + #[test] + fn test_quantity_try_from_string() { + let qty = Quantity::try_from("123.45").unwrap(); + assert!((qty.to_f64() - 123.45).abs() < 0.00001); + } + + // ============================================================================= + // Money Tests + // ============================================================================= + + #[test] + fn test_money_new() { + let amount = rust_decimal::Decimal::from_f64_retain(100.50).unwrap(); + let money = Money::new(amount, Currency::USD); + assert_eq!(money.currency, Currency::USD); + assert_eq!(money.amount, amount); + } + + #[test] + fn test_money_display() { + let amount = rust_decimal::Decimal::from_f64_retain(100.50).unwrap(); + let money = Money::new(amount, Currency::USD); + let display = format!("{}", money); + assert!(display.contains("100.5")); + assert!(display.contains("USD")); + } + + // ============================================================================= + // Symbol Tests + // ============================================================================= + + #[test] + fn test_symbol_new() { + let symbol = Symbol::new("AAPL".to_owned()); + assert_eq!(symbol.as_str(), "AAPL"); + } + + #[test] + fn test_symbol_new_validated_valid() { + let symbol = Symbol::new_validated("AAPL".to_owned()).unwrap(); + assert_eq!(symbol.as_str(), "AAPL"); + } + + #[test] + fn test_symbol_new_validated_empty() { + let result = Symbol::new_validated("".to_owned()); + assert!(result.is_err()); + } + + #[test] + fn test_symbol_new_validated_whitespace() { + let result = Symbol::new_validated(" ".to_owned()); + assert!(result.is_err()); + } + + #[test] + fn test_symbol_from_str() { + let symbol = Symbol::from_str("AAPL").unwrap(); + assert_eq!(symbol.as_str(), "AAPL"); + } + + #[test] + fn test_symbol_to_uppercase() { + let symbol = Symbol::from_str("aapl").unwrap(); + assert_eq!(symbol.to_uppercase(), "AAPL"); + } + + #[test] + fn test_symbol_replace() { + let symbol = Symbol::from_str("AAPL.US").unwrap(); + assert_eq!(symbol.replace(".US", ""), "AAPL"); + } + + #[test] + fn test_symbol_contains() { + let symbol = Symbol::from_str("AAPL.US").unwrap(); + assert!(symbol.contains("AAPL")); + assert!(!symbol.contains("MSFT")); + } + + #[test] + fn test_symbol_partial_eq_str() { + let symbol = Symbol::from_str("AAPL").unwrap(); + assert_eq!("AAPL", symbol); + assert_eq!(symbol.as_str(), "AAPL"); + } + + #[test] + fn test_symbol_none() { + let symbol = Symbol::none(); + assert_eq!(symbol.as_str(), "NONE"); + } + + // ============================================================================= + // TimeInForce Tests + // ============================================================================= + + #[test] + fn test_time_in_force_display() { + assert_eq!(format!("{}", TimeInForce::Day), "DAY"); + assert_eq!(format!("{}", TimeInForce::GoodTillCancel), "GTC"); + assert_eq!(format!("{}", TimeInForce::ImmediateOrCancel), "IOC"); + assert_eq!(format!("{}", TimeInForce::FillOrKill), "FOK"); + } + + #[test] + fn test_time_in_force_default() { + assert_eq!(TimeInForce::default(), TimeInForce::Day); + } + + // ============================================================================= + // OrderType Tests + // ============================================================================= + + #[test] + fn test_order_type_display() { + assert_eq!(format!("{}", OrderType::Market), "MARKET"); + assert_eq!(format!("{}", OrderType::Limit), "LIMIT"); + assert_eq!(format!("{}", OrderType::Stop), "STOP"); + assert_eq!(format!("{}", OrderType::StopLimit), "STOP_LIMIT"); + } + + #[test] + fn test_order_type_try_from_i32_valid() { + assert_eq!(OrderType::try_from(0).unwrap(), OrderType::Market); + assert_eq!(OrderType::try_from(1).unwrap(), OrderType::Limit); + assert_eq!(OrderType::try_from(2).unwrap(), OrderType::Stop); + } + + #[test] + fn test_order_type_try_from_i32_invalid() { + let result = OrderType::try_from(99); + assert!(result.is_err()); + } + + #[test] + fn test_order_type_default() { + assert_eq!(OrderType::default(), OrderType::Market); + } + + // ============================================================================= + // OrderStatus Tests + // ============================================================================= + + #[test] + fn test_order_status_display() { + assert_eq!(format!("{}", OrderStatus::Created), "CREATED"); + assert_eq!(format!("{}", OrderStatus::Filled), "FILLED"); + assert_eq!(format!("{}", OrderStatus::Cancelled), "CANCELLED"); + } + + #[test] + fn test_order_status_try_from_i32_valid() { + assert_eq!(OrderStatus::try_from(0).unwrap(), OrderStatus::Created); + assert_eq!(OrderStatus::try_from(3).unwrap(), OrderStatus::Filled); + assert_eq!(OrderStatus::try_from(5).unwrap(), OrderStatus::Cancelled); + } + + #[test] + fn test_order_status_try_from_i32_invalid() { + let result = OrderStatus::try_from(99); + assert!(result.is_err()); + } + + // ============================================================================= + // OrderSide Tests + // ============================================================================= + + #[test] + fn test_order_side_display() { + assert_eq!(format!("{}", OrderSide::Buy), "BUY"); + assert_eq!(format!("{}", OrderSide::Sell), "SELL"); + } + + #[test] + fn test_order_side_try_from_i32_valid() { + assert_eq!(OrderSide::try_from(0).unwrap(), OrderSide::Buy); + assert_eq!(OrderSide::try_from(1).unwrap(), OrderSide::Sell); + } + + #[test] + fn test_order_side_try_from_i32_invalid() { + let result = OrderSide::try_from(99); + assert!(result.is_err()); + } + + #[test] + fn test_order_side_default() { + assert_eq!(OrderSide::default(), OrderSide::Buy); + } + + // ============================================================================= + // Currency Tests + // ============================================================================= + + #[test] + fn test_currency_display() { + assert_eq!(format!("{}", Currency::USD), "USD"); + assert_eq!(format!("{}", Currency::EUR), "EUR"); + assert_eq!(format!("{}", Currency::BTC), "BTC"); + } + + #[test] + fn test_currency_default() { + assert_eq!(Currency::default(), Currency::USD); + } + + // ============================================================================= + // Error Type Tests + // ============================================================================= + + #[test] + fn test_common_type_error_invalid_price() { + let error = CommonTypeError::InvalidPrice { + value: "abc".to_owned(), + reason: "not a number".to_owned(), + }; + let display = format!("{}", error); + assert!(display.contains("abc")); + } + + #[test] + fn test_common_type_error_invalid_quantity() { + let error = CommonTypeError::InvalidQuantity { + value: "xyz".to_owned(), + reason: "not a number".to_owned(), + }; + let display = format!("{}", error); + assert!(display.contains("xyz")); + } + + #[test] + fn test_common_type_error_validation() { + let error = CommonTypeError::ValidationError { + field: "symbol".to_owned(), + reason: "cannot be empty".to_owned(), + }; + let display = format!("{}", error); + assert!(display.contains("symbol")); + } +} diff --git a/crates/common/src/types/service.rs b/crates/common/src/types/service.rs new file mode 100644 index 000000000..6e1f9022c --- /dev/null +++ b/crates/common/src/types/service.rs @@ -0,0 +1,165 @@ +//! Service infrastructure types +//! +//! Types for service identification, status, and configuration versioning. + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::fmt; +use uuid::Uuid; + +// ============================================================================= +// Core Data Types +// ============================================================================= + +/// Unique identifier for services +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ServiceId(pub String); + +impl ServiceId { + /// Create a new service ID + pub fn new>(id: S) -> Self { + Self(id.into()) + } + + /// Get the inner string value + /// + /// Get the execution ID as a string slice + /// + /// Get execution ID as string slice + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for ServiceId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From<&str> for ServiceId { + fn from(s: &str) -> Self { + Self(s.to_owned()) + } +} + +impl From for ServiceId { + fn from(s: String) -> Self { + Self(s) + } +} + +/// Service status enumeration +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ServiceStatus { + /// Service is starting up + Starting, + /// Service is running normally + Running, + /// Service is degraded but functional + Degraded, + /// Service is stopping + Stopping, + /// Service is stopped + Stopped, + /// Service has encountered an error + Error, + /// Service is in maintenance mode + Maintenance, +} + +impl fmt::Display for ServiceStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Starting => write!(f, "STARTING"), + Self::Running => write!(f, "RUNNING"), + Self::Degraded => write!(f, "DEGRADED"), + Self::Stopping => write!(f, "STOPPING"), + Self::Stopped => write!(f, "STOPPED"), + Self::Error => write!(f, "ERROR"), + Self::Maintenance => write!(f, "MAINTENANCE"), + } + } +} + +impl ServiceStatus { + /// Check if the service is healthy + pub const fn is_healthy(&self) -> bool { + matches!(self, Self::Running | Self::Starting) + } + + /// Check if the service is available for requests + pub const fn is_available(&self) -> bool { + matches!(self, Self::Running | Self::Degraded) + } +} + +/// Configuration version for tracking changes +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ConfigVersion { + /// Version number + pub version: u64, + /// Timestamp when version was created + pub timestamp: DateTime, + /// Optional description of changes + pub description: Option, +} + +impl ConfigVersion { + /// Create a new config version + pub fn new(version: u64) -> Self { + Self { + version, + timestamp: Utc::now(), + description: None, + } + } + + /// Create a new config version with description + pub fn with_description>(version: u64, description: S) -> Self { + Self { + version, + timestamp: Utc::now(), + description: Some(description.into()), + } + } +} + +// TECHNICAL DEBT ELIMINATED - Use DateTime directly instead of Timestamp alias + +/// Timestamp type alias for consistency across the system +pub type Timestamp = DateTime; + +/// Request ID for tracing and correlation +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct RequestId(pub Uuid); + +impl Default for RequestId { + /// Create a default request ID with a new UUID + fn default() -> Self { + Self::new() + } +} + +impl RequestId { + /// Generate a new random request ID + pub fn new() -> Self { + Self(Uuid::new_v4()) + } + + /// Create from UUID + pub const fn from_uuid(uuid: Uuid) -> Self { + Self(uuid) + } + + /// Get the inner UUID + pub const fn as_uuid(&self) -> Uuid { + self.0 + } +} + +impl fmt::Display for RequestId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/crates/common/src/types/trading_enums.rs b/crates/common/src/types/trading_enums.rs new file mode 100644 index 000000000..0901ba98f --- /dev/null +++ b/crates/common/src/types/trading_enums.rs @@ -0,0 +1,295 @@ +//! Trading enumeration types +//! +//! Canonical definitions for order types, broker types, order status, +//! order side, currency, and time-in-force. + +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; +use std::fmt; + +// ============================================================================= +// ORDER TYPES (Moved from trading_engine) +// ============================================================================= + +/// Order type specifying execution behavior - CANONICAL DEFINITION +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[non_exhaustive] +pub enum OrderType { + /// Market order - executes immediately at current market price + Market, + /// Limit order - executes only at specified price or better + Limit, + /// Stop order - becomes market order when stop price is reached + Stop, + /// Stop-limit order - becomes limit order when stop price is reached + StopLimit, + /// Iceberg order - large order split into smaller visible portions + Iceberg, + /// Trailing stop order - stop price adjusts with favorable price movement + TrailingStop, + /// Hidden order - not displayed in order book + Hidden, +} + +impl fmt::Display for OrderType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Market => write!(f, "MARKET"), + Self::Limit => write!(f, "LIMIT"), + Self::Stop => write!(f, "STOP"), + Self::StopLimit => write!(f, "STOP_LIMIT"), + Self::Iceberg => write!(f, "ICEBERG"), + Self::TrailingStop => write!(f, "TRAILING_STOP"), + Self::Hidden => write!(f, "HIDDEN"), + } + } +} + +impl Default for OrderType { + /// Returns the default order type (Market) + fn default() -> Self { + Self::Market + } +} + +impl TryFrom for OrderType { + type Error = String; + + fn try_from(value: i32) -> Result { + match value { + 0 => Ok(OrderType::Market), + 1 => Ok(OrderType::Limit), + 2 => Ok(OrderType::Stop), + 3 => Ok(OrderType::StopLimit), + 4 => Ok(OrderType::Iceberg), + 5 => Ok(OrderType::TrailingStop), + 6 => Ok(OrderType::Hidden), + _ => Err(format!("Invalid OrderType: {}", value)), + } + } +} + +/// Supported broker types - CANONICAL DEFINITION +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum BrokerType { + /// Interactive Brokers TWS/API + InteractiveBrokers, + /// IC Markets FIX API + ICMarkets, + /// Paper trading simulation + PaperTrading, + /// Demo/Test broker + Demo, +} + +impl Default for BrokerType { + /// Returns the default broker type (`InteractiveBrokers`) + fn default() -> Self { + Self::InteractiveBrokers + } +} + +/// Order status throughout its lifecycle - CANONICAL DEFINITION +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[non_exhaustive] +pub enum OrderStatus { + /// Order has been created but not yet submitted to broker + Created, + /// Order has been submitted to broker for execution + Submitted, + /// Order has been partially executed with remaining quantity + PartiallyFilled, + /// Order has been completely executed + Filled, + /// Order was rejected by broker or exchange + Rejected, + /// Order was cancelled by user or system + Cancelled, + /// New order accepted by broker + New, + /// Order expired due to time restrictions + Expired, + /// Order is pending broker acceptance + Pending, + /// Order is actively working in the market + Working, + /// Order status is unknown or not yet determined + Unknown, + /// Order is temporarily suspended + Suspended, + /// Order cancellation is pending + PendingCancel, + /// Order modification is pending + PendingReplace, +} +impl fmt::Display for OrderStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Created => write!(f, "CREATED"), + Self::Submitted => write!(f, "SUBMITTED"), + Self::PartiallyFilled => write!(f, "PARTIALLY_FILLED"), + Self::Filled => write!(f, "FILLED"), + Self::Rejected => write!(f, "REJECTED"), + Self::Cancelled => write!(f, "CANCELLED"), + Self::New => write!(f, "NEW"), + Self::Expired => write!(f, "EXPIRED"), + Self::Pending => write!(f, "PENDING"), + Self::Working => write!(f, "WORKING"), + Self::Unknown => write!(f, "UNKNOWN"), + Self::Suspended => write!(f, "SUSPENDED"), + Self::PendingCancel => write!(f, "PENDING_CANCEL"), + Self::PendingReplace => write!(f, "PENDING_REPLACE"), + } + } +} + +impl Default for OrderStatus { + /// Returns the default order status (Created) + fn default() -> Self { + Self::Created + } +} + +impl TryFrom for OrderStatus { + type Error = String; + + fn try_from(value: i32) -> Result { + match value { + 0 => Ok(OrderStatus::Created), + 1 => Ok(OrderStatus::Submitted), + 2 => Ok(OrderStatus::PartiallyFilled), + 3 => Ok(OrderStatus::Filled), + 4 => Ok(OrderStatus::Rejected), + 5 => Ok(OrderStatus::Cancelled), + 6 => Ok(OrderStatus::New), + 7 => Ok(OrderStatus::Expired), + 8 => Ok(OrderStatus::Pending), + 9 => Ok(OrderStatus::Working), + 10 => Ok(OrderStatus::Unknown), + 11 => Ok(OrderStatus::Suspended), + 12 => Ok(OrderStatus::PendingCancel), + 13 => Ok(OrderStatus::PendingReplace), + _ => Err(format!("Invalid OrderStatus: {}", value)), + } + } +} + +/// Order side - whether the order is a buy or sell - CANONICAL DEFINITION +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum OrderSide { + /// Buy order - purchasing securities + Buy, + /// Sell order - selling securities + Sell, +} + +impl fmt::Display for OrderSide { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Buy => write!(f, "BUY"), + Self::Sell => write!(f, "SELL"), + } + } +} + +impl Default for OrderSide { + /// Returns the default order side (Buy) + fn default() -> Self { + Self::Buy + } +} + +impl TryFrom for OrderSide { + type Error = String; + + fn try_from(value: i32) -> Result { + match value { + 0 => Ok(OrderSide::Buy), + 1 => Ok(OrderSide::Sell), + _ => Err(format!("Invalid OrderSide: {}", value)), + } + } +} + +// REMOVED: Side alias - use OrderSide directly + +/// Currency enumeration - CANONICAL DEFINITION +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[cfg_attr(feature = "database", derive(sqlx::Type))] +pub enum Currency { + /// US Dollar + USD, + /// Euro + EUR, + /// British Pound Sterling + GBP, + /// Japanese Yen + JPY, + /// Swiss Franc + CHF, + /// Canadian Dollar + CAD, + /// Australian Dollar + AUD, + /// New Zealand Dollar + NZD, + /// Bitcoin + BTC, + /// Ethereum + ETH, +} + +impl fmt::Display for Currency { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::USD => write!(f, "USD"), + Self::EUR => write!(f, "EUR"), + Self::GBP => write!(f, "GBP"), + Self::JPY => write!(f, "JPY"), + Self::CHF => write!(f, "CHF"), + Self::CAD => write!(f, "CAD"), + Self::AUD => write!(f, "AUD"), + Self::NZD => write!(f, "NZD"), + Self::BTC => write!(f, "BTC"), + Self::ETH => write!(f, "ETH"), + } + } +} + +impl Default for Currency { + /// Returns the default currency (USD) + fn default() -> Self { + Self::USD + } +} + +/// Time in force enumeration - CANONICAL DEFINITION +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum TimeInForce { + /// Order is valid for the current trading day only + Day, + /// Order remains active until explicitly cancelled + GoodTillCancel, + /// Order must be executed immediately or cancelled + ImmediateOrCancel, + /// Order must be executed completely or cancelled + FillOrKill, +} + +impl fmt::Display for TimeInForce { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Day => write!(f, "DAY"), + Self::GoodTillCancel => write!(f, "GTC"), + Self::ImmediateOrCancel => write!(f, "IOC"), + Self::FillOrKill => write!(f, "FOK"), + } + } +} + +impl Default for TimeInForce { + /// Returns the default time in force (Day) + fn default() -> Self { + Self::Day + } +} diff --git a/crates/common/src/types/type_error.rs b/crates/common/src/types/type_error.rs new file mode 100644 index 000000000..466ae76da --- /dev/null +++ b/crates/common/src/types/type_error.rs @@ -0,0 +1,283 @@ +//! Common error types for trading operations +//! +//! Error types that implement Send + Sync for use in async contexts. + +use serde::{Deserialize, Serialize}; + +// ============================================================================= +// TRADING TYPES (Migrated from foxhunt-common-types) +// ============================================================================= + +/// Common error types for trading operations +/// +/// This error type implements Send + Sync for use in async contexts +#[derive(thiserror::Error, Debug)] +pub enum CommonTypeError { + /// Invalid price value + #[error("Invalid price: {value} - {reason}")] + InvalidPrice { + /// The invalid price value as string + value: String, + /// Reason why the price is invalid + reason: String, + }, + + /// Invalid quantity value + #[error("Invalid quantity: {value} - {reason}")] + InvalidQuantity { + /// The invalid quantity value as string + value: String, + /// Reason why the quantity is invalid + reason: String, + }, + + /// Invalid identifier + #[error("Invalid {field}: {reason}")] + InvalidIdentifier { + /// The field name that contains the invalid identifier + field: String, + /// Reason why the identifier is invalid + reason: String, + }, + + /// Validation error + #[error("Validation error for {field}: {reason}")] + ValidationError { + /// The field name that failed validation + field: String, + /// Reason why the validation failed + reason: String, + }, + + /// Conversion error + #[error("Conversion error: {message}")] + ConversionError { + /// Detailed error message describing the conversion failure + message: String, + }, + + /// I/O error + #[error("I/O error: {0}")] + IoError(#[from] std::io::Error), + + /// JSON serialization/deserialization error + #[error("JSON error: {0}")] + JsonError(#[from] serde_json::Error), + + /// Float parsing error + #[error("Float parsing error: {0}")] + ParseFloatError(#[from] std::num::ParseFloatError), + + /// Integer parsing error + #[error("Integer parsing error: {0}")] + ParseIntError(#[from] std::num::ParseIntError), +} + +// Manual trait implementations for CommonTypeError +// (Cannot derive Clone, PartialEq, Eq, Serialize due to std::io::Error and serde_json::Error) + +impl Clone for CommonTypeError { + /// Clone the error, converting IO and JSON errors to conversion errors + fn clone(&self) -> Self { + match self { + Self::InvalidPrice { value, reason } => Self::InvalidPrice { + value: value.clone(), + reason: reason.clone(), + }, + Self::InvalidQuantity { value, reason } => Self::InvalidQuantity { + value: value.clone(), + reason: reason.clone(), + }, + Self::InvalidIdentifier { field, reason } => Self::InvalidIdentifier { + field: field.clone(), + reason: reason.clone(), + }, + Self::ValidationError { field, reason } => Self::ValidationError { + field: field.clone(), + reason: reason.clone(), + }, + Self::ConversionError { message } => Self::ConversionError { + message: message.clone(), + }, + // Cannot clone std::io::Error or serde_json::Error, so create new instances + Self::IoError(e) => Self::ConversionError { + message: format!("I/O error: {}", e), + }, + Self::JsonError(e) => Self::ConversionError { + message: format!("JSON error: {}", e), + }, + Self::ParseFloatError(e) => Self::ParseFloatError(e.clone()), + Self::ParseIntError(e) => Self::ParseIntError(e.clone()), + } + } +} +impl PartialEq for CommonTypeError { + /// Compare two errors for equality + fn eq(&self, other: &Self) -> bool { + match (self, other) { + ( + Self::InvalidPrice { + value: v1, + reason: r1, + }, + Self::InvalidPrice { + value: v2, + reason: r2, + }, + ) => v1 == v2 && r1 == r2, + ( + Self::InvalidQuantity { + value: v1, + reason: r1, + }, + Self::InvalidQuantity { + value: v2, + reason: r2, + }, + ) => v1 == v2 && r1 == r2, + ( + Self::InvalidIdentifier { + field: f1, + reason: r1, + }, + Self::InvalidIdentifier { + field: f2, + reason: r2, + }, + ) => f1 == f2 && r1 == r2, + ( + Self::ValidationError { + field: f1, + reason: r1, + }, + Self::ValidationError { + field: f2, + reason: r2, + }, + ) => f1 == f2 && r1 == r2, + (Self::ConversionError { message: m1 }, Self::ConversionError { message: m2 }) => { + m1 == m2 + }, + (Self::ParseFloatError(e1), Self::ParseFloatError(e2)) => e1 == e2, + (Self::ParseIntError(e1), Self::ParseIntError(e2)) => e1 == e2, + // std::io::Error and serde_json::Error don't implement PartialEq, so they're never equal + (Self::IoError(_), Self::IoError(_)) => false, + (Self::JsonError(_), Self::JsonError(_)) => false, + _ => false, + } + } +} + +impl Eq for CommonTypeError {} + +// Note: Display is automatically implemented by thiserror::Error derive +// based on the #[error("...")] attributes on each variant +impl Serialize for CommonTypeError { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + match self { + Self::InvalidPrice { value, reason } => { + let mut state = serializer.serialize_struct("InvalidPrice", 2)?; + state.serialize_field("value", value)?; + state.serialize_field("reason", reason)?; + state.end() + }, + Self::InvalidQuantity { value, reason } => { + let mut state = serializer.serialize_struct("InvalidQuantity", 2)?; + state.serialize_field("value", value)?; + state.serialize_field("reason", reason)?; + state.end() + }, + Self::InvalidIdentifier { field, reason } => { + let mut state = serializer.serialize_struct("InvalidIdentifier", 2)?; + state.serialize_field("field", field)?; + state.serialize_field("reason", reason)?; + state.end() + }, + Self::ValidationError { field, reason } => { + let mut state = serializer.serialize_struct("ValidationError", 2)?; + state.serialize_field("field", field)?; + state.serialize_field("reason", reason)?; + state.end() + }, + Self::ConversionError { message } => { + let mut state = serializer.serialize_struct("ConversionError", 1)?; + state.serialize_field("message", message)?; + state.end() + }, + Self::IoError(e) => { + let mut state = serializer.serialize_struct("IoError", 1)?; + state.serialize_field("message", &format!("I/O error: {}", e))?; + state.end() + }, + Self::JsonError(e) => { + let mut state = serializer.serialize_struct("JsonError", 1)?; + state.serialize_field("message", &format!("JSON error: {}", e))?; + state.end() + }, + Self::ParseFloatError(e) => { + let mut state = serializer.serialize_struct("ParseFloatError", 1)?; + state.serialize_field("message", &format!("Float parsing error: {}", e))?; + state.end() + }, + Self::ParseIntError(e) => { + let mut state = serializer.serialize_struct("ParseIntError", 1)?; + state.serialize_field("message", &format!("Integer parsing error: {}", e))?; + state.end() + }, + } + } +} + +impl<'de> Deserialize<'de> for CommonTypeError { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + // For deserialization, we'll convert everything to ConversionError since + // we can't reconstruct std::io::Error or serde_json::Error from serialized form + use serde::de::{MapAccess, Visitor}; + use std::fmt; + + struct CommonTypeErrorVisitor; + + impl<'de> Visitor<'de> for CommonTypeErrorVisitor { + type Value = CommonTypeError; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a CommonTypeError") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + // For simplicity, deserialize everything as ConversionError + let mut message = String::new(); + while let Some(key) = map.next_key::()? { + let value: serde_json::Value = map.next_value()?; + if key == "message" { + if let Some(msg) = value.as_str() { + message = msg.to_owned(); + } + } else { + message = format!("Deserialized error: {}: {}", key, value); + } + } + if message.is_empty() { + message = "Unknown deserialized error".to_owned(); + } + Ok(CommonTypeError::ConversionError { message }) + } + } + + deserializer.deserialize_struct( + "CommonTypeError", + &["value", "reason", "field", "message"], + CommonTypeErrorVisitor, + ) + } +}